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