ContourPlotExtendedData.java

package org.opentrafficsim.animation.graphs;

import java.util.List;
import java.util.function.Function;

import org.djunits.unit.Unit;
import org.djunits.value.vdouble.scalar.Duration;
import org.djunits.value.vdouble.scalar.Length;
import org.djunits.value.vdouble.scalar.base.DoubleScalarRel;
import org.djutils.exceptions.Throw;
import org.djutils.math.means.ArithmeticMean;
import org.opentrafficsim.animation.BoundsPaintScale;
import org.opentrafficsim.animation.egtf.Quantity;
import org.opentrafficsim.animation.graphs.ContourDataSource.ContourAdditionalDataType;
import org.opentrafficsim.animation.graphs.ContourDataSource.ContourDataType;
import org.opentrafficsim.kpi.sampling.SamplingException;
import org.opentrafficsim.kpi.sampling.Trajectory;
import org.opentrafficsim.kpi.sampling.TrajectoryGroup;
import org.opentrafficsim.kpi.sampling.data.ExtendedDataNumber;
import org.opentrafficsim.kpi.sampling.data.ExtendedDataType;

/**
 * Contour data plot for any numerical extended trajectory data. For extended trajectory data valued with a DJUNITS type,
 * sub-class {@link UnitPlot} is available.
 * <p>
 * Copyright (c) 2026-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 Wouter Schakel
 * @param <Z> value type
 */
public class ContourPlotExtendedData<Z extends Number> extends AbstractContourPlot<Z>
{

    /**
     * Constructor with default paint scale (red at minimum, yellow at mid-point, green at maximum).
     * @param caption caption
     * @param source data source
     * @param extendedDataType extended data type
     * @param valueConverter value converter
     * @param bounds paint scale bounds
     * @param labelData label data
     */
    public ContourPlotExtendedData(final String caption, final ContourDataSource source,
            final ExtendedDataNumber<?> extendedDataType, final Function<Double, Z> valueConverter, final Bounds<Z> bounds,
            final LabelData<Z> labelData)
    {
        super(caption, source, constructDataType(extendedDataType, valueConverter), bounds, labelData);
    }

    /**
     * Constructor with specified paint scale.
     * @param caption caption
     * @param source data source
     * @param extendedDataType extended data type
     * @param valueConverter value converter
     * @param paintScale paint scale
     * @param labelData label data
     */
    public ContourPlotExtendedData(final String caption, final ContourDataSource source,
            final ExtendedDataNumber<?> extendedDataType, final Function<Double, Z> valueConverter,
            final BoundsPaintScale paintScale, final LabelData<Z> labelData)
    {
        super(caption, source, constructDataType(extendedDataType, valueConverter), paintScale, labelData);
    }

    /**
     * This method is a total hack, as super will call getContourDataType() which will return this.contourDataType, which at
     * that point has not been set. So we set one statically before it is called, by forwarding the caption argument through
     * this method.
     * @param <Z> value type
     * @param extendedDataType extended data type
     * @param valueConverter value converter
     * @return contour data type
     */
    private static <Z extends Number> ExtendedContourDataType<Z> constructDataType(final ExtendedDataNumber<?> extendedDataType,
            final Function<Double, Z> valueConverter)
    {
        Quantity<Z, double[][]> quantity = Quantity.si("extended_data_" + extendedDataType.getId());
        return new ExtendedContourDataType<>(extendedDataType, quantity, valueConverter);
    }

    @Override
    public GraphType getGraphType()
    {
        return GraphType.OTHER;
    }

    @Override
    protected double scale(final double si)
    {
        return si;
    }

    @Override
    public String toString()
    {
        return "ContourPlotExtendedData [" + getCaption() + "]";
    }

    /**
     * Attention contour data type.
     * @param <Z> value type
     */
    private static class ExtendedContourDataType<Z extends Number>
            implements ContourAdditionalDataType<Z, ArithmeticMean<Double, Double>>
    {

        /** Extended data type. */
        private final ExtendedDataType<?, ? extends float[], ?, ?> dataType;

        /** Quantity. */
        private final Quantity<Z, ?> quantity;

        /** Value converter. */
        private final Function<Double, Z> valueConverter;

        /**
         * Constructor.
         * @param dataType extended data type
         * @param quantity quantity
         * @param valueConverter value converter
         */
        ExtendedContourDataType(final ExtendedDataType<?, ? extends float[], ?, ?> dataType, final Quantity<Z, ?> quantity,
                final Function<Double, Z> valueConverter)
        {
            this.dataType = dataType;
            this.quantity = quantity;
            this.valueConverter = valueConverter;
        }

        @Override
        public ArithmeticMean<Double, Double> identity()
        {
            return new ArithmeticMean<>();
        }

        @Override
        public ArithmeticMean<Double, Double> processSeries(final ArithmeticMean<Double, Double> intermediate,
                final List<TrajectoryGroup<?>> trajectories, final List<Length> xFrom, final List<Length> xTo,
                final Duration tFrom, final Duration tTo)
        {
            for (int i = 0; i < trajectories.size(); i++)
            {
                TrajectoryGroup<?> trajectoryGroup = trajectories.get(i);
                for (Trajectory<?> trajectory : trajectoryGroup.getTrajectories())
                {
                    if (GraphUtil.considerTrajectory(trajectory, tFrom, tTo))
                    {
                        trajectory = trajectory.subSet(xFrom.get(i), xTo.get(i), tFrom, tTo);
                        try
                        {
                            ContourDataType.weightedNaN(trajectory.getExtendedData(this.dataType), trajectory.getX(),
                                    intermediate);
                        }
                        catch (SamplingException ex)
                        {
                            throw new RuntimeException(ex);
                        }
                    }
                }
            }
            return intermediate;
        }

        @Override
        public Z finalize(final ArithmeticMean<Double, Double> intermediate)
        {
            return this.valueConverter.apply(intermediate.getMean());
        }

        @Override
        public Quantity<Z, ?> getQuantity()
        {
            return this.quantity;
        }

        @Override
        public boolean normalize()
        {
            return false;
        }

    };

