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