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-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&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 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 }