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