AbstractContourPlot.java

package org.opentrafficsim.animation.graphs;

import java.awt.Color;

import org.djunits.value.vdouble.scalar.Duration;
import org.djunits.value.vdouble.scalar.Length;
import org.djutils.event.EventListener;
import org.djutils.event.EventType;
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.animation.BoundsPaintScale;
import org.opentrafficsim.animation.graphs.AbstractContourPlot.ContourPaintState;
import org.opentrafficsim.animation.graphs.AbstractPlot.PaintState;
import org.opentrafficsim.animation.graphs.ContourDataSource.ContourAdditionalDataType;
import org.opentrafficsim.animation.graphs.ContourDataSource.ContourDataType;
import org.opentrafficsim.animation.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 source.
 * Sub-classes only need to specify a few plot specific variables and functionalities.
 * <p>
 * Copyright (c) 2013-2026 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 Alexander Verbraeck
 * @author Peter Knoppers
 * @author Wouter Schakel
 * @param <Z> z-value type
 */
public abstract class AbstractContourPlot<Z extends Number> extends AbstractSpaceTimePlot<ContourPaintState>
        implements XyInterpolatedDataset
{

    /** Color scale for the graph. */
    private final BoundsPaintScale paintScale;

    /** Label data. */
    private final LabelData<Z> labelData;

    /** Data source. */
    private final ContourDataSource source;

    /** Contour data type. */
    private final ContourDataType<Z> contourDataType;

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

    /**
     * Constructor with specified paint scale.
     * @param caption caption
     * @param source data source
     * @param contourDataType contour data type
     * @param paintScale paint scale
     * @param labelData label data
     */
    public AbstractContourPlot(final String caption, final ContourDataSource source, final ContourDataType<Z> contourDataType,
            final BoundsPaintScale paintScale, final LabelData<Z> labelData)
    {
        super(caption, source.getInitialUpdateInterval(), source.getPlotScheduler(), source.getDelay(),
                DEFAULT_INITIAL_UPPER_TIME_BOUND);
        this.source = Throw.whenNull(source, "dataPool");
        this.contourDataType = Throw.whenNull(contourDataType, "contourDataType");
        this.paintScale = Throw.whenNull(paintScale, "paintScale");
        this.labelData = Throw.whenNull(labelData, "labelData");
        this.blockRenderer = new XyInterpolatedBlockRenderer(this);
        this.blockRenderer.setPaintScale(this.paintScale);
        this.blockRenderer.setBlockHeight(source.getGranularity(Dimension.DISTANCE));
        this.blockRenderer.setBlockWidth(source.getGranularity(Dimension.TIME));
        source.addPlot(this);
        setChart(createChart());
    }

    /**
     * Constructor with default paint scale (red at minimum, yellow at mid-point, green at maximum).
     * @param caption caption
     * @param source data source
     * @param contourDataType contour data type
     * @param bounds paint scale bounds
     * @param labelData label data
     */
    public AbstractContourPlot(final String caption, final ContourDataSource source,
            final ContourAdditionalDataType<Z, ?> contourDataType, final Bounds<Z> bounds, final LabelData<Z> labelData)
    {
        this(caption, source, contourDataType, createPaintScale(bounds), labelData);
    }

    /**
     * Creates a default paint scale from red, via yellow to green.
     * @param bounds paint scale bounds
     * @return default paint scale
     */
    private static BoundsPaintScale createPaintScale(final Bounds<?> bounds)
    {
        Throw.when(bounds.minimum().doubleValue() >= bounds.maximum().doubleValue(), IllegalArgumentException.class,
                "Minimum value %s is above or equal to maximum value %s.", bounds.minimum(), bounds.maximum());
        double[] boundaries = {bounds.minimum().doubleValue(),
                (bounds.minimum().doubleValue() + bounds.maximum().doubleValue()) / 2.0, bounds.maximum().doubleValue()};
        Color[] colorValues = {Color.RED, Color.YELLOW, Color.GREEN};
        return new BoundsPaintScale(boundaries, colorValues);
    }

    /**
     * Create a chart.
     * @return 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.labelData.legendStep().doubleValue();
            if (value > this.paintScale.getUpperBound() + 1e-6)
            {
                break;
            }
            legend.add(new LegendItem(String.format(this.labelData.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;
    }

    @Override
    protected ContourPaintState emptyPaintState()
    {
        return new ContourPaintState(new float[0], 1.0, 1.0, 0, false, Duration.ZERO);
    }

    /**
     * Returns the contour data type for use in a {@link ContourDataSource}.
     * @return contour data type
     */
    protected ContourDataType<Z> getContourDataType()
    {
        return this.contourDataType;
    }

    /**
     * Scale the z-value from SI to the desired unit for users, in line with unit in {@link LabelData#labelFormat}.
     * @param si SI value
     * @return scaled value
     */
    protected abstract double scale(double si);

    @Override
    protected void setPaintState()
    {
        super.setPaintState();
        this.blockRenderer.setInterpolate(getPaintState().interpolate());
        this.blockRenderer.setBlockHeight(getPaintState().dx());
        this.blockRenderer.setBlockWidth(getPaintState().dt());
    }

    /**
     * Returns the status label when the mouse is over the given location.
     * @param domainValue domain value (x-axis)
     * @param rangeValue range value (y-axis)
     * @return status label when the mouse is over the given location
     */
    @Override
    public String getStatusLabel(final double domainValue, final double rangeValue)
    {
        if (getPaintState().data().length == 0)
        {
            return String.format("time %.0fs, distance %.0fm", domainValue, rangeValue);
        }
        int i = getPaintState().getSpaceSlice(rangeValue);
        int j = getPaintState().getTimeSlice(domainValue);
        int item = j * getPaintState().nSpaceSlices() + i;
        double zValue = scale(getZValue(1, item));
        return String.format("time %.0fs, distance %.0fm, " + this.labelData.labelFormat(), domainValue, rangeValue, zValue);
    }

    // ===== XyInterpolatedDataset =====

    @Override
    public int getItemCount(final int series)
    {
        return getPaintState().getItemCount();
    }

    @Override
    public Number getX(final int series, final int item)
    {
        return getXValue(series, item);
    }

    @Override
    public double getXValue(final int series, final int item)
    {
        return getPaintState().getTimeValue(item);
    }

    @Override
    public Number getY(final int series, final int item)
    {
        return getYValue(series, item);
    }

    @Override
    public double getYValue(final int series, final int item)
    {
        return getPaintState().getSpaceValue(item);
    }

    @Override
    public Number getZ(final int series, final int item)
    {
        return getZValue(series, item);
    }

    @Override
    public Comparable<String> getSeriesKey(final int series)
    {
        return getCaption();
    }

    @SuppressWarnings("rawtypes")
    @Override
    public int indexOf(final Comparable seriesKey)
    {
        return 0;
    }

    @Override
    public DomainOrder getDomainOrder()
    {
        return DomainOrder.ASCENDING;
    }

    @Override
    public double getZValue(final int series, final int item)
    {
        // default 1 series
        return getPaintState().data()[item];
    }

    @Override
    public int getSeriesCount()
    {
        return 1; // default
    }

    @Override
    public int getRangeBinCount()
    {
        return getPaintState().nSpaceSlices();
    }

    // ===== Delegate forwards =====

    /**
     * Add listener to synchronize UI element when another UI element changes a setting, or the setting is changed
     * programmatically.
     * @param listener listener
     * @param eventType event type (i.e. {@code ContourDataSource.GRANULARITY}, {@code ContourDataSource.INTERPOLATE} or
     *            {@code ContourDataSource.SMOOTH})
     */
    public void addListener(final EventListener listener, final EventType eventType)
    {
        this.source.addListener(listener, eventType);
    }

    /**
     * Returns the available granularities that a linked plot may use.
     * @param dimension space or time
     * @return available granularities that a linked plot may use
     */
    public double[] getGranularities(final Dimension dimension)
    {
        return this.source.getGranularities(dimension);
    }

    /**
     * Returns the selected granularity that a linked plot should use.
     * @param dimension space or time
     * @return granularity that a linked plot should use
     */
    public double getGranularity(final Dimension dimension)
    {
        return this.source.getGranularity(dimension);
    }

    /**
     * Sets the granularity of the plot. This will invalidate the plot triggering a redraw.
     * @param dimension space or time
     * @param granularity granularity in space or time (SI unit)
     */
    public void setGranularity(final Dimension dimension, final double granularity)
    {
        this.source.setGranularity(dimension, granularity);
        invalidate();
    }

    /**
     * Sets bi-linear interpolation enabled or disabled. This will invalidate the plot triggering a redraw.
     * @param interpolate whether to enable interpolation
     */
    public void setInterpolate(final boolean interpolate)
    {
        this.source.setInterpolate(interpolate);
        invalidate();
    }

    /**
     * Sets the adaptive smoothing enabled or disabled. This will invalidate the plot triggering a redraw.
     * @param smooth whether to smooth the plot
     */
    public void setSmooth(final boolean smooth)
    {
        this.source.setSmooth(smooth);
        invalidate();
    }

    @Override
    protected void calculatePaintState(final Duration time)
    {
        this.source.calculatePaintStateSafe(time);
    }

    @Override
    protected Length getEndLocation()
    {
        return this.source.getPath().getTotalLength();
    }

    // ===== Helper classes =====

    /**
     * Bounds for the color bounds scale.
     * @param minimum minimum value
     * @param maximum maximum value
     * @param <Z> value type
     */
    public record Bounds<Z extends Number>(Z minimum, Z maximum)
    {
    }

    /**
     * Input container for label input.
     * @param legendStep increment between color legend entries
     * @param legendFormat format string for the captions in the color legend, for example {@code %.1fm/s} for {@code 30.2m/s}
     * @param labelFormat format string used to create status label (under the mouse), e.g. {@code desired speed %.1f m/s}
     * @param <Z> value type
     */
    public record LabelData<Z extends Number>(Z legendStep, String legendFormat, String labelFormat)
    {
    }

    /**
     * Paint state for contour plots.
     * @param data data as flat array
     * @param dx space granularity
     * @param dt time granularity
     * @param nSpaceSlices number of slices for space
     * @param interpolate whether to interpolate
     * @param getAvailableTime available time
     */
    public record ContourPaintState(float[] data, double dx, double dt, int nSpaceSlices, boolean interpolate,
            Duration getAvailableTime) implements PaintState
    {

        /**
         * Returns the number of available items.
         * @return number of available items
         */
        public int getItemCount()
        {
            return data().length;
        }

        /**
         * Returns the time slice number of the item.
         * @param item item number
         * @return time slice number of the item
         */
        public double getTimeValue(final int item)
        {
            return dt() * (item / nSpaceSlices());
        }

        /**
         * Returns the space slice number of the item.
         * @param item item number
         * @return space slice number of the item
         */
        public double getSpaceValue(final int item)
        {
            return dx() * (item % nSpaceSlices());
        }

        /**
         * Returns the time slice index for the given time value.
         * @param t time value
         * @return time slice index
         */
        public int getTimeSlice(final double t)
        {
            int n = (int) (t / dt());
            int nMax = data().length / nSpaceSlices();
            return n < 0 ? 0 : (n > nMax ? nMax : n);
        }

        /**
         * Returns the space slice index for the given space value.
         * @param x space value
         * @return space slice index
         */
        public int getSpaceSlice(final double x)
        {
            int n = (int) (x / dx());
            return n < 0 ? 0 : (n > nSpaceSlices() ? nSpaceSlices() : n);
        }

    }

}