View Javadoc
1   package org.opentrafficsim.animation.graphs;
2   
3   import java.util.LinkedHashSet;
4   import java.util.Set;
5   
6   import org.djunits.value.vdouble.scalar.Duration;
7   import org.djutils.event.LocalEventProducer;
8   import org.djutils.immutablecollections.ImmutableLinkedHashSet;
9   import org.djutils.immutablecollections.ImmutableSet;
10  import org.opentrafficsim.animation.graphs.AbstractPlot.PaintState;
11  
12  /**
13   * Plot delegate. This class functions as a template for a data source that is shared among different plots. To keep logic local
14   * the delegate is intended as an internal state of a plot. Any changes to settings should occur through the plots, and not
15   * directly on the delegate. Typical usage is:
16   * <ul>
17   * <li>Each plot receives the {@link PlotDelegate} in its constructor and requests from it an initial update interval, delay,
18   * and {@link PlotScheduler}.</li>
19   * <li>UI events should not directly call the delegate, but only plot objects. When a setting is changed through UI, the plot
20   * should be invoked, and the plot should call a method on the delegate to set the relevant setting.</li>
21   * <li>When a setting is changed on the delegate that invalidates the whole time span, {@link #invalidateTimeSpan} should be
22   * invoked by the method of the delegate that changes the setting.</li>
23   * <li>The delegate method should also fire an event defined to indicate the setting change to all UI components that reflect
24   * its value.</li>
25   * <li>UI elements should listen for the event by calling a method on the plot that adds the listener to the delegate through
26   * {@link #addListener}.</li>
27   * <li>When {@link AbstractPlot#calculatePaintState} is called on a plot that uses a delegate, it can simply call
28   * {@link #calculatePaintStateSafe} on the delegate.</li>
29   * <li>If a setting is changed for which the update interval should change, the delegate should invoke
30   * {@link AbstractPlot#offerUpdateInterval} on all plots using {@link #getPlots()}.</li>
31   * <li>To know whether all of the time span needs to be calculated the calculation method can use
32   * {@link #getAndResetInvalidTimeSpan}.</li>
33   * <li>To know whether (expensive) calculations can be abandoned as a setting was changed that invalidated the whole time span,
34   * the calculation method can use {@link #isInvalidTimeSpan}.</li>
35   * <li>The delegate should calculate all state(s) and offer them to the relevant plots using {@link #getPlots()} and
36   * {@link AbstractPlot#offerPaintState}.</li>
37   * </ul>
38   * Notes on synchronization:
39   * <ul>
40   * <li>Implementations need to synchronize parts that read and write settings, as different threads may access them.</li>
41   * <li>Synchronization should be otherwise minimized to prevent a slow UI or delayed calculations. For example when setting the
42   * {@code smooth} setting:
43   *
44   * <pre>
45   * public void setSmooth(final boolean smooth)
46   * {
47   *     synchronized (this)
48   *     {
49   *         this.smooth = smooth;
50   *         invalidateTimeSpan();
51   *     }
52   *     fireEvent(SMOOTH, smooth);
53   * }
54   * </pre>
55   *
56   * </li>
57   * <li>Calculation of the paint state should not be class-level synchronized; that would make the UI have to wait on
58   * calculations. Method {@link #calculatePaintStateSafe} makes sure a separate lock prevents parallel calculations.</li>
59   * <li>Calculations are based on settings. These settings need to be gathered at class-level synchronization, which then needs
60   * to be released for the actual calculations. This should occur in {@link #calculatePaintStateUnsafe}. For example:
61   *
62   * <pre>
63   * public void calculatePaintStateUnsafe(final Duration time)
64   * {
65   *     boolean smooth0;
66   *     synchronized (this) // obtain settings safely
67   *     {
68   *         smooth0 = this.smooth;
69   *     }
70   *
71   *     // do calculations ...
72   *
73   *     for (FundamentalDiagram plot : getPlots())
74   *     {
75   *         plot.offerPaintState(paintState);
76   *     }
77   * }
78   * </pre>
79   *
80   * </li>
81   * </ul>
82   * <p>
83   * Copyright (c) 2026-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
84   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
85   * </p>
86   * @author Wouter Schakel
87   * @param <S> paint state for the plot(s)
88   * @param <P> plot type
89   */
90  public abstract class PlotDelegate<S extends PaintState, P extends AbstractPlot<S>> extends LocalEventProducer
91  {
92  
93      /** Initial update interval. */
94      private final Duration initialUpdateInterval;
95  
96      /** Delay so critical future events have occurred, e.g. GTU's next move's to extend trajectories. */
97      private final Duration delay;
98  
99      /** Plot scheduler. */
100     private PlotScheduler plotScheduler;
101 
102     /** Plots. */
103     private final Set<P> plots = new LinkedHashSet<>();
104 
105     /** Whether the whole time span is invalid. */
106     private volatile boolean invalidTimeSpan = true;
107 
108     /** Lock to prevent simultaneous calculations. */
109     private final Object calculationLock = new Object();
110 
111     /**
112      * Constructor.
113      * @param initialUpdateInterval initial update interval
114      * @param delay delay so critical future events have occurred, e.g. GTU's next move's to extend trajectories
115      * @param plotScheduler plot scheduler
116      */
117     public PlotDelegate(final Duration initialUpdateInterval, final Duration delay, final PlotScheduler plotScheduler)
118     {
119         this.initialUpdateInterval = initialUpdateInterval;
120         this.delay = delay;
121         this.plotScheduler = plotScheduler;
122     }
123 
124     /**
125      * Returns the update interval for a plot using this delegate.
126      * @return update interval
127      */
128     public Duration getInitialUpdateInterval()
129     {
130         return this.initialUpdateInterval;
131     }
132 
133     /**
134      * Returns the delay for a plot using this delegate.
135      * @return delay
136      */
137     public Duration getDelay()
138     {
139         return this.delay;
140     }
141 
142     /**
143      * Returns the plot scheduler for the first plot that requests one. This plot will be in charge of the updates. All other
144      * plots will receive a plot scheduler that will ignore the scheduling of update events.
145      * @return plot scheduler
146      */
147     public PlotScheduler getPlotScheduler()
148     {
149         PlotScheduler out = this.plotScheduler;
150         this.plotScheduler = new PlotScheduler()
151         {
152             @Override
153             public Duration getTime()
154             {
155                 return out.getTime();
156             }
157         };
158         return out;
159     }
160 
161     /**
162      * Add plot. Used to notify plots when data has changed.
163      * @param plot plot
164      */
165     public void addPlot(final P plot)
166     {
167         this.plots.add(plot);
168     }
169 
170     /**
171      * Clears all connected plots.
172      */
173     public void clearPlots()
174     {
175         this.plots.clear();
176     }
177 
178     /**
179      * Returns the plots.
180      * @return plots
181      */
182     public ImmutableSet<P> getPlots()
183     {
184         return new ImmutableLinkedHashSet<>(this.plots);
185     }
186 
187     /**
188      * Invalidates the whole time span.
189      */
190     public synchronized void invalidateTimeSpan()
191     {
192         this.invalidTimeSpan = true;
193     }
194 
195     /**
196      * Returns whether the time span is invalid. This can indicate that calculations can be stopped as some setting was changed
197      * that invalidated the time span.
198      * @return whether the time span is invalid
199      */
200     public boolean isInvalidTimeSpan()
201     {
202         return this.invalidTimeSpan;
203     }
204 
205     /**
206      * Returns whether the whole time span is invalid, and resets this information.
207      * @return whether the whole time span is invalid
208      */
209     public synchronized boolean getAndResetInvalidTimeSpan()
210     {
211         boolean out = this.invalidTimeSpan;
212         this.invalidTimeSpan = false;
213         return out;
214     }
215 
216     /**
217      * Invokes {@link #calculatePaintStateUnsafe} in a thread-safe manner. This method should be invoked by plots that use a
218      * delegate when the plot is asked to calculate the paint state.
219      * @param time current time
220      */
221     public void calculatePaintStateSafe(final Duration time)
222     {
223         // worker thread from one plot may call this while another is still calculating
224         synchronized (this.calculationLock)
225         {
226             calculatePaintStateUnsafe(time);
227         }
228     }
229 
230     /**
231      * Calculates paint state and offers it to the coupled plots. This method should only be invoked by
232      * {@link #calculatePaintStateSafe} which makes sure that setting changes from different plots (with different working
233      * threads) do not cause parallel calculations on the same delegate. This makes sure that internal data gathering can occur
234      * consistently.
235      * @param time current time
236      */
237     protected abstract void calculatePaintStateUnsafe(Duration time);
238 
239 }