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://github.com/peter-knoppers">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 scheduler.
90       * @param caption caption
91       * @param updateInterval regular update interval (simulation time)
92       * @param delay 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 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 width
131      * @param height height
132      * @param fontSize font size (16 is the original on screen size)
133      * @return 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     @Override
155     public final DatasetGroup getGroup()
156     {
157         return null; // not used
158     }
159 
160     @Override
161     public final void setGroup(final DatasetGroup group)
162     {
163         // not used
164     }
165 
166     /**
167      * Overridable; activates auto bounds on domain axis from user input. This class does not force the use of {@code XYPlot}s,
168      * but the auto bounds command comes from the {@code ChartPanel} that this class creates. In case the used plot is a
169      * {@code XYPlot}, this method is then invoked. Sub classes with auto domain bounds that work with an {@code XYPlot} should
170      * implement this. The method is not abstract as the use of {@code XYPlot} is not obligated.
171      * @param plot plot
172      */
173     public void setAutoBoundDomain(final XYPlot plot)
174     {
175         //
176     }
177 
178     /**
179      * Overridable; activates auto bounds on range axis from user input. This class does not force the use of {@code XYPlot}s,
180      * but the auto bounds command comes from the {@code ChartPanel} that this class creates. In case the used plot is a
181      * {@code XYPlot}, this method is then invoked. Sub classes with auto range bounds that work with an {@code XYPlot} should
182      * implement this. The method is not abstract as the use of {@code XYPlot} is not obligated.
183      * @param plot plot
184      */
185     public void setAutoBoundRange(final XYPlot plot)
186     {
187         //
188     }
189 
190     /**
191      * Return the graph type for transceiver.
192      * @return the graph type.
193      */
194     public abstract GraphType getGraphType();
195 
196     /**
197      * Returns the status label when the mouse is over the given location.
198      * @param domainValue domain value (x-axis)
199      * @param rangeValue range value (y-axis)
200      * @return status label when the mouse is over the given location
201      */
202     public abstract String getStatusLabel(double domainValue, double rangeValue);
203 
204     /**
205      * Increase the simulated time span.
206      * @param time time to increase to
207      */
208     protected abstract void increaseTime(Time time);
209 
210     /**
211      * Notify all change listeners.
212      */
213     public final void notifyPlotChange()
214     {
215         DatasetChangeEvent event = new DatasetChangeEvent(this, this);
216         for (DatasetChangeListener dcl : this.listeners)
217         {
218             dcl.datasetChanged(event);
219         }
220     }
221 
222     /**
223      * Returns the chart.
224      * @return chart
225      */
226     public final JFreeChart getChart()
227     {
228         return this.chart;
229     }
230 
231     @Override
232     public final String getId()
233     {
234         return this.id;
235     }
236 
237     @Override
238     public final void addChangeListener(final DatasetChangeListener listener)
239     {
240         this.listeners.add(listener);
241     }
242 
243     @Override
244     public final void removeChangeListener(final DatasetChangeListener listener)
245     {
246         this.listeners.remove(listener);
247     }
248 
249     /**
250      * Sets a new update interval.
251      * @param interval update interval
252      */
253     public final void setUpdateInterval(final Duration interval)
254     {
255         this.scheduler.cancelEvent(this);
256         this.updates = (int) (this.scheduler.getTime().si / interval.si);
257         this.updateInterval = interval;
258         this.updateTime = Time.instantiateSI(this.updates * this.updateInterval.si);
259         scheduleNextUpdateEvent();
260     }
261 
262     /**
263      * Returns time until which data should be plotted.
264      * @return time until which data should be plotted
265      */
266     public final Time getUpdateTime()
267     {
268         return this.updateTime;
269     }
270 
271     /**
272      * Redraws the plot and schedules the next update.
273      */
274     protected void update()
275     {
276         // TODO: next event may be scheduled in the past if the scheduler is running fast during these few calls
277         this.updateTime = this.scheduler.getTime();
278         increaseTime(this.updateTime.minus(this.delay));
279         notifyPlotChange();
280         scheduleNextUpdateEvent();
281     }
282 
283     /**
284      * Schedules the next update event.
285      */
286     private void scheduleNextUpdateEvent()
287     {
288         this.updates++;
289         // events are scheduled slightly later, so all influencing movements have occurred
290         this.scheduler.scheduleUpdate(Time.instantiateSI(this.updateInterval.si * this.updates + this.delay.si), this);
291     }
292 
293     /**
294      * Retrieve the caption.
295      * @return the caption of the plot
296      */
297     public String getCaption()
298     {
299         return this.caption;
300     }
301 
302 }