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