1 package org.opentrafficsim.animation.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.animation.Colors;
34 import org.opentrafficsim.animation.colorer.ColorbarColorer;
35 import org.opentrafficsim.animation.colorer.Colorer;
36 import org.opentrafficsim.animation.colorer.LegendColorer;
37 import org.opentrafficsim.animation.colorer.LegendColorer.LegendEntry;
38 import org.opentrafficsim.animation.colorer.trajectory.TrajectoryColorer;
39 import org.opentrafficsim.animation.graphs.AbstractPlot.PaintState;
40 import org.opentrafficsim.animation.graphs.GraphPath.Section;
41 import org.opentrafficsim.animation.graphs.OffsetTrajectory.TrajectorySection;
42 import org.opentrafficsim.animation.graphs.TrajectoryPlot.TrajectoriesPaintState;
43 import org.opentrafficsim.kpi.interfaces.LaneData;
44 import org.opentrafficsim.kpi.sampling.SamplerData;
45 import org.opentrafficsim.kpi.sampling.Trajectory;
46 import org.opentrafficsim.kpi.sampling.TrajectoryGroup;
47
48
49
50
51
52
53
54
55
56
57
58 public class TrajectoryPlot extends AbstractSpaceTimePlot<TrajectoriesPaintState> implements XYDataset
59 {
60
61
62 private final SamplerData<?> samplerData;
63
64
65 private final GraphPath<? extends LaneData<?>> path;
66
67
68 private static final Shape NO_SHAPE = new Line2D.Float(0, 0, 0, 0);
69
70
71 private static final Color[] COLORMAP;
72
73
74 private static final BasicStroke[] STROKES;
75
76
77 private static final Shape LEGEND_LINE = new CubicCurve2D.Float(-20, 7, -10, -7, 0, 7, 20, -7);
78
79
80 private final Map<LaneData<?>, Integer> knownTrajectories = new LinkedHashMap<>();
81
82
83 private List<List<OffsetTrajectory>> curves = new ArrayList<>();
84
85
86 private List<List<Stroke>> strokes = new ArrayList<>();
87
88
89 private List<Integer> curvesPerLane = new ArrayList<>();
90
91
92 private LegendItemCollection legend;
93
94
95 private final List<Boolean> laneVisible = new ArrayList<>();
96
97
98 private Colorer<? super TrajectorySection> colorer;
99
100
101 private XYLineAndShapeRendererColor renderer;
102
103
104 private PaintScaleLegend colorbar;
105
106 static
107 {
108 Color[] c = Colors.hue(6);
109 COLORMAP = new Color[] {c[0], c[4], c[2].darker().darker(), c[1], c[3], c[5]};
110 float lw = 1.0f;
111 STROKES = new BasicStroke[] {new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 10.0f, null, 0.0f),
112 new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 1.0f, new float[] {13f, 4f}, 0.0f),
113 new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 1.0f, new float[] {11f, 3f, 2f, 3f}, 0.0f)};
114 }
115
116
117
118
119
120
121
122
123
124
125 public TrajectoryPlot(final String caption, final Duration updateInterval, final PlotScheduler scheduler,
126 final SamplerData<?> samplerData, final GraphPath<? extends LaneData<?>> path)
127 {
128 super(caption, updateInterval, scheduler, Duration.ZERO, DEFAULT_INITIAL_UPPER_TIME_BOUND);
129 Throw.when(path.getNumberOfSeries() > 6, IllegalArgumentException.class, "The trajectory plot supports up to 6 lanes");
130 this.samplerData = samplerData;
131 this.path = path;
132 for (int i = 0; i < path.getNumberOfSeries(); i++)
133 {
134 this.curves.add(new ArrayList<>());
135 this.strokes.add(new ArrayList<>());
136 this.curvesPerLane.add(0);
137 this.laneVisible.add(true);
138 }
139 setChart(createChart());
140 }
141
142
143
144
145
146 private JFreeChart createChart()
147 {
148 NumberAxis xAxis = new NumberAxis("Time [s] \u2192");
149 NumberAxis yAxis = new NumberAxis("Distance [m] \u2192");
150 this.renderer = new XYLineAndShapeRendererColor();
151 XYPlot plot = new XYPlot(this, xAxis, yAxis, this.renderer);
152 if (this.path.getNumberOfSeries() > 1)
153 {
154 this.legend = new LegendItemCollection();
155 for (int i = 0; i < this.path.getNumberOfSeries(); i++)
156 {
157 LegendItem li = new LegendItem(this.path.getName(i));
158 li.setSeriesKey(i);
159 li.setShape(STROKES[i & STROKES.length].createStrokedShape(LEGEND_LINE));
160 li.setFillPaint(COLORMAP[i % COLORMAP.length]);
161 this.legend.add(li);
162 }
163 plot.setFixedLegendItems(this.legend);
164 }
165 return new JFreeChart(getCaption(), JFreeChart.DEFAULT_TITLE_FONT, plot, true);
166 }
167
168
169
170
171
172 public void setColorer(final TrajectoryColorer colorer)
173 {
174 this.colorer = colorer;
175 this.renderer.setDrawSeriesLineAsPath(colorer.isSingleColor());
176 if (this.path.getNumberOfSeries() < 2)
177 {
178 if (this.colorbar != null)
179 {
180 getChart().removeSubtitle(this.colorbar);
181 }
182 LegendItemCollection colorerLegend = new LegendItemCollection();
183 if (colorer instanceof ColorbarColorer<?> colorbarColorer)
184 {
185 NumberAxis scaleAxis = new NumberAxis("");
186 scaleAxis.setNumberFormatOverride(colorbarColorer.getNumberFormat());
187
188 scaleAxis.setTickLabelInsets(new RectangleInsets(5.0, 4.0, 5.0, 4.0));
189 this.colorbar = new PaintScaleLegend(colorbarColorer.getBoundsPaintScale(), scaleAxis);
190 this.colorbar.setSubdivisionCount(256);
191 this.colorbar.setPosition(RectangleEdge.RIGHT);
192
193 this.colorbar.setPadding(10.0, 15.0, 40.0, 10.0);
194 getChart().addSubtitle(this.colorbar);
195 }
196 else if (colorer instanceof LegendColorer<?> legendColorer)
197 {
198
199 for (LegendEntry entry : legendColorer.getLegend())
200 {
201 colorerLegend.add(new LegendItem(entry.name(), entry.name(), entry.name(), entry.name(),
202 new Rectangle(10, 10), entry.color(), new BasicStroke(0.5f), Color.BLACK));
203 }
204 }
205 ((XYPlot) getChart().getPlot()).setFixedLegendItems(colorerLegend);
206 }
207 }
208
209 @Override
210 public GraphType getGraphType()
211 {
212 return GraphType.TRAJECTORY;
213 }
214
215 @Override
216 public String getStatusLabel(final double domainValue, final double rangeValue)
217 {
218 return String.format("time %.0fs, distance %.0fm", domainValue, rangeValue);
219 }
220
221 @Override
222 public int getSeriesCount()
223 {
224 return getPaintState().getSeriesCount();
225 }
226
227
228
229
230
231 public int getLaneCount()
232 {
233 return this.curves.size();
234 }
235
236 @Override
237 public Comparable<Integer> getSeriesKey(final int series)
238 {
239 return series;
240 }
241
242 @SuppressWarnings("rawtypes")
243 @Override
244 public int indexOf(final Comparable seriesKey)
245 {
246 return 0;
247 }
248
249 @Override
250 public DomainOrder getDomainOrder()
251 {
252 return DomainOrder.ASCENDING;
253 }
254
255 @Override
256 public int getItemCount(final int series)
257 {
258 OffsetTrajectory t = getPaintState().series()[series];
259 return t == null ? 0 : t.size();
260 }
261
262 @Override
263 public Number getX(final int series, final int item)
264 {
265 return getXValue(series, item);
266 }
267
268 @Override
269 public double getXValue(final int series, final int item)
270 {
271 return getPaintState().series()[series].getT(item);
272 }
273
274 @Override
275 public Number getY(final int series, final int item)
276 {
277 return getYValue(series, item);
278 }
279
280 @Override
281 public double getYValue(final int series, final int item)
282 {
283 return getPaintState().series()[series].getX(item);
284 }
285
286
287
288
289 private final class XYLineAndShapeRendererColor extends XYLineAndShapeRenderer
290 {
291
292
293 private static final long serialVersionUID = 20181014L;
294
295
296
297
298 XYLineAndShapeRendererColor()
299 {
300 super(false, true);
301 setDefaultLinesVisible(true);
302 setDefaultShapesVisible(false);
303 setDrawSeriesLineAsPath(true);
304 setDefaultCreateEntities(false);
305 setDefaultItemLabelsVisible(false);
306 }
307
308 @Override
309 public boolean isSeriesVisible(final int series)
310 {
311 int lane = getPaintState().laneOfSeries()[series];
312 return TrajectoryPlot.this.laneVisible == null || lane >= TrajectoryPlot.this.laneVisible.size() ? false
313 : TrajectoryPlot.this.laneVisible.get(lane);
314 }
315
316 @Override
317 public Stroke getSeriesStroke(final int series)
318 {
319 if (getPaintState().laneCount() == 1)
320 {
321 return STROKES[0];
322 }
323 Stroke s = getPaintState().strokeOfSeries()[series];
324 return (s != null ? s : STROKES[0]);
325 }
326
327 @Override
328 public Paint getSeriesPaint(final int series)
329 {
330 int lane = getPaintState().laneOfSeries()[series];
331 return COLORMAP[lane % COLORMAP.length];
332 }
333
334 @Override
335 public Paint getItemPaint(final int row, final int column)
336 {
337 if (TrajectoryPlot.this.colorer == null)
338 {
339 return getSeriesPaint(row);
340 }
341 return TrajectoryPlot.this.colorer.getColor(new TrajectorySection(getPaintState().series()[row], column));
342 }
343
344
345
346
347
348 @Override
349 protected void addEntity(final EntityCollection entities, final Shape hotspot, final XYDataset dataset,
350 final int series, final int item, final double entityX, final double entityY)
351 {
352
353 if (!getItemCreateEntity(series, item))
354 {
355 return;
356 }
357
358
359
360 Shape hotspot2 = hotspot == null ? NO_SHAPE : hotspot;
361 String tip = null;
362 XYToolTipGenerator generator = getToolTipGenerator(series, item);
363 if (generator != null)
364 {
365 tip = generator.generateToolTip(dataset, series, item);
366 }
367 String url = null;
368 if (getURLGenerator() != null)
369 {
370 url = getURLGenerator().generateURL(dataset, series, item);
371 }
372 XYItemEntity entity = new XYItemEntity(hotspot2, dataset, series, item, tip, url);
373 entities.add(entity);
374 }
375
376 @Override
377 public String toString()
378 {
379 return "XYLineAndShapeRendererID []";
380 }
381
382 }
383
384 @Override
385 public String toString()
386 {
387 return "TrajectoryPlot []";
388 }
389
390
391
392
393
394 public LegendItemCollection getLegend()
395 {
396 return this.legend;
397 }
398
399
400
401
402
403 public List<Boolean> getLaneVisible()
404 {
405 return this.laneVisible;
406 }
407
408 @Override
409 protected final Length getEndLocation()
410 {
411 return this.path.getTotalLength();
412 }
413
414 @Override
415 protected TrajectoriesPaintState emptyPaintState()
416 {
417 return new TrajectoriesPaintState(new OffsetTrajectory[0], new int[0], new Stroke[0], 0, Duration.ZERO);
418 }
419
420 @Override
421 protected void calculatePaintState(final Duration time)
422 {
423
424 for (Section<? extends LaneData<?>> section : this.path.getSections())
425 {
426 Length startDistance = this.path.getStartDistance(section);
427 for (int i = 0; i < this.path.getNumberOfSeries(); i++)
428 {
429 LaneData<?> lane = section.getSource(i);
430 if (lane == null)
431 {
432 continue;
433 }
434 TrajectoryGroup<?> trajectoryGroup = this.samplerData.getTrajectoryGroup(lane).orElse(null);
435 if (trajectoryGroup == null)
436 {
437
438 return;
439 }
440 int from = this.knownTrajectories.getOrDefault(lane, 0);
441 int to = trajectoryGroup.size();
442 double scaleFactor = section.length().si / lane.getLength().si;
443 for (Trajectory<?> trajectory : trajectoryGroup.getTrajectories().subList(from, to))
444 {
445 if (this.path.getNumberOfSeries() > 1)
446 {
447
448 BasicStroke stroke = STROKES[i % STROKES.length];
449 if (stroke.getDashArray() != null)
450 {
451 float dashLength = 0.0f;
452 for (float d : stroke.getDashArray())
453 {
454 dashLength += d;
455 }
456 stroke = new BasicStroke(stroke.getLineWidth(), stroke.getEndCap(), stroke.getLineJoin(),
457 stroke.getMiterLimit(), stroke.getDashArray(), (float) (Math.random() * dashLength));
458 }
459 this.strokes.get(i).add(stroke);
460 }
461 this.curves.get(i).add(new OffsetTrajectory(trajectory, startDistance, scaleFactor));
462 }
463 this.knownTrajectories.put(lane, to);
464 }
465 }
466
467
468 offerPaintState(buildPaintStateFromLists(time));
469 }
470
471
472
473
474
475
476 private TrajectoriesPaintState buildPaintStateFromLists(final Duration time)
477 {
478
479 int laneCount = this.curves.size();
480 int totalSeries = 0;
481 for (int i = 0; i < laneCount; i++)
482 {
483 totalSeries += this.curves.get(i).size();
484 }
485
486
487 OffsetTrajectory[] series = new OffsetTrajectory[totalSeries];
488 int[] laneOfSeries = new int[totalSeries];
489 Stroke[] strokeOfSeries = (laneCount > 1) ? new Stroke[totalSeries] : null;
490
491
492 int k = 0;
493 for (int lane = 0; lane < laneCount; lane++)
494 {
495 List<OffsetTrajectory> laneCurves = this.curves.get(lane);
496 List<Stroke> laneStrokes = (laneCount > 1) ? this.strokes.get(lane) : null;
497 for (int j = 0; j < laneCurves.size(); j++)
498 {
499 series[k] = laneCurves.get(j);
500 laneOfSeries[k] = lane;
501 if (strokeOfSeries != null)
502 {
503 strokeOfSeries[k] = laneStrokes.get(j);
504 }
505 k++;
506 }
507 }
508
509 return new TrajectoriesPaintState(series, laneOfSeries, (strokeOfSeries != null ? strokeOfSeries : new Stroke[0]),
510 laneCount, time);
511 }
512
513
514
515
516
517
518
519
520
521 public record TrajectoriesPaintState(OffsetTrajectory[] series, int[] laneOfSeries, Stroke[] strokeOfSeries, int laneCount,
522 Duration getAvailableTime) implements PaintState
523 {
524
525
526
527
528
529 int getSeriesCount()
530 {
531 return this.series().length;
532 }
533
534 }
535
536 }