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.event.ActionListener;
7   import java.awt.geom.Line2D;
8   import java.util.ArrayList;
9   import java.util.HashMap;
10  import java.util.List;
11  
12  import javax.swing.JFrame;
13  import javax.swing.JLabel;
14  import javax.swing.JPopupMenu;
15  import javax.swing.SwingConstants;
16  import javax.swing.event.EventListenerList;
17  
18  import org.djunits.unit.LengthUnit;
19  import org.djunits.unit.TimeUnit;
20  import org.djunits.value.StorageType;
21  import org.djunits.value.ValueException;
22  import org.djunits.value.vdouble.scalar.DoubleScalar;
23  import org.djunits.value.vdouble.scalar.Length;
24  import org.djunits.value.vdouble.scalar.Time;
25  import org.djunits.value.vdouble.vector.LengthVector;
26  import org.jfree.chart.ChartFactory;
27  import org.jfree.chart.ChartPanel;
28  import org.jfree.chart.JFreeChart;
29  import org.jfree.chart.StandardChartTheme;
30  import org.jfree.chart.axis.NumberAxis;
31  import org.jfree.chart.axis.ValueAxis;
32  import org.jfree.chart.plot.PlotOrientation;
33  import org.jfree.chart.plot.XYPlot;
34  import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
35  import org.jfree.data.DomainOrder;
36  import org.jfree.data.general.DatasetChangeEvent;
37  import org.jfree.data.general.DatasetChangeListener;
38  import org.jfree.data.general.DatasetGroup;
39  import org.jfree.data.xy.XYDataset;
40  import org.opentrafficsim.core.gtu.GTUException;
41  import org.opentrafficsim.core.network.NetworkException;
42  import org.opentrafficsim.road.gtu.lane.LaneBasedGTU;
43  import org.opentrafficsim.road.network.lane.Lane;
44  
45  /**
46   * Trajectory plot.
47   * <p>
48   * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
49   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
50   * <p>
51   * $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
52   * initial version Jul 24, 2014 <br>
53   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
54   */
55  public class TrajectoryPlot extends JFrame implements ActionListener, XYDataset, MultipleViewerChart, LaneBasedGTUSampler
56  
57  {
58      /** */
59      private static final long serialVersionUID = 20140724L;
60  
61      /** Sample interval of this TrajectoryPlot. */
62      private final Time.Rel sampleInterval;
63  
64      /**
65       * @return sampleInterval
66       */
67      public final Time.Rel getSampleInterval()
68      {
69          return this.sampleInterval;
70      }
71  
72      /** The series of Lanes that provide the data for this TrajectoryPlot. */
73      private final ArrayList<Lane> path;
74  
75      /** The cumulative lengths of the elements of path. */
76      private final LengthVector.Rel cumulativeLengths;
77  
78      /**
79       * Retrieve the cumulative length of the sampled path at the end of a path element.
80       * @param index int; the index of the path element; if -1, the total length of the path is returned
81       * @return Length.Rel; the cumulative length at the end of the specified path element
82       */
83      public final Length.Rel getCumulativeLength(final int index)
84      {
85          int useIndex = -1 == index ? this.cumulativeLengths.size() - 1 : index;
86          try
87          {
88              return new Length.Rel(this.cumulativeLengths.get(useIndex));
89          }
90          catch (ValueException exception)
91          {
92              exception.printStackTrace();
93          }
94          return null; // NOTREACHED
95      }
96  
97      /** Maximum of the time axis. */
98      private Time.Abs maximumTime = new Time.Abs(300, TimeUnit.SECOND);
99  
100     /**
101      * @return maximumTime
102      */
103     public final Time.Abs getMaximumTime()
104     {
105         return this.maximumTime;
106     }
107 
108     /**
109      * @param maximumTime set maximumTime
110      */
111     public final void setMaximumTime(final Time.Abs maximumTime)
112     {
113         this.maximumTime = maximumTime;
114     }
115 
116     /** List of parties interested in changes of this ContourPlot. */
117     private transient EventListenerList listenerList = new EventListenerList();
118 
119     /** Not used internally. */
120     private DatasetGroup datasetGroup = null;
121 
122     /** Name of the chart. */
123     private final String caption;
124 
125     /**
126      * Create a new TrajectoryPlot.
127      * @param caption String; the text to show above the TrajectoryPlot
128      * @param sampleInterval DoubleScalarRel&lt;TimeUnit&gt;; the time between samples of this TrajectoryPlot
129      * @param path ArrayList&lt;Lane&gt;; the series of Lanes that will provide the data for this TrajectoryPlot
130      */
131     public TrajectoryPlot(final String caption, final Time.Rel sampleInterval, final List<Lane> path)
132     {
133         this.sampleInterval = sampleInterval;
134         this.path = new ArrayList<Lane>(path); // make a copy
135         double[] endLengths = new double[path.size()];
136         double cumulativeLength = 0;
137         LengthVector.Rel lengths = null;
138         for (int i = 0; i < path.size(); i++)
139         {
140             Lane lane = path.get(i);
141             lane.addSampler(this);
142             cumulativeLength += lane.getLength().getSI();
143             endLengths[i] = cumulativeLength;
144         }
145         try
146         {
147             lengths = new LengthVector.Rel(endLengths, LengthUnit.SI, StorageType.DENSE);
148         }
149         catch (ValueException exception)
150         {
151             exception.printStackTrace();
152         }
153         this.cumulativeLengths = lengths;
154         this.caption = caption;
155         createChart(this);
156         this.reGraph(); // fixes the domain axis
157     }
158 
159     /**
160      * Create the visualization.
161      * @param container JFrame; the JFrame that will be filled with chart and the status label
162      * @return JFreeChart; the visualization
163      */
164     private JFreeChart createChart(final JFrame container)
165     {
166         final JLabel statusLabel = new JLabel(" ", SwingConstants.CENTER);
167         container.add(statusLabel, BorderLayout.SOUTH);
168         ChartFactory.setChartTheme(new StandardChartTheme("JFree/Shadow", false));
169         final JFreeChart result =
170                 ChartFactory.createXYLineChart(this.caption, "", "", this, PlotOrientation.VERTICAL, false, false, false);
171         // Overrule the default background paint because some of the lines are invisible on top of this default.
172         result.getPlot().setBackgroundPaint(new Color(0.9f, 0.9f, 0.9f));
173         FixCaption.fixCaption(result);
174         NumberAxis xAxis = new NumberAxis("\u2192 " + "time [s]");
175         xAxis.setLowerMargin(0.0);
176         xAxis.setUpperMargin(0.0);
177         NumberAxis yAxis = new NumberAxis("\u2192 " + "Distance [m]");
178         yAxis.setAutoRangeIncludesZero(false);
179         yAxis.setLowerMargin(0.0);
180         yAxis.setUpperMargin(0.0);
181         yAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
182         result.getXYPlot().setDomainAxis(xAxis);
183         result.getXYPlot().setRangeAxis(yAxis);
184         Length.Rel minimumPosition = Length.Rel.ZERO;
185         Length.Rel maximumPosition = getCumulativeLength(-1);
186         configureAxis(result.getXYPlot().getRangeAxis(), DoubleScalar.minus(maximumPosition, minimumPosition).getSI());
187         final XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) result.getXYPlot().getRenderer();
188         renderer.setBaseLinesVisible(true);
189         renderer.setBaseShapesVisible(false);
190         renderer.setBaseShape(new Line2D.Float(0, 0, 0, 0));
191         final ChartPanel cp = new ChartPanel(result);
192         cp.setMouseWheelEnabled(true);
193         final PointerHandler ph = new PointerHandler()
194         {
195             /** {@inheritDoc} */
196             @Override
197             void updateHint(final double domainValue, final double rangeValue)
198             {
199                 if (Double.isNaN(domainValue))
200                 {
201                     statusLabel.setText(" ");
202                     return;
203                 }
204                 String value = "";
205                 /*-
206                 XYDataset dataset = plot.getDataset();
207                 double bestDistance = Double.MAX_VALUE;
208                 Trajectory bestTrajectory = null;
209                 final int mousePrecision = 5;
210                 java.awt.geom.Point2D.Double mousePoint = new java.awt.geom.Point2D.Double(t, distance);
211                 double lowTime =
212                         plot.getDomainAxis().java2DToValue(p.getX() - mousePrecision, pi.getDataArea(),
213                                 plot.getDomainAxisEdge()) - 1;
214                 double highTime =
215                         plot.getDomainAxis().java2DToValue(p.getX() + mousePrecision, pi.getDataArea(),
216                                 plot.getDomainAxisEdge()) + 1;
217                 double lowDistance =
218                         plot.getRangeAxis().java2DToValue(p.getY() + mousePrecision, pi.getDataArea(),
219                                 plot.getRangeAxisEdge()) - 20;
220                 double highDistance =
221                         plot.getRangeAxis().java2DToValue(p.getY() - mousePrecision, pi.getDataArea(),
222                                 plot.getRangeAxisEdge()) + 20;
223                 // System.out.println(String.format("Searching area t[%.1f-%.1f], x[%.1f,%.1f]", lowTime, highTime,
224                 // lowDistance, highDistance));
225                 for (Trajectory trajectory : this.trajectories)
226                 {
227                     java.awt.geom.Point2D.Double[] clippedTrajectory =
228                             trajectory.clipTrajectory(lowTime, highTime, lowDistance, highDistance);
229                     if (null == clippedTrajectory)
230                         continue;
231                     java.awt.geom.Point2D.Double prevPoint = null;
232                     for (java.awt.geom.Point2D.Double trajectoryPoint : clippedTrajectory)
233                     {
234                         if (null != prevPoint)
235                         {
236                             double thisDistance = Planar.distancePolygonToPoint(clippedTrajectory, mousePoint);
237                             if (thisDistance < bestDistance)
238                             {
239                                 bestDistance = thisDistance;
240                                 bestTrajectory = trajectory;
241                             }
242                         }
243                         prevPoint = trajectoryPoint;
244                     }
245                 }
246                 if (null != bestTrajectory)
247                 {
248                     for (SimulatedObject so : indices.keySet())
249                         if (this.trajectories.get(indices.get(so)) == bestTrajectory)
250                         {
251                             Point2D.Double bestPosition = bestTrajectory.getEstimatedPosition(t);
252                             if (null == bestPosition)
253                                 continue;
254                             value =
255                                     String.format(
256                                             Main.locale,
257                                             ": vehicle %s; location on measurement path at t=%.1fs: "
258                                             + "longitudinal %.1fm, lateral %.1fm",
259                                             so.toString(), t, bestPosition.x, bestPosition.y);
260                         }
261                 }
262                 else
263                     value = "";
264                  */
265                 statusLabel.setText(String.format("t=%.0fs, distance=%.0fm%s", domainValue, rangeValue, value));
266             }
267         };
268         cp.addMouseMotionListener(ph);
269         cp.addMouseListener(ph);
270         container.add(cp, BorderLayout.CENTER);
271         // TODO ensure that shapes for all the data points don't get allocated.
272         // Currently JFreeChart allocates many megabytes of memory for Ellipses that are never drawn.
273         JPopupMenu popupMenu = cp.getPopupMenu();
274         popupMenu.add(new JPopupMenu.Separator());
275         popupMenu.add(StandAloneChartWindow.createMenuItem(this));
276         return result;
277     }
278 
279     /**
280      * Redraw this TrajectoryGraph (after the underlying data has been changed).
281      */
282     public final void reGraph()
283     {
284         for (DatasetChangeListener dcl : this.listenerList.getListeners(DatasetChangeListener.class))
285         {
286             if (dcl instanceof XYPlot)
287             {
288                 configureAxis(((XYPlot) dcl).getDomainAxis(), this.maximumTime.getSI());
289             }
290         }
291         notifyListeners(new DatasetChangeEvent(this, null)); // This guess work actually works!
292     }
293 
294     /**
295      * Notify interested parties of an event affecting this TrajectoryPlot.
296      * @param event DatasetChangedEvent
297      */
298     private void notifyListeners(final DatasetChangeEvent event)
299     {
300         for (DatasetChangeListener dcl : this.listenerList.getListeners(DatasetChangeListener.class))
301         {
302             dcl.datasetChanged(event);
303         }
304     }
305 
306     /**
307      * Configure the range of an axis.
308      * @param valueAxis ValueAxis
309      * @param range double; the upper bound of the axis
310      */
311     private static void configureAxis(final ValueAxis valueAxis, final double range)
312     {
313         valueAxis.setUpperBound(range);
314         valueAxis.setLowerMargin(0);
315         valueAxis.setUpperMargin(0);
316         valueAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
317         valueAxis.setAutoRange(true);
318         valueAxis.setAutoRangeMinimumSize(range);
319         valueAxis.centerRange(range / 2);
320     }
321 
322     /** {@inheritDoc} */
323     @Override
324     public void actionPerformed(final ActionEvent e)
325     {
326         // not yet
327     }
328 
329     /** All stored trajectories. */
330     private HashMap<String, Trajectory> trajectories = new HashMap<String, Trajectory>();
331 
332     /** Quick access to the Nth trajectory. */
333     private ArrayList<Trajectory> trajectoryIndices = new ArrayList<Trajectory>();
334 
335     /** {@inheritDoc} */
336     public final void addData(final LaneBasedGTU car, final Lane lane) throws NetworkException, GTUException
337     {
338         // final Time.Abs startTime = car.getLastEvaluationTime();
339         // System.out.println("addData car: " + car + ", lastEval: " + startTime);
340         // Convert the position of the car to a position on path.
341         // Find a (the first) lane that car is on that is in our path.
342         double lengthOffset = 0;
343         int index = this.path.indexOf(lane);
344         if (index >= 0)
345         {
346             if (index > 0)
347             {
348                 try
349                 {
350                     lengthOffset = this.cumulativeLengths.getSI(index - 1);
351                 }
352                 catch (ValueException exception)
353                 {
354                     exception.printStackTrace();
355                 }
356             }
357         }
358         else
359         {
360             throw new Error("Car is not on any lane in the path");
361         }
362         // System.out.println("lane index is " + index + " car is " + car);
363         // final Length.Rel startPosition =
364         // DoubleScalar.plus(new Length.Rel(lengthOffset, LengthUnit.SI),
365         // car.position(lane, car.getReference(), startTime));
366         String key = car.getId().toString();
367         Trajectory carTrajectory = this.trajectories.get(key);
368         if (null == carTrajectory)
369         {
370             // Create a new Trajectory for this GTU
371             carTrajectory = new Trajectory(key);
372             this.trajectoryIndices.add(carTrajectory);
373             this.trajectories.put(key, carTrajectory);
374             // System.out.println("Creating new trajectory for GTU " + key);
375         }
376         carTrajectory.addSegment(car, lane, lengthOffset);
377     }
378 
379     /**
380      * Store trajectory data.
381      * <p>
382      * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
383      * <p>
384      * See for project information <a href="http://www.simulation.tudelft.nl/"> www.simulation.tudelft.nl</a>.
385      * <p>
386      * The OpenTrafficSim project is distributed under the following BSD-style license:<br>
387      * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
388      * following conditions are met:
389      * <ul>
390      * <li>Redistributions of source code must retain the above copyright notice, this list of conditions and the following
391      * disclaimer.</li>
392      * <li>Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
393      * disclaimer in the documentation and/or other materials provided with the distribution.</li>
394      * <li>Neither the name of Delft University of Technology, nor the names of its contributors may be used to endorse or
395      * promote products derived from this software without specific prior written permission.</li>
396      * </ul>
397      * This software is provided by the copyright holders and contributors "as is" and any express or implied warranties,
398      * including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are
399      * disclaimed. In no event shall the copyright holder or contributors be liable for any direct, indirect, incidental,
400      * special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services;
401      * loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in
402      * contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this
403      * software, even if advised of the possibility of such damage. $LastChangedDate: 2015-07-15 11:18:39 +0200 (Wed, 15 Jul
404      * 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $, initial versionJul 24, 2014 <br>
405      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
406      */
407     class Trajectory
408     {
409         /** Time of (current) end of trajectory. */
410         private Time.Abs currentEndTime;
411 
412         /**
413          * Retrieve the current end time of this Trajectory.
414          * @return currentEndTime
415          */
416         public final Time.Abs getCurrentEndTime()
417         {
418             return this.currentEndTime;
419         }
420 
421         /** Position of (current) end of trajectory. */
422         private Length.Rel currentEndPosition;
423 
424         /**
425          * Retrieve the current end position of this Trajectory.
426          * @return currentEndPosition
427          */
428         public final Length.Rel getCurrentEndPosition()
429         {
430             return this.currentEndPosition;
431         }
432 
433         /** ID of the GTU. */
434         private final Object id;
435 
436         /**
437          * Retrieve the id of this Trajectory.
438          * @return Object; the id of this Trajectory
439          */
440         public final Object getId()
441         {
442             return this.id;
443         }
444 
445         /** Storage for the position of the car. */
446         private ArrayList<Double> positions = new ArrayList<Double>();
447 
448         /** Time sample of first sample in positions (successive entries will each be one sampleTime later). */
449         private int firstSample;
450 
451         /**
452          * Construct a Trajectory.
453          * @param id Object; Id of the new Trajectory
454          */
455         public Trajectory(final Object id)
456         {
457             this.id = id;
458         }
459 
460         /**
461          * Add a trajectory segment and update the currentEndTime and currentEndPosition.
462          * @param car AbstractLaneBasedGTU; the GTU whose currently committed trajectory segment must be added
463          * @param lane Lane; the Lane that the positionOffset is valid for
464          * @param positionOffset double; offset needed to convert the position in the current Lane to a position on the
465          *            trajectory
466          * @throws NetworkException when car is not on lane anymore
467          * @throws GTUException on problems obtaining data from the GTU
468          */
469         public final void addSegment(final LaneBasedGTU car, final Lane lane, final double positionOffset)
470                 throws NetworkException, GTUException
471         {
472 //            if ("4".equals(car.getId()) && "Lane lane.0 of FirstVia to SecondVia".equals(lane.toString()))
473 //            {
474 //                System.out.println("Enter. positions.size is " + this.positions.size() + ", currentEndPosition is "
475 //                        + this.currentEndPosition);
476 //            }
477             try
478             {
479                 final int startSample =
480                         (int) Math.ceil(car.getOperationalPlan().getStartTime().getSI() / getSampleInterval().getSI());
481                 final int endSample =
482                         (int) (Math.ceil(car.getOperationalPlan().getEndTime().getSI() / getSampleInterval().getSI()));
483                 for (int sample = startSample; sample < endSample; sample++)
484                 {
485                     Time.Abs sampleTime = new Time.Abs(sample * getSampleInterval().getSI(), TimeUnit.SI);
486                     Double position = car.position(lane, car.getReference(), sampleTime).getSI() + positionOffset;
487                     if (this.positions.size() > 0 && null != this.currentEndPosition && position < this.currentEndPosition.getSI() - 0.001)
488                     {
489                         if (0 != positionOffset)
490                         {
491                             // System.out.println("Already added " + car);
492                             break;
493                         }
494                         // System.out.println("inserting null for " + car);
495                         position = null; // Wrapping on circular path?
496                     }
497                     if (this.positions.size() == 0)
498                     {
499                         this.firstSample = sample;
500                     }
501                     /*-
502                     if (sample - this.firstSample > this.positions.size())
503                     {
504                         System.out.println("Inserting " + (sample - this.positions.size()) 
505                                 + " nulls; this is trajectory number " + trajectoryIndices.indexOf(this));
506                     }
507                      */
508                     while (sample - this.firstSample > this.positions.size())
509                     {
510                         // System.out.println("Inserting nulls");
511                         this.positions.add(null); // insert nulls as place holders for unsampled data (usually because
512                                                   // vehicle was temporarily in a parallel Lane)
513                     }
514                     if (null != position && this.positions.size() > sample - this.firstSample)
515                     {
516                         // System.out.println("Skipping sample " + car);
517                         continue;
518                     }
519                     this.positions.add(position);
520                 }
521                 this.currentEndTime = car.getOperationalPlan().getEndTime();
522                 this.currentEndPosition =
523                         new Length.Rel(car.position(lane, car.getReference(), this.currentEndTime).getSI() + positionOffset,
524                                 LengthUnit.SI);
525                 if (car.getOperationalPlan().getEndTime().gt(getMaximumTime()))
526                 {
527                     setMaximumTime(car.getOperationalPlan().getEndTime());
528                 }
529             }
530             catch (Exception e)
531             {
532                 // TODO lane change causes error...
533                 System.err.println("Trajectoryplot caught unexpected Exception: " + e.getMessage());
534                 e.printStackTrace();
535             }
536         }
537 
538         /**
539          * Retrieve the number of samples in this Trajectory.
540          * @return Integer; number of positions in this Trajectory
541          */
542         public int size()
543         {
544             return this.positions.size();
545         }
546 
547         /**
548          * @param item Integer; the sample number
549          * @return Double; the time of the sample indexed by item
550          */
551         public double getTime(final int item)
552         {
553             return (item + this.firstSample) * getSampleInterval().getSI();
554         }
555 
556         /**
557          * @param item Integer; the sample number
558          * @return Double; the position indexed by item
559          */
560         public double getDistance(final int item)
561         {
562             Double distance = this.positions.get(item);
563             if (null == distance)
564             {
565                 return Double.NaN;
566             }
567             return this.positions.get(item);
568         }
569     }
570 
571     /** {@inheritDoc} */
572     @Override
573     public final int getSeriesCount()
574     {
575         return this.trajectories.size();
576     }
577 
578     /** {@inheritDoc} */
579     @Override
580     public final Comparable<Integer> getSeriesKey(final int series)
581     {
582         return series;
583     }
584 
585     /** {@inheritDoc} */
586     @SuppressWarnings("rawtypes")
587     @Override
588     public final int indexOf(final Comparable seriesKey)
589     {
590         if (seriesKey instanceof Integer)
591         {
592             return (Integer) seriesKey;
593         }
594         return -1;
595     }
596 
597     /** {@inheritDoc} */
598     @Override
599     public final void addChangeListener(final DatasetChangeListener listener)
600     {
601         this.listenerList.add(DatasetChangeListener.class, listener);
602     }
603 
604     /** {@inheritDoc} */
605     @Override
606     public final void removeChangeListener(final DatasetChangeListener listener)
607     {
608         this.listenerList.remove(DatasetChangeListener.class, listener);
609     }
610 
611     /** {@inheritDoc} */
612     @Override
613     public final DatasetGroup getGroup()
614     {
615         return this.datasetGroup;
616     }
617 
618     /** {@inheritDoc} */
619     @Override
620     public final void setGroup(final DatasetGroup group)
621     {
622         this.datasetGroup = group;
623     }
624 
625     /** {@inheritDoc} */
626     @Override
627     public final DomainOrder getDomainOrder()
628     {
629         return DomainOrder.ASCENDING;
630     }
631 
632     /** {@inheritDoc} */
633     @Override
634     public final int getItemCount(final int series)
635     {
636         return this.trajectoryIndices.get(series).size();
637     }
638 
639     /** {@inheritDoc} */
640     @Override
641     public final Number getX(final int series, final int item)
642     {
643         double v = getXValue(series, item);
644         if (Double.isNaN(v))
645         {
646             return null;
647         }
648         return v;
649     }
650 
651     /** {@inheritDoc} */
652     @Override
653     public final double getXValue(final int series, final int item)
654     {
655         return this.trajectoryIndices.get(series).getTime(item);
656     }
657 
658     /** {@inheritDoc} */
659     @Override
660     public final Number getY(final int series, final int item)
661     {
662         double v = getYValue(series, item);
663         if (Double.isNaN(v))
664         {
665             return null;
666         }
667         return v;
668     }
669 
670     /** {@inheritDoc} */
671     @Override
672     public final double getYValue(final int series, final int item)
673     {
674         return this.trajectoryIndices.get(series).getDistance(item);
675     }
676 
677     /** {@inheritDoc} */
678     @Override
679     public final JFrame addViewer()
680     {
681         JFrame result = new JFrame(this.caption);
682         result.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
683         JFreeChart newChart = createChart(result);
684         newChart.setTitle((String) null);
685         addChangeListener(newChart.getPlot());
686         return result;
687     }
688 
689 }