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