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