View Javadoc
1   package org.opentrafficsim.trafficcontrol;
2   
3   import java.rmi.RemoteException;
4   import java.util.ArrayList;
5   import java.util.Collections;
6   import java.util.LinkedHashMap;
7   import java.util.LinkedHashSet;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.Set;
11  
12  import org.djunits.value.vdouble.scalar.Duration;
13  import org.djunits.value.vdouble.scalar.Time;
14  import org.djutils.exceptions.Throw;
15  import org.djutils.immutablecollections.Immutable;
16  import org.djutils.immutablecollections.ImmutableArrayList;
17  import org.djutils.immutablecollections.ImmutableHashSet;
18  import org.djutils.immutablecollections.ImmutableList;
19  import org.djutils.immutablecollections.ImmutableMap;
20  import org.djutils.immutablecollections.ImmutableSet;
21  import org.opentrafficsim.base.Identifiable;
22  import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
23  import org.opentrafficsim.core.network.Network;
24  import org.opentrafficsim.core.network.NetworkException;
25  import org.opentrafficsim.core.object.InvisibleObjectInterface;
26  import org.opentrafficsim.road.network.lane.object.trafficlight.TrafficLight;
27  import org.opentrafficsim.road.network.lane.object.trafficlight.TrafficLightColor;
28  
29  import nl.tudelft.simulation.dsol.SimRuntimeException;
30  import nl.tudelft.simulation.event.EventInterface;
31  
32  /**
33   * Fixed time traffic light control.
34   * <p>
35   * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
36   * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
37   * <p>
38   * @version $Revision$, $LastChangedDate$, by $Author$, initial version 21 feb. 2019 <br>
39   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
40   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
41   * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
42   */
43  public class FixedTimeController extends AbstractTrafficController
44  {
45  
46      /** */
47      private static final long serialVersionUID = 20190221L;
48  
49      /** Cycle time. */
50      private final Duration cycleTime;
51  
52      /** Offset. */
53      private final Duration offset;
54  
55      /** Signal groups, for cloning. */
56      private final Set<SignalGroup> signalGroups;
57  
58      /**
59       * Constructor for fixed time traffic controller.
60       * @param id String; id
61       * @param simulator OTSSimulatorInterface; simulator
62       * @param network Network; network
63       * @param offset Duration; off set from simulation start time
64       * @param cycleTime Duration; cycle time
65       * @param signalGroups Set&lt;SignalGroup&gt;; signal groups
66       * @throws SimRuntimeException simulator is past zero time
67       */
68      public FixedTimeController(final String id, final OTSSimulatorInterface simulator, final Network network,
69              final Duration cycleTime, final Duration offset, final Set<SignalGroup> signalGroups) throws SimRuntimeException
70      {
71          super(id, simulator);
72          Throw.whenNull(simulator, "Simulator may not be null.");
73          Throw.whenNull(network, "Network may not be null.");
74          Throw.whenNull(cycleTime, "Cycle time may not be null.");
75          Throw.whenNull(offset, "Offset may not be null.");
76          Throw.whenNull(signalGroups, "Signal groups may not be null.");
77          Throw.when(cycleTime.le0(), IllegalArgumentException.class, "Cycle time must be positive.");
78          // Throw.when(signalGroups.isEmpty(), IllegalArgumentException.class, "Signal groups may not be empty.");
79          /*- This is no longer considered an error
80          for (SignalGroup signalGroup1 : signalGroups)
81          {
82              for (SignalGroup signalGroup2 : signalGroups)
83              {
84                  if (!signalGroup1.equals(signalGroup2))
85                  {
86                      Throw.when(!ImmutableCollections.disjoint(signalGroup1.trafficLightIds, signalGroup2.trafficLightIds),
87                              IllegalArgumentException.class, "A traffic light is in both signal group %s and signal group %s.",
88                              signalGroup1.getId(), signalGroup2.getId());
89                  }
90              }
91          }
92          */
93          this.cycleTime = cycleTime;
94          this.offset = offset;
95          this.signalGroups = new LinkedHashSet<>(signalGroups); // make a copy so we can modify it.
96          // Identify traffic lights that are present in more than one signal group
97          Map<String, List<SignalGroup>> signalGroupsOfTrafficLight = new LinkedHashMap<>();
98          for (SignalGroup sg : this.signalGroups)
99          {
100             for (String trafficLightId : sg.getTrafficLightIds())
101             {
102                 List<SignalGroup> sgList = signalGroupsOfTrafficLight.get(trafficLightId);
103                 if (null == sgList)
104                 {
105                     sgList = new ArrayList<>();
106                     signalGroupsOfTrafficLight.put(trafficLightId, sgList);
107                 }
108                 sgList.add(sg);
109             }
110         }
111         // Collect all flanks that persist for nonzero duration
112         int nextNumber = 0;
113         for (String trafficLightId : signalGroupsOfTrafficLight.keySet())
114         {
115             List<SignalGroup> sgList = signalGroupsOfTrafficLight.get(trafficLightId);
116             if (sgList.size() > 1)
117             {
118                 // System.out.println("Signal " + trafficLightId + " is used in multiple signal groups");
119                 // Check for overlapping or adjacent green phases
120                 List<Flank> flanks = new ArrayList<>();
121                 for (SignalGroup sg : sgList)
122                 {
123                     double sgOffset = sg.getOffset().si;
124                     double preGreenDuration = sg.getPreGreen().si;
125                     if (preGreenDuration > 0)
126                     {
127                         flanks.add(new Flank(sgOffset % this.cycleTime.si, TrafficLightColor.PREGREEN));
128                         sgOffset += preGreenDuration;
129                     }
130                     flanks.add(new Flank(sgOffset % this.cycleTime.si, TrafficLightColor.GREEN));
131                     sgOffset += sg.getGreen().si;
132                     double yellowDuration = sg.getYellow().si;
133                     if (yellowDuration > 0)
134                     {
135                         flanks.add(new Flank(sgOffset % this.cycleTime.si, TrafficLightColor.YELLOW));
136                         sgOffset += yellowDuration;
137                     }
138                     flanks.add(new Flank(sgOffset % this.cycleTime.si, TrafficLightColor.RED));
139                 }
140                 Collections.sort(flanks);
141                 /*-
142                 System.out.println("Collected " + flanks.size() + " flanks:");
143                 for (Flank flank : flanks)
144                 {
145                     System.out.println(flank);
146                 }
147                 */
148                 boolean combined = false;
149                 int greenCount = 0;
150                 for (int index = 0; index < flanks.size(); index++)
151                 {
152                     Flank flank = flanks.get(index);
153                     TrafficLightColor nextColor = flank.getTrafficLightColor();
154                     if (TrafficLightColor.GREEN == nextColor)
155                     {
156                         greenCount++;
157                         if (greenCount > 1)
158                         {
159                             flanks.remove(index);
160                             index--;
161                             combined = true;
162                             continue;
163                         }
164                     }
165                     else if (TrafficLightColor.YELLOW == nextColor)
166                     {
167                         if (greenCount > 1)
168                         {
169                             flanks.remove(index);
170                             index--;
171                             continue;
172                         }
173                     }
174                     else if (TrafficLightColor.RED == nextColor)
175                     {
176                         greenCount--;
177                         if (greenCount > 0)
178                         {
179                             flanks.remove(index);
180                             index--;
181                             continue;
182                         }
183                     }
184                 }
185                 /*-
186                 System.out.println("Reduced " + flanks.size() + " flanks:");
187                 for (Flank flank : flanks)
188                 {
189                     System.out.println(flank);
190                 }
191                 */
192                 if (combined)
193                 {
194                     // Traffic light has adjacent or overlapping green realizations.
195                     String newSignalGroupName = "CombinedSignalGroups_";
196                     // Remove the traffic light from the current signal groups that it is part of
197                     for (SignalGroup sg : sgList)
198                     {
199                         // System.out.println("Reducing " + sg);
200                         newSignalGroupName = newSignalGroupName + "_" + sg.getId();
201                         Set<String> trafficLightIds = new LinkedHashSet<>();
202                         for (String tlId : sg.getTrafficLightIds())
203                         {
204                             if (!tlId.equals(trafficLightId))
205                             {
206                                 trafficLightIds.add(tlId);
207                             }
208                         }
209                         this.signalGroups.remove(sg);
210                         if (trafficLightIds.size() > 0)
211                         {
212                             SignalGroup newSignalGroup = new SignalGroup(sg.getId(), trafficLightIds, sg.getOffset(),
213                                     sg.getPreGreen(), sg.getGreen(), sg.getYellow());
214                             // System.out.println("reduced signal group " + newSignalGroup);
215                             this.signalGroups.add(newSignalGroup);
216                         }
217                         /*-
218                         else
219                         {
220                             System.out.println("Signal group became empty");
221                         }
222                         */
223                     }
224                     // Create new signal group(s) for each green realization of the traffic light
225                     Duration sgOffset = null;
226                     Duration preGreen = Duration.ZERO;
227                     Duration green = null;
228                     Duration yellow = Duration.ZERO;
229                     double cumulativeOffset = 0;
230                     for (int index = 0; index < flanks.size(); index++)
231                     {
232                         Flank flank = flanks.get(index);
233                         if (null == sgOffset)
234                         {
235                             sgOffset = Duration.instantiateSI(flank.getOffset());
236                         }
237                         if (TrafficLightColor.GREEN == flank.getTrafficLightColor())
238                         {
239                             preGreen = Duration.instantiateSI(flank.getOffset() - sgOffset.si);
240                         }
241                         if (TrafficLightColor.YELLOW == flank.getTrafficLightColor())
242                         {
243                             green = Duration.instantiateSI(flank.getOffset() - cumulativeOffset);
244                         }
245                         if (TrafficLightColor.RED == flank.getTrafficLightColor())
246                         {
247                             nextNumber++;
248                             yellow = Duration.instantiateSI(flank.getOffset() - cumulativeOffset);
249                             Set<String> trafficLightIds = new LinkedHashSet<>(1);
250                             trafficLightIds.add(trafficLightId);
251                             SignalGroup newSignalGroup = new SignalGroup(newSignalGroupName + "_" + nextNumber, trafficLightIds,
252                                     sgOffset, preGreen, green, yellow);
253                             this.signalGroups.add(newSignalGroup);
254                             // System.out.println("Created signal group " + newSignalGroup);
255                         }
256                         cumulativeOffset = flank.getOffset();
257                     }
258                 }
259             }
260         }
261         // Schedule setup at time == 0 (when the network should be fully created and all traffic lights have been constructed)
262         simulator.scheduleEventAbs(Time.ZERO, this, this, "setup", new Object[] { simulator, network });
263     }
264 
265     /**
266      * Initiates all traffic control events.
267      * @param simulator OTSSimulatorInterface; simulator
268      * @param network Network; network
269      * @throws SimRuntimeException when traffic light does not exist in the network
270      */
271     @SuppressWarnings("unused")
272     private void setup(final OTSSimulatorInterface simulator, final Network network) throws SimRuntimeException
273     {
274         for (SignalGroup signalGroup : this.signalGroups)
275         {
276             signalGroup.startup(this.offset, this.cycleTime, simulator, network);
277         }
278     }
279 
280     /** {@inheritDoc} */
281     @Override
282     public void notify(final EventInterface event) throws RemoteException
283     {
284         // nothing
285     }
286 
287     /** {@inheritDoc} */
288     @Override
289     public InvisibleObjectInterface clone(final OTSSimulatorInterface newSimulator, final Network newNetwork)
290             throws NetworkException
291     {
292         Set<SignalGroup> signalGroupsCloned = new LinkedHashSet<>();
293         for (SignalGroup signalGroup : this.signalGroups)
294         {
295             signalGroupsCloned.add(signalGroup.clone());
296         }
297         try
298         {
299             return new FixedTimeController(getId(), newSimulator, newNetwork, this.cycleTime, this.offset, signalGroupsCloned);
300         }
301         catch (SimRuntimeException exception)
302         {
303             throw new RuntimeException("Cloning using a simulator that is not at time 0.");
304         }
305     }
306 
307     /** {@inheritDoc} */
308     @Override
309     public String getFullId()
310     {
311         return getId();
312     }
313 
314     /**
315      * @return cycleTime.
316      */
317     public final Duration getCycleTime()
318     {
319         return this.cycleTime;
320     }
321 
322     /**
323      * @return offset.
324      */
325     public final Duration getOffset()
326     {
327         return this.offset;
328     }
329 
330     /**
331      * @return signalGroups.
332      */
333     public final Set<SignalGroup> getSignalGroups()
334     {
335         return this.signalGroups;
336     }
337 
338     /** {@inheritDoc} */
339     @Override
340     public String toString()
341     {
342         return "FixedTimeController [cycleTime=" + this.cycleTime + ", offset=" + this.offset + ", signalGroups="
343                 + this.signalGroups + ", full id=" + this.getFullId() + "]";
344     }
345 
346     /**
347      * Fixed time signal group.
348      * <p>
349      * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
350      * <br>
351      * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
352      * <p>
353      * @version $Revision$, $LastChangedDate$, by $Author$, initial version 21 feb. 2019 <br>
354      * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
355      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
356      * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
357      */
358     public static class SignalGroup implements Identifiable
359     {
360 
361         /** Id. */
362         private final String id;
363 
364         /** Traffic light ids. */
365         private final ImmutableSet<String> trafficLightIds;
366 
367         /** Offset from start of cycle. */
368         private final Duration offset;
369 
370         /** Pre-green duration. */
371         private final Duration preGreen;
372 
373         /** Green duration. */
374         private final Duration green;
375 
376         /** Yellow duration. */
377         private final Duration yellow;
378 
379         /** Current color (according to <b>this</b> SignalGroup). */
380         private TrafficLightColor currentColor = TrafficLightColor.RED;
381 
382         // The following properties are remembered for the updates after the startup
383 
384         /** Traffic light objects. */
385         private List<TrafficLight> trafficLights;
386 
387         /** Simulator. */
388         private OTSSimulatorInterface simulator;
389 
390         /** Red time. */
391         private Duration red;
392 
393         /**
394          * Constructor without pre-green duration.
395          * @param id String; id
396          * @param trafficLightIds Set&lt;String&gt;; traffic light ids
397          * @param offset Duration; offset from start of cycle
398          * @param green Duration; green duration
399          * @param yellow Duration; yellow duration
400          */
401         public SignalGroup(final String id, final Set<String> trafficLightIds, final Duration offset, final Duration green,
402                 final Duration yellow)
403         {
404             this(id, trafficLightIds, offset, Duration.ZERO, green, yellow);
405         }
406 
407         /**
408          * Constructor with pre-green duration.
409          * @param id String; id
410          * @param trafficLightIds Set&lt;String&gt;; traffic light ids
411          * @param offset Duration; offset from start of cycle
412          * @param preGreen Duration; pre-green duration
413          * @param green Duration; green duration
414          * @param yellow Duration; yellow duration
415          */
416         public SignalGroup(final String id, final Set<String> trafficLightIds, final Duration offset, final Duration preGreen,
417                 final Duration green, final Duration yellow)
418         {
419             Throw.whenNull(id, "Id may not be null.");
420             Throw.whenNull(trafficLightIds, "Traffic light ids may not be null.");
421             Throw.whenNull(offset, "Offset may not be null.");
422             Throw.whenNull(preGreen, "Pre-green may not be null.");
423             Throw.when(preGreen.lt(Duration.ZERO), IllegalArgumentException.class, "Pre green duration may not be negative");
424             Throw.whenNull(green, "Green may not be null.");
425             Throw.when(green.lt(Duration.ZERO), IllegalArgumentException.class, "Green duration may not be negative");
426             Throw.whenNull(yellow, "Yellow may not be null.");
427             Throw.when(yellow.lt(Duration.ZERO), IllegalArgumentException.class, "Yellow duration may not be negative");
428             Throw.when(trafficLightIds.isEmpty(), IllegalArgumentException.class, "Traffic light ids may not be empty.");
429             this.id = id;
430             this.trafficLightIds = new ImmutableHashSet<>(trafficLightIds, Immutable.COPY);
431             this.offset = offset;
432             this.preGreen = preGreen;
433             this.green = green;
434             this.yellow = yellow;
435         }
436 
437         /**
438          * Retrieve the id of this signal group.
439          * @return String
440          */
441         @Override
442         public String getId()
443         {
444             return this.id;
445         }
446 
447         /**
448          * Connect to the traffic lights in the network, initialize the traffic lights to their initial color and schedule the
449          * first transitions.
450          * @param controllerOffset Duration;
451          * @param cycleTime Duration;
452          * @param simulator OTSSimulatorInterface;
453          * @param network Network;
454          * @throws SimRuntimeException when traffic light does not exist in the network
455          */
456         public void startup(final Duration controllerOffset, final Duration cycleTime, final OTSSimulatorInterface simulator,
457                 final Network network) throws SimRuntimeException
458         {
459             this.simulator = simulator;
460             double totalOffsetSI = this.offset.si + controllerOffset.si;
461             while (totalOffsetSI < 0.0)
462             {
463                 totalOffsetSI += cycleTime.si;
464             }
465             Duration totalOffset = Duration.instantiateSI(totalOffsetSI % cycleTime.si);
466             this.red = cycleTime.minus(this.preGreen).minus(this.green).minus(this.yellow);
467             Throw.when(this.red.lt0(), IllegalArgumentException.class, "Cycle time shorter than sum of non-red times.");
468 
469             this.trafficLights = new ArrayList<>();
470             ImmutableMap<String, TrafficLight> trafficLightObjects = network.getObjectMap(TrafficLight.class);
471             for (String trafficLightId : this.trafficLightIds)
472             {
473                 TrafficLight trafficLight = trafficLightObjects.get(trafficLightId);
474                 if (null == trafficLight) // Traffic light not found using id; try to find it by full id
475                 {
476                     // TODO: networkId.trafficLightId? Shouldn't that be linkId.trafficLightId?
477                     trafficLight = trafficLightObjects.get(network.getId() + "." + trafficLightId);
478                 }
479                 Throw.when(trafficLight == null, SimRuntimeException.class, "Traffic light \"" + trafficLightId
480                         + "\" in fixed time controller could not be found in network " + network.getId() + ".");
481                 this.trafficLights.add(trafficLight);
482             }
483             // System.out.println("Starting up " + this);
484             Duration inCycleTime = Duration.ZERO.minus(totalOffset);
485             while (inCycleTime.si < 0)
486             {
487                 inCycleTime = inCycleTime.plus(cycleTime);
488             }
489             Duration duration = null;
490             if (inCycleTime.ge(this.preGreen.plus(this.green).plus(this.yellow)))
491             {
492                 this.currentColor = TrafficLightColor.RED; // redundant; it is already RED
493                 duration = cycleTime.minus(inCycleTime);
494             }
495             else if (inCycleTime.lt(this.preGreen))
496             {
497                 this.currentColor = TrafficLightColor.PREGREEN;
498                 duration = this.preGreen.minus(inCycleTime);
499             }
500             else if (inCycleTime.lt(this.preGreen.plus(this.green)))
501             {
502                 this.currentColor = TrafficLightColor.GREEN;
503                 duration = this.preGreen.plus(this.green).minus(inCycleTime);
504             }
505             else if (inCycleTime.lt(this.preGreen.plus(this.green).plus(this.yellow)))
506             {
507                 this.currentColor = TrafficLightColor.YELLOW;
508                 duration = this.preGreen.plus(this.green).plus(this.yellow).minus(inCycleTime);
509             }
510             else
511             {
512                 throw new SimRuntimeException("Cannot determine initial state of signal group " + this);
513             }
514             // System.out.println("Initial color is " + this.currentColor + " next flank after " + duration);
515             setTrafficLights(this.currentColor);
516             this.simulator.scheduleEventRel(duration, this, this, "updateColors", null);
517         }
518 
519         /**
520          * Updates the color of the traffic lights.
521          */
522         @SuppressWarnings("unused")
523         private void updateColors()
524         {
525             try
526             {
527                 Duration duration = Duration.ZERO;
528                 TrafficLightColor color = this.currentColor;
529                 while (duration.le0())
530                 {
531                     switch (color)
532                     {
533                         case PREGREEN:
534                             color = TrafficLightColor.GREEN;
535                             duration = this.green;
536                             break;
537                         case GREEN:
538                             color = TrafficLightColor.YELLOW;
539                             duration = this.yellow;
540                             break;
541                         case YELLOW:
542                             color = TrafficLightColor.RED;
543                             duration = this.red;
544                             break;
545                         case RED:
546                             color = TrafficLightColor.PREGREEN;
547                             duration = this.preGreen;
548                             break;
549                         default:
550                             throw new RuntimeException("Cannot happen.");
551                     }
552                 }
553                 setTrafficLights(color);
554                 this.simulator.scheduleEventRel(duration, this, this, "updateColors", null);
555             }
556             catch (SimRuntimeException exception)
557             {
558                 // cannot happen; we check all durations for consistency
559                 throw new RuntimeException(exception);
560             }
561         }
562 
563         /**
564          * Change the color of our traffic lights.
565          * @param trafficLightColor TrafficLightColor; the new traffic light color to show
566          */
567         private void setTrafficLights(final TrafficLightColor trafficLightColor)
568         {
569             this.currentColor = trafficLightColor;
570             for (TrafficLight trafficLight : this.trafficLights)
571             {
572                 trafficLight.setTrafficLightColor(trafficLightColor);
573             }
574         }
575 
576         /**
577          * Clones the object for a cloned simulation.
578          */
579         @Override
580         public SignalGroup clone()
581         {
582             return new SignalGroup(getId(), this.trafficLightIds.toSet(), this.offset, this.preGreen, this.green, this.yellow);
583         }
584 
585         /** {@inheritDoc} */
586         @Override
587         public int hashCode()
588         {
589             final int prime = 31;
590             int result = 1;
591             result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
592             return result;
593         }
594 
595         /** {@inheritDoc} */
596         @Override
597         public boolean equals(Object obj)
598         {
599             if (this == obj)
600             {
601                 return true;
602             }
603             if (obj == null)
604             {
605                 return false;
606             }
607             if (getClass() != obj.getClass())
608             {
609                 return false;
610             }
611             SignalGroup other = (SignalGroup) obj;
612             if (this.id == null)
613             {
614                 if (other.id != null)
615                 {
616                     return false;
617                 }
618             }
619             else if (!this.id.equals(other.id))
620             {
621                 return false;
622             }
623             return true;
624         }
625 
626         /**
627          * @return trafficLights.
628          */
629         public final ImmutableList<TrafficLight> getTrafficLights()
630         {
631             return new ImmutableArrayList<>(this.trafficLights);
632         }
633 
634         /**
635          * @return red.
636          */
637         public final Duration getRed()
638         {
639             return this.red;
640         }
641 
642         /**
643          * @return trafficLightIds.
644          */
645         public final ImmutableSet<String> getTrafficLightIds()
646         {
647             return this.trafficLightIds;
648         }
649 
650         /**
651          * @return offset.
652          */
653         public final Duration getOffset()
654         {
655             return this.offset;
656         }
657 
658         /**
659          * @return preGreen.
660          */
661         public final Duration getPreGreen()
662         {
663             return this.preGreen;
664         }
665 
666         /**
667          * @return green.
668          */
669         public final Duration getGreen()
670         {
671             return this.green;
672         }
673 
674         /**
675          * @return yellow.
676          */
677         public final Duration getYellow()
678         {
679             return this.yellow;
680         }
681 
682         /**
683          * Retrieve the current color of this SignalGroup.
684          * @return TrafficLightColor; the current color of this signal group.
685          */
686         public TrafficLightColor getCurrentColor()
687         {
688             return currentColor;
689         }
690 
691         /** {@inheritDoc} */
692         @Override
693         public String toString()
694         {
695             return "SignalGroup [id=" + this.id + ", trafficLightIds=" + this.trafficLightIds + ", offset=" + this.offset
696                     + ", preGreen=" + this.preGreen + ", green=" + this.green + ", yellow=" + this.yellow + "currentColor="
697                     + this.currentColor + "]";
698         }
699 
700     }
701 
702     /**
703      * Storage of an offset within a cycle and the new traffic light color. Used to sort the flanks.
704      */
705     class Flank implements Comparable<Flank>
706     {
707         /** When (in the cycle time is this transition. */
708         final double offset;
709 
710         /** What is the color after this transition. */
711         final TrafficLightColor newColor;
712 
713         /**
714          * Construct a new Flank.
715          * @param offset double; offset within the cycle time
716          * @param newColor TrafficLightColor; color to show after this transition
717          */
718         public Flank(final double offset, final TrafficLightColor newColor)
719         {
720             this.offset = offset;
721             this.newColor = newColor;
722         }
723 
724         /**
725          * Retrieve the offset.
726          * @return double; the offset
727          */
728         public double getOffset()
729         {
730             return this.offset;
731         }
732 
733         /**
734          * Retrieve the color after this transition.
735          * @return TrafficLightColor; the color after this transition
736          */
737         public TrafficLightColor getTrafficLightColor()
738         {
739             return this.newColor;
740         }
741 
742         @Override
743         public String toString()
744         {
745             return "Flank [offset=" + offset + ", newColor=" + newColor + "]";
746         }
747 
748         /** Cumulative rounding errors are less than this value and traffic light transitions are spaced further apart. */
749         static private final double compareMargin = 0.01;
750 
751         @Override
752         public int compareTo(Flank o)
753         {
754             double deltaOffset = this.offset - o.offset;
755             if (Math.abs(deltaOffset) < compareMargin)
756             {
757                 deltaOffset = 0;
758             }
759             if (deltaOffset > 0)
760             {
761                 return 1;
762             }
763             if (deltaOffset < 0)
764             {
765                 return -1;
766             }
767             if (TrafficLightColor.GREEN == this.newColor && TrafficLightColor.GREEN != o.newColor)
768             {
769                 return -1;
770             }
771             if (TrafficLightColor.GREEN == o.newColor && TrafficLightColor.GREEN != this.newColor)
772             {
773                 return 1;
774             }
775             return 0;
776         }
777 
778     }
779 
780 }