View Javadoc
1   package org.opentrafficsim.draw.graphs;
2   
3   import java.awt.BasicStroke;
4   import java.awt.Color;
5   import java.awt.Paint;
6   import java.awt.Shape;
7   import java.awt.Stroke;
8   import java.awt.geom.CubicCurve2D;
9   import java.awt.geom.Line2D;
10  import java.util.ArrayList;
11  import java.util.LinkedHashMap;
12  import java.util.List;
13  import java.util.Map;
14  
15  import org.djunits.value.vdouble.scalar.Duration;
16  import org.djunits.value.vdouble.scalar.Length;
17  import org.djunits.value.vdouble.scalar.Time;
18  import org.djutils.exceptions.Try;
19  import org.jfree.chart.ChartMouseListener;
20  import org.jfree.chart.JFreeChart;
21  import org.jfree.chart.LegendItem;
22  import org.jfree.chart.LegendItemCollection;
23  import org.jfree.chart.axis.NumberAxis;
24  import org.jfree.chart.entity.EntityCollection;
25  import org.jfree.chart.entity.XYItemEntity;
26  import org.jfree.chart.labels.XYToolTipGenerator;
27  import org.jfree.chart.plot.XYPlot;
28  import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
29  import org.jfree.data.DomainOrder;
30  import org.jfree.data.xy.XYDataset;
31  import org.opentrafficsim.core.animation.gtu.colorer.IDGTUColorer;
32  import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
33  import org.opentrafficsim.draw.core.BoundsPaintScale;
34  import org.opentrafficsim.draw.graphs.GraphPath.Section;
35  import org.opentrafficsim.kpi.sampling.KpiLaneDirection;
36  import org.opentrafficsim.kpi.sampling.Sampler;
37  import org.opentrafficsim.kpi.sampling.SamplingException;
38  import org.opentrafficsim.kpi.sampling.Trajectory;
39  import org.opentrafficsim.kpi.sampling.TrajectoryGroup;
40  
41  /**
42   * Plot of trajectories along a path.
43   * <p>
44   * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
45   * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
46   * <p>
47   * @version $Revision$, $LastChangedDate$, by $Author$, initial version 13 okt. 2018 <br>
48   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
49   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
50   * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
51   */
52  public class TrajectoryPlot extends AbstractSamplerPlot implements XYDataset
53  {
54      /** */
55      private static final long serialVersionUID = 20181013L;
56  
57      /** Single shape to provide due to non-null requirement, but actually not used. */
58      private static final Shape NO_SHAPE = new Line2D.Float(0, 0, 0, 0);
59  
60      /** Color map. */
61      private static final Color[] COLORMAP;
62  
63      /** Strokes. */
64      private static final BasicStroke[] STROKES;
65  
66      /** Shape for the legend entries to draw the line over. */
67      private static final Shape LEGEND_LINE = new CubicCurve2D.Float(-20, 7, -10, -7, 0, 7, 20, -7);
68  
69      /** Updater for update times. */
70      private final GraphUpdater<Time> graphUpdater;
71  
72      /** Counter of the number of trajectories imported per lane. */
73      private final Map<KpiLaneDirection, Integer> knownTrajectories = new LinkedHashMap<>();
74  
75      /** Per lane, mapping from series rank number to trajectory. */
76      private List<List<OffsetTrajectory>> curves = new ArrayList<>();
77  
78      /** Stroke per series. */
79      private List<List<Stroke>> strokes = new ArrayList<>();
80  
81      /** Number of curves per lane. This may be less than the length of {@code List<OffsetTrajectory>} due to concurrency. */
82      private List<Integer> curvesPerLane = new ArrayList<>();
83  
84      /** Legend to change text color to indicate visibility. */
85      private LegendItemCollection legend;
86  
87      /** Whether each lane is visible or not. */
88      private final List<Boolean> laneVisible = new ArrayList<>();
89  
90      static
91      {
92          Color[] c = BoundsPaintScale.hue(6);
93          COLORMAP = new Color[] { c[0], c[4], c[2], c[1], c[3], c[5] };
94          float lw = 0.4f;
95          STROKES = new BasicStroke[] { new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 10.0f, null, 0.0f),
96                  new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 1.0f, new float[] { 13f, 4f }, 0.0f),
97                  new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 1.0f, new float[] { 11f, 3f, 2f, 3f },
98                          0.0f) };
99      }
100 
101     /**
102      * Constructor.
103      * @param caption String; caption
104      * @param updateInterval Duration; regular update interval (simulation time)
105      * @param simulator OTSSimulatorInterface; simulator
106      * @param sampler Sampler&lt;?&gt;; road sampler
107      * @param path GraphPath&lt;KpiLaneDirection&gt;; path
108      */
109     public TrajectoryPlot(final String caption, final Duration updateInterval, final OTSSimulatorInterface simulator,
110             final Sampler<?> sampler, final GraphPath<KpiLaneDirection> path)
111     {
112         super(caption, updateInterval, simulator, sampler, path, Duration.ZERO);
113         for (int i = 0; i < path.getNumberOfSeries(); i++)
114         {
115             this.curves.add(new ArrayList<>());
116             this.strokes.add(new ArrayList<>());
117             this.curvesPerLane.add(0);
118             this.laneVisible.add(true);
119         }
120         setChart(createChart());
121 
122         // setup updater to do the actual work in another thread
123         this.graphUpdater = new GraphUpdater<>("Trajectories worker", Thread.currentThread(), (t) ->
124         {
125             for (Section<KpiLaneDirection> section : path.getSections())
126             {
127                 Length startDistance = path.getStartDistance(section);
128                 for (int i = 0; i < path.getNumberOfSeries(); i++)
129                 {
130                     KpiLaneDirection lane = section.getSource(i);
131                     TrajectoryGroup<?> trajectoryGroup = getSampler().getTrajectoryGroup(lane);
132                     int from = this.knownTrajectories.getOrDefault(lane, 0);
133                     int to = trajectoryGroup.size();
134                     double scaleFactor = section.getLength().si / lane.getLaneData().getLength().si;
135                     for (Trajectory<?> trajectory : trajectoryGroup.getTrajectories().subList(from, to))
136                     {
137                         if (getPath().getNumberOfSeries() > 1)
138                         {
139                             // assign a stroke with random offset, otherwise it will look artificial
140                             BasicStroke stroke = STROKES[i % STROKES.length];
141                             if (stroke.getDashArray() != null)
142                             {
143                                 float dashLength = 0.0f;
144                                 for (float d : stroke.getDashArray())
145                                 {
146                                     dashLength += d;
147                                 }
148                                 stroke = new BasicStroke(stroke.getLineWidth(), stroke.getEndCap(), stroke.getLineJoin(),
149                                         stroke.getMiterLimit(), stroke.getDashArray(), (float) (Math.random() * dashLength));
150                             }
151                             this.strokes.get(i).add(stroke);
152                         }
153                         this.curves.get(i).add(
154                                 new OffsetTrajectory(trajectory, startDistance, scaleFactor, lane.getLaneData().getLength()));
155                     }
156                     this.knownTrajectories.put(lane, to);
157                 }
158             }
159         });
160     }
161 
162     /**
163      * Create a chart.
164      * @return JFreeChart; chart
165      */
166     private JFreeChart createChart()
167     {
168         NumberAxis xAxis = new NumberAxis("Time [s] \u2192");
169         NumberAxis yAxis = new NumberAxis("Distance [m] \u2192");
170         XYLineAndShapeRendererID renderer = new XYLineAndShapeRendererID();
171         XYPlot plot = new XYPlot(this, xAxis, yAxis, renderer);
172         boolean showLegend;
173         if (getPath().getNumberOfSeries() < 2)
174         {
175             plot.setFixedLegendItems(null);
176             showLegend = false;
177         }
178         else
179         {
180             this.legend = new LegendItemCollection();
181             for (int i = 0; i < getPath().getNumberOfSeries(); i++)
182             {
183                 LegendItem li = new LegendItem(getPath().getName(i));
184                 li.setSeriesKey(i); // lane series, not curve series
185                 li.setShape(STROKES[i & STROKES.length].createStrokedShape(LEGEND_LINE));
186                 li.setFillPaint(COLORMAP[i % COLORMAP.length]);
187                 this.legend.add(li);
188             }
189             plot.setFixedLegendItems(this.legend);
190             showLegend = true;
191         }
192         return new JFreeChart(getCaption(), JFreeChart.DEFAULT_TITLE_FONT, plot, showLegend);
193     }
194 
195     /** {@inheritDoc} This implementation creates a listener to disable and enable lanes through the legend. */
196     @Override
197     protected ChartMouseListener getChartMouseListener()
198     {
199         if (this.getPath().getNumberOfSeries() < 2)
200         {
201             return null;
202         }
203         return GraphUtil.getToggleSeriesByLegendListener(this.legend, this.laneVisible);
204     }
205 
206     /** {@inheritDoc} */
207     @Override
208     public GraphType getGraphType()
209     {
210         return GraphType.TRAJECTORY;
211     }
212 
213     /** {@inheritDoc} */
214     @Override
215     protected String getStatusLabel(final double domainValue, final double rangeValue)
216     {
217         return String.format("time %.0fs, distance %.0fm", domainValue, rangeValue);
218     }
219 
220     /** {@inheritDoc} */
221     @Override
222     protected void increaseTime(final Time time)
223     {
224         if (this.graphUpdater != null) // null during construction
225         {
226             this.graphUpdater.offer(time);
227         }
228     }
229 
230     /** {@inheritDoc} */
231     @Override
232     public int getSeriesCount()
233     {
234         int n = 0;
235         for (int i = 0; i < this.curves.size(); i++)
236         {
237             List<OffsetTrajectory> list = this.curves.get(i);
238             int m = list.size();
239             this.curvesPerLane.set(i, m);
240             n += m;
241         }
242         return n;
243     }
244 
245     /** {@inheritDoc} */
246     @Override
247     public Comparable<Integer> getSeriesKey(final int series)
248     {
249         return series;
250     }
251 
252     /** {@inheritDoc} */
253     @SuppressWarnings("rawtypes")
254     @Override
255     public int indexOf(final Comparable seriesKey)
256     {
257         return 0;
258     }
259 
260     /** {@inheritDoc} */
261     @Override
262     public DomainOrder getDomainOrder()
263     {
264         return DomainOrder.ASCENDING;
265     }
266 
267     /** {@inheritDoc} */
268     @Override
269     public int getItemCount(final int series)
270     {
271         OffsetTrajectory trajectory = getTrajectory(series);
272         return trajectory == null ? 0 : trajectory.size();
273     }
274 
275     /** {@inheritDoc} */
276     @Override
277     public Number getX(final int series, final int item)
278     {
279         return getXValue(series, item);
280     }
281 
282     /** {@inheritDoc} */
283     @Override
284     public double getXValue(final int series, final int item)
285     {
286         return getTrajectory(series).getT(item);
287     }
288 
289     /** {@inheritDoc} */
290     @Override
291     public Number getY(final int series, final int item)
292     {
293         return getYValue(series, item);
294     }
295 
296     /** {@inheritDoc} */
297     @Override
298     public double getYValue(final int series, final int item)
299     {
300         return getTrajectory(series).getX(item);
301     }
302 
303     /**
304      * Get the trajectory of the series number.
305      * @param series int; series
306      * @return OffsetTrajectory; trajectory of the series number
307      */
308     private OffsetTrajectory getTrajectory(final int series)
309     {
310         int[] n = getLaneAndSeriesNumber(series);
311         return this.curves.get(n[0]).get(n[1]);
312     }
313 
314     /**
315      * Returns the lane number, and series number within the lane data.
316      * @param series int; overall series number
317      * @return int[]; lane number, and series number within the lane data
318      */
319     private int[] getLaneAndSeriesNumber(final int series)
320     {
321         int n = series;
322         for (int i = 0; i < this.curves.size(); i++)
323         {
324             int m = this.curvesPerLane.get(i);
325             if (n < m)
326             {
327                 return new int[] { i, n };
328             }
329             n -= m;
330         }
331         throw new RuntimeException("Discrepancy between series number and available data.");
332     }
333 
334     /**
335      * Extension of a line renderer to select a color based on GTU ID, and to overrule an unused shape to save memory.
336      * <p>
337      * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
338      * <br>
339      * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
340      * <p>
341      * @version $Revision$, $LastChangedDate$, by $Author$, initial version 14 okt. 2018 <br>
342      * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
343      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
344      * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
345      */
346     private final class XYLineAndShapeRendererID extends XYLineAndShapeRenderer
347     {
348         /** */
349         private static final long serialVersionUID = 20181014L;
350 
351         /**
352          * Constructor.
353          */
354         XYLineAndShapeRendererID()
355         {
356             super(false, true);
357             setDefaultLinesVisible(true);
358             setDefaultShapesVisible(false);
359             setDrawSeriesLineAsPath(true);
360         }
361 
362         /** {@inheritDoc} */
363         @SuppressWarnings("synthetic-access")
364         @Override
365         public boolean isSeriesVisible(final int series)
366         {
367             int[] n = getLaneAndSeriesNumber(series);
368             return TrajectoryPlot.this.laneVisible.get(n[0]);
369         }
370 
371         /** {@inheritDoc} */
372         @SuppressWarnings("synthetic-access")
373         @Override
374         public Stroke getSeriesStroke(final int series)
375         {
376             if (TrajectoryPlot.this.curves.size() == 1)
377             {
378                 return STROKES[0];
379             }
380             int[] n = getLaneAndSeriesNumber(series);
381             return TrajectoryPlot.this.strokes.get(n[0]).get(n[1]);
382         }
383 
384         /** {@inheritDoc} */
385         @SuppressWarnings("synthetic-access")
386         @Override
387         public Paint getSeriesPaint(final int series)
388         {
389             if (TrajectoryPlot.this.curves.size() == 1)
390             {
391                 String gtuId = getTrajectory(series).getGtuId();
392                 for (int pos = gtuId.length(); --pos >= 0;)
393                 {
394                     Character c = gtuId.charAt(pos);
395                     if (Character.isDigit(c))
396                     {
397                         return IDGTUColorer.LEGEND.get(c - '0').getColor();
398                     }
399                 }
400             }
401             int[] n = getLaneAndSeriesNumber(series);
402             return COLORMAP[n[0] % COLORMAP.length];
403         }
404 
405         /**
406          * {@inheritDoc} Largely based on the super implementation, but returns a dummy shape for markers to save memory and as
407          * markers are not used.
408          */
409         @SuppressWarnings("synthetic-access")
410         @Override
411         protected void addEntity(final EntityCollection entities, final Shape hotspot, final XYDataset dataset,
412                 final int series, final int item, final double entityX, final double entityY)
413         {
414 
415             if (!getItemCreateEntity(series, item))
416             {
417                 return;
418             }
419 
420             // if not hotspot is provided, we create a default based on the
421             // provided data coordinates (which are already in Java2D space)
422             Shape hotspot2 = hotspot == null ? NO_SHAPE : hotspot;
423             String tip = null;
424             XYToolTipGenerator generator = getToolTipGenerator(series, item);
425             if (generator != null)
426             {
427                 tip = generator.generateToolTip(dataset, series, item);
428             }
429             String url = null;
430             if (getURLGenerator() != null)
431             {
432                 url = getURLGenerator().generateURL(dataset, series, item);
433             }
434             XYItemEntity entity = new XYItemEntity(hotspot2, dataset, series, item, tip, url);
435             entities.add(entity);
436         }
437 
438         /** {@inheritDoc} */
439         @Override
440         public String toString()
441         {
442             return "XYLineAndShapeRendererID []";
443         }
444 
445     }
446 
447     /**
448      * Class containing a trajectory with an offset. Takes care of bits that are before and beyond the lane without affecting
449      * the trajectory itself.
450      * <p>
451      * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
452      * <br>
453      * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
454      * <p>
455      * @version $Revision$, $LastChangedDate$, by $Author$, initial version 14 okt. 2018 <br>
456      * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
457      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
458      * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
459      */
460     private class OffsetTrajectory
461     {
462         /** The trajectory. */
463         private final Trajectory<?> trajectory;
464 
465         /** The offset. */
466         private final double offset;
467 
468         /** Scale factor for space dimension. */
469         private final double scaleFactor;
470 
471         /** First index of the trajectory to include, possibly cutting some measurements before the lane. */
472         private int first;
473 
474         /** Size of the trajectory to consider starting at first, possibly cutting some measurements beyond the lane. */
475         private int size;
476 
477         /** Length of the lane to determine {@code size}. */
478         private final Length laneLength;
479 
480         /**
481          * Construct a new TrajectoryAndLengthOffset object.
482          * @param trajectory Trajectory&lt;?&gt;; the trajectory
483          * @param offset Length; the length from the beginning of the sampled path to the start of the lane to which the
484          *            trajectory belongs
485          * @param scaleFactor double; scale factor for space dimension
486          * @param laneLength Length; length of the lane
487          */
488         OffsetTrajectory(final Trajectory<?> trajectory, final Length offset, final double scaleFactor, final Length laneLength)
489         {
490             this.trajectory = trajectory;
491             this.offset = offset.si;
492             this.scaleFactor = scaleFactor;
493             this.laneLength = laneLength;
494         }
495 
496         /**
497          * Returns the number of measurements in the trajectory.
498          * @return int; number of measurements in the trajectory
499          */
500         public final int size()
501         {
502             // as trajectories grow, this calculation needs to be done on each request
503             try
504             {
505                 /*
506                  * Note on overlap:
507                  * 
508                  * Suppose a GTU crosses a lane boundary producing the following events, where distance e->| is the front, and
509                  * |->l is the tail, relative to the reference point of the GTU.
510                  * @formatter:off
511                  * -------------------------------------------  o) regular move event
512                  *  o     e   o         o |  l    o         o   e) lane enter event on next lane
513                  * -------------------------------------------  l) lane leave event on previous lane
514                  *  o         o         o   (l)                 measurements on previous lane
515                  *       (e) (o)       (o)        o         o   measurements on next lane
516                  * @formatter:on
517                  * Trajectories of a particular GTU are not explicitly tied together. Not only would this involve quite some
518                  * work, it is also impossible to distinguish a lane change near the start or end of a lane, from moving
519                  * longitudinally on to the next lane. The basic idea to minimize overlap is to remove all positions on the
520                  * previous lane beyond the lane length, and all negative positions on the next lane, i.e. all between ( ). This
521                  * would however create a gap at the lane boundary '|'. Allowing one event beyond the lane length may still
522                  * result in a gap, l->o in this case. Allowing one event before the lane would work in this case, but 'e' could
523                  * also fall between an 'o' and '|'. At one trajectory it is thus not known whether the other trajectory
524                  * continues from, or is continued from, the extra point. Hence we require an extra point before the lane and
525                  * one beyond the lane to assure there is no gap. The resulting overlap can be as large as a move, but this is
526                  * better than occasional gaps.
527                  */
528                 int f = 0;
529                 while (f < this.trajectory.size() - 1 && this.trajectory.getX(f + 1) < 0.0)
530                 {
531                     f++;
532                 }
533                 this.first = f;
534                 int s = this.trajectory.size() - 1;
535                 while (s > 1 && this.trajectory.getX(s - 1) > this.laneLength.si)
536                 {
537                     s--;
538                 }
539                 this.size = s - f + 1;
540             }
541             catch (SamplingException exception)
542             {
543                 throw new RuntimeException("Unexpected exception while obtaining location value from trajectory for plotting.",
544                         exception);
545             }
546             return this.size;
547         }
548 
549         /**
550          * Returns the location, including offset, of an item.
551          * @param item int; item (sample) number
552          * @return double; location, including offset, of an item
553          */
554         public final double getX(final int item)
555         {
556             return Try.assign(() -> this.offset + this.trajectory.getX(this.first + item) * this.scaleFactor,
557                     "Unexpected exception while obtaining location value from trajectory for plotting.");
558         }
559 
560         /**
561          * Returns the time of an item.
562          * @param item int; item (sample) number
563          * @return double; time of an item
564          */
565         public final double getT(final int item)
566         {
567             return Try.assign(() -> (double) this.trajectory.getT(this.first + item),
568                     "Unexpected exception while obtaining time value from trajectory for plotting.");
569         }
570 
571         /**
572          * Returns the ID of the GTU of this trajectory.
573          * @return String; the ID of the GTU of this trajectory
574          */
575         public final String getGtuId()
576         {
577             return this.trajectory.getGtuId();
578         }
579 
580         /** {@inheritDoc} */
581         @Override
582         public final String toString()
583         {
584             return "OffsetTrajectory [trajectory=" + this.trajectory + ", offset=" + this.offset + "]";
585         }
586 
587     }
588 
589     /** {@inheritDoc} */
590     @Override
591     public String toString()
592     {
593         return "TrajectoryPlot [graphUpdater=" + this.graphUpdater + ", knownTrajectories=" + this.knownTrajectories
594                 + ", curves=" + this.curves + ", strokes=" + this.strokes + ", curvesPerLane=" + this.curvesPerLane
595                 + ", legend=" + this.legend + ", laneVisible=" + this.laneVisible + "]";
596     }
597 
598 }