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-2016 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.SECOND);
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 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 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 public 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(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 // wrap around... probably circular lane, insert a GTU left trajectory event.
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 * Store that the GTU went off of the trajectory.
616 */
617 public void recordGTULeftTrajectoryEvent()
618 {
619 this.samples.add(null);
620 }
621
622 /** {@inheritDoc} */
623 @Override
624 public int size()
625 {
626 return this.samples.size();
627 }
628
629 /**
630 * Retrieve the Nth sample.
631 * @param item int; the number of the sample
632 * @return DistanceAndTime; the Nth sample (samples can be null to indicate that GTU went off the trajectory).
633 */
634 private DistanceAndTime getSample(int item)
635 {
636 return this.samples.get(item);
637 }
638
639 /** {@inheritDoc} */
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 /** {@inheritDoc} */
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 /** {@inheritDoc} */
664 @Override
665 public String toString()
666 {
667 return "VariableSampleRateTrajectory [id=" + this.id + ", currentEndTime=" + this.currentEndTime + "]";
668 }
669
670 /**
671 * Store a position and a time.
672 */
673 class DistanceAndTime
674 {
675 /** The position [m]. */
676 final double distance;
677
678 /** The time [s]. */
679 final double time;
680
681 /**
682 * Construct a new DistanceAndTime object.
683 * @param distance double; the position
684 * @param time double; the time
685 */
686 public DistanceAndTime(final double distance, final double time)
687 {
688 this.distance = distance;
689 this.time = time;
690 }
691
692 /**
693 * Retrieve the position.
694 * @return double; the position
695 */
696 public double getDistance()
697 {
698 return this.distance;
699 }
700
701 /**
702 * Retrieve the time.
703 * @return double; the time
704 */
705 public double getTime()
706 {
707 return this.time;
708 }
709
710 /** {@inheritDoc} */
711 @Override
712 public String toString()
713 {
714 return "DistanceAndTime [distance=" + this.distance + ", time=" + this.time + "]";
715 }
716
717 }
718 }
719
720 /**
721 * Store trajectory data for use with a fixed sample rate.
722 * <p>
723 * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
724 */
725 class FixedSampleRateTrajectory implements Trajectory, Serializable
726 {
727 /** */
728 private static final long serialVersionUID = 20140000L;
729
730 /** Time of (current) end of trajectory. */
731 private Time currentEndTime;
732
733 /** ID of the GTU. */
734 private final String id;
735
736 /** Storage for the position of the GTU. */
737 private ArrayList<Double> positions = new ArrayList<Double>();
738
739 /** Sample number of sample with index 0 in positions (following entries will each be one sampleTime later). */
740 private int firstSample;
741
742 /**
743 * Construct a FixedSampleRateTrajectory.
744 * @param id String; id of the new Trajectory (id of the GTU)
745 */
746 FixedSampleRateTrajectory(final String id)
747 {
748 this.id = id;
749 }
750
751 /** {@inheritDoc} */
752 public final Time getCurrentEndTime()
753 {
754 return this.currentEndTime;
755 }
756
757 /** {@inheritDoc} */
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 /** {@inheritDoc} */
772 public final String getId()
773 {
774 return this.id;
775 }
776
777 /** {@inheritDoc} */
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 // insert nulls as place holders for unsampled data (usually because vehicle was in a parallel Lane)
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 // wrap around... probably circular lane.
796 adjustedPosition = null;
797 }
798 this.positions.add(adjustedPosition);
799
800 this.currentEndTime = gtu.getSimulator().getSimulatorTime().getTime();
801
802 /*-
803 try
804 {
805 final int startSample =
806 (int) Math.ceil(car.getOperationalPlan().getStartTime().getSI() / getSampleInterval());
807 final int endSample =
808 (int) (Math.ceil(car.getOperationalPlan().getEndTime().getSI() / getSampleInterval()));
809 for (int sample = startSample; sample < endSample; sample++)
810 {
811 Time sampleTime = new Time(sample * getSampleInterval(), TimeUnit.SI);
812 Double position = car.position(lane, car.getReference(), sampleTime).getSI() + positionOffset;
813 if (this.positions.size() > 0 && null != this.currentEndPosition
814 && position < this.currentEndPosition.getSI() - 0.001)
815 {
816 if (0 != positionOffset)
817 {
818 // System.out.println("Already added " + car);
819 break;
820 }
821 // System.out.println("inserting null for " + car);
822 position = null; // Wrapping on circular path?
823 }
824 if (this.positions.size() == 0)
825 {
826 this.firstSample = sample;
827 }
828 while (sample - this.firstSample > this.positions.size())
829 {
830 // System.out.println("Inserting nulls");
831 this.positions.add(null); // insert nulls as place holders for unsampled data (usually because
832 // vehicle was temporarily in a parallel Lane)
833 }
834 if (null != position && this.positions.size() > sample - this.firstSample)
835 {
836 // System.out.println("Skipping sample " + car);
837 continue;
838 }
839 this.positions.add(position);
840 }
841 this.currentEndTime = car.getOperationalPlan().getEndTime();
842 this.currentEndPosition = new Length(
843 car.position(lane, car.getReference(), this.currentEndTime).getSI() + positionOffset, LengthUnit.SI);
844 }
845 catch (Exception e)
846 {
847 // TODO lane change causes error...
848 System.err.println("Trajectoryplot caught unexpected Exception: " + e.getMessage());
849 e.printStackTrace();
850 }
851 */
852 if (gtu.getSimulator().getSimulatorTime().getTime().gt(getMaximumTime()))
853 {
854 setMaximumTime(gtu.getSimulator().getSimulatorTime().getTime());
855 }
856 }
857
858 /** {@inheritDoc} */
859 public int size()
860 {
861 return this.positions.size();
862 }
863
864 /** {@inheritDoc} */
865 public double getTime(final int item)
866 {
867 return (item + this.firstSample) * getSampleInterval().si;
868 }
869
870 /**
871 * @param item Integer; the sample number
872 * @return Double; the position indexed by item
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 /** {@inheritDoc} */
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 /** {@inheritDoc} */
895 @Override
896 public final int getSeriesCount()
897 {
898 return this.trajectories.size();
899 }
900
901 /** {@inheritDoc} */
902 @Override
903 public final Comparable<Integer> getSeriesKey(final int series)
904 {
905 return series;
906 }
907
908 /** {@inheritDoc} */
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 /** {@inheritDoc} */
921 @Override
922 public final DatasetGroup getGroup()
923 {
924 return this.datasetGroup;
925 }
926
927 /** {@inheritDoc} */
928 @Override
929 public final void setGroup(final DatasetGroup group)
930 {
931 this.datasetGroup = group;
932 }
933
934 /** {@inheritDoc} */
935 @Override
936 public final DomainOrder getDomainOrder()
937 {
938 return DomainOrder.ASCENDING;
939 }
940
941 /** {@inheritDoc} */
942 @Override
943 public final int getItemCount(final int series)
944 {
945 return this.trajectoryIndices.get(series).size();
946 }
947
948 /** {@inheritDoc} */
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 /** {@inheritDoc} */
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 /** {@inheritDoc} */
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 /** {@inheritDoc} */
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 /** {@inheritDoc} */
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 }