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