1 package org.opentrafficsim.graphs;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
5 import java.awt.event.ActionEvent;
6 import java.awt.event.ActionListener;
7 import java.awt.geom.Line2D;
8 import java.io.Serializable;
9 import java.rmi.RemoteException;
10 import java.util.ArrayList;
11 import java.util.HashMap;
12 import java.util.HashSet;
13 import java.util.List;
14 import java.util.Set;
15
16 import javax.swing.JFrame;
17 import javax.swing.JLabel;
18 import javax.swing.JPopupMenu;
19 import javax.swing.SwingConstants;
20 import javax.swing.event.EventListenerList;
21
22 import org.djunits.unit.LengthUnit;
23 import org.djunits.unit.TimeUnit;
24 import org.djunits.value.vdouble.scalar.DoubleScalar;
25 import org.djunits.value.vdouble.scalar.Duration;
26 import org.djunits.value.vdouble.scalar.Length;
27 import org.djunits.value.vdouble.scalar.Time;
28 import org.jfree.chart.ChartFactory;
29 import org.jfree.chart.ChartPanel;
30 import org.jfree.chart.JFreeChart;
31 import org.jfree.chart.StandardChartTheme;
32 import org.jfree.chart.axis.NumberAxis;
33 import org.jfree.chart.axis.ValueAxis;
34 import org.jfree.chart.plot.PlotOrientation;
35 import org.jfree.chart.plot.XYPlot;
36 import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
37 import org.jfree.data.DomainOrder;
38 import org.jfree.data.general.DatasetChangeEvent;
39 import org.jfree.data.general.DatasetChangeListener;
40 import org.jfree.data.general.DatasetGroup;
41 import org.jfree.data.xy.XYDataset;
42 import org.opentrafficsim.core.dsol.OTSSimTimeDouble;
43 import org.opentrafficsim.core.gtu.GTUException;
44 import org.opentrafficsim.core.network.NetworkException;
45 import org.opentrafficsim.road.gtu.lane.LaneBasedGTU;
46 import org.opentrafficsim.road.network.lane.Lane;
47
48 import nl.tudelft.simulation.event.EventInterface;
49 import nl.tudelft.simulation.event.EventListenerInterface;
50 import nl.tudelft.simulation.event.TimedEvent;
51
52
53
54
55
56
57
58
59
60
61
62 public class TrajectoryPlot extends JFrame
63 implements ActionListener, XYDataset, MultipleViewerChart, LaneBasedGTUSampler, EventListenerInterface
64
65 {
66
67 private static final long serialVersionUID = 20140724L;
68
69
70 private final double sampleInterval;
71
72
73
74
75 public final double getSampleInterval()
76 {
77 return this.sampleInterval;
78 }
79
80
81 private final ArrayList<Lane> path;
82
83
84 private final double[] cumulativeLengths;
85
86
87
88
89
90
91 public final double getCumulativeLength(final int index)
92 {
93 return index == -1 ? this.cumulativeLengths[this.cumulativeLengths.length - 1] : this.cumulativeLengths[index];
94 }
95
96
97 private Time maximumTime = new Time(300, TimeUnit.SECOND);
98
99
100
101
102 public final Time getMaximumTime()
103 {
104 return this.maximumTime;
105 }
106
107
108
109
110 public final void setMaximumTime(final Time maximumTime)
111 {
112 this.maximumTime = maximumTime;
113 }
114
115
116 private transient EventListenerList listenerList = new EventListenerList();
117
118
119 private DatasetGroup datasetGroup = null;
120
121
122 private final String caption;
123
124
125
126
127
128
129
130 public TrajectoryPlot(final String caption, final Duration sampleInterval, final List<Lane> path)
131 {
132 this.sampleInterval = sampleInterval.si;
133 this.path = new ArrayList<Lane>(path);
134 double[] endLengths = new double[path.size()];
135 double cumulativeLength = 0;
136 for (int i = 0; i < path.size(); i++)
137 {
138 Lane lane = path.get(i);
139 lane.addListener(this, Lane.GTU_ADD_EVENT, true);
140 lane.addListener(this, Lane.GTU_REMOVE_EVENT, true);
141 try
142 {
143
144 for (LaneBasedGTU gtu : lane.getGtuList())
145 {
146 notify(new TimedEvent<OTSSimTimeDouble>(Lane.GTU_ADD_EVENT, lane, new Object[] { gtu.getId(), gtu },
147 gtu.getSimulator().getSimulatorTime()));
148 }
149 }
150 catch (RemoteException exception)
151 {
152 exception.printStackTrace();
153 }
154 cumulativeLength += lane.getLength().getSI();
155 endLengths[i] = cumulativeLength;
156 }
157 this.cumulativeLengths = endLengths;
158 this.caption = caption;
159 createChart(this);
160 this.reGraph();
161 }
162
163
164 private Set<LaneBasedGTU> gtusOfInterest = new HashSet<>();
165
166
167 @Override
168 @SuppressWarnings("checkstyle:designforextension")
169 public void notify(final EventInterface event) throws RemoteException
170 {
171 LaneBasedGTU gtu;
172 if (event.getType().equals(Lane.GTU_ADD_EVENT))
173 {
174 Object[] content = (Object[]) event.getContent();
175 gtu = (LaneBasedGTU) content[1];
176 if (!this.gtusOfInterest.contains(gtu))
177 {
178 this.gtusOfInterest.add(gtu);
179 gtu.addListener(this, LaneBasedGTU.MOVE_EVENT);
180 }
181 }
182 else if (event.getType().equals(Lane.GTU_REMOVE_EVENT))
183 {
184 Object[] content = (Object[]) event.getContent();
185 gtu = (LaneBasedGTU) content[1];
186 boolean interest = false;
187 for (Lane lane : gtu.getLanes().keySet())
188 {
189 if (this.path.contains(lane))
190 {
191 interest = true;
192 }
193 }
194 if (!interest)
195 {
196 this.gtusOfInterest.remove(gtu);
197 gtu.removeListener(this, LaneBasedGTU.MOVE_EVENT);
198 }
199 }
200 else if (event.getType().equals(LaneBasedGTU.MOVE_EVENT))
201 {
202 Object[] content = (Object[]) event.getContent();
203 Lane lane = (Lane) content[6];
204 Length posOnLane = (Length) content[7];
205 gtu = (LaneBasedGTU) event.getSource();
206 if (this.path.contains(lane))
207 {
208 addData(gtu, lane, posOnLane.si);
209 }
210 }
211 }
212
213
214
215
216
217
218 private JFreeChart createChart(final JFrame container)
219 {
220 final JLabel statusLabel = new JLabel(" ", SwingConstants.CENTER);
221 container.add(statusLabel, BorderLayout.SOUTH);
222 ChartFactory.setChartTheme(new StandardChartTheme("JFree/Shadow", false));
223 final JFreeChart result =
224 ChartFactory.createXYLineChart(this.caption, "", "", this, PlotOrientation.VERTICAL, false, false, false);
225
226 result.getPlot().setBackgroundPaint(new Color(0.9f, 0.9f, 0.9f));
227 FixCaption.fixCaption(result);
228 NumberAxis xAxis = new NumberAxis("\u2192 " + "time [s]");
229 xAxis.setLowerMargin(0.0);
230 xAxis.setUpperMargin(0.0);
231 NumberAxis yAxis = new NumberAxis("\u2192 " + "Distance [m]");
232 yAxis.setAutoRangeIncludesZero(false);
233 yAxis.setLowerMargin(0.0);
234 yAxis.setUpperMargin(0.0);
235 yAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
236 result.getXYPlot().setDomainAxis(xAxis);
237 result.getXYPlot().setRangeAxis(yAxis);
238 Length minimumPosition = Length.ZERO;
239 Length maximumPosition = new Length(getCumulativeLength(-1), LengthUnit.SI);
240 configureAxis(result.getXYPlot().getRangeAxis(), DoubleScalar.minus(maximumPosition, minimumPosition).getSI());
241 final XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) result.getXYPlot().getRenderer();
242 renderer.setBaseLinesVisible(true);
243 renderer.setBaseShapesVisible(false);
244 renderer.setBaseShape(new Line2D.Float(0, 0, 0, 0));
245 final ChartPanel cp = new ChartPanel(result);
246 cp.setMouseWheelEnabled(true);
247 final PointerHandler ph = new PointerHandler()
248 {
249
250 @Override
251 void updateHint(final double domainValue, final double rangeValue)
252 {
253 if (Double.isNaN(domainValue))
254 {
255 statusLabel.setText(" ");
256 return;
257 }
258 String value = "";
259
260
261
262
263
264
265
266
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 statusLabel.setText(String.format("t=%.0fs, distance=%.0fm%s", domainValue, rangeValue, value));
320 }
321 };
322 cp.addMouseMotionListener(ph);
323 cp.addMouseListener(ph);
324 container.add(cp, BorderLayout.CENTER);
325
326
327 JPopupMenu popupMenu = cp.getPopupMenu();
328 popupMenu.add(new JPopupMenu.Separator());
329 popupMenu.add(StandAloneChartWindow.createMenuItem(this));
330 return result;
331 }
332
333
334
335
336 public final void reGraph()
337 {
338 for (DatasetChangeListener dcl : this.listenerList.getListeners(DatasetChangeListener.class))
339 {
340 if (dcl instanceof XYPlot)
341 {
342 configureAxis(((XYPlot) dcl).getDomainAxis(), this.maximumTime.getSI());
343 }
344 }
345 notifyListeners(new DatasetChangeEvent(this, null));
346 }
347
348
349
350
351
352 private void notifyListeners(final DatasetChangeEvent event)
353 {
354 for (DatasetChangeListener dcl : this.listenerList.getListeners(DatasetChangeListener.class))
355 {
356 dcl.datasetChanged(event);
357 }
358 }
359
360
361
362
363
364
365 private static void configureAxis(final ValueAxis valueAxis, final double range)
366 {
367 valueAxis.setUpperBound(range);
368 valueAxis.setLowerMargin(0);
369 valueAxis.setUpperMargin(0);
370 valueAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
371 valueAxis.setAutoRange(true);
372 valueAxis.setAutoRangeMinimumSize(range);
373 valueAxis.centerRange(range / 2);
374 }
375
376
377 @Override
378 public void actionPerformed(final ActionEvent e)
379 {
380
381 }
382
383
384 private HashMap<String, Trajectory> trajectories = new HashMap<String, Trajectory>();
385
386
387 private ArrayList<Trajectory> trajectoryIndices = new ArrayList<Trajectory>();
388
389
390
391
392
393
394
395 protected final void addData(final LaneBasedGTU gtu, final Lane lane, final double posOnLane)
396 {
397 int index = this.path.indexOf(lane);
398 if (index < 0)
399 {
400
401 System.err.println("TrajectoryPlot: GTU " + gtu.getId() + " is not registered on lane " + lane.toString());
402 return;
403 }
404 double lengthOffset = index == 0 ? 0 : this.cumulativeLengths[index - 1];
405
406 String key = gtu.getId();
407 Trajectory carTrajectory = this.trajectories.get(key);
408 if (null == carTrajectory)
409 {
410
411 carTrajectory = new Trajectory(key);
412 this.trajectoryIndices.add(carTrajectory);
413 this.trajectories.put(key, carTrajectory);
414 }
415 try
416 {
417 carTrajectory.addSegment(gtu, lane, lengthOffset, posOnLane);
418 }
419 catch (NetworkException | GTUException exception)
420 {
421
422 System.err.println("TrajectoryPlot: GTU " + gtu.getId() + " on lane " + lane.toString() + " caused exception "
423 + exception.getMessage());
424 }
425 }
426
427
428
429
430
431
432 class Trajectory implements Serializable
433 {
434
435 private static final long serialVersionUID = 20140000L;
436
437
438 private Time currentEndTime;
439
440
441
442
443
444 public final Time getCurrentEndTime()
445 {
446 return this.currentEndTime;
447 }
448
449
450 private Double lastPosition;
451
452
453
454
455
456 public final Double getLastPosition()
457 {
458 return this.lastPosition;
459 }
460
461
462 private final Object id;
463
464
465
466
467
468 public final Object getId()
469 {
470 return this.id;
471 }
472
473
474 private ArrayList<Double> positions = new ArrayList<Double>();
475
476
477 private int firstSample;
478
479
480
481
482
483 Trajectory(final Object id)
484 {
485 this.id = id;
486 }
487
488
489
490
491
492
493
494
495
496
497
498 public final void addSegment(final LaneBasedGTU gtu, final Lane lane, final double positionOffset,
499 final double posOnLane) throws NetworkException, GTUException
500 {
501
502 Double position = posOnLane + positionOffset;
503 final int sample = (int) Math.ceil(gtu.getOperationalPlan().getStartTime().si / getSampleInterval());
504 if (this.positions.size() == 0)
505 {
506 this.firstSample = sample;
507 }
508 while (sample - this.firstSample > this.positions.size())
509 {
510
511 this.positions.add(null);
512 }
513 if (this.lastPosition != null && Math.abs(this.lastPosition - position) > 0.9 * getCumulativeLength(-1))
514 {
515
516 position = null;
517 }
518 this.positions.add(position);
519 this.lastPosition = position;
520
521 this.currentEndTime = gtu.getOperationalPlan().getEndTime();
522 if (this.currentEndTime.gt(getMaximumTime()))
523 {
524 setMaximumTime(this.currentEndTime);
525 }
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581 }
582
583
584
585
586
587 public int size()
588 {
589 return this.positions.size();
590 }
591
592
593
594
595
596 public double getTime(final int item)
597 {
598 return (item + this.firstSample) * getSampleInterval();
599 }
600
601
602
603
604
605 public double getDistance(final int item)
606 {
607 Double distance = this.positions.get(item);
608 if (null == distance)
609 {
610 return Double.NaN;
611 }
612 return this.positions.get(item);
613 }
614
615
616 @Override
617 public final String toString()
618 {
619 return "Trajectory [currentEndTime=" + this.currentEndTime + ", currentEndPosition=" + this.lastPosition
620 + ", id=" + this.id + ", positions.size=" + this.positions.size() + ", firstSample=" + this.firstSample
621 + "]";
622 }
623 }
624
625
626 @Override
627 public final int getSeriesCount()
628 {
629 return this.trajectories.size();
630 }
631
632
633 @Override
634 public final Comparable<Integer> getSeriesKey(final int series)
635 {
636 return series;
637 }
638
639
640 @SuppressWarnings("rawtypes")
641 @Override
642 public final int indexOf(final Comparable seriesKey)
643 {
644 if (seriesKey instanceof Integer)
645 {
646 return (Integer) seriesKey;
647 }
648 return -1;
649 }
650
651
652 @Override
653 public final void addChangeListener(final DatasetChangeListener listener)
654 {
655 this.listenerList.add(DatasetChangeListener.class, listener);
656 }
657
658
659 @Override
660 public final void removeChangeListener(final DatasetChangeListener listener)
661 {
662 this.listenerList.remove(DatasetChangeListener.class, listener);
663 }
664
665
666 @Override
667 public final DatasetGroup getGroup()
668 {
669 return this.datasetGroup;
670 }
671
672
673 @Override
674 public final void setGroup(final DatasetGroup group)
675 {
676 this.datasetGroup = group;
677 }
678
679
680 @Override
681 public final DomainOrder getDomainOrder()
682 {
683 return DomainOrder.ASCENDING;
684 }
685
686
687 @Override
688 public final int getItemCount(final int series)
689 {
690 return this.trajectoryIndices.get(series).size();
691 }
692
693
694 @Override
695 public final Number getX(final int series, final int item)
696 {
697 double v = getXValue(series, item);
698 if (Double.isNaN(v))
699 {
700 return null;
701 }
702 return v;
703 }
704
705
706 @Override
707 public final double getXValue(final int series, final int item)
708 {
709 return this.trajectoryIndices.get(series).getTime(item);
710 }
711
712
713 @Override
714 public final Number getY(final int series, final int item)
715 {
716 double v = getYValue(series, item);
717 if (Double.isNaN(v))
718 {
719 return null;
720 }
721 return v;
722 }
723
724
725 @Override
726 public final double getYValue(final int series, final int item)
727 {
728 return this.trajectoryIndices.get(series).getDistance(item);
729 }
730
731
732 @Override
733 public final JFrame addViewer()
734 {
735 JFrame result = new JFrame(this.caption);
736 result.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
737 JFreeChart newChart = createChart(result);
738 newChart.setTitle((String) null);
739 addChangeListener(newChart.getPlot());
740 return result;
741 }
742
743
744 @Override
745 public String toString()
746 {
747 return "TrajectoryPlot [sampleInterval=" + this.sampleInterval + ", path=" + this.path + ", cumulativeLengths.length="
748 + this.cumulativeLengths.length + ", maximumTime=" + this.maximumTime + ", caption=" + this.caption
749 + ", trajectories.size=" + this.trajectories.size() + "]";
750 }
751
752 }