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