TrafCod.java

  1. package org.opentrafficsim.trafficcontrol.trafcod;

  2. import java.awt.Container;
  3. import java.awt.geom.Point2D;
  4. import java.awt.image.BufferedImage;
  5. import java.io.BufferedReader;
  6. import java.io.IOException;
  7. import java.io.InputStreamReader;
  8. import java.net.URL;
  9. import java.rmi.RemoteException;
  10. import java.util.ArrayList;
  11. import java.util.EnumSet;
  12. import java.util.LinkedHashMap;
  13. import java.util.LinkedHashSet;
  14. import java.util.List;
  15. import java.util.Locale;
  16. import java.util.Map;
  17. import java.util.Set;

  18. import javax.swing.JPanel;

  19. import org.djunits.unit.DurationUnit;
  20. import org.djunits.value.vdouble.scalar.Duration;
  21. import org.djutils.event.Event;
  22. import org.djutils.event.EventListener;
  23. import org.djutils.event.EventType;
  24. import org.djutils.exceptions.Throw;
  25. import org.djutils.immutablecollections.ImmutableCollection;
  26. import org.opentrafficsim.core.dsol.OtsModelInterface;
  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.object.LocatedObject;
  31. import org.opentrafficsim.road.network.lane.object.detector.TrafficLightDetector;
  32. import org.opentrafficsim.road.network.lane.object.detector.TrafficLightDetector.StartEndDetector;
  33. import org.opentrafficsim.road.network.lane.object.trafficlight.TrafficLight;
  34. import org.opentrafficsim.road.network.lane.object.trafficlight.TrafficLightColor;
  35. import org.opentrafficsim.trafficcontrol.AbstractTrafficController;
  36. import org.opentrafficsim.trafficcontrol.ActuatedTrafficController;
  37. import org.opentrafficsim.trafficcontrol.TrafficControlException;
  38. import org.opentrafficsim.trafficcontrol.TrafficController;

  39. import nl.tudelft.simulation.dsol.SimRuntimeException;

  40. /**
  41.  * TrafCOD evaluator. TrafCOD is a language for writing traffic control programs. A TrafCOD program consists of a set of rules
  42.  * that must be evaluated repeatedly (until no more changes occurr) every time step. The time step size is 0.1 seconds.
  43.  * <p>
  44.  * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
  45.  * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  46.  * </p>
  47.  * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  48.  */
  49. public class TrafCod extends AbstractTrafficController implements ActuatedTrafficController, EventListener
  50. {
  51.     /** */
  52.     private static final long serialVersionUID = 20161014L;

  53.     /** Version of the supported TrafCOD files. */
  54.     static final int TrafCod_VERSION = 100;

  55.     /** The evaluation interval of TrafCOD. */
  56.     static final Duration EVALUATION_INTERVAL = new Duration(0.1, DurationUnit.SECOND);

  57.     /** Text leading up to the TrafCOD version number. */
  58.     private static final String VERSION_PREFIX = "trafcod-version=";

  59.     /** Text on line before the sequence line. */
  60.     private static final String SEQUENCE_KEY = "Sequence";

  61.     /** Text leading up to the control program structure. */
  62.     private static final String STRUCTURE_PREFIX = "Structure:";

  63.     /** The tokenized rules. */
  64.     private final List<Object[]> tokenisedRules = new ArrayList<>();

  65.     /** The TrafCOD variables. */
  66.     private final Map<String, Variable> variables = new LinkedHashMap<>();

  67.     /** The TrafCOD variables in order of definition. */
  68.     private final List<Variable> variablesInDefinitionOrder = new ArrayList<>();

  69.     /** The detectors. */
  70.     private final Map<String, Variable> detectors = new LinkedHashMap<>();

  71.     /** Comment starter in TrafCOD. */
  72.     static final String COMMENT_PREFIX = "#";

  73.     /** Prefix for initialization rules. */
  74.     private static final String INIT_PREFIX = "%init ";

  75.     /** Prefix for time initializer rules. */
  76.     private static final String TIME_PREFIX = "%time ";

  77.     /** Prefix for export rules. */
  78.     private static final String EXPORT_PREFIX = "%export ";

  79.     /** Number of conflict groups in the control program. */
  80.     private int numberOfConflictGroups = -1;

  81.     /** Sequence information; size of conflict group. */
  82.     private int conflictGroupSize = -1;

  83.     /** Chosen structure number (as assigned by VRIGen). */
  84.     private int structureNumber = -1;

  85.     /** The conflict groups in order that they will be served. */
  86.     private List<List<Short>> conflictGroups = new ArrayList<List<Short>>();

  87.     /** Maximum number of evaluation loops. */
  88.     private int maxLoopCount = 10;

  89.     /** Position in current expression. */
  90.     private int currentToken;

  91.     /** The expression evaluation stack. */
  92.     private List<Integer> stack = new ArrayList<Integer>();

  93.     /** Rule that is currently being evaluated. */
  94.     private Object[] currentRule;

  95.     /** The current time in units of 0.1 s. */
  96.     private int currentTime10 = 0;

  97.     /** The unparsed TrafCOD rules (needed for cloning). */
  98.     private final List<String> trafCODRules;

  99.     /** Container for controller state display. */
  100.     private final Container displayContainer = new JPanel();

  101.     /** Background image for state display. */
  102.     private final BufferedImage displayBackground;

  103.     /** Objects to draw on top of display background. */
  104.     private final List<String> displayObjectLocations;

  105.     /** Animation of the current state of this TrafCOD controller. */
  106.     private TrafCodDisplay stateDisplay = null;

  107.     /** The simulation engine. */
  108.     private final OtsSimulatorInterface simulator;

  109.     /** Space-separated list of the traffic streams in the currently active conflict group. */
  110.     private String currentConflictGroup = "";

  111.     /**
  112.      * Construct a new TrafCOD traffic light controller.
  113.      * @param controllerName String; name of this TrafCOD traffic light controller
  114.      * @param trafCodURL URL; the URL of the TrafCOD rules
  115.      * @param simulator OtsSimulatorInterface; the simulation engine
  116.      * @param display Container; if non-null, a controller display is constructed and shown in the supplied container
  117.      * @param displayBackground BufferedImage; background for controller display image
  118.      * @param displayObjectLocations List&lt;String&gt;; list of sensors and traffic lights and their locations on the
  119.      *            <code>displayBackGround</code>
  120.      * @throws TrafficControlException when a rule cannot be parsed
  121.      * @throws SimRuntimeException when scheduling the first evaluation event fails
  122.      * @throws IOException when loading the TrafCOD rules from the URL fails
  123.      */
  124.     public TrafCod(final String controllerName, final URL trafCodURL, final OtsSimulatorInterface simulator,
  125.             final Container display, final BufferedImage displayBackground, final List<String> displayObjectLocations)
  126.             throws TrafficControlException, SimRuntimeException, IOException
  127.     {
  128.         this(controllerName, loadTextFromURL(trafCodURL), simulator, displayBackground, displayObjectLocations);
  129.     }

  130.     /**
  131.      * Construct a new TrafCOD traffic light controller.
  132.      * @param controllerName String; name of this TrafCOD traffic light controller
  133.      * @param trafCODRules List&lt;String&gt;; the TrafCOD rules
  134.      * @param simulator OtsSimulatorInterface; the simulation engine
  135.      * @param displayBackground BufferedImage; background for controller display image
  136.      * @param displayObjectLocations List&lt;String&gt;; list of sensors and traffic lights and their locations on the
  137.      *            <code>displayBackGround</code>
  138.      * @throws TrafficControlException when a rule cannot be parsed
  139.      * @throws SimRuntimeException when scheduling the first evaluation event fails
  140.      */
  141.     public TrafCod(final String controllerName, final List<String> trafCODRules, final OtsSimulatorInterface simulator,
  142.             final BufferedImage displayBackground, final List<String> displayObjectLocations)
  143.             throws TrafficControlException, SimRuntimeException
  144.     {
  145.         super(controllerName, simulator);
  146.         Throw.whenNull(controllerName, "controllerName may not be null");
  147.         Throw.whenNull(simulator, "simulator may not be null");
  148.         this.simulator = simulator;
  149.         this.displayBackground = displayBackground;
  150.         this.displayObjectLocations = displayObjectLocations;
  151.         Throw.whenNull(trafCODRules, "trafCodRules may not be null");
  152.         this.trafCODRules = trafCODRules;
  153.         parseTrafCODRules();

  154.         // Initialize the variables that have a non-zero initial value
  155.         for (Variable v : this.variablesInDefinitionOrder)
  156.         {
  157.             v.initialize();
  158.             double value = v.getValue();
  159.             if (v.isTimer())
  160.             {
  161.                 value /= 10.0;
  162.             }
  163.             fireTimedEvent(TrafficController.TRAFFICCONTROL_VARIABLE_CREATED,
  164.                     new Object[] {getId(), v.getName(), v.getStream(), value}, simulator.getSimulatorTime());
  165.         }
  166.         if (null != this.displayContainer && null != this.displayBackground && null != this.displayObjectLocations)
  167.         {
  168.             this.stateDisplay = new TrafCodDisplay(this.displayBackground);
  169.             this.displayContainer.add(this.stateDisplay);
  170.             try
  171.             {
  172.                 addTrafCODDisplay(this.displayObjectLocations);
  173.             }
  174.             catch (IOException e)
  175.             {
  176.                 e.printStackTrace();
  177.             }
  178.         }
  179.         // Schedule the consistency check (don't call it directly) to allow interested parties to subscribe before the
  180.         // consistency check is performed
  181.         this.simulator.scheduleEventRel(Duration.ZERO, this, "checkConsistency", null);
  182.         // The first rule evaluation should occur at t=0.1s
  183.         this.simulator.scheduleEventRel(EVALUATION_INTERVAL, this, "evalExprs", null);
  184.     }

  185.     /**
  186.      * Read a text from a URL and convert it to a list of strings.
  187.      * @param url URL; the URL to open and read
  188.      * @return List&lt;String&gt;; the lines read from the URL (trimmed).
  189.      * @throws IOException when opening or reading the URL failed.
  190.      */
  191.     public static List<String> loadTextFromURL(final URL url) throws IOException
  192.     {
  193.         List<String> result = new ArrayList<>();
  194.         BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
  195.         String inputLine;
  196.         while ((inputLine = in.readLine()) != null)
  197.         {
  198.             result.add(inputLine.trim());
  199.         }
  200.         return result;
  201.     }

  202.     /**
  203.      * Read and parse the TrafCOD traffic control program.
  204.      * @throws TrafficControlException when the TrafCOD file contains errors
  205.      */
  206.     private void parseTrafCODRules() throws TrafficControlException
  207.     {
  208.         for (int lineno = 0; lineno < this.trafCODRules.size(); lineno++)
  209.         {
  210.             String trimmedLine = this.trafCODRules.get(lineno);
  211.             // System.out.println(lineno + ":\t" + inputLine);
  212.             if (trimmedLine.length() == 0)
  213.             {
  214.                 continue;
  215.             }
  216.             String locationDescription = "TrafCOD rule" + "(" + lineno + ") ";
  217.             if (trimmedLine.startsWith(COMMENT_PREFIX))
  218.             {
  219.                 String commentStripped = trimmedLine.substring(1).trim();
  220.                 if (stringBeginsWithIgnoreCase(VERSION_PREFIX, commentStripped))
  221.                 {
  222.                     String versionString = commentStripped.substring(VERSION_PREFIX.length());
  223.                     try
  224.                     {
  225.                         int observedVersion = Integer.parseInt(versionString);
  226.                         if (TrafCod_VERSION != observedVersion)
  227.                         {
  228.                             throw new TrafficControlException(
  229.                                     "Wrong TrafCOD version (expected " + TrafCod_VERSION + ", got " + observedVersion + ")");
  230.                         }
  231.                     }
  232.                     catch (NumberFormatException nfe)
  233.                     {
  234.                         nfe.printStackTrace();
  235.                         throw new TrafficControlException("Could not parse TrafCOD version (got \"" + versionString + ")");
  236.                     }
  237.                 }
  238.                 else if (stringBeginsWithIgnoreCase(SEQUENCE_KEY, commentStripped))
  239.                 {
  240.                     while (trimmedLine.startsWith(COMMENT_PREFIX))
  241.                     {
  242.                         if (++lineno >= this.trafCODRules.size())
  243.                         {
  244.                             throw new TrafficControlException(
  245.                                     "Unexpected EOF (reading sequence key at " + locationDescription + ")");
  246.                         }
  247.                         trimmedLine = this.trafCODRules.get(lineno);
  248.                     }
  249.                     String[] fields = trimmedLine.split("\\s");
  250.                     Throw.when(fields.length != 2, TrafficControlException.class,
  251.                             "Wrong number of fields in Sequence information line (%s)", trimmedLine);
  252.                     try
  253.                     {
  254.                         this.numberOfConflictGroups = Integer.parseInt(fields[0]);
  255.                         this.conflictGroupSize = Integer.parseInt(fields[1]);
  256.                     }
  257.                     catch (NumberFormatException nfe)
  258.                     {
  259.                         nfe.printStackTrace();
  260.                         throw new TrafficControlException("Bad number of conflict groups or bad conflict group size");
  261.                     }
  262.                 }
  263.                 else if (stringBeginsWithIgnoreCase(STRUCTURE_PREFIX, commentStripped))
  264.                 {
  265.                     String structureNumberString = commentStripped.substring(STRUCTURE_PREFIX.length()).trim();
  266.                     try
  267.                     {
  268.                         this.structureNumber = Integer.parseInt(structureNumberString);
  269.                     }
  270.                     catch (NumberFormatException nfe)
  271.                     {
  272.                         nfe.printStackTrace();
  273.                         throw new TrafficControlException(
  274.                                 "Bad structure number (got \"" + structureNumberString + "\" at " + locationDescription + ")");
  275.                     }
  276.                     for (int i = 0; i < this.conflictGroupSize; i++)
  277.                     {
  278.                         this.conflictGroups.add(new ArrayList<Short>());
  279.                     }
  280.                     for (int conflictMemberLine = 0; conflictMemberLine < this.numberOfConflictGroups; conflictMemberLine++)
  281.                     {
  282.                         if (++lineno >= this.trafCODRules.size())
  283.                         {
  284.                             throw new TrafficControlException(
  285.                                     "Unexpected EOF (reading conflict groups at " + locationDescription + ")");
  286.                         }
  287.                         trimmedLine = this.trafCODRules.get(lineno);
  288.                         while (trimmedLine.startsWith(COMMENT_PREFIX))
  289.                         {
  290.                             if (++lineno >= this.trafCODRules.size())
  291.                             {
  292.                                 throw new TrafficControlException(
  293.                                         "Unexpected EOF (reading conflict groups at " + locationDescription + ")");
  294.                             }
  295.                             trimmedLine = this.trafCODRules.get(lineno);
  296.                         }
  297.                         String[] fields = trimmedLine.split("\\s+");
  298.                         if (fields.length != this.conflictGroupSize)
  299.                         {
  300.                             throw new TrafficControlException("Wrong number of conflict groups in Structure information");
  301.                         }
  302.                         for (int col = 0; col < this.conflictGroupSize; col++)
  303.                         {
  304.                             try
  305.                             {
  306.                                 Short stream = Short.parseShort(fields[col]);
  307.                                 this.conflictGroups.get(col).add(stream);
  308.                             }
  309.                             catch (NumberFormatException nfe)
  310.                             {
  311.                                 nfe.printStackTrace();
  312.                                 throw new TrafficControlException("Wrong number of streams in conflict group " + trimmedLine);
  313.                             }
  314.                         }
  315.                     }
  316.                 }
  317.                 continue;
  318.             }
  319.             if (stringBeginsWithIgnoreCase(INIT_PREFIX, trimmedLine))
  320.             {
  321.                 String varNameAndInitialValue = trimmedLine.substring(INIT_PREFIX.length()).trim().replaceAll("[ \t]+", " ");
  322.                 String[] fields = varNameAndInitialValue.split(" ");
  323.                 NameAndStream nameAndStream = new NameAndStream(fields[0], locationDescription);
  324.                 installVariable(nameAndStream.getName(), nameAndStream.getStream(), EnumSet.noneOf(Flags.class),
  325.                         locationDescription).setFlag(Flags.INITED);
  326.                 // The supplied initial value is ignored (in this version of the TrafCOD interpreter)!
  327.                 continue;
  328.             }
  329.             if (stringBeginsWithIgnoreCase(TIME_PREFIX, trimmedLine))
  330.             {
  331.                 String timerNameAndMaximumValue = trimmedLine.substring(INIT_PREFIX.length()).trim().replaceAll("[ \t]+", " ");
  332.                 String[] fields = timerNameAndMaximumValue.split(" ");
  333.                 NameAndStream nameAndStream = new NameAndStream(fields[0], locationDescription);
  334.                 Variable variable = installVariable(nameAndStream.getName(), nameAndStream.getStream(),
  335.                         EnumSet.noneOf(Flags.class), locationDescription);
  336.                 int value10 = Integer.parseInt(fields[1]);
  337.                 variable.setTimerMax(value10);
  338.                 continue;
  339.             }
  340.             if (stringBeginsWithIgnoreCase(EXPORT_PREFIX, trimmedLine))
  341.             {
  342.                 String varNameAndOutputValue = trimmedLine.substring(EXPORT_PREFIX.length()).trim().replaceAll("[ \t]+", " ");
  343.                 String[] fields = varNameAndOutputValue.split(" ");
  344.                 NameAndStream nameAndStream = new NameAndStream(fields[0], locationDescription);
  345.                 Variable variable = installVariable(nameAndStream.getName(), nameAndStream.getStream(),
  346.                         EnumSet.noneOf(Flags.class), locationDescription);
  347.                 int value = Integer.parseInt(fields[1]);
  348.                 variable.setOutput(value);
  349.                 continue;
  350.             }
  351.             Object[] tokenisedRule = parse(trimmedLine, locationDescription);
  352.             if (null != tokenisedRule && tokenisedRule.length > 0)
  353.             {
  354.                 this.tokenisedRules.add(tokenisedRule);
  355.             }
  356.         }
  357.     }

  358.     /**
  359.      * Check the consistency of the traffic control program and perform initializations that require a completely built network.
  360.      * @throws SimRuntimeException when the simulation model is not an OtsModelInterface
  361.      * @throws TrafficControlException when a required traffic light or sensor is not present in the network
  362.      */
  363.     public void checkConsistency() throws SimRuntimeException, TrafficControlException
  364.     {
  365.         for (Variable v : this.variablesInDefinitionOrder)
  366.         {
  367.             if (0 == v.getRefCount() && (!v.isOutput()) && (!v.getName().matches("^RA.")))
  368.             {
  369.                 // System.out.println("Warning: " + v.getName() + v.getStream() + " is never referenced");
  370.                 fireTimedEvent(TRAFFICCONTROL_CONTROLLER_WARNING,
  371.                         new Object[] {getId(), v.toString(EnumSet.of(PrintFlags.ID)) + " is never referenced"},
  372.                         this.simulator.getSimulatorTime());
  373.             }
  374.             if (!v.isDetector())
  375.             {
  376.                 if (!v.getFlags().contains(Flags.HAS_START_RULE))
  377.                 {
  378.                     // System.out.println("Warning: " + v.getName() + v.getStream() + " has no start rule");
  379.                     fireTimedEvent(TRAFFICCONTROL_CONTROLLER_WARNING,
  380.                             new Object[] {getId(), v.toString(EnumSet.of(PrintFlags.ID)) + " has no start rule"},
  381.                             this.simulator.getSimulatorTime());
  382.                 }
  383.                 if ((!v.getFlags().contains(Flags.HAS_END_RULE)) && (!v.isTimer()))
  384.                 {
  385.                     // System.out.println("Warning: " + v.getName() + v.getStream() + " has no end rule");
  386.                     fireTimedEvent(TRAFFICCONTROL_CONTROLLER_WARNING,
  387.                             new Object[] {getId(), v.toString(EnumSet.of(PrintFlags.ID)) + " has no end rule"},
  388.                             this.simulator.getSimulatorTime());
  389.                 }
  390.             }
  391.         }
  392.         Network network = null;
  393.         try
  394.         {
  395.             network = ((OtsModelInterface) this.simulator.getModel()).getNetwork();
  396.         }
  397.         catch (ClassCastException e)
  398.         {
  399.             throw new SimRuntimeException("Model is not an OtsModelInterface");
  400.         }
  401.         ImmutableCollection<TrafficLight> trafficLights = network.getObjectMap(TrafficLight.class).values();
  402.         Map<String, List<TrafficLight>> trafficLightMap = new LinkedHashMap<>();
  403.         for (TrafficLight tl : trafficLights)
  404.         {
  405.             String trafficLightName = tl.getId();
  406.             if (trafficLightName.startsWith(getId()))
  407.             {
  408.                 trafficLightName = trafficLightName.substring(getId().length());
  409.                 if (trafficLightName.startsWith("."))
  410.                 {
  411.                     trafficLightName = trafficLightName.substring(1);
  412.                 }
  413.             }
  414.             if (trafficLightName.substring(trafficLightName.length() - 2).startsWith("."))
  415.             {
  416.                 trafficLightName = trafficLightName.substring(0, trafficLightName.length() - 2);
  417.             }
  418.             List<TrafficLight> list = trafficLightMap.get(trafficLightName);
  419.             if (null == list)
  420.             {
  421.                 list = new ArrayList<>();
  422.                 trafficLightMap.put(trafficLightName, list);
  423.             }
  424.             list.add(tl);
  425.         }
  426.         Map<String, TrafficLightDetector> detectors = new LinkedHashMap<>();
  427.         // Look up all the start/end detector and collect their parents (the traffic light sensors)
  428.         for (StartEndDetector startEndDetector : network.getObjectMap(StartEndDetector.class).values())
  429.         {
  430.             TrafficLightDetector trafficLightSensor = startEndDetector.getParent();
  431.             detectors.put(trafficLightSensor.getId(), trafficLightSensor);
  432.         }
  433.         for (Variable variable : this.variables.values())
  434.         {
  435.             if (variable.isOutput())
  436.             {
  437.                 if (variable.getValue() != 0)
  438.                 {
  439.                     for (TrafficLight trafficLight : variable.getTrafficLights())
  440.                     {
  441.                         trafficLight.setTrafficLightColor(variable.getColor());
  442.                     }
  443.                 }
  444.                 int added = 0;
  445.                 String name = String.format("%s%02d", variable.getName(), variable.getStream());
  446.                 String digits = String.format("%02d", variable.getStream());
  447.                 List<TrafficLight> matchingLights = trafficLightMap.get(digits);
  448.                 if (null == matchingLights)
  449.                 {
  450.                     throw new TrafficControlException("No traffic light for stream " + digits + " found");
  451.                 }
  452.                 for (TrafficLight tl : matchingLights)
  453.                 {
  454.                     try
  455.                     {
  456.                         variable.addOutput(tl);
  457.                     }
  458.                     catch (TrafficControlException exception)
  459.                     {
  460.                         // CANNOT HAPPEN
  461.                         exception.printStackTrace();
  462.                         throw new SimRuntimeException(exception);
  463.                     }
  464.                     if (variable.getValue() != 0)
  465.                     {
  466.                         tl.setTrafficLightColor(variable.getColor());
  467.                     }
  468.                     added++;
  469.                 }
  470.                 if (0 == added)
  471.                 {
  472.                     throw new TrafficControlException("No traffic light found that matches output " + name + " and " + getId());
  473.                 }
  474.             }
  475.             else if (variable.isDetector())
  476.             {
  477.                 String name = variable.getName();
  478.                 String subNumber = name.substring(name.length() - 1);
  479.                 name = name.substring(0, name.length() - 1);
  480.                 name = String.format("%s%02d", name, variable.getStream());
  481.                 String digits = String.format("%02d", variable.getStream());
  482.                 TrafficLightDetector tls = detectors.get("D" + digits + subNumber);
  483.                 if (null == tls)
  484.                 {
  485.                     throw new TrafficControlException(
  486.                             "No sensor found that matches " + name + " subNumber " + subNumber + " and " + getId());
  487.                 }
  488.                 variable.subscribeToDetector(tls);
  489.                 if (null != this.stateDisplay)
  490.                 {
  491.                     // Lookup the detector
  492.                     EventListener el =
  493.                             this.stateDisplay.getDetectorImage(String.format("%02d.%s", variable.getStream(), subNumber));
  494.                     if (null == el)
  495.                     {
  496.                         throw new TrafficControlException("Cannor find detector image matching variable " + variable);
  497.                     }
  498.                     // System.out.println("creating subscriptions to sensor " + tls);
  499.                     tls.addListener(el, TrafficLightDetector.TRAFFIC_LIGHT_DETECTOR_TRIGGER_ENTRY_EVENT);
  500.                     tls.addListener(el, TrafficLightDetector.TRAFFIC_LIGHT_DETECTOR_TRIGGER_EXIT_EVENT);
  501.                 }
  502.             }
  503.         }
  504.     }

  505.     /**
  506.      * Construct the display of this TrafCOD machine and connect it to the displayed traffic lights and sensors to this TrafCOD
  507.      * machine.
  508.      * @param rules List&lt;String&gt;; the individual lines that specify the graphics file and the locations of the sensor and
  509.      *            lights in the image
  510.      * @throws TrafficControlException when the tfg data is invalid
  511.      * @throws IOException when reading the background image fails
  512.      */
  513.     private void addTrafCODDisplay(final List<String> rules) throws TrafficControlException, IOException
  514.     {
  515.         boolean useFirstCoordinates = true;
  516.         int lineno = 0;
  517.         for (String line : rules)
  518.         {
  519.             lineno++;
  520.             String rule = line.trim();
  521.             if (rule.length() == 0)
  522.             {
  523.                 continue;
  524.             }
  525.             String[] fields = rule.split("=");
  526.             if ("mapfile".contentEquals(fields[0]))
  527.             {
  528.                 if (fields[1].matches("[Bb][Mm][Pp]|[Pp][Nn][Gg]$"))
  529.                 {
  530.                     useFirstCoordinates = false; // TODO really figure out which coordinates to use
  531.                 }
  532.                 // System.out.println("map file description is " + inputLine);
  533.                 // Make a decent attempt at constructing the URL of the map file
  534.             }
  535.             else if ("light".equals(fields[0]))
  536.             {
  537.                 // Extract the stream number
  538.                 int streamNumber;
  539.                 try
  540.                 {
  541.                     streamNumber = Integer.parseInt(fields[1].substring(0, 2));
  542.                 }
  543.                 catch (NumberFormatException nfe)
  544.                 {
  545.                     throw new TrafficControlException("Bad traffic light number in coordinates: " + rule);
  546.                 }
  547.                 // Extract the coordinates and create the image
  548.                 TrafficLightImage tli =
  549.                         new TrafficLightImage(this.stateDisplay, getCoordinates(fields[1].substring(3), useFirstCoordinates),
  550.                                 String.format("Traffic Light %02d", streamNumber));
  551.                 for (Variable v : this.variablesInDefinitionOrder)
  552.                 {
  553.                     if (v.isOutput() && v.getStream() == streamNumber)
  554.                     {
  555.                         // TODO: coupling between traffic light and tli via pub/sub, not as direct output of control
  556.                         //v.addOutput(tli);
  557.                     }
  558.                 }
  559.             }
  560.             else if ("detector".equals(fields[0]))
  561.             {
  562.                 int detectorStream;
  563.                 int detectorSubNumber;
  564.                 try
  565.                 {
  566.                     detectorStream = Integer.parseInt(fields[1].substring(0, 2));
  567.                     detectorSubNumber = Integer.parseInt(fields[1].substring(3, 4));
  568.                 }
  569.                 catch (NumberFormatException nfe)
  570.                 {
  571.                     throw new TrafficControlException("Cannot parse detector number in coordinates " + rule);
  572.                 }
  573.                 String detectorName = String.format("D%02d%d", detectorStream, detectorSubNumber);
  574.                 Variable detectorVariable = this.variables.get(detectorName);
  575.                 if (null == detectorVariable)
  576.                 {
  577.                     throw new TrafficControlException(
  578.                             "coordinates defines detector " + detectorName + " which does not exist in the TrafCOD program");
  579.                 }
  580.                 // DetectorImage di =
  581.                 new DetectorImage(this.stateDisplay, getCoordinates(fields[1].substring(5), useFirstCoordinates),
  582.                         String.format("%02d.%d", detectorStream, detectorSubNumber),
  583.                         String.format("Detector %02d.%d", detectorStream, detectorSubNumber));
  584.             }
  585.             else
  586.             {
  587.                 throw new TrafficControlException("Cannot parse coordinates line " + lineno + "in \"" + line + "\"");
  588.             }
  589.         }
  590.     }

  591.     /**
  592.      * Extract two coordinates from a line of text.
  593.      * @param line String; the text
  594.      * @param useFirstCoordinates boolean; if true; process the first pair of integer values; if false; use the second pair of
  595.      *            integer values
  596.      * @return Point2D
  597.      * @throws TrafficControlException when the coordinates could not be parsed
  598.      */
  599.     private static Point2D getCoordinates(final String line, final boolean useFirstCoordinates) throws TrafficControlException
  600.     {
  601.         String work = line.replaceAll("[ ,\t]+", "\t").trim();
  602.         int x;
  603.         int y;
  604.         String[] fields = work.split("\t");
  605.         if (fields.length < (useFirstCoordinates ? 2 : 4))
  606.         {
  607.             throw new TrafficControlException("not enough fields in tfg line \"" + line + "\"");
  608.         }
  609.         try
  610.         {
  611.             x = Integer.parseInt(fields[useFirstCoordinates ? 0 : 2]);
  612.             y = Integer.parseInt(fields[useFirstCoordinates ? 1 : 3]);
  613.         }
  614.         catch (NumberFormatException nfe)
  615.         {
  616.             throw new TrafficControlException("Bad value in tfg line \"" + line + "\"");
  617.         }
  618.         return new Point2D.Double(x, y);
  619.     }

  620.     /**
  621.      * Decrement all running timers.
  622.      * @return int; the total number of timers that expired
  623.      * @throws TrafficControlException Should never happen
  624.      */
  625.     private int decrementTimers() throws TrafficControlException
  626.     {
  627.         // System.out.println("Decrement running timers");
  628.         int changeCount = 0;
  629.         for (Variable v : this.variables.values())
  630.         {
  631.             if (v.isTimer() && v.getValue() > 0 && v.decrementTimer(this.currentTime10))
  632.             {
  633.                 changeCount++;
  634.             }
  635.         }
  636.         return changeCount;
  637.     }

  638.     /**
  639.      * Reset the START, END and CHANGED flags of all timers. (These do not get reset during the normal rule evaluation phase.)
  640.      */
  641.     private void resetTimerFlags()
  642.     {
  643.         for (Variable v : this.variablesInDefinitionOrder)
  644.         {
  645.             if (v.isTimer())
  646.             {
  647.                 v.clearChangedFlag();
  648.                 v.clearFlag(Flags.START);
  649.                 v.clearFlag(Flags.END);
  650.             }
  651.         }
  652.     }

  653.     /**
  654.      * Evaluate all expressions until no more changes occur.
  655.      * @throws TrafficControlException when evaluation of a rule fails
  656.      * @throws SimRuntimeException when scheduling the next evaluation fails
  657.      */
  658.     @SuppressWarnings("unused")
  659.     private void evalExprs() throws TrafficControlException, SimRuntimeException
  660.     {
  661.         fireTimedEvent(TrafficController.TRAFFICCONTROL_CONTROLLER_EVALUATING, new Object[] {getId()},
  662.                 this.simulator.getSimulatorTime());
  663.         // System.out.println("evalExprs: time is " + EngineeringFormatter.format(this.simulator.getSimulatorTime().si));
  664.         // insert some delay for testing; without this the simulation runs too fast
  665.         // try
  666.         // {
  667.         // Thread.sleep(10);
  668.         // }
  669.         // catch (InterruptedException exception)
  670.         // {
  671.         // System.out.println("Sleep in evalExprs was interrupted");
  672.         // // exception.printStackTrace();
  673.         // }
  674.         // Contrary to the C++ builder version; this implementation decrements the times at the start of evalExprs
  675.         // By doing it before updating this.currentTime10; the debugging output should be very similar
  676.         decrementTimers();
  677.         this.currentTime10 = (int) (this.simulator.getSimulatorTime().si * 10);
  678.         int loop;
  679.         for (loop = 0; loop < this.maxLoopCount; loop++)
  680.         {
  681.             int changeCount = evalExpressionsOnce();
  682.             resetTimerFlags();
  683.             if (changeCount == 0)
  684.             {
  685.                 break;
  686.             }
  687.         }
  688.         // System.out.println("Executed " + (loop + 1) + " iteration(s)");
  689.         if (loop >= this.maxLoopCount)
  690.         {
  691.             StringBuffer warningMessage = new StringBuffer();
  692.             warningMessage.append(String
  693.                     .format("Control program did not settle to a final state in %d iterations; oscillating variables:", loop));
  694.             for (Variable v : this.variablesInDefinitionOrder)
  695.             {
  696.                 if (v.getFlags().contains(Flags.CHANGED))
  697.                 {
  698.                     warningMessage.append(String.format(" %s%02d", v.getName(), v.getStream()));
  699.                 }
  700.             }
  701.             fireTimedEvent(TrafficController.TRAFFICCONTROL_CONTROLLER_WARNING,
  702.                     new Object[] {getId(), warningMessage.toString()}, this.simulator.getSimulatorTime());
  703.         }
  704.         this.simulator.scheduleEventRel(EVALUATION_INTERVAL, this, "evalExprs", null);
  705.     }

  706.     /**
  707.      * Evaluate all expressions and return the number of changed variables.
  708.      * @return int; the number of changed variables
  709.      * @throws TrafficControlException when evaluation of a rule fails
  710.      */
  711.     private int evalExpressionsOnce() throws TrafficControlException
  712.     {
  713.         for (Variable variable : this.variables.values())
  714.         {
  715.             variable.clearChangedFlag();
  716.         }
  717.         int changeCount = 0;
  718.         for (Object[] rule : this.tokenisedRules)
  719.         {
  720.             if (evalRule(rule))
  721.             {
  722.                 changeCount++;
  723.             }
  724.         }
  725.         return changeCount;
  726.     }

  727.     /**
  728.      * Evaluate a rule.
  729.      * @param rule Object[]; the tokenised rule
  730.      * @return boolean; true if the variable that is affected by the rule has changed; false if no variable was changed
  731.      * @throws TrafficControlException when evaluation of the rule fails
  732.      */
  733.     private boolean evalRule(final Object[] rule) throws TrafficControlException
  734.     {
  735.         boolean result = false;
  736.         Token ruleType = (Token) rule[0];
  737.         Variable destination = (Variable) rule[1];
  738.         if (destination.isTimer())
  739.         {
  740.             if (destination.getFlags().contains(Flags.TIMEREXPIRED))
  741.             {
  742.                 destination.clearFlag(Flags.TIMEREXPIRED);
  743.                 destination.setFlag(Flags.END);
  744.             }
  745.             else if (destination.getFlags().contains(Flags.START) || destination.getFlags().contains(Flags.END))
  746.             {
  747.                 destination.clearFlag(Flags.START);
  748.                 destination.clearFlag(Flags.END);
  749.                 destination.setFlag(Flags.CHANGED);
  750.             }
  751.         }
  752.         else
  753.         {
  754.             // Normal Variable or detector
  755.             if (Token.START_RULE == ruleType)
  756.             {
  757.                 destination.clearFlag(Flags.START);
  758.             }
  759.             else if (Token.END_RULE == ruleType)
  760.             {
  761.                 destination.clearFlag(Flags.END);
  762.             }
  763.             else
  764.             {
  765.                 destination.clearFlag(Flags.START);
  766.                 destination.clearFlag(Flags.END);
  767.             }
  768.         }

  769.         int currentValue = destination.getValue();
  770.         if (Token.START_RULE == ruleType && currentValue != 0 || Token.END == ruleType && currentValue == 0
  771.                 || Token.INIT_TIMER == ruleType && currentValue != 0)
  772.         {
  773.             return false; // Value cannot change from zero to nonzero or vice versa due to evaluating the expression
  774.         }
  775.         this.currentRule = rule;
  776.         this.currentToken = 2; // Point to first token of the RHS
  777.         this.stack.clear();
  778.         evalExpr(0);
  779.         if (this.currentToken < this.currentRule.length && Token.CLOSE_PAREN == this.currentRule[this.currentToken])
  780.         {
  781.             throw new TrafficControlException("Too many closing parentheses");
  782.         }
  783.         int resultValue = pop();
  784.         if (Token.END_RULE == ruleType)
  785.         {
  786.             // Invert the result
  787.             if (0 == resultValue)
  788.             {
  789.                 resultValue = destination.getValue(); // preserve the current value
  790.             }
  791.             else
  792.             {
  793.                 resultValue = 0;
  794.             }
  795.         }
  796.         if (resultValue != 0 && destination.getValue() == 0)
  797.         {
  798.             destination.setFlag(Flags.START);
  799.         }
  800.         else if (resultValue == 0 && destination.getValue() != 0)
  801.         {
  802.             destination.setFlag(Flags.END);
  803.         }
  804.         if (destination.isTimer())
  805.         {
  806.             if (resultValue != 0 && Token.END_RULE != ruleType)
  807.             {
  808.                 if (0 == destination.getValue())
  809.                 {
  810.                     result = true;
  811.                 }
  812.                 int timerValue10 = destination.getTimerMax();
  813.                 if (timerValue10 < 1)
  814.                 {
  815.                     // Cheat; ensure it will property expire on the next timer tick
  816.                     timerValue10 = 1;
  817.                 }
  818.                 result = destination.setValue(timerValue10, this.currentTime10, new CausePrinter(rule), this);
  819.             }
  820.             else if (0 == resultValue && Token.END_RULE == ruleType && destination.getValue() != 0)
  821.             {
  822.                 result = destination.setValue(0, this.currentTime10, new CausePrinter(rule), this);
  823.             }
  824.         }
  825.         else if (destination.getValue() != resultValue)
  826.         {
  827.             result = destination.setValue(resultValue, this.currentTime10, new CausePrinter(rule), this);
  828.             if (destination.isOutput())
  829.             {
  830.                 fireTimedEvent(TRAFFIC_LIGHT_CHANGED, new Object[] {getId(), destination.getStream(), destination.getColor()},
  831.                         getSimulator().getSimulatorAbsTime());
  832.             }
  833.             if (destination.isConflictGroup() && resultValue != 0)
  834.             {
  835.                 int conflictGroupRank = destination.conflictGroupRank();
  836.                 StringBuilder conflictGroupList = new StringBuilder();
  837.                 for (Short stream : this.conflictGroups.get(conflictGroupRank))
  838.                 {
  839.                     if (conflictGroupList.length() > 0)
  840.                     {
  841.                         conflictGroupList.append(" ");
  842.                     }
  843.                     conflictGroupList.append(String.format("%02d", stream));
  844.                 }
  845.                 fireTimedEvent(TRAFFICCONTROL_CONFLICT_GROUP_CHANGED,
  846.                         new Object[] {getId(), this.currentConflictGroup, conflictGroupList.toString()},
  847.                         getSimulator().getSimulatorTime());
  848.                 // System.out.println("Conflict group changed from " + this.currentConflictGroup + " to "
  849.                 // + conflictGroupList.toString());
  850.                 this.currentConflictGroup = conflictGroupList.toString();
  851.             }
  852.         }
  853.         return result;
  854.     }

  855.     /** Binding strength of relational operators. */
  856.     private static final int BIND_RELATIONAL_OPERATOR = 1;

  857.     /** Binding strength of addition and subtraction. */
  858.     private static final int BIND_ADDITION = 2;

  859.     /** Binding strength of multiplication and division. */
  860.     private static final int BIND_MULTIPLY = 3;

  861.     /** Binding strength of unary minus. */
  862.     private static final int BIND_UNARY_MINUS = 4;

  863.     /**
  864.      * Evaluate an expression. <br>
  865.      * The methods evalExpr and evalRHS together evaluate an expression. This is done using recursion and a stack. The argument
  866.      * bindingStrength that is passed around is the binding strength of the last preceding pending operator. if a binary
  867.      * operator with the same or a lower strength is encountered, the pending operator must be applied first. On the other hand
  868.      * of a binary operator with higher binding strength is encountered, that operator takes precedence over the pending
  869.      * operator. To evaluate an expression, call evalExpr with a bindingStrength value of 0. On return verify that currentToken
  870.      * has incremented to the end of the expression and that there is one value (the result) on the stack.
  871.      * @param bindingStrength int; the binding strength of a not yet applied binary operator (higher value must be applied
  872.      *            first)
  873.      * @throws TrafficControlException when the expression is not valid
  874.      */
  875.     private void evalExpr(final int bindingStrength) throws TrafficControlException
  876.     {
  877.         if (this.currentToken >= this.currentRule.length)
  878.         {
  879.             throw new TrafficControlException("Missing operand at end of expression " + printRule(this.currentRule, false));
  880.         }
  881.         Token token = (Token) this.currentRule[this.currentToken++];
  882.         Object nextToken = null;
  883.         if (this.currentToken < this.currentRule.length)
  884.         {
  885.             nextToken = this.currentRule[this.currentToken];
  886.         }
  887.         switch (token)
  888.         {
  889.             case UNARY_MINUS:
  890.                 if (Token.OPEN_PAREN != nextToken && Token.VARIABLE != nextToken && Token.NEG_VARIABLE != nextToken
  891.                         && Token.CONSTANT != nextToken && Token.START != nextToken && Token.END != nextToken)
  892.                 {
  893.                     throw new TrafficControlException("Operand expected after unary minus");
  894.                 }
  895.                 evalExpr(BIND_UNARY_MINUS);
  896.                 push(-pop());
  897.                 break;

  898.             case OPEN_PAREN:
  899.                 evalExpr(0);
  900.                 if (Token.CLOSE_PAREN != this.currentRule[this.currentToken])
  901.                 {
  902.                     throw new TrafficControlException("Missing closing parenthesis");
  903.                 }
  904.                 this.currentToken++;
  905.                 break;

  906.             case START:
  907.                 if (Token.VARIABLE != nextToken || this.currentToken >= this.currentRule.length - 1)
  908.                 {
  909.                     throw new TrafficControlException("Missing variable after S");
  910.                 }
  911.                 nextToken = this.currentRule[++this.currentToken];
  912.                 if (!(nextToken instanceof Variable))
  913.                 {
  914.                     throw new TrafficControlException("Missing variable after S");
  915.                 }
  916.                 push(((Variable) nextToken).getFlags().contains(Flags.START) ? 1 : 0);
  917.                 this.currentToken++;
  918.                 break;

  919.             case END:
  920.                 if (Token.VARIABLE != nextToken || this.currentToken >= this.currentRule.length - 1)
  921.                 {
  922.                     throw new TrafficControlException("Missing variable after E");
  923.                 }
  924.                 nextToken = this.currentRule[++this.currentToken];
  925.                 if (!(nextToken instanceof Variable))
  926.                 {
  927.                     throw new TrafficControlException("Missing variable after E");
  928.                 }
  929.                 push(((Variable) nextToken).getFlags().contains(Flags.END) ? 1 : 0);
  930.                 this.currentToken++;
  931.                 break;

  932.             case VARIABLE:
  933.             {
  934.                 Variable operand = (Variable) nextToken;
  935.                 if (operand.isTimer())
  936.                 {
  937.                     push(operand.getValue() == 0 ? 0 : 1);
  938.                 }
  939.                 else
  940.                 {
  941.                     push(operand.getValue());
  942.                 }
  943.                 this.currentToken++;
  944.                 break;
  945.             }

  946.             case CONSTANT:
  947.                 push((Integer) nextToken);
  948.                 this.currentToken++;
  949.                 break;

  950.             case NEG_VARIABLE:
  951.                 Variable operand = (Variable) nextToken;
  952.                 push(operand.getValue() == 0 ? 1 : 0);
  953.                 this.currentToken++;
  954.                 break;

  955.             default:
  956.                 throw new TrafficControlException("Operand missing");
  957.         }
  958.         evalRHS(bindingStrength);
  959.     }

  960.     /**
  961.      * Evaluate the right-hand-side of an expression.
  962.      * @param bindingStrength int; the binding strength of the most recent, not yet applied, binary operator
  963.      * @throws TrafficControlException when the RHS of an expression is invalid
  964.      */
  965.     private void evalRHS(final int bindingStrength) throws TrafficControlException
  966.     {
  967.         while (true)
  968.         {
  969.             if (this.currentToken >= this.currentRule.length)
  970.             {
  971.                 return;
  972.             }
  973.             Token token = (Token) this.currentRule[this.currentToken];
  974.             switch (token)
  975.             {
  976.                 case CLOSE_PAREN:
  977.                     return;

  978.                 case TIMES:
  979.                     if (BIND_MULTIPLY <= bindingStrength)
  980.                     {
  981.                         return; // apply pending operator now
  982.                     }
  983.                     /*-
  984.                      * apply pending operator later
  985.                      * 1: evaluate the RHS operand.
  986.                      * 2: multiply the top-most two operands on the stack and push the result on the stack.
  987.                      */
  988.                     this.currentToken++;
  989.                     evalExpr(BIND_MULTIPLY);
  990.                     push(pop() * pop() == 0 ? 0 : 1);
  991.                     break;

  992.                 case EQ:
  993.                 case NOTEQ:
  994.                 case LE:
  995.                 case LEEQ:
  996.                 case GT:
  997.                 case GTEQ:
  998.                     if (BIND_RELATIONAL_OPERATOR <= bindingStrength)
  999.                     {
  1000.                         return; // apply pending operator now
  1001.                     }
  1002.                     /*-
  1003.                      * apply pending operator later
  1004.                      * 1: evaluate the RHS operand.
  1005.                      * 2: compare the top-most two operands on the stack and push the result on the stack.
  1006.                      */
  1007.                     this.currentToken++;
  1008.                     evalExpr(BIND_RELATIONAL_OPERATOR);
  1009.                     switch (token)
  1010.                     {
  1011.                         case EQ:
  1012.                             push(pop() == pop() ? 1 : 0);
  1013.                             break;

  1014.                         case NOTEQ:
  1015.                             push(pop() != pop() ? 1 : 0);
  1016.                             break;

  1017.                         case GT:
  1018.                             push(pop() < pop() ? 1 : 0);
  1019.                             break;

  1020.                         case GTEQ:
  1021.                             push(pop() <= pop() ? 1 : 0);
  1022.                             break;

  1023.                         case LE:
  1024.                             push(pop() > pop() ? 1 : 0);
  1025.                             break;

  1026.                         case LEEQ:
  1027.                             push(pop() >= pop() ? 1 : 0);
  1028.                             break;

  1029.                         default:
  1030.                             throw new TrafficControlException("Bad relational operator");
  1031.                     }
  1032.                     break;

  1033.                 case PLUS:
  1034.                     if (BIND_ADDITION <= bindingStrength)
  1035.                     {
  1036.                         return; // apply pending operator now
  1037.                     }
  1038.                     /*-
  1039.                      * apply pending operator later
  1040.                      * 1: evaluate the RHS operand.
  1041.                      * 2: add (OR) the top-most two operands on the stack and push the result on the stack.
  1042.                      */
  1043.                     this.currentToken++;
  1044.                     evalExpr(BIND_ADDITION);
  1045.                     push(pop() + pop() == 0 ? 0 : 1);
  1046.                     break;

  1047.                 case MINUS:
  1048.                     if (BIND_ADDITION <= bindingStrength)
  1049.                     {
  1050.                         return; // apply pending operator now
  1051.                     }
  1052.                     /*-
  1053.                      * apply pending operator later
  1054.                      * 1: evaluate the RHS operand.
  1055.                      * 2: subtract the top-most two operands on the stack and push the result on the stack.
  1056.                      */
  1057.                     this.currentToken++;
  1058.                     evalExpr(BIND_ADDITION);
  1059.                     push(-pop() + pop());
  1060.                     break;

  1061.                 default:
  1062.                     throw new TrafficControlException("Missing binary operator");
  1063.             }
  1064.         }
  1065.     }

  1066.     /**
  1067.      * Push a value on the evaluation stack.
  1068.      * @param value int; the value to push on the evaluation stack
  1069.      */
  1070.     private void push(final int value)
  1071.     {
  1072.         this.stack.add(value);
  1073.     }

  1074.     /**
  1075.      * Remove the last not-yet-removed value from the evaluation stack and return it.
  1076.      * @return int; the last non-yet-removed value on the evaluation stack
  1077.      * @throws TrafficControlException when the stack is empty
  1078.      */
  1079.     private int pop() throws TrafficControlException
  1080.     {
  1081.         if (this.stack.size() < 1)
  1082.         {
  1083.             throw new TrafficControlException("Stack empty");
  1084.         }
  1085.         return this.stack.remove(this.stack.size() - 1);
  1086.     }

  1087.     /**
  1088.      * Print a tokenized rule.
  1089.      * @param tokens Object[]; the tokens
  1090.      * @param printValues boolean; if true; print the values of all encountered variable; if false; do not print the values of
  1091.      *            all encountered variable
  1092.      * @return String; a textual approximation of the original rule
  1093.      * @throws TrafficControlException when tokens does not match the expected grammar
  1094.      */
  1095.     static String printRule(final Object[] tokens, final boolean printValues) throws TrafficControlException
  1096.     {
  1097.         EnumSet<PrintFlags> variableFlags = EnumSet.of(PrintFlags.ID);
  1098.         if (printValues)
  1099.         {
  1100.             variableFlags.add(PrintFlags.VALUE);
  1101.         }
  1102.         EnumSet<PrintFlags> negatedVariableFlags = EnumSet.copyOf(variableFlags);
  1103.         negatedVariableFlags.add(PrintFlags.NEGATED);
  1104.         StringBuilder result = new StringBuilder();
  1105.         for (int inPos = 0; inPos < tokens.length; inPos++)
  1106.         {
  1107.             Object token = tokens[inPos];
  1108.             if (token instanceof Token)
  1109.             {
  1110.                 switch ((Token) token)
  1111.                 {
  1112.                     case EQUALS_RULE:
  1113.                         result.append(((Variable) tokens[++inPos]).toString(variableFlags));
  1114.                         result.append("=");
  1115.                         break;

  1116.                     case NEG_EQUALS_RULE:
  1117.                         result.append(((Variable) tokens[++inPos]).toString(negatedVariableFlags));
  1118.                         result.append("=");
  1119.                         break;

  1120.                     case START_RULE:
  1121.                         result.append(((Variable) tokens[++inPos]).toString(variableFlags));
  1122.                         result.append(".=");
  1123.                         break;

  1124.                     case END_RULE:
  1125.                         result.append(((Variable) tokens[++inPos]).toString(variableFlags));
  1126.                         result.append("N.=");
  1127.                         break;

  1128.                     case INIT_TIMER:
  1129.                         result.append(((Variable) tokens[++inPos]).toString(EnumSet.of(PrintFlags.ID, PrintFlags.INITTIMER)));
  1130.                         result.append(".=");
  1131.                         break;

  1132.                     case REINIT_TIMER:
  1133.                         result.append(((Variable) tokens[++inPos]).toString(EnumSet.of(PrintFlags.ID, PrintFlags.REINITTIMER)));
  1134.                         result.append(".=");
  1135.                         break;

  1136.                     case START:
  1137.                         result.append("S");
  1138.                         break;

  1139.                     case END:
  1140.                         result.append("E");
  1141.                         break;

  1142.                     case VARIABLE:
  1143.                         result.append(((Variable) tokens[++inPos]).toString(variableFlags));
  1144.                         break;

  1145.                     case NEG_VARIABLE:
  1146.                         result.append(((Variable) tokens[++inPos]).toString(variableFlags));
  1147.                         result.append("N");
  1148.                         break;

  1149.                     case CONSTANT:
  1150.                         result.append(tokens[++inPos]).toString();
  1151.                         break;

  1152.                     case UNARY_MINUS:
  1153.                     case MINUS:
  1154.                         result.append("-");
  1155.                         break;

  1156.                     case PLUS:
  1157.                         result.append("+");
  1158.                         break;

  1159.                     case TIMES:
  1160.                         result.append(".");
  1161.                         break;

  1162.                     case EQ:
  1163.                         result.append("=");
  1164.                         break;

  1165.                     case NOTEQ:
  1166.                         result.append("<>");
  1167.                         break;

  1168.                     case GT:
  1169.                         result.append(">");
  1170.                         break;

  1171.                     case GTEQ:
  1172.                         result.append(">=");
  1173.                         break;

  1174.                     case LE:
  1175.                         result.append("<");
  1176.                         break;

  1177.                     case LEEQ:
  1178.                         result.append("<=");
  1179.                         break;

  1180.                     case OPEN_PAREN:
  1181.                         result.append("(");
  1182.                         break;

  1183.                     case CLOSE_PAREN:
  1184.                         result.append(")");
  1185.                         break;

  1186.                     default:
  1187.                         System.out.println(
  1188.                                 "<<<ERROR>>> encountered a non-Token object: " + token + " after " + result.toString());
  1189.                         throw new TrafficControlException("Unknown token");
  1190.                 }
  1191.             }
  1192.             else
  1193.             {
  1194.                 System.out.println("<<<ERROR>>> encountered a non-Token object: " + token + " after " + result.toString());
  1195.                 throw new TrafficControlException("Not a token");
  1196.             }
  1197.         }
  1198.         return result.toString();
  1199.     }

  1200.     /**
  1201.      * States of the rule parser.
  1202.      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
  1203.      */
  1204.     enum ParserState
  1205.     {
  1206.         /** Looking for the left hand side of an assignment. */
  1207.         FIND_LHS,
  1208.         /** Looking for an assignment operator. */
  1209.         FIND_ASSIGN,
  1210.         /** Looking for the right hand side of an assignment. */
  1211.         FIND_RHS,
  1212.         /** Looking for an optional unary minus. */
  1213.         MAY_UMINUS,
  1214.         /** Looking for an expression. */
  1215.         FIND_EXPR,
  1216.     }

  1217.     /**
  1218.      * Types of TrafCOD tokens.
  1219.      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
  1220.      */
  1221.     enum Token
  1222.     {
  1223.         /** Equals rule. */
  1224.         EQUALS_RULE,
  1225.         /** Not equals rule. */
  1226.         NEG_EQUALS_RULE,
  1227.         /** Assignment rule. */
  1228.         ASSIGNMENT,
  1229.         /** Start rule. */
  1230.         START_RULE,
  1231.         /** End rule. */
  1232.         END_RULE,
  1233.         /** Timer initialize rule. */
  1234.         INIT_TIMER,
  1235.         /** Timer re-initialize rule. */
  1236.         REINIT_TIMER,
  1237.         /** Unary minus operator. */
  1238.         UNARY_MINUS,
  1239.         /** Less than or equal to (&lt;=). */
  1240.         LEEQ,
  1241.         /** Not equal to (!=). */
  1242.         NOTEQ,
  1243.         /** Less than (&lt;). */
  1244.         LE,
  1245.         /** Greater than or equal to (&gt;=). */
  1246.         GTEQ,
  1247.         /** Greater than (&gt;). */
  1248.         GT,
  1249.         /** Equals to (=). */
  1250.         EQ,
  1251.         /** True if following variable has just started. */
  1252.         START,
  1253.         /** True if following variable has just ended. */
  1254.         END,
  1255.         /** Variable follows. */
  1256.         VARIABLE,
  1257.         /** Variable that follows must be logically negated. */
  1258.         NEG_VARIABLE,
  1259.         /** Integer follows. */
  1260.         CONSTANT,
  1261.         /** Addition operator. */
  1262.         PLUS,
  1263.         /** Subtraction operator. */
  1264.         MINUS,
  1265.         /** Multiplication operator. */
  1266.         TIMES,
  1267.         /** Opening parenthesis. */
  1268.         OPEN_PAREN,
  1269.         /** Closing parenthesis. */
  1270.         CLOSE_PAREN,
  1271.     }

  1272.     /**
  1273.      * Parse one TrafCOD rule.
  1274.      * @param rawRule String; the TrafCOD rule
  1275.      * @param locationDescription String; description of the location (file, line) where the rule was found
  1276.      * @return Object[]; array filled with the tokenized rule
  1277.      * @throws TrafficControlException when the rule is not a valid TrafCOD rule
  1278.      */
  1279.     private Object[] parse(final String rawRule, final String locationDescription) throws TrafficControlException
  1280.     {
  1281.         if (rawRule.length() == 0)
  1282.         {
  1283.             throw new TrafficControlException("empty rule at " + locationDescription);
  1284.         }
  1285.         ParserState state = ParserState.FIND_LHS;
  1286.         String rule = rawRule.toUpperCase(Locale.US);
  1287.         Token ruleType = Token.ASSIGNMENT;
  1288.         int inPos = 0;
  1289.         NameAndStream lhsNameAndStream = null;
  1290.         List<Object> tokens = new ArrayList<>();
  1291.         while (inPos < rule.length())
  1292.         {
  1293.             char character = rule.charAt(inPos);
  1294.             if (Character.isWhitespace(character))
  1295.             {
  1296.                 inPos++;
  1297.                 continue;
  1298.             }
  1299.             switch (state)
  1300.             {
  1301.                 case FIND_LHS:
  1302.                 {
  1303.                     if ('S' == character)
  1304.                     {
  1305.                         ruleType = Token.START_RULE;
  1306.                         inPos++;
  1307.                         lhsNameAndStream = new NameAndStream(rule.substring(inPos), locationDescription);
  1308.                         inPos += lhsNameAndStream.getNumberOfChars();
  1309.                     }
  1310.                     else if ('E' == character)
  1311.                     {
  1312.                         ruleType = Token.END_RULE;
  1313.                         inPos++;
  1314.                         lhsNameAndStream = new NameAndStream(rule.substring(inPos), locationDescription);
  1315.                         inPos += lhsNameAndStream.getNumberOfChars();
  1316.                     }
  1317.                     else if ('I' == character && 'T' == rule.charAt(inPos + 1))
  1318.                     {
  1319.                         ruleType = Token.INIT_TIMER;
  1320.                         inPos++; // The 'T' is part of the name of the time; do not consume it
  1321.                         lhsNameAndStream = new NameAndStream(rule.substring(inPos), locationDescription);
  1322.                         inPos += lhsNameAndStream.getNumberOfChars();
  1323.                     }
  1324.                     else if ('R' == character && 'I' == rule.charAt(inPos + 1) && 'T' == rule.charAt(inPos + 2))
  1325.                     {
  1326.                         ruleType = Token.REINIT_TIMER;
  1327.                         inPos += 2; // The 'T' is part of the name of the timer; do not consume it
  1328.                         lhsNameAndStream = new NameAndStream(rule.substring(inPos), locationDescription);
  1329.                         inPos += lhsNameAndStream.getNumberOfChars();
  1330.                     }
  1331.                     else if ('T' == character && rule.indexOf('=') >= 0
  1332.                             && (rule.indexOf('N') < 0 || rule.indexOf('N') > rule.indexOf('=')))
  1333.                     {
  1334.                         throw new TrafficControlException("Bad time initialization at " + locationDescription);
  1335.                     }
  1336.                     else
  1337.                     {
  1338.                         ruleType = Token.EQUALS_RULE;
  1339.                         lhsNameAndStream = new NameAndStream(rule.substring(inPos), locationDescription);
  1340.                         inPos += lhsNameAndStream.getNumberOfChars();
  1341.                         if (lhsNameAndStream.isNegated())
  1342.                         {
  1343.                             ruleType = Token.NEG_EQUALS_RULE;
  1344.                         }
  1345.                     }
  1346.                     state = ParserState.FIND_ASSIGN;
  1347.                     break;
  1348.                 }

  1349.                 case FIND_ASSIGN:
  1350.                 {
  1351.                     if ('.' == character && '=' == rule.charAt(inPos + 1))
  1352.                     {
  1353.                         if (Token.EQUALS_RULE == ruleType)
  1354.                         {
  1355.                             ruleType = Token.START_RULE;
  1356.                         }
  1357.                         else if (Token.NEG_EQUALS_RULE == ruleType)
  1358.                         {
  1359.                             ruleType = Token.END_RULE;
  1360.                         }
  1361.                         inPos += 2;
  1362.                     }
  1363.                     else if ('=' == character)
  1364.                     {
  1365.                         if (Token.START_RULE == ruleType || Token.END_RULE == ruleType || Token.INIT_TIMER == ruleType
  1366.                                 || Token.REINIT_TIMER == ruleType)
  1367.                         {
  1368.                             throw new TrafficControlException("Bad assignment at " + locationDescription);
  1369.                         }
  1370.                         inPos++;
  1371.                     }
  1372.                     tokens.add(ruleType);
  1373.                     EnumSet<Flags> lhsFlags = EnumSet.noneOf(Flags.class);
  1374.                     if (Token.START_RULE == ruleType || Token.EQUALS_RULE == ruleType || Token.NEG_EQUALS_RULE == ruleType
  1375.                             || Token.INIT_TIMER == ruleType || Token.REINIT_TIMER == ruleType)
  1376.                     {
  1377.                         lhsFlags.add(Flags.HAS_START_RULE);
  1378.                     }
  1379.                     if (Token.END_RULE == ruleType || Token.EQUALS_RULE == ruleType || Token.NEG_EQUALS_RULE == ruleType)
  1380.                     {
  1381.                         lhsFlags.add(Flags.HAS_END_RULE);
  1382.                     }
  1383.                     Variable lhsVariable = installVariable(lhsNameAndStream.getName(), lhsNameAndStream.getStream(), lhsFlags,
  1384.                             locationDescription);
  1385.                     tokens.add(lhsVariable);
  1386.                     state = ParserState.MAY_UMINUS;
  1387.                     break;
  1388.                 }

  1389.                 case MAY_UMINUS:
  1390.                     if ('-' == character)
  1391.                     {
  1392.                         tokens.add(Token.UNARY_MINUS);
  1393.                         inPos++;
  1394.                     }
  1395.                     state = ParserState.FIND_EXPR;
  1396.                     break;

  1397.                 case FIND_EXPR:
  1398.                 {
  1399.                     if (Character.isDigit(character))
  1400.                     {
  1401.                         int constValue = 0;
  1402.                         while (inPos < rule.length() && Character.isDigit(rule.charAt(inPos)))
  1403.                         {
  1404.                             int digit = rule.charAt(inPos) - '0';
  1405.                             if (constValue >= (Integer.MAX_VALUE - digit) / 10)
  1406.                             {
  1407.                                 throw new TrafficControlException("Number too large at " + locationDescription);
  1408.                             }
  1409.                             constValue = 10 * constValue + digit;
  1410.                             inPos++;
  1411.                         }
  1412.                         tokens.add(Token.CONSTANT);
  1413.                         tokens.add(new Integer(constValue));
  1414.                     }
  1415.                     if (inPos >= rule.length())
  1416.                     {
  1417.                         return tokens.toArray();
  1418.                     }
  1419.                     character = rule.charAt(inPos);
  1420.                     switch (character)
  1421.                     {
  1422.                         case '+':
  1423.                             tokens.add(Token.PLUS);
  1424.                             inPos++;
  1425.                             break;

  1426.                         case '-':
  1427.                             tokens.add(Token.MINUS);
  1428.                             inPos++;
  1429.                             break;

  1430.                         case '.':
  1431.                             tokens.add(Token.TIMES);
  1432.                             inPos++;
  1433.                             break;

  1434.                         case ')':
  1435.                             tokens.add(Token.CLOSE_PAREN);
  1436.                             inPos++;
  1437.                             break;

  1438.                         case '<':
  1439.                         {
  1440.                             Character nextChar = rule.charAt(++inPos);
  1441.                             if ('=' == nextChar)
  1442.                             {
  1443.                                 tokens.add(Token.LEEQ);
  1444.                                 inPos++;
  1445.                             }
  1446.                             else if ('>' == nextChar)
  1447.                             {
  1448.                                 tokens.add(Token.NOTEQ);
  1449.                                 inPos++;
  1450.                             }
  1451.                             else
  1452.                             {
  1453.                                 tokens.add(Token.LE);
  1454.                             }
  1455.                             break;
  1456.                         }

  1457.                         case '>':
  1458.                         {
  1459.                             Character nextChar = rule.charAt(++inPos);
  1460.                             if ('=' == nextChar)
  1461.                             {
  1462.                                 tokens.add(Token.GTEQ);
  1463.                                 inPos++;
  1464.                             }
  1465.                             else if ('<' == nextChar)
  1466.                             {
  1467.                                 tokens.add(Token.NOTEQ);
  1468.                                 inPos++;
  1469.                             }
  1470.                             else
  1471.                             {
  1472.                                 tokens.add(Token.GT);
  1473.                             }
  1474.                             break;
  1475.                         }

  1476.                         case '=':
  1477.                         {
  1478.                             Character nextChar = rule.charAt(++inPos);
  1479.                             if ('<' == nextChar)
  1480.                             {
  1481.                                 tokens.add(Token.LEEQ);
  1482.                                 inPos++;
  1483.                             }
  1484.                             else if ('>' == nextChar)
  1485.                             {
  1486.                                 tokens.add(Token.GTEQ);
  1487.                                 inPos++;
  1488.                             }
  1489.                             else
  1490.                             {
  1491.                                 tokens.add(Token.EQ);
  1492.                             }
  1493.                             break;
  1494.                         }

  1495.                         case '(':
  1496.                         {
  1497.                             inPos++;
  1498.                             tokens.add(Token.OPEN_PAREN);
  1499.                             state = ParserState.MAY_UMINUS;
  1500.                             break;
  1501.                         }

  1502.                         default:
  1503.                         {
  1504.                             if ('S' == character)
  1505.                             {
  1506.                                 tokens.add(Token.START);
  1507.                                 inPos++;
  1508.                             }
  1509.                             else if ('E' == character)
  1510.                             {
  1511.                                 tokens.add(Token.END);
  1512.                                 inPos++;
  1513.                             }
  1514.                             NameAndStream nas = new NameAndStream(rule.substring(inPos), locationDescription);
  1515.                             inPos += nas.getNumberOfChars();
  1516.                             if (nas.isNegated())
  1517.                             {
  1518.                                 tokens.add(Token.NEG_VARIABLE);
  1519.                             }
  1520.                             else
  1521.                             {
  1522.                                 tokens.add(Token.VARIABLE);
  1523.                             }
  1524.                             Variable variable = installVariable(nas.getName(), nas.getStream(), EnumSet.noneOf(Flags.class),
  1525.                                     locationDescription);
  1526.                             variable.incrementReferenceCount();
  1527.                             tokens.add(variable);
  1528.                         }
  1529.                     }
  1530.                     break;
  1531.                 }
  1532.                 default:
  1533.                     throw new TrafficControlException("Error: bad switch; case " + state + " should not happen");
  1534.             }
  1535.         }
  1536.         return tokens.toArray();
  1537.     }

  1538.     /**
  1539.      * Check if a String begins with the text of a supplied String (ignoring case).
  1540.      * @param sought String; the sought pattern (NOT a regular expression)
  1541.      * @param supplied String; the String that might start with the sought string
  1542.      * @return boolean; true if the supplied String begins with the sought String (case insensitive)
  1543.      */
  1544.     private boolean stringBeginsWithIgnoreCase(final String sought, final String supplied)
  1545.     {
  1546.         if (sought.length() > supplied.length())
  1547.         {
  1548.             return false;
  1549.         }
  1550.         return (sought.equalsIgnoreCase(supplied.substring(0, sought.length())));
  1551.     }

  1552.     /**
  1553.      * Generate the key for a variable name and stream for use in this.variables.
  1554.      * @param name String; name of the variable
  1555.      * @param stream short; stream of the variable
  1556.      * @return String
  1557.      */
  1558.     private String variableKey(final String name, final short stream)
  1559.     {
  1560.         if (name.startsWith("D"))
  1561.         {
  1562.             return String.format("D%02d%s", stream, name.substring(1));
  1563.         }
  1564.         return String.format("%s%02d", name.toUpperCase(Locale.US), stream);
  1565.     }

  1566.     /**
  1567.      * Lookup or create a new Variable.
  1568.      * @param name String; name of the variable
  1569.      * @param stream short; stream number of the variable
  1570.      * @param flags EnumSet&lt;Flags&gt;; some (possibly empty) combination of Flags.HAS_START_RULE and Flags.HAS_END_RULE; no
  1571.      *            other flags are allowed
  1572.      * @param location String; description of the location in the TrafCOD file that triggered the call to this method
  1573.      * @return Variable; the new (or already existing) variable
  1574.      * @throws TrafficControlException if the variable already exists and already has (one of) the specified flag(s)
  1575.      */
  1576.     private Variable installVariable(final String name, final short stream, final EnumSet<Flags> flags, final String location)
  1577.             throws TrafficControlException
  1578.     {
  1579.         EnumSet<Flags> forbidden = EnumSet.complementOf(EnumSet.of(Flags.HAS_START_RULE, Flags.HAS_END_RULE));
  1580.         EnumSet<Flags> badFlags = EnumSet.copyOf(forbidden);
  1581.         badFlags.retainAll(flags);
  1582.         if (badFlags.size() > 0)
  1583.         {
  1584.             throw new TrafficControlException("installVariable was called with wrong flag(s): " + badFlags);
  1585.         }
  1586.         String key = variableKey(name, stream);
  1587.         Variable variable = this.variables.get(key);
  1588.         if (null == variable)
  1589.         {
  1590.             // Create and install a new variable
  1591.             variable = new Variable(name, stream, this);
  1592.             this.variables.put(key, variable);
  1593.             this.variablesInDefinitionOrder.add(variable);
  1594.             if (variable.isDetector())
  1595.             {
  1596.                 this.detectors.put(key, variable);
  1597.             }
  1598.         }
  1599.         if (flags.contains(Flags.HAS_START_RULE))
  1600.         {
  1601.             variable.setStartSource(location);
  1602.         }
  1603.         if (flags.contains(Flags.HAS_END_RULE))
  1604.         {
  1605.             variable.setEndSource(location);
  1606.         }
  1607.         return variable;
  1608.     }

  1609.     /**
  1610.      * Retrieve the simulator.
  1611.      * @return SimulatorInterface&lt;Time, Duration, SimTimeDoubleUnit&gt;
  1612.      */
  1613.     public OtsSimulatorInterface getSimulator()
  1614.     {
  1615.         return this.simulator;
  1616.     }

  1617.     /**
  1618.      * Retrieve the structure number.
  1619.      * @return int; the structureNumber
  1620.      */
  1621.     public int getStructureNumber()
  1622.     {
  1623.         return this.structureNumber;
  1624.     }

  1625.     /** {@inheritDoc} */
  1626.     @Override
  1627.     public void updateDetector(final String detectorId, final boolean detectingGTU)
  1628.     {
  1629.         Variable detector = this.detectors.get(detectorId);
  1630.         detector.setValue(detectingGTU ? 1 : 0, this.currentTime10,
  1631.                 new CausePrinter(
  1632.                         String.format("Detector %s becoming %s", detectorId, (detectingGTU ? "occupied" : "unoccupied"))),
  1633.                 this);
  1634.     }

  1635.     /**
  1636.      * Switch tracing of all variables of a particular traffic stream, or all variables that do not have an associated traffic
  1637.      * stream on or off.
  1638.      * @param stream int; the traffic stream number, or <code>TrafCOD.NO_STREAM</code> to affect all variables that do not have
  1639.      *            an associated traffic stream
  1640.      * @param trace boolean; if true; switch on tracing; if false; switch off tracing
  1641.      */
  1642.     public void traceVariablesOfStream(final int stream, final boolean trace)
  1643.     {
  1644.         for (Variable v : this.variablesInDefinitionOrder)
  1645.         {
  1646.             if (v.getStream() == stream)
  1647.             {
  1648.                 if (trace)
  1649.                 {
  1650.                     v.setFlag(Flags.TRACED);
  1651.                 }
  1652.                 else
  1653.                 {
  1654.                     v.clearFlag(Flags.TRACED);
  1655.                 }
  1656.             }
  1657.         }
  1658.     }

  1659.     /**
  1660.      * Switch tracing of one variable on or off.
  1661.      * @param variableName String; name of the variable
  1662.      * @param stream int; traffic stream of the variable, or <code>TrafCOD.NO_STREAM</code> to select a variable that does not
  1663.      *            have an associated traffic stream
  1664.      * @param trace boolean; if true; switch on tracing; if false; switch off tracing
  1665.      */
  1666.     public void traceVariable(final String variableName, final int stream, final boolean trace)
  1667.     {
  1668.         for (Variable v : this.variablesInDefinitionOrder)
  1669.         {
  1670.             if (v.getStream() == stream && variableName.equals(v.getName()))
  1671.             {
  1672.                 if (trace)
  1673.                 {
  1674.                     v.setFlag(Flags.TRACED);
  1675.                 }
  1676.                 else
  1677.                 {
  1678.                     v.clearFlag(Flags.TRACED);
  1679.                 }
  1680.             }
  1681.         }
  1682.     }

  1683.     /** {@inheritDoc} */
  1684.     @Override
  1685.     public void notify(final Event event) throws RemoteException
  1686.     {
  1687.         System.out.println("TrafCOD: received an event");
  1688.         if (event.getType().equals(TrafficController.TRAFFICCONTROL_SET_TRACING))
  1689.         {
  1690.             Object content = event.getContent();
  1691.             if (!(content instanceof Object[]))
  1692.             {
  1693.                 System.err.println("TrafCOD controller " + getId() + " received event with bad payload (" + content + ")");
  1694.                 return;
  1695.             }
  1696.             Object[] fields = (Object[]) event.getContent();
  1697.             if (getId().equals(fields[0]))
  1698.             {
  1699.                 if (fields.length < 4 || !(fields[1] instanceof String) || !(fields[2] instanceof Integer)
  1700.                         || !(fields[3] instanceof Boolean))
  1701.                 {
  1702.                     System.err.println("TrafCOD controller " + getId() + " received event with bad payload (" + content + ")");
  1703.                     return;
  1704.                 }
  1705.                 String name = (String) fields[1];
  1706.                 int stream = (Integer) fields[2];
  1707.                 boolean trace = (Boolean) fields[3];
  1708.                 if (name.length() > 1)
  1709.                 {
  1710.                     Variable v = this.variables.get(variableKey(name, (short) stream));
  1711.                     if (null == v)
  1712.                     {
  1713.                         System.err.println("Received trace notification for nonexistent variable (name=\"" + name
  1714.                                 + "\", stream=" + stream + ")");
  1715.                     }
  1716.                     if (trace)
  1717.                     {
  1718.                         v.setFlag(Flags.TRACED);
  1719.                     }
  1720.                     else
  1721.                     {
  1722.                         v.clearFlag(Flags.TRACED);
  1723.                     }
  1724.                 }
  1725.                 else
  1726.                 {
  1727.                     for (Variable v : this.variablesInDefinitionOrder)
  1728.                     {
  1729.                         if (v.getStream() == stream)
  1730.                         {
  1731.                             if (trace)
  1732.                             {
  1733.                                 v.setFlag(Flags.TRACED);
  1734.                             }
  1735.                             else
  1736.                             {
  1737.                                 v.clearFlag(Flags.TRACED);
  1738.                             }
  1739.                         }
  1740.                     }
  1741.                 }
  1742.             }
  1743.             // else: event not destined for this controller
  1744.         }

  1745.     }

  1746.     /**
  1747.      * Fire an event on behalf of this TrafCOD engine (used for tracing variable changes).
  1748.      * @param eventType EventType; the type of the event
  1749.      * @param payload Object[]; the payload of the event
  1750.      */
  1751.     void fireTrafCODEvent(final EventType eventType, final Object[] payload)
  1752.     {
  1753.         fireTimedEvent(eventType, payload, getSimulator().getSimulatorTime());
  1754.     }

  1755.     /** {@inheritDoc} */
  1756.     @Override
  1757.     public String getFullId()
  1758.     {
  1759.         return getId();
  1760.     }

  1761.     /** {@inheritDoc} */
  1762.     @Override
  1763.     public Container getDisplayContainer()
  1764.     {
  1765.         return this.displayContainer;
  1766.     }

  1767.     /** {@inheritDoc} */
  1768.     @Override
  1769.     public String toString()
  1770.     {
  1771.         return "TrafCOD [ie=" + getId() + "]";
  1772.     }

  1773. }

  1774. /**
  1775.  * Store a variable name, stream, isTimer, isNegated and number characters consumed information.
  1776.  */
  1777. class NameAndStream
  1778. {
  1779.     /** The name. */
  1780.     private final String name;

  1781.     /** The stream number. */
  1782.     private short stream = TrafficController.NO_STREAM;

  1783.     /** Number characters parsed. */
  1784.     private int numberOfChars = 0;

  1785.     /** Was a letter N consumed while parsing the name?. */
  1786.     private boolean negated = false;

  1787.     /**
  1788.      * Parse a TrafCOD identifier and extract all required information.
  1789.      * @param text String; the TrafCOD identifier (may be followed by more text)
  1790.      * @param locationDescription String; description of the location in the input file
  1791.      * @throws TrafficControlException when text is not a valid TrafCOD variable name
  1792.      */
  1793.     NameAndStream(final String text, final String locationDescription) throws TrafficControlException
  1794.     {
  1795.         int pos = 0;
  1796.         while (pos < text.length() && Character.isWhitespace(text.charAt(pos)))
  1797.         {
  1798.             pos++;
  1799.         }
  1800.         while (pos < text.length())
  1801.         {
  1802.             char character = text.charAt(pos);
  1803.             if (!Character.isLetterOrDigit(character))
  1804.             {
  1805.                 break;
  1806.             }
  1807.             pos++;
  1808.         }
  1809.         this.numberOfChars = pos;
  1810.         String trimmed = text.substring(0, pos).replaceAll(" ", "");
  1811.         if (trimmed.length() == 0)
  1812.         {
  1813.             throw new TrafficControlException("missing variable at " + locationDescription);
  1814.         }
  1815.         if (trimmed.matches("^D([Nn]?\\d\\d\\d)|(\\d\\d\\d[Nn])"))
  1816.         {
  1817.             // Handle a detector
  1818.             if (trimmed.charAt(1) == 'N' || trimmed.charAt(1) == 'n')
  1819.             {
  1820.                 // Move the 'N' to the end
  1821.                 trimmed = "D" + trimmed.substring(2, 5) + "N" + trimmed.substring(5);
  1822.                 this.negated = true;
  1823.             }
  1824.             this.name = "D" + trimmed.charAt(3);
  1825.             this.stream = (short) (10 * (trimmed.charAt(1) - '0') + trimmed.charAt(2) - '0');
  1826.             return;
  1827.         }
  1828.         StringBuilder nameBuilder = new StringBuilder();
  1829.         for (pos = 0; pos < trimmed.length(); pos++)
  1830.         {
  1831.             char nextChar = trimmed.charAt(pos);
  1832.             if (pos < trimmed.length() - 1 && Character.isDigit(nextChar) && Character.isDigit(trimmed.charAt(pos + 1))
  1833.                     && TrafficController.NO_STREAM == this.stream)
  1834.             {
  1835.                 if (0 == pos || (1 == pos && trimmed.startsWith("N")))
  1836.                 {
  1837.                     throw new TrafficControlException("Bad variable name: " + trimmed + " at " + locationDescription);
  1838.                 }
  1839.                 if (trimmed.charAt(pos - 1) == 'N')
  1840.                 {
  1841.                     // Previous N was NOT part of the name
  1842.                     nameBuilder.deleteCharAt(nameBuilder.length() - 1);
  1843.                     // Move the 'N' after the digits
  1844.                     trimmed =
  1845.                             trimmed.substring(0, pos - 1) + trimmed.substring(pos, pos + 2) + trimmed.substring(pos + 2) + "N";
  1846.                     pos--;
  1847.                 }
  1848.                 this.stream = (short) (10 * (trimmed.charAt(pos) - '0') + trimmed.charAt(pos + 1) - '0');
  1849.                 pos++;
  1850.             }
  1851.             else
  1852.             {
  1853.                 nameBuilder.append(nextChar);
  1854.             }
  1855.         }
  1856.         if (trimmed.endsWith("N"))
  1857.         {
  1858.             nameBuilder.deleteCharAt(nameBuilder.length() - 1);
  1859.             this.negated = true;
  1860.         }
  1861.         this.name = nameBuilder.toString();
  1862.     }

  1863.     /**
  1864.      * Was a negation operator ('N') embedded in the name?
  1865.      * @return boolean
  1866.      */
  1867.     public boolean isNegated()
  1868.     {
  1869.         return this.negated;
  1870.     }

  1871.     /**
  1872.      * Retrieve the stream number.
  1873.      * @return short; the stream number
  1874.      */
  1875.     public short getStream()
  1876.     {
  1877.         return this.stream;
  1878.     }

  1879.     /**
  1880.      * Retrieve the name.
  1881.      * @return String; the name (without the stream number)
  1882.      */
  1883.     public String getName()
  1884.     {
  1885.         return this.name;
  1886.     }

  1887.     /**
  1888.      * Retrieve the number of characters consumed from the input.
  1889.      * @return int; the number of characters consumed from the input
  1890.      */
  1891.     public int getNumberOfChars()
  1892.     {
  1893.         return this.numberOfChars;
  1894.     }

  1895.     /** {@inheritDoc} */
  1896.     @Override
  1897.     public String toString()
  1898.     {
  1899.         return "NameAndStream [name=" + this.name + ", stream=" + this.stream + ", numberOfChars=" + this.numberOfChars
  1900.                 + ", negated=" + this.negated + "]";
  1901.     }

  1902. }

  1903. /**
  1904.  * A TrafCOD variable, timer, or detector.
  1905.  */
  1906. class Variable implements EventListener
  1907. {
  1908.     /** ... */
  1909.     private static final long serialVersionUID = 20200313L;

  1910.     /** The TrafCOD engine. */
  1911.     private final TrafCod trafCOD;

  1912.     /** Flags. */
  1913.     private EnumSet<Flags> flags = EnumSet.noneOf(Flags.class);

  1914.     /** The current value. */
  1915.     private int value;

  1916.     /** Limit value (if this is a timer variable). */
  1917.     private int timerMax10;

  1918.     /** Output color (if this is an export variable). */
  1919.     private TrafficLightColor color;

  1920.     /** Name of this variable (without the traffic stream). */
  1921.     private final String name;

  1922.     /** Traffic stream number. */
  1923.     private final short stream;

  1924.     /** Number of rules that refer to this variable. */
  1925.     private int refCount;

  1926.     /** Time of last update in tenth of second. */
  1927.     private int updateTime10;

  1928.     /** Source of start rule. */
  1929.     private String startSource;

  1930.     /** Source of end rule. */
  1931.     private String endSource;

  1932.     /** The traffic light (only set if this Variable is an output). */
  1933.     private Set<TrafficLight> trafficLights;

  1934.     /** Letters that are used to distinguish conflict groups in the MRx variables. */
  1935.     private static String rowLetters = "ABCDXYZUVW";

  1936.     /**
  1937.      * Retrieve the number of rules that refer to this variable.
  1938.      * @return int; the number of rules that refer to this variable
  1939.      */
  1940.     public int getRefCount()
  1941.     {
  1942.         return this.refCount;
  1943.     }

  1944.     /**
  1945.      * Retrieve the traffic lights controlled by this variable.
  1946.      * @return Set&lt;TrafficLight&gt;; the traffic lights controlled by this variable, or null when this variable has no
  1947.      *         traffic lights
  1948.      */
  1949.     public Set<TrafficLight> getTrafficLights()
  1950.     {
  1951.         return this.trafficLights;
  1952.     }

  1953.     /**
  1954.      * Construct a new Variable.
  1955.      * @param name String; name of the new variable (without the stream number)
  1956.      * @param stream short; stream number to which the new Variable is associated
  1957.      * @param trafCOD TrafCOD; the TrafCOD engine
  1958.      */
  1959.     Variable(final String name, final short stream, final TrafCod trafCOD)
  1960.     {
  1961.         this.name = name.toUpperCase(Locale.US);
  1962.         this.stream = stream;
  1963.         this.trafCOD = trafCOD;
  1964.         if (this.name.startsWith("T"))
  1965.         {
  1966.             this.flags.add(Flags.IS_TIMER);
  1967.         }
  1968.         if (this.name.length() == 2 && this.name.startsWith("D") && Character.isDigit(this.name.charAt(1)))
  1969.         {
  1970.             this.flags.add(Flags.IS_DETECTOR);
  1971.         }
  1972.         if (TrafficController.NO_STREAM == stream && this.name.startsWith("MR") && this.name.length() == 3
  1973.                 && rowLetters.indexOf(this.name.charAt(2)) >= 0)
  1974.         {
  1975.             this.flags.add(Flags.CONFLICT_GROUP);
  1976.         }
  1977.     }

  1978.     /**
  1979.      * Retrieve the name of this variable.
  1980.      * @return String; the name (without the stream number) of this Variable
  1981.      */
  1982.     public String getName()
  1983.     {
  1984.         return this.name;
  1985.     }

  1986.     /**
  1987.      * Link a detector variable to a sensor.
  1988.      * @param sensor TrafficLightSensor; the sensor
  1989.      * @throws TrafficControlException when this variable is not a detector
  1990.      */
  1991.     public void subscribeToDetector(final TrafficLightDetector sensor) throws TrafficControlException
  1992.     {
  1993.         if (!isDetector())
  1994.         {
  1995.             throw new TrafficControlException("Cannot subscribe a non-detector to a TrafficLightSensor");
  1996.         }
  1997.         sensor.addListener(this, TrafficLightDetector.TRAFFIC_LIGHT_DETECTOR_TRIGGER_ENTRY_EVENT);
  1998.         sensor.addListener(this, TrafficLightDetector.TRAFFIC_LIGHT_DETECTOR_TRIGGER_EXIT_EVENT);
  1999.     }

  2000.     /**
  2001.      * Initialize this variable if it has the INITED flag set.
  2002.      */
  2003.     public void initialize()
  2004.     {
  2005.         if (this.flags.contains(Flags.INITED))
  2006.         {
  2007.             if (isTimer())
  2008.             {
  2009.                 setValue(this.timerMax10, 0, new CausePrinter("Timer initialization rule"), this.trafCOD);
  2010.             }
  2011.             else
  2012.             {
  2013.                 setValue(1, 0, new CausePrinter("Variable initialization rule"), this.trafCOD);
  2014.             }
  2015.         }
  2016.     }

  2017.     /**
  2018.      * Decrement the value of a timer.
  2019.      * @param timeStamp10 int; the current simulator time in tenths of a second
  2020.      * @return boolean; true if the timer expired due to this call; false if the timer is still running, or expired before this
  2021.      *         call
  2022.      * @throws TrafficControlException when this Variable is not a timer
  2023.      */
  2024.     public boolean decrementTimer(final int timeStamp10) throws TrafficControlException
  2025.     {
  2026.         if (!isTimer())
  2027.         {
  2028.             throw new TrafficControlException("Variable " + this + " is not a timer");
  2029.         }
  2030.         if (this.value <= 0)
  2031.         {
  2032.             return false;
  2033.         }
  2034.         if (0 == --this.value)
  2035.         {
  2036.             this.flags.add(Flags.CHANGED);
  2037.             this.flags.add(Flags.END);
  2038.             this.value = 0;
  2039.             this.updateTime10 = timeStamp10;
  2040.             if (this.flags.contains(Flags.TRACED))
  2041.             {
  2042.                 System.out.println("Timer " + toString() + " expired");
  2043.             }
  2044.             return true;
  2045.         }
  2046.         return false;
  2047.     }

  2048.     /**
  2049.      * Retrieve the color for an output Variable.
  2050.      * @return int; the color code for this Variable
  2051.      * @throws TrafficControlException if this Variable is not an output
  2052.      */
  2053.     public TrafficLightColor getColor() throws TrafficControlException
  2054.     {
  2055.         if (!this.flags.contains(Flags.IS_OUTPUT))
  2056.         {
  2057.             throw new TrafficControlException("Stream " + this.toString() + "is not an output");
  2058.         }
  2059.         return this.color;
  2060.     }

  2061.     /**
  2062.      * Report whether a change in this variable must be published.
  2063.      * @return boolean; true if this Variable is an output; false if this Variable is not an output
  2064.      */
  2065.     public boolean isOutput()
  2066.     {
  2067.         return this.flags.contains(Flags.IS_OUTPUT);
  2068.     }

  2069.     /**
  2070.      * Report of this Variable identifies the current conflict group.
  2071.      * @return boolean; true if this Variable identifies the current conflict group; false if it does not.
  2072.      */
  2073.     public boolean isConflictGroup()
  2074.     {
  2075.         return this.flags.contains(Flags.CONFLICT_GROUP);
  2076.     }

  2077.     /**
  2078.      * Retrieve the rank of the conflict group that this Variable represents.
  2079.      * @return int; the rank of the conflict group that this Variable represents
  2080.      * @throws TrafficControlException if this Variable is not a conflict group identifier
  2081.      */
  2082.     public int conflictGroupRank() throws TrafficControlException
  2083.     {
  2084.         if (!isConflictGroup())
  2085.         {
  2086.             throw new TrafficControlException("Variable " + this + " is not a conflict group identifier");
  2087.         }
  2088.         return rowLetters.indexOf(this.name.charAt(2));
  2089.     }

  2090.     /**
  2091.      * Report if this Variable is a detector.
  2092.      * @return boolean; true if this Variable is a detector; false if this Variable is not a detector
  2093.      */
  2094.     public boolean isDetector()
  2095.     {
  2096.         return this.flags.contains(Flags.IS_DETECTOR);
  2097.     }

  2098.     /**
  2099.      * @param newValue int; the new value of this Variable
  2100.      * @param timeStamp10 int; the time stamp of this update
  2101.      * @param cause CausePrinter; rule, timer, or detector that caused the change
  2102.      * @param trafCODController TrafCOD; the TrafCOD controller
  2103.      * @return boolean; true if the value of this variable changed
  2104.      */
  2105.     public boolean setValue(final int newValue, final int timeStamp10, final CausePrinter cause,
  2106.             final TrafCod trafCODController)
  2107.     {
  2108.         boolean result = false;
  2109.         if (this.value != newValue)
  2110.         {
  2111.             this.updateTime10 = timeStamp10;
  2112.             setFlag(Flags.CHANGED);
  2113.             if (0 == newValue)
  2114.             {
  2115.                 setFlag(Flags.END);
  2116.                 result = true;
  2117.             }
  2118.             else if (!isTimer() || 0 == this.value)
  2119.             {
  2120.                 setFlag(Flags.START);
  2121.                 result = true;
  2122.             }
  2123.             if (isOutput() && newValue != 0)
  2124.             {
  2125.                 for (TrafficLight trafficLight : this.trafficLights)
  2126.                 {
  2127.                     trafficLight.setTrafficLightColor(this.color);
  2128.                 }
  2129.             }
  2130.         }
  2131.         if (this.flags.contains(Flags.TRACED))
  2132.         {
  2133.             // System.out.println("Variable " + this.name + this.stream + " changes from " + this.value + " to " + newValue
  2134.             // + " due to " + cause.toString());
  2135.             trafCODController.fireTrafCODEvent(TrafficController.TRAFFICCONTROL_TRACED_VARIABLE_UPDATED,
  2136.                     new Object[] {trafCODController.getId(), toString(EnumSet.of(PrintFlags.ID)), this.stream, this.value,
  2137.                             newValue, cause.toString()});
  2138.         }
  2139.         this.value = newValue;
  2140.         return result;
  2141.     }

  2142.     /**
  2143.      * Copy the state of this variable from another variable. Only used when cloning the TrafCOD engine.
  2144.      * @param fromVariable Variable; the variable whose state is copied
  2145.      * @param newNetwork Network; the Network that contains the new traffic control engine
  2146.      * @throws NetworkException when the clone of a traffic light of fromVariable does not exist in newNetwork
  2147.      */
  2148.     public void cloneState(final Variable fromVariable, final Network newNetwork) throws NetworkException
  2149.     {
  2150.         this.value = fromVariable.value;
  2151.         this.flags = EnumSet.copyOf(fromVariable.flags);
  2152.         this.updateTime10 = fromVariable.updateTime10;
  2153.         if (fromVariable.isOutput())
  2154.         {
  2155.             for (TrafficLight tl : fromVariable.trafficLights)
  2156.             {
  2157.                 LocatedObject clonedTrafficLight = newNetwork.getObjectMap().get(tl.getId());
  2158.                 if (null != clonedTrafficLight)
  2159.                 {
  2160.                     throw new NetworkException("newNetwork does not contain a clone of traffic light " + tl.getId());
  2161.                 }
  2162.                 if (clonedTrafficLight instanceof TrafficLight)
  2163.                 {
  2164.                     throw new NetworkException(
  2165.                             "newNetwork contains an object with name " + tl.getId() + " but this object is not a TrafficLight");
  2166.                 }
  2167.                 this.trafficLights.add((TrafficLight) clonedTrafficLight);
  2168.             }
  2169.         }
  2170.         if (isOutput())
  2171.         {
  2172.             for (TrafficLight trafficLight : this.trafficLights)
  2173.             {
  2174.                 trafficLight.setTrafficLightColor(this.color);
  2175.             }
  2176.         }
  2177.     }

  2178.     /**
  2179.      * Retrieve the start value of this timer in units of 0.1 seconds (1 second is represented by the value 10).
  2180.      * @return int; the timerMax10 value
  2181.      * @throws TrafficControlException when this class is not a Timer
  2182.      */
  2183.     public int getTimerMax() throws TrafficControlException
  2184.     {
  2185.         if (!this.isTimer())
  2186.         {
  2187.             throw new TrafficControlException("This is not a timer");
  2188.         }
  2189.         return this.timerMax10;
  2190.     }

  2191.     /**
  2192.      * Retrieve the current value of this Variable.
  2193.      * @return int; the value of this Variable
  2194.      */
  2195.     public int getValue()
  2196.     {
  2197.         return this.value;
  2198.     }

  2199.     /**
  2200.      * Set one flag.
  2201.      * @param flag Flags; Flags
  2202.      */
  2203.     public void setFlag(final Flags flag)
  2204.     {
  2205.         this.flags.add(flag);
  2206.     }

  2207.     /**
  2208.      * Clear one flag.
  2209.      * @param flag Flags; the flag to clear
  2210.      */
  2211.     public void clearFlag(final Flags flag)
  2212.     {
  2213.         this.flags.remove(flag);
  2214.     }

  2215.     /**
  2216.      * Report whether this Variable is a timer.
  2217.      * @return boolean; true if this Variable is a timer; false if this variable is not a timer
  2218.      */
  2219.     public boolean isTimer()
  2220.     {
  2221.         return this.flags.contains(Flags.IS_TIMER);
  2222.     }

  2223.     /**
  2224.      * Clear the CHANGED flag of this Variable.
  2225.      */
  2226.     public void clearChangedFlag()
  2227.     {
  2228.         this.flags.remove(Flags.CHANGED);
  2229.     }

  2230.     /**
  2231.      * Increment the reference counter of this variable. The reference counter counts the number of rules where this variable
  2232.      * occurs on the right hand side of the assignment operator.
  2233.      */
  2234.     public void incrementReferenceCount()
  2235.     {
  2236.         this.refCount++;
  2237.     }

  2238.     /**
  2239.      * Return a safe copy of the flags.
  2240.      * @return EnumSet&lt;Flags&gt;
  2241.      */
  2242.     public EnumSet<Flags> getFlags()
  2243.     {
  2244.         return EnumSet.copyOf(this.flags);
  2245.     }

  2246.     /**
  2247.      * Make this variable an output variable and set the color value.
  2248.      * @param colorValue int; the output value (as used in the TrafCOD file)
  2249.      * @throws TrafficControlException when the colorValue is invalid, or this method is called more than once for this variable
  2250.      */
  2251.     public void setOutput(final int colorValue) throws TrafficControlException
  2252.     {
  2253.         if (null != this.color)
  2254.         {
  2255.             throw new TrafficControlException("setOutput has already been called for " + this);
  2256.         }
  2257.         if (null == this.trafficLights)
  2258.         {
  2259.             this.trafficLights = new LinkedHashSet<>();
  2260.         }
  2261.         // Convert the TrafCOD color value to the corresponding TrafficLightColor
  2262.         TrafficLightColor newColor;
  2263.         switch (colorValue)
  2264.         {
  2265.             case 'R':
  2266.                 newColor = TrafficLightColor.RED;
  2267.                 break;
  2268.             case 'G':
  2269.                 newColor = TrafficLightColor.GREEN;
  2270.                 break;
  2271.             case 'Y':
  2272.                 newColor = TrafficLightColor.YELLOW;
  2273.                 break;
  2274.             default:
  2275.                 throw new TrafficControlException("Bad color value: " + colorValue);
  2276.         }
  2277.         this.color = newColor;
  2278.         this.flags.add(Flags.IS_OUTPUT);
  2279.     }

  2280.     /**
  2281.      * Add a traffic light to this variable.
  2282.      * @param trafficLight TrafficLight; the traffic light to add
  2283.      * @throws TrafficControlException when this variable is not an output
  2284.      */
  2285.     public void addOutput(final TrafficLight trafficLight) throws TrafficControlException
  2286.     {
  2287.         if (!this.isOutput())
  2288.         {
  2289.             throw new TrafficControlException("Cannot add an output to an non-output variable");
  2290.         }
  2291.         this.trafficLights.add(trafficLight);
  2292.     }

  2293.     /**
  2294.      * Set the maximum time of this timer.
  2295.      * @param value10 int; the maximum time in 0.1 s
  2296.      * @throws TrafficControlException when this Variable is not a timer
  2297.      */
  2298.     public void setTimerMax(final int value10) throws TrafficControlException
  2299.     {
  2300.         if (!this.flags.contains(Flags.IS_TIMER))
  2301.         {
  2302.             throw new TrafficControlException(
  2303.                     "Cannot set maximum timer value of " + this.toString() + " because this is not a timer");
  2304.         }
  2305.         this.timerMax10 = value10;
  2306.     }

  2307.     /**
  2308.      * Describe the rule that starts this variable.
  2309.      * @return String
  2310.      */
  2311.     public String getStartSource()
  2312.     {
  2313.         return this.startSource;
  2314.     }

  2315.     /**
  2316.      * Set the description of the rule that starts this variable.
  2317.      * @param startSource String; description of the rule that starts this variable
  2318.      * @throws TrafficControlException when a start source has already been set
  2319.      */
  2320.     public void setStartSource(final String startSource) throws TrafficControlException
  2321.     {
  2322.         if (null != this.startSource)
  2323.         {
  2324.             throw new TrafficControlException("Conflicting rules: " + this.startSource + " vs " + startSource);
  2325.         }
  2326.         this.startSource = startSource;
  2327.         this.flags.add(Flags.HAS_START_RULE);
  2328.     }

  2329.     /**
  2330.      * Describe the rule that ends this variable.
  2331.      * @return String
  2332.      */
  2333.     public String getEndSource()
  2334.     {
  2335.         return this.endSource;
  2336.     }

  2337.     /**
  2338.      * Set the description of the rule that ends this variable.
  2339.      * @param endSource String; description of the rule that ends this variable
  2340.      * @throws TrafficControlException when an end source has already been set
  2341.      */
  2342.     public void setEndSource(final String endSource) throws TrafficControlException
  2343.     {
  2344.         if (null != this.endSource)
  2345.         {
  2346.             throw new TrafficControlException("Conflicting rules: " + this.startSource + " vs " + endSource);
  2347.         }
  2348.         this.endSource = endSource;
  2349.         this.flags.add(Flags.HAS_END_RULE);
  2350.     }

  2351.     /**
  2352.      * Retrieve the stream to which this variable belongs.
  2353.      * @return short; the stream to which this variable belongs
  2354.      */
  2355.     public short getStream()
  2356.     {
  2357.         return this.stream;
  2358.     }

  2359.     /** {@inheritDoc} */
  2360.     @Override
  2361.     public String toString()
  2362.     {
  2363.         return "Variable [" + toString(EnumSet.of(PrintFlags.ID, PrintFlags.VALUE, PrintFlags.FLAGS)) + "]";
  2364.     }

  2365.     /**
  2366.      * Convert selected fields to a String.
  2367.      * @param printFlags EnumSet&lt;PrintFlags&gt;; the set of fields to convert
  2368.      * @return String
  2369.      */
  2370.     public String toString(final EnumSet<PrintFlags> printFlags)
  2371.     {
  2372.         StringBuilder result = new StringBuilder();
  2373.         if (printFlags.contains(PrintFlags.ID))
  2374.         {
  2375.             if (this.flags.contains(Flags.IS_DETECTOR))
  2376.             {
  2377.                 result.append("D");
  2378.             }
  2379.             else if (isTimer() && printFlags.contains(PrintFlags.INITTIMER))
  2380.             {
  2381.                 result.append("I");
  2382.                 result.append(this.name);
  2383.             }
  2384.             else if (isTimer() && printFlags.contains(PrintFlags.REINITTIMER))
  2385.             {
  2386.                 result.append("RI");
  2387.                 result.append(this.name);
  2388.             }
  2389.             else
  2390.             {
  2391.                 result.append(this.name);
  2392.             }
  2393.             if (this.stream > 0)
  2394.             {
  2395.                 // Insert the stream BEFORE the first digit in the name (if any); otherwise append
  2396.                 int pos;
  2397.                 for (pos = 0; pos < result.length(); pos++)
  2398.                 {
  2399.                     if (Character.isDigit(result.charAt(pos)))
  2400.                     {
  2401.                         break;
  2402.                     }
  2403.                 }
  2404.                 result.insert(pos, String.format("%02d", this.stream));
  2405.             }
  2406.             if (this.flags.contains(Flags.IS_DETECTOR))
  2407.             {
  2408.                 result.append(this.name.substring(1));
  2409.             }
  2410.             if (printFlags.contains(PrintFlags.NEGATED))
  2411.             {
  2412.                 result.append("N");
  2413.             }
  2414.         }
  2415.         int printValue = Integer.MIN_VALUE; // That value should stand out if not changed by the code below this line.
  2416.         if (printFlags.contains(PrintFlags.VALUE))
  2417.         {
  2418.             if (printFlags.contains(PrintFlags.NEGATED))
  2419.             {
  2420.                 printValue = 0 == this.value ? 1 : 0;
  2421.             }
  2422.             else
  2423.             {
  2424.                 printValue = this.value;
  2425.             }
  2426.             if (printFlags.contains(PrintFlags.S))
  2427.             {
  2428.                 if (this.flags.contains(Flags.START))
  2429.                 {
  2430.                     printValue = 1;
  2431.                 }
  2432.                 else
  2433.                 {
  2434.                     printValue = 0;
  2435.                 }
  2436.             }
  2437.             if (printFlags.contains(PrintFlags.E))
  2438.             {
  2439.                 if (this.flags.contains(Flags.END))
  2440.                 {
  2441.                     printValue = 1;
  2442.                 }
  2443.                 else
  2444.                 {
  2445.                     printValue = 0;
  2446.                 }
  2447.             }
  2448.         }
  2449.         if (printFlags.contains(PrintFlags.VALUE) || printFlags.contains(PrintFlags.S) || printFlags.contains(PrintFlags.E)
  2450.                 || printFlags.contains(PrintFlags.FLAGS))
  2451.         {
  2452.             result.append("<");
  2453.             if (printFlags.contains(PrintFlags.VALUE) || printFlags.contains(PrintFlags.S) || printFlags.contains(PrintFlags.E))
  2454.             {
  2455.                 result.append(printValue);
  2456.             }
  2457.             if (printFlags.contains(PrintFlags.FLAGS))
  2458.             {
  2459.                 if (this.flags.contains(Flags.START))
  2460.                 {
  2461.                     result.append("S");
  2462.                 }
  2463.                 if (this.flags.contains(Flags.END))
  2464.                 {
  2465.                     result.append("E");
  2466.                 }
  2467.             }
  2468.             result.append(">");
  2469.         }
  2470.         if (printFlags.contains(PrintFlags.MODIFY_TIME))
  2471.         {
  2472.             result.append(String.format(" (%d.%d)", this.updateTime10 / 10, this.updateTime10 % 10));
  2473.         }
  2474.         return result.toString();
  2475.     }

  2476.     /** {@inheritDoc} */
  2477.     @Override
  2478.     public void notify(final Event event) throws RemoteException
  2479.     {
  2480.         if (event.getType().equals(TrafficLightDetector.TRAFFIC_LIGHT_DETECTOR_TRIGGER_ENTRY_EVENT))
  2481.         {
  2482.             setValue(1, this.updateTime10, new CausePrinter("Detector became occupied"), this.trafCOD);
  2483.         }
  2484.         else if (event.getType().equals(TrafficLightDetector.TRAFFIC_LIGHT_DETECTOR_TRIGGER_EXIT_EVENT))
  2485.         {
  2486.             setValue(0, this.updateTime10, new CausePrinter("Detector became unoccupied"), this.trafCOD);
  2487.         }
  2488.     }

  2489. }

  2490. /**
  2491.  * Class that can print a text version describing why a variable changed. Any work that has to be done (such as a call to
  2492.  * <code>TrafCOD.printRule</code>) is deferred until the <code>toString</code> method is called.
  2493.  */
  2494. class CausePrinter
  2495. {
  2496.     /** Object that describes the cause of the variable change. */
  2497.     private final Object cause;

  2498.     /**
  2499.      * Construct a new CausePrinter object.
  2500.      * @param cause Object; this should be either a String, or a Object[] that contains a tokenized TrafCOD rule.
  2501.      */
  2502.     CausePrinter(final Object cause)
  2503.     {
  2504.         this.cause = cause;
  2505.     }

  2506.     @Override
  2507.     public String toString()
  2508.     {
  2509.         if (this.cause instanceof String)
  2510.         {
  2511.             return (String) this.cause;
  2512.         }
  2513.         else if (this.cause instanceof Object[])
  2514.         {
  2515.             try
  2516.             {
  2517.                 return TrafCod.printRule((Object[]) this.cause, true);
  2518.             }
  2519.             catch (TrafficControlException exception)
  2520.             {
  2521.                 exception.printStackTrace();
  2522.                 return ("printRule failed");
  2523.             }
  2524.         }
  2525.         return this.cause.toString();
  2526.     }
  2527. }

  2528. /**
  2529.  * Flags for toString method of a Variable.
  2530.  */
  2531. enum PrintFlags
  2532. {
  2533.     /** The name and stream of the Variable. */
  2534.     ID,
  2535.     /** The value of the Variable. */
  2536.     VALUE,
  2537.     /** Print "I" before the name (indicates that a timer is initialized). */
  2538.     INITTIMER,
  2539.     /** Print "RI" before the name (indicates that a timer is re-initialized). */
  2540.     REINITTIMER,
  2541.     /** Print value as "1" if just set, else print "0". */
  2542.     S,
  2543.     /** Print value as "1" if just reset, else print "0". */
  2544.     E,
  2545.     /** Print the negated Variable. */
  2546.     NEGATED,
  2547.     /** Print the flags of the Variable. */
  2548.     FLAGS,
  2549.     /** Print the time of last modification of the Variable. */
  2550.     MODIFY_TIME,
  2551. }

  2552. /**
  2553.  * Flags of a TrafCOD variable.
  2554.  */
  2555. enum Flags
  2556. {
  2557.     /** Variable becomes active. */
  2558.     START,
  2559.     /** Variable becomes inactive. */
  2560.     END,
  2561.     /** Timer has just expired. */
  2562.     TIMEREXPIRED,
  2563.     /** Variable has just changed value. */
  2564.     CHANGED,
  2565.     /** Variable is a timer. */
  2566.     IS_TIMER,
  2567.     /** Variable is a detector. */
  2568.     IS_DETECTOR,
  2569.     /** Variable has a start rule. */
  2570.     HAS_START_RULE,
  2571.     /** Variable has an end rule. */
  2572.     HAS_END_RULE,
  2573.     /** Variable is an output. */
  2574.     IS_OUTPUT,
  2575.     /** Variable must be initialized to 1 at start of control program. */
  2576.     INITED,
  2577.     /** Variable is traced; all changes must be printed. */
  2578.     TRACED,
  2579.     /** Variable identifies the currently active conflict group. */
  2580.     CONFLICT_GROUP,
  2581. }