TrajectoryPlot.java
package org.opentrafficsim.graphs;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Paint;
import java.awt.event.ActionEvent;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPopupMenu;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
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.Frequency;
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.gtu.animation.IDGTUColorer;
import org.opentrafficsim.kpi.sampling.KpiGtuDirectionality;
import org.opentrafficsim.kpi.sampling.KpiLaneDirection;
import org.opentrafficsim.kpi.sampling.SamplingException;
import org.opentrafficsim.kpi.sampling.SpaceTimeRegion;
import org.opentrafficsim.kpi.sampling.TrajectoryGroup;
import org.opentrafficsim.road.network.lane.Lane;
import org.opentrafficsim.road.network.sampling.LaneData;
import org.opentrafficsim.road.network.sampling.RoadSampler;
import nl.tudelft.simulation.dsol.simulators.DEVSSimulatorInterface;
/**
* Trajectory plot.
* <p>
* Copyright (c) 2013-2018 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
* BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
* <p>
* $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
* initial version Jul 24, 2014 <br>
* @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
*/
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 DEVSSimulatorInterface.TimeDoubleUnit 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.BASE);
/**
* Retrieve the maximum time.
* @return Time; the maximum time
*/
public final Time getMaximumTime()
{
return this.maximumTime;
}
/**
* Set the maximum time.
* @param maximumTime Time; set the maximum time
*/
public final void setMaximumTime(final Time maximumTime)
{
this.maximumTime = maximumTime;
}
/** Not used internally. */
private DatasetGroup datasetGroup = null;
/** The underlying sampler. */
private RoadSampler roadSampler;
/** The lanes that make up the path. */
private List<KpiLaneDirection> lanes;
/** Mapping from series rank number to trajectory. */
private List<TrajectoryAndLengthOffset> curves = null;
/** Re generate the mapping on the next call to getSeriesCount. */
private boolean shouldGenerateNewCurves = true;
/**
* 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 DEVSSimulatorInterface.TimeDoubleUnit; the simulator
*/
public TrajectoryPlot(final String caption, final Duration sampleInterval, final List<Lane> path,
final DEVSSimulatorInterface.TimeDoubleUnit simulator)
{
super(caption, path);
this.roadSampler = null == sampleInterval ? new RoadSampler(simulator)
: new RoadSampler(simulator, Frequency.createSI(1 / sampleInterval.si));
this.lanes = new ArrayList<>();
for (Lane lane : path)
{
KpiLaneDirection kpiLaneDirection =
new KpiLaneDirection(new LaneData(lane), KpiGtuDirectionality.DIR_PLUS);
SpaceTimeRegion spaceTimeRegion = new SpaceTimeRegion(kpiLaneDirection, Length.ZERO, lane.getLength(),
Time.ZERO, Time.createSI(Double.MAX_VALUE));
this.roadSampler.registerSpaceTimeRegion(spaceTimeRegion);
this.lanes.add(kpiLaneDirection);
}
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);
cumulativeLength += lane.getLength().getSI();
endLengths[i] = cumulativeLength;
}
this.cumulativeLengths = endLengths;
setChart(createChart(this));
this.reGraph(); // fixes the domain axis
}
/**
* Derived from example on stackoverflow.
* http://stackoverflow.com/questions/7283902/setting-different-color-to-particular-row-in-series-jfreechart/7285922#7285922
*/
private class MyRenderer extends XYLineAndShapeRenderer
{
/** */
private static final long serialVersionUID = 20170503L;
/**
* Construct a new MyRenderer.
* @param lines boolean; draw connecting lines
* @param shapes boolean; draw shapes at the points that define the lines
*/
MyRenderer(final boolean lines, final boolean shapes)
{
super(lines, shapes);
}
@Override
public Paint getItemPaint(final int row, final int col)
{
@SuppressWarnings("synthetic-access")
TrajectoryAndLengthOffset tal = getTrajectory(row);
String gtuId = tal.getTrajectory().getGtuId();
int colorIndex = 0;
for (int pos = gtuId.length(); --pos >= 0;)
{
Character c = gtuId.charAt(pos);
if (Character.isDigit(c))
{
colorIndex = c - '0';
break;
}
}
return IDGTUColorer.LEGEND.get(colorIndex).getColor();
}
/** {@inheritDoc} */
@Override
public final String toString()
{
return "MyRenderer []";
}
}
/** {@inheritDoc} */
@Override
public final GraphType getGraphType()
{
return GraphType.TRAJECTORY;
}
/** {@inheritDoc} */
@Override
protected final 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();
MyRenderer renderer = new MyRenderer(false, true);
result.getXYPlot().setRenderer(renderer);
renderer.setDefaultLinesVisible(true);
renderer.setDefaultShapesVisible(false);
renderer.setDefaultShape(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()
{
SwingUtilities.invokeLater(new Runnable()
{
@SuppressWarnings({ "synthetic-access", "unqualified-field-access" })
@Override
public void run()
{
for (DatasetChangeListener dcl : getListenerList().getListeners(DatasetChangeListener.class))
{
if (dcl instanceof XYPlot)
{
Time simulatorTime = simulator.getSimulatorTime();
if (getMaximumTime().lt(simulatorTime))
{
setMaximumTime(simulatorTime);
}
configureAxis(((XYPlot) dcl).getDomainAxis(), maximumTime.getSI());
}
}
shouldGenerateNewCurves = true;
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
*/
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);
// System.out.println("centerRange is " + (range / 2));
}
/** {@inheritDoc} */
@Override
public void actionPerformed(final ActionEvent e)
{
// not yet
}
/** {@inheritDoc} */
@Override
public final int getSeriesCount()
{
if (null == this.curves || this.shouldGenerateNewCurves)
{
List<TrajectoryAndLengthOffset> newCurves = new ArrayList<>();
double cumulativeLength = 0;
for (KpiLaneDirection kld : this.lanes)
{
TrajectoryGroup tg = this.roadSampler.getTrajectoryGroup(kld);
if (null == tg)
{
continue;
}
for (org.opentrafficsim.kpi.sampling.Trajectory trajectory : tg.getTrajectories())
{
newCurves.add(new TrajectoryAndLengthOffset(trajectory, cumulativeLength));
}
cumulativeLength += kld.getLaneData().getLength().si;
}
this.curves = newCurves;
this.shouldGenerateNewCurves = false;
}
return this.curves.size();
}
/** {@inheritDoc} */
@Override
public final Comparable<Integer> 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;
}
/**
* Storage for a trajectory and a length.
*/
class TrajectoryAndLengthOffset
{
/** The trajectory. */
private final org.opentrafficsim.kpi.sampling.Trajectory trajectory;
/** The length. */
private final double lengthOffset;
/**
* Construct a new TrajectoryAndLengthOffset object.
* @param trajectory org.opentrafficsim.kpi.sampling.Trajectory; the trajectory
* @param lengthOffset double; the length from the beginning of the sampled path to the start of the lane to which the
* trajectory belongs
*/
TrajectoryAndLengthOffset(final org.opentrafficsim.kpi.sampling.Trajectory trajectory,
final double lengthOffset)
{
this.trajectory = trajectory;
this.lengthOffset = lengthOffset;
}
/**
* Retrieve the trajectory.
* @return org.opentrafficsim.kpi.sampling.Trajectory; the trajectory
*/
public org.opentrafficsim.kpi.sampling.Trajectory getTrajectory()
{
return this.trajectory;
}
/**
* Retrieve the lengthOffset.
* @return double; the lengthOffset
*/
public double getLengthOffset()
{
return this.lengthOffset;
}
/** {@inheritDoc} */
@Override
public final String toString()
{
return "TrajectoryAndLengthOffset [trajectory=" + this.trajectory + ", lengthOffset=" + this.lengthOffset + "]";
}
}
/**
* Retrieve the Nth trajectory.
* @param index int; the index of the requested trajectory
* @return org.opentrafficsim.kpi.sampling.Trajectory; the Nth trajectory, or null if the provided index is out of range
*/
private TrajectoryAndLengthOffset getTrajectory(final int index)
{
if (index < 0)
{
System.err.println("Negative index (" + index + ")");
return null;
}
while (null == this.curves)
{
getSeriesCount();
}
if (index >= this.curves.size())
{
System.err.println("index out of range (" + index + " >= " + this.curves.size() + ")");
return null;
}
return this.curves.get(index);
}
/** {@inheritDoc} */
@Override
public final int getItemCount(final int series)
{
return getTrajectory(series).getTrajectory().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)
{
TrajectoryAndLengthOffset tal = getTrajectory(series);
try
{
return tal.getTrajectory().getT(item);
}
catch (SamplingException exception)
{
exception.printStackTrace();
System.out.println("index out of bounds: item=" + item + ", limit=" + tal.getTrajectory().size());
return Double.NaN;
}
}
/** {@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)
{
TrajectoryAndLengthOffset tal = getTrajectory(series);
try
{
return tal.getTrajectory().getX(item) + tal.getLengthOffset();
}
catch (SamplingException exception)
{
exception.printStackTrace();
System.out.println("index out of bounds: item=" + item + ", limit=" + tal.getTrajectory().size());
return Double.NaN;
}
}
/** {@inheritDoc} */
@Override
public final String toString()
{
return "TrajectoryPlot [sampleInterval=" + this.sampleInterval + ", path=" + getPath() + ", cumulativeLengths.length="
+ this.cumulativeLengths.length + ", maximumTime=" + this.maximumTime + ", caption=" + getCaption() + "]";
}
}