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