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