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