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