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