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