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://tudelft.nl/staff/p.knoppers-1">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.instantiateSI(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 Time updateTime;
80
81 /** Number of updates. */
82 private int updates = 0;
83
84 /** Update interval. */
85 private Duration updateInterval;
86
87 /**
88 * Constructor.
89 * @param scheduler PlotScheduler; scheduler.
90 * @param caption String; caption
91 * @param updateInterval Duration; regular update interval (simulation time)
92 * @param delay Duration; amount of time that chart runs behind simulation to prevent gaps in the charted data
93 */
94 public AbstractPlot(final PlotScheduler scheduler, final String caption, final Duration updateInterval,
95 final Duration delay)
96 {
97 this.scheduler = scheduler;
98 this.caption = caption;
99 this.updateInterval = updateInterval;
100 this.delay = delay;
101 this.updates = (int) (scheduler.getTime().si / updateInterval.si); // when creating plot during simulation
102 update(); // start redraw chain
103 }
104
105 /**
106 * Sets the chart and adds menus and listeners.
107 * @param chart JFreeChart; chart
108 */
109 @SuppressWarnings("methodlength")
110 protected void setChart(final JFreeChart chart)
111 {
112 this.chart = chart;
113
114 // make title somewhat smaller
115 chart.setTitle(new TextTitle(chart.getTitle().getText(), new Font("SansSerif", java.awt.Font.BOLD, 16)));
116
117 // default colors and zoom behavior
118 chart.getPlot().setBackgroundPaint(Color.LIGHT_GRAY);
119 chart.setBackgroundPaint(Color.WHITE);
120 if (chart.getPlot() instanceof XYPlot)
121 {
122 chart.getXYPlot().setDomainGridlinePaint(Color.WHITE);
123 chart.getXYPlot().setRangeGridlinePaint(Color.WHITE);
124 }
125
126 }
127
128 /**
129 * Returns the chart as a byte array representing a png image.
130 * @param width int; width
131 * @param height int; height
132 * @param fontSize double; font size (16 is the original on screen size)
133 * @return byte[]; the chart as a byte array representing a png image
134 * @throws IOException on IO exception
135 */
136 public byte[] encodeAsPng(final int width, final int height, final double fontSize) throws IOException
137 {
138 // to double the font size, we halve the base dimensions
139 // JFreeChart will the assign more area (relatively) to the fixed actual font size
140 double baseWidth = width / (fontSize / 16);
141 double baseHeight = height / (fontSize / 16);
142 // this code is from ChartUtils.writeScaledChartAsPNG
143 BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
144 Graphics2D g2 = image.createGraphics();
145 // to compensate for the base dimensions which are not w x h, we scale the drawing
146 AffineTransform saved = g2.getTransform();
147 g2.transform(AffineTransform.getScaleInstance(width / baseWidth, height / baseHeight));
148 getChart().draw(g2, new Rectangle2D.Double(0, 0, baseWidth, baseHeight), null, null);
149 g2.setTransform(saved);
150 g2.dispose();
151 return ChartUtils.encodeAsPNG(image);
152 }
153
154 /** {@inheritDoc} */
155 @Override
156 public final DatasetGroup getGroup()
157 {
158 return null; // not used
159 }
160
161 /** {@inheritDoc} */
162 @Override
163 public final void setGroup(final DatasetGroup group)
164 {
165 // not used
166 }
167
168 /**
169 * Overridable; activates auto bounds on domain axis from user input. This class does not force the use of {@code XYPlot}s,
170 * but the auto bounds command comes from the {@code ChartPanel} that this class creates. In case the used plot is a
171 * {@code XYPlot}, this method is then invoked. Sub classes with auto domain bounds that work with an {@code XYPlot} should
172 * implement this. The method is not abstract as the use of {@code XYPlot} is not obligated.
173 * @param plot XYPlot; plot
174 */
175 public void setAutoBoundDomain(final XYPlot plot)
176 {
177 //
178 }
179
180 /**
181 * Overridable; activates auto bounds on range axis from user input. This class does not force the use of {@code XYPlot}s,
182 * but the auto bounds command comes from the {@code ChartPanel} that this class creates. In case the used plot is a
183 * {@code XYPlot}, this method is then invoked. Sub classes with auto range bounds that work with an {@code XYPlot} should
184 * implement this. The method is not abstract as the use of {@code XYPlot} is not obligated.
185 * @param plot XYPlot; plot
186 */
187 public void setAutoBoundRange(final XYPlot plot)
188 {
189 //
190 }
191
192 /**
193 * Return the graph type for transceiver.
194 * @return GraphType; the graph type.
195 */
196 public abstract GraphType getGraphType();
197
198 /**
199 * Returns the status label when the mouse is over the given location.
200 * @param domainValue double; domain value (x-axis)
201 * @param rangeValue double; range value (y-axis)
202 * @return String; status label when the mouse is over the given location
203 */
204 public abstract String getStatusLabel(double domainValue, double rangeValue);
205
206 /**
207 * Increase the simulated time span.
208 * @param time Time; time to increase to
209 */
210 protected abstract void increaseTime(Time time);
211
212 /**
213 * Notify all change listeners.
214 */
215 public final void notifyPlotChange()
216 {
217 DatasetChangeEvent event = new DatasetChangeEvent(this, this);
218 for (DatasetChangeListener dcl : this.listeners)
219 {
220 dcl.datasetChanged(event);
221 }
222 }
223
224 /**
225 * Returns the chart.
226 * @return JFreeChart; chart
227 */
228 public final JFreeChart getChart()
229 {
230 return this.chart;
231 }
232
233 /** {@inheritDoc} */
234 @Override
235 public final String getId()
236 {
237 return this.id;
238 }
239
240 /** {@inheritDoc} */
241 @Override
242 public final void addChangeListener(final DatasetChangeListener listener)
243 {
244 this.listeners.add(listener);
245 }
246
247 /** {@inheritDoc} */
248 @Override
249 public final void removeChangeListener(final DatasetChangeListener listener)
250 {
251 this.listeners.remove(listener);
252 }
253
254 /**
255 * Sets a new update interval.
256 * @param interval Duration; update interval
257 */
258 public final void setUpdateInterval(final Duration interval)
259 {
260 this.scheduler.cancelEvent(this);
261 this.updates = (int) (this.scheduler.getTime().si / interval.si);
262 this.updateInterval = interval;
263 this.updateTime = Time.instantiateSI(this.updates * this.updateInterval.si);
264 scheduleNextUpdateEvent();
265 }
266
267 /**
268 * Returns time until which data should be plotted.
269 * @return Time; time until which data should be plotted
270 */
271 public final Time getUpdateTime()
272 {
273 return this.updateTime;
274 }
275
276 /**
277 * Redraws the plot and schedules the next update.
278 */
279 protected void update()
280 {
281 // TODO: next event may be scheduled in the past if the scheduler is running fast during these few calls
282 this.updateTime = this.scheduler.getTime();
283 increaseTime(this.updateTime.minus(this.delay));
284 notifyPlotChange();
285 scheduleNextUpdateEvent();
286 }
287
288 /**
289 * Schedules the next update event.
290 */
291 private void scheduleNextUpdateEvent()
292 {
293 this.updates++;
294 // events are scheduled slightly later, so all influencing movements have occurred
295 this.scheduler.scheduleUpdate(Time.instantiateSI(this.updateInterval.si * this.updates + this.delay.si), this);
296 }
297
298 /**
299 * Retrieve the caption.
300 * @return String; the caption of the plot
301 */
302 public String getCaption()
303 {
304 return this.caption;
305 }
306
307 }