package org.opentrafficsim.graphs; import java.awt.BorderLayout; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.geom.Line2D; import java.io.Serializable; import java.rmi.RemoteException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPopupMenu; import javax.swing.SwingConstants; import org.djunits.unit.LengthUnit; import org.djunits.unit.TimeUnit; import org.djunits.value.vdouble.scalar.DoubleScalar; import org.djunits.value.vdouble.scalar.Duration; import org.djunits.value.vdouble.scalar.Length; import org.djunits.value.vdouble.scalar.Time; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.StandardChartTheme; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.data.DomainOrder; import org.jfree.data.general.DatasetChangeEvent; import org.jfree.data.general.DatasetChangeListener; import org.jfree.data.general.DatasetGroup; import org.jfree.data.xy.XYDataset; import org.opentrafficsim.core.dsol.OTSDEVSSimulatorInterface; import org.opentrafficsim.core.dsol.OTSSimTimeDouble; import org.opentrafficsim.core.gtu.GTUException; import org.opentrafficsim.core.network.NetworkException; import org.opentrafficsim.road.gtu.lane.LaneBasedGTU; import org.opentrafficsim.road.network.lane.Lane; import nl.tudelft.simulation.dsol.SimRuntimeException; import nl.tudelft.simulation.event.EventInterface; import nl.tudelft.simulation.event.EventListenerInterface; import nl.tudelft.simulation.event.TimedEvent; /** * Trajectory plot. *

* Copyright (c) 2013-2016 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See OpenTrafficSim License. *

* $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $, * initial version Jul 24, 2014
* @author Peter Knoppers */ public class TrajectoryPlot extends AbstractOTSPlot implements XYDataset, LaneBasedGTUSampler, EventListenerInterface { /** */ private static final long serialVersionUID = 20140724L; /** Sample interval of this TrajectoryPlot. */ private final Duration sampleInterval; /** The simulator. */ private final OTSDEVSSimulatorInterface simulator; /** * @return sampleInterval if this TrajectoryPlot samples at a fixed rate, or null if this TrajectoryPlot samples on the GTU * move events */ public final Duration getSampleInterval() { return this.sampleInterval; } /** The cumulative lengths of the elements of path. */ private final double[] cumulativeLengths; /** * Retrieve the cumulative length of the sampled path at the end of a path element. * @param index int; the index of the path element; if -1, the total length of the path is returned * @return double; the cumulative length at the end of the specified path element in meters (si) */ public final double getCumulativeLength(final int index) { return index == -1 ? this.cumulativeLengths[this.cumulativeLengths.length - 1] : this.cumulativeLengths[index]; } /** Maximum of the time axis. */ private Time maximumTime = new Time(300, TimeUnit.SECOND); /** * @return maximumTime */ public final Time getMaximumTime() { return this.maximumTime; } /** * @param maximumTime set maximumTime */ public final void setMaximumTime(final Time maximumTime) { this.maximumTime = maximumTime; } /** Not used internally. */ private DatasetGroup datasetGroup = null; /** * Create a new TrajectoryPlot. * @param caption String; the text to show above the TrajectoryPlot * @param sampleInterval DoubleScalarRel<TimeUnit>; the time between samples of this TrajectoryPlot, or null in which * case the GTUs are sampled whenever they fire a MOVE_EVENT * @param path ArrayList<Lane>; the series of Lanes that will provide the data for this TrajectoryPlot * @param simulator OTSDEVSSimulatorInterface; the simulator */ public TrajectoryPlot(final String caption, final Duration sampleInterval, final List path, final OTSDEVSSimulatorInterface simulator) { super(caption, path); this.sampleInterval = sampleInterval; this.simulator = simulator; double[] endLengths = new double[path.size()]; double cumulativeLength = 0; for (int i = 0; i < path.size(); i++) { Lane lane = path.get(i); lane.addListener(this, Lane.GTU_ADD_EVENT, true); lane.addListener(this, Lane.GTU_REMOVE_EVENT, true); try { // Register the GTUs currently (i.e. already) on the lane (if any) for statistics sampling. for (LaneBasedGTU gtu : lane.getGtuList()) { notify(new TimedEvent(Lane.GTU_ADD_EVENT, lane, new Object[] { gtu.getId(), gtu }, gtu.getSimulator().getSimulatorTime())); } } catch (RemoteException exception) { exception.printStackTrace(); } cumulativeLength += lane.getLength().getSI(); endLengths[i] = cumulativeLength; } this.cumulativeLengths = endLengths; setChart(createChart(this)); this.reGraph(); // fixes the domain axis if (null != this.sampleInterval) { try { this.simulator.scheduleEventRel(Duration.ZERO, this, this, "sample", null); } catch (SimRuntimeException exception) { exception.printStackTrace(); } } } /** {@inheritDoc} */ @Override public final GraphType getGraphType() { return GraphType.TRAJECTORY; } /** * Sample all the GTUs on the observed lanes. */ public void sample() { Time now = this.simulator.getSimulatorTime().getTime(); for (LaneBasedGTU gtu : this.gtusOfInterest) { try { Map positions = gtu.positions(gtu.getReference(), now); int hits = 0; for (Lane lane : positions.keySet()) { if (getPath().contains(lane)) { Length position = positions.get(lane); if (position.si >= 0 && position.si <= lane.getLength().si) { addData(gtu, lane, positions.get(lane).si); hits++; } } } if (1 != hits) { System.err.println("GTU " + gtu + " scored " + hits + " (expected 1 hit)"); } } catch (GTUException exception) { exception.printStackTrace(); } } // Schedule the next sample try { this.simulator.scheduleEventRel(this.sampleInterval, this, this, "sample", null); } catch (SimRuntimeException exception) { exception.printStackTrace(); } } /** The GTUs that might be of interest to gather statistics about. */ private Set gtusOfInterest = new HashSet<>(); /** {@inheritDoc} */ @Override @SuppressWarnings("checkstyle:designforextension") public void notify(final EventInterface event) throws RemoteException { LaneBasedGTU gtu; if (event.getType().equals(Lane.GTU_ADD_EVENT)) { Object[] content = (Object[]) event.getContent(); gtu = (LaneBasedGTU) content[1]; if (!this.gtusOfInterest.contains(gtu)) { this.gtusOfInterest.add(gtu); if (null == this.sampleInterval) { gtu.addListener(this, LaneBasedGTU.LANEBASED_MOVE_EVENT); } } } else if (event.getType().equals(Lane.GTU_REMOVE_EVENT)) { Object[] content = (Object[]) event.getContent(); gtu = (LaneBasedGTU) content[1]; Lane lane = null; try { lane = gtu.getReferencePosition().getLane(); } catch (GTUException exception) { // ignore - lane will be null } if (lane == null || !getPath().contains(lane)) { this.gtusOfInterest.remove(gtu); if (null != this.sampleInterval) { gtu.removeListener(this, LaneBasedGTU.LANEBASED_MOVE_EVENT); } else { String key = gtu.getId(); VariableSampleRateTrajectory carTrajectory = (VariableSampleRateTrajectory) this.trajectories.get(key); if (null != carTrajectory) { carTrajectory.recordGTULeftTrajectoryEvent(); } } } } else if (event.getType().equals(LaneBasedGTU.LANEBASED_MOVE_EVENT)) { Object[] content = (Object[]) event.getContent(); Lane lane = (Lane) content[6]; Length posOnLane = (Length) content[7]; gtu = (LaneBasedGTU) event.getSource(); if (getPath().contains(lane)) { addData(gtu, lane, posOnLane.si); } } } /** {@inheritDoc} */ @Override protected JFreeChart createChart(final JFrame container) { final JLabel statusLabel = new JLabel(" ", SwingConstants.CENTER); container.add(statusLabel, BorderLayout.SOUTH); ChartFactory.setChartTheme(new StandardChartTheme("JFree/Shadow", false)); final JFreeChart result = ChartFactory.createXYLineChart(getCaption(), "", "", this, PlotOrientation.VERTICAL, false, false, false); // Overrule the default background paint because some of the lines are invisible on top of this default. result.getPlot().setBackgroundPaint(new Color(0.9f, 0.9f, 0.9f)); FixCaption.fixCaption(result); NumberAxis xAxis = new NumberAxis("\u2192 " + "time [s]"); xAxis.setLowerMargin(0.0); xAxis.setUpperMargin(0.0); NumberAxis yAxis = new NumberAxis("\u2192 " + "Distance [m]"); yAxis.setAutoRangeIncludesZero(false); yAxis.setLowerMargin(0.0); yAxis.setUpperMargin(0.0); yAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); result.getXYPlot().setDomainAxis(xAxis); result.getXYPlot().setRangeAxis(yAxis); Length minimumPosition = Length.ZERO; Length maximumPosition = new Length(getCumulativeLength(-1), LengthUnit.SI); configureAxis(result.getXYPlot().getRangeAxis(), DoubleScalar.minus(maximumPosition, minimumPosition).getSI()); final XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) result.getXYPlot().getRenderer(); renderer.setBaseLinesVisible(true); renderer.setBaseShapesVisible(false); renderer.setBaseShape(new Line2D.Float(0, 0, 0, 0)); final ChartPanel cp = new ChartPanel(result); cp.setMouseWheelEnabled(true); final PointerHandler ph = new PointerHandler() { /** {@inheritDoc} */ @Override void updateHint(final double domainValue, final double rangeValue) { if (Double.isNaN(domainValue)) { statusLabel.setText(" "); return; } String value = ""; /*- XYDataset dataset = plot.getDataset(); double bestDistance = Double.MAX_VALUE; Trajectory bestTrajectory = null; final int mousePrecision = 5; java.awt.geom.Point2D.Double mousePoint = new java.awt.geom.Point2D.Double(t, distance); double lowTime = plot.getDomainAxis().java2DToValue(p.getX() - mousePrecision, pi.getDataArea(), plot.getDomainAxisEdge()) - 1; double highTime = plot.getDomainAxis().java2DToValue(p.getX() + mousePrecision, pi.getDataArea(), plot.getDomainAxisEdge()) + 1; double lowDistance = plot.getRangeAxis().java2DToValue(p.getY() + mousePrecision, pi.getDataArea(), plot.getRangeAxisEdge()) - 20; double highDistance = plot.getRangeAxis().java2DToValue(p.getY() - mousePrecision, pi.getDataArea(), plot.getRangeAxisEdge()) + 20; // System.out.println(String.format("Searching area t[%.1f-%.1f], x[%.1f,%.1f]", lowTime, highTime, // lowDistance, highDistance)); for (Trajectory trajectory : this.trajectories) { java.awt.geom.Point2D.Double[] clippedTrajectory = trajectory.clipTrajectory(lowTime, highTime, lowDistance, highDistance); if (null == clippedTrajectory) continue; java.awt.geom.Point2D.Double prevPoint = null; for (java.awt.geom.Point2D.Double trajectoryPoint : clippedTrajectory) { if (null != prevPoint) { double thisDistance = Planar.distancePolygonToPoint(clippedTrajectory, mousePoint); if (thisDistance < bestDistance) { bestDistance = thisDistance; bestTrajectory = trajectory; } } prevPoint = trajectoryPoint; } } if (null != bestTrajectory) { for (SimulatedObject so : indices.keySet()) if (this.trajectories.get(indices.get(so)) == bestTrajectory) { Point2D.Double bestPosition = bestTrajectory.getEstimatedPosition(t); if (null == bestPosition) continue; value = String.format( Main.locale, ": vehicle %s; location on measurement path at t=%.1fs: " + "longitudinal %.1fm, lateral %.1fm", so.toString(), t, bestPosition.x, bestPosition.y); } } else value = ""; */ statusLabel.setText(String.format("t=%.0fs, distance=%.0fm%s", domainValue, rangeValue, value)); } }; cp.addMouseMotionListener(ph); cp.addMouseListener(ph); container.add(cp, BorderLayout.CENTER); // TODO ensure that shapes for all the data points don't get allocated. // Currently JFreeChart allocates many megabytes of memory for Ellipses that are never drawn. JPopupMenu popupMenu = cp.getPopupMenu(); popupMenu.add(new JPopupMenu.Separator()); popupMenu.add(StandAloneChartWindow.createMenuItem(this)); return result; } /** {@inheritDoc} */ @Override public final void reGraph() { for (DatasetChangeListener dcl : getListenerList().getListeners(DatasetChangeListener.class)) { if (dcl instanceof XYPlot) { configureAxis(((XYPlot) dcl).getDomainAxis(), this.maximumTime.getSI()); } } notifyListeners(new DatasetChangeEvent(this, null)); // This guess work actually works! } /** * Configure the range of an axis. * @param valueAxis ValueAxis * @param range double; the upper bound of the axis */ private static void configureAxis(final ValueAxis valueAxis, final double range) { valueAxis.setUpperBound(range); valueAxis.setLowerMargin(0); valueAxis.setUpperMargin(0); valueAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); valueAxis.setAutoRange(true); valueAxis.setAutoRangeMinimumSize(range); valueAxis.centerRange(range / 2); } /** {@inheritDoc} */ @Override public void actionPerformed(final ActionEvent e) { // not yet } /** All stored trajectories. */ private HashMap trajectories = new HashMap(); /** Quick access to the Nth trajectory. */ private ArrayList trajectoryIndices = new ArrayList(); /** * Add data for a GTU on a lane to this graph. * @param gtu the gtu to add the data for * @param lane the lane on which the GTU is registered * @param posOnLane the position on the lane as a double si Length */ protected final void addData(final LaneBasedGTU gtu, final Lane lane, final double posOnLane) { int index = getPath().indexOf(lane); if (index < 0) { // error -- silently ignore for now. Graphs should not cause errors. System.err.println("TrajectoryPlot: GTU " + gtu.getId() + " is not registered on lane " + lane.toString()); return; } double lengthOffset = index == 0 ? 0 : this.cumulativeLengths[index - 1]; String key = gtu.getId(); Trajectory carTrajectory = this.trajectories.get(key); if (null == carTrajectory) { // Create a new Trajectory for this GTU carTrajectory = null == this.sampleInterval ? new VariableSampleRateTrajectory(key) : new FixedSampleRateTrajectory(key); this.trajectoryIndices.add(carTrajectory); this.trajectories.put(key, carTrajectory); } try { carTrajectory.addSample(gtu, lane, lengthOffset + posOnLane); } catch (NetworkException | GTUException exception) { // error -- silently ignore for now. Graphs should not cause errors. System.err.println("TrajectoryPlot: GTU " + gtu.getId() + " on lane " + lane.toString() + " caused exception " + exception.getMessage()); } } /** * Common interface for both (all?) types of trajectories. */ interface Trajectory { /** * Retrieve the time of the last stored event. * @return Time; the time of the last stored event */ Time getCurrentEndTime(); /** * Retrieve the last recorded non-null position, or null if no non-null positions have been recorded yet. * @return Double; the last recorded position of this Trajectory in meters */ Double getLastPosition(); /** * Retrieve the id of this Trajectory. * @return Object; the id of this Trajectory */ String getId(); /** * Add a trajectory segment sample and update the currentEndTime and currentEndPosition. * @param gtu AbstractLaneBasedGTU; the GTU whose currently committed trajectory segment must be added * @param lane Lane; the Lane that the positionOffset is valid for * @param position Double; distance in meters from the start of the trajectory * @throws NetworkException when car is not on lane anymore * @throws GTUException on problems obtaining data from the GTU */ void addSample(LaneBasedGTU gtu, Lane lane, double position) throws NetworkException, GTUException; /** * Retrieve the number of stored samples in this Trajectory. * @return int; number of stored samples */ int size(); /** * Return the time of the Nth stored sample. * @param item int; the index of the sample * @return double; the time of the sample */ double getTime(int item); /** * Return the distance of the Nth stored sample. * @param item int; the index of the sample * @return double; the distance of the sample */ double getDistance(int item); } /** * Store trajectory data for use with a variable sample rate. *

* @author Peter Knoppers */ class VariableSampleRateTrajectory implements Trajectory, Serializable { /** */ private static final long serialVersionUID = 20140000L; /** Time of (current) end of trajectory. */ private Time currentEndTime; /** ID of the GTU. */ private final String id; /** Storage for the samples of the GTU. */ private ArrayList samples = new ArrayList(); /** * Construct a new VariableSamplerateTrajectory. * @param id String; id of the new Trajectory (id of the GTU) */ public VariableSampleRateTrajectory(final String id) { this.id = id; } /** {@inheritDoc} */ @Override public Time getCurrentEndTime() { return this.currentEndTime; } /** {@inheritDoc} */ @Override public Double getLastPosition() { return null; } /** {@inheritDoc} */ @Override public String getId() { return this.id; } /** {@inheritDoc} */ @Override public void addSample(LaneBasedGTU gtu, Lane lane, double position) throws NetworkException, GTUException { if (this.samples.size() > 0) { DistanceAndTime lastSample = this.samples.get(this.samples.size() - 1); if (null != lastSample) { Double lastPosition = lastSample.getDistance(); if (null != lastPosition && Math.abs(lastPosition - position) > 0.9 * getCumulativeLength(-1)) { // wrap around... probably circular lane, insert a GTU left trajectory event. recordGTULeftTrajectoryEvent(); } } } this.currentEndTime = gtu.getSimulator().getSimulatorTime().getTime(); this.samples.add(new DistanceAndTime(position, this.currentEndTime.si)); } /** * Store that the GTU went off of the trajectory. */ public void recordGTULeftTrajectoryEvent() { this.samples.add(null); } /** {@inheritDoc} */ @Override public int size() { return this.samples.size(); } /** * Retrieve the Nth sample. * @param item int; the number of the sample * @return DistanceAndTime; the Nth sample (samples can be null to indicate that GTU went off the trajectory). */ private DistanceAndTime getSample(int item) { return this.samples.get(item); } /** {@inheritDoc} */ @Override public double getTime(int item) { DistanceAndTime sample = getSample(item); if (null == sample) { return Double.NaN; } return this.samples.get(item).getTime(); } /** {@inheritDoc} */ @Override public double getDistance(int item) { DistanceAndTime sample = getSample(item); if (null == sample) { return Double.NaN; } return sample.getDistance(); } /** {@inheritDoc} */ @Override public String toString() { return "VariableSampleRateTrajectory [id=" + this.id + ", currentEndTime=" + this.currentEndTime + "]"; } /** * Store a position and a time. */ class DistanceAndTime { /** The position [m]. */ final double distance; /** The time [s]. */ final double time; /** * Construct a new DistanceAndTime object. * @param distance double; the position * @param time double; the time */ public DistanceAndTime(final double distance, final double time) { this.distance = distance; this.time = time; } /** * Retrieve the position. * @return double; the position */ public double getDistance() { return this.distance; } /** * Retrieve the time. * @return double; the time */ public double getTime() { return this.time; } /** {@inheritDoc} */ @Override public String toString() { return "DistanceAndTime [distance=" + this.distance + ", time=" + this.time + "]"; } } } /** * Store trajectory data for use with a fixed sample rate. *

* @author Peter Knoppers */ class FixedSampleRateTrajectory implements Trajectory, Serializable { /** */ private static final long serialVersionUID = 20140000L; /** Time of (current) end of trajectory. */ private Time currentEndTime; /** ID of the GTU. */ private final String id; /** Storage for the position of the GTU. */ private ArrayList positions = new ArrayList(); /** Sample number of sample with index 0 in positions (following entries will each be one sampleTime later). */ private int firstSample; /** * Construct a FixedSampleRateTrajectory. * @param id String; id of the new Trajectory (id of the GTU) */ FixedSampleRateTrajectory(final String id) { this.id = id; } /** {@inheritDoc} */ public final Time getCurrentEndTime() { return this.currentEndTime; } /** {@inheritDoc} */ public final Double getLastPosition() { for (int i = this.positions.size(); --i >= 0;) { Double result = this.positions.get(i); if (null != result) { return result; } } return null; } /** {@inheritDoc} */ public final String getId() { return this.id; } /** {@inheritDoc} */ public final void addSample(final LaneBasedGTU gtu, final Lane lane, final double position) throws NetworkException, GTUException { final int sample = (int) Math.ceil(gtu.getOperationalPlan().getStartTime().si / getSampleInterval().si); if (0 == this.positions.size()) { this.firstSample = sample; } while (sample - this.firstSample > this.positions.size()) { // insert nulls as place holders for unsampled data (usually because vehicle was in a parallel Lane) this.positions.add(null); } Double adjustedPosition = position; Double lastPosition = this.positions.size() > 0 ? this.positions.get(this.positions.size() - 1) : null; if (null != lastPosition && Math.abs(lastPosition - position) > 0.9 * getCumulativeLength(-1)) { // wrap around... probably circular lane. adjustedPosition = null; } this.positions.add(adjustedPosition); this.currentEndTime = gtu.getSimulator().getSimulatorTime().getTime(); /*- try { final int startSample = (int) Math.ceil(car.getOperationalPlan().getStartTime().getSI() / getSampleInterval()); final int endSample = (int) (Math.ceil(car.getOperationalPlan().getEndTime().getSI() / getSampleInterval())); for (int sample = startSample; sample < endSample; sample++) { Time sampleTime = new Time(sample * getSampleInterval(), TimeUnit.SI); Double position = car.position(lane, car.getReference(), sampleTime).getSI() + positionOffset; if (this.positions.size() > 0 && null != this.currentEndPosition && position < this.currentEndPosition.getSI() - 0.001) { if (0 != positionOffset) { // System.out.println("Already added " + car); break; } // System.out.println("inserting null for " + car); position = null; // Wrapping on circular path? } if (this.positions.size() == 0) { this.firstSample = sample; } while (sample - this.firstSample > this.positions.size()) { // System.out.println("Inserting nulls"); this.positions.add(null); // insert nulls as place holders for unsampled data (usually because // vehicle was temporarily in a parallel Lane) } if (null != position && this.positions.size() > sample - this.firstSample) { // System.out.println("Skipping sample " + car); continue; } this.positions.add(position); } this.currentEndTime = car.getOperationalPlan().getEndTime(); this.currentEndPosition = new Length( car.position(lane, car.getReference(), this.currentEndTime).getSI() + positionOffset, LengthUnit.SI); } catch (Exception e) { // TODO lane change causes error... System.err.println("Trajectoryplot caught unexpected Exception: " + e.getMessage()); e.printStackTrace(); } */ if (gtu.getSimulator().getSimulatorTime().getTime().gt(getMaximumTime())) { setMaximumTime(gtu.getSimulator().getSimulatorTime().getTime()); } } /** {@inheritDoc} */ public int size() { return this.positions.size(); } /** {@inheritDoc} */ public double getTime(final int item) { return (item + this.firstSample) * getSampleInterval().si; } /** * @param item Integer; the sample number * @return Double; the position indexed by item */ public double getDistance(final int item) { Double distance = this.positions.get(item); if (null == distance) { return Double.NaN; } return this.positions.get(item); } /** {@inheritDoc} */ @Override public final String toString() { return "FixedSampleRateTrajectory [currentEndTime=" + this.currentEndTime + ", id=" + this.id + ", positions.size=" + this.positions.size() + ", firstSample=" + this.firstSample + "]"; } } /** {@inheritDoc} */ @Override public final int getSeriesCount() { return this.trajectories.size(); } /** {@inheritDoc} */ @Override public final Comparable getSeriesKey(final int series) { return series; } /** {@inheritDoc} */ @SuppressWarnings("rawtypes") @Override public final int indexOf(final Comparable seriesKey) { if (seriesKey instanceof Integer) { return (Integer) seriesKey; } return -1; } /** {@inheritDoc} */ @Override public final DatasetGroup getGroup() { return this.datasetGroup; } /** {@inheritDoc} */ @Override public final void setGroup(final DatasetGroup group) { this.datasetGroup = group; } /** {@inheritDoc} */ @Override public final DomainOrder getDomainOrder() { return DomainOrder.ASCENDING; } /** {@inheritDoc} */ @Override public final int getItemCount(final int series) { return this.trajectoryIndices.get(series).size(); } /** {@inheritDoc} */ @Override public final Number getX(final int series, final int item) { double v = getXValue(series, item); if (Double.isNaN(v)) { return null; } return v; } /** {@inheritDoc} */ @Override public final double getXValue(final int series, final int item) { return this.trajectoryIndices.get(series).getTime(item); } /** {@inheritDoc} */ @Override public final Number getY(final int series, final int item) { double v = getYValue(series, item); if (Double.isNaN(v)) { return null; } return v; } /** {@inheritDoc} */ @Override public final double getYValue(final int series, final int item) { return this.trajectoryIndices.get(series).getDistance(item); } /** {@inheritDoc} */ @Override public final String toString() { return "TrajectoryPlot [sampleInterval=" + this.sampleInterval + ", path=" + getPath() + ", cumulativeLengths.length=" + this.cumulativeLengths.length + ", maximumTime=" + this.maximumTime + ", caption=" + getCaption() + ", trajectories.size=" + this.trajectories.size() + "]"; } }