1 package org.opentrafficsim.graphs;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
5 import java.awt.Paint;
6 import java.awt.event.ActionEvent;
7 import java.awt.geom.Line2D;
8 import java.util.ArrayList;
9 import java.util.List;
10
11 import javax.swing.JFrame;
12 import javax.swing.JLabel;
13 import javax.swing.JPopupMenu;
14 import javax.swing.SwingConstants;
15 import javax.swing.SwingUtilities;
16
17 import org.djunits.unit.LengthUnit;
18 import org.djunits.unit.TimeUnit;
19 import org.djunits.value.vdouble.scalar.DoubleScalar;
20 import org.djunits.value.vdouble.scalar.Duration;
21 import org.djunits.value.vdouble.scalar.Frequency;
22 import org.djunits.value.vdouble.scalar.Length;
23 import org.djunits.value.vdouble.scalar.Time;
24 import org.jfree.chart.ChartFactory;
25 import org.jfree.chart.ChartPanel;
26 import org.jfree.chart.JFreeChart;
27 import org.jfree.chart.StandardChartTheme;
28 import org.jfree.chart.axis.NumberAxis;
29 import org.jfree.chart.axis.ValueAxis;
30 import org.jfree.chart.plot.PlotOrientation;
31 import org.jfree.chart.plot.XYPlot;
32 import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
33 import org.jfree.data.DomainOrder;
34 import org.jfree.data.general.DatasetChangeEvent;
35 import org.jfree.data.general.DatasetChangeListener;
36 import org.jfree.data.general.DatasetGroup;
37 import org.jfree.data.xy.XYDataset;
38 import org.opentrafficsim.core.gtu.animation.IDGTUColorer;
39 import org.opentrafficsim.kpi.sampling.KpiGtuDirectionality;
40 import org.opentrafficsim.kpi.sampling.KpiLaneDirection;
41 import org.opentrafficsim.kpi.sampling.SamplingException;
42 import org.opentrafficsim.kpi.sampling.SpaceTimeRegion;
43 import org.opentrafficsim.kpi.sampling.TrajectoryGroup;
44 import org.opentrafficsim.road.network.lane.Lane;
45 import org.opentrafficsim.road.network.sampling.LaneData;
46 import org.opentrafficsim.road.network.sampling.RoadSampler;
47
48 import nl.tudelft.simulation.dsol.simulators.DEVSSimulatorInterface;
49
50
51
52
53
54
55
56
57
58
59
60 public class TrajectoryPlot extends AbstractOTSPlot implements XYDataset, LaneBasedGTUSampler
61 {
62
63 private static final long serialVersionUID = 20140724L;
64
65
66 private final Duration sampleInterval;
67
68
69 private final DEVSSimulatorInterface.TimeDoubleUnit simulator;
70
71
72
73
74
75 public final Duration getSampleInterval()
76 {
77 return this.sampleInterval;
78 }
79
80
81 private final double[] cumulativeLengths;
82
83
84
85
86
87
88 public final double getCumulativeLength(final int index)
89 {
90 return index == -1 ? this.cumulativeLengths[this.cumulativeLengths.length - 1] : this.cumulativeLengths[index];
91 }
92
93
94 private Time maximumTime = new Time(300, TimeUnit.BASE);
95
96
97
98
99
100 public final Time getMaximumTime()
101 {
102 return this.maximumTime;
103 }
104
105
106
107
108
109 public final void setMaximumTime(final Time maximumTime)
110 {
111 this.maximumTime = maximumTime;
112 }
113
114
115 private DatasetGroup datasetGroup = null;
116
117
118 private RoadSampler roadSampler;
119
120
121 private List<KpiLaneDirection> lanes;
122
123
124 private List<TrajectoryAndLengthOffset> curves = null;
125
126
127 private boolean shouldGenerateNewCurves = true;
128
129
130
131
132
133
134
135
136
137 public TrajectoryPlot(final String caption, final Duration sampleInterval, final List<Lane> path,
138 final DEVSSimulatorInterface.TimeDoubleUnit simulator)
139 {
140 super(caption, path);
141 this.roadSampler = null == sampleInterval ? new RoadSampler(simulator)
142 : new RoadSampler(simulator, Frequency.createSI(1 / sampleInterval.si));
143 this.lanes = new ArrayList<>();
144 for (Lane lane : path)
145 {
146 KpiLaneDirection kpiLaneDirection =
147 new KpiLaneDirection(new LaneData(lane), KpiGtuDirectionality.DIR_PLUS);
148 SpaceTimeRegion spaceTimeRegion = new SpaceTimeRegion(kpiLaneDirection, Length.ZERO, lane.getLength(),
149 Time.ZERO, Time.createSI(Double.MAX_VALUE));
150 this.roadSampler.registerSpaceTimeRegion(spaceTimeRegion);
151 this.lanes.add(kpiLaneDirection);
152 }
153 this.sampleInterval = sampleInterval;
154 this.simulator = simulator;
155 double[] endLengths = new double[path.size()];
156 double cumulativeLength = 0;
157 for (int i = 0; i < path.size(); i++)
158 {
159 Lane lane = path.get(i);
160 cumulativeLength += lane.getLength().getSI();
161 endLengths[i] = cumulativeLength;
162 }
163 this.cumulativeLengths = endLengths;
164 setChart(createChart(this));
165 this.reGraph();
166 }
167
168
169
170
171
172 private class MyRenderer extends XYLineAndShapeRenderer
173 {
174
175
176 private static final long serialVersionUID = 20170503L;
177
178
179
180
181
182
183 MyRenderer(final boolean lines, final boolean shapes)
184 {
185 super(lines, shapes);
186 }
187
188 @Override
189 public Paint getItemPaint(final int row, final int col)
190 {
191 @SuppressWarnings("synthetic-access")
192 TrajectoryAndLengthOffset tal = getTrajectory(row);
193 String gtuId = tal.getTrajectory().getGtuId();
194 int colorIndex = 0;
195 for (int pos = gtuId.length(); --pos >= 0;)
196 {
197 Character c = gtuId.charAt(pos);
198 if (Character.isDigit(c))
199 {
200 colorIndex = c - '0';
201 break;
202 }
203 }
204 return IDGTUColorer.LEGEND.get(colorIndex).getColor();
205 }
206
207
208 @Override
209 public final String toString()
210 {
211 return "MyRenderer []";
212 }
213 }
214
215
216 @Override
217 public final GraphType getGraphType()
218 {
219 return GraphType.TRAJECTORY;
220 }
221
222
223 @Override
224 protected final JFreeChart createChart(final JFrame container)
225 {
226 final JLabel statusLabel = new JLabel(" ", SwingConstants.CENTER);
227 container.add(statusLabel, BorderLayout.SOUTH);
228 ChartFactory.setChartTheme(new StandardChartTheme("JFree/Shadow", false));
229 final JFreeChart result =
230 ChartFactory.createXYLineChart(getCaption(), "", "", this, PlotOrientation.VERTICAL, false, false, false);
231
232 result.getPlot().setBackgroundPaint(new Color(0.9f, 0.9f, 0.9f));
233 FixCaption.fixCaption(result);
234 NumberAxis xAxis = new NumberAxis("\u2192 " + "time [s]");
235 xAxis.setLowerMargin(0.0);
236 xAxis.setUpperMargin(0.0);
237 NumberAxis yAxis = new NumberAxis("\u2192 " + "Distance [m]");
238 yAxis.setAutoRangeIncludesZero(false);
239 yAxis.setLowerMargin(0.0);
240 yAxis.setUpperMargin(0.0);
241 yAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
242 result.getXYPlot().setDomainAxis(xAxis);
243 result.getXYPlot().setRangeAxis(yAxis);
244 Length minimumPosition = Length.ZERO;
245 Length maximumPosition = new Length(getCumulativeLength(-1), LengthUnit.SI);
246 configureAxis(result.getXYPlot().getRangeAxis(), DoubleScalar.minus(maximumPosition, minimumPosition).getSI());
247
248 MyRenderer renderer = new MyRenderer(false, true);
249 result.getXYPlot().setRenderer(renderer);
250 renderer.setDefaultLinesVisible(true);
251 renderer.setDefaultShapesVisible(false);
252 renderer.setDefaultShape(new Line2D.Float(0, 0, 0, 0));
253 final ChartPanel cp = new ChartPanel(result);
254 cp.setMouseWheelEnabled(true);
255 final PointerHandler ph = new PointerHandler()
256 {
257
258 @Override
259 void updateHint(final double domainValue, final double rangeValue)
260 {
261 if (Double.isNaN(domainValue))
262 {
263 statusLabel.setText(" ");
264 return;
265 }
266 String value = "";
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327 statusLabel.setText(String.format("t=%.0fs, distance=%.0fm%s", domainValue, rangeValue, value));
328 }
329 };
330 cp.addMouseMotionListener(ph);
331 cp.addMouseListener(ph);
332 container.add(cp, BorderLayout.CENTER);
333
334
335 JPopupMenu popupMenu = cp.getPopupMenu();
336 popupMenu.add(new JPopupMenu.Separator());
337 popupMenu.add(StandAloneChartWindow.createMenuItem(this));
338 return result;
339 }
340
341
342 @Override
343 public final void reGraph()
344 {
345
346 SwingUtilities.invokeLater(new Runnable()
347 {
348
349 @SuppressWarnings({ "synthetic-access", "unqualified-field-access" })
350 @Override
351 public void run()
352 {
353 for (DatasetChangeListener dcl : getListenerList().getListeners(DatasetChangeListener.class))
354 {
355 if (dcl instanceof XYPlot)
356 {
357 Time simulatorTime = simulator.getSimulatorTime();
358 if (getMaximumTime().lt(simulatorTime))
359 {
360 setMaximumTime(simulatorTime);
361 }
362 configureAxis(((XYPlot) dcl).getDomainAxis(), maximumTime.getSI());
363 }
364 }
365 shouldGenerateNewCurves = true;
366 notifyListeners(new DatasetChangeEvent(this, null));
367 }
368 });
369 }
370
371
372
373
374
375
376 static void configureAxis(final ValueAxis valueAxis, final double range)
377 {
378 valueAxis.setUpperBound(range);
379 valueAxis.setLowerMargin(0);
380 valueAxis.setUpperMargin(0);
381 valueAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
382 valueAxis.setAutoRange(true);
383 valueAxis.setAutoRangeMinimumSize(range);
384 valueAxis.centerRange(range / 2);
385
386 }
387
388
389 @Override
390 public void actionPerformed(final ActionEvent e)
391 {
392
393 }
394
395
396 @Override
397 public final int getSeriesCount()
398 {
399 if (null == this.curves || this.shouldGenerateNewCurves)
400 {
401 List<TrajectoryAndLengthOffset> newCurves = new ArrayList<>();
402 double cumulativeLength = 0;
403 for (KpiLaneDirection kld : this.lanes)
404 {
405 TrajectoryGroup tg = this.roadSampler.getTrajectoryGroup(kld);
406 if (null == tg)
407 {
408 continue;
409 }
410 for (org.opentrafficsim.kpi.sampling.Trajectory trajectory : tg.getTrajectories())
411 {
412 newCurves.add(new TrajectoryAndLengthOffset(trajectory, cumulativeLength));
413 }
414 cumulativeLength += kld.getLaneData().getLength().si;
415 }
416 this.curves = newCurves;
417 this.shouldGenerateNewCurves = false;
418 }
419 return this.curves.size();
420 }
421
422
423 @Override
424 public final Comparable<Integer> getSeriesKey(final int series)
425 {
426 return series;
427 }
428
429
430 @SuppressWarnings("rawtypes")
431 @Override
432 public final int indexOf(final Comparable seriesKey)
433 {
434 if (seriesKey instanceof Integer)
435 {
436 return (Integer) seriesKey;
437 }
438 return -1;
439 }
440
441
442 @Override
443 public final DatasetGroup getGroup()
444 {
445 return this.datasetGroup;
446 }
447
448
449 @Override
450 public final void setGroup(final DatasetGroup group)
451 {
452 this.datasetGroup = group;
453 }
454
455
456 @Override
457 public final DomainOrder getDomainOrder()
458 {
459 return DomainOrder.ASCENDING;
460 }
461
462
463
464
465 class TrajectoryAndLengthOffset
466 {
467
468 private final org.opentrafficsim.kpi.sampling.Trajectory trajectory;
469
470
471 private final double lengthOffset;
472
473
474
475
476
477
478
479 TrajectoryAndLengthOffset(final org.opentrafficsim.kpi.sampling.Trajectory trajectory,
480 final double lengthOffset)
481 {
482 this.trajectory = trajectory;
483 this.lengthOffset = lengthOffset;
484 }
485
486
487
488
489
490 public org.opentrafficsim.kpi.sampling.Trajectory getTrajectory()
491 {
492 return this.trajectory;
493 }
494
495
496
497
498
499 public double getLengthOffset()
500 {
501 return this.lengthOffset;
502 }
503
504
505 @Override
506 public final String toString()
507 {
508 return "TrajectoryAndLengthOffset [trajectory=" + this.trajectory + ", lengthOffset=" + this.lengthOffset + "]";
509 }
510
511 }
512
513
514
515
516
517
518 private TrajectoryAndLengthOffset getTrajectory(final int index)
519 {
520 if (index < 0)
521 {
522 System.err.println("Negative index (" + index + ")");
523 return null;
524 }
525 while (null == this.curves)
526 {
527 getSeriesCount();
528 }
529 if (index >= this.curves.size())
530 {
531 System.err.println("index out of range (" + index + " >= " + this.curves.size() + ")");
532 return null;
533 }
534 return this.curves.get(index);
535 }
536
537
538 @Override
539 public final int getItemCount(final int series)
540 {
541 return getTrajectory(series).getTrajectory().size();
542 }
543
544
545 @Override
546 public final Number getX(final int series, final int item)
547 {
548 double v = getXValue(series, item);
549 if (Double.isNaN(v))
550 {
551 return null;
552 }
553 return v;
554 }
555
556
557 @Override
558 public final double getXValue(final int series, final int item)
559 {
560 TrajectoryAndLengthOffset tal = getTrajectory(series);
561 try
562 {
563 return tal.getTrajectory().getT(item);
564 }
565 catch (SamplingException exception)
566 {
567 exception.printStackTrace();
568 System.out.println("index out of bounds: item=" + item + ", limit=" + tal.getTrajectory().size());
569 return Double.NaN;
570 }
571 }
572
573
574 @Override
575 public final Number getY(final int series, final int item)
576 {
577 double v = getYValue(series, item);
578 if (Double.isNaN(v))
579 {
580 return null;
581 }
582 return v;
583 }
584
585
586 @Override
587 public final double getYValue(final int series, final int item)
588 {
589 TrajectoryAndLengthOffset tal = getTrajectory(series);
590 try
591 {
592 return tal.getTrajectory().getX(item) + tal.getLengthOffset();
593 }
594 catch (SamplingException exception)
595 {
596 exception.printStackTrace();
597 System.out.println("index out of bounds: item=" + item + ", limit=" + tal.getTrajectory().size());
598 return Double.NaN;
599 }
600 }
601
602
603 @Override
604 public final String toString()
605 {
606 return "TrajectoryPlot [sampleInterval=" + this.sampleInterval + ", path=" + getPath() + ", cumulativeLengths.length="
607 + this.cumulativeLengths.length + ", maximumTime=" + this.maximumTime + ", caption=" + getCaption() + "]";
608 }
609
610 }