    /**
     * Extension of {@link ContourPlotExtendedData} for DJUNITS data types. The legend format will be <code>"%.1f{unit}"</code>.
     * The label format will be <code>"{quantity} %.2f {unit}"</code>. A last digit in the unit will be made in to a
     * superscript.
     * @param <U> unit type
     * @param <Z> unit value type
     */
    public static class UnitPlot<U extends Unit<U>, Z extends DoubleScalarRel<U, Z>> extends ContourPlotExtendedData<Z>
    {

        /** Character codes for superscripts 0-9. */
        private static final char[] SUPER =
                {'\u2070', '\u00B9', '\u00B2', '\u00B3', '\u2074', '\u2075', '\u2076', '\u2077', '\u2078', '\u2079'};

        /** One value. */
        private final Z one;

        /** Unit. */
        private final U unit;

        /**
         * Constructor with default paint scale (red at minimum, yellow at mid-point, green at maximum).
         * @param caption caption
         * @param source data source
         * @param extendedDataType extended data type
         * @param bounds paint scale bounds
         * @param legendStep legend step
         * @param unit unit to display values in
         */
        public UnitPlot(final String caption, final ContourDataSource source, final ExtendedDataNumber<?> extendedDataType,
                final Bounds<Z> bounds, final Z legendStep, final U unit)
        {
            super(caption, source, extendedDataType, getConverter(legendStep), bounds, constructLabelData(legendStep, unit));
            this.one = legendStep.divide(legendStep.si);
            this.unit = unit;
        }

        /**
         * Constructor with specified paint scale.
         * @param caption caption
         * @param source data source
         * @param extendedDataType extended data type
         * @param paintScale paint scale
         * @param legendStep legend step
         * @param unit unit to display values in
         */
        public UnitPlot(final String caption, final ContourDataSource source, final ExtendedDataNumber<?> extendedDataType,
                final BoundsPaintScale paintScale, final Z legendStep, final U unit)
        {
            super(caption, source, extendedDataType, getConverter(legendStep), paintScale,
                    constructLabelData(legendStep, unit));
            this.one = legendStep.divide(legendStep.si);
            this.unit = unit;
        }

        /**
         * Constructs value converter.
         * @param <U> unit type
         * @param <Z> unit value type
         * @param legendStep legend step
         * @return value converter
         */
        private static <U extends Unit<U>, Z extends DoubleScalarRel<U, Z>> Function<Double, Z> getConverter(final Z legendStep)
        {
            // TODO: use unit to create value with new DJUNITS version
            Throw.whenNull(legendStep, "legendStep");
            Z one = legendStep.divide(legendStep.si);
            return (v) -> one.times(v);
        }

        /**
         * Constructs label data. The legend format will be <code>"%.1f{unit}"</code>. The label format will be
         * <code>"{quantity} %.2f {unit}"</code>. A last digit in the unit will be made in to a superscript.
         * @param <U> unit type
         * @param <Z> unit value type
         * @param legendStep legend step
         * @param unit unit
         * @return label data
         */
        private static <U extends Unit<U>, Z extends DoubleScalarRel<U, Z>> LabelData<Z> constructLabelData(final Z legendStep,
                final U unit)
        {
            String unitString = unit.getId();
            // superscript last digit
            if (unitString != null && !unitString.isEmpty())
            {
                int last = unitString.length() - 1;
                char c = unitString.charAt(last);
                if (Character.isDigit(c))
                {
                    unitString = unitString.substring(0, last) + SUPER[c - '0'];
                }
            }
            return new LabelData<>(legendStep, "%.1f" + unitString,
                    "%.2f " + unit.getQuantity().getName().toLowerCase() + " " + unitString);
        }

        @Override
        protected double scale(final double si)
        {
            // TODO: use unit to create value with new DJUNITS version
            return this.one.times(si).getInUnit(this.unit);
        }

    }
}