package org.opentrafficsim.graphs; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.Serializable; import java.util.ArrayList; 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.FrequencyUnit; import org.djunits.unit.LinearDensityUnit; import org.djunits.unit.SpeedUnit; 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.LinearDensity; import org.djunits.value.vdouble.scalar.Speed; 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.event.AxisChangeEvent; import org.jfree.chart.labels.XYItemLabelGenerator; import org.jfree.chart.plot.PlotOrientation; 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.OTSSimulatorInterface; import org.opentrafficsim.core.gtu.GTUException; import org.opentrafficsim.core.gtu.RelativePosition; import org.opentrafficsim.core.network.NetworkException; import org.opentrafficsim.road.gtu.lane.LaneBasedGTU; import org.opentrafficsim.road.network.lane.CrossSectionElement; import org.opentrafficsim.road.network.lane.Lane; import org.opentrafficsim.road.network.lane.object.sensor.AbstractSensor; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import nl.tudelft.simulation.language.Throw; /** * The Fundamental Diagram Graph; see Wikipedia: * http://en.wikipedia.org/wiki/Fundamental_diagram_of_traffic_flow. *

* 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 31, 2014
* @author Peter Knoppers */ public class FundamentalDiagram extends JFrame implements XYDataset, ActionListener, Serializable { /** */ private static final long serialVersionUID = 20140701L; /** The ChartPanel for this Fundamental Diagram. */ private JFreeChart chartPanel; /** Caption for this Fundamental Diagram. */ private final String caption; /** Position of this Fundamental Diagram sensor. */ private final Length position; /** Area to show status information. */ private final JLabel statusLabel; /** Sample duration of the detector that generates this Fundamental Diagram. */ private final Duration aggregationTime; /** * @return aggregationTime */ public final Duration getAggregationTime() { return this.aggregationTime; } /** Storage for the Samples. */ private ArrayList samples = new ArrayList(); /** Definition of the density axis. */ private Axis densityAxis = new Axis(new LinearDensity(0, LinearDensityUnit.PER_KILOMETER), new LinearDensity(200, LinearDensityUnit.PER_KILOMETER), null, 0d, "Density [veh/km]", "Density", "density %.1f veh/km"); /** * @return densityAxis */ public final Axis getDensityAxis() { return this.densityAxis; } /** Definition of the speed axis. */ private Axis speedAxis = new Axis(new Speed(0, SpeedUnit.KM_PER_HOUR), new Speed(180, SpeedUnit.KM_PER_HOUR), null, 0d, "Speed [km/h]", "Speed", "speed %.0f km/h"); /** * @return speedAxis */ public final Axis getSpeedAxis() { return this.speedAxis; } /** * @return flowAxis */ public final Axis getFlowAxis() { return this.flowAxis; } /** Definition of the flow axis. */ private Axis flowAxis = new Axis(new Frequency(0, FrequencyUnit.PER_HOUR), new Frequency(3000d, FrequencyUnit.HERTZ), null, 0d, "Flow [veh/h]", "Flow", "flow %.0f veh/h"); /** The currently shown X-axis. */ private Axis xAxis; /** The currently shown Y-axis. */ private Axis yAxis; /** List of parties interested in changes of this ContourPlot. */ private transient EventListenerList listenerList = new EventListenerList(); /** Not used internally. */ private DatasetGroup datasetGroup = null; /** * Retrieve the format string for the Y axis. * @return format string */ public final String getYAxisFormat() { return this.yAxis.getFormat(); } /** * Retrieve the format string for the X axis. * @return format string */ public final String getXAxisFormat() { return this.xAxis.getFormat(); } /** * Graph a Fundamental Diagram. * @param caption String; the caption shown above the graphing area. * @param aggregationTime DoubleScalarRel<TimeUnit>; the aggregation of the detector that generates the data for this * Fundamental diagram * @param lane Lane; the Lane on which the traffic will be sampled * @param position DoubleScalarRel<LengthUnit>; longitudinal position of the detector on the Lane * @param simulator the simulator * @throws NetworkException on network inconsistency */ public FundamentalDiagram(final String caption, final Duration aggregationTime, final Lane lane, final Length position, final OTSDEVSSimulatorInterface simulator) throws NetworkException { if (aggregationTime.getSI() <= 0) { throw new Error("Aggregation time must be > 0 (got " + aggregationTime + ")"); } this.aggregationTime = aggregationTime; this.caption = caption; this.position = position; ChartFactory.setChartTheme(new StandardChartTheme("JFree/Shadow", false)); this.chartPanel = ChartFactory.createXYLineChart(this.caption, "", "", this, PlotOrientation.VERTICAL, false, false, false); FixCaption.fixCaption(this.chartPanel); final XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) this.chartPanel.getXYPlot().getRenderer(); renderer.setBaseLinesVisible(true); renderer.setBaseShapesVisible(true); renderer.setBaseItemLabelGenerator(new XYItemLabelGenerator() { @Override public String generateLabel(final XYDataset dataset, final int series, final int item) { return String.format("%.0fs", item * aggregationTime.getSI()); } }); renderer.setBaseItemLabelsVisible(true); final ChartPanel cp = new ChartPanel(this.chartPanel); PointerHandler ph = new PointerHandler() { /** */ private static final long serialVersionUID = 20140000L; /** {@inheritDoc} */ @Override void updateHint(final double domainValue, final double rangeValue) { if (Double.isNaN(domainValue)) { setStatusText(" "); return; } String s1 = String.format(getXAxisFormat(), domainValue); String s2 = String.format(getYAxisFormat(), rangeValue); setStatusText(s1 + ", " + s2); } }; cp.addMouseMotionListener(ph); cp.addMouseListener(ph); cp.setMouseWheelEnabled(true); final JMenu subMenu = new JMenu("Set layout"); final ButtonGroup group = new ButtonGroup(); final JRadioButtonMenuItem defaultItem = addMenuItem(subMenu, group, getDensityAxis(), this.flowAxis, true); addMenuItem(subMenu, group, this.flowAxis, this.speedAxis, false); addMenuItem(subMenu, group, this.densityAxis, this.speedAxis, false); actionPerformed(new ActionEvent(this, 0, defaultItem.getActionCommand())); final JPopupMenu popupMenu = cp.getPopupMenu(); popupMenu.insert(subMenu, 0); this.add(cp, BorderLayout.CENTER); this.statusLabel = new JLabel(" ", SwingConstants.CENTER); this.add(this.statusLabel, BorderLayout.SOUTH); new FundamentalDiagramSensor(lane, position, simulator); } /** * Update the status text. * @param newText String; the new text to show */ public final void setStatusText(final String newText) { this.statusLabel.setText(newText); } /** * Retrieve the position of the detector. * @return Length; the position of the detector */ public final Length getPosition() { return this.position; } /** * Build one JRadioButtonMenuItem for the sub menu of the context menu. * @param subMenu JMenu; the menu to which the new JRadioButtonMenuItem is added * @param group ButtonGroup; the buttonGroup for the new JRadioButtonMenuItem * @param xAxisToSelect Axis; the Axis that will become X-axis when this item is clicked * @param yAxisToSelect Axis; the Axis that will become Y-axis when this item is clicked * @param selected Boolean; if true, the new JRadioButtonMenuItem will be selected; if false, the new JRadioButtonMenuItem * will not be selected * @return JRatioButtonMenuItem; the newly added item */ private JRadioButtonMenuItem addMenuItem(final JMenu subMenu, final ButtonGroup group, final Axis xAxisToSelect, final Axis yAxisToSelect, final boolean selected) { final JRadioButtonMenuItem item = new JRadioButtonMenuItem(yAxisToSelect.getShortName() + " / " + xAxisToSelect.getShortName()); item.setSelected(selected); item.setActionCommand(yAxisToSelect.getShortName() + "/" + xAxisToSelect.getShortName()); item.addActionListener(this); subMenu.add(item); group.add(item); return item; } /** * Add the effect of one passing car to this Fundamental Diagram. * @param gtu AbstractLaneBasedGTU; the GTU that passes the detection point * @throws GTUException when the speed of the GTU cannot be assessed */ public final void addData(final LaneBasedGTU gtu) throws GTUException { Time detectionTime = gtu.getSimulator().getSimulatorTime().getTime(); // Figure out the time bin final int timeBin = (int) Math.floor(detectionTime.getSI() / this.aggregationTime.getSI()); // Extend storage if needed while (timeBin >= this.samples.size()) { this.samples.add(new Sample()); } Sample sample = this.samples.get(timeBin); sample.addData(gtu.getSpeed()); } /** * Set up a JFreeChart axis. * @param valueAxis ValueAxis; the axis to set up * @param axis Axis; the Axis that provides the data to setup the ValueAxis */ private static void configureAxis(final ValueAxis valueAxis, final Axis axis) { valueAxis.setLabel("\u2192 " + axis.getName()); valueAxis.setRange(axis.getMinimumValue().getInUnit(), axis.getMaximumValue().getInUnit()); } /** * Redraw this TrajectoryGraph (after the underlying data has been changed, or to change axes). */ public final void reGraph() { NumberAxis numberAxis = new NumberAxis(); configureAxis(numberAxis, this.xAxis); this.chartPanel.getXYPlot().setDomainAxis(numberAxis); this.chartPanel.getPlot().axisChanged(new AxisChangeEvent(numberAxis)); numberAxis = new NumberAxis(); configureAxis(numberAxis, this.yAxis); this.chartPanel.getXYPlot().setRangeAxis(numberAxis); this.chartPanel.getPlot().axisChanged(new AxisChangeEvent(numberAxis)); 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); } } /** {@inheritDoc} */ @Override public final int getSeriesCount() { return 1; } /** {@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.samples.size(); } /** * Retrieve a value from the recorded samples. * @param item Integer; the rank number of the sample * @param axis Axis; the axis that determines which quantity to retrieve * @return Double; the requested value, or Double.NaN if the sample does not (yet) exist */ private Double getSample(final int item, final Axis axis) { if (item >= this.samples.size()) { return Double.NaN; } double result = this.samples.get(item).getValue(axis); /*- System.out.println(String.format("getSample(item=%d, axis=%s) returns %f", item, axis.name, result)); */ return result; } /** {@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) { return getSample(item, this.xAxis); } /** {@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 getSample(item, this.yAxis); } /** {@inheritDoc} */ @Override public final String toString() { return "FundamentalDiagram [caption=" + this.caption + ", aggregationTime=" + this.aggregationTime + ", samples.size=" + this.samples.size() + "]"; } /** * Storage for one sample of data collected by a point-detector that accumulates harmonic mean speed and flow. */ class Sample implements Serializable { /** */ private static final long serialVersionUID = 20140000L; /** Harmonic mean speed observed during this sample [m/s]. */ private double harmonicMeanSpeed; /** Flow observed during this sample [veh/s]. */ private double flow; /** * Retrieve a value stored in this Sample. * @param axis Axis; the axis along which the data is requested * @return double; the retrieved value */ public double getValue(final Axis axis) { if (axis == getDensityAxis()) { return this.flow * 3600 / getAggregationTime().getSI() / this.harmonicMeanSpeed; } else if (axis == getFlowAxis()) { return this.flow * 3600 / getAggregationTime().getSI(); } else if (axis == getSpeedAxis()) { return this.harmonicMeanSpeed * 3600 / 1000; } else { throw new Error("Sample.getValue: Can not identify axis"); } } /** * Add one Car detection to this Sample. * @param speed Speed; the detected speed */ public void addData(final Speed speed) { double sumReciprocalSpeeds = 0; if (this.flow > 0) { sumReciprocalSpeeds = this.flow / this.harmonicMeanSpeed; } this.flow += 1; sumReciprocalSpeeds += 1d / speed.getSI(); this.harmonicMeanSpeed = this.flow / sumReciprocalSpeeds; } /** {@inheritDoc} */ @Override public final String toString() { return "Sample [harmonicMeanSpeed=" + this.harmonicMeanSpeed + ", flow=" + this.flow + "]"; } } /** {@inheritDoc} */ @SuppressFBWarnings("ES_COMPARING_STRINGS_WITH_EQ") @Override public final void actionPerformed(final ActionEvent actionEvent) { final String command = actionEvent.getActionCommand(); // System.out.println("command is \"" + command + "\""); final String[] fields = command.split("[/]"); if (fields.length == 2) { for (String field : fields) { if (field.equalsIgnoreCase(this.densityAxis.getShortName())) { if (field == fields[0]) { this.yAxis = this.densityAxis; } else { this.xAxis = this.densityAxis; } } else if (field.equalsIgnoreCase(this.flowAxis.getShortName())) { if (field == fields[0]) { this.yAxis = this.flowAxis; } else { this.xAxis = this.flowAxis; } } else if (field.equalsIgnoreCase(this.speedAxis.getShortName())) { if (field == fields[0]) { this.yAxis = this.speedAxis; } else { this.xAxis = this.speedAxis; } } else { throw new Error("Cannot find axis name: " + field); } } reGraph(); } else { throw new Error("Unknown ActionEvent"); } } /** * Internal Sensor class. *

* 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 feb. 2015
* @author Peter Knoppers */ class FundamentalDiagramSensor extends AbstractSensor { /** */ private static final long serialVersionUID = 20150203L; /** * Construct a FundamentalDiagramSensor. * @param lane Lane; the Lane on which the new FundamentalDiagramSensor is to be added * @param longitudinalPosition Length; longitudinal position on the Lane of the new FundamentalDiagramSensor * @param simulator simulator to allow animation * @throws NetworkException on network inconsistency */ FundamentalDiagramSensor(final Lane lane, final Length longitudinalPosition, final OTSDEVSSimulatorInterface simulator) throws NetworkException { super("FUNDAMENTAL_DIAGRAM_SENSOR@" + lane.toString(), lane, longitudinalPosition, RelativePosition.REFERENCE, simulator); } /** {@inheritDoc} */ @Override public void triggerResponse(final LaneBasedGTU gtu) { try { addData(gtu); } catch (GTUException exception) { exception.printStackTrace(); // TODO } } /** {@inheritDoc} */ public final String toString() { return "FundamentalDiagramSensor at " + getLongitudinalPosition(); } /** {@inheritDoc} */ @Override public FundamentalDiagramSensor clone(final CrossSectionElement newCSE, final OTSSimulatorInterface newSimulator, final boolean animation) throws NetworkException { Throw.when(!(newCSE instanceof Lane), NetworkException.class, "sensors can only be cloned for Lanes"); Throw.when(!(newSimulator instanceof OTSDEVSSimulatorInterface), NetworkException.class, "simulator should be a DEVSSimulator"); return new FundamentalDiagramSensor((Lane) newCSE, getLongitudinalPosition(), (OTSDEVSSimulatorInterface) newSimulator); } } }