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