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