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