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