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 }