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