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.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 }