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