View Javadoc
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&lt;TimeUnit&gt;; 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&lt;Lane&gt;; 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 }