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 }