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.sampling.KpiLaneDirection;
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
51 public class TrajectoryPlot extends AbstractSamplerPlot implements XYDataset
52 {
53
54 private static final Shape NO_SHAPE = new Line2D.Float(0, 0, 0, 0);
55
56
57 private static final Color[] COLORMAP;
58
59
60 private static final BasicStroke[] STROKES;
61
62
63 private static final Shape LEGEND_LINE = new CubicCurve2D.Float(-20, 7, -10, -7, 0, 7, 20, -7);
64
65
66 private final GraphUpdater<Time> graphUpdater;
67
68
69 private final Map<KpiLaneDirection, Integer> knownTrajectories = new LinkedHashMap<>();
70
71
72 private List<List<OffsetTrajectory>> curves = new ArrayList<>();
73
74
75 private List<List<Stroke>> strokes = new ArrayList<>();
76
77
78 private List<Integer> curvesPerLane = new ArrayList<>();
79
80
81 private LegendItemCollection legend;
82
83
84 private final List<Boolean> laneVisible = new ArrayList<>();
85
86 static
87 {
88 Color[] c = BoundsPaintScale.hue(6);
89 COLORMAP = new Color[] {c[0], c[4], c[2], c[1], c[3], c[5]};
90 float lw = 0.4f;
91 STROKES = new BasicStroke[] {new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 10.0f, null, 0.0f),
92 new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 1.0f, new float[] {13f, 4f}, 0.0f),
93 new BasicStroke(lw, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 1.0f, new float[] {11f, 3f, 2f, 3f}, 0.0f)};
94 }
95
96
97
98
99
100
101
102
103
104 public TrajectoryPlot(final String caption, final Duration updateInterval, final OTSSimulatorInterface simulator,
105 final SamplerData<?> samplerData, final GraphPath<KpiLaneDirection> path)
106 {
107 super(caption, updateInterval, simulator, samplerData, path, Duration.ZERO);
108 for (int i = 0; i < path.getNumberOfSeries(); i++)
109 {
110 this.curves.add(new ArrayList<>());
111 this.strokes.add(new ArrayList<>());
112 this.curvesPerLane.add(0);
113 this.laneVisible.add(true);
114 }
115 setChart(createChart());
116
117
118 this.graphUpdater = new GraphUpdater<>("Trajectories worker", Thread.currentThread(), (t) ->
119 {
120 for (Section<KpiLaneDirection> section : path.getSections())
121 {
122 Length startDistance = path.getStartDistance(section);
123 for (int i = 0; i < path.getNumberOfSeries(); i++)
124 {
125 KpiLaneDirection lane = section.getSource(i);
126 if (lane == null)
127 {
128 continue;
129 }
130 TrajectoryGroup<?> trajectoryGroup = getSamplerData().getTrajectoryGroup(lane);
131 int from = this.knownTrajectories.getOrDefault(lane, 0);
132 int to = trajectoryGroup.size();
133 double scaleFactor = section.getLength().si / lane.getLaneData().getLength().si;
134 for (Trajectory<?> trajectory : trajectoryGroup.getTrajectories().subList(from, to))
135 {
136 if (getPath().getNumberOfSeries() > 1)
137 {
138
139 BasicStroke stroke = STROKES[i % STROKES.length];
140 if (stroke.getDashArray() != null)
141 {
142 float dashLength = 0.0f;
143 for (float d : stroke.getDashArray())
144 {
145 dashLength += d;
146 }
147 stroke = new BasicStroke(stroke.getLineWidth(), stroke.getEndCap(), stroke.getLineJoin(),
148 stroke.getMiterLimit(), stroke.getDashArray(), (float) (Math.random() * dashLength));
149 }
150 this.strokes.get(i).add(stroke);
151 }
152 this.curves.get(i).add(
153 new OffsetTrajectory(trajectory, startDistance, scaleFactor, lane.getLaneData().getLength()));
154 }
155 this.knownTrajectories.put(lane, to);
156 }
157 }
158 });
159 }
160
161
162
163
164
165 private JFreeChart createChart()
166 {
167 NumberAxis xAxis = new NumberAxis("Time [s] \u2192");
168 NumberAxis yAxis = new NumberAxis("Distance [m] \u2192");
169 XYLineAndShapeRendererID renderer = new XYLineAndShapeRendererID();
170 XYPlot plot = new XYPlot(this, xAxis, yAxis, renderer);
171 boolean showLegend;
172 if (getPath().getNumberOfSeries() < 2)
173 {
174 plot.setFixedLegendItems(null);
175 showLegend = false;
176 }
177 else
178 {
179 this.legend = new LegendItemCollection();
180 for (int i = 0; i < getPath().getNumberOfSeries(); i++)
181 {
182 LegendItem li = new LegendItem(getPath().getName(i));
183 li.setSeriesKey(i);
184 li.setShape(STROKES[i & STROKES.length].createStrokedShape(LEGEND_LINE));
185 li.setFillPaint(COLORMAP[i % COLORMAP.length]);
186 this.legend.add(li);
187 }
188 plot.setFixedLegendItems(this.legend);
189 showLegend = true;
190 }
191 return new JFreeChart(getCaption(), JFreeChart.DEFAULT_TITLE_FONT, plot, showLegend);
192 }
193
194
195 @Override
196 public GraphType getGraphType()
197 {
198 return GraphType.TRAJECTORY;
199 }
200
201
202 @Override
203 public String getStatusLabel(final double domainValue, final double rangeValue)
204 {
205 return String.format("time %.0fs, distance %.0fm", domainValue, rangeValue);
206 }
207
208
209 @Override
210 protected void increaseTime(final Time time)
211 {
212 if (this.graphUpdater != null)
213 {
214 this.graphUpdater.offer(time);
215 }
216 }
217
218
219 @Override
220 public int getSeriesCount()
221 {
222 int n = 0;
223 for (int i = 0; i < this.curves.size(); i++)
224 {
225 List<OffsetTrajectory> list = this.curves.get(i);
226 int m = list.size();
227 this.curvesPerLane.set(i, m);
228 n += m;
229 }
230 return n;
231 }
232
233
234 @Override
235 public Comparable<Integer> getSeriesKey(final int series)
236 {
237 return series;
238 }
239
240
241 @SuppressWarnings("rawtypes")
242 @Override
243 public int indexOf(final Comparable seriesKey)
244 {
245 return 0;
246 }
247
248
249 @Override
250 public DomainOrder getDomainOrder()
251 {
252 return DomainOrder.ASCENDING;
253 }
254
255
256 @Override
257 public int getItemCount(final int series)
258 {
259 OffsetTrajectory trajectory = getTrajectory(series);
260 return trajectory == null ? 0 : trajectory.size();
261 }
262
263
264 @Override
265 public Number getX(final int series, final int item)
266 {
267 return getXValue(series, item);
268 }
269
270
271 @Override
272 public double getXValue(final int series, final int item)
273 {
274 return getTrajectory(series).getT(item);
275 }
276
277
278 @Override
279 public Number getY(final int series, final int item)
280 {
281 return getYValue(series, item);
282 }
283
284
285 @Override
286 public double getYValue(final int series, final int item)
287 {
288 return getTrajectory(series).getX(item);
289 }
290
291
292
293
294
295
296 private OffsetTrajectory getTrajectory(final int series)
297 {
298 int[] n = getLaneAndSeriesNumber(series);
299 return this.curves.get(n[0]).get(n[1]);
300 }
301
302
303
304
305
306
307 private int[] getLaneAndSeriesNumber(final int series)
308 {
309 int n = series;
310 for (int i = 0; i < this.curves.size(); i++)
311 {
312 int m = this.curvesPerLane.get(i);
313 if (n < m)
314 {
315 return new int[] {i, n};
316 }
317 n -= m;
318 }
319 throw new RuntimeException("Discrepancy between series number and available data.");
320 }
321
322
323
324
325
326
327
328
329
330
331
332
333
334 private final class XYLineAndShapeRendererID extends XYLineAndShapeRenderer
335 {
336
337 private static final long serialVersionUID = 20181014L;
338
339
340
341
342 XYLineAndShapeRendererID()
343 {
344 super(false, true);
345 setDefaultLinesVisible(true);
346 setDefaultShapesVisible(false);
347 setDrawSeriesLineAsPath(true);
348 }
349
350
351 @SuppressWarnings("synthetic-access")
352 @Override
353 public boolean isSeriesVisible(final int series)
354 {
355 int[] n = getLaneAndSeriesNumber(series);
356 return TrajectoryPlot.this.laneVisible.get(n[0]);
357 }
358
359
360 @SuppressWarnings("synthetic-access")
361 @Override
362 public Stroke getSeriesStroke(final int series)
363 {
364 if (TrajectoryPlot.this.curves.size() == 1)
365 {
366 return STROKES[0];
367 }
368 int[] n = getLaneAndSeriesNumber(series);
369 return TrajectoryPlot.this.strokes.get(n[0]).get(n[1]);
370 }
371
372
373 @SuppressWarnings("synthetic-access")
374 @Override
375 public Paint getSeriesPaint(final int series)
376 {
377 if (TrajectoryPlot.this.curves.size() == 1)
378 {
379 String gtuId = getTrajectory(series).getGtuId();
380 for (int pos = gtuId.length(); --pos >= 0;)
381 {
382 Character c = gtuId.charAt(pos);
383 if (Character.isDigit(c))
384 {
385 return IDGTUColorer.LEGEND.get(c - '0').getColor();
386 }
387 }
388 }
389 int[] n = getLaneAndSeriesNumber(series);
390 return COLORMAP[n[0] % COLORMAP.length];
391 }
392
393
394
395
396
397 @SuppressWarnings("synthetic-access")
398 @Override
399 protected void addEntity(final EntityCollection entities, final Shape hotspot, final XYDataset dataset,
400 final int series, final int item, final double entityX, final double entityY)
401 {
402
403 if (!getItemCreateEntity(series, item))
404 {
405 return;
406 }
407
408
409
410 Shape hotspot2 = hotspot == null ? NO_SHAPE : hotspot;
411 String tip = null;
412 XYToolTipGenerator generator = getToolTipGenerator(series, item);
413 if (generator != null)
414 {
415 tip = generator.generateToolTip(dataset, series, item);
416 }
417 String url = null;
418 if (getURLGenerator() != null)
419 {
420 url = getURLGenerator().generateURL(dataset, series, item);
421 }
422 XYItemEntity entity = new XYItemEntity(hotspot2, dataset, series, item, tip, url);
423 entities.add(entity);
424 }
425
426
427 @Override
428 public String toString()
429 {
430 return "XYLineAndShapeRendererID []";
431 }
432
433 }
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448 private class OffsetTrajectory
449 {
450
451 private final Trajectory<?> trajectory;
452
453
454 private final double offset;
455
456
457 private final double scaleFactor;
458
459
460 private int first;
461
462
463 private int size;
464
465
466 private final Length laneLength;
467
468
469
470
471
472
473
474
475
476 OffsetTrajectory(final Trajectory<?> trajectory, final Length offset, final double scaleFactor, final Length laneLength)
477 {
478 this.trajectory = trajectory;
479 this.offset = offset.si;
480 this.scaleFactor = scaleFactor;
481 this.laneLength = laneLength;
482 }
483
484
485
486
487
488 public final int size()
489 {
490
491 try
492 {
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516 int f = 0;
517 while (f < this.trajectory.size() - 1 && this.trajectory.getX(f + 1) < 0.0)
518 {
519 f++;
520 }
521 this.first = f;
522 int s = this.trajectory.size() - 1;
523 while (s > 1 && this.trajectory.getX(s - 1) > this.laneLength.si)
524 {
525 s--;
526 }
527 this.size = s - f + 1;
528 }
529 catch (SamplingException exception)
530 {
531 throw new RuntimeException("Unexpected exception while obtaining location value from trajectory for plotting.",
532 exception);
533 }
534 return this.size;
535 }
536
537
538
539
540
541
542 public final double getX(final int item)
543 {
544 return Try.assign(() -> this.offset + this.trajectory.getX(this.first + item) * this.scaleFactor,
545 "Unexpected exception while obtaining location value from trajectory for plotting.");
546 }
547
548
549
550
551
552
553 public final double getT(final int item)
554 {
555 return Try.assign(() -> (double) this.trajectory.getT(this.first + item),
556 "Unexpected exception while obtaining time value from trajectory for plotting.");
557 }
558
559
560
561
562
563 public final String getGtuId()
564 {
565 return this.trajectory.getGtuId();
566 }
567
568
569 @Override
570 public final String toString()
571 {
572 return "OffsetTrajectory [trajectory=" + this.trajectory + ", offset=" + this.offset + "]";
573 }
574
575 }
576
577
578 @Override
579 public String toString()
580 {
581 return "TrajectoryPlot [graphUpdater=" + this.graphUpdater + ", knownTrajectories=" + this.knownTrajectories
582 + ", curves=" + this.curves + ", strokes=" + this.strokes + ", curvesPerLane=" + this.curvesPerLane
583 + ", legend=" + this.legend + ", laneVisible=" + this.laneVisible + "]";
584 }
585
586
587
588
589
590 public LegendItemCollection getLegend()
591 {
592 return this.legend;
593 }
594
595
596
597
598
599 public List<Boolean> getLaneVisible()
600 {
601 return this.laneVisible;
602 }
603
604 }