View Javadoc
1   package trafficcontrol;
2   
3   import static org.junit.jupiter.api.Assertions.assertEquals;
4   import static org.junit.jupiter.api.Assertions.assertTrue;
5   import static org.junit.jupiter.api.Assertions.fail;
6   
7   import java.io.ByteArrayOutputStream;
8   import java.io.PrintStream;
9   import java.util.LinkedHashMap;
10  import java.util.LinkedHashSet;
11  import java.util.Map;
12  import java.util.Set;
13  
14  import javax.naming.NamingException;
15  
16  import org.djunits.value.vdouble.scalar.Duration;
17  import org.djunits.value.vdouble.scalar.Time;
18  import org.djutils.immutablecollections.ImmutableSet;
19  import org.mockito.ArgumentMatchers;
20  import org.mockito.Mockito;
21  import org.mockito.invocation.InvocationOnMock;
22  import org.mockito.stubbing.Answer;
23  import org.opentrafficsim.core.dsol.OtsModelInterface;
24  import org.opentrafficsim.core.dsol.OtsSimulator;
25  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
26  import org.opentrafficsim.core.network.NetworkException;
27  import org.opentrafficsim.core.network.Network;
28  import org.opentrafficsim.road.network.lane.object.trafficlight.TrafficLight;
29  import org.opentrafficsim.road.network.lane.object.trafficlight.TrafficLightColor;
30  import org.opentrafficsim.trafficcontrol.FixedTimeController;
31  import org.opentrafficsim.trafficcontrol.FixedTimeController.SignalGroup;
32  
33  import nl.tudelft.simulation.dsol.SimRuntimeException;
34  
35  /**
36   * Test the fixed time traffic controller class.
37   * <p>
38   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
39   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
40   * </p>
41   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
42   * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
43   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
44   */
45  public class TestFixedTimeController
46  {
47  
48      /**
49       * Test the constructors and initializers of the signal group and fixed time controller classes.
50       * @throws SimRuntimeException if that happens uncaught; this test has failed
51       * @throws NamingException if that happens uncaught; this test has failed
52       * @throws NetworkException on exception
53       */
54      // TODO: @Test
55      public void testConstructors() throws SimRuntimeException, NamingException, NetworkException
56      {
57          String signalGroupId = "sgId";
58          Set<String> trafficLightIds = new LinkedHashSet<>();
59          String trafficLightId = "08.1";
60          trafficLightIds.add(trafficLightId);
61          Duration signalGroupOffset = Duration.instantiateSI(5);
62          Duration preGreen = Duration.instantiateSI(2);
63          Duration green = Duration.instantiateSI(10);
64          Duration yellow = Duration.instantiateSI(3.5);
65          try
66          {
67              new SignalGroup(null, trafficLightIds, signalGroupOffset, preGreen, green, yellow);
68              fail("Null pointer for signalGroupId should have thrown a null pointer exception");
69          }
70          catch (NullPointerException npe)
71          {
72              // Ignore expected exception
73          }
74          try
75          {
76              new SignalGroup(signalGroupId, null, signalGroupOffset, preGreen, green, yellow);
77              fail("Null pointer for trafficLightIds should have thrown a null pointer exception");
78          }
79          catch (NullPointerException npe)
80          {
81              // Ignore expected exception
82          }
83          try
84          {
85              new SignalGroup(signalGroupId, trafficLightIds, null, preGreen, green, yellow);
86              fail("Null pointer for signalGroupOffset should have thrown a null pointer exception");
87          }
88          catch (NullPointerException npe)
89          {
90              // Ignore expected exception
91          }
92          try
93          {
94              new SignalGroup(signalGroupId, trafficLightIds, signalGroupOffset, null, green, yellow);
95              fail("Null pointer for preGreen should have thrown a null pointer exception");
96          }
97          catch (NullPointerException npe)
98          {
99              // Ignore expected exception
100         }
101         try
102         {
103             new SignalGroup(signalGroupId, trafficLightIds, signalGroupOffset, preGreen, null, yellow);
104             fail("Null pointer for green should have thrown a null pointer exception");
105         }
106         catch (NullPointerException npe)
107         {
108             // Ignore expected exception
109         }
110         try
111         {
112             new SignalGroup(signalGroupId, trafficLightIds, signalGroupOffset, preGreen, green, null);
113             fail("Null pointer for yellow should have thrown a null pointer exception");
114         }
115         catch (NullPointerException npe)
116         {
117             // Ignore expected exception
118         }
119         try
120         {
121             new SignalGroup(signalGroupId, new LinkedHashSet<String>(), signalGroupOffset, preGreen, green, yellow);
122             fail("Empty list of traffic light ids should have thrown an illegal argument exception");
123         }
124         catch (IllegalArgumentException iae)
125         {
126             // Ignore expected exception
127         }
128         // Test the controller that adds default for pre green time
129         SignalGroup sg = new SignalGroup(signalGroupId, trafficLightIds, signalGroupOffset, green, yellow);
130         assertEquals(0, sg.getPreGreen().si, 0, "default for pre green");
131         assertEquals(green.si, sg.getGreen().si, 0, "green");
132         assertEquals(yellow.si, sg.getYellow().si, 0, "yellow");
133         // Now that we've tested all ways that the constructor should have told us to go to hell, create a signal group
134         sg = new SignalGroup(signalGroupId, trafficLightIds, signalGroupOffset, preGreen, green, yellow);
135         assertEquals(signalGroupId, sg.getId(), "group id");
136         assertTrue(sg.toString().startsWith("SignalGroup ["), "toString returns something descriptive");
137 
138         String ftcId = "FTCid";
139         OtsSimulatorInterface simulator = new OtsSimulator("TestFixedTimeController");
140         simulator.initialize(Time.ZERO, Duration.ZERO, Duration.instantiateSI(3600), createModelMock());
141         Map<String, TrafficLight> trafficLightMap = new LinkedHashMap<String, TrafficLight>();
142         String networkId = "networkID";
143         trafficLightMap.put(trafficLightId, createTrafficLightMock(trafficLightId, networkId, simulator));
144         Network network = new Network(networkId, simulator);
145         network.addObject(trafficLightMap.get(trafficLightId));
146 
147         Duration cycleTime = Duration.instantiateSI(90);
148         Duration offset = Duration.instantiateSI(20);
149         Set<SignalGroup> signalGroups = new LinkedHashSet<>();
150         ImmutableSet<String> ids = sg.getTrafficLightIds();
151         for (String tlId : ids)
152         {
153             assertTrue(trafficLightMap.containsKey(tlId), "returned id is in provided set");
154         }
155         for (String tlId : trafficLightMap.keySet())
156         {
157             assertTrue(ids.contains(tlId), "provided id is returned");
158         }
159         signalGroups.add(sg);
160         try
161         {
162             new FixedTimeController(null, simulator, network, cycleTime, offset, signalGroups);
163             fail("Null pointer for controller id should have thrown an exception");
164         }
165         catch (NullPointerException npe)
166         {
167             // Ignore
168         }
169         try
170         {
171             new FixedTimeController(ftcId, null, network, cycleTime, offset, signalGroups);
172             fail("Null pointer for simulator should have thrown an exception");
173         }
174         catch (NullPointerException npe)
175         {
176             // Ignore
177         }
178         try
179         {
180             new FixedTimeController(ftcId, simulator, null, cycleTime, offset, signalGroups);
181             fail("Null pointer for network should have thrown an exception");
182         }
183         catch (NullPointerException npe)
184         {
185             // Ignore
186         }
187         try
188         {
189             new FixedTimeController(ftcId, simulator, network, null, offset, signalGroups);
190             fail("Null pointer for cycle time should have thrown an exception");
191         }
192         catch (NullPointerException npe)
193         {
194             // Ignore
195         }
196         try
197         {
198             new FixedTimeController(ftcId, simulator, network, cycleTime, null, signalGroups);
199             fail("Null pointer for offset should have thrown an exception");
200         }
201         catch (NullPointerException npe)
202         {
203             // Ignore
204         }
205         try
206         {
207             new FixedTimeController(ftcId, simulator, network, cycleTime, offset, null);
208             fail("Null pointer for signal groups should have thrown an exception");
209         }
210         catch (NullPointerException npe)
211         {
212             // Ignore
213         }
214         try
215         {
216             new FixedTimeController(ftcId, simulator, network, cycleTime, offset, new LinkedHashSet<SignalGroup>());
217             fail("Empty signal groups should have thrown an exception");
218         }
219         catch (IllegalArgumentException iae)
220         {
221             // Ignore
222         }
223         try
224         {
225             new FixedTimeController(ftcId, simulator, network, Duration.instantiateSI(0), offset, signalGroups);
226             fail("Illegal cycle time should hav thrown an exception");
227         }
228         catch (IllegalArgumentException iae)
229         {
230             // Ignore
231         }
232         try
233         {
234             new FixedTimeController(ftcId, simulator, network, Duration.instantiateSI(-10), offset, signalGroups);
235             fail("Illegal cycle time should hav thrown an exception");
236         }
237         catch (IllegalArgumentException iae)
238         {
239             // Ignore
240         }
241         // Not testing check for identical signal groups; yet
242         // Now that we've tested all ways that the constructor should have told us to go to hell, create a controller
243         FixedTimeController ftc = new FixedTimeController(ftcId, simulator, network, cycleTime, offset, signalGroups);
244         assertEquals(ftcId, ftc.getId(), "FTC id");
245         assertTrue(ftc.toString().startsWith("FixedTimeController ["), "toString returns something descriptive");
246 
247         simulator.runUpTo(Time.instantiateSI(1));
248         while (simulator.isStartingOrRunning())
249         {
250             try
251             {
252                 Thread.sleep(100);
253             }
254             catch (InterruptedException exception)
255             {
256                 exception.printStackTrace();
257             }
258         }
259         for (TrafficLight tl : sg.getTrafficLights())
260         {
261             assertTrue(trafficLightMap.containsKey(tl.getId()), "acquired traffic light is in the proved set");
262         }
263         assertEquals(cycleTime.minus(preGreen).minus(green).minus(yellow).si, sg.getRed().si,
264                 0.0001, "red time makes up remainder of cycle time");
265     }
266 
267     /**
268      * Test detection of non-disjoint sets of traffic lights.
269      * @throws NamingException on exception
270      * @throws SimRuntimeException on exception
271      * @throws NetworkException on exception
272      */
273     // TODO: @Test
274     public void testDisjoint() throws SimRuntimeException, NamingException, NetworkException
275     {
276         String signalGroupId = "sgId1";
277         Set<String> trafficLightIds1 = new LinkedHashSet<>();
278         String trafficLightId = "08.1";
279         trafficLightIds1.add(trafficLightId);
280         Duration signalGroupOffset = Duration.instantiateSI(5);
281         Duration preGreen = Duration.instantiateSI(2);
282         Duration green = Duration.instantiateSI(10);
283         Duration yellow = Duration.instantiateSI(3.5);
284         SignalGroup sg1 = new SignalGroup(signalGroupId, trafficLightIds1, signalGroupOffset, preGreen, green, yellow);
285         String signalGroupId2 = "sgId2";
286         Set<String> trafficLightIds2 = new LinkedHashSet<>();
287         trafficLightIds2.add(trafficLightId);
288         SignalGroup sg2 = new SignalGroup(signalGroupId2, trafficLightIds2, signalGroupOffset, preGreen, green, yellow);
289 
290         String ftcId = "FTCid";
291         OtsSimulatorInterface simulator = new OtsSimulator("TestFixedTimeController");
292         simulator.initialize(Time.ZERO, Duration.ZERO, Duration.instantiateSI(3600), createModelMock());
293         Map<String, TrafficLight> trafficLightMap = new LinkedHashMap<String, TrafficLight>();
294         String networkId = "networkID";
295         trafficLightMap.put(trafficLightId, createTrafficLightMock(trafficLightId, networkId, simulator));
296         Network network = new Network(networkId, simulator);
297         network.addObject(trafficLightMap.get(trafficLightId));
298 
299         Duration cycleTime = Duration.instantiateSI(90);
300         Duration offset = Duration.instantiateSI(20);
301         Set<SignalGroup> signalGroups = new LinkedHashSet<>();
302         signalGroups.add(sg1);
303         signalGroups.add(sg2);
304         try
305         {
306             new FixedTimeController(ftcId, simulator, network, cycleTime, offset, signalGroups);
307             fail("Same traffic light in different signal groups should have thrown an IllegalArgumnentException");
308         }
309         catch (IllegalArgumentException iae)
310         {
311             // Ignore
312         }
313     }
314 
315     /**
316      * Test timing of fixed time controller.
317      * @throws SimRuntimeException if that happens uncaught; this test has failed
318      * @throws NamingException if that happens uncaught; this test has failed
319      * @throws NetworkException on exception
320      */
321     // TODO: @Test
322     public void testTimings() throws SimRuntimeException, NamingException, NetworkException
323     {
324         String signalGroupId = "sgId";
325         Set<String> trafficLightIds = new LinkedHashSet<>();
326         String trafficLightId = "08.1";
327         trafficLightIds.add(trafficLightId);
328         Set<SignalGroup> signalGroups = new LinkedHashSet<>();
329         for (int cycleTime : new int[] {60, 90})
330         {
331             Duration cycle = Duration.instantiateSI(cycleTime);
332             for (int ftcOffsetTime : new int[] {-100, -10, 0, 10, 100})
333             {
334                 Duration ftcOffset = Duration.instantiateSI(ftcOffsetTime);
335                 for (int sgOffsetTime : new int[] {-99, -9, 0, 9, 99})
336                 {
337                     Duration sgOffset = Duration.instantiateSI(sgOffsetTime);
338                     for (int preGreenTime : new int[] {0, 3})
339                     {
340                         Duration preGreen = Duration.instantiateSI(preGreenTime);
341                         for (int greenTime : new int[] {5, 15, 100})
342                         {
343                             Duration green = Duration.instantiateSI(greenTime);
344                             for (double yellowTime : new double[] {0, 3.5, 4.5})
345                             {
346                                 Duration yellow = Duration.instantiateSI(yellowTime);
347                                 double minimumCycleTime = preGreenTime + greenTime + yellowTime;
348                                 SignalGroup sg =
349                                         new SignalGroup(signalGroupId, trafficLightIds, sgOffset, preGreen, green, yellow);
350                                 signalGroups.clear();
351                                 signalGroups.add(sg);
352                                 String ftcId = "FTCid";
353                                 OtsSimulatorInterface simulator = new OtsSimulator("TestFixedTimeController");
354                                 simulator.initialize(Time.ZERO, Duration.ZERO, Duration.instantiateSI(3600), createModelMock());
355                                 Map<String, TrafficLight> trafficLightMap = new LinkedHashMap<String, TrafficLight>();
356                                 String networkId = "networkID";
357                                 trafficLightMap.put(trafficLightId,
358                                         createTrafficLightMock(trafficLightId, networkId, simulator));
359                                 Network network = new Network(networkId, simulator);
360                                 network.addObject(trafficLightMap.get(trafficLightId));
361                                 // System.out.println(cycle);
362                                 FixedTimeController ftc =
363                                         new FixedTimeController(ftcId, simulator, network, cycle, ftcOffset, signalGroups);
364                                 // System.out.print(ftc);
365                                 if (cycleTime < minimumCycleTime)
366                                 {
367                                     PrintStream originalError = System.err;
368                                     boolean exceptionThrown = false;
369                                     try
370                                     {
371                                         while (simulator.getSimulatorTime().si <= 0)
372                                         {
373                                             System.setErr(new PrintStream(new ByteArrayOutputStream()));
374                                             try
375                                             {
376                                                 simulator.step();
377                                             }
378                                             finally
379                                             {
380                                                 System.setErr(originalError);
381                                             }
382                                         }
383                                     }
384                                     catch (SimRuntimeException exception)
385                                     {
386                                         exceptionThrown = true;
387                                         assertTrue(exception.getCause().getCause().getMessage().contains("Cycle time shorter "),
388                                                 "exception explains cycle time problem");
389                                     }
390                                     assertTrue(exceptionThrown,
391                                             "Too short cycle time should have thrown a SimRuntimeException");
392                                 }
393                                 else
394                                 {
395                                     // All transitions are at multiples of 0.5 seconds; check the state at 0.25 and 0.75 in each
396                                     // second
397                                     for (int second = 0; second <= 300; second++)
398                                     {
399                                         Object[] args = new Object[] {simulator, ftc, Boolean.TRUE};
400                                         simulator.scheduleEventAbsTime(Time.instantiateSI(second + 0.25), this, "checkState",
401                                                 args);
402                                         simulator.scheduleEventAbsTime(Time.instantiateSI(second + 0.75), this, "checkState",
403                                                 args);
404                                     }
405                                     Time stopTime = Time.instantiateSI(300);
406                                     simulator.runUpTo(stopTime);
407                                     while (simulator.isStartingOrRunning())
408                                     {
409                                         try
410                                         {
411                                             Thread.sleep(1);
412                                         }
413                                         catch (InterruptedException exception)
414                                         {
415                                             exception.printStackTrace();
416                                         }
417                                     }
418                                     if (simulator.getSimulatorAbsTime().lt(stopTime))
419                                     {
420                                         // something went wrong; call checkState with stopSimulatorOnError set to false
421                                         checkState(simulator, ftc, Boolean.FALSE);
422                                         fail("checkState should have thrown an assert error");
423                                     }
424                                 }
425                             }
426                         }
427                     }
428                 }
429             }
430         }
431     }
432 
433     /**
434      * Check that the current state of a fixed time traffic light controller matches the design.
435      * @param simulator OtsSimulatorInterface; the simulator
436      * @param ftc FixedTimeController; the fixed time traffic light controller
437      * @param stopSimulatorOnError boolean; if true; stop the simulator on error; if false; execute the failing assert on error
438      */
439     public void checkState(final OtsSimulatorInterface simulator, final FixedTimeController ftc,
440             final boolean stopSimulatorOnError)
441     {
442         double cycleTime = ftc.getCycleTime().si;
443         double time = simulator.getSimulatorTime().si;
444         double mainOffset = ftc.getOffset().si;
445         for (SignalGroup sg : ftc.getSignalGroups())
446         {
447             double phaseOffset = sg.getOffset().si + mainOffset;
448             double phase = time + phaseOffset;
449             while (phase < 0)
450             {
451                 phase += cycleTime;
452             }
453             phase %= cycleTime;
454             TrafficLightColor expectedColor = null;
455             if (phase < sg.getPreGreen().si)
456             {
457                 expectedColor = TrafficLightColor.PREGREEN;
458             }
459             else if (phase < sg.getPreGreen().plus(sg.getGreen()).si)
460             {
461                 expectedColor = TrafficLightColor.GREEN;
462             }
463             else if (phase < sg.getPreGreen().plus(sg.getGreen()).plus(sg.getYellow()).si)
464             {
465                 expectedColor = TrafficLightColor.YELLOW;
466             }
467             else
468             {
469                 expectedColor = TrafficLightColor.RED;
470             }
471             // Verify the color of all traffic lights
472             for (TrafficLight tl : sg.getTrafficLights())
473             {
474                 if (!expectedColor.equals(tl.getTrafficLightColor()))
475                 {
476                     if (stopSimulatorOnError)
477                     {
478                         try
479                         {
480                             simulator.stop();
481                         }
482                         catch (SimRuntimeException exception)
483                         {
484                             exception.printStackTrace();
485                         }
486                     }
487                     else
488                     {
489                         assertEquals(
490                                 expectedColor + " which is in phase " + phase + " of cycle time " + cycleTime,
491                                 tl.getTrafficLightColor(),
492                                 "Traffic light color mismatch at simulator time " + simulator.getSimulatorTime()
493                                         + " of signal group " + sg);
494                     }
495                 }
496             }
497         }
498     }
499 
500     /**
501      * Create a mocked OtsModelInterface.
502      * @return OtsModelInterface
503      */
504     public OtsModelInterface createModelMock()
505     {
506         return Mockito.mock(OtsModelInterface.class);
507     }
508 
509     /** Remember current state of all mocked traffic lights. */
510     private Map<String, TrafficLightColor> currentTrafficLightColors = new LinkedHashMap<>();
511 
512     /**
513      * Mock a traffic light.
514      * @param id String; value that will be returned by the getId method
515      * @param networkId String; name of network (prepended to id for result of getFullId method)
516      * @param simulator TODO
517      * @return TrafficLight
518      */
519     public TrafficLight createTrafficLightMock(final String id, final String networkId, final OtsSimulatorInterface simulator)
520     {
521         TrafficLight result = Mockito.mock(TrafficLight.class);
522         Mockito.when(result.getId()).thenReturn(id);
523         Mockito.when(result.getFullId()).thenReturn(networkId + "." + id);
524         Mockito.when(result.getTrafficLightColor()).thenAnswer(new Answer<TrafficLightColor>()
525         {
526             @Override
527             public TrafficLightColor answer(final InvocationOnMock invocation) throws Throwable
528             {
529                 return TestFixedTimeController.this.currentTrafficLightColors.get(result.getFullId());
530             }
531         });
532         Mockito.doAnswer((Answer<Void>) invocation ->
533         {
534             TrafficLightColor tlc = invocation.getArgument(0);
535             // System.out.println(simulator.getSimulatorTime() + " changing color of " + result.getFullId() + " from "
536             // + this.currentTrafficLightColors.get(result.getFullId()) + " to " + tlc);
537             this.currentTrafficLightColors.put(result.getFullId(), tlc);
538             return null;
539         }).when(result).setTrafficLightColor(ArgumentMatchers.any(TrafficLightColor.class));
540         this.currentTrafficLightColors.put(result.getFullId(), TrafficLightColor.BLACK);
541         return result;
542     }
543 
544 }