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