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