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