View Javadoc
1   package org.opentrafficsim.trafficcontrol.trafcod;
2   
3   import java.awt.Container;
4   import java.awt.geom.Point2D;
5   import java.awt.image.BufferedImage;
6   import java.io.BufferedReader;
7   import java.io.IOException;
8   import java.io.InputStreamReader;
9   import java.net.MalformedURLException;
10  import java.net.URL;
11  import java.rmi.RemoteException;
12  import java.util.ArrayList;
13  import java.util.EnumSet;
14  import java.util.HashMap;
15  import java.util.HashSet;
16  import java.util.List;
17  import java.util.Locale;
18  import java.util.Map;
19  import java.util.Set;
20  
21  import javax.imageio.ImageIO;
22  
23  import nl.tudelft.simulation.dsol.SimRuntimeException;
24  import nl.tudelft.simulation.dsol.simulators.DEVSSimulator;
25  import nl.tudelft.simulation.dsol.simulators.SimulatorInterface;
26  import nl.tudelft.simulation.event.EventInterface;
27  import nl.tudelft.simulation.event.EventListenerInterface;
28  import nl.tudelft.simulation.event.EventProducer;
29  import nl.tudelft.simulation.event.EventType;
30  import nl.tudelft.simulation.language.Throw;
31  
32  import org.djunits.unit.TimeUnit;
33  import org.djunits.value.vdouble.scalar.Duration;
34  import org.djunits.value.vdouble.scalar.Time;
35  import org.opentrafficsim.core.dsol.OTSSimTimeDouble;
36  import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
37  import org.opentrafficsim.core.network.Network;
38  import org.opentrafficsim.core.network.NetworkException;
39  import org.opentrafficsim.core.network.OTSNetwork;
40  import org.opentrafficsim.core.object.InvisibleObjectInterface;
41  import org.opentrafficsim.core.object.ObjectInterface;
42  import org.opentrafficsim.road.network.lane.object.sensor.NonDirectionalOccupancySensor;
43  import org.opentrafficsim.road.network.lane.object.sensor.TrafficLightSensor;
44  import org.opentrafficsim.road.network.lane.object.trafficlight.TrafficLight;
45  import org.opentrafficsim.road.network.lane.object.trafficlight.TrafficLightColor;
46  import org.opentrafficsim.trafficcontrol.TrafficControlException;
47  import org.opentrafficsim.trafficcontrol.TrafficController;
48  
49  /**
50   * TrafCOD evaluator. TrafCOD is a language for writing traffic control programs. A TrafCOD program consists of a set of rules
51   * that must be evaluated repeatedly (until no more changes occurr) every time step. The time step size is 0.1 seconds.
52   * <p>
53   * Copyright (c) 2013-2016 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
54   * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
55   * <p>
56   * @version $Revision$, $LastChangedDate$, by $Author$, initial version Oct 5, 2016 <br>
57   * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
58   */
59  public class TrafCOD extends EventProducer implements TrafficController, EventListenerInterface
60  {
61      /** */
62      private static final long serialVersionUID = 20161014L;
63  
64      /** Name of this TrafCod controller. */
65      final String controllerName;
66  
67      /** Version of the supported TrafCOD files. */
68      final static int TRAFCOD_VERSION = 100;
69  
70      /** The evaluation interval of TrafCOD. */
71      final static Duration EVALUATION_INTERVAL = new Duration(0.1, TimeUnit.SECOND);
72  
73      /** Text leading up to the TrafCOD version number. */
74      private final static String VERSION_PREFIX = "trafcod-version=";
75  
76      /** Text on line before the sequence line. */
77      private final static String SEQUENCE_KEY = "Sequence";
78  
79      /** Text leading up to the control program structure. */
80      private final static String STRUCTURE_PREFIX = "Structure:";
81  
82      /** The original rules. */
83      final List<String> trafcodRules = new ArrayList<>();
84  
85      /** The tokenized rules. */
86      final List<Object[]> tokenisedRules = new ArrayList<>();
87  
88      /** The TrafCOD variables. */
89      final Map<String, Variable> variables = new HashMap<>();
90  
91      /** The TrafCOD variables in order of definition. */
92      final List<Variable> variablesInDefinitionOrder = new ArrayList<>();
93  
94      /** The detectors. */
95      final Map<String, Variable> detectors = new HashMap<>();
96  
97      /** Comment starter in TrafCOD. */
98      final static String COMMENT_PREFIX = "#";
99  
100     /** Prefix for initialization rules. */
101     private final static String INIT_PREFIX = "%init ";
102 
103     /** Prefix for time initializer rules. */
104     private final static String TIME_PREFIX = "%time ";
105 
106     /** Prefix for export rules. */
107     private final static String EXPORT_PREFIX = "%export ";
108 
109     /** Number of conflict groups in the control program. */
110     int numberOfConflictGroups = -1;
111 
112     /** Sequence information; size of conflict group. */
113     private int conflictGroupSize = -1;
114 
115     /** Chosen structure number (as assigned by VRIGen). */
116     private int structureNumber = -1;
117 
118     /** The conflict groups in order that they will be served. */
119     private List<List<Short>> conflictGroups = new ArrayList<List<Short>>();
120 
121     /** Maximum number of evaluation loops. */
122     private int maxLoopCount = 10;
123 
124     /** Position in current expression. */
125     private int currentToken;
126 
127     /** The expression evaluation stack. */
128     private List<Integer> stack = new ArrayList<Integer>();
129 
130     /** Rule that is currently being evaluated. */
131     private Object[] currentRule;
132 
133     /** The current time in units of 0.1 s */
134     private int currentTime10 = 0;
135 
136     /** The simulation engine. */
137     private final DEVSSimulator<Time, Duration, OTSSimTimeDouble> simulator;
138 
139     /** Space-separated list of the traffic streams in the currently active conflict group. */
140     private String currentConflictGroup = "";
141 
142     /**
143      * Construct a new TrafCOD traffic light controller.
144      * @param controllerName String; name of this TrafCOD traffic light controller
145      * @param trafCodURL URL; the URL of the TrafCOD rules
146      * @param trafficLights Set&lt;TrafficLight&gt;; the traffic lights. The ids of the traffic lights must end with two digits
147      *            that match the stream numbers as used in the traffic control program
148      * @param sensors Set&lt;TrafficLightSensor&gt;; the traffic sensors. The ids of the traffic sensors must end with three
149      *            digits; the first two of those must match the stream and sensor numbers used in the traffic control program
150      * @param simulator DEVSSimulator&lt;Time, Duration, OTSSimTimeDouble&gt;; the simulation engine
151      * @param display Container; if non-null, a controller display is constructed and shown in the supplied container
152      * @throws TrafficControlException when a rule cannot be parsed
153      * @throws SimRuntimeException when scheduling the first evaluation event fails
154      */
155     public TrafCOD(String controllerName, final URL trafCodURL, final Set<TrafficLight> trafficLights,
156             final Set<TrafficLightSensor> sensors, final DEVSSimulator<Time, Duration, OTSSimTimeDouble> simulator,
157             Container display) throws TrafficControlException, SimRuntimeException
158     {
159         this(controllerName, simulator, display);
160         Throw.whenNull(trafCodURL, "trafCodURL may not be null");
161         Throw.whenNull(trafficLights, "trafficLights may not be null");
162         try
163         {
164             parseTrafCODRules(trafCodURL, trafficLights, sensors);
165         }
166         catch (IOException exception)
167         {
168             throw new TrafficControlException(exception);
169         }
170         if (null != display)
171         {
172             String path = trafCodURL.getPath();
173             // System.out.println("path of URL is \"" + path + "\"");
174             if (null == path)
175             {
176                 return; // give up
177             }
178             path = path.replaceFirst("\\.[Tt][Ff][Cc]$", ".tfg");
179             int pos = path.lastIndexOf("/");
180             if (pos > 0)
181             {
182                 path = path.substring(pos + 1);
183             }
184             // System.out.println("fixed last component is \"" + path + "\"");
185             try
186             {
187                 URL mapFileURL = new URL(trafCodURL, path);
188                 // System.out.println("path of mapFileURL is \"" + mapFileURL.getPath() + "\"");
189                 TrafCODDisplay tcd = makeDisplay(mapFileURL, sensors);
190                 if (null != tcd)
191                 {
192                     display.add(tcd);
193                 }
194             }
195             catch (MalformedURLException exception)
196             {
197                 exception.printStackTrace();
198             }
199             // trafCodURL.replaceFirst("\\.[Tt][Ff][Cc]$", ".tfg");
200             // // System.out.println("mapFileURL is \"" + tfgFileURL + "\"");
201         }
202         fireTimedEvent(TrafficController.TRAFFICCONTROL_CONTROLLER_CREATED, new Object[] { this.controllerName,
203                 TrafficController.STARTING_UP }, simulator.getSimulatorTime());
204         // Initialize the variables that have a non-zero initial value
205         for (Variable v : this.variablesInDefinitionOrder)
206         {
207             v.initialize();
208             double value = v.getValue();
209             if (v.isTimer())
210             {
211                 value /= 10.0;
212             }
213             fireTimedEvent(TrafficController.TRAFFICCONTROL_VARIABLE_CREATED, new Object[] { this.controllerName, v.getName(),
214                     v.getStream(), value }, simulator.getSimulatorTime());
215         }
216         // Schedule the consistency check (don't call it directly) to allow interested parties to subscribe before the
217         // consistency check is performed
218         this.simulator.scheduleEventRel(Duration.ZERO, this, this, "checkConsistency", null);
219         // The first rule evaluation should occur at t=0.1s
220         this.simulator.scheduleEventRel(EVALUATION_INTERVAL, this, this, "evalExprs", null);
221     }
222 
223     /**
224      * @param controllerName String; name of this TrafCOD traffic light controller
225      * @param simulator DEVSSimulator&lt;Time, Duration, OTSSimTimeDouble&gt;; the simulation engine
226      * @param display Container; if non-null, a controller display is constructed and shown in the supplied container
227      * @throws TrafficControlException when a rule cannot be parsed
228      * @throws SimRuntimeException when scheduling the first evaluation event fails
229      */
230     private TrafCOD(String controllerName, final DEVSSimulator<Time, Duration, OTSSimTimeDouble> simulator, Container display)
231             throws TrafficControlException, SimRuntimeException
232     {
233         Throw.whenNull(controllerName, "controllerName may not be null");
234         Throw.whenNull(simulator, "simulator may not be null");
235         this.simulator = simulator;
236         this.controllerName = controllerName;
237     }
238 
239     /**
240      * Read and parse the TrafCOD traffic control program.
241      * @param trafCodURL URL; the URL where the TrafCOD file is to be read from
242      * @param trafficLights Set&lt;TrafficLight&gt;; the traffic lights that may be referenced from the TrafCOD file
243      * @param sensors Set&lt;TrafficLightSensor&gt;; the detectors that may be referenced from the TrafCOD file
244      * @throws MalformedURLException when the URL is invalid
245      * @throws IOException when the TrafCOD file could not be read
246      * @throws TrafficControlException when the TrafCOD file contains errors
247      */
248     private void parseTrafCODRules(final URL trafCodURL, final Set<TrafficLight> trafficLights,
249             final Set<TrafficLightSensor> sensors) throws MalformedURLException, IOException, TrafficControlException
250     {
251         BufferedReader in = new BufferedReader(new InputStreamReader(trafCodURL.openStream()));
252         String inputLine;
253         int lineno = 0;
254         while ((inputLine = in.readLine()) != null)
255         {
256             ++lineno;
257             // System.out.println(lineno + ":\t" + inputLine);
258             String trimmedLine = inputLine.trim();
259             if (trimmedLine.length() == 0)
260             {
261                 continue;
262             }
263             String locationDescription = trafCodURL + "(" + lineno + ") ";
264             if (trimmedLine.startsWith(COMMENT_PREFIX))
265             {
266                 String commentStripped = trimmedLine.substring(1).trim();
267                 if (stringBeginsWithIgnoreCase(VERSION_PREFIX, commentStripped))
268                 {
269                     String versionString = commentStripped.substring(VERSION_PREFIX.length());
270                     try
271                     {
272                         int observedVersion = Integer.parseInt(versionString);
273                         if (TRAFCOD_VERSION != observedVersion)
274                         {
275                             throw new TrafficControlException("Wrong TrafCOD version (expected " + TRAFCOD_VERSION + ", got "
276                                     + observedVersion + ")");
277                         }
278                     }
279                     catch (NumberFormatException nfe)
280                     {
281                         nfe.printStackTrace();
282                         throw new TrafficControlException("Could not parse TrafCOD version (got \"" + versionString + ")");
283                     }
284                 }
285                 else if (stringBeginsWithIgnoreCase(SEQUENCE_KEY, commentStripped))
286                 {
287                     while (trimmedLine.startsWith(COMMENT_PREFIX))
288                     {
289                         inputLine = in.readLine();
290                         if (null == inputLine)
291                         {
292                             throw new TrafficControlException("Unexpected EOF (reading sequence key at " + locationDescription
293                                     + ")");
294                         }
295                         ++lineno;
296                         trimmedLine = inputLine.trim();
297                     }
298                     String[] fields = inputLine.split("\t");
299                     if (fields.length != 2)
300                     {
301                         throw new TrafficControlException("Wrong number of fields in Sequence information");
302                     }
303                     try
304                     {
305                         this.numberOfConflictGroups = Integer.parseInt(fields[0]);
306                         this.conflictGroupSize = Integer.parseInt(fields[1]);
307                     }
308                     catch (NumberFormatException nfe)
309                     {
310                         nfe.printStackTrace();
311                         throw new TrafficControlException("Bad number of conflict groups or bad conflict group size");
312                     }
313                 }
314                 else if (stringBeginsWithIgnoreCase(STRUCTURE_PREFIX, commentStripped))
315                 {
316                     String structureNumberString = commentStripped.substring(STRUCTURE_PREFIX.length()).trim();
317                     try
318                     {
319                         this.structureNumber = Integer.parseInt(structureNumberString);
320                     }
321                     catch (NumberFormatException nfe)
322                     {
323                         nfe.printStackTrace();
324                         throw new TrafficControlException("Bad structure number (got \"" + structureNumberString + "\" at "
325                                 + locationDescription + ")");
326                     }
327                     for (int i = 0; i < this.conflictGroupSize; i++)
328                     {
329                         this.conflictGroups.add(new ArrayList<Short>());
330                     }
331                     for (int conflictMemberLine = 0; conflictMemberLine < this.numberOfConflictGroups; conflictMemberLine++)
332                     {
333                         inputLine = in.readLine();
334                         ++lineno;
335                         trimmedLine = inputLine.trim();
336                         while (trimmedLine.startsWith(COMMENT_PREFIX))
337                         {
338                             inputLine = in.readLine();
339                             if (null == inputLine)
340                             {
341                                 throw new TrafficControlException("Unexpected EOF (reading sequence key at "
342                                         + locationDescription + ")");
343                             }
344                             ++lineno;
345                             trimmedLine = inputLine.trim();
346                         }
347                         String[] fields = inputLine.split("\t");
348                         if (fields.length != this.conflictGroupSize)
349                         {
350                             throw new TrafficControlException("Wrong number of conflict groups in Structure information");
351                         }
352                         for (int col = 0; col < this.conflictGroupSize; col++)
353                         {
354                             try
355                             {
356                                 Short stream = Short.parseShort(fields[col]);
357                                 this.conflictGroups.get(col).add(stream);
358                             }
359                             catch (NumberFormatException nfe)
360                             {
361                                 nfe.printStackTrace();
362                                 throw new TrafficControlException("Wrong number of streams in conflict group " + trimmedLine);
363                             }
364                         }
365                     }
366                 }
367                 continue;
368             }
369             if (stringBeginsWithIgnoreCase(INIT_PREFIX, trimmedLine))
370             {
371                 String varNameAndInitialValue = trimmedLine.substring(INIT_PREFIX.length()).trim().replaceAll("[ \t]+", " ");
372                 String[] fields = varNameAndInitialValue.split(" ");
373                 NameAndStream nameAndStream = new NameAndStream(fields[0], locationDescription);
374                 installVariable(nameAndStream.getName(), nameAndStream.getStream(), EnumSet.noneOf(Flags.class),
375                         locationDescription).setFlag(Flags.INITED);
376                 // The supplied initial value is ignored (in this version of the TrafCOD interpreter)!
377                 continue;
378             }
379             if (stringBeginsWithIgnoreCase(TIME_PREFIX, trimmedLine))
380             {
381                 String timerNameAndMaximumValue = trimmedLine.substring(INIT_PREFIX.length()).trim().replaceAll("[ \t]+", " ");
382                 String[] fields = timerNameAndMaximumValue.split(" ");
383                 NameAndStream nameAndStream = new NameAndStream(fields[0], locationDescription);
384                 Variable variable =
385                         installVariable(nameAndStream.getName(), nameAndStream.getStream(), EnumSet.noneOf(Flags.class),
386                                 locationDescription);
387                 int value10 = Integer.parseInt(fields[1]);
388                 variable.setTimerMax(value10);
389                 continue;
390             }
391             if (stringBeginsWithIgnoreCase(EXPORT_PREFIX, trimmedLine))
392             {
393                 String varNameAndOutputValue = trimmedLine.substring(EXPORT_PREFIX.length()).trim().replaceAll("[ \t]+", " ");
394                 String[] fields = varNameAndOutputValue.split(" ");
395                 NameAndStream nameAndStream = new NameAndStream(fields[0], locationDescription);
396                 Variable variable =
397                         installVariable(nameAndStream.getName(), nameAndStream.getStream(), EnumSet.noneOf(Flags.class),
398                                 locationDescription);
399                 int value = Integer.parseInt(fields[1]);
400                 variable.setOutput(value);
401                 int added = 0;
402                 // TODO create the set of traffic lights of this stream only once (not repeat for each possible color)
403                 for (TrafficLight trafficLight : trafficLights)
404                 {
405                     String id = trafficLight.getId();
406                     if (id.length() < 2)
407                     {
408                         throw new TrafficControlException("Id of traffic light " + trafficLight + " does not end on two digits");
409                     }
410                     String streamLetters = id.substring(id.length() - 2);
411                     if (!Character.isDigit(streamLetters.charAt(0)) || !Character.isDigit(streamLetters.charAt(1)))
412                     {
413                         throw new TrafficControlException("Id of traffic light " + trafficLight + " does not end on two digits");
414                     }
415                     int stream = Integer.parseInt(streamLetters);
416                     if (variable.getStream() == stream)
417                     {
418                         variable.addOutput(trafficLight);
419                         added++;
420                     }
421                 }
422                 if (0 == added)
423                 {
424                     throw new TrafficControlException("No traffic light provided that matches stream " + variable.getStream());
425                 }
426                 continue;
427             }
428             this.trafcodRules.add(trimmedLine);
429             Object[] tokenisedRule = parse(trimmedLine, locationDescription);
430             if (null != tokenisedRule)
431             {
432                 this.tokenisedRules.add(tokenisedRule);
433                 // System.out.println(printRule(tokenisedRule, false));
434             }
435         }
436         in.close();
437         for (Variable variable : this.variables.values())
438         {
439             if (variable.isDetector())
440             {
441                 String detectorName = variable.toString(EnumSet.of(PrintFlags.ID));
442                 int detectorNumber = variable.getStream() * 10 + detectorName.charAt(detectorName.length() - 1) - '0';
443                 TrafficLightSensor sensor = null;
444                 for (TrafficLightSensor tls : sensors)
445                 {
446                     if (tls.getId().endsWith(detectorName))
447                     {
448                         sensor = tls;
449                     }
450                 }
451                 if (null == sensor)
452                 {
453                     throw new TrafficControlException("Cannot find detector " + detectorName + " with number " + detectorNumber
454                             + " among the provided sensors");
455                 }
456                 variable.subscribeToDetector(sensor);
457             }
458         }
459         // System.out.println("Installed " + this.variables.size() + " variables");
460         // for (String key : this.variables.keySet())
461         // {
462         // Variable v = this.variables.get(key);
463         // System.out.println(key
464         // + ":\t"
465         // + v.toString(EnumSet.of(PrintFlags.ID, PrintFlags.VALUE, PrintFlags.INITTIMER, PrintFlags.REINITTIMER,
466         // PrintFlags.S, PrintFlags.E)));
467         // }
468     }
469 
470     /**
471      * Check the consistency of the traffic control program.
472      */
473     public void checkConsistency()
474     {
475         for (Variable v : this.variablesInDefinitionOrder)
476         {
477             if (0 == v.refCount && (!v.isOutput()) && (!v.getName().matches("^RA.")))
478             {
479                 // System.out.println("Warning: " + v.getName() + v.getStream() + " is never referenced");
480                 fireTimedEvent(TRAFFICCONTROL_CONTROLLER_WARNING,
481                         new Object[] { this.controllerName, v.toString(EnumSet.of(PrintFlags.ID)) + " is never referenced" },
482                         this.simulator.getSimulatorTime());
483             }
484             if (!v.isDetector())
485             {
486                 if (!v.getFlags().contains(Flags.HAS_START_RULE))
487                 {
488                     // System.out.println("Warning: " + v.getName() + v.getStream() + " has no start rule");
489                     fireTimedEvent(TRAFFICCONTROL_CONTROLLER_WARNING,
490                             new Object[] { this.controllerName, v.toString(EnumSet.of(PrintFlags.ID)) + " has no start rule" },
491                             this.simulator.getSimulatorTime());
492                 }
493                 if ((!v.getFlags().contains(Flags.HAS_END_RULE)) && (!v.isTimer()))
494                 {
495                     // System.out.println("Warning: " + v.getName() + v.getStream() + " has no end rule");
496                     fireTimedEvent(TRAFFICCONTROL_CONTROLLER_WARNING,
497                             new Object[] { this.controllerName, v.toString(EnumSet.of(PrintFlags.ID)) + " has no end rule" },
498                             this.simulator.getSimulatorTime());
499                 }
500             }
501         }
502     }
503 
504     /**
505      * Construct the display of this TrafCOD machine and connect the displayed traffic lights and sensors to this TrafCOD
506      * machine.
507      * @param tfgFileURL String; the URL where the display information is to be read from
508      * @param sensors Set&lt;TrafficLightSensor&gt;; the traffic light sensors
509      * @return TrafCODDisplay, or null when the display information could not be read, was incomplete, or invalid
510      * @throws TrafficControlException when the tfg file could not be read or is invalid
511      */
512     private TrafCODDisplay makeDisplay(final URL tfgFileURL, Set<TrafficLightSensor> sensors) throws TrafficControlException
513     {
514         TrafCODDisplay result = null;
515         boolean useFirstCoordinates = true;
516         try
517         {
518             BufferedReader mapReader = new BufferedReader(new InputStreamReader(tfgFileURL.openStream()));
519             int lineno = 0;
520             String inputLine;
521             while ((inputLine = mapReader.readLine()) != null)
522             {
523                 ++lineno;
524                 inputLine = inputLine.trim();
525                 if (inputLine.startsWith("mapfile="))
526                 {
527                     try
528                     {
529                         URL imageFileURL = new URL(tfgFileURL, inputLine.substring(8));
530                         // System.out.println("path of imageFileURL is \"" + imageFileURL.getPath() + "\"");
531                         BufferedImage image = ImageIO.read(imageFileURL);
532                         result = new TrafCODDisplay(image);
533                         if (inputLine.matches("[Bb][Mm][Pp]|[Pp][Nn][Gg]$"))
534                         {
535                             useFirstCoordinates = false;
536                         }
537                     }
538                     catch (MalformedURLException exception)
539                     {
540                         exception.printStackTrace();
541                     }
542                     // System.out.println("map file description is " + inputLine);
543                     // Make a decent attempt at constructing the URL of the map file
544                 }
545                 else if (inputLine.startsWith("light="))
546                 {
547                     if (null == result)
548                     {
549                         throw new TrafficControlException("tfg file defines light before mapfile");
550                     }
551                     // Extract the stream number
552                     int streamNumber;
553                     try
554                     {
555                         streamNumber = Integer.parseInt(inputLine.substring(6, 8));
556                     }
557                     catch (NumberFormatException nfe)
558                     {
559                         throw new TrafficControlException("Bad traffic light number in tfg file: " + inputLine);
560                     }
561                     // Extract the coordinates and create the image
562                     TrafficLightImage tli =
563                             new TrafficLightImage(result, getCoordinates(inputLine.substring(9), useFirstCoordinates),
564                                     String.format("Traffic Light %02d", streamNumber));
565                     for (Variable v : this.variablesInDefinitionOrder)
566                     {
567                         if (v.isOutput() && v.getStream() == streamNumber)
568                         {
569                             v.addOutput(tli);
570                         }
571                     }
572                 }
573                 else if (inputLine.startsWith("detector="))
574                 {
575                     if (null == result)
576                     {
577                         throw new TrafficControlException("tfg file defines detector before mapfile");
578                     }
579 
580                     int detectorStream;
581                     int detectorSubNumber;
582                     try
583                     {
584                         detectorStream = Integer.parseInt(inputLine.substring(9, 11));
585                         detectorSubNumber = Integer.parseInt(inputLine.substring(12, 13));
586                     }
587                     catch (NumberFormatException nfe)
588                     {
589                         throw new TrafficControlException("Cannot parse detector number in " + inputLine);
590                     }
591                     String detectorName = String.format("D%02d%d", detectorStream, detectorSubNumber);
592                     Variable detectorVariable = this.variables.get(detectorName);
593                     if (null == detectorVariable)
594                     {
595                         throw new TrafficControlException("tfg file defines detector " + detectorName
596                                 + " which does not exist in the TrafCOD program");
597                     }
598                     DetectorImage di =
599                             new DetectorImage(result, getCoordinates(inputLine.substring(14), useFirstCoordinates),
600                                     String.format("Detector %02d%d", detectorStream, detectorSubNumber));
601                     TrafficLightSensor sensor = null;
602                     for (TrafficLightSensor tls : sensors)
603                     {
604                         if (tls.getId().endsWith(detectorName))
605                         {
606                             sensor = tls;
607                         }
608                     }
609                     if (null == sensor)
610                     {
611                         throw new TrafficControlException("Cannot find detector " + detectorName + " with number "
612                                 + detectorName + " among the provided sensors");
613                     }
614                     sensor.addListener(di, NonDirectionalOccupancySensor.NON_DIRECTIONAL_OCCUPANCY_SENSOR_TRIGGER_ENTRY_EVENT);
615                     sensor.addListener(di, NonDirectionalOccupancySensor.NON_DIRECTIONAL_OCCUPANCY_SENSOR_TRIGGER_EXIT_EVENT);
616                 }
617                 else
618                 {
619                     System.out.println("Ignoring tfg line(" + lineno + ") \"" + inputLine + "\"");
620                 }
621             }
622         }
623         catch (IOException e)
624         {
625             throw new TrafficControlException(e);
626         }
627         return result;
628     }
629 
630     /**
631      * Extract two coordinates from a line of text.
632      * @param line String; the text
633      * @param useFirstCoordinates boolean; if true; process the first pair of integer values; if false; use the second pair of
634      *            integer values
635      * @return Point2D
636      * @throws TrafficControlException when the coordinates could not be parsed
637      */
638     private Point2D getCoordinates(final String line, final boolean useFirstCoordinates) throws TrafficControlException
639     {
640         String work = line.replaceAll("[ ,\t]+", "\t").trim();
641         int x;
642         int y;
643         String[] fields = work.split("\t");
644         if (fields.length < (useFirstCoordinates ? 2 : 4))
645         {
646             throw new TrafficControlException("not enough fields in tfg line \"" + line + "\"");
647         }
648         try
649         {
650             x = Integer.parseInt(fields[useFirstCoordinates ? 0 : 2]);
651             y = Integer.parseInt(fields[useFirstCoordinates ? 1 : 3]);
652         }
653         catch (NumberFormatException nfe)
654         {
655             throw new TrafficControlException("Bad value in tfg line \"" + line + "\"");
656         }
657         return new Point2D.Double(x, y);
658     }
659 
660     /**
661      * Decrement all running timers.
662      * @return int; the total number of timers that expired
663      * @throws TrafficControlException Should never happen
664      */
665     private int decrementTimers() throws TrafficControlException
666     {
667         // System.out.println("Decrement running timers");
668         int changeCount = 0;
669         for (Variable v : this.variables.values())
670         {
671             if (v.isTimer() && v.getValue() > 0 && v.decrementTimer(this.currentTime10))
672             {
673                 changeCount++;
674             }
675         }
676         return changeCount;
677     }
678 
679     /**
680      * Reset the START, END and CHANGED flags of all timers. (These do not get reset during the normal rule evaluation phase.)
681      */
682     private void resetTimerFlags()
683     {
684         for (Variable v : this.variablesInDefinitionOrder)
685         {
686             if (v.isTimer())
687             {
688                 v.clearChangedFlag();
689                 v.clearFlag(Flags.START);
690                 v.clearFlag(Flags.END);
691             }
692         }
693     }
694 
695     /**
696      * Evaluate all expressions until no more changes occur.
697      * @throws TrafficControlException when evaluation of a rule fails
698      * @throws SimRuntimeException when scheduling the next evaluation fails
699      */
700     @SuppressWarnings("unused")
701     private void evalExprs() throws TrafficControlException, SimRuntimeException
702     {
703         fireTimedEvent(TrafficController.TRAFFICCONTROL_CONTROLLER_EVALUATING, new Object[] { this.controllerName },
704                 this.simulator.getSimulatorTime());
705         // System.out.println("evalExprs: time is " + EngineeringFormatter.format(this.simulator.getSimulatorTime().get().si));
706         // insert some delay for testing; without this the simulation runs too fast
707         // try
708         // {
709         // Thread.sleep(10);
710         // }
711         // catch (InterruptedException exception)
712         // {
713         // System.out.println("Sleep in evalExprs was interrupted");
714         // // exception.printStackTrace();
715         // }
716         // Contrary to the C++ builder version; this implementation decrements the times at the start of evalExprs
717         // By doing it before updating this.currentTime10; the debugging output should be very similar
718         decrementTimers();
719         this.currentTime10 = (int) (this.simulator.getSimulatorTime().get().si * 10);
720         int loop;
721         for (loop = 0; loop < this.maxLoopCount; loop++)
722         {
723             int changeCount = evalExpressionsOnce();
724             resetTimerFlags();
725             if (changeCount == 0)
726             {
727                 break;
728             }
729         }
730         // System.out.println("Executed " + (loop + 1) + " iteration(s)");
731         if (loop >= this.maxLoopCount)
732         {
733             StringBuffer warningMessage = new StringBuffer();
734             warningMessage.append(String.format(
735                     "Control program did not settle to a final state in %d iterations; oscillating variables:", loop));
736             for (Variable v : this.variablesInDefinitionOrder)
737             {
738                 if (v.getFlags().contains(Flags.CHANGED))
739                 {
740                     warningMessage.append(String.format(" %s%02d", v.getName(), v.getStream()));
741                 }
742             }
743             fireTimedEvent(TrafficController.TRAFFICCONTROL_CONTROLLER_WARNING, new Object[] { this.controllerName,
744                     warningMessage.toString() }, this.simulator.getSimulatorTime());
745         }
746         this.simulator.scheduleEventRel(EVALUATION_INTERVAL, this, this, "evalExprs", null);
747     }
748 
749     /**
750      * Evaluate all expressions and return the number of changed variables.
751      * @return int; the number of changed variables
752      * @throws TrafficControlException when evaluation of a rule fails
753      */
754     private int evalExpressionsOnce() throws TrafficControlException
755     {
756         for (Variable variable : this.variables.values())
757         {
758             variable.clearChangedFlag();
759         }
760         int changeCount = 0;
761         for (Object[] rule : this.tokenisedRules)
762         {
763             if (evalRule(rule))
764             {
765                 changeCount++;
766             }
767         }
768         return changeCount;
769     }
770 
771     /**
772      * Evaluate a rule.
773      * @param rule Object[]; the tokenised rule
774      * @return boolean; true if the variable that is affected by the rule has changed; false if no variable was changed
775      * @throws TrafficControlException when evaluation of the rule fails
776      */
777     private boolean evalRule(final Object[] rule) throws TrafficControlException
778     {
779         boolean result = false;
780         Token ruleType = (Token) rule[0];
781         Variable destination = (Variable) rule[1];
782         if (destination.isTimer())
783         {
784             if (destination.getFlags().contains(Flags.TIMEREXPIRED))
785             {
786                 destination.clearFlag(Flags.TIMEREXPIRED);
787                 destination.setFlag(Flags.END);
788             }
789             else if (destination.getFlags().contains(Flags.START) || destination.getFlags().contains(Flags.END))
790             {
791                 destination.clearFlag(Flags.START);
792                 destination.clearFlag(Flags.END);
793                 destination.setFlag(Flags.CHANGED);
794             }
795         }
796         else
797         {
798             // Normal Variable or detector
799             if (Token.START_RULE == ruleType)
800             {
801                 destination.clearFlag(Flags.START);
802             }
803             else if (Token.END_RULE == ruleType)
804             {
805                 destination.clearFlag(Flags.END);
806             }
807             else
808             {
809                 destination.clearFlag(Flags.START);
810                 destination.clearFlag(Flags.END);
811             }
812         }
813 
814         int currentValue = destination.getValue();
815         if (Token.START_RULE == ruleType && currentValue != 0 || Token.END == ruleType && currentValue == 0
816                 || Token.INIT_TIMER == ruleType && currentValue != 0)
817         {
818             return false; // Value cannot change from zero to nonzero or vice versa due to evaluating the expression
819         }
820         this.currentRule = rule;
821         this.currentToken = 2; // Point to first token of the RHS
822         this.stack.clear();
823         evalExpr(0);
824         if (this.currentToken < this.currentRule.length && Token.CLOSE_PAREN == this.currentRule[this.currentToken])
825         {
826             throw new TrafficControlException("Too many closing parentheses");
827         }
828         int resultValue = pop();
829         if (Token.END_RULE == ruleType)
830         {
831             // Invert the result
832             if (0 == resultValue)
833             {
834                 resultValue = destination.getValue(); // preserve the current value
835             }
836             else
837             {
838                 resultValue = 0;
839             }
840         }
841         if (resultValue != 0 && destination.getValue() == 0)
842         {
843             destination.setFlag(Flags.START);
844         }
845         else if (resultValue == 0 && destination.getValue() != 0)
846         {
847             destination.setFlag(Flags.END);
848         }
849         if (destination.isTimer())
850         {
851             if (resultValue != 0 && Token.END_RULE != ruleType)
852             {
853                 if (0 == destination.getValue())
854                 {
855                     result = true;
856                 }
857                 int timerValue10 = destination.getTimerMax();
858                 if (timerValue10 < 1)
859                 {
860                     // Cheat; ensure it will property expire on the next timer tick
861                     timerValue10 = 1;
862                 }
863                 result = destination.setValue(timerValue10, this.currentTime10, new CausePrinter(rule), this);
864             }
865             else if (0 == resultValue && Token.END_RULE == ruleType && destination.getValue() != 0)
866             {
867                 result = destination.setValue(0, this.currentTime10, new CausePrinter(rule), this);
868             }
869         }
870         else if (destination.getValue() != resultValue)
871         {
872             result = destination.setValue(resultValue, this.currentTime10, new CausePrinter(rule), this);
873             if (destination.isOutput())
874             {
875                 fireEvent(TRAFFIC_LIGHT_CHANGED, new Object[] { this.controllerName, new Integer(destination.getStream()),
876                         destination.getColor() });
877             }
878             if (destination.isConflictGroup() && resultValue != 0)
879             {
880                 int conflictGroupRank = destination.conflictGroupRank();
881                 StringBuilder conflictGroupList = new StringBuilder();
882                 for (Short stream : this.conflictGroups.get(conflictGroupRank))
883                 {
884                     if (conflictGroupList.length() > 0)
885                     {
886                         conflictGroupList.append(" ");
887                     }
888                     conflictGroupList.append(String.format("%02d", stream));
889                 }
890                 fireEvent(TRAFFICCONTROL_CONFLICT_GROUP_CHANGED, new Object[] { this.controllerName, this.currentConflictGroup,
891                         conflictGroupList.toString() });
892                 // System.out.println("Conflict group changed from " + this.currentConflictGroup + " to "
893                 // + conflictGroupList.toString());
894                 this.currentConflictGroup = conflictGroupList.toString();
895             }
896         }
897         return result;
898     }
899 
900     /** Binding strength of relational operators. */
901     private static final int BIND_RELATIONAL_OPERATOR = 1;
902 
903     /** Binding strength of addition and subtraction. */
904     private static final int BIND_ADDITION = 2;
905 
906     /** Binding strength of multiplication and division. */
907     private static final int BIND_MULTIPLY = 3;
908 
909     /** Binding strength of unary minus. */
910     private static int BIND_UNARY_MINUS = 4;
911 
912     /**
913      * Evaluate an expression. <br>
914      * The methods evalExpr and evalRHS together evaluate an expression. This is done using recursion and a stack. The argument
915      * bindingStrength that is passed around is the binding strength of the last preceding pending operator. if a binary
916      * operator with the same or a lower strength is encountered, the pending operator must be applied first. On the other hand
917      * of a binary operator with higher binding strength is encountered, that operator takes precedence over the pending
918      * operator. To evaluate an expression, call evalExpr with a bindingStrength value of 0. On return verify that currentToken
919      * has incremented to the end of the expression and that there is one value (the result) on the stack.
920      * @param bindingStrength int; the binding strength of a not yet applied binary operator (higher value must be applied
921      *            first)
922      * @throws TrafficControlException when the expression is not valid
923      */
924     private void evalExpr(final int bindingStrength) throws TrafficControlException
925     {
926         if (this.currentToken >= this.currentRule.length)
927         {
928             throw new TrafficControlException("Missing operand at end of expression " + printRule(this.currentRule, false));
929         }
930         Token token = (Token) this.currentRule[this.currentToken++];
931         Object nextToken = null;
932         if (this.currentToken < this.currentRule.length)
933         {
934             nextToken = this.currentRule[this.currentToken];
935         }
936         switch (token)
937         {
938             case UNARY_MINUS:
939                 if (Token.OPEN_PAREN != nextToken && Token.VARIABLE != nextToken && Token.NEG_VARIABLE != nextToken
940                         && Token.CONSTANT != nextToken && Token.START != nextToken && Token.END != nextToken)
941                 {
942                     throw new TrafficControlException("Operand expected after unary minus");
943                 }
944                 evalExpr(BIND_UNARY_MINUS);
945                 push(-pop());
946                 break;
947 
948             case OPEN_PAREN:
949                 evalExpr(0);
950                 if (Token.CLOSE_PAREN != this.currentRule[this.currentToken])
951                 {
952                     throw new TrafficControlException("Missing closing parenthesis");
953                 }
954                 this.currentToken++;
955                 break;
956 
957             case START:
958                 if (Token.VARIABLE != nextToken || this.currentToken >= this.currentRule.length - 1)
959                 {
960                     throw new TrafficControlException("Missing variable after S");
961                 }
962                 nextToken = this.currentRule[++this.currentToken];
963                 if (!(nextToken instanceof Variable))
964                 {
965                     throw new TrafficControlException("Missing variable after S");
966                 }
967                 push(((Variable) nextToken).getFlags().contains(Flags.START) ? 1 : 0);
968                 this.currentToken++;
969                 break;
970 
971             case END:
972                 if (Token.VARIABLE != nextToken || this.currentToken >= this.currentRule.length - 1)
973                 {
974                     throw new TrafficControlException("Missing variable after E");
975                 }
976                 nextToken = this.currentRule[++this.currentToken];
977                 if (!(nextToken instanceof Variable))
978                 {
979                     throw new TrafficControlException("Missing variable after E");
980                 }
981                 push(((Variable) nextToken).getFlags().contains(Flags.END) ? 1 : 0);
982                 this.currentToken++;
983                 break;
984 
985             case VARIABLE:
986             {
987                 Variable operand = (Variable) nextToken;
988                 if (operand.isTimer())
989                 {
990                     push(operand.getValue() == 0 ? 0 : 1);
991                 }
992                 else
993                 {
994                     push(operand.getValue());
995                 }
996                 this.currentToken++;
997                 break;
998             }
999 
1000             case CONSTANT:
1001                 push((Integer) nextToken);
1002                 this.currentToken++;
1003                 break;
1004 
1005             case NEG_VARIABLE:
1006                 Variable operand = (Variable) nextToken;
1007                 push(operand.getValue() == 0 ? 1 : 0);
1008                 this.currentToken++;
1009                 break;
1010 
1011             default:
1012                 throw new TrafficControlException("Operand missing");
1013         }
1014         evalRHS(bindingStrength);
1015     }
1016 
1017     /**
1018      * Evaluate the right-hand-side of an expression.
1019      * @param bindingStrength int; the binding strength of the most recent, not yet applied, binary operator
1020      * @throws TrafficControlException when the RHS of an expression is invalid
1021      */
1022     private void evalRHS(final int bindingStrength) throws TrafficControlException
1023     {
1024         while (true)
1025         {
1026             if (this.currentToken >= this.currentRule.length)
1027             {
1028                 return;
1029             }
1030             Token token = (Token) this.currentRule[this.currentToken];
1031             switch (token)
1032             {
1033                 case CLOSE_PAREN:
1034                     return;
1035 
1036                 case TIMES:
1037                     if (BIND_MULTIPLY <= bindingStrength)
1038                     {
1039                         return; // apply pending operator now
1040                     }
1041                     /*-
1042                      * apply pending operator later 
1043                      * 1: evaluate the RHS operand. 
1044                      * 2: multiply the top-most two operands on the stack and push the result on the stack.
1045                      */
1046                     this.currentToken++;
1047                     evalExpr(BIND_MULTIPLY);
1048                     push(pop() * pop() == 0 ? 0 : 1);
1049                     break;
1050 
1051                 case EQ:
1052                 case NOTEQ:
1053                 case LE:
1054                 case LEEQ:
1055                 case GT:
1056                 case GTEQ:
1057                     if (BIND_RELATIONAL_OPERATOR <= bindingStrength)
1058                     {
1059                         return; // apply pending operator now
1060                     }
1061                     /*-
1062                      * apply pending operator later 
1063                      * 1: evaluate the RHS operand. 
1064                      * 2: compare the top-most two operands on the stack and push the result on the stack.
1065                      */
1066                     this.currentToken++;
1067                     evalExpr(BIND_RELATIONAL_OPERATOR);
1068                     switch (token)
1069                     {
1070                         case EQ:
1071                             push(pop() == pop() ? 1 : 0);
1072                             break;
1073 
1074                         case NOTEQ:
1075                             push(pop() != pop() ? 1 : 0);
1076                             break;
1077 
1078                         case GT:
1079                             push(pop() < pop() ? 1 : 0);
1080                             break;
1081 
1082                         case GTEQ:
1083                             push(pop() <= pop() ? 1 : 0);
1084                             break;
1085 
1086                         case LE:
1087                             push(pop() > pop() ? 1 : 0);
1088                             break;
1089 
1090                         case LEEQ:
1091                             push(pop() >= pop() ? 1 : 0);
1092                             break;
1093 
1094                         default:
1095                             throw new TrafficControlException("Bad relational operator");
1096                     }
1097                     break;
1098 
1099                 case PLUS:
1100                     if (BIND_ADDITION <= bindingStrength)
1101                     {
1102                         return; // apply pending operator now
1103                     }
1104                     /*-
1105                      * apply pending operator later 
1106                      * 1: evaluate the RHS operand. 
1107                      * 2: add (OR) the top-most two operands on the stack and push the result on the stack.
1108                      */
1109                     this.currentToken++;
1110                     evalExpr(BIND_ADDITION);
1111                     push(pop() + pop() == 0 ? 0 : 1);
1112                     break;
1113 
1114                 case MINUS:
1115                     if (BIND_ADDITION <= bindingStrength)
1116                     {
1117                         return; // apply pending operator now
1118                     }
1119                     /*-
1120                      * apply pending operator later 
1121                      * 1: evaluate the RHS operand. 
1122                      * 2: subtract the top-most two operands on the stack and push the result on the stack.
1123                      */
1124                     this.currentToken++;
1125                     evalExpr(BIND_ADDITION);
1126                     push(-pop() + pop());
1127                     break;
1128 
1129                 default:
1130                     throw new TrafficControlException("Missing binary operator");
1131             }
1132         }
1133     }
1134 
1135     /**
1136      * Push a value on the evaluation stack.
1137      * @param value int; the value to push on the evaluation stack
1138      */
1139     private void push(final int value)
1140     {
1141         this.stack.add(value);
1142     }
1143 
1144     /**
1145      * Remove the last not-yet-removed value from the evaluation stack and return it.
1146      * @return int; the last non-yet-removed value on the evaluation stack
1147      * @throws TrafficControlException when the stack is empty
1148      */
1149     private int pop() throws TrafficControlException
1150     {
1151         if (this.stack.size() < 1)
1152         {
1153             throw new TrafficControlException("Stack empty");
1154         }
1155         return this.stack.remove(this.stack.size() - 1);
1156     }
1157 
1158     /**
1159      * Print a tokenized rule.
1160      * @param tokens Object[]; the tokens
1161      * @param printValues boolean; if true; print the values of all encountered variable; if false; do not print the values of
1162      *            all encountered variable
1163      * @return String; a textual approximation of the original rule
1164      * @throws TrafficControlException when tokens does not match the expected grammar
1165      */
1166     static String printRule(Object[] tokens, final boolean printValues) throws TrafficControlException
1167     {
1168         EnumSet<PrintFlags> variableFlags = EnumSet.of(PrintFlags.ID);
1169         if (printValues)
1170         {
1171             variableFlags.add(PrintFlags.VALUE);
1172         }
1173         EnumSet<PrintFlags> negatedVariableFlags = EnumSet.copyOf(variableFlags);
1174         negatedVariableFlags.add(PrintFlags.NEGATED);
1175         StringBuilder result = new StringBuilder();
1176         for (int inPos = 0; inPos < tokens.length; inPos++)
1177         {
1178             Object token = tokens[inPos];
1179             if (token instanceof Token)
1180             {
1181                 switch ((Token) token)
1182                 {
1183                     case EQUALS_RULE:
1184                         result.append(((Variable) tokens[++inPos]).toString(variableFlags));
1185                         result.append("=");
1186                         break;
1187 
1188                     case NEG_EQUALS_RULE:
1189                         result.append(((Variable) tokens[++inPos]).toString(negatedVariableFlags));
1190                         result.append("=");
1191                         break;
1192 
1193                     case START_RULE:
1194                         result.append(((Variable) tokens[++inPos]).toString(variableFlags));
1195                         result.append(".=");
1196                         break;
1197 
1198                     case END_RULE:
1199                         result.append(((Variable) tokens[++inPos]).toString(variableFlags));
1200                         result.append("N.=");
1201                         break;
1202 
1203                     case INIT_TIMER:
1204                         result.append(((Variable) tokens[++inPos]).toString(EnumSet.of(PrintFlags.ID, PrintFlags.INITTIMER)));
1205                         result.append(".=");
1206                         break;
1207 
1208                     case REINIT_TIMER:
1209                         result.append(((Variable) tokens[++inPos]).toString(EnumSet.of(PrintFlags.ID, PrintFlags.REINITTIMER)));
1210                         result.append(".=");
1211                         break;
1212 
1213                     case START:
1214                         result.append("S");
1215                         break;
1216 
1217                     case END:
1218                         result.append("E");
1219                         break;
1220 
1221                     case VARIABLE:
1222                         result.append(((Variable) tokens[++inPos]).toString(variableFlags));
1223                         break;
1224 
1225                     case NEG_VARIABLE:
1226                         result.append(((Variable) tokens[++inPos]).toString(variableFlags));
1227                         result.append("N");
1228                         break;
1229 
1230                     case CONSTANT:
1231                         result.append(tokens[++inPos]).toString();
1232                         break;
1233 
1234                     case UNARY_MINUS:
1235                     case MINUS:
1236                         result.append("-");
1237                         break;
1238 
1239                     case PLUS:
1240                         result.append("+");
1241                         break;
1242 
1243                     case TIMES:
1244                         result.append(".");
1245                         break;
1246 
1247                     case EQ:
1248                         result.append("=");
1249                         break;
1250 
1251                     case NOTEQ:
1252                         result.append("<>");
1253                         break;
1254 
1255                     case GT:
1256                         result.append(">");
1257                         break;
1258 
1259                     case GTEQ:
1260                         result.append(">=");
1261                         break;
1262 
1263                     case LE:
1264                         result.append("<");
1265                         break;
1266 
1267                     case LEEQ:
1268                         result.append("<=");
1269                         break;
1270 
1271                     case OPEN_PAREN:
1272                         result.append("(");
1273                         break;
1274 
1275                     case CLOSE_PAREN:
1276                         result.append(")");
1277                         break;
1278 
1279                     default:
1280                         System.out.println("<<<ERROR>>> encountered a non-Token object: " + token + " after "
1281                                 + result.toString());
1282                         throw new TrafficControlException("Unknown token");
1283                 }
1284             }
1285             else
1286             {
1287                 System.out.println("<<<ERROR>>> encountered a non-Token object: " + token + " after " + result.toString());
1288                 throw new TrafficControlException("Not a token");
1289             }
1290         }
1291         return result.toString();
1292     }
1293 
1294     /**
1295      * States of the rule parser.
1296      * <p>
1297      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
1298      */
1299     enum ParserState
1300     {
1301         /** Looking for the left hand side of an assignment. */
1302         FIND_LHS,
1303         /** Looking for an assignment operator. */
1304         FIND_ASSIGN,
1305         /** Looking for the right hand side of an assignment. */
1306         FIND_RHS,
1307         /** Looking for an optional unary minus. */
1308         MAY_UMINUS,
1309         /** Looking for an expression. */
1310         FIND_EXPR,
1311     }
1312 
1313     /**
1314      * Types of TrafCOD tokens.
1315      * <p>
1316      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
1317      */
1318     enum Token
1319     {
1320         /** Equals rule. */
1321         EQUALS_RULE,
1322         /** Not equals rule. */
1323         NEG_EQUALS_RULE,
1324         /** Assignment rule. */
1325         ASSIGNMENT,
1326         /** Start rule. */
1327         START_RULE,
1328         /** End rule. */
1329         END_RULE,
1330         /** Timer initialize rule. */
1331         INIT_TIMER,
1332         /** Timer re-initialize rule. */
1333         REINIT_TIMER,
1334         /** Unary minus operator. */
1335         UNARY_MINUS,
1336         /** Less than or equal to (&lt;=). */
1337         LEEQ,
1338         /** Not equal to (!=). */
1339         NOTEQ,
1340         /** Less than (&lt;). */
1341         LE,
1342         /** Greater than or equal to (&gt;=). */
1343         GTEQ,
1344         /** Greater than (&gt;). */
1345         GT,
1346         /** Equals to (=). */
1347         EQ,
1348         /** True if following variable has just started. */
1349         START,
1350         /** True if following variable has just ended. */
1351         END,
1352         /** Variable follows. */
1353         VARIABLE,
1354         /** Variable that follows must be logically negated. */
1355         NEG_VARIABLE,
1356         /** Integer follows. */
1357         CONSTANT,
1358         /** Addition operator. */
1359         PLUS,
1360         /** Subtraction operator. */
1361         MINUS,
1362         /** Multiplication operator. */
1363         TIMES,
1364         /** Opening parenthesis. */
1365         OPEN_PAREN,
1366         /** Closing parenthesis. */
1367         CLOSE_PAREN,
1368     }
1369 
1370     /**
1371      * Parse one TrafCOD rule.
1372      * @param rawRule String; the TrafCOD rule
1373      * @param locationDescription String; description of the location (file, line) where the rule was found
1374      * @return Object[]; array filled with the tokenized rule
1375      * @throws TrafficControlException when the rule is not a valid TrafCOD rule
1376      */
1377     private Object[] parse(final String rawRule, final String locationDescription) throws TrafficControlException
1378     {
1379         if (rawRule.length() == 0)
1380         {
1381             throw new TrafficControlException("empty rule at " + locationDescription);
1382         }
1383         ParserState state = ParserState.FIND_LHS;
1384         String rule = rawRule.toUpperCase(Locale.US);
1385         Token ruleType = Token.ASSIGNMENT;
1386         int inPos = 0;
1387         NameAndStream lhsNameAndStream = null;
1388         List<Object> tokens = new ArrayList<>();
1389         while (inPos < rule.length())
1390         {
1391             char character = rule.charAt(inPos);
1392             if (Character.isWhitespace(character))
1393             {
1394                 inPos++;
1395                 continue;
1396             }
1397             switch (state)
1398             {
1399                 case FIND_LHS:
1400                 {
1401                     if ('S' == character)
1402                     {
1403                         ruleType = Token.START_RULE;
1404                         inPos++;
1405                         lhsNameAndStream = new NameAndStream(rule.substring(inPos), locationDescription);
1406                         inPos += lhsNameAndStream.getNumberOfChars();
1407                     }
1408                     else if ('E' == character)
1409                     {
1410                         ruleType = Token.END_RULE;
1411                         inPos++;
1412                         lhsNameAndStream = new NameAndStream(rule.substring(inPos), locationDescription);
1413                         inPos += lhsNameAndStream.getNumberOfChars();
1414                     }
1415                     else if ('I' == character && 'T' == rule.charAt(inPos + 1))
1416                     {
1417                         ruleType = Token.INIT_TIMER;
1418                         inPos++; // The 'T' is part of the name of the time; do not consume it
1419                         lhsNameAndStream = new NameAndStream(rule.substring(inPos), locationDescription);
1420                         inPos += lhsNameAndStream.getNumberOfChars();
1421                     }
1422                     else if ('R' == character && 'I' == rule.charAt(inPos + 1) && 'T' == rule.charAt(inPos + 2))
1423                     {
1424                         ruleType = Token.REINIT_TIMER;
1425                         inPos += 2; // The 'T' is part of the name of the timer; do not consume it
1426                         lhsNameAndStream = new NameAndStream(rule.substring(inPos), locationDescription);
1427                         inPos += lhsNameAndStream.getNumberOfChars();
1428                     }
1429                     else if ('T' == character && rule.indexOf('=') >= 0
1430                             && (rule.indexOf('N') < 0 || rule.indexOf('N') > rule.indexOf('=')))
1431                     {
1432                         throw new TrafficControlException("Bad time initialization at " + locationDescription);
1433                     }
1434                     else
1435                     {
1436                         ruleType = Token.EQUALS_RULE;
1437                         lhsNameAndStream = new NameAndStream(rule.substring(inPos), locationDescription);
1438                         inPos += lhsNameAndStream.getNumberOfChars();
1439                         if (lhsNameAndStream.isNegated())
1440                         {
1441                             ruleType = Token.NEG_EQUALS_RULE;
1442                         }
1443                     }
1444                     state = ParserState.FIND_ASSIGN;
1445                     break;
1446                 }
1447 
1448                 case FIND_ASSIGN:
1449                 {
1450                     if ('.' == character && '=' == rule.charAt(inPos + 1))
1451                     {
1452                         if (Token.EQUALS_RULE == ruleType)
1453                         {
1454                             ruleType = Token.START_RULE;
1455                         }
1456                         else if (Token.NEG_EQUALS_RULE == ruleType)
1457                         {
1458                             ruleType = Token.END_RULE;
1459                         }
1460                         inPos += 2;
1461                     }
1462                     else if ('=' == character)
1463                     {
1464                         if (Token.START_RULE == ruleType || Token.END_RULE == ruleType || Token.INIT_TIMER == ruleType
1465                                 || Token.REINIT_TIMER == ruleType)
1466                         {
1467                             throw new TrafficControlException("Bad assignment at " + locationDescription);
1468                         }
1469                         inPos++;
1470                     }
1471                     tokens.add(ruleType);
1472                     EnumSet<Flags> lhsFlags = EnumSet.noneOf(Flags.class);
1473                     if (Token.START_RULE == ruleType || Token.EQUALS_RULE == ruleType || Token.NEG_EQUALS_RULE == ruleType
1474                             || Token.INIT_TIMER == ruleType || Token.REINIT_TIMER == ruleType)
1475                     {
1476                         lhsFlags.add(Flags.HAS_START_RULE);
1477                     }
1478                     if (Token.END_RULE == ruleType || Token.EQUALS_RULE == ruleType || Token.NEG_EQUALS_RULE == ruleType)
1479                     {
1480                         lhsFlags.add(Flags.HAS_END_RULE);
1481                     }
1482                     Variable lhsVariable =
1483                             installVariable(lhsNameAndStream.getName(), lhsNameAndStream.getStream(), lhsFlags,
1484                                     locationDescription);
1485                     tokens.add(lhsVariable);
1486                     state = ParserState.MAY_UMINUS;
1487                     break;
1488                 }
1489 
1490                 case MAY_UMINUS:
1491                     if ('-' == character)
1492                     {
1493                         tokens.add(Token.UNARY_MINUS);
1494                         inPos++;
1495                     }
1496                     state = ParserState.FIND_EXPR;
1497                     break;
1498 
1499                 case FIND_EXPR:
1500                 {
1501                     if (Character.isDigit(character))
1502                     {
1503                         int constValue = 0;
1504                         while (inPos < rule.length() && Character.isDigit(rule.charAt(inPos)))
1505                         {
1506                             int digit = rule.charAt(inPos) - '0';
1507                             if (constValue >= (Integer.MAX_VALUE - digit) / 10)
1508                             {
1509                                 throw new TrafficControlException("Number too large at " + locationDescription);
1510                             }
1511                             constValue = 10 * constValue + digit;
1512                             inPos++;
1513                         }
1514                         tokens.add(Token.CONSTANT);
1515                         tokens.add(new Integer(constValue));
1516                     }
1517                     if (inPos >= rule.length())
1518                     {
1519                         return tokens.toArray();
1520                     }
1521                     character = rule.charAt(inPos);
1522                     switch (character)
1523                     {
1524                         case '+':
1525                             tokens.add(Token.PLUS);
1526                             inPos++;
1527                             break;
1528 
1529                         case '-':
1530                             tokens.add(Token.MINUS);
1531                             inPos++;
1532                             break;
1533 
1534                         case '.':
1535                             tokens.add(Token.TIMES);
1536                             inPos++;
1537                             break;
1538 
1539                         case ')':
1540                             tokens.add(Token.CLOSE_PAREN);
1541                             inPos++;
1542                             break;
1543 
1544                         case '<':
1545                         {
1546                             Character nextChar = rule.charAt(++inPos);
1547                             if ('=' == nextChar)
1548                             {
1549                                 tokens.add(Token.LEEQ);
1550                                 inPos++;
1551                             }
1552                             else if ('>' == nextChar)
1553                             {
1554                                 tokens.add(Token.NOTEQ);
1555                                 inPos++;
1556                             }
1557                             else
1558                             {
1559                                 tokens.add(Token.LE);
1560                             }
1561                             break;
1562                         }
1563 
1564                         case '>':
1565                         {
1566                             Character nextChar = rule.charAt(++inPos);
1567                             if ('=' == nextChar)
1568                             {
1569                                 tokens.add(Token.GTEQ);
1570                                 inPos++;
1571                             }
1572                             else if ('<' == nextChar)
1573                             {
1574                                 tokens.add(Token.NOTEQ);
1575                                 inPos++;
1576                             }
1577                             else
1578                             {
1579                                 tokens.add(Token.GT);
1580                             }
1581                             break;
1582                         }
1583 
1584                         case '=':
1585                         {
1586                             Character nextChar = rule.charAt(++inPos);
1587                             if ('<' == nextChar)
1588                             {
1589                                 tokens.add(Token.LEEQ);
1590                                 inPos++;
1591                             }
1592                             else if ('>' == nextChar)
1593                             {
1594                                 tokens.add(Token.GTEQ);
1595                                 inPos++;
1596                             }
1597                             else
1598                             {
1599                                 tokens.add(Token.EQ);
1600                             }
1601                             break;
1602                         }
1603 
1604                         case '(':
1605                         {
1606                             inPos++;
1607                             tokens.add(Token.OPEN_PAREN);
1608                             state = ParserState.MAY_UMINUS;
1609                             break;
1610                         }
1611 
1612                         default:
1613                         {
1614                             if ('S' == character)
1615                             {
1616                                 tokens.add(Token.START);
1617                                 inPos++;
1618                             }
1619                             else if ('E' == character)
1620                             {
1621                                 tokens.add(Token.END);
1622                                 inPos++;
1623                             }
1624                             NameAndStream nas = new NameAndStream(rule.substring(inPos), locationDescription);
1625                             inPos += nas.getNumberOfChars();
1626                             if (nas.isNegated())
1627                             {
1628                                 tokens.add(Token.NEG_VARIABLE);
1629                             }
1630                             else
1631                             {
1632                                 tokens.add(Token.VARIABLE);
1633                             }
1634                             Variable variable =
1635                                     installVariable(nas.getName(), nas.getStream(), EnumSet.noneOf(Flags.class),
1636                                             locationDescription);
1637                             variable.incrementReferenceCount();
1638                             tokens.add(variable);
1639                         }
1640                     }
1641                     break;
1642                 }
1643                 default:
1644                     throw new TrafficControlException("Error: bad switch; case " + state + " should not happen");
1645             }
1646         }
1647         return tokens.toArray();
1648     }
1649 
1650     /**
1651      * Check if a String begins with the text of a supplied String (ignoring case).
1652      * @param sought String; the sought pattern (NOT a regular expression)
1653      * @param supplied String; the String that might start with the sought string
1654      * @return boolean; true if the supplied String begins with the sought String (case insensitive)
1655      */
1656     private boolean stringBeginsWithIgnoreCase(final String sought, final String supplied)
1657     {
1658         if (sought.length() > supplied.length())
1659         {
1660             return false;
1661         }
1662         return (sought.equalsIgnoreCase(supplied.substring(0, sought.length())));
1663     }
1664 
1665     /**
1666      * Generate the key for a variable name and stream for use in this.variables.
1667      * @param name String; name of the variable
1668      * @param stream short; stream of the variable
1669      * @return String
1670      */
1671     private String variableKey(final String name, final short stream)
1672     {
1673         if (name.startsWith("D"))
1674         {
1675             return String.format("D%02d%s", stream, name.substring(1));
1676         }
1677         return String.format("%s%02d", name.toUpperCase(Locale.US), stream);
1678     }
1679 
1680     /**
1681      * Lookup or create a new Variable.
1682      * @param name String; name of the variable
1683      * @param stream short; stream number of the variable
1684      * @param flags EnumSet&lt;Flags&gt;; some (possibly empty) combination of Flags.HAS_START_RULE and Flags.HAS_END_RULE; no
1685      *            other flags are allowed
1686      * @param location String; description of the location in the TrafCOD file that triggered the call to this method
1687      * @return Variable; the new (or already existing) variable
1688      * @throws TrafficControlException if the variable already exists and already has (one of) the specified flag(s)
1689      */
1690     private Variable installVariable(String name, short stream, EnumSet<Flags> flags, String location)
1691             throws TrafficControlException
1692     {
1693         EnumSet<Flags> forbidden = EnumSet.complementOf(EnumSet.of(Flags.HAS_START_RULE, Flags.HAS_END_RULE));
1694         EnumSet<Flags> badFlags = EnumSet.copyOf(forbidden);
1695         badFlags.retainAll(flags);
1696         if (badFlags.size() > 0)
1697         {
1698             throw new TrafficControlException("installVariable was called with wrong flag(s): " + badFlags);
1699         }
1700         String key = variableKey(name, stream);
1701         Variable variable = this.variables.get(key);
1702         if (null == variable)
1703         {
1704             // Create and install a new variable
1705             variable = new Variable(name, stream, this);
1706             this.variables.put(key, variable);
1707             this.variablesInDefinitionOrder.add(variable);
1708             if (variable.isDetector())
1709             {
1710                 this.detectors.put(key, variable);
1711             }
1712         }
1713         if (flags.contains(Flags.HAS_START_RULE))
1714         {
1715             variable.setStartSource(location);
1716         }
1717         if (flags.contains(Flags.HAS_END_RULE))
1718         {
1719             variable.setEndSource(location);
1720         }
1721         return variable;
1722     }
1723 
1724     /**
1725      * Retrieve the simulator.
1726      * @return SimulatorInterface&lt;Time, Duration, OTSSimTimeDouble&gt;
1727      */
1728     public SimulatorInterface<Time, Duration, OTSSimTimeDouble> getSimulator()
1729     {
1730         return this.simulator;
1731     }
1732 
1733     /**
1734      * Retrieve the structure number.
1735      * @return int; the structureNumber
1736      */
1737     public int getStructureNumber()
1738     {
1739         return this.structureNumber;
1740     }
1741 
1742     /** {@inheritDoc} */
1743     @Override
1744     public void updateDetector(String detectorId, boolean detectingGTU)
1745     {
1746         Variable detector = this.detectors.get(detectorId);
1747         detector.setValue(
1748                 detectingGTU ? 1 : 0,
1749                 this.currentTime10,
1750                 new CausePrinter(String.format("Detector %s becoming %s", detectorId,
1751                         (detectingGTU ? "occupied" : "unoccupied"))), this);
1752     }
1753 
1754     /**
1755      * Switch tracing of all variables of a particular traffic stream, or all variables that do not have an associated traffic
1756      * stream on or off.
1757      * @param stream int; the traffic stream number, or <code>TrafCOD.NO_STREAM</code> to affect all variables that do not have
1758      *            an associated traffic stream
1759      * @param trace boolean; if true; switch on tracing; if false; switch off tracing
1760      */
1761     public void traceVariablesOfStream(final int stream, final boolean trace)
1762     {
1763         for (Variable v : this.variablesInDefinitionOrder)
1764         {
1765             if (v.getStream() == stream)
1766             {
1767                 if (trace)
1768                 {
1769                     v.setFlag(Flags.TRACED);
1770                 }
1771                 else
1772                 {
1773                     v.clearFlag(Flags.TRACED);
1774                 }
1775             }
1776         }
1777     }
1778 
1779     /**
1780      * Switch tracing of one variable on or off.
1781      * @param variableName String; name of the variable
1782      * @param stream int; traffic stream of the variable, or <code>TrafCOD.NO_STREAM</code> to select a variable that does not
1783      *            have an associated traffic stream
1784      * @param trace boolean; if true; switch on tracing; if false; switch off tracing
1785      */
1786     public void traceVariable(final String variableName, final int stream, final boolean trace)
1787     {
1788         for (Variable v : this.variablesInDefinitionOrder)
1789         {
1790             if (v.getStream() == stream && variableName.equals(v.getName()))
1791             {
1792                 if (trace)
1793                 {
1794                     v.setFlag(Flags.TRACED);
1795                 }
1796                 else
1797                 {
1798                     v.clearFlag(Flags.TRACED);
1799                 }
1800             }
1801         }
1802     }
1803 
1804     /** {@inheritDoc} */
1805     @Override
1806     public void notify(EventInterface event) throws RemoteException
1807     {
1808         System.out.println("TrafCOD: received an event");
1809         if (event.getType().equals(TrafficController.TRAFFICCONTROL_SET_TRACING))
1810         {
1811             Object content = event.getContent();
1812             if (!(content instanceof Object[]))
1813             {
1814                 System.err.println("TrafCOD controller " + this.controllerName + " received event with bad payload (" + content
1815                         + ")");
1816                 return;
1817             }
1818             Object[] fields = (Object[]) event.getContent();
1819             if (this.controllerName.equals(fields[0]))
1820             {
1821                 if (fields.length < 4 || !(fields[1] instanceof String) || !(fields[2] instanceof Integer)
1822                         || !(fields[3] instanceof Boolean))
1823                 {
1824                     System.err.println("TrafCOD controller " + this.controllerName + " received event with bad payload ("
1825                             + content + ")");
1826                     return;
1827                 }
1828                 String name = (String) fields[1];
1829                 int stream = (Integer) fields[2];
1830                 boolean trace = (Boolean) fields[3];
1831                 if (name.length() > 1)
1832                 {
1833                     Variable v = this.variables.get(variableKey(name, (short) stream));
1834                     if (null == v)
1835                     {
1836                         System.err.println("Received trace notification for nonexistent variable (name=\"" + name
1837                                 + "\", stream=" + stream + ")");
1838                     }
1839                     if (trace)
1840                     {
1841                         v.setFlag(Flags.TRACED);
1842                     }
1843                     else
1844                     {
1845                         v.clearFlag(Flags.TRACED);
1846                     }
1847                 }
1848                 else
1849                 {
1850                     for (Variable v : this.variablesInDefinitionOrder)
1851                     {
1852                         if (v.getStream() == stream)
1853                         {
1854                             if (trace)
1855                             {
1856                                 v.setFlag(Flags.TRACED);
1857                             }
1858                             else
1859                             {
1860                                 v.clearFlag(Flags.TRACED);
1861                             }
1862                         }
1863                     }
1864                 }
1865             }
1866             // else: event not destined for this controller
1867         }
1868 
1869     }
1870 
1871     /**
1872      * Fire an event on behalf of this TrafCOD engine (used for tracing variable changes).
1873      * @param eventType EventType; the type of the event
1874      * @param payload Object[]; the payload of the event
1875      */
1876     void fireTrafCODEvent(final EventType eventType, final Object[] payload)
1877     {
1878         try
1879         {
1880             fireTimedEvent(eventType, payload, getSimulator().getSimulatorTime());
1881         }
1882         catch (RemoteException exception)
1883         {
1884             exception.printStackTrace();
1885         }
1886     }
1887 
1888     /** {@inheritDoc} */
1889     @Override
1890     public String getId()
1891     {
1892         return this.controllerName;
1893     }
1894 
1895     /** {@inheritDoc} */
1896     @Override
1897     public final InvisibleObjectInterface clone(final OTSSimulatorInterface newSimulator, final Network newNetwork)
1898             throws NetworkException
1899     {
1900         try
1901         {
1902             // TODO figure out how to provide a display for the clone
1903             @SuppressWarnings("unchecked")
1904             TrafCOD result = new TrafCOD(getId(), (DEVSSimulator<Time, Duration, OTSSimTimeDouble>) newSimulator, null);
1905             result.fireTimedEvent(TRAFFICCONTROL_CONTROLLER_CREATED, new Object[] { this.controllerName,
1906                     TrafficController.BEING_CLONED }, newSimulator.getSimulatorTime());
1907             // Clone the variables
1908             for (Variable v : this.variablesInDefinitionOrder)
1909             {
1910                 Variable clonedVariable = result.installVariable(v.getName(), v.getStream(), EnumSet.noneOf(Flags.class), null);
1911                 clonedVariable.setStartSource(v.getStartSource());
1912                 clonedVariable.setEndSource(v.getEndSource());
1913                 if (clonedVariable.isDetector())
1914                 {
1915                     String detectorName = clonedVariable.toString(EnumSet.of(PrintFlags.ID));
1916                     int detectorNumber = clonedVariable.getStream() * 10 + detectorName.charAt(detectorName.length() - 1) - '0';
1917                     TrafficLightSensor clonedSensor = null;
1918                     for (ObjectInterface oi : newNetwork.getObjectMap().values())
1919                     {
1920                         if (oi instanceof TrafficLightSensor)
1921                         {
1922                             TrafficLightSensor tls = (TrafficLightSensor) oi;
1923                             if (tls.getId().endsWith(detectorName))
1924                             {
1925                                 clonedSensor = tls;
1926                             }
1927                         }
1928                     }
1929                     if (null == clonedSensor)
1930                     {
1931                         throw new TrafficControlException("Cannot find detector " + detectorName + " with number "
1932                                 + detectorNumber + " among the provided sensors");
1933                     }
1934                     clonedVariable.subscribeToDetector(clonedSensor);
1935                 }
1936                 clonedVariable.cloneState(v, newNetwork); // also updates traffic lights
1937                 String key = variableKey(clonedVariable.getName(), clonedVariable.getStream());
1938                 result.variables.put(key, clonedVariable);
1939             }
1940             return result;
1941         }
1942         catch (TrafficControlException | SimRuntimeException tce)
1943         {
1944             throw new NetworkException(
1945                     "Internal error; caught an unexpected TrafficControlException or SimRunTimeException in clone");
1946         }
1947     }
1948 
1949 }
1950 
1951 /**
1952  * Store a variable name, stream, isTimer, isNegated and number characters consumed information.
1953  */
1954 class NameAndStream
1955 {
1956     /** The name. */
1957     private final String name;
1958 
1959     /** The stream number. */
1960     private short stream = TrafficController.NO_STREAM;
1961 
1962     /** Number characters parsed. */
1963     private int numberOfChars = 0;
1964 
1965     /** Was a letter N consumed while parsing the name?. */
1966     private boolean negated = false;
1967 
1968     /**
1969      * Parse a TrafCOD identifier and extract all required information.
1970      * @param text String; the TrafCOD identifier (may be followed by more text)
1971      * @param locationDescription String; description of the location in the input file
1972      * @throws TrafficControlException when text is not a valid TrafCOD variable name
1973      */
1974     public NameAndStream(final String text, final String locationDescription) throws TrafficControlException
1975     {
1976         int pos = 0;
1977         while (pos < text.length() && Character.isWhitespace(text.charAt(pos)))
1978         {
1979             pos++;
1980         }
1981         while (pos < text.length())
1982         {
1983             char character = text.charAt(pos);
1984             if (!Character.isLetterOrDigit(character))
1985             {
1986                 break;
1987             }
1988             pos++;
1989         }
1990         this.numberOfChars = pos;
1991         String trimmed = text.substring(0, pos).replaceAll(" ", "");
1992         if (trimmed.length() == 0)
1993         {
1994             throw new TrafficControlException("missing variable at " + locationDescription);
1995         }
1996         if (trimmed.matches("^D([Nn]?\\d\\d\\d)|(\\d\\d\\d[Nn])"))
1997         {
1998             // Handle a detector
1999             if (trimmed.charAt(1) == 'N' || trimmed.charAt(1) == 'n')
2000             {
2001                 // Move the 'N' to the end
2002                 trimmed = "D" + trimmed.substring(2, 5) + "N" + trimmed.substring(5);
2003                 this.negated = true;
2004             }
2005             this.name = "D" + trimmed.charAt(3);
2006             this.stream = (short) (10 * (trimmed.charAt(1) - '0') + trimmed.charAt(2) - '0');
2007             return;
2008         }
2009         StringBuilder nameBuilder = new StringBuilder();
2010         for (pos = 0; pos < trimmed.length(); pos++)
2011         {
2012             char nextChar = trimmed.charAt(pos);
2013             if (pos < trimmed.length() - 1 && Character.isDigit(nextChar) && Character.isDigit(trimmed.charAt(pos + 1))
2014                     && TrafficController.NO_STREAM == this.stream)
2015             {
2016                 if (0 == pos || (1 == pos && trimmed.startsWith("N")))
2017                 {
2018                     throw new TrafficControlException("Bad variable name: " + trimmed + " at " + locationDescription);
2019                 }
2020                 if (trimmed.charAt(pos - 1) == 'N')
2021                 {
2022                     // Previous N was NOT part of the name
2023                     nameBuilder.deleteCharAt(nameBuilder.length() - 1);
2024                     // Move the 'N' after the digits
2025                     trimmed =
2026                             trimmed.substring(0, pos - 1) + trimmed.substring(pos, pos + 2) + trimmed.substring(pos + 2) + "N";
2027                     pos--;
2028                 }
2029                 this.stream = (short) (10 * (trimmed.charAt(pos) - '0') + trimmed.charAt(pos + 1) - '0');
2030                 pos++;
2031             }
2032             else
2033             {
2034                 nameBuilder.append(nextChar);
2035             }
2036         }
2037         if (trimmed.endsWith("N"))
2038         {
2039             nameBuilder.deleteCharAt(nameBuilder.length() - 1);
2040             this.negated = true;
2041         }
2042         this.name = nameBuilder.toString();
2043     }
2044 
2045     /**
2046      * Was a negation operator ('N') embedded in the name?
2047      * @return boolean
2048      */
2049     public boolean isNegated()
2050     {
2051         return this.negated;
2052     }
2053 
2054     /**
2055      * Retrieve the stream number.
2056      * @return short; the stream number
2057      */
2058     public short getStream()
2059     {
2060         return this.stream;
2061     }
2062 
2063     /**
2064      * Retrieve the name.
2065      * @return String; the name (without the stream number)
2066      */
2067     public String getName()
2068     {
2069         return this.name;
2070     }
2071 
2072     /**
2073      * Retrieve the number of characters consumed from the input.
2074      * @return int; the number of characters consumed from the input
2075      */
2076     public int getNumberOfChars()
2077     {
2078         return this.numberOfChars;
2079     }
2080 
2081     /** {@inheritDoc} */
2082     @Override
2083     public String toString()
2084     {
2085         return "NameAndStream [name=" + this.name + ", stream=" + this.stream + ", numberOfChars=" + this.numberOfChars
2086                 + ", negated=" + this.negated + "]";
2087     }
2088 
2089 }
2090 
2091 /**
2092  * A TrafCOD variable, timer, or detector.
2093  */
2094 class Variable implements EventListenerInterface
2095 {
2096     /** The TrafCOD engine. */
2097     private final TrafCOD trafCOD;
2098 
2099     /** Flags. */
2100     EnumSet<Flags> flags = EnumSet.noneOf(Flags.class);
2101 
2102     /** The current value. */
2103     int value;
2104 
2105     /** Limit value (if this is a timer variable). */
2106     int timerMax10;
2107 
2108     /** Output color (if this is an export variable). */
2109     TrafficLightColor color;
2110 
2111     /** Name of this variable (without the traffic stream). */
2112     final String name;
2113 
2114     /** Traffic stream number */
2115     final short stream;
2116 
2117     /** Number of rules that refer to this variable. */
2118     int refCount;
2119 
2120     /** Time of last update in tenth of second. */
2121     int updateTime10;
2122 
2123     /** Source of start rule. */
2124     String startSource;
2125 
2126     /** Source of end rule. */
2127     String endSource;
2128 
2129     /** The traffic light (only set if this Variable is an output(. */
2130     private Set<TrafficLight> trafficLights;
2131 
2132     /** Letters that are used to distinguish conflict groups in the MRx variables. */
2133     private static String ROWLETTERS = "ABCDXYZUVW";
2134 
2135     /**
2136      * @param newNetwork OTSNetwork; the OTS Network in which the clone will exist
2137      * @param newTrafCOD TrafCOD; the TrafCOD engine that will own the new Variable
2138      * @return Variable; the clone of this variable in the new network
2139      * @throws NetworkException when a traffic light or sensor is not present in newNetwork
2140      * @throws TrafficControlException when the output for the cloned traffic light cannot be created
2141      */
2142     final Variable clone(final OTSNetwork newNetwork, final TrafCOD newTrafCOD) throws NetworkException,
2143             TrafficControlException
2144     {
2145         Variable result = new Variable(getName(), getStream(), newTrafCOD);
2146         result.flags = EnumSet.copyOf(this.flags);
2147         result.value = this.value;
2148         result.timerMax10 = this.timerMax10;
2149         result.color = this.color;
2150         result.refCount = this.refCount;
2151         result.updateTime10 = this.updateTime10;
2152         result.startSource = this.startSource;
2153         result.endSource = this.endSource;
2154         for (TrafficLight tl : this.trafficLights)
2155         {
2156             ObjectInterface clonedTrafficLight = newNetwork.getObjectMap().get(tl.getId());
2157             Throw.when(null == clonedTrafficLight, NetworkException.class,
2158                     "Cannot find clone of traffic light %s in newNetwork", tl.getId());
2159             Throw.when(!(clonedTrafficLight instanceof TrafficLight), NetworkException.class,
2160                     "Object %s in newNetwork is not a TrafficLight", clonedTrafficLight);
2161             result.addOutput((TrafficLight) clonedTrafficLight);
2162         }
2163         return result;
2164     }
2165 
2166     /**
2167      * Construct a new Variable.
2168      * @param name String; name of the new variable (without the stream number)
2169      * @param stream short; stream number to which the new Variable is associated
2170      * @param trafCOD TrafCOD; the TrafCOD engine
2171      */
2172     public Variable(final String name, final short stream, TrafCOD trafCOD)
2173     {
2174         this.name = name.toUpperCase(Locale.US);
2175         this.stream = stream;
2176         this.trafCOD = trafCOD;
2177         if (this.name.startsWith("T"))
2178         {
2179             this.flags.add(Flags.IS_TIMER);
2180         }
2181         if (this.name.length() == 2 && this.name.startsWith("D") && Character.isDigit(this.name.charAt(1)))
2182         {
2183             this.flags.add(Flags.IS_DETECTOR);
2184         }
2185         if (TrafficController.NO_STREAM == stream && this.name.startsWith("MR") && this.name.length() == 3
2186                 && ROWLETTERS.indexOf(this.name.charAt(2)) >= 0)
2187         {
2188             this.flags.add(Flags.CONFLICT_GROUP);
2189         }
2190     }
2191 
2192     /**
2193      * Retrieve the name of this variable.
2194      * @return String; the name (without the stream number) of this Variable
2195      */
2196     public String getName()
2197     {
2198         return this.name;
2199     }
2200 
2201     /**
2202      * Link a detector variable to a sensor.
2203      * @param sensor TrafficLightSensor; the sensor
2204      * @throws TrafficControlException when this variable is not a detector
2205      */
2206     public void subscribeToDetector(TrafficLightSensor sensor) throws TrafficControlException
2207     {
2208         if (!isDetector())
2209         {
2210             throw new TrafficControlException("Cannot subscribe a non-detector to a TrafficLightSensor");
2211         }
2212         sensor.addListener(this, NonDirectionalOccupancySensor.NON_DIRECTIONAL_OCCUPANCY_SENSOR_TRIGGER_ENTRY_EVENT);
2213         sensor.addListener(this, NonDirectionalOccupancySensor.NON_DIRECTIONAL_OCCUPANCY_SENSOR_TRIGGER_EXIT_EVENT);
2214     }
2215 
2216     /**
2217      * Initialize this variable if it has the INITED flag set.
2218      */
2219     public void initialize()
2220     {
2221         if (this.flags.contains(Flags.INITED))
2222         {
2223             if (isTimer())
2224             {
2225                 setValue(this.timerMax10, 0, new CausePrinter("Timer initialization rule"), this.trafCOD);
2226             }
2227             else
2228             {
2229                 setValue(1, 0, new CausePrinter("Variable initialization rule"), this.trafCOD);
2230             }
2231         }
2232     }
2233 
2234     /**
2235      * Decrement the value of a timer.
2236      * @param timeStamp10 int; the current simulator time in tenths of a second
2237      * @return boolean; true if the timer expired due to this call; false if the timer is still running, or expired before this
2238      *         call
2239      * @throws TrafficControlException when this Variable is not a timer
2240      */
2241     public boolean decrementTimer(final int timeStamp10) throws TrafficControlException
2242     {
2243         if (!isTimer())
2244         {
2245             throw new TrafficControlException("Variable " + this + " is not a timer");
2246         }
2247         if (this.value <= 0)
2248         {
2249             return false;
2250         }
2251         if (0 == --this.value)
2252         {
2253             this.flags.add(Flags.CHANGED);
2254             this.flags.add(Flags.END);
2255             this.value = 0;
2256             this.updateTime10 = timeStamp10;
2257             if (this.flags.contains(Flags.TRACED))
2258             {
2259                 System.out.println("Timer " + toString() + " expired");
2260             }
2261             return true;
2262         }
2263         return false;
2264     }
2265 
2266     /**
2267      * Retrieve the color for an output Variable.
2268      * @return int; the color code for this Variable
2269      * @throws TrafficControlException if this Variable is not an output
2270      */
2271     public TrafficLightColor getColor() throws TrafficControlException
2272     {
2273         if (!this.flags.contains(Flags.IS_OUTPUT))
2274         {
2275             throw new TrafficControlException("Stream " + this.toString() + "is not an output");
2276         }
2277         return this.color;
2278     }
2279 
2280     /**
2281      * Report whether a change in this variable must be published.
2282      * @return boolean; true if this Variable is an output; false if this Variable is not an output
2283      */
2284     public boolean isOutput()
2285     {
2286         return this.flags.contains(Flags.IS_OUTPUT);
2287     }
2288 
2289     /**
2290      * Report of this Variable identifies the current conflict group.
2291      * @return boolean; true if this Variable identifies the current conflict group; false if it does not.
2292      */
2293     public boolean isConflictGroup()
2294     {
2295         return this.flags.contains(Flags.CONFLICT_GROUP);
2296     }
2297 
2298     /**
2299      * Retrieve the rank of the conflict group that this Variable represents.
2300      * @return int; the rank of the conflict group that this Variable represents
2301      * @throws TrafficControlException if this Variable is not a conflict group identifier
2302      */
2303     public int conflictGroupRank() throws TrafficControlException
2304     {
2305         if (!isConflictGroup())
2306         {
2307             throw new TrafficControlException("Variable " + this + " is not a conflict group identifier");
2308         }
2309         return ROWLETTERS.indexOf(this.name.charAt(2));
2310     }
2311 
2312     /**
2313      * Report if this Variable is a detector.
2314      * @return boolean; true if this Variable is a detector; false if this Variable is not a detector
2315      */
2316     public boolean isDetector()
2317     {
2318         return this.flags.contains(Flags.IS_DETECTOR);
2319     }
2320 
2321     /**
2322      * @param newValue int; the new value of this Variable
2323      * @param timeStamp10 int; the time stamp of this update
2324      * @param cause CausePrinter; rule, timer, or detector that caused the change
2325      * @param trafCOD TrafCOD; the TrafCOD controller
2326      * @return boolean; true if the value of this variable changed
2327      */
2328     public boolean setValue(int newValue, int timeStamp10, CausePrinter cause, TrafCOD trafCOD)
2329     {
2330         boolean result = false;
2331         if (this.value != newValue)
2332         {
2333             this.updateTime10 = timeStamp10;
2334             setFlag(Flags.CHANGED);
2335             if (0 == newValue)
2336             {
2337                 setFlag(Flags.END);
2338                 result = true;
2339             }
2340             else if (!isTimer() || 0 == this.value)
2341             {
2342                 setFlag(Flags.START);
2343                 result = true;
2344             }
2345             if (isOutput() && newValue != 0)
2346             {
2347                 for (TrafficLight trafficLight : this.trafficLights)
2348                 {
2349                     trafficLight.setTrafficLightColor(this.color);
2350                 }
2351             }
2352         }
2353         if (this.flags.contains(Flags.TRACED))
2354         {
2355             // System.out.println("Variable " + this.name + this.stream + " changes from " + this.value + " to " + newValue
2356             // + " due to " + cause.toString());
2357             trafCOD.fireTrafCODEvent(TrafficController.TRAFFICCONTROL_TRACED_VARIABLE_UPDATED, new Object[] { trafCOD.getId(),
2358                     toString(EnumSet.of(PrintFlags.ID)), this.stream, this.value, newValue, cause.toString() });
2359         }
2360         this.value = newValue;
2361         return result;
2362     }
2363 
2364     /**
2365      * Copy the state of this variable from another variable. Only used when cloning the TrafCOD engine.
2366      * @param fromVariable Variable; the variable whose state is copied
2367      * @param newNetwork Network; the Network that contains the new traffic control engine
2368      * @throws NetworkException when the clone of a traffic light of fromVariable does not exist in newNetwork
2369      */
2370     public void cloneState(final Variable fromVariable, final Network newNetwork) throws NetworkException
2371     {
2372         this.value = fromVariable.value;
2373         this.flags = EnumSet.copyOf(fromVariable.flags);
2374         this.updateTime10 = fromVariable.updateTime10;
2375         if (fromVariable.isOutput())
2376         {
2377             for (TrafficLight tl : fromVariable.trafficLights)
2378             {
2379                 ObjectInterface clonedTrafficLight = newNetwork.getObjectMap().get(tl.getId());
2380                 if (null != clonedTrafficLight)
2381                 {
2382                     throw new NetworkException("newNetwork does not contain a clone of traffic light " + tl.getId());
2383                 }
2384                 if (clonedTrafficLight instanceof TrafficLight)
2385                 {
2386                     throw new NetworkException("newNetwork contains an object with name " + tl.getId() + " but this object is not a TrafficLight");
2387                 }
2388                 this.trafficLights.add((TrafficLight) clonedTrafficLight);
2389             }
2390         }
2391         if (isOutput())
2392         {
2393             for (TrafficLight trafficLight : this.trafficLights)
2394             {
2395                 trafficLight.setTrafficLightColor(this.color);
2396             }
2397         }
2398     }
2399 
2400     /**
2401      * Retrieve the start value of this timer in units of 0.1 seconds (1 second is represented by the value 10).
2402      * @return int; the timerMax10 value
2403      * @throws TrafficControlException when this class is not a Timer
2404      */
2405     public int getTimerMax() throws TrafficControlException
2406     {
2407         if (!this.isTimer())
2408         {
2409             throw new TrafficControlException("This is not a timer");
2410         }
2411         return this.timerMax10;
2412     }
2413 
2414     /**
2415      * Retrieve the current value of this Variable.
2416      * @return int; the value of this Variable
2417      */
2418     public int getValue()
2419     {
2420         return this.value;
2421     }
2422 
2423     /**
2424      * Set one flag.
2425      * @param flag Flags
2426      */
2427     public void setFlag(final Flags flag)
2428     {
2429         this.flags.add(flag);
2430     }
2431 
2432     /**
2433      * Clear one flag.
2434      * @param flag Flags; the flag to clear
2435      */
2436     public void clearFlag(final Flags flag)
2437     {
2438         this.flags.remove(flag);
2439     }
2440 
2441     /**
2442      * Report whether this Variable is a timer.
2443      * @return boolean; true if this Variable is a timer; false if this variable is not a timer
2444      */
2445     public boolean isTimer()
2446     {
2447         return this.flags.contains(Flags.IS_TIMER);
2448     }
2449 
2450     /**
2451      * Clear the CHANGED flag of this Variable.
2452      */
2453     public void clearChangedFlag()
2454     {
2455         this.flags.remove(Flags.CHANGED);
2456     }
2457 
2458     /**
2459      * Increment the reference counter of this variable. The reference counter counts the number of rules where this variable
2460      * occurs on the right hand side of the assignment operator.
2461      */
2462     public void incrementReferenceCount()
2463     {
2464         this.refCount++;
2465     }
2466 
2467     /**
2468      * Return a safe copy of the flags.
2469      * @return EnumSet&lt;Flags&gt;
2470      */
2471     public EnumSet<Flags> getFlags()
2472     {
2473         return EnumSet.copyOf(this.flags);
2474     }
2475 
2476     /**
2477      * Make this variable an output variable and set the color value.
2478      * @param colorValue int; the output value (as used in the TrafCOD file)
2479      * @throws TrafficControlException when the colorValue is invalid, or this method is called more than once for this variable
2480      */
2481     public void setOutput(int colorValue) throws TrafficControlException
2482     {
2483         if (null != this.color)
2484         {
2485             throw new TrafficControlException("setOutput has already been called for " + this);
2486         }
2487         if (null == this.trafficLights)
2488         {
2489             this.trafficLights = new HashSet<>();
2490         }
2491         // Convert the TrafCOD color value to the corresponding TrafficLightColor
2492         TrafficLightColor newColor;
2493         switch (colorValue)
2494         {
2495             case 'R':
2496                 newColor = TrafficLightColor.RED;
2497                 break;
2498             case 'G':
2499                 newColor = TrafficLightColor.GREEN;
2500                 break;
2501             case 'Y':
2502                 newColor = TrafficLightColor.YELLOW;
2503                 break;
2504             default:
2505                 throw new TrafficControlException("Bad color value: " + colorValue);
2506         }
2507         this.color = newColor;
2508         this.flags.add(Flags.IS_OUTPUT);
2509     }
2510 
2511     /**
2512      * Add a traffic light to this variable.
2513      * @param trafficLight TrafficLight; the traffic light to add
2514      * @throws TrafficControlException when this variable is not an output
2515      */
2516     public void addOutput(final TrafficLight trafficLight) throws TrafficControlException
2517     {
2518         if (!this.isOutput())
2519         {
2520             throw new TrafficControlException("Cannot add an output to an non-output variable");
2521         }
2522         this.trafficLights.add(trafficLight);
2523     }
2524 
2525     /**
2526      * Set the maximum time of this timer.
2527      * @param value10 int; the maximum time in 0.1 s
2528      * @throws TrafficControlException when this Variable is not a timer
2529      */
2530     public void setTimerMax(int value10) throws TrafficControlException
2531     {
2532         if (!this.flags.contains(Flags.IS_TIMER))
2533         {
2534             throw new TrafficControlException("Cannot set maximum timer value of " + this.toString()
2535                     + " because this is not a timer");
2536         }
2537         this.timerMax10 = value10;
2538     }
2539 
2540     /**
2541      * Describe the rule that starts this variable.
2542      * @return String
2543      */
2544     public String getStartSource()
2545     {
2546         return this.startSource;
2547     }
2548 
2549     /**
2550      * Set the description of the rule that starts this variable.
2551      * @param startSource String; description of the rule that starts this variable
2552      * @throws TrafficControlException when a start source has already been set
2553      */
2554     public void setStartSource(String startSource) throws TrafficControlException
2555     {
2556         if (null != this.startSource)
2557         {
2558             throw new TrafficControlException("Conflicting rules: " + this.startSource + " vs " + startSource);
2559         }
2560         this.startSource = startSource;
2561         this.flags.add(Flags.HAS_START_RULE);
2562     }
2563 
2564     /**
2565      * Describe the rule that ends this variable.
2566      * @return String
2567      */
2568     public String getEndSource()
2569     {
2570         return this.endSource;
2571     }
2572 
2573     /**
2574      * Set the description of the rule that ends this variable.
2575      * @param endSource String; description of the rule that ends this variable
2576      * @throws TrafficControlException when an end source has already been set
2577      */
2578     public void setEndSource(String endSource) throws TrafficControlException
2579     {
2580         if (null != this.endSource)
2581         {
2582             throw new TrafficControlException("Conflicting rules: " + this.startSource + " vs " + endSource);
2583         }
2584         this.endSource = endSource;
2585         this.flags.add(Flags.HAS_END_RULE);
2586     }
2587 
2588     /**
2589      * Retrieve the stream to which this variable belongs.
2590      * @return short; the stream to which this variable belongs
2591      */
2592     public short getStream()
2593     {
2594         return this.stream;
2595     }
2596 
2597     /** {@inheritDoc} */
2598     @Override
2599     public String toString()
2600     {
2601         return "Variable [" + toString(EnumSet.of(PrintFlags.ID, PrintFlags.VALUE, PrintFlags.FLAGS)) + "]";
2602     }
2603 
2604     /**
2605      * Convert selected fields to a String.
2606      * @param printFlags EnumSet&lt;PrintFlags&gt;; the set of fields to convert
2607      * @return String
2608      */
2609     public String toString(EnumSet<PrintFlags> printFlags)
2610     {
2611         StringBuilder result = new StringBuilder();
2612         if (printFlags.contains(PrintFlags.ID))
2613         {
2614             if (this.flags.contains(Flags.IS_DETECTOR))
2615             {
2616                 result.append("D");
2617             }
2618             else if (isTimer() && printFlags.contains(PrintFlags.INITTIMER))
2619             {
2620                 result.append("I");
2621                 result.append(this.name);
2622             }
2623             else if (isTimer() && printFlags.contains(PrintFlags.REINITTIMER))
2624             {
2625                 result.append("RI");
2626                 result.append(this.name);
2627             }
2628             else
2629             {
2630                 result.append(this.name);
2631             }
2632             if (this.stream > 0)
2633             {
2634                 // Insert the stream BEFORE the first digit in the name (if any); otherwise append
2635                 int pos;
2636                 for (pos = 0; pos < result.length(); pos++)
2637                 {
2638                     if (Character.isDigit(result.charAt(pos)))
2639                     {
2640                         break;
2641                     }
2642                 }
2643                 result.insert(pos, String.format("%02d", this.stream));
2644             }
2645             if (this.flags.contains(Flags.IS_DETECTOR))
2646             {
2647                 result.append(this.name.substring(1));
2648             }
2649             if (printFlags.contains(PrintFlags.NEGATED))
2650             {
2651                 result.append("N");
2652             }
2653         }
2654         int printValue = Integer.MIN_VALUE; // That value should stand out if not changed by the code below this line.
2655         if (printFlags.contains(PrintFlags.VALUE))
2656         {
2657             if (printFlags.contains(PrintFlags.NEGATED))
2658             {
2659                 printValue = 0 == this.value ? 1 : 0;
2660             }
2661             else
2662             {
2663                 printValue = this.value;
2664             }
2665             if (printFlags.contains(PrintFlags.S))
2666             {
2667                 if (this.flags.contains(Flags.START))
2668                 {
2669                     printValue = 1;
2670                 }
2671                 else
2672                 {
2673                     printValue = 0;
2674                 }
2675             }
2676             if (printFlags.contains(PrintFlags.E))
2677             {
2678                 if (this.flags.contains(Flags.END))
2679                 {
2680                     printValue = 1;
2681                 }
2682                 else
2683                 {
2684                     printValue = 0;
2685                 }
2686             }
2687         }
2688         if (printFlags.contains(PrintFlags.VALUE) || printFlags.contains(PrintFlags.S) || printFlags.contains(PrintFlags.E)
2689                 || printFlags.contains(PrintFlags.FLAGS))
2690         {
2691             result.append("<");
2692             if (printFlags.contains(PrintFlags.VALUE) || printFlags.contains(PrintFlags.S) || printFlags.contains(PrintFlags.E))
2693             {
2694                 result.append(printValue);
2695             }
2696             if (printFlags.contains(PrintFlags.FLAGS))
2697             {
2698                 if (this.flags.contains(Flags.START))
2699                 {
2700                     result.append("S");
2701                 }
2702                 if (this.flags.contains(Flags.END))
2703                 {
2704                     result.append("E");
2705                 }
2706             }
2707             result.append(">");
2708         }
2709         if (printFlags.contains(PrintFlags.MODIFY_TIME))
2710         {
2711             result.append(String.format(" (%d.%d)", this.updateTime10 / 10, this.updateTime10 % 10));
2712         }
2713         return result.toString();
2714     }
2715 
2716     /** {@inheritDoc} */
2717     @Override
2718     public void notify(EventInterface event) throws RemoteException
2719     {
2720         if (event.getType().equals(NonDirectionalOccupancySensor.NON_DIRECTIONAL_OCCUPANCY_SENSOR_TRIGGER_ENTRY_EVENT))
2721         {
2722             setValue(1, this.updateTime10, new CausePrinter("Detector became occupied"), this.trafCOD);
2723         }
2724         else if (event.getType().equals(NonDirectionalOccupancySensor.NON_DIRECTIONAL_OCCUPANCY_SENSOR_TRIGGER_EXIT_EVENT))
2725         {
2726             setValue(0, this.updateTime10, new CausePrinter("Detector became unoccupied"), this.trafCOD);
2727         }
2728     }
2729 
2730 }
2731 
2732 /**
2733  * Class that can print a text version describing why a variable changed. Any work that has to be done (such as a call to
2734  * <code>TrafCOD.printRule</code>) is deferred until the <code>toString</code> method is called.
2735  */
2736 class CausePrinter
2737 {
2738     /** Object that describes the cause of the variable change. */
2739     final Object cause;
2740 
2741     /**
2742      * Construct a new CausePrinter object.
2743      * @param cause Object; this should be either a String, or a Object[] that contains a tokenized TrafCOD rule.
2744      */
2745     public CausePrinter(final Object cause)
2746     {
2747         this.cause = cause;
2748     }
2749 
2750     public String toString()
2751     {
2752         if (this.cause instanceof String)
2753         {
2754             return (String) this.cause;
2755         }
2756         else if (this.cause instanceof Object[])
2757         {
2758             try
2759             {
2760                 return TrafCOD.printRule((Object[]) this.cause, true);
2761             }
2762             catch (TrafficControlException exception)
2763             {
2764                 exception.printStackTrace();
2765                 return ("printRule failed");
2766             }
2767         }
2768         return this.cause.toString();
2769     }
2770 }
2771 
2772 /**
2773  * Flags for toString method of a Variable.
2774  */
2775 enum PrintFlags
2776 {
2777     /** The name and stream of the Variable. */
2778     ID,
2779     /** The value of the Variable. */
2780     VALUE,
2781     /** Print "I" before the name (indicates that a timer is initialized). */
2782     INITTIMER,
2783     /** Print "RI" before the name (indicates that a timer is re-initialized). */
2784     REINITTIMER,
2785     /** Print value as "1" if just set, else print "0". */
2786     S,
2787     /** Print value as "1" if just reset, else print "0". */
2788     E,
2789     /** Print the negated Variable. */
2790     NEGATED,
2791     /** Print the flags of the Variable. */
2792     FLAGS,
2793     /** Print the time of last modification of the Variable. */
2794     MODIFY_TIME,
2795 }
2796 
2797 /**
2798  * Flags of a TrafCOD variable.
2799  */
2800 enum Flags
2801 {
2802     /** Variable becomes active. */
2803     START,
2804     /** Variable becomes inactive. */
2805     END,
2806     /** Timer has just expired. */
2807     TIMEREXPIRED,
2808     /** Variable has just changed value. */
2809     CHANGED,
2810     /** Variable is a timer. */
2811     IS_TIMER,
2812     /** Variable is a detector. */
2813     IS_DETECTOR,
2814     /** Variable has a start rule. */
2815     HAS_START_RULE,
2816     /** Variable has an end rule. */
2817     HAS_END_RULE,
2818     /** Variable is an output. */
2819     IS_OUTPUT,
2820     /** Variable must be initialized to 1 at start of control program. */
2821     INITED,
2822     /** Variable is traced; all changes must be printed. */
2823     TRACED,
2824     /** Variable identifies the currently active conflict group. */
2825     CONFLICT_GROUP,
2826 }