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://tudelft.nl/staff/p.knoppers-1">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 Set&lt;Short&gt;; 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 short; the number of the stream to check
265      * @return boolean; 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 int; the number of the object
275      * @return boolean; 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 int; the standard Dutch traffic stream number
286      * @return LaneType; 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 XYPair; the XYPair
327      * @param rotation int; rotation in multiples of 90 degrees
328      * @return int; 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 XYPair; the XYPair
351      * @param rotation int; rotation in multiples of 90 degrees
352      * @return int; 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 int; the X object at the end of this route step
415          * @param y int; 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 int; the X object at the end of this route step
428          * @param y int; the Y object at the end of this route step
429          * @param command 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 Command; an IF, ELSE, ENDIF, or ELSE_IF command
450          * @param streamCondition int; 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 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 int; the X object
484          */
485         public int getX()
486         {
487             return this.x;
488         }
489 
490         /**
491          * Retrieve the Y object.
492          * @return int; 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 int; the streamCondition
511          */
512         public int getStreamCondition()
513         {
514             return this.streamCondition;
515         }
516 
517         /** {@inheritDoc} */
518         @Override
519         public String toString()
520         {
521             return "RouteStep [x=" + this.x + ", y=" + this.y + ", command=" + this.command + ", streamCondition="
522                     + this.streamCondition + "]";
523         }
524 
525     }
526 
527     /**
528      * Pack two integer coordinates in one object.
529      */
530     class XYPair
531     {
532         /** X. */
533         private final int x;
534 
535         /** Y. */
536         private final int y;
537 
538         /**
539          * Construct a new XY pair.
540          * @param x int; the X value
541          * @param y int; the Y value
542          */
543         XYPair(final int x, final int y)
544         {
545             this.x = x;
546             this.y = y;
547         }
548 
549         /**
550          * Construct a new XY pair from a route step.
551          * @param routeStep RouteStep; the route step
552          */
553         XYPair(final RouteStep routeStep)
554         {
555             this.x = routeStep.getX();
556             this.y = routeStep.getY();
557         }
558 
559         /**
560          * Construct a rotated version of an XYPair.
561          * @param in XYPair; the initial version
562          * @param quadrant int; the quadrant
563          */
564         XYPair(final XYPair in, final int quadrant)
565         {
566             this.x = rotatedX(in, quadrant);
567             this.y = rotatedY(in, quadrant);
568         }
569 
570         /**
571          * Retrieve the X value.
572          * @return int; the X value
573          */
574         public int getX()
575         {
576             return this.x;
577         }
578 
579         /**
580          * Retrieve the Y value.
581          * @return int; the Y value
582          */
583         public int getY()
584         {
585             return this.y;
586         }
587 
588         /** {@inheritDoc} */
589         @Override
590         public String toString()
591         {
592             return "XYPair [x=" + this.x + ", y=" + this.y + "]";
593         }
594 
595     }
596 
597     /**
598      * Construct a route.
599      * @param quadrant int; the quadrant to assemble the route for
600      * @param steps RouteStep...; an array, or series of arguments of type RouteStep
601      * @return XYPair[]; an array of XY pairs describing the route through the intersection
602      * @throws TrafficLightException when the route contains commands other than NO_OP and STOP_LINE
603      */
604     private XYPair[] rotateRoute(final int quadrant, final RouteStep... steps) throws TrafficControlException
605     {
606         List<XYPair> route = new ArrayList<>();
607         boolean on = true;
608 
609         for (RouteStep step : steps)
610         {
611             switch (step.getCommand())
612             {
613                 case NO_OP:
614                 case STOP_LINE:
615                 case ICON:
616                 case STOP_LINE_AND_ICON:
617                     if (on)
618                     {
619                         route.add(new XYPair(new XYPair(step), quadrant));
620                     }
621                     break;
622 
623                 default:
624                     throw new TrafficControlException("Bad command in rotateRoute: " + step.getCommand());
625 
626             }
627         }
628         return route.toArray(new XYPair[route.size()]);
629     }
630 
631     /**
632      * Construct a route through the intersection.
633      * @param steps RouteStep...; the steps of the route description
634      * @return RouteStep[]; the route through the intersection
635      * @throws TrafficControlException when something is very wrong
636      */
637     private RouteStep[] assembleRoute(final RouteStep... steps) throws TrafficControlException
638     {
639         List<RouteStep> result = new ArrayList<>();
640         RouteStep step;
641         for (int pointNo = 0; null != (step = routePoint(pointNo, steps)); pointNo++)
642         {
643             result.add(step);
644         }
645         return result.toArray(new RouteStep[result.size()]);
646     }
647 
648     /**
649      * Return the Nth step in a route.
650      * @param pointNo int; the rank of the requested step
651      * @param steps RouteStep...; RouteStep... the steps
652      * @return RouteStep; the Nth step in the route or null if the route does not have <code>pointNo</code> steps
653      * @throws TrafficControlException when the command in a routestep is not recognized
654      */
655     private RouteStep routePoint(final int pointNo, final RouteStep... steps) throws TrafficControlException
656     {
657         boolean active = true;
658         boolean beenActive = false;
659         int index = 0;
660 
661         for (RouteStep routeStep : steps)
662         {
663             switch (routeStep.getCommand())
664             {
665                 case NO_OP:
666                 case STOP_LINE:
667                 case ICON:
668                 case STOP_LINE_AND_ICON:
669                     if (active)
670                     {
671                         if (index++ == pointNo)
672                         {
673                             return routeStep;
674                         }
675                     }
676                     break;
677 
678                 case IF:
679                     active = streamExists((short) routeStep.getStreamCondition());
680                     beenActive = active;
681                     break;
682 
683                 case ELSE_IF:
684                     if (active)
685                     {
686                         active = false;
687                     }
688                     else if (!beenActive)
689                     {
690                         active = this.streams.contains(routeStep.getStreamCondition());
691                     }
692                     if (active)
693                     {
694                         beenActive = true;
695                     }
696                     break;
697 
698                 case ELSE:
699                     active = !beenActive;
700                     break;
701 
702                 case END_IF:
703                     active = true;
704                     break;
705 
706                 default:
707                     throw new TrafficControlException("Bad switch: " + routeStep);
708 
709             }
710         }
711         return null;
712     }
713 
714     /**
715      * Create a BufferedImage and render the schematic on it.
716      * @return BufferedImage
717      */
718     public BufferedImage render()
719     {
720         int range = 2 * BOUNDARY + 1;
721         int cellSize = 10;
722         BufferedImage result = new BufferedImage(range * cellSize, range * cellSize, BufferedImage.TYPE_INT_RGB);
723         Graphics2D graphics = (Graphics2D) result.getGraphics();
724         graphics.setColor(Color.GREEN);
725         graphics.fillRect(0, 0, result.getWidth(), result.getHeight());
726         for (Short stream : this.streams)
727         {
728             switch (laneType(stream))
729             {
730                 case BICYCLE_LANE:
731                     graphics.setColor(Color.RED);
732                     break;
733 
734                 case CAR_LANE:
735                     graphics.setColor(Color.BLACK);
736                     break;
737 
738                 case PEDESTRIAN_LANE:
739                     graphics.setColor(Color.BLUE);
740                     break;
741 
742                 case PUBLIC_TRANSIT_LANE:
743                     graphics.setColor(Color.BLACK);
744                     break;
745 
746                 default:
747                     graphics.setColor(Color.WHITE);
748                     break;
749 
750             }
751             XYPair[] path = this.routes.get(stream);
752             if (null == path)
753             {
754                 System.err.println("Cannot find path for stream " + stream);
755                 continue;
756             }
757             XYPair prevPair = null;
758             for (XYPair xyPair : path)
759             {
760                 if (null != prevPair)
761                 {
762                     int dx = (int) Math.signum(xyPair.getX() - prevPair.getX());
763                     int dy = (int) Math.signum(xyPair.getY() - prevPair.getY());
764                     int x = prevPair.getX() + dx;
765                     int y = prevPair.getY() + dy;
766                     while (x != xyPair.getX() || y != xyPair.getY())
767                     {
768                         fillXYPair(graphics, new XYPair(x, y));
769                         if (x != xyPair.getX())
770                         {
771                             x += dx;
772                         }
773                         if (y != xyPair.getY())
774                         {
775                             y += dy;
776                         }
777                     }
778 
779                 }
780                 fillXYPair(graphics, xyPair);
781                 prevPair = xyPair;
782             }
783         }
784         return result;
785     }
786 
787     /**
788      * Fill one box taking care to rotate to display conventions.
789      * @param graphics Graphics2D; the graphics environment
790      * @param xyPair XYPair; the box to fill
791      */
792     private void fillXYPair(final Graphics2D graphics, final XYPair xyPair)
793     {
794         int cellSize = 10;
795         graphics.fillRect(cellSize * (BOUNDARY - xyPair.getX()), cellSize * (BOUNDARY - xyPair.getY()), cellSize, cellSize);
796     }
797 
798     /**
799      * Test the Diagram code.
800      * @param args String[]; the command line arguments (not used)
801      */
802     public static void main(final String[] args)
803     {
804         SwingUtilities.invokeLater(new Runnable()
805         {
806             @Override
807             public void run()
808             {
809                 JFrame frame = new JFrame("Diagram test");
810                 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
811                 frame.setMinimumSize(new Dimension(1000, 1000));
812                 JPanel mainPanel = new JPanel(new BorderLayout());
813                 frame.add(mainPanel);
814                 checkBoxPanel = new JPanel();
815                 checkBoxPanel.setLayout(new BoxLayout(checkBoxPanel, BoxLayout.Y_AXIS));
816                 JScrollPane scrollPane = new JScrollPane(checkBoxPanel);
817                 scrollPane.setPreferredSize(new Dimension(150, 1000));
818                 mainPanel.add(scrollPane, BorderLayout.LINE_START);
819                 for (int stream = 1; stream <= 12; stream++)
820                 {
821                     checkBoxPanel.add(makeCheckBox(stream, stream % 3 == 2));
822                 }
823                 for (int stream = 21; stream <= 28; stream++)
824                 {
825                     checkBoxPanel.add(makeCheckBox(stream, false));
826                 }
827                 for (int stream = 31; stream <= 38; stream++)
828                 {
829                     checkBoxPanel.add(makeCheckBox(stream, false));
830                 }
831                 for (int stream = 41; stream <= 52; stream++)
832                 {
833                     checkBoxPanel.add(makeCheckBox(stream, false));
834                 }
835                 for (int stream = 61; stream <= 72; stream++)
836                 {
837                     if (stream % 3 == 1)
838                     {
839                         continue;
840                     }
841                     checkBoxPanel.add(makeCheckBox(stream, false));
842                 }
843                 testPanel = new JPanel();
844                 rebuildTestPanel();
845                 mainPanel.add(testPanel, BorderLayout.CENTER);
846                 frame.setVisible(true);
847             }
848         });
849 
850     }
851 
852     /**
853      * Make a check box to switch a particular stream number on or off.
854      * @param stream int; the stream number
855      * @param initialState boolean; if true; the check box will be checked
856      * @return JCheckBox
857      */
858     public static JCheckBox makeCheckBox(final int stream, final boolean initialState)
859     {
860         JCheckBox result = new JCheckBox(String.format("Stream %02d", stream));
861         result.setSelected(initialState);
862         result.addActionListener(new ActionListener()
863         {
864 
865             @Override
866             public void actionPerformed(final ActionEvent e)
867             {
868                 rebuildTestPanel();
869             }
870         });
871         return result;
872     }
873 
874     /** JPanel used to render the intersection for testing. */
875     private static JPanel testPanel = null;
876 
877     /** JPanel that holds all the check boxes. */
878     private static JPanel checkBoxPanel = null;
879 
880     /**
881      * Render the intersection.
882      */
883     static void rebuildTestPanel()
884     {
885         testPanel.removeAll();
886         Set<Short> streamList = new LinkedHashSet<>();
887         for (Component c : checkBoxPanel.getComponents())
888         {
889             if (c instanceof JCheckBox)
890             {
891                 JCheckBox checkBox = (JCheckBox) c;
892                 if (checkBox.isSelected())
893                 {
894                     String caption = checkBox.getText();
895                     String streamText = caption.substring(caption.length() - 2);
896                     Short stream = Short.parseShort(streamText);
897                     streamList.add(stream);
898                 }
899             }
900         }
901         try
902         {
903             Diagram diagram = new Diagram(streamList);
904             testPanel.add(new JLabel(new ImageIcon(diagram.render())));
905         }
906         catch (TrafficControlException exception)
907         {
908             exception.printStackTrace();
909         }
910         testPanel.repaint();
911         testPanel.revalidate();
912     }
913 
914 }