package org.opentrafficsim.graphs; import java.awt.BorderLayout; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.Line2D; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPopupMenu; import javax.swing.SwingConstants; import javax.swing.event.EventListenerList; import org.djunits.unit.LengthUnit; import org.djunits.unit.TimeUnit; import org.djunits.value.StorageType; import org.djunits.value.ValueException; import org.djunits.value.vdouble.scalar.DoubleScalar; import org.djunits.value.vdouble.scalar.Length; import org.djunits.value.vdouble.scalar.Time; import org.djunits.value.vdouble.vector.LengthVector; 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.gtu.GTUException; import org.opentrafficsim.core.network.NetworkException; import org.opentrafficsim.road.gtu.lane.LaneBasedGTU; import org.opentrafficsim.road.network.lane.Lane; /** * Trajectory plot. *

* Copyright (c) 2013-2015 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 JFrame implements ActionListener, XYDataset, MultipleViewerChart, LaneBasedGTUSampler { /** */ private static final long serialVersionUID = 20140724L; /** Sample interval of this TrajectoryPlot. */ private final Time.Rel sampleInterval; /** * @return sampleInterval */ public final Time.Rel getSampleInterval() { return this.sampleInterval; } /** The series of Lanes that provide the data for this TrajectoryPlot. */ private final ArrayList path; /** The cumulative lengths of the elements of path. */ private final LengthVector.Rel 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 Length.Rel; the cumulative length at the end of the specified path element */ public final Length.Rel getCumulativeLength(final int index) { int useIndex = -1 == index ? this.cumulativeLengths.size() - 1 : index; try { return new Length.Rel(this.cumulativeLengths.get(useIndex)); } catch (ValueException exception) { exception.printStackTrace(); } return null; // NOTREACHED } /** Maximum of the time axis. */ private Time.Abs maximumTime = new Time.Abs(300, TimeUnit.SECOND); /** * @return maximumTime */ public final Time.Abs getMaximumTime() { return this.maximumTime; } /** * @param maximumTime set maximumTime */ public final void setMaximumTime(final Time.Abs maximumTime) { this.maximumTime = maximumTime; } /** List of parties interested in changes of this ContourPlot. */ private transient EventListenerList listenerList = new EventListenerList(); /** Not used internally. */ private DatasetGroup datasetGroup = null; /** Name of the chart. */ private final String caption; /** * 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 * @param path ArrayList<Lane>; the series of Lanes that will provide the data for this TrajectoryPlot */ public TrajectoryPlot(final String caption, final Time.Rel sampleInterval, final List path) { this.sampleInterval = sampleInterval; this.path = new ArrayList(path); // make a copy double[] endLengths = new double[path.size()]; double cumulativeLength = 0; LengthVector.Rel lengths = null; for (int i = 0; i < path.size(); i++) { Lane lane = path.get(i); lane.addSampler(this); cumulativeLength += lane.getLength().getSI(); endLengths[i] = cumulativeLength; } try { lengths = new LengthVector.Rel(endLengths, LengthUnit.SI, StorageType.DENSE); } catch (ValueException exception) { exception.printStackTrace(); } this.cumulativeLengths = lengths; this.caption = caption; createChart(this); this.reGraph(); // fixes the domain axis } /** * Create the visualization. * @param container JFrame; the JFrame that will be filled with chart and the status label * @return JFreeChart; the visualization */ private 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(this.caption, "", "", 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.Rel minimumPosition = Length.Rel.ZERO; Length.Rel maximumPosition = getCumulativeLength(-1); 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; } /** * Redraw this TrajectoryGraph (after the underlying data has been changed). */ public final void reGraph() { for (DatasetChangeListener dcl : this.listenerList.getListeners(DatasetChangeListener.class)) { if (dcl instanceof XYPlot) { configureAxis(((XYPlot) dcl).getDomainAxis(), this.maximumTime.getSI()); } } notifyListeners(new DatasetChangeEvent(this, null)); // This guess work actually works! } /** * Notify interested parties of an event affecting this TrajectoryPlot. * @param event DatasetChangedEvent */ private void notifyListeners(final DatasetChangeEvent event) { for (DatasetChangeListener dcl : this.listenerList.getListeners(DatasetChangeListener.class)) { dcl.datasetChanged(event); } } /** * 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(); /** {@inheritDoc} */ public final void addData(final LaneBasedGTU car, final Lane lane) throws NetworkException, GTUException { // final Time.Abs startTime = car.getLastEvaluationTime(); // System.out.println("addData car: " + car + ", lastEval: " + startTime); // Convert the position of the car to a position on path. // Find a (the first) lane that car is on that is in our path. double lengthOffset = 0; int index = this.path.indexOf(lane); if (index >= 0) { if (index > 0) { try { lengthOffset = this.cumulativeLengths.getSI(index - 1); } catch (ValueException exception) { exception.printStackTrace(); } } } else { throw new Error("Car is not on any lane in the path"); } // System.out.println("lane index is " + index + " car is " + car); // final Length.Rel startPosition = // DoubleScalar.plus(new Length.Rel(lengthOffset, LengthUnit.SI), // car.position(lane, car.getReference(), startTime)); String key = car.getId().toString(); Trajectory carTrajectory = this.trajectories.get(key); if (null == carTrajectory) { // Create a new Trajectory for this GTU carTrajectory = new Trajectory(key); this.trajectoryIndices.add(carTrajectory); this.trajectories.put(key, carTrajectory); // System.out.println("Creating new trajectory for GTU " + key); } carTrajectory.addSegment(car, lane, lengthOffset); } /** * Store trajectory data. *

* Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. *

* See for project information www.simulation.tudelft.nl. *

* The OpenTrafficSim project is distributed under the following BSD-style license:
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * following conditions are met: *

* This software is provided by the copyright holders and contributors "as is" and any express or implied warranties, * including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are * disclaimed. In no event shall the copyright holder or contributors be liable for any direct, indirect, incidental, * special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; * loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in * contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this * software, even if advised of the possibility of such damage. $LastChangedDate: 2015-07-15 11:18:39 +0200 (Wed, 15 Jul * 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $, initial versionJul 24, 2014
* @author Peter Knoppers */ class Trajectory { /** Time of (current) end of trajectory. */ private Time.Abs currentEndTime; /** * Retrieve the current end time of this Trajectory. * @return currentEndTime */ public final Time.Abs getCurrentEndTime() { return this.currentEndTime; } /** Position of (current) end of trajectory. */ private Length.Rel currentEndPosition; /** * Retrieve the current end position of this Trajectory. * @return currentEndPosition */ public final Length.Rel getCurrentEndPosition() { return this.currentEndPosition; } /** ID of the GTU. */ private final Object id; /** * Retrieve the id of this Trajectory. * @return Object; the id of this Trajectory */ public final Object getId() { return this.id; } /** Storage for the position of the car. */ private ArrayList positions = new ArrayList(); /** Time sample of first sample in positions (successive entries will each be one sampleTime later). */ private int firstSample; /** * Construct a Trajectory. * @param id Object; Id of the new Trajectory */ public Trajectory(final Object id) { this.id = id; } /** * Add a trajectory segment and update the currentEndTime and currentEndPosition. * @param car AbstractLaneBasedGTU; the GTU whose currently committed trajectory segment must be added * @param lane Lane; the Lane that the positionOffset is valid for * @param positionOffset double; offset needed to convert the position in the current Lane to a position on the * trajectory * @throws NetworkException when car is not on lane anymore * @throws GTUException on problems obtaining data from the GTU */ public final void addSegment(final LaneBasedGTU car, final Lane lane, final double positionOffset) throws NetworkException, GTUException { // if ("4".equals(car.getId()) && "Lane lane.0 of FirstVia to SecondVia".equals(lane.toString())) // { // System.out.println("Enter. positions.size is " + this.positions.size() + ", currentEndPosition is " // + this.currentEndPosition); // } try { final int startSample = (int) Math.ceil(car.getOperationalPlan().getStartTime().getSI() / getSampleInterval().getSI()); final int endSample = (int) (Math.ceil(car.getOperationalPlan().getEndTime().getSI() / getSampleInterval().getSI())); for (int sample = startSample; sample < endSample; sample++) { Time.Abs sampleTime = new Time.Abs(sample * getSampleInterval().getSI(), 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; } /*- if (sample - this.firstSample > this.positions.size()) { System.out.println("Inserting " + (sample - this.positions.size()) + " nulls; this is trajectory number " + trajectoryIndices.indexOf(this)); } */ 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.Rel(car.position(lane, car.getReference(), this.currentEndTime).getSI() + positionOffset, LengthUnit.SI); if (car.getOperationalPlan().getEndTime().gt(getMaximumTime())) { setMaximumTime(car.getOperationalPlan().getEndTime()); } } catch (Exception e) { // TODO lane change causes error... System.err.println("Trajectoryplot caught unexpected Exception: " + e.getMessage()); e.printStackTrace(); } } /** * Retrieve the number of samples in this Trajectory. * @return Integer; number of positions in this Trajectory */ public int size() { return this.positions.size(); } /** * @param item Integer; the sample number * @return Double; the time of the sample indexed by item */ public double getTime(final int item) { return (item + this.firstSample) * getSampleInterval().getSI(); } /** * @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 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 void addChangeListener(final DatasetChangeListener listener) { this.listenerList.add(DatasetChangeListener.class, listener); } /** {@inheritDoc} */ @Override public final void removeChangeListener(final DatasetChangeListener listener) { this.listenerList.remove(DatasetChangeListener.class, listener); } /** {@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 JFrame addViewer() { JFrame result = new JFrame(this.caption); result.setDefaultCloseOperation(DISPOSE_ON_CLOSE); JFreeChart newChart = createChart(result); newChart.setTitle((String) null); addChangeListener(newChart.getPlot()); return result; } }