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 * Trajectory plot.
55 * <p>
56 * Copyright (c) 2013-2017 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
57 * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
58 * <p>
59 * $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
60 * initial version Jul 24, 2014 <br>
61 * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
62 */
63 public class TrajectoryPlot extends AbstractOTSPlot implements XYDataset, LaneBasedGTUSampler, EventListenerInterface
64
65 {
66 /** */
67 private static final long serialVersionUID = 20140724L;
68
69 /** Sample interval of this TrajectoryPlot. */
70 private final Duration sampleInterval;
71
72 /** The simulator. */
73 private final OTSDEVSSimulatorInterface simulator;
74
75 /**
76 * @return sampleInterval if this TrajectoryPlot samples at a fixed rate, or null if this TrajectoryPlot samples on the GTU
77 * move events
78 */
79 public final Duration getSampleInterval()
80 {
81 return this.sampleInterval;
82 }
83
84 /** The cumulative lengths of the elements of path. */
85 private final double[] cumulativeLengths;
86
87 /**
88 * Retrieve the cumulative length of the sampled path at the end of a path element.
89 * @param index int; the index of the path element; if -1, the total length of the path is returned
90 * @return double; the cumulative length at the end of the specified path element in meters (si)
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 /** Maximum of the time axis. */
98 private Time maximumTime = new Time(300, TimeUnit.BASE);
99
100 /**
101 * @return maximumTime
102 */
103 public final Time getMaximumTime()
104 {
105 return this.maximumTime;
106 }
107
108 /**
109 * @param maximumTime set maximumTime
110 */
111 public final void setMaximumTime(final Time maximumTime)
112 {
113 this.maximumTime = maximumTime;
114 }
115
116 /** Not used internally. */
117 private DatasetGroup datasetGroup = null;
118
119 /**
120 * Create a new TrajectoryPlot.
121 * @param caption String; the text to show above the TrajectoryPlot
122 * @param sampleInterval DoubleScalarRel<TimeUnit>; the time between samples of this TrajectoryPlot, or null in which
123 * case the GTUs are sampled whenever they fire a MOVE_EVENT
124 * @param path ArrayList<Lane>; the series of Lanes that will provide the data for this TrajectoryPlot
125 * @param simulator OTSDEVSSimulatorInterface; the simulator
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 // Register the GTUs currently (i.e. already) on the lane (if any) for statistics sampling.
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(); // fixes the domain axis
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 /** {@inheritDoc} */
173 @Override
174 public final GraphType getGraphType()
175 {
176 return GraphType.TRAJECTORY;
177 }
178
179 /**
180 * Sample all the GTUs on the observed lanes.
181 */
182 public final 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 // Schedule the next sample
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 /** The GTUs that might be of interest to gather statistics about. */
225 private Set<LaneBasedGTU> gtusOfInterest = new HashSet<>();
226
227 /** {@inheritDoc} */
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 // ignore - lane will be null
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 /** {@inheritDoc} */
291 @Override
292 protected final 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 // Overrule the default background paint because some of the lines are invisible on top of this default.
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 /** {@inheritDoc} */
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 XYDataset dataset = plot.getDataset();
335 double bestDistance = Double.MAX_VALUE;
336 Trajectory bestTrajectory = null;
337 final int mousePrecision = 5;
338 java.awt.geom.Point2D.Double mousePoint = new java.awt.geom.Point2D.Double(t, distance);
339 double lowTime =
340 plot.getDomainAxis().java2DToValue(p.getX() - mousePrecision, pi.getDataArea(),
341 plot.getDomainAxisEdge()) - 1;
342 double highTime =
343 plot.getDomainAxis().java2DToValue(p.getX() + mousePrecision, pi.getDataArea(),
344 plot.getDomainAxisEdge()) + 1;
345 double lowDistance =
346 plot.getRangeAxis().java2DToValue(p.getY() + mousePrecision, pi.getDataArea(),
347 plot.getRangeAxisEdge()) - 20;
348 double highDistance =
349 plot.getRangeAxis().java2DToValue(p.getY() - mousePrecision, pi.getDataArea(),
350 plot.getRangeAxisEdge()) + 20;
351 // System.out.println(String.format("Searching area t[%.1f-%.1f], x[%.1f,%.1f]", lowTime, highTime,
352 // lowDistance, highDistance));
353 for (Trajectory trajectory : this.trajectories)
354 {
355 java.awt.geom.Point2D.Double[] clippedTrajectory =
356 trajectory.clipTrajectory(lowTime, highTime, lowDistance, highDistance);
357 if (null == clippedTrajectory)
358 continue;
359 java.awt.geom.Point2D.Double prevPoint = null;
360 for (java.awt.geom.Point2D.Double trajectoryPoint : clippedTrajectory)
361 {
362 if (null != prevPoint)
363 {
364 double thisDistance = Planar.distancePolygonToPoint(clippedTrajectory, mousePoint);
365 if (thisDistance < bestDistance)
366 {
367 bestDistance = thisDistance;
368 bestTrajectory = trajectory;
369 }
370 }
371 prevPoint = trajectoryPoint;
372 }
373 }
374 if (null != bestTrajectory)
375 {
376 for (SimulatedObject so : indices.keySet())
377 if (this.trajectories.get(indices.get(so)) == bestTrajectory)
378 {
379 Point2D.Double bestPosition = bestTrajectory.getEstimatedPosition(t);
380 if (null == bestPosition)
381 continue;
382 value =
383 String.format(
384 Main.locale,
385 ": vehicle %s; location on measurement path at t=%.1fs: "
386 + "longitudinal %.1fm, lateral %.1fm",
387 so.toString(), t, bestPosition.x, bestPosition.y);
388 }
389 }
390 else
391 value = "";
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 // TODO ensure that shapes for all the data points don't get allocated.
400 // Currently JFreeChart allocates many megabytes of memory for Ellipses that are never drawn.
401 JPopupMenu popupMenu = cp.getPopupMenu();
402 popupMenu.add(new JPopupMenu.Separator());
403 popupMenu.add(StandAloneChartWindow.createMenuItem(this));
404 return result;
405 }
406
407 /** {@inheritDoc} */
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)); // This guess work actually works!
419 }
420
421 /**
422 * Configure the range of an axis.
423 * @param valueAxis ValueAxis
424 * @param range double; the upper bound of the axis
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 /** {@inheritDoc} */
438 @Override
439 public void actionPerformed(final ActionEvent e)
440 {
441 // not yet
442 }
443
444 /** All stored trajectories. */
445 private HashMap<String, Trajectory> trajectories = new HashMap<String, Trajectory>();
446
447 /** Quick access to the Nth trajectory. */
448 private ArrayList<Trajectory> trajectoryIndices = new ArrayList<Trajectory>();
449
450 /**
451 * Add data for a GTU on a lane to this graph.
452 * @param gtu the gtu to add the data for
453 * @param lane the lane on which the GTU is registered
454 * @param posOnLane the position on the lane as a double si Length
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 // error -- silently ignore for now. Graphs should not cause errors.
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 // Create a new Trajectory for this GTU
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 // error -- silently ignore for now. Graphs should not cause errors.
484 System.err.println("TrajectoryPlot: GTU " + gtu.getId() + " on lane " + lane.toString() + " caused exception "
485 + exception.getMessage());
486 }
487 }
488
489 /**
490 * Common interface for both (all?) types of trajectories.
491 */
492 interface Trajectory
493 {
494 /**
495 * Retrieve the time of the last stored event.
496 * @return Time; the time of the last stored event
497 */
498 Time getCurrentEndTime();
499
500 /**
501 * Retrieve the last recorded non-null position, or null if no non-null positions have been recorded yet.
502 * @return Double; the last recorded position of this Trajectory in meters
503 */
504 Double getLastPosition();
505
506 /**
507 * Retrieve the id of this Trajectory.
508 * @return Object; the id of this Trajectory
509 */
510 String getId();
511
512 /**
513 * Add a trajectory segment sample and update the currentEndTime and currentEndPosition.
514 * @param gtu AbstractLaneBasedGTU; the GTU whose currently committed trajectory segment must be added
515 * @param lane Lane; the Lane that the positionOffset is valid for
516 * @param position Double; distance in meters from the start of the trajectory
517 * @throws NetworkException when car is not on lane anymore
518 * @throws GTUException on problems obtaining data from the GTU
519 */
520 void addSample(LaneBasedGTU gtu, Lane lane, double position) throws NetworkException, GTUException;
521
522 /**
523 * Retrieve the number of stored samples in this Trajectory.
524 * @return int; number of stored samples
525 */
526 int size();
527
528 /**
529 * Return the time of the Nth stored sample.
530 * @param item int; the index of the sample
531 * @return double; the time of the sample
532 */
533 double getTime(int item);
534
535 /**
536 * Return the distance of the Nth stored sample.
537 * @param item int; the index of the sample
538 * @return double; the distance of the sample
539 */
540 double getDistance(int item);
541
542 }
543
544 /**
545 * Store trajectory data for use with a variable sample rate.
546 * <p>
547 * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
548 */
549 class VariableSampleRateTrajectory implements Trajectory, Serializable
550 {
551 /** */
552 private static final long serialVersionUID = 20140000L;
553
554 /** Time of (current) end of trajectory. */
555 private Time currentEndTime;
556
557 /** ID of the GTU. */
558 private final String id;
559
560 /** Storage for the samples of the GTU. */
561 private ArrayList<DistanceAndTime> samples = new ArrayList<DistanceAndTime>();
562
563 /**
564 * Construct a new VariableSamplerateTrajectory.
565 * @param id String; id of the new Trajectory (id of the GTU)
566 */
567 VariableSampleRateTrajectory(final String id)
568 {
569 this.id = id;
570 }
571
572 /** {@inheritDoc} */
573 @Override
574 public Time getCurrentEndTime()
575 {
576 return this.currentEndTime;
577 }
578
579 /** {@inheritDoc} */
580 @Override
581 public Double getLastPosition()
582 {
583 return null;
584 }
585
586 /** {@inheritDoc} */
587 @Override
588 public String getId()
589 {
590 return this.id;
591 }
592
593 /** {@inheritDoc} */
594 @Override
595 public void addSample(final LaneBasedGTU gtu, final Lane lane, final double position)
596 throws NetworkException, GTUException
597 {
598 if (this.samples.size() > 0)
599 {
600 DistanceAndTime lastSample = this.samples.get(this.samples.size() - 1);
601 if (null != lastSample)
602 {
603 Double lastPosition = lastSample.getDistance();
604 if (null != lastPosition && Math.abs(lastPosition - position) > 0.9 * getCumulativeLength(-1))
605 {
606 // wrap around... probably circular lane, insert a GTU left trajectory event.
607 recordGTULeftTrajectoryEvent();
608 }
609 }
610 }
611 this.currentEndTime = gtu.getSimulator().getSimulatorTime().getTime();
612 this.samples.add(new DistanceAndTime(position, this.currentEndTime.si));
613 if (gtu.getSimulator().getSimulatorTime().getTime().gt(getMaximumTime()))
614 {
615 setMaximumTime(gtu.getSimulator().getSimulatorTime().getTime());
616 }
617 }
618
619 /**
620 * Store that the GTU went off of the trajectory.
621 */
622 public void recordGTULeftTrajectoryEvent()
623 {
624 this.samples.add(null);
625 }
626
627 /** {@inheritDoc} */
628 @Override
629 public int size()
630 {
631 return this.samples.size();
632 }
633
634 /**
635 * Retrieve the Nth sample.
636 * @param item int; the number of the sample
637 * @return DistanceAndTime; the Nth sample (samples can be null to indicate that GTU went off the trajectory).
638 */
639 private DistanceAndTime getSample(final int item)
640 {
641 return this.samples.get(item);
642 }
643
644 /** {@inheritDoc} */
645 @Override
646 public double getTime(final int item)
647 {
648 DistanceAndTime sample = getSample(item);
649 if (null == sample)
650 {
651 return Double.NaN;
652 }
653 return this.samples.get(item).getTime();
654 }
655
656 /** {@inheritDoc} */
657 @Override
658 public double getDistance(final int item)
659 {
660 DistanceAndTime sample = getSample(item);
661 if (null == sample)
662 {
663 return Double.NaN;
664 }
665 return sample.getDistance();
666 }
667
668 /** {@inheritDoc} */
669 @Override
670 public String toString()
671 {
672 return "VariableSampleRateTrajectory [id=" + this.id + ", currentEndTime=" + this.currentEndTime + "]";
673 }
674
675 /**
676 * Store a position and a time.
677 */
678 class DistanceAndTime
679 {
680 /** The position [m]. */
681 private final double distance;
682
683 /** The time [s]. */
684 private final double time;
685
686 /**
687 * Construct a new DistanceAndTime object.
688 * @param distance double; the position
689 * @param time double; the time
690 */
691 DistanceAndTime(final double distance, final double time)
692 {
693 this.distance = distance;
694 this.time = time;
695 }
696
697 /**
698 * Retrieve the position.
699 * @return double; the position
700 */
701 public double getDistance()
702 {
703 return this.distance;
704 }
705
706 /**
707 * Retrieve the time.
708 * @return double; the time
709 */
710 public double getTime()
711 {
712 return this.time;
713 }
714
715 /** {@inheritDoc} */
716 @Override
717 public String toString()
718 {
719 return "DistanceAndTime [distance=" + this.distance + ", time=" + this.time + "]";
720 }
721
722 }
723 }
724
725 /**
726 * Store trajectory data for use with a fixed sample rate.
727 * <p>
728 * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
729 */
730 class FixedSampleRateTrajectory implements Trajectory, Serializable
731 {
732 /** */
733 private static final long serialVersionUID = 20140000L;
734
735 /** Time of (current) end of trajectory. */
736 private Time currentEndTime;
737
738 /** ID of the GTU. */
739 private final String id;
740
741 /** Storage for the position of the GTU. */
742 private ArrayList<Double> positions = new ArrayList<Double>();
743
744 /** Sample number of sample with index 0 in positions (following entries will each be one sampleTime later). */
745 private int firstSample;
746
747 /**
748 * Construct a FixedSampleRateTrajectory.
749 * @param id String; id of the new Trajectory (id of the GTU)
750 */
751 FixedSampleRateTrajectory(final String id)
752 {
753 this.id = id;
754 }
755
756 /** {@inheritDoc} */
757 public final Time getCurrentEndTime()
758 {
759 return this.currentEndTime;
760 }
761
762 /** {@inheritDoc} */
763 public final Double getLastPosition()
764 {
765 for (int i = this.positions.size(); --i >= 0;)
766 {
767 Double result = this.positions.get(i);
768 if (null != result)
769 {
770 return result;
771 }
772 }
773 return null;
774 }
775
776 /** {@inheritDoc} */
777 public final String getId()
778 {
779 return this.id;
780 }
781
782 /** {@inheritDoc} */
783 public final void addSample(final LaneBasedGTU gtu, final Lane lane, final double position)
784 throws NetworkException, GTUException
785 {
786 final int sample = (int) Math.ceil(gtu.getOperationalPlan().getStartTime().si / getSampleInterval().si);
787 if (0 == this.positions.size())
788 {
789 this.firstSample = sample;
790 }
791 while (sample - this.firstSample > this.positions.size())
792 {
793 // insert nulls as place holders for unsampled data (usually because vehicle was in a parallel Lane)
794 this.positions.add(null);
795 }
796 Double adjustedPosition = position;
797 Double lastPosition = this.positions.size() > 0 ? this.positions.get(this.positions.size() - 1) : null;
798 if (null != lastPosition && Math.abs(lastPosition - position) > 0.9 * getCumulativeLength(-1))
799 {
800 // wrap around... probably circular lane.
801 adjustedPosition = null;
802 }
803 this.positions.add(adjustedPosition);
804
805 /*-
806 try
807 {
808 final int startSample =
809 (int) Math.ceil(car.getOperationalPlan().getStartTime().getSI() / getSampleInterval());
810 final int endSample =
811 (int) (Math.ceil(car.getOperationalPlan().getEndTime().getSI() / getSampleInterval()));
812 for (int sample = startSample; sample < endSample; sample++)
813 {
814 Time sampleTime = new Time(sample * getSampleInterval(), TimeUnit.SI);
815 Double position = car.position(lane, car.getReference(), sampleTime).getSI() + positionOffset;
816 if (this.positions.size() > 0 && null != this.currentEndPosition
817 && position < this.currentEndPosition.getSI() - 0.001)
818 {
819 if (0 != positionOffset)
820 {
821 // System.out.println("Already added " + car);
822 break;
823 }
824 // System.out.println("inserting null for " + car);
825 position = null; // Wrapping on circular path?
826 }
827 if (this.positions.size() == 0)
828 {
829 this.firstSample = sample;
830 }
831 while (sample - this.firstSample > this.positions.size())
832 {
833 // System.out.println("Inserting nulls");
834 this.positions.add(null); // insert nulls as place holders for unsampled data (usually because
835 // vehicle was temporarily in a parallel Lane)
836 }
837 if (null != position && this.positions.size() > sample - this.firstSample)
838 {
839 // System.out.println("Skipping sample " + car);
840 continue;
841 }
842 this.positions.add(position);
843 }
844 this.currentEndTime = car.getOperationalPlan().getEndTime();
845 this.currentEndPosition = new Length(
846 car.position(lane, car.getReference(), this.currentEndTime).getSI() + positionOffset, LengthUnit.SI);
847 }
848 catch (Exception e)
849 {
850 // TODO lane change causes error...
851 System.err.println("Trajectoryplot caught unexpected Exception: " + e.getMessage());
852 e.printStackTrace();
853 }
854 */
855 if (gtu.getSimulator().getSimulatorTime().getTime().gt(getMaximumTime()))
856 {
857 setMaximumTime(gtu.getSimulator().getSimulatorTime().getTime());
858 }
859 }
860
861 /** {@inheritDoc} */
862 public int size()
863 {
864 return this.positions.size();
865 }
866
867 /** {@inheritDoc} */
868 public double getTime(final int item)
869 {
870 return (item + this.firstSample) * getSampleInterval().si;
871 }
872
873 /**
874 * @param item Integer; the sample number
875 * @return Double; the position indexed by item
876 */
877 public double getDistance(final int item)
878 {
879 Double distance = this.positions.get(item);
880 if (null == distance)
881 {
882 return Double.NaN;
883 }
884 return this.positions.get(item);
885 }
886
887 /** {@inheritDoc} */
888 @Override
889 public final String toString()
890 {
891 return "FixedSampleRateTrajectory [currentEndTime=" + this.currentEndTime + ", id=" + this.id + ", positions.size="
892 + this.positions.size() + ", firstSample=" + this.firstSample + "]";
893 }
894
895 }
896
897 /** {@inheritDoc} */
898 @Override
899 public final int getSeriesCount()
900 {
901 return this.trajectories.size();
902 }
903
904 /** {@inheritDoc} */
905 @Override
906 public final Comparable<Integer> getSeriesKey(final int series)
907 {
908 return series;
909 }
910
911 /** {@inheritDoc} */
912 @SuppressWarnings("rawtypes")
913 @Override
914 public final int indexOf(final Comparable seriesKey)
915 {
916 if (seriesKey instanceof Integer)
917 {
918 return (Integer) seriesKey;
919 }
920 return -1;
921 }
922
923 /** {@inheritDoc} */
924 @Override
925 public final DatasetGroup getGroup()
926 {
927 return this.datasetGroup;
928 }
929
930 /** {@inheritDoc} */
931 @Override
932 public final void setGroup(final DatasetGroup group)
933 {
934 this.datasetGroup = group;
935 }
936
937 /** {@inheritDoc} */
938 @Override
939 public final DomainOrder getDomainOrder()
940 {
941 return DomainOrder.ASCENDING;
942 }
943
944 /** {@inheritDoc} */
945 @Override
946 public final int getItemCount(final int series)
947 {
948 return this.trajectoryIndices.get(series).size();
949 }
950
951 /** {@inheritDoc} */
952 @Override
953 public final Number getX(final int series, final int item)
954 {
955 double v = getXValue(series, item);
956 if (Double.isNaN(v))
957 {
958 return null;
959 }
960 return v;
961 }
962
963 /** {@inheritDoc} */
964 @Override
965 public final double getXValue(final int series, final int item)
966 {
967 return this.trajectoryIndices.get(series).getTime(item);
968 }
969
970 /** {@inheritDoc} */
971 @Override
972 public final Number getY(final int series, final int item)
973 {
974 double v = getYValue(series, item);
975 if (Double.isNaN(v))
976 {
977 return null;
978 }
979 return v;
980 }
981
982 /** {@inheritDoc} */
983 @Override
984 public final double getYValue(final int series, final int item)
985 {
986 return this.trajectoryIndices.get(series).getDistance(item);
987 }
988
989 /** {@inheritDoc} */
990 @Override
991 public final String toString()
992 {
993 return "TrajectoryPlot [sampleInterval=" + this.sampleInterval + ", path=" + getPath() + ", cumulativeLengths.length="
994 + this.cumulativeLengths.length + ", maximumTime=" + this.maximumTime + ", caption=" + getCaption()
995 + ", trajectories.size=" + this.trajectories.size() + "]";
996 }
997
998 }