View Javadoc
1   package org.opentrafficsim.trafficcontrol.trafcod;
2   
3   import java.awt.BorderLayout;
4   import java.awt.Color;
5   import java.awt.Component;
6   import java.awt.Dimension;
7   import java.awt.Graphics2D;
8   import java.awt.event.ActionEvent;
9   import java.awt.event.ActionListener;
10  import java.awt.image.BufferedImage;
11  import java.util.ArrayList;
12  import java.util.Comparator;
13  import java.util.LinkedHashMap;
14  import java.util.LinkedHashSet;
15  import java.util.List;
16  import java.util.Map;
17  import java.util.Set;
18  
19  import javax.swing.BoxLayout;
20  import javax.swing.ImageIcon;
21  import javax.swing.JCheckBox;
22  import javax.swing.JFrame;
23  import javax.swing.JLabel;
24  import javax.swing.JPanel;
25  import javax.swing.JScrollPane;
26  import javax.swing.SwingUtilities;
27  
28  import org.djutils.exceptions.Throw;
29  import org.opentrafficsim.base.logger.Logger;
30  import org.opentrafficsim.trafficcontrol.TrafficControlException;
31  import org.opentrafficsim.trafficcontrol.TrafficController;
32  
33  /**
34   * Functions that can draw a schematic diagram of an intersection given the list of traffic streams. The traffic stream numbers
35   * must follow the Dutch conventions.
36   * <p>
37   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
38   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
39   * </p>
40   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
41   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
42   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
43   */
44  public class Diagram
45  {
46      /** Numbering of the lateral objects/positions from the median to the shoulder. */
47      /** Central divider. */
48      static final int DIVIDER_1 = 0;
49  
50      /** Left turn area on roundabout. */
51      static final int CAR_ROUNDABOUT_LEFT = 1;
52  
53      /** Public transit between divider and left turn lane. */
54      static final int PT_DIV_L = 3;
55  
56      /** Divider between center public transit and left turn lane. */
57      static final int DIVIDER_2 = 4;
58  
59      /** Left turn lane(s). */
60      static final int CAR_LEFT = 5;
61  
62      /** No turn (center) lane(s). */
63      static final int CAR_CENTER = 7;
64  
65      /** Right turn lane(s). */
66      static final int CAR_RIGHT = 9;
67  
68      /** Divider between right turn lane and bicycle lane. */
69      static final int DIVIDER_3 = 10;
70  
71      /** Public transit between right turn lane and bicycle lane. */
72      static final int PT_RIGHT_BICYCLE = 11;
73  
74      /** Divider. */
75      static final int DIVIDER_4 = 12;
76  
77      /** Bicycle lane. */
78      static final int BICYCLE = 13;
79  
80      /** Divider. */
81      static final int DIVIDER_5 = 14;
82  
83      /** Public transit between bicycle lane and right sidewalk. */
84      static final int PT_BICYCLE_SIDEWALK = 15;
85  
86      /** Divider. */
87      static final int DIVIDER_6 = 16;
88  
89      /** Sidewalk. */
90      static final int SIDEWALK = 17;
91  
92      /** Divider. */
93      static final int DIVIDER_7 = 18;
94  
95      /** Public transit right of right sidewalk. */
96      static final int PT_SIDEWALK_SHOULDER = 19;
97  
98      /** Shoulder right of right sidewalk. */
99      static final int SHOULDER = 20;
100 
101     /** Boundary of schematic intersection. */
102     static final int BOUNDARY = 21;
103 
104     /** The streams crossing the intersection. */
105     private final List<Short> streams;
106 
107     /** The routes through the intersection. */
108     private final Map<Short, XYPair[]> routes = new LinkedHashMap<>();
109 
110     /**
111      * Construct a new diagram.
112      * @param streams the streams (numbered according to the Dutch standard) that cross the intersection.
113      * @throws TrafficControlException when a route is invalid
114      */
115     public Diagram(final Set<Short> streams) throws TrafficControlException
116     {
117         this.streams = new ArrayList<Short>(streams); // make a deep copy and sort by stream number
118         this.streams.sort(new Comparator<Short>()
119         {
120 
121             @Override
122             public int compare(final Short o1, final Short o2)
123             {
124                 return o1 - o2;
125             }
126         });
127 
128         // Primary car streams
129         //@formatter:off
130         for (short stream = 1; stream <= 12; stream += 3)
131         {
132             int quadrant = (stream - 1) / 3;
133             this.routes.put(stream, rotateRoute(quadrant, assembleRoute(
134                     new RouteStep(-BOUNDARY, CAR_RIGHT),
135                     new RouteStep(-SHOULDER, CAR_RIGHT, Command.STOP_LINE_AND_ICON),
136                     new RouteStep(-CAR_CENTER, CAR_RIGHT),
137                     new RouteStep(-CAR_CENTER, BOUNDARY))));
138             this.routes.put((short) (stream + 1), rotateRoute(quadrant, assembleRoute(
139                     new RouteStep(-BOUNDARY, CAR_CENTER),
140                     new RouteStep(-SHOULDER, CAR_CENTER, Command.STOP_LINE_AND_ICON),
141                     new RouteStep(Command.IF, stream + 1 + 60),
142                         new RouteStep(-CAR_ROUNDABOUT_LEFT, CAR_CENTER),
143                     new RouteStep(Command.ELSE),
144                         new RouteStep(BOUNDARY, CAR_CENTER),
145                     new RouteStep(Command.END_IF))));
146             this.routes.put((short) (stream + 2), rotateRoute(quadrant, assembleRoute(
147                     new RouteStep(-BOUNDARY, CAR_LEFT),
148                     new RouteStep(-SHOULDER, CAR_LEFT, Command.STOP_LINE_AND_ICON),
149                     new RouteStep(Command.IF, stream + 2 + 60),
150                         new RouteStep(-CAR_ROUNDABOUT_LEFT, CAR_LEFT),
151                     new RouteStep(Command.ELSE_IF, (stream + 10) % 12 + 60),
152                         new RouteStep(CAR_CENTER, CAR_LEFT),
153                         new RouteStep(CAR_CENTER, CAR_ROUNDABOUT_LEFT),
154                     new RouteStep(Command.ELSE),
155                         new RouteStep(-CAR_LEFT, CAR_LEFT),
156                         new RouteStep(-CAR_LEFT, PT_DIV_L),
157                         new RouteStep(-CAR_ROUNDABOUT_LEFT, PT_DIV_L),
158                         new RouteStep(-CAR_ROUNDABOUT_LEFT, -CAR_LEFT),
159                         new RouteStep(PT_DIV_L, -CAR_LEFT),
160                         new RouteStep(PT_DIV_L, -CAR_CENTER),
161                         new RouteStep(CAR_CENTER, -CAR_CENTER),
162                         new RouteStep(CAR_CENTER, -BOUNDARY),
163                     new RouteStep(Command.END_IF))));
164         }
165         // Bicycle streams
166         for (short stream = 21; stream <= 28; stream += 2)
167         {
168             int quadrant = (stream - 19) / 2 % 4;
169             this.routes.put(stream, rotateRoute(quadrant, assembleRoute(
170                     new RouteStep(DIVIDER_1, BICYCLE, Command.ICON),
171                     new RouteStep(SHOULDER, BICYCLE),
172                     new RouteStep(BOUNDARY, BICYCLE, Command.ICON))));
173             this.routes.put((short) (stream + 1), rotateRoute(quadrant, assembleRoute(
174                     new RouteStep(-BOUNDARY, BICYCLE),
175                     new RouteStep(-DIVIDER_3, BICYCLE, Command.ICON),
176                     new RouteStep(Command.IF, stream),
177                     new RouteStep(-DIVIDER_1, BICYCLE, Command.ICON),
178                     new RouteStep(Command.ELSE),
179                         new RouteStep(SHOULDER, BICYCLE),
180                         new RouteStep(BOUNDARY, BICYCLE, Command.ICON),
181                     new RouteStep(Command.END_IF))));
182         }
183         // Pedestrian streams
184         for (short stream = 31; stream <= 38; stream += 2)
185         {
186             int quadrant = (stream - 29) / 2 % 4;
187             this.routes.put(stream, rotateRoute(quadrant, assembleRoute(
188                     new RouteStep(DIVIDER_1, SIDEWALK),
189                     new RouteStep(BOUNDARY, SIDEWALK))));
190             this.routes.put((short) (stream + 1), rotateRoute(quadrant, assembleRoute(
191                     new RouteStep(-BOUNDARY, SIDEWALK),
192                     new RouteStep(Command.IF, stream),
193                         new RouteStep(-DIVIDER_1, SIDEWALK),
194                     new RouteStep(Command.ELSE),
195                         new RouteStep(BOUNDARY, SIDEWALK),
196                     new RouteStep(Command.END_IF))));
197         }
198         // Public transit streams
199         for (short stream = 41; stream <= 52; stream += 3)
200         {
201             int quadrant = (stream - 41) / 3;
202             this.routes.put(stream, rotateRoute(quadrant, assembleRoute(
203                     new RouteStep(-BOUNDARY, PT_DIV_L),
204                     new RouteStep(-SHOULDER, PT_DIV_L, Command.STOP_LINE),
205                     new RouteStep(-PT_SIDEWALK_SHOULDER, PT_DIV_L, Command.ICON),
206                     new RouteStep(-CAR_RIGHT, PT_DIV_L),
207                     new RouteStep(-CAR_RIGHT, CAR_LEFT),
208                     new RouteStep(-PT_DIV_L, CAR_LEFT),
209                     new RouteStep(-PT_DIV_L, SHOULDER),
210                     new RouteStep(-PT_DIV_L, BOUNDARY, Command.ICON))));
211             this.routes.put((short) (stream + 1), rotateRoute(quadrant, assembleRoute(
212                     new RouteStep(-BOUNDARY, PT_DIV_L),
213                     new RouteStep(-SHOULDER, PT_DIV_L, Command.STOP_LINE),
214                     new RouteStep(-PT_SIDEWALK_SHOULDER, PT_DIV_L, Command.ICON),
215                     new RouteStep(SHOULDER, PT_DIV_L),
216                     new RouteStep(BOUNDARY, PT_DIV_L))));
217             this.routes.put((short) (stream + 2), rotateRoute(quadrant, assembleRoute(
218                     new RouteStep(-BOUNDARY, PT_DIV_L),
219                     new RouteStep(-SHOULDER, PT_DIV_L, Command.STOP_LINE),
220                     new RouteStep(-PT_SIDEWALK_SHOULDER, PT_DIV_L, Command.ICON),
221                     new RouteStep(-CAR_RIGHT, PT_DIV_L),
222                     new RouteStep(-CAR_RIGHT, CAR_ROUNDABOUT_LEFT),
223                     new RouteStep(-PT_DIV_L, CAR_ROUNDABOUT_LEFT),
224                     new RouteStep(Command.IF, (stream + 2 - 40) % 12 + 60),
225                         new RouteStep(-PT_DIV_L, -PT_DIV_L),
226                         new RouteStep(PT_DIV_L, -PT_DIV_L),
227                     new RouteStep(Command.ELSE),
228                         new RouteStep(-PT_DIV_L, -CAR_CENTER),
229                         new RouteStep(CAR_ROUNDABOUT_LEFT, -CAR_CENTER),
230                         new RouteStep(CAR_ROUNDABOUT_LEFT, -CAR_RIGHT),
231                         new RouteStep(PT_DIV_L, -CAR_RIGHT),
232                     new RouteStep(Command.END_IF),
233                     new RouteStep(PT_DIV_L, -SHOULDER),
234                     new RouteStep(PT_DIV_L, -BOUNDARY, Command.ICON))));
235         }
236         // Secondary car streams
237         for (short stream = 62; stream <= 72; stream += 3)
238         {
239             int quadrant = (stream - 61) / 3;
240             this.routes.put(stream, rotateRoute(quadrant, assembleRoute(
241                     new RouteStep(-CAR_ROUNDABOUT_LEFT, CAR_CENTER),
242                     new RouteStep(CAR_ROUNDABOUT_LEFT, CAR_CENTER, Command.STOP_LINE_AND_ICON),
243                     new RouteStep(BOUNDARY, CAR_CENTER))));
244             this.routes.put((short) (stream + 1), rotateRoute(quadrant, assembleRoute(
245                     new RouteStep(-CAR_ROUNDABOUT_LEFT, CAR_LEFT),
246                     new RouteStep(CAR_ROUNDABOUT_LEFT, CAR_LEFT, Command.STOP_LINE_AND_ICON),
247                     new RouteStep(CAR_CENTER, CAR_LEFT),
248                     new RouteStep(Command.IF, ((stream - 61) + 11) % 12 + 60),
249                     new RouteStep(CAR_CENTER, CAR_ROUNDABOUT_LEFT),
250                     new RouteStep(Command.ELSE),
251                     new RouteStep(CAR_CENTER, -BOUNDARY),
252                     new RouteStep(Command.END_IF))));
253         }
254        // @formatter:on
255     }
256 
257     /**
258      * Check that a particular stream exists. Beware that the keys in this.streams are Short.
259      * @param stream the number of the stream to check
260      * @return true if the stream exists; false if it does not exist
261      */
262     private boolean streamExists(final short stream)
263     {
264         return this.streams.contains(stream);
265     }
266 
267     /**
268      * Report if object is inaccessible to all traffic.
269      * @param i the number of the object
270      * @return true if the object is inaccessible to all traffic
271      */
272     public static final boolean isGrass(final int i)
273     {
274         return i == DIVIDER_1 || i == DIVIDER_2 || i == DIVIDER_3 || i == DIVIDER_4 || i == DIVIDER_5 || i == DIVIDER_6
275                 || i == DIVIDER_7 || i == SHOULDER;
276     }
277 
278     /**
279      * Return the LaneType for a stream number.
280      * @param streamNumber the standard Dutch traffic stream number
281      * @return the lane type of the stream; or null if the stream number is reserved or invalid
282      */
283     final LaneType laneType(final int streamNumber)
284     {
285         if (streamNumber < 20 || streamNumber > 60 && streamNumber <= 80)
286         {
287             return LaneType.CAR_LANE;
288         }
289         if (streamNumber >= 20 && streamNumber < 30)
290         {
291             return LaneType.BICYCLE_LANE;
292         }
293         if (streamNumber >= 30 && streamNumber < 40)
294         {
295             return LaneType.PEDESTRIAN_LANE;
296         }
297         if (streamNumber > 40 && streamNumber <= 52 || streamNumber >= 81 && streamNumber <= 92)
298         {
299             return LaneType.PUBLIC_TRANSIT_LANE;
300         }
301         return null;
302     }
303 
304     /**
305      * Types of lanes.
306      */
307     enum LaneType
308     {
309         /** Car. */
310         CAR_LANE,
311         /** BICYCLE. */
312         BICYCLE_LANE,
313         /** Public transit. */
314         PUBLIC_TRANSIT_LANE,
315         /** Pedestrian. */
316         PEDESTRIAN_LANE,
317     }
318 
319     /**
320      * Return the rotated x value.
321      * @param xyPair the XYPair
322      * @param rotation rotation in multiples of 90 degrees
323      * @return the x component of the rotated coordinates
324      */
325     final int rotatedX(final XYPair xyPair, final int rotation)
326     {
327         switch (rotation % 4)
328         {
329             case 0:
330                 return xyPair.getX();
331             case 1:
332                 return -xyPair.getY();
333             case 2:
334                 return -xyPair.getX();
335             case 3:
336                 return xyPair.getY();
337             default:
338                 break; // cannot happen
339         }
340         return 0; // cannot happen
341     }
342 
343     /**
344      * Return the rotated y value.
345      * @param xyPair the XYPair
346      * @param rotation rotation in multiples of 90 degrees
347      * @return the y component of the rotated coordinates
348      */
349     final int rotatedY(final XYPair xyPair, final int rotation)
350     {
351         switch (rotation % 4)
352         {
353             case 0:
354                 return xyPair.getY();
355             case 1:
356                 return xyPair.getX();
357             case 2:
358                 return -xyPair.getY();
359             case 3:
360                 return -xyPair.getX();
361             default:
362                 break; // cannot happen
363         }
364         return 0; // cannot happen
365     }
366 
367     /**
368      * Commands used in RouteStep.
369      */
370     enum Command
371     {
372         /** No operation. */
373         NO_OP,
374         /** If. */
375         IF,
376         /** Else. */
377         ELSE,
378         /** Else if. */
379         ELSE_IF,
380         /** End if. */
381         END_IF,
382         /** Stop line. */
383         STOP_LINE,
384         /** Icon (bus, bicycle symbol). */
385         ICON,
386         /** Stop line AND icon. */
387         STOP_LINE_AND_ICON,
388     }
389 
390     /**
391      * Step in a schematic route through the intersection.
392      */
393     class RouteStep
394     {
395         /** X object. */
396         private final int x;
397 
398         /** Y object. */
399         private final int y;
400 
401         /** Command of this step. */
402         private final Command command;
403 
404         /** Condition for IF and ELSE_IF commands. */
405         private final int streamCondition;
406 
407         /**
408          * Construct a RouteStep that has a NO_OP command.
409          * @param x the X object at the end of this route step
410          * @param y the Y object at the end of this route step
411          */
412         RouteStep(final int x, final int y)
413         {
414             this.x = x;
415             this.y = y;
416             this.command = Command.NO_OP;
417             this.streamCondition = TrafficController.NO_STREAM;
418         }
419 
420         /**
421          * Construct a RouteStep with a command condition.
422          * @param x the X object at the end of this route step
423          * @param y the Y object at the end of this route step
424          * @param command a STOP_LINE or NO_OP command
425          * @throws TrafficControlException when an IF or ELSE_IF has an invalid streamCondition, or when an ELSE or END_IF has a
426          *             valid streamCOndition
427          */
428         RouteStep(final int x, final int y, final Command command) throws TrafficControlException
429         {
430             Throw.when(
431                     Command.STOP_LINE != command && Command.NO_OP != command && Command.ICON != command
432                             && Command.STOP_LINE_AND_ICON != command,
433                     TrafficControlException.class,
434                     "X and Y should only be provided with a NO_OP, STOP_LINE, ICON, or STOP_LINE_AND_ICON command; not with "
435                             + command);
436             this.x = x;
437             this.y = y;
438             this.command = command;
439             this.streamCondition = TrafficController.NO_STREAM;
440         }
441 
442         /**
443          * Construct a RouteStep with a command condition.
444          * @param command an IF, ELSE, ENDIF, or ELSE_IF command
445          * @param streamCondition the stream that must exist for the condition to be true
446          * @throws TrafficControlException when an IF or ELSE_IF has an invalid streamCondition, or when an ELSE or END_IF has a
447          *             valid streamCOndition
448          */
449         RouteStep(final Command command, final int streamCondition) throws TrafficControlException
450         {
451             Throw.when(Command.IF != command && Command.ELSE_IF != command, TrafficControlException.class,
452                     "RouteStep constructor with stream condition must use command IF or ELSE_IF");
453             this.x = TrafficController.NO_STREAM;
454             this.y = TrafficController.NO_STREAM;
455             this.command = command;
456             Throw.when(streamCondition == TrafficController.NO_STREAM, TrafficControlException.class,
457                     "IF or ELSE_IF need a valid traffic stream number");
458             this.streamCondition = streamCondition;
459         }
460 
461         /**
462          * Construct a RouteStep for ELSE or END_IF command.
463          * @param command either <code>Command.ELSE</code> or <code>Command.END_IF</code>
464          * @throws TrafficControlException when the Command is not ELSE or END_IF
465          */
466         RouteStep(final Command command) throws TrafficControlException
467         {
468             Throw.when(Command.ELSE != command && Command.END_IF != command, TrafficControlException.class,
469                     "RouteStep constructor with single command parameter requires ELSE or END_IF command");
470             this.x = TrafficController.NO_STREAM;
471             this.y = TrafficController.NO_STREAM;
472             this.command = command;
473             this.streamCondition = TrafficController.NO_STREAM;
474         }
475 
476         /**
477          * Retrieve the X object.
478          * @return the X object
479          */
480         public int getX()
481         {
482             return this.x;
483         }
484 
485         /**
486          * Retrieve the Y object.
487          * @return the Y object
488          */
489         public int getY()
490         {
491             return this.y;
492         }
493 
494         /**
495          * Retrieve the command.
496          * @return Command
497          */
498         public Command getCommand()
499         {
500             return this.command;
501         }
502 
503         /**
504          * Retrieve the stream condition.
505          * @return the streamCondition
506          */
507         public int getStreamCondition()
508         {
509             return this.streamCondition;
510         }
511 
512         @Override
513         public String toString()
514         {
515             return "RouteStep [x=" + this.x + ", y=" + this.y + ", command=" + this.command + ", streamCondition="
516                     + this.streamCondition + "]";
517         }
518 
519     }
520 
521     /**
522      * Pack two integer coordinates in one object.
523      */
524     class XYPair
525     {
526         /** X. */
527         private final int x;
528 
529         /** Y. */
530         private final int y;
531 
532         /**
533          * Construct a new XY pair.
534          * @param x the X value
535          * @param y the Y value
536          */
537         XYPair(final int x, final int y)
538         {
539             this.x = x;
540             this.y = y;
541         }
542 
543         /**
544          * Construct a new XY pair from a route step.
545          * @param routeStep the route step
546          */
547         XYPair(final RouteStep routeStep)
548         {
549             this.x = routeStep.getX();
550             this.y = routeStep.getY();
551         }
552 
553         /**
554          * Construct a rotated version of an XYPair.
555          * @param in the initial version
556          * @param quadrant the quadrant
557          */
558         XYPair(final XYPair in, final int quadrant)
559         {
560             this.x = rotatedX(in, quadrant);
561             this.y = rotatedY(in, quadrant);
562         }
563 
564         /**
565          * Retrieve the X value.
566          * @return the X value
567          */
568         public int getX()
569         {
570             return this.x;
571         }
572 
573         /**
574          * Retrieve the Y value.
575          * @return the Y value
576          */
577         public int getY()
578         {
579             return this.y;
580         }
581 
582         @Override
583         public String toString()
584         {
585             return "XYPair [x=" + this.x + ", y=" + this.y + "]";
586         }
587 
588     }
589 
590     /**
591      * Construct a route.
592      * @param quadrant the quadrant to assemble the route for
593      * @param steps an array, or series of arguments of type RouteStep
594      * @return an array of XY pairs describing the route through the intersection
595      * @throws TrafficLightException when the route contains commands other than NO_OP and STOP_LINE
596      */
597     private XYPair[] rotateRoute(final int quadrant, final RouteStep... steps) throws TrafficControlException
598     {
599         List<XYPair> route = new ArrayList<>();
600         boolean on = true;
601 
602         for (RouteStep step : steps)
603         {
604             switch (step.getCommand())
605             {
606                 case NO_OP:
607                 case STOP_LINE:
608                 case ICON:
609                 case STOP_LINE_AND_ICON:
610                     if (on)
611                     {
612                         route.add(new XYPair(new XYPair(step), quadrant));
613                     }
614                     break;
615 
616                 default:
617                     throw new TrafficControlException("Bad command in rotateRoute: " + step.getCommand());
618 
619             }
620         }
621         return route.toArray(new XYPair[route.size()]);
622     }
623 
624     /**
625      * Construct a route through the intersection.
626      * @param steps the steps of the route description
627      * @return the route through the intersection
628      * @throws TrafficControlException when something is very wrong
629      */
630     private RouteStep[] assembleRoute(final RouteStep... steps) throws TrafficControlException
631     {
632         List<RouteStep> result = new ArrayList<>();
633         RouteStep step;
634         for (int pointNo = 0; null != (step = routePoint(pointNo, steps)); pointNo++)
635         {
636             result.add(step);
637         }
638         return result.toArray(new RouteStep[result.size()]);
639     }
640 
641     /**
642      * Return the Nth step in a route.
643      * @param pointNo the rank of the requested step
644      * @param steps RouteStep... the steps
645      * @return the Nth step in the route or null if the route does not have <code>pointNo</code> steps
646      * @throws TrafficControlException when the command in a routestep is not recognized
647      */
648     private RouteStep routePoint(final int pointNo, final RouteStep... steps) throws TrafficControlException
649     {
650         boolean active = true;
651         boolean beenActive = false;
652         int index = 0;
653 
654         for (RouteStep routeStep : steps)
655         {
656             switch (routeStep.getCommand())
657             {
658                 case NO_OP:
659                 case STOP_LINE:
660                 case ICON:
661                 case STOP_LINE_AND_ICON:
662                     if (active)
663                     {
664                         if (index++ == pointNo)
665                         {
666                             return routeStep;
667                         }
668                     }
669                     break;
670 
671                 case IF:
672                     active = streamExists((short) routeStep.getStreamCondition());
673                     beenActive = active;
674                     break;
675 
676                 case ELSE_IF:
677                     if (active)
678                     {
679                         active = false;
680                     }
681                     else if (!beenActive)
682                     {
683                         active = this.streams.contains(routeStep.getStreamCondition());
684                     }
685                     if (active)
686                     {
687                         beenActive = true;
688                     }
689                     break;
690 
691                 case ELSE:
692                     active = !beenActive;
693                     break;
694 
695                 case END_IF:
696                     active = true;
697                     break;
698 
699                 default:
700                     throw new TrafficControlException("Bad switch: " + routeStep);
701 
702             }
703         }
704         return null;
705     }
706 
707     /**
708      * Create a BufferedImage and render the schematic on it.
709      * @return BufferedImage
710      */
711     public BufferedImage render()
712     {
713         int range = 2 * BOUNDARY + 1;
714         int cellSize = 10;
715         BufferedImage result = new BufferedImage(range * cellSize, range * cellSize, BufferedImage.TYPE_INT_RGB);
716         Graphics2D graphics = (Graphics2D) result.getGraphics();
717         graphics.setColor(Color.GREEN);
718         graphics.fillRect(0, 0, result.getWidth(), result.getHeight());
719         for (Short stream : this.streams)
720         {
721             switch (laneType(stream))
722             {
723                 case BICYCLE_LANE:
724                     graphics.setColor(Color.RED);
725                     break;
726 
727                 case CAR_LANE:
728                     graphics.setColor(Color.BLACK);
729                     break;
730 
731                 case PEDESTRIAN_LANE:
732                     graphics.setColor(Color.BLUE);
733                     break;
734 
735                 case PUBLIC_TRANSIT_LANE:
736                     graphics.setColor(Color.BLACK);
737                     break;
738 
739                 default:
740                     graphics.setColor(Color.WHITE);
741                     break;
742 
743             }
744             XYPair[] path = this.routes.get(stream);
745             if (null == path)
746             {
747                 Logger.ots().error("Cannot find path for stream {}", stream);
748                 continue;
749             }
750             XYPair prevPair = null;
751             for (XYPair xyPair : path)
752             {
753                 if (null != prevPair)
754                 {
755                     int dx = (int) Math.signum(xyPair.getX() - prevPair.getX());
756                     int dy = (int) Math.signum(xyPair.getY() - prevPair.getY());
757                     int x = prevPair.getX() + dx;
758                     int y = prevPair.getY() + dy;
759                     while (x != xyPair.getX() || y != xyPair.getY())
760                     {
761                         fillXYPair(graphics, new XYPair(x, y));
762                         if (x != xyPair.getX())
763                         {
764                             x += dx;
765                         }
766                         if (y != xyPair.getY())
767                         {
768                             y += dy;
769                         }
770                     }
771 
772                 }
773                 fillXYPair(graphics, xyPair);
774                 prevPair = xyPair;
775             }
776         }
777         return result;
778     }
779 
780     /**
781      * Fill one box taking care to rotate to display conventions.
782      * @param graphics the graphics environment
783      * @param xyPair the box to fill
784      */
785     private void fillXYPair(final Graphics2D graphics, final XYPair xyPair)
786     {
787         int cellSize = 10;
788         graphics.fillRect(cellSize * (BOUNDARY - xyPair.getX()), cellSize * (BOUNDARY - xyPair.getY()), cellSize, cellSize);
789     }
790 
791     /**
792      * Test the Diagram code.
793      * @param args the command line arguments (not used)
794      */
795     public static void main(final String[] args)
796     {
797         SwingUtilities.invokeLater(new Runnable()
798         {
799             @Override
800             public void run()
801             {
802                 JFrame frame = new JFrame("Diagram test");
803                 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
804                 frame.setMinimumSize(new Dimension(1000, 1000));
805                 JPanel mainPanel = new JPanel(new BorderLayout());
806                 frame.add(mainPanel);
807                 checkBoxPanel = new JPanel();
808                 checkBoxPanel.setLayout(new BoxLayout(checkBoxPanel, BoxLayout.Y_AXIS));
809                 JScrollPane scrollPane = new JScrollPane(checkBoxPanel);
810                 scrollPane.setPreferredSize(new Dimension(150, 1000));
811                 mainPanel.add(scrollPane, BorderLayout.LINE_START);
812                 for (int stream = 1; stream <= 12; stream++)
813                 {
814                     checkBoxPanel.add(makeCheckBox(stream, stream % 3 == 2));
815                 }
816                 for (int stream = 21; stream <= 28; stream++)
817                 {
818                     checkBoxPanel.add(makeCheckBox(stream, false));
819                 }
820                 for (int stream = 31; stream <= 38; stream++)
821                 {
822                     checkBoxPanel.add(makeCheckBox(stream, false));
823                 }
824                 for (int stream = 41; stream <= 52; stream++)
825                 {
826                     checkBoxPanel.add(makeCheckBox(stream, false));
827                 }
828                 for (int stream = 61; stream <= 72; stream++)
829                 {
830                     if (stream % 3 == 1)
831                     {
832                         continue;
833                     }
834                     checkBoxPanel.add(makeCheckBox(stream, false));
835                 }
836                 testPanel = new JPanel();
837                 rebuildTestPanel();
838                 mainPanel.add(testPanel, BorderLayout.CENTER);
839                 frame.setVisible(true);
840             }
841         });
842 
843     }
844 
845     /**
846      * Make a check box to switch a particular stream number on or off.
847      * @param stream the stream number
848      * @param initialState if true; the check box will be checked
849      * @return JCheckBox
850      */
851     public static JCheckBox makeCheckBox(final int stream, final boolean initialState)
852     {
853         JCheckBox result = new JCheckBox(String.format("Stream %02d", stream));
854         result.setSelected(initialState);
855         result.addActionListener(new ActionListener()
856         {
857 
858             @Override
859             public void actionPerformed(final ActionEvent e)
860             {
861                 rebuildTestPanel();
862             }
863         });
864         return result;
865     }
866 
867     /** JPanel used to render the intersection for testing. */
868     private static JPanel testPanel = null;
869 
870     /** JPanel that holds all the check boxes. */
871     private static JPanel checkBoxPanel = null;
872 
873     /**
874      * Render the intersection.
875      */
876     static void rebuildTestPanel()
877     {
878         testPanel.removeAll();
879         Set<Short> streamList = new LinkedHashSet<>();
880         for (Component c : checkBoxPanel.getComponents())
881         {
882             if (c instanceof JCheckBox)
883             {
884                 JCheckBox checkBox = (JCheckBox) c;
885                 if (checkBox.isSelected())
886                 {
887                     String caption = checkBox.getText();
888                     String streamText = caption.substring(caption.length() - 2);
889                     Short stream = Short.parseShort(streamText);
890                     streamList.add(stream);
891                 }
892             }
893         }
894         try
895         {
896             Diagram diagram = new Diagram(streamList);
897             testPanel.add(new JLabel(new ImageIcon(diagram.render())));
898         }
899         catch (TrafficControlException exception)
900         {
901             exception.printStackTrace();
902         }
903         testPanel.repaint();
904         testPanel.revalidate();
905     }
906 
907 }