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