View Javadoc
1   package org.opentrafficsim.draw.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.LinkedHashSet;
11  import java.util.Set;
12  import java.util.UUID;
13  
14  import org.djunits.value.vdouble.scalar.Duration;
15  import org.djunits.value.vdouble.scalar.Time;
16  import org.djutils.base.Identifiable;
17  import org.djutils.event.EventType;
18  import org.djutils.metadata.MetaData;
19  import org.djutils.metadata.ObjectDescriptor;
20  import org.jfree.chart.ChartUtils;
21  import org.jfree.chart.JFreeChart;
22  import org.jfree.chart.plot.XYPlot;
23  import org.jfree.chart.title.TextTitle;
24  import org.jfree.data.general.Dataset;
25  import org.jfree.data.general.DatasetChangeEvent;
26  import org.jfree.data.general.DatasetChangeListener;
27  import org.jfree.data.general.DatasetGroup;
28  
29  /**
30   * Super class of all plots. This schedules regular updates, creates menus and deals with listeners. There are a number of
31   * delegate methods for sub classes to implement.
32   * <p>
33   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
34   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
35   * </p>
36   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
37   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
38   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
39   */
40  public abstract class AbstractPlot implements Identifiable, Dataset
41  {
42  
43      /**
44       * The (regular, not timed) event type for pub/sub indicating the addition of a graph. Not used internally.<br>
45       * Payload: String graph caption (not an array, just a String)
46       */
47      public static final EventType GRAPH_ADD_EVENT = new EventType("GRAPH.ADD",
48              new MetaData("Graph add", "Graph added", new ObjectDescriptor("Graph id", "Id of the graph", String.class)));
49  
50      /**
51       * The (regular, not timed) event type for pub/sub indicating the removal of a graph. Not used internally.<br>
52       * Payload: String Graph caption (not an array, just a String)
53       */
54      public static final EventType GRAPH_REMOVE_EVENT = new EventType("GRAPH.REMOVE",
55              new MetaData("Graph remove", "Graph removed", new ObjectDescriptor("Graph id", "Id of the graph", String.class)));
56  
57      /** Initial upper bound for the time scale. */
58      public static final Time DEFAULT_INITIAL_UPPER_TIME_BOUND = Time.ofSI(300.0);
59  
60      /** Scheduler. */
61      private final PlotScheduler scheduler;
62  
63      /** Unique ID of the chart. */
64      private final String id = UUID.randomUUID().toString();
65  
66      /** Caption. */
67      private final String caption;
68  
69      /** The chart, so we can export it. */
70      private JFreeChart chart;
71  
72      /** List of parties interested in changes of this plot. */
73      private Set<DatasetChangeListener> listeners = new LinkedHashSet<>();
74  
75      /** Delay so critical future events have occurred, e.g. GTU's next move's to extend trajectories. */
76      private final Duration delay;
77  
78      /** Time of last data update. */
79      private Duration updateTime;
80  
81      /** Number of updates. */
82      private int updates = 0;
83  
84      /** Update interval. */
85      private Duration updateInterval;
86  
87      /** Whether the plot is in an update. */
88      private boolean inUpdate = false;
89  
90      /** Offered update interval to set on next update scheduling. */
91      private Duration offeredUpdateInterval;
92  
93      /**
94       * Constructor.
95       * @param scheduler scheduler.
96       * @param caption caption
97       * @param updateInterval regular update interval (simulation time)
98       * @param delay amount of time that chart runs behind simulation to prevent gaps in the charted data
99       */
100     public AbstractPlot(final PlotScheduler scheduler, final String caption, final Duration updateInterval,
101             final Duration delay)
102     {
103         this.scheduler = scheduler;
104         this.caption = caption;
105         this.updateInterval = updateInterval;
106         this.delay = delay;
107         this.updates = (int) (scheduler.getTime().si / updateInterval.si); // when creating plot during simulation
108         update(); // start redraw chain
109     }
110 
111     /**
112      * Sets the chart and adds menus and listeners.
113      * @param chart chart
114      */
115     @SuppressWarnings("methodlength")
116     protected void setChart(final JFreeChart chart)
117     {
118         this.chart = chart;
119 
120         // make title somewhat smaller
121         chart.setTitle(new TextTitle(chart.getTitle().getText(), new Font("SansSerif", java.awt.Font.BOLD, 16)));
122 
123         // default colors and zoom behavior
124         chart.getPlot().setBackgroundPaint(Color.LIGHT_GRAY);
125         chart.setBackgroundPaint(Color.WHITE);
126         if (chart.getPlot() instanceof XYPlot)
127         {
128             chart.getXYPlot().setDomainGridlinePaint(Color.WHITE);
129             chart.getXYPlot().setRangeGridlinePaint(Color.WHITE);
130         }
131 
132     }
133 
134     /**
135      * Returns the chart as a byte array representing a png image.
136      * @param width width
137      * @param height height
138      * @param fontSize font size (16 is the original on screen size)
139      * @return the chart as a byte array representing a png image
140      * @throws IOException on IO exception
141      */
142     public byte[] encodeAsPng(final int width, final int height, final double fontSize) throws IOException
143     {
144         // to double the font size, we halve the base dimensions
145         // JFreeChart will the assign more area (relatively) to the fixed actual font size
146         double baseWidth = width / (fontSize / 16);
147         double baseHeight = height / (fontSize / 16);
148         // this code is from ChartUtils.writeScaledChartAsPNG
149         BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
150         Graphics2D g2 = image.createGraphics();
151         // to compensate for the base dimensions which are not w x h, we scale the drawing
152         AffineTransform saved = g2.getTransform();
153         g2.transform(AffineTransform.getScaleInstance(width / baseWidth, height / baseHeight));
154         getChart().draw(g2, new Rectangle2D.Double(0, 0, baseWidth, baseHeight), null, null);
155         g2.setTransform(saved);
156         g2.dispose();
157         return ChartUtils.encodeAsPNG(image);
158     }
159 
160     @Override
161     public final DatasetGroup getGroup()
162     {
163         return null; // not used
164     }
165 
166     @Override
167     public final void setGroup(final DatasetGroup group)
168     {
169         // not used
170     }
171 
172     /**
173      * Overridable; activates auto bounds on domain axis from user input. This class does not force the use of {@code XYPlot}s,
174      * but the auto bounds command comes from the {@code ChartPanel} that this class creates. In case the used plot is a
175      * {@code XYPlot}, this method is then invoked. Sub classes with auto domain bounds that work with an {@code XYPlot} should
176      * implement this. The method is not abstract as the use of {@code XYPlot} is not obligated.
177      * @param plot plot
178      */
179     public void setAutoBoundDomain(final XYPlot plot)
180     {
181         //
182     }
183 
184     /**
185      * Overridable; activates auto bounds on range axis from user input. This class does not force the use of {@code XYPlot}s,
186      * but the auto bounds command comes from the {@code ChartPanel} that this class creates. In case the used plot is a
187      * {@code XYPlot}, this method is then invoked. Sub classes with auto range bounds that work with an {@code XYPlot} should
188      * implement this. The method is not abstract as the use of {@code XYPlot} is not obligated.
189      * @param plot plot
190      */
191     public void setAutoBoundRange(final XYPlot plot)
192     {
193         //
194     }
195 
196     /**
197      * Return the graph type for transceiver.
198      * @return the graph type.
199      */
200     public abstract GraphType getGraphType();
201 
202     /**
203      * Returns the status label when the mouse is over the given location.
204      * @param domainValue domain value (x-axis)
205      * @param rangeValue range value (y-axis)
206      * @return status label when the mouse is over the given location
207      */
208     public abstract String getStatusLabel(double domainValue, double rangeValue);
209 
210     /**
211      * Increase the simulated time span.
212      * @param time time to increase to
213      */
214     protected abstract void increaseTime(Duration time);
215 
216     /**
217      * Notify all change listeners.
218      */
219     public final void notifyPlotChange()
220     {
221         DatasetChangeEvent event = new DatasetChangeEvent(this, this);
222         for (DatasetChangeListener dcl : this.listeners)
223         {
224             dcl.datasetChanged(event);
225         }
226     }
227 
228     /**
229      * Returns the chart.
230      * @return chart
231      */
232     public final JFreeChart getChart()
233     {
234         return this.chart;
235     }
236 
237     @Override
238     public final String getId()
239     {
240         return this.id;
241     }
242 
243     @Override
244     public final void addChangeListener(final DatasetChangeListener listener)
245     {
246         this.listeners.add(listener);
247     }
248 
249     @Override
250     public final void removeChangeListener(final DatasetChangeListener listener)
251     {
252         this.listeners.remove(listener);
253     }
254 
255     /**
256      * Sets a new update interval.
257      * @param interval update interval
258      */
259     public final synchronized void setUpdateInterval(final Duration interval)
260     {
261         this.scheduler.cancelEvent(this);
262         this.updates = (int) (this.scheduler.getTime().si / interval.si);
263         this.updateInterval = interval;
264         this.updateTime = Duration.ofSI(this.updates * this.updateInterval.si);
265         scheduleNextUpdateEvent();
266     }
267 
268     /**
269      * Offer new update time. This is a Thread-safe method as this update time will only be set once a current update finishes,
270      * or no current update is running.
271      * @param interval update interval
272      */
273     public void offerUpdateInterval(final Duration interval)
274     {
275         if (this.inUpdate)
276         {
277             // Let the current update finish and pick it up when scheduling the next update
278             this.offeredUpdateInterval = interval;
279         }
280         else
281         {
282             // We can set it immediately, so let's not delay until next update for if we go from a large to small update time
283             setUpdateInterval(interval);
284         }
285     }
286 
287     /**
288      * Returns time until which data should be plotted.
289      * @return time until which data should be plotted
290      */
291     public final Duration getUpdateTime()
292     {
293         return this.updateTime;
294     }
295 
296     /**
297      * Redraws the plot and schedules the next update.
298      */
299     public synchronized void update()
300     {
301         this.inUpdate = true;
302         this.updateTime = this.scheduler.getTime();
303         increaseTime(this.updateTime.minus(this.delay));
304         notifyPlotChange();
305         scheduleNextUpdateEvent();
306         this.inUpdate = false;
307     }
308 
309     /**
310      * Schedules the next update event.
311      */
312     private void scheduleNextUpdateEvent()
313     {
314         if (this.offeredUpdateInterval != null)
315         {
316             this.scheduler.cancelEvent(this);
317             this.updates = (int) (this.scheduler.getTime().si / this.offeredUpdateInterval.si);
318             this.updateInterval = this.offeredUpdateInterval;
319             this.updateTime = Duration.ofSI(this.updates * this.updateInterval.si);
320             this.offeredUpdateInterval = null;
321         }
322         this.updates++;
323         // events are scheduled slightly later, so all influencing movements have occurred
324         this.scheduler.scheduleUpdate(Duration.ofSI(this.updateInterval.si * this.updates + this.delay.si), this);
325     }
326 
327     /**
328      * Retrieve the caption.
329      * @return the caption of the plot
330      */
331     public String getCaption()
332     {
333         return this.caption;
334     }
335 
336 }