View Javadoc
1   package org.opentrafficsim.animation.graphs;
2   
3   import java.awt.Color;
4   import java.awt.Font;
5   import java.awt.Graphics2D;
6   import java.awt.geom.AffineTransform;
7   import java.awt.geom.Rectangle2D;
8   import java.awt.image.BufferedImage;
9   import java.io.IOException;
10  import java.util.ArrayList;
11  import java.util.LinkedHashSet;
12  import java.util.List;
13  import java.util.Set;
14  import java.util.UUID;
15  import java.util.concurrent.BlockingQueue;
16  import java.util.concurrent.CompletableFuture;
17  import java.util.concurrent.LinkedBlockingQueue;
18  import java.util.concurrent.atomic.AtomicBoolean;
19  import java.util.concurrent.atomic.AtomicReference;
20  
21  import javax.swing.SwingUtilities;
22  
23  import org.djunits.value.vdouble.scalar.Duration;
24  import org.djutils.base.Identifiable;
25  import org.djutils.event.EventType;
26  import org.djutils.metadata.MetaData;
27  import org.djutils.metadata.ObjectDescriptor;
28  import org.jfree.chart.ChartUtils;
29  import org.jfree.chart.JFreeChart;
30  import org.jfree.chart.plot.XYPlot;
31  import org.jfree.chart.title.TextTitle;
32  import org.jfree.data.general.Dataset;
33  import org.jfree.data.general.DatasetChangeEvent;
34  import org.jfree.data.general.DatasetChangeListener;
35  import org.jfree.data.general.DatasetGroup;
36  import org.opentrafficsim.animation.graphs.AbstractPlot.PaintState;
37  import org.opentrafficsim.base.logger.Logger;
38  
39  /**
40   * Super class of all plots. This schedules regular updates, creates menus and deals with listeners. There are a number of
41   * methods for sub-classes to implement.
42   * <p>
43   * Copyright (c) 2013-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
44   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
45   * </p>
46   * @author Alexander Verbraeck
47   * @author Peter Knoppers
48   * @author Wouter Schakel
49   * @param <S> paint state implementation
50   */
51  public abstract class AbstractPlot<S extends PaintState> implements Identifiable, Dataset
52  {
53  
54      /**
55       * The (regular, not timed) event type for pub/sub indicating the addition of a graph. Not used internally.<br>
56       * Payload: String graph caption (not an array, just a String)
57       */
58      public static final EventType GRAPH_ADD_EVENT = new EventType("GRAPH.ADD",
59              new MetaData("Graph add", "Graph added", new ObjectDescriptor("Graph id", "Id of the graph", String.class)));
60  
61      /**
62       * The (regular, not timed) event type for pub/sub indicating the removal of a graph. Not used internally.<br>
63       * Payload: String Graph caption (not an array, just a String)
64       */
65      public static final EventType GRAPH_REMOVE_EVENT = new EventType("GRAPH.REMOVE",
66              new MetaData("Graph remove", "Graph removed", new ObjectDescriptor("Graph id", "Id of the graph", String.class)));
67  
68      /** Initial upper bound for the time scale. */
69      public static final Duration DEFAULT_INITIAL_UPPER_TIME_BOUND = Duration.ofSI(300.0);
70  
71      /** Scheduler. */
72      private final PlotScheduler scheduler;
73  
74      /** Unique ID of the chart. */
75      private final String id = UUID.randomUUID().toString();
76  
77      /** Caption. */
78      private final String caption;
79  
80      /** The chart, so we can export it. */
81      private JFreeChart chart;
82  
83      /** List of parties interested in changes of this plot. */
84      private Set<DatasetChangeListener> listeners = new LinkedHashSet<>();
85  
86      /** Delay so critical future events have occurred, e.g. GTU's next move to extend trajectories. */
87      private final Duration delay;
88  
89      /** Update interval. */
90      private Duration updateInterval;
91  
92      /** New update interval to use. */
93      private volatile Duration suggestedUpdateInterval;
94  
95      /** Queue for the worker thread. */
96      private BlockingQueue<Duration> workerQueue = new LinkedBlockingQueue<>();
97  
98      /** Current paint state. */
99      private volatile S paintState;
100 
101     /** Thread safe offered paint state. */
102     private final AtomicReference<S> pendingPaintState = new AtomicReference<>();
103 
104     /** Makes sure the paint state is only set once. */
105     private final AtomicBoolean adoptionPosted = new AtomicBoolean(false);
106 
107     /**
108      * Constructor.
109      * @param scheduler scheduler
110      * @param caption caption
111      * @param updateInterval regular update interval (simulation time)
112      * @param delay amount of time that chart runs behind simulation to prevent gaps in the charted data
113      */
114     public AbstractPlot(final PlotScheduler scheduler, final String caption, final Duration updateInterval,
115             final Duration delay)
116     {
117         this.scheduler = scheduler;
118         this.caption = caption;
119         this.updateInterval = updateInterval;
120         this.delay = delay;
121         this.paintState = emptyPaintState();
122         scheduleUpdateEvent(); // start redraw chain
123 
124         // worker thread
125         Thread invokingThread = Thread.currentThread();
126         Thread thread = new Thread(new Runnable()
127         {
128             @Override
129             public void run()
130             {
131                 while (!invokingThread.isInterrupted())
132                 {
133                     try
134                     {
135                         Duration time = AbstractPlot.this.workerQueue.take();
136                         if (time != null && AbstractPlot.this.workerQueue.isEmpty()) // only take last update request
137                         {
138                             calculatePaintState(time);
139                         }
140                     }
141                     catch (InterruptedException exception)
142                     {
143                         Logger.ots().error(exception, "Worker thread for plot {} stopped.", AbstractPlot.this.caption);
144                         break;
145                     }
146                 }
147             }
148         }, AbstractPlot.this.caption);
149         thread.setDaemon(true);
150         thread.start();
151     }
152 
153     /**
154      * Returns an empty paint state. This is used at plot initialization.
155      * @return empty paint state.
156      */
157     protected abstract S emptyPaintState();
158 
159     /**
160      * Sets the chart and adds menus and listeners.
161      * @param chart chart
162      */
163     protected void setChart(final JFreeChart chart)
164     {
165         this.chart = chart;
166 
167         // make title somewhat smaller
168         chart.setTitle(new TextTitle(chart.getTitle().getText(), new Font("SansSerif", java.awt.Font.BOLD, 16)));
169 
170         // default colors and zoom behavior
171         chart.getPlot().setBackgroundPaint(Color.LIGHT_GRAY);
172         chart.setBackgroundPaint(Color.WHITE);
173         if (chart.getPlot() instanceof XYPlot)
174         {
175             chart.getXYPlot().setDomainGridlinePaint(Color.WHITE);
176             chart.getXYPlot().setRangeGridlinePaint(Color.WHITE);
177         }
178     }
179 
180     /**
181      * Returns the chart as a byte array representing a PNG image.
182      * @param width width
183      * @param height height
184      * @param fontSize font size (16 is the original on screen size)
185      * @return the chart as a byte array representing a PNG image
186      * @throws IOException on IO exception
187      */
188     public byte[] encodeAsPng(final int width, final int height, final double fontSize) throws IOException
189     {
190         // to double the font size, we halve the base dimensions
191         // JFreeChart will the assign more area (relatively) to the fixed actual font size
192         double baseWidth = width / (fontSize / 16);
193         double baseHeight = height / (fontSize / 16);
194         // this code is from ChartUtils.writeScaledChartAsPNG
195         BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
196         Graphics2D g2 = image.createGraphics();
197         // to compensate for the base dimensions which are not w x h, we scale the drawing
198         AffineTransform saved = g2.getTransform();
199         g2.transform(AffineTransform.getScaleInstance(width / baseWidth, height / baseHeight));
200         getChart().draw(g2, new Rectangle2D.Double(0, 0, baseWidth, baseHeight), null, null);
201         g2.setTransform(saved);
202         g2.dispose();
203         return ChartUtils.encodeAsPNG(image);
204     }
205 
206     @Override
207     public final DatasetGroup getGroup()
208     {
209         return null; // not used
210     }
211 
212     @Override
213     public final void setGroup(final DatasetGroup group)
214     {
215         // not used
216     }
217 
218     /**
219      * Overridable; activates auto bounds on domain axis from user input. This class does not force the use of {@link XYPlot}s,
220      * but the auto bounds command comes from the {@code ChartPanel} that shows this plot. In case the used plot is a
221      * {@link XYPlot}, this method is then invoked. Sub classes with auto domain bounds that work with an {@link XYPlot} should
222      * implement this. The method is not abstract as the use of {@code XYPlot} is not obligated.
223      * @param plot plot
224      */
225     public void setAutoBoundDomain(final XYPlot plot)
226     {
227         throw new UnsupportedOperationException("Plot is a XYPlot but does not implement setAutoBoundDomain");
228     }
229 
230     /**
231      * Overridable; activates auto bounds on range axis from user input. This class does not force the use of {@link XYPlot}s,
232      * but the auto bounds command comes from the {@code ChartPanel} that shows this plot. In case the used plot is a
233      * {@link XYPlot}, this method is then invoked. Sub classes with auto range bounds that work with an {@link XYPlot} should
234      * implement this. The method is not abstract as the use of {@code XYPlot} is not obligated.
235      * @param plot plot
236      */
237     public void setAutoBoundRange(final XYPlot plot)
238     {
239         throw new UnsupportedOperationException("Plot is a XYPlot but does not implement setAutoBoundRange");
240     }
241 
242     /**
243      * Return the graph type for transceiver.
244      * @return the graph type.
245      */
246     public abstract GraphType getGraphType();
247 
248     /**
249      * Returns the status label when the mouse is over the given location.
250      * @param domainValue domain value (x-axis)
251      * @param rangeValue range value (y-axis)
252      * @return status label when the mouse is over the given location
253      */
254     public abstract String getStatusLabel(double domainValue, double rangeValue);
255 
256     /**
257      * Returns the chart.
258      * @return chart
259      */
260     public JFreeChart getChart()
261     {
262         return this.chart;
263     }
264 
265     @Override
266     public String getId()
267     {
268         return this.id;
269     }
270 
271     /**
272      * Retrieve the caption.
273      * @return the caption of the plot
274      */
275     public String getCaption()
276     {
277         return this.caption;
278     }
279 
280     // ===== Listeners =====
281 
282     @Override
283     public void addChangeListener(final DatasetChangeListener listener)
284     {
285         this.listeners.add(listener);
286     }
287 
288     @Override
289     public void removeChangeListener(final DatasetChangeListener listener)
290     {
291         this.listeners.remove(listener);
292     }
293 
294     /**
295      * Notify all change listeners.
296      */
297     public void notifyPlotChange()
298     {
299         // take a snapshot to avoid concurrent modification during iteration
300         final List<DatasetChangeListener> snapshot;
301         synchronized (this)
302         {
303             snapshot = new ArrayList<>(this.listeners);
304         }
305 
306         Runnable r = () ->
307         {
308             DatasetChangeEvent event = new DatasetChangeEvent(this, this);
309             for (DatasetChangeListener dcl : snapshot)
310             {
311                 dcl.datasetChanged(event);
312             }
313         };
314 
315         // invoke only on Swing EDT
316         if (SwingUtilities.isEventDispatchThread())
317         {
318             r.run();
319         }
320         else
321         {
322             SwingUtilities.invokeLater(r);
323         }
324     }
325 
326     // ===== Paint state =====
327 
328     /**
329      * Requests a calculation of the paint state. May be invoked by sub-classes whenever a setting was changed that needs a
330      * recalculation.
331      */
332     protected void invalidate()
333     {
334         this.workerQueue.offer(this.scheduler.getTime());
335     }
336 
337     /**
338      * Calculates the paint state object and offers it through {@link #offerPaintState}, or delegates this work to a delegate.
339      * This method is invoked by the worker thread and can thus perform heavy calculations outside of the Swing EDT.
340      * Intermediate paint states during long calculations may also be offered. It is up to the implementation to either
341      * calculate a complete paint state, or cumulatively built on the results from previous calls. It is also up to the
342      * implementation to know when the whole time span needs to be recalculated due to property changes.
343      * @param time time until which data in the paint state should be calculated
344      */
345     protected abstract void calculatePaintState(Duration time);
346 
347     /**
348      * Offer new paint state. This method can be invoked by any thread, and will make sure the actual setting of the paint state
349      * will occur on the Swing EDT. This assures that no paint state is changed as Swing is painting (i.e. as the plot is asked
350      * for data to paint). Listeners are notified on the Swing EDT as soon as the paint state has been set.
351      * @param paintState paint state
352      */
353     @SuppressWarnings("hiddenfield")
354     public void offerPaintState(final S paintState)
355     {
356         Logger.ots().trace("Offering paint state on plot: {}", this.caption);
357         this.pendingPaintState.set(paintState);
358         if (this.adoptionPosted.compareAndSet(false, true))
359         {
360             if (SwingUtilities.isEventDispatchThread())
361             {
362                 setPaintState();
363             }
364             else
365             {
366                 SwingUtilities.invokeLater(() -> setPaintState());
367             }
368         }
369     }
370 
371     /**
372      * Sets the paint state in a thread safe manner and notifies the listeners. This method is always invoked on the Swing EDT.
373      * This method may be overridden to use a newly set paint state (after calling {@code super.setPaintState()}) to set
374      * internal properties. For example, setting the block size of an internal block renderer based on the granularity of the
375      * data.
376      */
377     protected void setPaintState()
378     {
379         Logger.ots().trace("Setting paint state on plot: {}", this.caption);
380         try
381         {
382             S s = this.pendingPaintState.getAndSet(null);
383             if (s != null)
384             {
385                 // single point where the visible paint state changes
386                 this.paintState = s;
387                 // notify on Swing EDT; painting will occur after this completes
388                 Logger.ots().trace("Notifying plot changed: {}", this.caption);
389                 notifyPlotChange();
390             }
391         }
392         finally
393         {
394             this.adoptionPosted.set(false);
395         }
396     }
397 
398     /**
399      * Returns the current paint state that should be used to return paint data (i.e. x-values, etc.)
400      * @return current paint state
401      */
402     protected S getPaintState()
403     {
404         return this.paintState;
405     }
406 
407     /**
408      * Returns up to what time data is available for painting.
409      * @return up to what time data is available for painting
410      */
411     public Duration getAvailableTime()
412     {
413         return this.paintState.getAvailableTime();
414     }
415 
416     // ===== Update chain =====
417 
418     /**
419      * Suggests a new update interval. This does not affect any time granularity, but only when update events occur. This method
420      * will ask the {@link PlotScheduler} to schedule an update now. The next update will set the update interval and schedule
421      * the next regular update aligning with the new interval. If an update is also desired right now {@link #invalidate} needs
422      * to be invoked. This method is typically called on the Swing EDT and will request the scheduler asynchronously. This
423      * method does not block.
424      * @param interval update interval
425      */
426     public void offerUpdateInterval(final Duration interval)
427     {
428         this.suggestedUpdateInterval = interval;
429         // run asynchronous because we do not want the Swing EDT to wait for the simulation thread semaphore
430         CompletableFuture.runAsync(() ->
431         {
432             this.scheduler.scheduleUpdateNow(this); // divert to scheduling thread
433         });
434     }
435 
436     /*
437      * Implementation note: A specific problem is prevented by using offerUpdateInterval() to schedule an update, and update()
438      * to then take up the new update interval. When the interval is changed, the next time to schedule an update needs to be
439      * determined. If this is done by offerUpdateInterval() by taking the current simulation time and adding a delta within the
440      * Swing EDT thread, the scheduler thread may progress time beyond the resulting update time before the event is actually
441      * scheduled. Only the scheduler thread should be in control of time and update event scheduling. Furthermore the scheduling
442      * is parallelized to allow the Swing EDT to not wait for a potentially very busy simulator thread.
443      */
444 
445     /**
446      * Requests the worker thread to perform calculations up to the current time and (in the Swing EDT thread) update the plots.
447      * The worker thread will take on this request once any current calculations are done. If multiple updates are requested
448      * before the worker thread is done, only the update with latest time is executed. This method should only be invoked by the
449      * thread that governs time (typically by the {@link PlotScheduler}). Otherwise events may be erroneously scheduled in the
450      * past.
451      * <p>
452      * After a new update interval was suggested through {@link #offerUpdateInterval} this method does not do the above, but
453      * instead only schedules the next update aligning with the new interval.
454      */
455     public void update()
456     {
457         if (this.updateInterval != null && this.suggestedUpdateInterval != null
458                 && !this.updateInterval.equals(this.suggestedUpdateInterval))
459         {
460             // take up new update interval and reset 'updates' to fall in alignment
461             this.updateInterval = this.suggestedUpdateInterval;
462             this.suggestedUpdateInterval = null;
463         }
464         else
465         {
466             invalidate();
467         }
468         scheduleUpdateEvent();
469     }
470 
471     /**
472      * Schedules the next update event.
473      */
474     private void scheduleUpdateEvent()
475     {
476         double t = this.scheduler.getTime().si;
477         int n = (int) (t / this.updateInterval.si) + 1; // robust to accidental duplicate/out-of-tempo updates
478         // events are scheduled slightly later, so all influencing movements have occurred
479         double tNext = this.updateInterval.si * n + this.delay.si;
480         if (tNext <= t)
481         {
482             tNext += this.updateInterval.si;
483         }
484         this.scheduler.scheduleUpdate(Duration.ofSI(tNext), this);
485     }
486 
487     /**
488      * Interface for paint state objects.
489      */
490     interface PaintState
491     {
492 
493         /**
494          * Returns up to what time data is available for painting.
495          * @return up to what time data is available for painting
496          */
497         Duration getAvailableTime();
498 
499     }
500 
501 }