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.geom.Line2D;
7 import java.io.Serializable;
8 import java.rmi.RemoteException;
9 import java.util.ArrayList;
10 import java.util.HashMap;
11 import java.util.HashSet;
12 import java.util.List;
13 import java.util.Map;
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
21 import org.djunits.unit.LengthUnit;
22 import org.djunits.unit.TimeUnit;
23 import org.djunits.value.vdouble.scalar.DoubleScalar;
24 import org.djunits.value.vdouble.scalar.Duration;
25 import org.djunits.value.vdouble.scalar.Length;
26 import org.djunits.value.vdouble.scalar.Time;
27 import org.jfree.chart.ChartFactory;
28 import org.jfree.chart.ChartPanel;
29 import org.jfree.chart.JFreeChart;
30 import org.jfree.chart.StandardChartTheme;
31 import org.jfree.chart.axis.NumberAxis;
32 import org.jfree.chart.axis.ValueAxis;
33 import org.jfree.chart.plot.PlotOrientation;
34 import org.jfree.chart.plot.XYPlot;
35 import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
36 import org.jfree.data.DomainOrder;
37 import org.jfree.data.general.DatasetChangeEvent;
38 import org.jfree.data.general.DatasetChangeListener;
39 import org.jfree.data.general.DatasetGroup;
40 import org.jfree.data.xy.XYDataset;
41 import org.opentrafficsim.core.dsol.OTSDEVSSimulatorInterface;
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.dsol.SimRuntimeException;
49 import nl.tudelft.simulation.event.EventInterface;
50 import nl.tudelft.simulation.event.EventListenerInterface;
51 import nl.tudelft.simulation.event.TimedEvent;
52
53
54
55
56
57
58
59
60
61
62
63 public class TrajectoryPlot extends AbstractOTSPlot implements XYDataset, LaneBasedGTUSampler, EventListenerInterface
64
65 {
66
67 private static final long serialVersionUID = 20140724L;
68
69
70 private final Duration sampleInterval;
71
72
73 private final OTSDEVSSimulatorInterface simulator;
74
75
76
77
78
79 public final Duration getSampleInterval()
80 {
81 return this.sampleInterval;
82 }
83
84
85 private final double[] cumulativeLengths;
86
87
88
89
90
91
92 public final double getCumulativeLength(final int index)
93 {
94 return index == -1 ? this.cumulativeLengths[this.cumulativeLengths.length - 1] : this.cumulativeLengths[index];
95 }
96
97
98 private Time maximumTime = new Time(300, TimeUnit.SECOND);
99
100
101
102
103 public final Time getMaximumTime()
104 {
105 return this.maximumTime;
106 }
107
108
109
110
111 public final void setMaximumTime(final Time maximumTime)
112 {
113 this.maximumTime = maximumTime;
114 }
115
116
117 private DatasetGroup datasetGroup = null;
118
119
120
121
122
123
124
125
126
127 public TrajectoryPlot(final String caption, final Duration sampleInterval, final List<Lane> path,
128 final OTSDEVSSimulatorInterface simulator)
129 {
130 super(caption, path);
131 this.sampleInterval = sampleInterval;
132 this.simulator = simulator;
133 double[] endLengths = new double[path.size()];
134 double cumulativeLength = 0;
135 for (int i = 0; i < path.size(); i++)
136 {
137 Lane lane = path.get(i);
138 lane.addListener(this, Lane.GTU_ADD_EVENT, true);
139 lane.addListener(this, Lane.GTU_REMOVE_EVENT, true);
140 try
141 {
142
143 for (LaneBasedGTU gtu : lane.getGtuList())
144 {
145 notify(new TimedEvent<OTSSimTimeDouble>(Lane.GTU_ADD_EVENT, lane, new Object[] { gtu.getId(), gtu },
146 gtu.getSimulator().getSimulatorTime()));
147 }
148 }
149 catch (RemoteException exception)
150 {
151 exception.printStackTrace();
152 }
153 cumulativeLength += lane.getLength().getSI();
154 endLengths[i] = cumulativeLength;
155 }
156 this.cumulativeLengths = endLengths;
157 setChart(createChart(this));
158 this.reGraph();
159 if (null != this.sampleInterval)
160 {
161 try
162 {
163 this.simulator.scheduleEventRel(Duration.ZERO, this, this, "sample", null);
164 }
165 catch (SimRuntimeException exception)
166 {
167 exception.printStackTrace();
168 }
169 }
170 }
171
172
173 @Override
174 public final GraphType getGraphType()
175 {
176 return GraphType.TRAJECTORY;
177 }
178
179
180
181
182 public void sample()
183 {
184 Time now = this.simulator.getSimulatorTime().getTime();
185 for (LaneBasedGTU gtu : this.gtusOfInterest)
186 {
187 try
188 {
189 Map<Lane, Length> positions = gtu.positions(gtu.getReference(), now);
190 int hits = 0;
191 for (Lane lane : positions.keySet())
192 {
193 if (getPath().contains(lane))
194 {
195 Length position = positions.get(lane);
196 if (position.si >= 0 && position.si <= lane.getLength().si)
197 {
198 addData(gtu, lane, positions.get(lane).si);
199 hits++;
200 }
201 }
202 }
203 if (1 != hits)
204 {
205 System.err.println("GTU " + gtu + " scored " + hits + " (expected 1 hit)");
206 }
207 }
208 catch (GTUException exception)
209 {
210 exception.printStackTrace();
211 }
212 }
213
214 try
215 {
216 this.simulator.scheduleEventRel(this.sampleInterval, this, this, "sample", null);
217 }
218 catch (SimRuntimeException exception)
219 {
220 exception.printStackTrace();
221 }
222 }
223
224
225 private Set<LaneBasedGTU> gtusOfInterest = new HashSet<>();
226
227
228 @Override
229 @SuppressWarnings("checkstyle:designforextension")
230 public void notify(final EventInterface event) throws RemoteException
231 {
232 LaneBasedGTU gtu;
233 if (event.getType().equals(Lane.GTU_ADD_EVENT))
234 {
235 Object[] content = (Object[]) event.getContent();
236 gtu = (LaneBasedGTU) content[1];
237 if (!this.gtusOfInterest.contains(gtu))
238 {
239 this.gtusOfInterest.add(gtu);
240 if (null == this.sampleInterval)
241 {
242 gtu.addListener(this, LaneBasedGTU.LANEBASED_MOVE_EVENT);
243 }
244 }
245 }
246 else if (event.getType().equals(Lane.GTU_REMOVE_EVENT))
247 {
248 Object[] content = (Object[]) event.getContent();
249 gtu = (LaneBasedGTU) content[1];
250 Lane lane = null;
251 try
252 {
253 lane = gtu.getReferencePosition().getLane();
254 }
255 catch (GTUException exception)
256 {
257
258 }
259 if (lane == null || !getPath().contains(lane))
260 {
261 this.gtusOfInterest.remove(gtu);
262 if (null != this.sampleInterval)
263 {
264 gtu.removeListener(this, LaneBasedGTU.LANEBASED_MOVE_EVENT);
265 }
266 else
267 {
268 String key = gtu.getId();
269 VariableSampleRateTrajectory carTrajectory = (VariableSampleRateTrajectory) this.trajectories.get(key);
270 if (null != carTrajectory)
271 {
272 carTrajectory.recordGTULeftTrajectoryEvent();
273 }
274 }
275 }
276 }
277 else if (event.getType().equals(LaneBasedGTU.LANEBASED_MOVE_EVENT))
278 {
279 Object[] content = (Object[]) event.getContent();
280 Lane lane = (Lane) content[6];
281 Length posOnLane = (Length) content[7];
282 gtu = (LaneBasedGTU) event.getSource();
283 if (getPath().contains(lane))
284 {
285 addData(gtu, lane, posOnLane.si);
286 }
287 }
288 }
289
290
291 @Override
292 protected JFreeChart createChart(final JFrame container)
293 {
294 final JLabel statusLabel = new JLabel(" ", SwingConstants.CENTER);
295 container.add(statusLabel, BorderLayout.SOUTH);
296 ChartFactory.setChartTheme(new StandardChartTheme("JFree/Shadow", false));
297 final JFreeChart result =
298 ChartFactory.createXYLineChart(getCaption(), "", "", this, PlotOrientation.VERTICAL, false, false, false);
299
300 result.getPlot().setBackgroundPaint(new Color(0.9f, 0.9f, 0.9f));
301 FixCaption.fixCaption(result);
302 NumberAxis xAxis = new NumberAxis("\u2192 " + "time [s]");
303 xAxis.setLowerMargin(0.0);
304 xAxis.setUpperMargin(0.0);
305 NumberAxis yAxis = new NumberAxis("\u2192 " + "Distance [m]");
306 yAxis.setAutoRangeIncludesZero(false);
307 yAxis.setLowerMargin(0.0);
308 yAxis.setUpperMargin(0.0);
309 yAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
310 result.getXYPlot().setDomainAxis(xAxis);
311 result.getXYPlot().setRangeAxis(yAxis);
312 Length minimumPosition = Length.ZERO;
313 Length maximumPosition = new Length(getCumulativeLength(-1), LengthUnit.SI);
314 configureAxis(result.getXYPlot().getRangeAxis(), DoubleScalar.minus(maximumPosition, minimumPosition).getSI());
315 final XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) result.getXYPlot().getRenderer();
316 renderer.setBaseLinesVisible(true);
317 renderer.setBaseShapesVisible(false);
318 renderer.setBaseShape(new Line2D.Float(0, 0, 0, 0));
319 final ChartPanel cp = new ChartPanel(result);
320 cp.setMouseWheelEnabled(true);
321 final PointerHandler ph = new PointerHandler()
322 {
323
324 @Override
325 void updateHint(final double domainValue, final double rangeValue)
326 {
327 if (Double.isNaN(domainValue))
328 {
329 statusLabel.setText(" ");
330 return;
331 }
332 String value = "";
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393 statusLabel.setText(String.format("t=%.0fs, distance=%.0fm%s", domainValue, rangeValue, value));
394 }
395 };
396 cp.addMouseMotionListener(ph);
397 cp.addMouseListener(ph);
398 container.add(cp, BorderLayout.CENTER);
399
400
401 JPopupMenu popupMenu = cp.getPopupMenu();
402 popupMenu.add(new JPopupMenu.Separator());
403 popupMenu.add(StandAloneChartWindow.createMenuItem(this));
404 return result;
405 }
406
407
408 @Override
409 public final void reGraph()
410 {
411 for (DatasetChangeListener dcl : getListenerList().getListeners(DatasetChangeListener.class))
412 {
413 if (dcl instanceof XYPlot)
414 {
415 configureAxis(((XYPlot) dcl).getDomainAxis(), this.maximumTime.getSI());
416 }
417 }
418 notifyListeners(new DatasetChangeEvent(this, null));
419 }
420
421
422
423
424
425
426 private static void configureAxis(final ValueAxis valueAxis, final double range)
427 {
428 valueAxis.setUpperBound(range);
429 valueAxis.setLowerMargin(0);
430 valueAxis.setUpperMargin(0);
431 valueAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
432 valueAxis.setAutoRange(true);
433 valueAxis.setAutoRangeMinimumSize(range);
434 valueAxis.centerRange(range / 2);
435 }
436
437
438 @Override
439 public void actionPerformed(final ActionEvent e)
440 {
441
442 }
443
444
445 private HashMap<String, Trajectory> trajectories = new HashMap<String, Trajectory>();
446
447
448 private ArrayList<Trajectory> trajectoryIndices = new ArrayList<Trajectory>();
449
450
451
452
453
454
455
456 protected final void addData(final LaneBasedGTU gtu, final Lane lane, final double posOnLane)
457 {
458 int index = getPath().indexOf(lane);
459 if (index < 0)
460 {
461
462 System.err.println("TrajectoryPlot: GTU " + gtu.getId() + " is not registered on lane " + lane.toString());
463 return;
464 }
465 double lengthOffset = index == 0 ? 0 : this.cumulativeLengths[index - 1];
466
467 String key = gtu.getId();
468 Trajectory carTrajectory = this.trajectories.get(key);
469 if (null == carTrajectory)
470 {
471
472 carTrajectory =
473 null == this.sampleInterval ? new VariableSampleRateTrajectory(key) : new FixedSampleRateTrajectory(key);
474 this.trajectoryIndices.add(carTrajectory);
475 this.trajectories.put(key, carTrajectory);
476 }
477 try
478 {
479 carTrajectory.addSample(gtu, lane, lengthOffset + posOnLane);
480 }
481 catch (NetworkException | GTUException exception)
482 {
483
484 System.err.println("TrajectoryPlot: GTU " + gtu.getId() + " on lane " + lane.toString() + " caused exception "
485 + exception.getMessage());
486 }
487 }
488
489
490
491
492 interface Trajectory
493 {
494
495
496
497
498 Time getCurrentEndTime();
499
500
501
502
503
504 Double getLastPosition();
505
506
507
508
509
510 String getId();
511
512
513
514
515
516
517
518
519
520 void addSample(LaneBasedGTU gtu, Lane lane, double position) throws NetworkException, GTUException;
521
522
523
524
525
526 int size();
527
528
529
530
531
532
533 double getTime(int item);
534
535
536
537
538
539
540 double getDistance(int item);
541
542 }
543
544
545
546
547
548
549 class VariableSampleRateTrajectory implements Trajectory, Serializable
550 {
551
552 private static final long serialVersionUID = 20140000L;
553
554
555 private Time currentEndTime;
556
557
558 private final String id;
559
560
561 private ArrayList<DistanceAndTime> samples = new ArrayList<DistanceAndTime>();
562
563
564
565
566
567 public VariableSampleRateTrajectory(final String id)
568 {
569 this.id = id;
570 }
571
572
573 @Override
574 public Time getCurrentEndTime()
575 {
576 return this.currentEndTime;
577 }
578
579
580 @Override
581 public Double getLastPosition()
582 {
583 return null;
584 }
585
586
587 @Override
588 public String getId()
589 {
590 return this.id;
591 }
592
593
594 @Override
595 public void addSample(LaneBasedGTU gtu, Lane lane, double position) throws NetworkException, GTUException
596 {
597 if (this.samples.size() > 0)
598 {
599 DistanceAndTime lastSample = this.samples.get(this.samples.size() - 1);
600 if (null != lastSample)
601 {
602 Double lastPosition = lastSample.getDistance();
603 if (null != lastPosition && Math.abs(lastPosition - position) > 0.9 * getCumulativeLength(-1))
604 {
605
606 recordGTULeftTrajectoryEvent();
607 }
608 }
609 }
610 this.currentEndTime = gtu.getSimulator().getSimulatorTime().getTime();
611 this.samples.add(new DistanceAndTime(position, this.currentEndTime.si));
612 }
613
614
615
616
617 public void recordGTULeftTrajectoryEvent()
618 {
619 this.samples.add(null);
620 }
621
622
623 @Override
624 public int size()
625 {
626 return this.samples.size();
627 }
628
629
630
631
632
633
634 private DistanceAndTime getSample(int item)
635 {
636 return this.samples.get(item);
637 }
638
639
640 @Override
641 public double getTime(int item)
642 {
643 DistanceAndTime sample = getSample(item);
644 if (null == sample)
645 {
646 return Double.NaN;
647 }
648 return this.samples.get(item).getTime();
649 }
650
651
652 @Override
653 public double getDistance(int item)
654 {
655 DistanceAndTime sample = getSample(item);
656 if (null == sample)
657 {
658 return Double.NaN;
659 }
660 return sample.getDistance();
661 }
662
663
664 @Override
665 public String toString()
666 {
667 return "VariableSampleRateTrajectory [id=" + this.id + ", currentEndTime=" + this.currentEndTime + "]";
668 }
669
670
671
672
673 class DistanceAndTime
674 {
675
676 final double distance;
677
678
679 final double time;
680
681
682
683
684
685
686 public DistanceAndTime(final double distance, final double time)
687 {
688 this.distance = distance;
689 this.time = time;
690 }
691
692
693
694
695
696 public double getDistance()
697 {
698 return this.distance;
699 }
700
701
702
703
704
705 public double getTime()
706 {
707 return this.time;
708 }
709
710
711 @Override
712 public String toString()
713 {
714 return "DistanceAndTime [distance=" + this.distance + ", time=" + this.time + "]";
715 }
716
717 }
718 }
719
720
721
722
723
724
725 class FixedSampleRateTrajectory implements Trajectory, Serializable
726 {
727
728 private static final long serialVersionUID = 20140000L;
729
730
731 private Time currentEndTime;
732
733
734 private final String id;
735
736
737 private ArrayList<Double> positions = new ArrayList<Double>();
738
739
740 private int firstSample;
741
742
743
744
745
746 FixedSampleRateTrajectory(final String id)
747 {
748 this.id = id;
749 }
750
751
752 public final Time getCurrentEndTime()
753 {
754 return this.currentEndTime;
755 }
756
757
758 public final Double getLastPosition()
759 {
760 for (int i = this.positions.size(); --i >= 0;)
761 {
762 Double result = this.positions.get(i);
763 if (null != result)
764 {
765 return result;
766 }
767 }
768 return null;
769 }
770
771
772 public final String getId()
773 {
774 return this.id;
775 }
776
777
778 public final void addSample(final LaneBasedGTU gtu, final Lane lane, final double position)
779 throws NetworkException, GTUException
780 {
781 final int sample = (int) Math.ceil(gtu.getOperationalPlan().getStartTime().si / getSampleInterval().si);
782 if (0 == this.positions.size())
783 {
784 this.firstSample = sample;
785 }
786 while (sample - this.firstSample > this.positions.size())
787 {
788
789 this.positions.add(null);
790 }
791 Double adjustedPosition = position;
792 Double lastPosition = this.positions.size() > 0 ? this.positions.get(this.positions.size() - 1) : null;
793 if (null != lastPosition && Math.abs(lastPosition - position) > 0.9 * getCumulativeLength(-1))
794 {
795
796 adjustedPosition = null;
797 }
798 this.positions.add(adjustedPosition);
799
800 this.currentEndTime = gtu.getSimulator().getSimulatorTime().getTime();
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852 if (gtu.getSimulator().getSimulatorTime().getTime().gt(getMaximumTime()))
853 {
854 setMaximumTime(gtu.getSimulator().getSimulatorTime().getTime());
855 }
856 }
857
858
859 public int size()
860 {
861 return this.positions.size();
862 }
863
864
865 public double getTime(final int item)
866 {
867 return (item + this.firstSample) * getSampleInterval().si;
868 }
869
870
871
872
873
874 public double getDistance(final int item)
875 {
876 Double distance = this.positions.get(item);
877 if (null == distance)
878 {
879 return Double.NaN;
880 }
881 return this.positions.get(item);
882 }
883
884
885 @Override
886 public final String toString()
887 {
888 return "FixedSampleRateTrajectory [currentEndTime=" + this.currentEndTime + ", id=" + this.id + ", positions.size="
889 + this.positions.size() + ", firstSample=" + this.firstSample + "]";
890 }
891
892 }
893
894
895 @Override
896 public final int getSeriesCount()
897 {
898 return this.trajectories.size();
899 }
900
901
902 @Override
903 public final Comparable<Integer> getSeriesKey(final int series)
904 {
905 return series;
906 }
907
908
909 @SuppressWarnings("rawtypes")
910 @Override
911 public final int indexOf(final Comparable seriesKey)
912 {
913 if (seriesKey instanceof Integer)
914 {
915 return (Integer) seriesKey;
916 }
917 return -1;
918 }
919
920
921 @Override
922 public final DatasetGroup getGroup()
923 {
924 return this.datasetGroup;
925 }
926
927
928 @Override
929 public final void setGroup(final DatasetGroup group)
930 {
931 this.datasetGroup = group;
932 }
933
934
935 @Override
936 public final DomainOrder getDomainOrder()
937 {
938 return DomainOrder.ASCENDING;
939 }
940
941
942 @Override
943 public final int getItemCount(final int series)
944 {
945 return this.trajectoryIndices.get(series).size();
946 }
947
948
949 @Override
950 public final Number getX(final int series, final int item)
951 {
952 double v = getXValue(series, item);
953 if (Double.isNaN(v))
954 {
955 return null;
956 }
957 return v;
958 }
959
960
961 @Override
962 public final double getXValue(final int series, final int item)
963 {
964 return this.trajectoryIndices.get(series).getTime(item);
965 }
966
967
968 @Override
969 public final Number getY(final int series, final int item)
970 {
971 double v = getYValue(series, item);
972 if (Double.isNaN(v))
973 {
974 return null;
975 }
976 return v;
977 }
978
979
980 @Override
981 public final double getYValue(final int series, final int item)
982 {
983 return this.trajectoryIndices.get(series).getDistance(item);
984 }
985
986
987 @Override
988 public final String toString()
989 {
990 return "TrajectoryPlot [sampleInterval=" + this.sampleInterval + ", path=" + getPath() + ", cumulativeLengths.length="
991 + this.cumulativeLengths.length + ", maximumTime=" + this.maximumTime + ", caption=" + getCaption()
992 + ", trajectories.size=" + this.trajectories.size() + "]";
993 }
994
995 }