package org.opentrafficsim.draw.graphs; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.LinkedHashMap; import java.util.Map; import javax.swing.ButtonGroup; import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenu; import javax.swing.JPopupMenu; import javax.swing.JRadioButtonMenuItem; import org.djunits.value.vdouble.scalar.Time; import org.djutils.exceptions.Throw; 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.plot.XYPlot; import org.jfree.data.DomainOrder; import org.opentrafficsim.core.dsol.OTSSimulatorInterface; import org.opentrafficsim.draw.core.BoundsPaintScale; import org.opentrafficsim.draw.graphs.ContourDataSource.ContourDataType; import org.opentrafficsim.draw.graphs.ContourDataSource.Dimension; /** * Class for contour plots. The data that is plotted is stored in a {@code ContourDataSource}, which may be shared among several * contour plots along the same path. This abstract class takes care of the interactions between the plot and the data pool. Sub * classes only need to specify a few plot specific variables and functionalities. *

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

* @version $Revision$, $LastChangedDate$, by $Author$, initial version 4 okt. 2018
* @author Alexander Verbraeck * @author Peter Knoppers * @author Wouter Schakel * @param z-value type */ public abstract class AbstractContourPlot extends AbstractSamplerPlot implements XYInterpolatedDataset { /** */ private static final long serialVersionUID = 20181004L; /** Color scale for the graph. */ private final BoundsPaintScale paintScale; /** Difference of successive values in the legend. */ private final Z legendStep; /** Format string used to create the captions in the legend. */ private final String legendFormat; /** Format string used to create status label (under the mouse). */ private final String valueFormat; /** Data pool. */ private final ContourDataSource dataPool; /** Map to set time granularity. */ private Map timeGranularityButtons = new LinkedHashMap<>(); /** Map to set space granularity. */ private Map spaceGranularityButtons = new LinkedHashMap<>(); /** Check box for smoothing. */ private JCheckBoxMenuItem smoothCheckBox; /** Check box for interpolation. */ private JCheckBoxMenuItem interpolateCheckBox; /** Block renderer in chart. */ private XYInterpolatedBlockRenderer blockRenderer = null; /** * Constructor with specified paint scale. * @param caption String; caption * @param simulator OTSSimulatorInterface; simulator * @param dataPool ContourDataSource<?>; data pool * @param paintScale BoundsPaintScale; paint scale * @param legendStep Z; increment between color legend entries * @param legendFormat String; format string for the captions in the color legend * @param valueFormat String; format string used to create status label (under the mouse) */ public AbstractContourPlot(final String caption, final OTSSimulatorInterface simulator, final ContourDataSource dataPool, final BoundsPaintScale paintScale, final Z legendStep, final String legendFormat, final String valueFormat) { super(caption, dataPool.getUpdateInterval(), simulator, dataPool.getSampler(), dataPool.getPath(), dataPool.getDelay()); dataPool.registerContourPlot(this); this.dataPool = dataPool; this.paintScale = paintScale; this.legendStep = legendStep; this.legendFormat = legendFormat; this.valueFormat = valueFormat; this.blockRenderer = new XYInterpolatedBlockRenderer(this); this.blockRenderer.setPaintScale(this.paintScale); this.blockRenderer.setBlockHeight(dataPool.getGranularity(Dimension.DISTANCE)); this.blockRenderer.setBlockWidth(dataPool.getGranularity(Dimension.TIME)); setChart(createChart()); } /** * Constructor with default paint scale. * @param caption String; caption * @param simulator OTSSimulatorInterface; simulator * @param dataPool ContourDataSource<?>; data pool * @param legendStep Z; increment between color legend entries * @param legendFormat String; format string for the captions in the color legend * @param minValue Z; minimum value * @param maxValue Z; maximum value * @param valueFormat String; format string used to create status label (under the mouse) */ @SuppressWarnings("parameternumber") public AbstractContourPlot(final String caption, final OTSSimulatorInterface simulator, final ContourDataSource dataPool, final Z legendStep, final String legendFormat, final Z minValue, final Z maxValue, final String valueFormat) { this(caption, simulator, dataPool, createPaintScale(minValue, maxValue), legendStep, legendFormat, valueFormat); } /** * Creates a default paint scale from red, via yellow to green. * @param minValue Number; minimum value * @param maxValue Number; maximum value * @return BoundsPaintScale; default paint scale */ private static BoundsPaintScale createPaintScale(final Number minValue, final Number maxValue) { Throw.when(minValue.doubleValue() >= maxValue.doubleValue(), IllegalArgumentException.class, "Minimum value %s is below or equal to maxumum value %s.", minValue, maxValue); double[] boundaries = { minValue.doubleValue(), (minValue.doubleValue() + maxValue.doubleValue()) / 2.0, maxValue.doubleValue() }; Color[] colorValues = { Color.RED, Color.YELLOW, Color.GREEN }; return new BoundsPaintScale(boundaries, colorValues); } /** * Create a chart. * @return JFreeChart; chart */ private JFreeChart createChart() { NumberAxis xAxis = new NumberAxis("Time [s] \u2192"); NumberAxis yAxis = new NumberAxis("Distance [m] \u2192"); XYPlot plot = new XYPlot(this, xAxis, yAxis, this.blockRenderer); LegendItemCollection legend = new LegendItemCollection(); for (int i = 0;; i++) { double value = this.paintScale.getLowerBound() + i * this.legendStep.doubleValue(); if (value > this.paintScale.getUpperBound() + 1e-6) { break; } legend.add(new LegendItem(String.format(this.legendFormat, scale(value)), this.paintScale.getPaint(value))); } legend.add(new LegendItem("No data", Color.BLACK)); plot.setFixedLegendItems(legend); final JFreeChart chart = new JFreeChart(getCaption(), plot); return chart; } /** {@inheritDoc} */ @Override protected void addPopUpMenuItems(final JPopupMenu popupMenu) { super.addPopUpMenuItems(popupMenu); JMenu spaceGranularityMenu = buildMenu("Distance granularity", "%.0f m", 1000, "%.0f km", "setSpaceGranularity", this.dataPool.getGranularities(Dimension.DISTANCE), this.dataPool.getGranularity(Dimension.DISTANCE), this.spaceGranularityButtons); popupMenu.insert(spaceGranularityMenu, 0); JMenu timeGranularityMenu = buildMenu("Time granularity", "%.0f s", 60.0, "%.0f min", "setTimeGranularity", this.dataPool.getGranularities(Dimension.TIME), this.dataPool.getGranularity(Dimension.TIME), this.timeGranularityButtons); popupMenu.insert(timeGranularityMenu, 1); this.smoothCheckBox = new JCheckBoxMenuItem("Adaptive smoothing method", false); this.smoothCheckBox.addActionListener(new ActionListener() { /** {@inheritDoc} */ @SuppressWarnings("synthetic-access") @Override public void actionPerformed(final ActionEvent e) { AbstractContourPlot.this.dataPool.setSmooth(((JCheckBoxMenuItem) e.getSource()).isSelected()); notifyPlotChange(); } }); popupMenu.insert(this.smoothCheckBox, 2); this.interpolateCheckBox = new JCheckBoxMenuItem("Bilinear interpolation", true); this.interpolateCheckBox.addActionListener(new ActionListener() { /** {@inheritDoc} */ @SuppressWarnings("synthetic-access") @Override public void actionPerformed(final ActionEvent e) { boolean interpolate = ((JCheckBoxMenuItem) e.getSource()).isSelected(); AbstractContourPlot.this.blockRenderer.setInterpolate(interpolate); AbstractContourPlot.this.dataPool.setInterpolate(interpolate); notifyPlotChange(); } }); popupMenu.insert(this.interpolateCheckBox, 3); } /** * Create a JMenu to let the user set the granularity. * @param menuName String; caption for the new JMenu * @param format1 String; format string for the values in the items under the new JMenu, below formatValue * @param formatValue double; format value * @param format2 String; format string for the values in the items under the new JMenu, above and equal to formatValue * @param command String; prefix for the actionCommand of the items under the new JMenu * @param values double[]; array of values to be formatted using the format strings to yield the items under the new JMenu * @param initialValue double; the currently selected value (used to put the bullet on the correct item) * @param granularityButtons Map<JRadioButtonMenuItem, Double>; map in to which buttons should be added * @return JMenu with JRadioMenuItems for the values and a bullet on the currentValue item */ private JMenu buildMenu(final String menuName, final String format1, final double formatValue, final String format2, final String command, final double[] values, final double initialValue, final Map granularityButtons) { JMenu result = new JMenu(menuName); ButtonGroup group = new ButtonGroup(); for (double value : values) { JRadioButtonMenuItem item = new JRadioButtonMenuItem( String.format(value < formatValue ? format1 : format2, value < formatValue ? value : value / formatValue)); granularityButtons.put(item, value); item.setSelected(value == initialValue); item.setActionCommand(command); item.addActionListener(new ActionListener() { /** {@inheritDoc} */ @SuppressWarnings("synthetic-access") @Override public void actionPerformed(final ActionEvent actionEvent) { if (command.equalsIgnoreCase("setSpaceGranularity")) { double granularity = AbstractContourPlot.this.spaceGranularityButtons.get(actionEvent.getSource()); AbstractContourPlot.this.dataPool.setGranularity(Dimension.DISTANCE, granularity); } else if (command.equalsIgnoreCase("setTimeGranularity")) { double granularity = AbstractContourPlot.this.timeGranularityButtons.get(actionEvent.getSource()); AbstractContourPlot.this.dataPool.setGranularity(Dimension.TIME, granularity); } else { throw new RuntimeException("Unknown ActionEvent"); } } }); result.add(item); group.add(item); } return result; } /** * Returns the time granularity, just for information. * @return double; time granularity */ public double getTimeGranularity() { return this.dataPool.getGranularity(Dimension.TIME); } /** * Returns the space granularity, just for information. * @return double; space granularity */ public double getSpaceGranularity() { return this.dataPool.getGranularity(Dimension.DISTANCE); } /** * Sets the correct space granularity radio button to selected. This is done from a {@code DataPool} to keep multiple plots * consistent. * @param granularity double; space granularity */ protected final void setSpaceGranularityRadioButton(final double granularity) { this.blockRenderer.setBlockHeight(granularity); for (JRadioButtonMenuItem button : this.spaceGranularityButtons.keySet()) { button.setSelected(this.spaceGranularityButtons.get(button) == granularity); } } /** * Sets the correct time granularity radio button to selected. This is done from a {@code DataPool} to keep multiple plots * consistent. * @param granularity double; time granularity */ protected final void setTimeGranularityRadioButton(final double granularity) { this.blockRenderer.setBlockWidth(granularity); for (JRadioButtonMenuItem button : this.timeGranularityButtons.keySet()) { button.setSelected(this.timeGranularityButtons.get(button) == granularity); } } /** * Sets the check box for smooth rendering. This is done from a {@code DataPool} to keep multiple plots consistent. * @param smooth boolean; selected or not */ protected final void setSmoothing(final boolean smooth) { this.smoothCheckBox.setSelected(smooth); } /** * Sets the check box for interpolated rendering and block renderer setting. This is done from a {@code DataPool} to keep * multiple plots consistent. * @param interpolate boolean; selected or not */ protected final void setInterpolation(final boolean interpolate) { this.blockRenderer.setInterpolate(interpolate); this.interpolateCheckBox.setSelected(interpolate); } /** * Returns the data pool for sub classes. * @return ContourDataSource; data pool for subclasses */ protected final ContourDataSource getDataPool() { return this.dataPool; } /** {@inheritDoc} */ @Override public final int getItemCount(final int series) { return this.dataPool.getBinCount(Dimension.DISTANCE) * this.dataPool.getBinCount(Dimension.TIME); } /** {@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 this.dataPool.getAxisValue(Dimension.TIME, item); } /** {@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.dataPool.getAxisValue(Dimension.DISTANCE, item); } /** {@inheritDoc} */ @Override public final Number getZ(final int series, final int item) { return getZValue(series, item); } /** {@inheritDoc} */ @Override public final Comparable getSeriesKey(final int series) { return getCaption(); } /** {@inheritDoc} */ @SuppressWarnings("rawtypes") @Override public final int indexOf(final Comparable seriesKey) { return 0; } /** {@inheritDoc} */ @Override public final DomainOrder getDomainOrder() { return DomainOrder.ASCENDING; } /** {@inheritDoc} */ @Override public final double getZValue(final int series, final int item) { // default 1 series return getValue(item, this.dataPool.getGranularity(Dimension.DISTANCE), this.dataPool.getGranularity(Dimension.TIME)); } /** {@inheritDoc} */ @Override public final int getSeriesCount() { return 1; // default } /** {@inheritDoc} */ @Override public int getRangeBinCount() { return this.dataPool.getBinCount(Dimension.DISTANCE); } /** * Returns the status label when the mouse is over the given location. * @param domainValue double; domain value (x-axis) * @param rangeValue double; range value (y-axis) * @return String; status label when the mouse is over the given location */ protected final String getStatusLabel(final double domainValue, final double rangeValue) { if (this.dataPool == null) { return String.format("time %.0fs, distance %.0fm", domainValue, rangeValue); } int i = this.dataPool.getAxisBin(Dimension.DISTANCE, rangeValue); int j = this.dataPool.getAxisBin(Dimension.TIME, domainValue); int item = j * this.dataPool.getBinCount(Dimension.DISTANCE) + i; double zValue = scale( getValue(item, this.dataPool.getGranularity(Dimension.DISTANCE), this.dataPool.getGranularity(Dimension.TIME))); return String.format("time %.0fs, distance %.0fm, " + this.valueFormat, domainValue, rangeValue, zValue); } /** {@inheritDoc} */ @Override protected final void increaseTime(final Time time) { if (this.dataPool != null) // dataPool is null at construction { this.dataPool.increaseTime(time); } } /** * Obtain value for cell from the data pool. * @param item int; item number * @param cellLength double; cell length * @param cellSpan double; cell duration * @return double; value for cell from the data pool */ protected abstract double getValue(int item, double cellLength, double cellSpan); /** * Scale the value from SI to the desired unit for users. * @param si double; SI value * @return double; scaled value */ protected abstract double scale(double si); /** * Returns the contour data type for use in a {@code ContourDataSource}. * @return CountorDataType; contour data type */ protected abstract ContourDataType getContourDataType(); }