PlotDelegate.java

package org.opentrafficsim.animation.graphs;

import java.util.LinkedHashSet;
import java.util.Set;

import org.djunits.value.vdouble.scalar.Duration;
import org.djutils.event.LocalEventProducer;
import org.djutils.immutablecollections.ImmutableLinkedHashSet;
import org.djutils.immutablecollections.ImmutableSet;
import org.opentrafficsim.animation.graphs.AbstractPlot.PaintState;

/**
 * Plot delegate. This class functions as a template for a data source that is shared among different plots. To keep logic local
 * the delegate is intended as an internal state of a plot. Any changes to settings should occur through the plots, and not
 * directly on the delegate. Typical usage is:
 * <ul>
 * <li>Each plot receives the {@link PlotDelegate} in its constructor and requests from it an initial update interval, delay,
 * and {@link PlotScheduler}.</li>
 * <li>UI events should not directly call the delegate, but only plot objects. When a setting is changed through UI, the plot
 * should be invoked, and the plot should call a method on the delegate to set the relevant setting.</li>
 * <li>When a setting is changed on the delegate that invalidates the whole time span, {@link #invalidateTimeSpan} should be
 * invoked by the method of the delegate that changes the setting.</li>
 * <li>The delegate method should also fire an event defined to indicate the setting change to all UI components that reflect
 * its value.</li>
 * <li>UI elements should listen for the event by calling a method on the plot that adds the listener to the delegate through
 * {@link #addListener}.</li>
 * <li>When {@link AbstractPlot#calculatePaintState} is called on a plot that uses a delegate, it can simply call
 * {@link #calculatePaintStateSafe} on the delegate.</li>
 * <li>If a setting is changed for which the update interval should change, the delegate should invoke
 * {@link AbstractPlot#offerUpdateInterval} on all plots using {@link #getPlots()}.</li>
 * <li>To know whether all of the time span needs to be calculated the calculation method can use
 * {@link #getAndResetInvalidTimeSpan}.</li>
 * <li>To know whether (expensive) calculations can be abandoned as a setting was changed that invalidated the whole time span,
 * the calculation method can use {@link #isInvalidTimeSpan}.</li>
 * <li>The delegate should calculate all state(s) and offer them to the relevant plots using {@link #getPlots()} and
 * {@link AbstractPlot#offerPaintState}.</li>
 * </ul>
 * Notes on synchronization:
 * <ul>
 * <li>Implementations need to synchronize parts that read and write settings, as different threads may access them.</li>
 * <li>Synchronization should be otherwise minimized to prevent a slow UI or delayed calculations. For example when setting the
 * {@code smooth} setting:
 *
 * <pre>
 * public void setSmooth(final boolean smooth)
 * {
 *     synchronized (this)
 *     {
 *         this.smooth = smooth;
 *         invalidateTimeSpan();
 *     }
 *     fireEvent(SMOOTH, smooth);
 * }
 * </pre>
 *
 * </li>
 * <li>Calculation of the paint state should not be class-level synchronized; that would make the UI have to wait on
 * calculations. Method {@link #calculatePaintStateSafe} makes sure a separate lock prevents parallel calculations.</li>
 * <li>Calculations are based on settings. These settings need to be gathered at class-level synchronization, which then needs
 * to be released for the actual calculations. This should occur in {@link #calculatePaintStateUnsafe}. For example:
 *
 * <pre>
 * public void calculatePaintStateUnsafe(final Duration time)
 * {
 *     boolean smooth0;
 *     synchronized (this) // obtain settings safely
 *     {
 *         smooth0 = this.smooth;
 *     }
 *
 *     // do calculations ...
 *
 *     for (FundamentalDiagram plot : getPlots())
 *     {
 *         plot.offerPaintState(paintState);
 *     }
 * }
 * </pre>
 *
 * </li>
 * </ul>
 * <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 <S> paint state for the plot(s)
 * @param <P> plot type
 */
