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();
}