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.Rectangle;
7   import java.awt.Shape;
8   import java.awt.Stroke;
9   import java.awt.geom.CubicCurve2D;
10  import java.awt.geom.Line2D;
11  import java.util.ArrayList;
12  import java.util.LinkedHashMap;
13  import java.util.List;
14  import java.util.Map;
15  
16  import org.djunits.value.vdouble.scalar.Duration;
17  import org.djunits.value.vdouble.scalar.Length;
18  import org.djutils.exceptions.Throw;
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.chart.title.PaintScaleLegend;
29  import org.jfree.chart.ui.RectangleEdge;
30  import org.jfree.chart.ui.RectangleInsets;
31  import org.jfree.data.DomainOrder;
32  import org.jfree.data.xy.XYDataset;
33  import org.opentrafficsim.base.OtsRuntimeException;
34  import org.opentrafficsim.draw.Colors;
35  import org.opentrafficsim.draw.colorer.ColorbarColorer;
36  import org.opentrafficsim.draw.colorer.Colorer;
37  import org.opentrafficsim.draw.colorer.LegendColorer;
38  import org.opentrafficsim.draw.colorer.LegendColorer.LegendEntry;
39  import org.opentrafficsim.draw.colorer.trajectory.TrajectoryColorer;
40  import org.opentrafficsim.draw.graphs.GraphPath.Section;
41  import org.opentrafficsim.draw.graphs.OffsetTrajectory.TrajectorySection;
42  import org.opentrafficsim.kpi.interfaces.LaneData;
43  import org.opentrafficsim.kpi.sampling.SamplerData;
44  import org.opentrafficsim.kpi.sampling.Trajectory;
45  import org.opentrafficsim.kpi.sampling.TrajectoryGroup;
46  
47  /**
48   * Plot of trajectories along a path.
49   * <p>
50   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
51   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
52   * </p>
53   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
54   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
55   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
56   */
57  public class TrajectoryPlot extends AbstractSamplerPlot implements XYDataset
58  {
59      /** Single shape to provide due to non-null requirement, but actually not used. */
60      private static final Shape NO_SHAPE = new Line2D.Float(0, 0, 0, 0);
61  
62      /** Color map for multiple curves. */
63      private static final Color[] COLORMAP;
64  
65      /** Strokes. */
66      private static final BasicStroke[] STROKES;
67  
68      /** Shape for the legend entries to draw the line over. */
69      private static final Shape LEGEND_LINE = new CubicCurve2D.Float(-20, 7, -10, -7, 0, 7, 20, -7);
70  
71      /** Updater for update times. */
72      private final GraphUpdater<Duration> graphUpdater;
73  
74      /** Counter of the number of trajectories imported per lane. */
75      private final Map<LaneData<?>, Integer> knownTrajectories = new LinkedHashMap<>();
76  
77      /** Per lane, mapping from series rank number to trajectory. */
78      private List<List<OffsetTrajectory>> curves = new ArrayList<>();
79  
80      /** Stroke per series. */
81      private List<List<Stroke>> strokes = new ArrayList<>();
82  
83      /** Number of curves per lane. This may be less than the length of {@code List<OffsetTrajectory>} due to concurrency. */
84      private List<Integer> curvesPerLane = new ArrayList<>();
85  
86      /** Legend to change text color to indicate visibility. */
87      private LegendItemCollection legend;
88  
89      /** Whether each lane is visible or not. */
90      private final List<Boolean> laneVisible = new ArrayList<>();
91  
92      /** Colorer. */
93      private Colorer<? super TrajectorySection> colorer;
94  
95      /** Line renderer. */
96      private XYLineAndShapeRendererColor renderer;
97  
98      /** Color bar. */
99      private PaintScaleLegend colorbar;
100 
101     static
102     {
103         Color[] c = Colors.hue(6);
104         COLORMAP = new Color[] {c[0], c[4], c[2].darker().darker(), c[1], c[3], c[5]};
105         float lw = 1.0f;
106         STROKES = new BasicStroke[] {new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 10.0f, null, 0.0f),
107                 new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 1.0f, new float[] {13f, 4f}, 0.0f),
108                 new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 1.0f, new float[] {11f, 3f, 2f, 3f}, 0.0f)};
109     }
110 
111     /**
112      * Constructor.
113      * @param caption caption
114      * @param updateInterval regular update interval (simulation time)
115      * @param scheduler scheduler.
116      * @param samplerData sampler data
117      * @param path path
118      * @throws IllegalArgumentException when the path contains more than 6 lanes
119      */
120     public TrajectoryPlot(final String caption, final Duration updateInterval, final PlotScheduler scheduler,
121             final SamplerData<?> samplerData, final GraphPath<? extends LaneData<?>> path)
122     {
123         super(caption, updateInterval, scheduler, samplerData, path, Duration.ZERO);
124         Throw.when(path.getNumberOfSeries() > 6, IllegalArgumentException.class, "The trajectory plot supports up to 6 lanes");
125         for (int i = 0; i < path.getNumberOfSeries(); i++)
126         {
127             this.curves.add(new ArrayList<>());
128             this.strokes.add(new ArrayList<>());
129             this.curvesPerLane.add(0);
130             this.laneVisible.add(true);
131         }
132         setChart(createChart());
133 
134         // setup updater to do the actual work in another thread
135         this.graphUpdater = new GraphUpdater<>("Trajectories worker", Thread.currentThread(), (t) ->
136         {
137             for (Section<? extends LaneData<?>> section : path.getSections())
138             {
139                 Length startDistance = path.getStartDistance(section);
140                 for (int i = 0; i < path.getNumberOfSeries(); i++)
141                 {
142                     LaneData<?> lane = section.getSource(i);
143                     if (lane == null)
144                     {
145                         continue; // lane is not part of this section, e.g. after a lane-drop
146                     }
147                     TrajectoryGroup<?> trajectoryGroup = getSamplerData().getTrajectoryGroup(lane).orElse(null);
148                     if (trajectoryGroup == null)
149                     {
150                         // recording of data not yet started
151                         return;
152                     }
153                     int from = this.knownTrajectories.getOrDefault(lane, 0);
154                     int to = trajectoryGroup.size();
155                     double scaleFactor = section.length().si / lane.getLength().si;
156                     for (Trajectory<?> trajectory : trajectoryGroup.getTrajectories().subList(from, to))
157                     {
158                         if (getPath().getNumberOfSeries() > 1)
159                         {
160                             // assign a stroke with random offset, otherwise it will look artificial
161                             BasicStroke stroke = STROKES[i % STROKES.length];
162                             if (stroke.getDashArray() != null)
163                             {
164                                 float dashLength = 0.0f;
165                                 for (float d : stroke.getDashArray())
166                                 {
167                                     dashLength += d;
168                                 }
169                                 stroke = new BasicStroke(stroke.getLineWidth(), stroke.getEndCap(), stroke.getLineJoin(),
170                                         stroke.getMiterLimit(), stroke.getDashArray(), (float) (Math.random() * dashLength));
171                             }
172                             this.strokes.get(i).add(stroke);
173                         }
174                         this.curves.get(i).add(new OffsetTrajectory(trajectory, startDistance, scaleFactor));
175                     }
176                     this.knownTrajectories.put(lane, to);
177                 }
178             }
179         });
180     }
181 
182     /**
183      * Create a chart.
184      * @return chart
185      */
186     private JFreeChart createChart()
187     {
188         NumberAxis xAxis = new NumberAxis("Time [s] \u2192");
189         NumberAxis yAxis = new NumberAxis("Distance [m] \u2192");
190         this.renderer = new XYLineAndShapeRendererColor();
191         XYPlot plot = new XYPlot(this, xAxis, yAxis, this.renderer);
192         if (getPath().getNumberOfSeries() > 1)
193         {
194             this.legend = new LegendItemCollection();
195             for (int i = 0; i < getPath().getNumberOfSeries(); i++)
196             {
197                 LegendItem li = new LegendItem(getPath().getName(i));
198                 li.setSeriesKey(i); // lane series, not curve series
199                 li.setShape(STROKES[i & STROKES.length].createStrokedShape(LEGEND_LINE));
200                 li.setFillPaint(COLORMAP[i % COLORMAP.length]);
201                 this.legend.add(li);
202             }
203             plot.setFixedLegendItems(this.legend);
204         }
205         return new JFreeChart(getCaption(), JFreeChart.DEFAULT_TITLE_FONT, plot, true);
206     }
207 
208     /**
209      * Sets the color renderer for trajectories.
210      * @param colorer color renderer
211      */
212     public void setColorer(final TrajectoryColorer colorer)
213     {
214         this.colorer = colorer;
215         this.renderer.setDrawSeriesLineAsPath(colorer.isSingleColor());
216         if (getPath().getNumberOfSeries() < 2)
217         {
218             if (this.colorbar != null)
219             {
220                 getChart().removeSubtitle(this.colorbar);
221             }
222             LegendItemCollection colorerLegend = new LegendItemCollection();
223             if (colorer instanceof ColorbarColorer<?> colorbarColorer)
224             {
225                 NumberAxis scaleAxis = new NumberAxis("");
226                 scaleAxis.setNumberFormatOverride(colorbarColorer.getNumberFormat());
227                 // increase tick insets from [t=2.0,l=4.0,b=2.0,r=4.0] to let the automatic ticks be less cluttered
228                 scaleAxis.setTickLabelInsets(new RectangleInsets(5.0, 4.0, 5.0, 4.0));
229                 this.colorbar = new PaintScaleLegend(colorbarColorer.getBoundsPaintScale(), scaleAxis);
230                 this.colorbar.setSubdivisionCount(256);
231                 this.colorbar.setPosition(RectangleEdge.RIGHT);
232                 // some padding to make space for last tick number on adjacent axes, and vertically match those axes
233                 this.colorbar.setPadding(10.0, 15.0, 40.0, 10.0);
234                 getChart().addSubtitle(this.colorbar);
235             }
236             else if (colorer instanceof LegendColorer<?> legendColorer)
237             {
238 
239                 for (LegendEntry entry : legendColorer.getLegend())
240                 {
241                     colorerLegend.add(new LegendItem(entry.name(), entry.name(), entry.name(), entry.name(),
242                             new Rectangle(10, 10), entry.color(), new BasicStroke(0.5f), Color.BLACK));
243                 }
244             }
245             ((XYPlot) getChart().getPlot()).setFixedLegendItems(colorerLegend);
246         }
247     }
248 
249     @Override
250     public GraphType getGraphType()
251     {
252         return GraphType.TRAJECTORY;
253     }
254 
255     @Override
256     public String getStatusLabel(final double domainValue, final double rangeValue)
257     {
258         return String.format("time %.0fs, distance %.0fm", domainValue, rangeValue);
259     }
260 
261     @Override
262     protected void increaseTime(final Duration time)
263     {
264         if (this.graphUpdater != null) // null during construction
265         {
266             this.graphUpdater.offer(time);
267         }
268     }
269 
270     @Override
271     public int getSeriesCount()
272     {
273         int n = 0;
274         for (int i = 0; i < this.curves.size(); i++)
275         {
276             List<OffsetTrajectory> list = this.curves.get(i);
277             int m = list.size();
278             this.curvesPerLane.set(i, m);
279             n += m;
280         }
281         return n;
282     }
283 
284     /**
285      * Returns the number of lanes.
286      * @return the number of lanes
287      */
288     public int getLaneCount()
289     {
290         return this.curves.size();
291     }
292 
293     @Override
294     public Comparable<Integer> getSeriesKey(final int series)
295     {
296         return series;
297     }
298 
299     @SuppressWarnings("rawtypes")
300     @Override
301     public int indexOf(final Comparable seriesKey)
302     {
303         return 0;
304     }
305 
306     @Override
307     public DomainOrder getDomainOrder()
308     {
309         return DomainOrder.ASCENDING;
310     }
311 
312     @Override
313     public int getItemCount(final int series)
314     {
315         OffsetTrajectory trajectory = getTrajectory(series);
316         return trajectory == null ? 0 : trajectory.size();
317     }
318 
319     @Override
320     public Number getX(final int series, final int item)
321     {
322         return getXValue(series, item);
323     }
324 
325     @Override
326     public double getXValue(final int series, final int item)
327     {
328         return getTrajectory(series).getT(item);
329     }
330 
331     @Override
332     public Number getY(final int series, final int item)
333     {
334         return getYValue(series, item);
335     }
336 
337     @Override
338     public double getYValue(final int series, final int item)
339     {
340         return getTrajectory(series).getX(item);
341     }
342 
343     /**
344      * Get the trajectory of the series number.
345      * @param series series
346      * @return trajectory of the series number
347      */
348     private OffsetTrajectory getTrajectory(final int series)
349     {
350         int[] n = getLaneAndSeriesNumber(series);
351         return this.curves.get(n[0]).get(n[1]);
352     }
353 
354     /**
355      * Returns the lane number, and series number within the lane data.
356      * @param series overall series number
357      * @return lane number, and series number within the lane data
358      */
359     private int[] getLaneAndSeriesNumber(final int series)
360     {
361         int n = series;
362         for (int i = 0; i < this.curves.size(); i++)
363         {
364             int m = this.curvesPerLane.get(i);
365             if (n < m)
366             {
367                 return new int[] {i, n};
368             }
369             n -= m;
370         }
371         throw new OtsRuntimeException("Discrepancy between series number and available data.");
372     }
373 
374     /**
375      * Extension of a line renderer to select a color based on GTU ID, and to overrule an unused shape to save memory.
376      * <p>
377      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
378      * <br>
379      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
380      * </p>
381      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
382      * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
383      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
384      */
385     private final class XYLineAndShapeRendererColor extends XYLineAndShapeRenderer
386     {
387         /** */
388         private static final long serialVersionUID = 20181014L;
389 
390         /**
391          * Constructor.
392          */
393         XYLineAndShapeRendererColor()
394         {
395             super(false, true);
396             setDefaultLinesVisible(true);
397             setDefaultShapesVisible(false);
398             setDrawSeriesLineAsPath(true);
399         }
400 
401         @SuppressWarnings("synthetic-access")
402         @Override
403         public boolean isSeriesVisible(final int series)
404         {
405             int[] n = getLaneAndSeriesNumber(series);
406             return TrajectoryPlot.this.laneVisible.get(n[0]);
407         }
408 
409         @SuppressWarnings("synthetic-access")
410         @Override
411         public Stroke getSeriesStroke(final int series)
412         {
413             if (TrajectoryPlot.this.curves.size() == 1)
414             {
415                 return STROKES[0];
416             }
417             int[] n = getLaneAndSeriesNumber(series);
418             return TrajectoryPlot.this.strokes.get(n[0]).get(n[1]);
419         }
420 
421         @SuppressWarnings("synthetic-access")
422         @Override
423         public Paint getSeriesPaint(final int series)
424         {
425             int[] n = getLaneAndSeriesNumber(series);
426             return COLORMAP[n[0] % COLORMAP.length];
427         }
428 
429         @Override
430         public Paint getItemPaint(final int row, final int column)
431         {
432             if (TrajectoryPlot.this.colorer == null)
433             {
434                 return getSeriesPaint(row);
435             }
436             return TrajectoryPlot.this.colorer.getColor(new TrajectorySection(getTrajectory(row), column));
437         }
438 
439         /**
440          * {@inheritDoc} Largely based on the super implementation, but returns a dummy shape for markers to save memory and as
441          * markers are not used.
442          */
443         @SuppressWarnings("synthetic-access")
444         @Override
445         protected void addEntity(final EntityCollection entities, final Shape hotspot, final XYDataset dataset,
446                 final int series, final int item, final double entityX, final double entityY)
447         {
448 
449             if (!getItemCreateEntity(series, item))
450             {
451                 return;
452             }
453 
454             // if not hotspot is provided, we create a default based on the
455             // provided data coordinates (which are already in Java2D space)
456             Shape hotspot2 = hotspot == null ? NO_SHAPE : hotspot;
457             String tip = null;
458             XYToolTipGenerator generator = getToolTipGenerator(series, item);
459             if (generator != null)
460             {
461                 tip = generator.generateToolTip(dataset, series, item);
462             }
463             String url = null;
464             if (getURLGenerator() != null)
465             {
466                 url = getURLGenerator().generateURL(dataset, series, item);
467             }
468             XYItemEntity entity = new XYItemEntity(hotspot2, dataset, series, item, tip, url);
469             entities.add(entity);
470         }
471 
472         @Override
473         public String toString()
474         {
475             return "XYLineAndShapeRendererID []";
476         }
477 
478     }
479 
480     @Override
481     public String toString()
482     {
483         return "TrajectoryPlot [graphUpdater=" + this.graphUpdater + ", knownTrajectories=" + this.knownTrajectories
484                 + ", curves=" + this.curves + ", strokes=" + this.strokes + ", curvesPerLane=" + this.curvesPerLane
485                 + ", legend=" + this.legend + ", laneVisible=" + this.laneVisible + "]";
486     }
487 
488     /**
489      * Retrieve the legend.
490      * @return the legend
491      */
492     public LegendItemCollection getLegend()
493     {
494         return this.legend;
495     }
496 
497     /**
498      * Retrieve the lane visibility flags.
499      * @return the lane visibility flags
500      */
501     public List<Boolean> getLaneVisible()
502     {
503         return this.laneVisible;
504     }
505 
506 }