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