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