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