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