public abstract class PlotDelegate<S extends PaintState, P extends AbstractPlot<S>> extends LocalEventProducer
{

    /** Initial update interval. */
    private final Duration initialUpdateInterval;

    /** Delay so critical future events have occurred, e.g. GTU's next move's to extend trajectories. */
    private final Duration delay;

    /** Plot scheduler. */
    private PlotScheduler plotScheduler;

    /** Plots. */
    private final Set<P> plots = new LinkedHashSet<>();

    /** Whether the whole time span is invalid. */
    private volatile boolean invalidTimeSpan = true;

    /** Lock to prevent simultaneous calculations. */
    private final Object calculationLock = new Object();

    /**
     * Constructor.
     * @param initialUpdateInterval initial update interval
     * @param delay delay so critical future events have occurred, e.g. GTU's next move's to extend trajectories
     * @param plotScheduler plot scheduler
     */
    public PlotDelegate(final Duration initialUpdateInterval, final Duration delay, final PlotScheduler plotScheduler)
    {
        this.initialUpdateInterval = initialUpdateInterval;
        this.delay = delay;
        this.plotScheduler = plotScheduler;
    }

    /**
     * Returns the update interval for a plot using this delegate.
     * @return update interval
     */
    public Duration getInitialUpdateInterval()
    {
        return this.initialUpdateInterval;
    }

    /**
     * Returns the delay for a plot using this delegate.
     * @return delay
     */
    public Duration getDelay()
    {
        return this.delay;
    }

    /**
     * Returns the plot scheduler for the first plot that requests one. This plot will be in charge of the updates. All other
     * plots will receive a plot scheduler that will ignore the scheduling of update events.
     * @return plot scheduler
     */
    public PlotScheduler getPlotScheduler()
    {
        PlotScheduler out = this.plotScheduler;
        this.plotScheduler = new PlotScheduler()
        {
            @Override
            public Duration getTime()
            {
                return out.getTime();
            }
        };
        return out;
    }

    /**
     * Add plot. Used to notify plots when data has changed.
     * @param plot plot
     */
    public void addPlot(final P plot)
    {
        this.plots.add(plot);
    }

    /**
     * Clears all connected plots.
     */
    public void clearPlots()
    {
        this.plots.clear();
    }

    /**
     * Returns the plots.
     * @return plots
     */
    public ImmutableSet<P> getPlots()
    {
        return new ImmutableLinkedHashSet<>(this.plots);
    }

    /**
     * Invalidates the whole time span.
     */
    public synchronized void invalidateTimeSpan()
    {
        this.invalidTimeSpan = true;
    }

    /**
     * Returns whether the time span is invalid. This can indicate that calculations can be stopped as some setting was changed
     * that invalidated the time span.
     * @return whether the time span is invalid
     */
    public boolean isInvalidTimeSpan()
    {
        return this.invalidTimeSpan;
    }

    /**
     * Returns whether the whole time span is invalid, and resets this information.
     * @return whether the whole time span is invalid
     */
    public synchronized boolean getAndResetInvalidTimeSpan()
    {
        boolean out = this.invalidTimeSpan;
        this.invalidTimeSpan = false;
        return out;
    }

    /**
     * Invokes {@link #calculatePaintStateUnsafe} in a thread-safe manner. This method should be invoked by plots that use a
     * delegate when the plot is asked to calculate the paint state.
     * @param time current time
     */
    public void calculatePaintStateSafe(final Duration time)
    {
        // worker thread from one plot may call this while another is still calculating
        synchronized (this.calculationLock)
        {
            calculatePaintStateUnsafe(time);
        }
    }

    /**
     * Calculates paint state and offers it to the coupled plots. This method should only be invoked by
     * {@link #calculatePaintStateSafe} which makes sure that setting changes from different plots (with different working
     * threads) do not cause parallel calculations on the same delegate. This makes sure that internal data gathering can occur
     * consistently.
     * @param time current time
     */
    protected abstract void calculatePaintStateUnsafe(Duration time);

}