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<TimeUnit>; the time between samples of this TrajectoryPlot
128 * @param path ArrayList<Lane>; 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 }