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
42
43
44
45
46
47
48
49
50 public class TrajectoryPlot extends AbstractSamplerPlot implements XYDataset
51 {
52
53 private static final Shape NO_SHAPE = new Line2D.Float(0, 0, 0, 0);
54
55
56 private static final Color[] COLORMAP;
57
58
59 private static final BasicStroke[] STROKES;
60
61
62 private static final Shape LEGEND_LINE = new CubicCurve2D.Float(-20, 7, -10, -7, 0, 7, 20, -7);
63
64
65 private final GraphUpdater<Time> graphUpdater;
66
67
68 private final Map<LaneData, Integer> knownTrajectories = new LinkedHashMap<>();
69
70
71 private List<List<OffsetTrajectory>> curves = new ArrayList<>();
72
73
74 private List<List<Stroke>> strokes = new ArrayList<>();
75
76
77 private List<Integer> curvesPerLane = new ArrayList<>();
78
79
80 private LegendItemCollection legend;
81
82
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
97
98
99
100
101
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
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;
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
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
161
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);
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
193 @Override
194 public GraphType getGraphType()
195 {
196 return GraphType.TRAJECTORY;
197 }
198
199
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
207 @Override
208 protected void increaseTime(final Time time)
209 {
210 if (this.graphUpdater != null)
211 {
212 this.graphUpdater.offer(time);
213 }
214 }
215
216
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
232 @Override
233 public Comparable<Integer> getSeriesKey(final int series)
234 {
235 return series;
236 }
237
238
239 @SuppressWarnings("rawtypes")
240 @Override
241 public int indexOf(final Comparable seriesKey)
242 {
243 return 0;
244 }
245
246
247 @Override
248 public DomainOrder getDomainOrder()
249 {
250 return DomainOrder.ASCENDING;
251 }
252
253
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
262 @Override
263 public Number getX(final int series, final int item)
264 {
265 return getXValue(series, item);
266 }
267
268
269 @Override
270 public double getXValue(final int series, final int item)
271 {
272 return getTrajectory(series).getT(item);
273 }
274
275
276 @Override
277 public Number getY(final int series, final int item)
278 {
279 return getYValue(series, item);
280 }
281
282
283 @Override
284 public double getYValue(final int series, final int item)
285 {
286 return getTrajectory(series).getX(item);
287 }
288
289
290
291
292
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
302
303
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
322
323
324
325
326
327
328
329
330
331 private final class XYLineAndShapeRendererID extends XYLineAndShapeRenderer
332 {
333
334 private static final long serialVersionUID = 20181014L;
335
336
337
338
339 XYLineAndShapeRendererID()
340 {
341 super(false, true);
342 setDefaultLinesVisible(true);
343 setDefaultShapesVisible(false);
344 setDrawSeriesLineAsPath(true);
345 }
346
347
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
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
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
392
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
406
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
424 @Override
425 public String toString()
426 {
427 return "XYLineAndShapeRendererID []";
428 }
429
430 }
431
432
433
434
435
436
437
438
439
440
441
442
443
444 private class OffsetTrajectory
445 {
446
447 private final Trajectory<?> trajectory;
448
449
450 private final double offset;
451
452
453 private final double scaleFactor;
454
455
456 private int first;
457
458
459 private int size;
460
461
462 private final Length laneLength;
463
464
465
466
467
468
469
470
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
482
483
484 public final int size()
485 {
486
487 try
488 {
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
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
535
536
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
546
547
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
557
558
559 public final String getGtuId()
560 {
561 return this.trajectory.getGtuId();
562 }
563
564
565 @Override
566 public final String toString()
567 {
568 return "OffsetTrajectory [trajectory=" + this.trajectory + ", offset=" + this.offset + "]";
569 }
570
571 }
572
573
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
584
585
586 public LegendItemCollection getLegend()
587 {
588 return this.legend;
589 }
590
591
592
593
594
595 public List<Boolean> getLaneVisible()
596 {
597 return this.laneVisible;
598 }
599
600 }