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