package org.opentrafficsim.graphs;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.swing.ButtonGroup;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JPopupMenu;
import javax.swing.JRadioButtonMenuItem;
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.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.LegendItem;
import org.jfree.chart.LegendItemCollection;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.event.PlotChangeEvent;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYBlockRenderer;
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.XYZDataset;
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 org.opentrafficsim.simulationengine.OTSSimulationException;
/**
* Common code for a contour plot.
* The data collection code for acceleration assumes constant acceleration during the evaluation period of the GTU.
*
* 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 16, 2014
* @author Peter Knoppers
*/
public abstract class ContourPlot extends JFrame implements ActionListener, XYZDataset, MultipleViewerChart,
LaneBasedGTUSampler
{
/** */
private static final long serialVersionUID = 20140716L;
/** Caption of the graph. */
private final String caption;
/** Color scale for the graph. */
private final ContinuousColorPaintScale paintScale;
/** Definition of the X-axis. */
@SuppressWarnings("visibilitymodifier")
protected final Axis xAxis;
/** Definition of the Y-axis. */
@SuppressWarnings("visibilitymodifier")
protected final Axis yAxis;
/** Difference of successive values in the legend. */
private final double legendStep;
/** Format string used to create the captions in the legend. */
private final String legendFormat;
/** Time granularity values. */
protected static final double[] STANDARDTIMEGRANULARITIES = { 1, 2, 5, 10, 20, 30, 60, 120, 300, 600 };
/** Index of the initial time granularity in standardTimeGranularites. */
protected static final int STANDARDINITIALTIMEGRANULARITYINDEX = 3;
/** Distance granularity values. */
protected static final double[] STANDARDDISTANCEGRANULARITIES = { 10, 20, 50, 100, 200, 500, 1000 };
/** Index of the initial distance granularity in standardTimeGranularites. */
protected static final int STANDARDINITIALDISTANCEGRANULARITYINDEX = 3;
/** Initial lower bound for the time scale. */
protected static final Time.Abs INITIALLOWERTIMEBOUND = new Time.Abs(0, TimeUnit.SECOND);
/** Initial upper bound for the time scale. */
protected static final Time.Abs INITIALUPPERTIMEBOUND = new Time.Abs(300, TimeUnit.SECOND);
/** The series of Lanes that provide the data for this TrajectoryPlot. */
private final ArrayList
* Do not rely on the (current) fact that the data is stored column by column!
* @param item Integer; the item
* @return Integer; the bin number along the y axis of the item
*/
protected final int yAxisBin(final int item)
{
int maxItem = getItemCount(0);
if (item < 0 || item >= maxItem)
{
throw new RuntimeException("yAxisBin: item out of range (value is " + item + "), valid range is 0.." + maxItem);
}
return item % yAxisBins();
}
/**
* Return the x-axis bin number (the column number) of an item.
* Do not rely on the (current) fact that the data is stored column by column!
* @param item Integer; the item
* @return Integer; the bin number along the x axis of the item
*/
protected final int xAxisBin(final int item)
{
int maxItem = getItemCount(0);
if (item < 0 || item >= maxItem)
{
throw new RuntimeException("xAxisBin: item out of range (value is " + item + "), valid range is 0.." + maxItem);
}
return item / yAxisBins();
}
/** Cached result of xAxisBins. */
private int cachedXAxisBins = -1;
/**
* Retrieve the number of cells to use along the time axis.
* @return Integer; the number of cells to use along the time axis
*/
protected final int xAxisBins()
{
if (this.cachedXAxisBins >= 0)
{
return this.cachedXAxisBins;
}
this.cachedXAxisBins = this.getXAxis().getAggregatedBinCount();
return this.cachedXAxisBins;
}
/** Cached result of getItemCount. */
private int cachedItemCount = -1;
/** {@inheritDoc} */
@Override
public final int getItemCount(final int series)
{
if (this.cachedItemCount >= 0)
{
return this.cachedItemCount;
}
this.cachedItemCount = yAxisBins() * xAxisBins();
return this.cachedItemCount;
}
/** {@inheritDoc} */
@Override
public final Number getX(final int series, final int item)
{
return getXValue(series, item);
}
/** {@inheritDoc} */
@Override
public final double getXValue(final int series, final int item)
{
double result = this.getXAxis().getValue(xAxisBin(item));
// System.out.println(String.format("XValue(%d, %d) -> %.3f, binCount=%d", series, item, result,
// this.yAxisDefinition.getAggregatedBinCount()));
return result;
}
/** {@inheritDoc} */
@Override
public final Number getY(final int series, final int item)
{
return getYValue(series, item);
}
/** {@inheritDoc} */
@Override
public final double getYValue(final int series, final int item)
{
return this.getYAxis().getValue(yAxisBin(item));
}
/** {@inheritDoc} */
@Override
public final Number getZ(final int series, final int item)
{
return getZValue(series, item);
}
/** {@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 null;
}
/** {@inheritDoc} */
@Override
public void setGroup(final DatasetGroup group)
{
// ignore
}
/** {@inheritDoc} */
@SuppressWarnings("rawtypes")
@Override
public final int indexOf(final Comparable seriesKey)
{
return 0;
}
/** {@inheritDoc} */
@Override
public final DomainOrder getDomainOrder()
{
return DomainOrder.ASCENDING;
}
/**
* Make sure that the results of the most called methods are re-calculated.
*/
private void clearCachedValues()
{
this.cachedItemCount = -1;
this.cachedXAxisBins = -1;
this.cachedYAxisBins = -1;
}
/** {@inheritDoc} */
@Override
public final void addData(final LaneBasedGTU car, final Lane lane) throws NetworkException, GTUException
{
// System.out.println("addData car: " + car + ", lastEval: " + car.getSimulator().getSimulatorTime()
// + " position of rear on lane " + lane + " is " + car.position(lane, car.getRear()));
// Convert the position of the car to a position on 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 RuntimeException("Cannot happen: Lane is not in the path");
}
final Time.Abs fromTime = car.getOperationalPlan().getStartTime();
if (car.position(lane, car.getRear(), fromTime).getSI() < 0 && lengthOffset > 0)
{
return;
}
final Time.Abs toTime = car.getOperationalPlan().getEndTime();
if (toTime.getSI() > this.getXAxis().getMaximumValue().getSI())
{
extendXRange(toTime);
clearCachedValues();
this.getXAxis().adjustMaximumValue(toTime);
}
if (toTime.le(fromTime)) // degenerate sample???
{
return;
}
// The "relative" values are "counting" distance or time in the minimum bin size unit
final double relativeFromDistance =
(car.position(lane, car.getRear(), fromTime).getSI() + lengthOffset) / this.getYAxis().getGranularities()[0];
final double relativeToDistance =
(car.position(lane, car.getRear(), toTime).getSI() + lengthOffset) / this.getYAxis().getGranularities()[0];
double relativeFromTime =
(fromTime.getSI() - this.getXAxis().getMinimumValue().getSI()) / this.getXAxis().getGranularities()[0];
final double relativeToTime =
(toTime.getSI() - this.getXAxis().getMinimumValue().getSI()) / this.getXAxis().getGranularities()[0];
final int fromTimeBin = (int) Math.floor(relativeFromTime);
final int toTimeBin = (int) Math.floor(relativeToTime) + 1;
double relativeMeanSpeed = (relativeToDistance - relativeFromDistance) / (relativeToTime - relativeFromTime);
// The code for acceleration assumes that acceleration is constant (which is correct for IDM+, but may be
// wrong for other car following algorithms).
double acceleration = car.getAcceleration(car.getOperationalPlan().getStartTime()).getSI();
for (int timeBin = fromTimeBin; timeBin < toTimeBin; timeBin++)
{
if (timeBin < 0)
{
continue;
}
double binEndTime = timeBin + 1;
if (binEndTime > relativeToTime)
{
binEndTime = relativeToTime;
}
if (binEndTime <= relativeFromTime)
{
continue; // no time spent in this timeBin
}
double binDistanceStart =
(car.position(lane, car.getRear(),
new Time.Abs(relativeFromTime * this.getXAxis().getGranularities()[0], TimeUnit.SECOND)).getSI()
- this.getYAxis().getMinimumValue().getSI() + lengthOffset)
/ this.getYAxis().getGranularities()[0];
double binDistanceEnd =
(car.position(lane, car.getRear(),
new Time.Abs(binEndTime * this.getXAxis().getGranularities()[0], TimeUnit.SECOND)).getSI()
- this.getYAxis().getMinimumValue().getSI() + lengthOffset)
/ this.getYAxis().getGranularities()[0];
// Compute the time in each distanceBin
for (int distanceBin = (int) Math.floor(binDistanceStart); distanceBin <= binDistanceEnd; distanceBin++)
{
double relativeDuration = 1;
if (relativeFromTime > timeBin)
{
relativeDuration -= relativeFromTime - timeBin;
}
if (distanceBin == (int) Math.floor(binDistanceEnd))
{
// This GTU does not move out of this distanceBin before the binEndTime
if (binEndTime < timeBin + 1)
{
relativeDuration -= timeBin + 1 - binEndTime;
}
}
else
{
// This GTU moves out of this distanceBin before the binEndTime
// Interpolate the time when this GTU crosses into the next distanceBin
// Using f.i. Newton-Rhaphson interpolation would yield a slightly more precise result...
double timeToBinBoundary = (distanceBin + 1 - binDistanceStart) / relativeMeanSpeed;
double endTime = relativeFromTime + timeToBinBoundary;
relativeDuration -= timeBin + 1 - endTime;
}
final double duration = relativeDuration * this.getXAxis().getGranularities()[0];
final double distance = duration * relativeMeanSpeed * this.getYAxis().getGranularities()[0];
// System.out.println(String.format(
// "timeBin=%d, distanceBin=%d, duration=%f, distance=%f, timeBinSize=%f, distanceBinSize=%f", timeBin,
// distanceBin, duration, distance, this.getYAxis().getGranularities()[0], this.getXAxis()
// .getGranularities()[0]));
incrementBinData(timeBin, distanceBin, duration, distance, acceleration);
relativeFromTime += relativeDuration;
binDistanceStart = distanceBin + 1;
}
relativeFromTime = timeBin + 1;
}
}
/**
* Increase storage for sample data.
* This is only implemented for the time axis.
* @param newUpperLimit DoubleScalar<?> new upper limit for the X range
*/
public abstract void extendXRange(DoubleScalar> newUpperLimit);
/**
* Increment the data of one bin.
* @param timeBin Integer; the rank of the bin on the time-scale
* @param distanceBin Integer; the rank of the bin on the distance-scale
* @param duration Double; the time spent in this bin
* @param distanceCovered Double; the distance covered in this bin
* @param acceleration Double; the average acceleration in this bin
*/
public abstract void incrementBinData(int timeBin, int distanceBin, double duration, double distanceCovered,
double acceleration);
/** {@inheritDoc} */
@Override
public final double getZValue(final int series, final int item)
{
final int timeBinGroup = xAxisBin(item);
final int distanceBinGroup = yAxisBin(item);
// System.out.println(String.format("getZValue(s=%d, i=%d) -> tbg=%d, dbg=%d", series, item, timeBinGroup,
// distanceBinGroup));
final int timeGroupSize = (int) (this.getXAxis().getCurrentGranularity() / this.getXAxis().getGranularities()[0]);
final int firstTimeBin = timeBinGroup * timeGroupSize;
final int distanceGroupSize = (int) (this.getYAxis().getCurrentGranularity() / this.getYAxis().getGranularities()[0]);
final int firstDistanceBin = distanceBinGroup * distanceGroupSize;
final int endTimeBin = Math.min(firstTimeBin + timeGroupSize, this.getXAxis().getBinCount());
final int endDistanceBin = Math.min(firstDistanceBin + distanceGroupSize, this.getYAxis().getBinCount());
return computeZValue(firstTimeBin, endTimeBin, firstDistanceBin, endDistanceBin);
}
/**
* Combine values in a range of time bins and distance bins to obtain a combined density value of the ranges.
* @param firstTimeBin Integer; the first time bin to use
* @param endTimeBin Integer; one higher than the last time bin to use
* @param firstDistanceBin Integer; the first distance bin to use
* @param endDistanceBin Integer; one higher than the last distance bin to use
* @return Double; the density value (or Double.NaN if no value can be computed)
*/
public abstract double computeZValue(int firstTimeBin, int endTimeBin, int firstDistanceBin, int endDistanceBin);
/**
* Get the X axis.
* @return Axis
*/
public final Axis getXAxis()
{
return this.xAxis;
}
/**
* Get the Y axis.
* @return Axis
*/
public final Axis getYAxis()
{
return this.yAxis;
}
/** {@inheritDoc} */
@Override
public final JFrame addViewer()
{
JFrame result = new JFrame(this.caption);
JFreeChart newChart = createChart(result);
newChart.setTitle((String) null);
addChangeListener(newChart.getPlot());
reGraph();
return result;
}
}