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