AbstractContourPlot.java

package org.opentrafficsim.draw.graphs;

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

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.draw.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.
 * <p>
 * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
 * </p>
 * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
 * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
 * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
 * @param <Z> z-value type
 */
public abstract class AbstractContourPlot<Z extends Number> extends AbstractSamplerPlot
        implements XyInterpolatedDataset, ActionListener
{

    /** 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;

    /** Block renderer in chart. */
    private XyInterpolatedBlockRenderer blockRenderer = null;

    /**
     * Constructor with specified paint scale.
     * @param caption String; caption
     * @param scheduler PlotScheduler; scheduler.
     * @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 PlotScheduler scheduler, final ContourDataSource dataPool,
            final BoundsPaintScale paintScale, final Z legendStep, final String legendFormat, final String valueFormat)
    {
        super(caption, dataPool.getUpdateInterval(), scheduler, dataPool.getSamplerData(), 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 scheduler PlotScheduler; scheduler.
     * @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 PlotScheduler scheduler, final ContourDataSource dataPool,
            final Z legendStep, final String legendFormat, final Z minValue, final Z maxValue, final String valueFormat)
    {
        this(caption, scheduler, 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;
    }

    /**
     * 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
     */
    public final void setSpaceGranularity(final double granularity)
    {
        this.blockRenderer.setBlockHeight(granularity);
        this.dataPool.spaceAxis.setGranularity(granularity); // XXX: AV added 16-5-2020
    }

    /**
     * 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
     */
    public final void setTimeGranularity(final double granularity)
    {
        this.blockRenderer.setBlockWidth(granularity);
        this.dataPool.timeAxis.setGranularity(granularity); // XXX: AV added 16-5-2020
    }

    /**
     * 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
     */
    public final void setInterpolation(final boolean interpolate)
    {
        this.blockRenderer.setInterpolate(interpolate);
        this.dataPool.timeAxis.setInterpolate(interpolate); // XXX: AV added 16-5-2020
        this.dataPool.spaceAxis.setInterpolate(interpolate); // XXX: AV added 16-5-2020
    }

    /**
     * Returns the data pool for sub classes.
     * @return ContourDataSource; data pool for subclasses
     */
    public 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<String> 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
     */
    @Override
    public 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<Z, ?> getContourDataType();

    /**
     * Returns the block renderer.
     * @return block renderer
     */
    public XyInterpolatedBlockRenderer getBlockRenderer()
    {
        return this.blockRenderer;
    }

    @Override
    public final void actionPerformed(final ActionEvent actionEvent)
    {
        String command = actionEvent.getActionCommand();
        if (command.equalsIgnoreCase("setSpaceGranularity"))
        {
            // The source field is abused to contain the granularity
            double granularity = (double) actionEvent.getSource();
            setSpaceGranularity(granularity);
        }
        else if (command.equalsIgnoreCase("setTimeGranularity"))
        {
            // The source field is abused to contain the granularity
            double granularity = (double) actionEvent.getSource();
            setTimeGranularity(granularity);
        }
        else
        {
            throw new RuntimeException("Unhandled ActionEvent (actionCommand is " + actionEvent.getActionCommand() + "\")");
        }
    }

}