View Javadoc
1   package org.opentrafficsim.draw.graphs;
2   
3   import java.awt.BorderLayout;
4   import java.awt.Color;
5   import java.awt.Dimension;
6   import java.awt.Font;
7   import java.awt.Graphics2D;
8   import java.awt.event.ActionEvent;
9   import java.awt.event.ActionListener;
10  import java.awt.event.WindowAdapter;
11  import java.awt.event.WindowEvent;
12  import java.awt.geom.AffineTransform;
13  import java.awt.geom.Rectangle2D;
14  import java.awt.image.BufferedImage;
15  import java.io.BufferedOutputStream;
16  import java.io.File;
17  import java.io.FileOutputStream;
18  import java.io.IOException;
19  import java.io.OutputStream;
20  import java.util.LinkedHashSet;
21  import java.util.Set;
22  import java.util.UUID;
23  
24  import javax.swing.JFileChooser;
25  import javax.swing.JFrame;
26  import javax.swing.JLabel;
27  import javax.swing.JMenuItem;
28  import javax.swing.JPopupMenu;
29  import javax.swing.JTextField;
30  import javax.swing.SwingConstants;
31  import javax.swing.filechooser.FileNameExtensionFilter;
32  
33  import org.djunits.value.vdouble.scalar.Duration;
34  import org.djunits.value.vdouble.scalar.Time;
35  import org.jfree.chart.ChartMouseListener;
36  import org.jfree.chart.ChartPanel;
37  import org.jfree.chart.ChartUtils;
38  import org.jfree.chart.JFreeChart;
39  import org.jfree.chart.plot.XYPlot;
40  import org.jfree.chart.title.TextTitle;
41  import org.jfree.data.general.Dataset;
42  import org.jfree.data.general.DatasetChangeEvent;
43  import org.jfree.data.general.DatasetChangeListener;
44  import org.jfree.data.general.DatasetGroup;
45  import org.opentrafficsim.base.Identifiable;
46  import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
47  
48  import nl.tudelft.simulation.dsol.SimRuntimeException;
49  import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEventInterface;
50  import nl.tudelft.simulation.dsol.simtime.SimTimeDoubleUnit;
51  import nl.tudelft.simulation.event.EventType;
52  
53  /**
54   * Super class of all plots. This schedules regular updates, creates menus and deals with listeners. There are a number of
55   * delegate methods for sub classes to implement.
56   * <p>
57   * Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
58   * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
59   * <p>
60   * @version $Revision$, $LastChangedDate$, by $Author$, initial version 4 okt. 2018 <br>
61   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
62   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
63   * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
64   */
65  public abstract class AbstractPlot extends JFrame implements Identifiable, Dataset
66  {
67  
68      /** */
69      private static final long serialVersionUID = 20181004L;
70  
71      /**
72       * The (regular, not timed) event type for pub/sub indicating the addition of a graph. Not used internally.<br>
73       * Payload: String graph caption (not an array, just a String)
74       */
75      public static final EventType GRAPH_ADD_EVENT = new EventType("GRAPH.ADD");
76  
77      /**
78       * The (regular, not timed) event type for pub/sub indicating the removal of a graph. Not used internally.<br>
79       * Payload: String Graph caption (not an array, just a String)
80       */
81      public static final EventType GRAPH_REMOVE_EVENT = new EventType("GRAPH.REMOVE");
82  
83      /** Initial upper bound for the time scale. */
84      public static final Time DEFAULT_INITIAL_UPPER_TIME_BOUND = Time.createSI(300.0);
85  
86      /** Caption. */
87      private final String caption;
88  
89      /** Update interval. */
90      private Duration updateInterval;
91  
92      /** Delay so critical future events have occurred, e.g. GTU's next move's to extend trajectories. */
93      private final Duration delay;
94  
95      /** Simulator. */
96      private final OTSSimulatorInterface simulator;
97  
98      /** Time of last data update. */
99      private Time updateTime;
100 
101     /** Number of updates. */
102     private int updates = 0;
103 
104     /** Unique ID of the chart. */
105     private final String id = UUID.randomUUID().toString();
106 
107     /** The chart, so we can export it. */
108     private JFreeChart chart;
109 
110     /** Status label. */
111     private JLabel statusLabel;
112 
113     /** Detach menu item. */
114     private JMenuItem detach;
115 
116     /** List of parties interested in changes of this plot. */
117     private Set<DatasetChangeListener> listeners = new LinkedHashSet<>();
118 
119     /** Event of next update. */
120     private SimEventInterface<SimTimeDoubleUnit> updateEvent;
121 
122     /**
123      * Constructor.
124      * @param caption String; caption
125      * @param updateInterval Duration; regular update interval (simulation time)
126      * @param simulator OTSSimulatorInterface; simulator
127      * @param delay Duration; delay so critical future events have occurred, e.g. GTU's next move's to extend trajectories
128      */
129     public AbstractPlot(final String caption, final Duration updateInterval, final OTSSimulatorInterface simulator,
130             final Duration delay)
131     {
132         this.caption = caption;
133         this.updateInterval = updateInterval;
134         this.simulator = simulator;
135         this.delay = delay;
136         update(); // start redraw chain
137     }
138 
139     /**
140      * Sets the chart and adds menus and listeners.
141      * @param chart JFreeChart; chart
142      */
143     @SuppressWarnings("methodlength")
144     protected void setChart(final JFreeChart chart)
145     {
146         this.chart = chart;
147 
148         // make title somewhat smaller
149         chart.setTitle(new TextTitle(chart.getTitle().getText(), new Font("SansSerif", java.awt.Font.BOLD, 16)));
150 
151         // default colors and zoom behavior
152         chart.getPlot().setBackgroundPaint(Color.LIGHT_GRAY);
153         chart.setBackgroundPaint(Color.WHITE);
154         if (chart.getPlot() instanceof XYPlot)
155         {
156             chart.getXYPlot().setDomainGridlinePaint(Color.WHITE);
157             chart.getXYPlot().setRangeGridlinePaint(Color.WHITE);
158         }
159 
160         // status label
161         this.statusLabel = new JLabel(" ", SwingConstants.CENTER);
162         add(this.statusLabel, BorderLayout.SOUTH);
163 
164         // override to gain some control over the auto bounds
165         ChartPanel chartPanel = new ChartPanel(chart)
166         {
167             /** */
168             private static final long serialVersionUID = 20181006L;
169 
170             /** {@inheritDoc} */
171             @Override
172             public void restoreAutoDomainBounds()
173             {
174                 super.restoreAutoDomainBounds();
175                 if (chart.getPlot() instanceof XYPlot)
176                 {
177                     setAutoBoundDomain(chart.getXYPlot());
178                 }
179             }
180 
181             /** {@inheritDoc} */
182             @Override
183             public void restoreAutoRangeBounds()
184             {
185                 super.restoreAutoRangeBounds();
186                 if (chart.getPlot() instanceof XYPlot)
187                 {
188                     setAutoBoundRange(chart.getXYPlot());
189                 }
190             }
191 
192             /** {@inheritDoc} This implementation adds control over the PNG image size and font size. */
193             @Override
194             public void doSaveAs() throws IOException
195             {
196                 // the code in this method is based on the code in the super implementation
197 
198                 // create setting components
199                 JLabel fontSizeLabel = new JLabel("font size");
200                 JTextField fontSize = new JTextField("32"); // by default, give more space for labels in a png export
201                 fontSize.setToolTipText("Font size of title (other fonts are scaled)");
202                 fontSize.setPreferredSize(new Dimension(40, 20));
203                 JTextField width = new JTextField("960");
204                 width.setToolTipText("Width [pixels]");
205                 width.setPreferredSize(new Dimension(40, 20));
206                 JLabel x = new JLabel("x");
207                 JTextField height = new JTextField("540");
208                 height.setToolTipText("Height [pixels]");
209                 height.setPreferredSize(new Dimension(40, 20));
210 
211                 // create file chooser with these components
212                 JFileChooser fileChooser = new JFileChooserWithSettings(fontSizeLabel, fontSize, width, x, height);
213                 fileChooser.setCurrentDirectory(getDefaultDirectoryForSaveAs());
214                 FileNameExtensionFilter filter =
215                         new FileNameExtensionFilter(localizationResources.getString("PNG_Image_Files"), "png");
216                 fileChooser.addChoosableFileFilter(filter);
217                 fileChooser.setFileFilter(filter);
218 
219                 int option = fileChooser.showSaveDialog(this);
220                 if (option == JFileChooser.APPROVE_OPTION)
221                 {
222                     String filename = fileChooser.getSelectedFile().getPath();
223                     if (isEnforceFileExtensions())
224                     {
225                         if (!filename.endsWith(".png"))
226                         {
227                             filename = filename + ".png";
228                         }
229                     }
230 
231                     // get settings from setting components
232                     double fs; // relative scale
233                     try
234                     {
235                         fs = Double.parseDouble(fontSize.getText());
236                     }
237                     catch (NumberFormatException exception)
238                     {
239                         fs = 16.0;
240                     }
241                     int w;
242                     try
243                     {
244                         w = Integer.parseInt(width.getText());
245                     }
246                     catch (NumberFormatException exception)
247                     {
248                         w = getWidth();
249                     }
250                     int h;
251                     try
252                     {
253                         h = Integer.parseInt(height.getText());
254                     }
255                     catch (NumberFormatException exception)
256                     {
257                         h = getHeight();
258                     }
259                     OutputStream out = new BufferedOutputStream(new FileOutputStream(new File(filename)));
260                     out.write(encodeAsPng(w, h, fs));
261                     out.close();
262                 }
263             }
264         };
265         ChartMouseListener chartListener = getChartMouseListener();
266         if (chartListener != null)
267         {
268             chartPanel.addChartMouseListener(chartListener);
269         }
270 
271         // pointer handler
272         final PointerHandler ph = new PointerHandler()
273         {
274             /** {@inheritDoc} */
275             @Override
276             public void updateHint(final double domainValue, final double rangeValue)
277             {
278                 if (Double.isNaN(domainValue))
279                 {
280                     setStatusLabel(" ");
281                 }
282                 else
283                 {
284                     setStatusLabel(getStatusLabel(domainValue, rangeValue));
285                 }
286             }
287         };
288         chartPanel.addMouseMotionListener(ph);
289         chartPanel.addMouseListener(ph);
290         add(chartPanel, BorderLayout.CENTER);
291         chartPanel.setMouseWheelEnabled(true);
292 
293         // pop up
294         JPopupMenu popupMenu = chartPanel.getPopupMenu();
295         popupMenu.add(new JPopupMenu.Separator());
296         this.detach = new JMenuItem("Show in detached window");
297         this.detach.addActionListener(new ActionListener()
298         {
299             @SuppressWarnings("synthetic-access")
300             @Override
301             public void actionPerformed(final ActionEvent e)
302             {
303                 AbstractPlot.this.detach.setEnabled(false);
304                 JFrame window = new JFrame(AbstractPlot.this.caption);
305                 window.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
306                 window.add(chartPanel, BorderLayout.CENTER);
307                 window.add(AbstractPlot.this.statusLabel, BorderLayout.SOUTH);
308                 window.addWindowListener(new WindowAdapter()
309                 {
310                     /** {@inheritDoc} */
311                     @Override
312                     public void windowClosing(@SuppressWarnings("hiding") final WindowEvent e)
313                     {
314                         add(chartPanel, BorderLayout.CENTER);
315                         add(AbstractPlot.this.statusLabel, BorderLayout.SOUTH);
316                         AbstractPlot.this.detach.setEnabled(true);
317                         AbstractPlot.this.getContentPane().validate();
318                         AbstractPlot.this.getContentPane().repaint();
319                     }
320                 });
321                 window.pack();
322                 window.setVisible(true);
323                 AbstractPlot.this.getContentPane().repaint();
324             }
325         });
326         popupMenu.add(this.detach);
327         addPopUpMenuItems(popupMenu);
328     }
329 
330     /**
331      * Returns the chart as a byte array representing a png image.
332      * @param width int; width
333      * @param height int; height
334      * @param fontSize double; font size (16 is the original on screen size)
335      * @return byte[]; the chart as a byte array representing a png image
336      * @throws IOException on IO exception
337      */
338     public byte[] encodeAsPng(final int width, final int height, final double fontSize) throws IOException
339     {
340         // to double the font size, we halve the base dimensions
341         // JFreeChart will the assign more area (relatively) to the fixed actual font size
342         double baseWidth = width / (fontSize / 16);
343         double baseHeight = height / (fontSize / 16);
344         // this code is from ChartUtils.writeScaledChartAsPNG
345         BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
346         Graphics2D g2 = image.createGraphics();
347         // to compensate for the base dimensions which are not w x h, we scale the drawing
348         AffineTransform saved = g2.getTransform();
349         g2.transform(AffineTransform.getScaleInstance(width / baseWidth, height / baseHeight));
350         getChart().draw(g2, new Rectangle2D.Double(0, 0, baseWidth, baseHeight), null, null);
351         g2.setTransform(saved);
352         g2.dispose();
353         return ChartUtils.encodeAsPNG(image);
354     }
355 
356     /** {@inheritDoc} */
357     @Override
358     public final DatasetGroup getGroup()
359     {
360         return null; // not used
361     }
362 
363     /** {@inheritDoc} */
364     @Override
365     public final void setGroup(final DatasetGroup group)
366     {
367         // not used
368     }
369 
370     /**
371      * Overridable method to add pop up items.
372      * @param popupMenu JPopupMenu; pop up menu
373      */
374     protected void addPopUpMenuItems(final JPopupMenu popupMenu)
375     {
376         //
377     }
378 
379     /**
380      * Overridable; activates auto bounds on domain axis from user input. This class does not force the use of {@code XYPlot}s,
381      * but the auto bounds command comes from the {@code ChartPanel} that this class creates. In case the used plot is a
382      * {@code XYPlot}, this method is then invoked. Sub classes with auto domain bounds that work with an {@code XYPlot} should
383      * implement this. The method is not abstract as the use of {@code XYPlot} is not obligated.
384      * @param plot XYPlot; plot
385      */
386     protected void setAutoBoundDomain(final XYPlot plot)
387     {
388         //
389     }
390 
391     /**
392      * Overridable; activates auto bounds on range axis from user input. This class does not force the use of {@code XYPlot}s,
393      * but the auto bounds command comes from the {@code ChartPanel} that this class creates. In case the used plot is a
394      * {@code XYPlot}, this method is then invoked. Sub classes with auto range bounds that work with an {@code XYPlot} should
395      * implement this. The method is not abstract as the use of {@code XYPlot} is not obligated.
396      * @param plot XYPlot; plot
397      */
398     protected void setAutoBoundRange(final XYPlot plot)
399     {
400         //
401     }
402 
403     /**
404      * Overridable; may return a chart listener for additional functions.
405      * @return ChartMouseListener, {@code null} by default
406      */
407     protected ChartMouseListener getChartMouseListener()
408     {
409         return null;
410     }
411 
412     /**
413      * Return the graph type for transceiver.
414      * @return GraphType; the graph type.
415      */
416     public abstract GraphType getGraphType();
417 
418     /**
419      * Returns the status label when the mouse is over the given location.
420      * @param domainValue double; domain value (x-axis)
421      * @param rangeValue double; range value (y-axis)
422      * @return String; status label when the mouse is over the given location
423      */
424     protected abstract String getStatusLabel(double domainValue, double rangeValue);
425 
426     /**
427      * Increase the simulated time span.
428      * @param time Time; time to increase to
429      */
430     protected abstract void increaseTime(Time time);
431 
432     /**
433      * Redraws the plot and schedules the next update.
434      */
435     protected void update()
436     {
437         this.updateTime = this.simulator.getSimulatorTime();
438         increaseTime(this.updateTime.minus(this.delay));
439         notifyPlotChange();
440         scheduleNextUpdateEvent();
441     }
442 
443     /**
444      * Schedules the next update event.
445      */
446     private void scheduleNextUpdateEvent()
447     {
448         try
449         {
450             this.updates++;
451             // events are scheduled slightly later, so all influencing movements have occurred
452             this.updateEvent = this.simulator.scheduleEventAbs(
453                     Time.createSI(this.updateInterval.si * this.updates + this.delay.si), this, this, "update", null);
454         }
455         catch (SimRuntimeException exception)
456         {
457             throw new RuntimeException("Unexpected exception while updating plot.", exception);
458         }
459     }
460 
461     /**
462      * Notify all change listeners.
463      */
464     public final void notifyPlotChange()
465     {
466         DatasetChangeEvent event = new DatasetChangeEvent(this, this);
467         for (DatasetChangeListener dcl : this.listeners)
468         {
469             dcl.datasetChanged(event);
470         }
471     }
472 
473     /**
474      * Sets a new update interval.
475      * @param interval Duration; update interval
476      */
477     protected final void setUpdateInterval(final Duration interval)
478     {
479         if (this.updateEvent != null)
480         {
481             this.simulator.cancelEvent(this.updateEvent);
482         }
483         this.updates = (int) (this.simulator.getSimulatorTime().si / interval.si);
484         this.updateInterval = interval;
485         this.updateTime = Time.createSI(this.updates * this.updateInterval.si);
486         scheduleNextUpdateEvent();
487     }
488 
489     /**
490      * Returns time until which data should be plotted.
491      * @return Time; time until which data should be plotted
492      */
493     protected final Time getUpdateTime()
494     {
495         return this.updateTime;
496     }
497 
498     /**
499      * Returns the chart.
500      * @return JFreeChart; chart
501      */
502     protected final JFreeChart getChart()
503     {
504         return this.chart;
505     }
506 
507     /** {@inheritDoc} */
508     @Override
509     public final String getId()
510     {
511         return this.id;
512     }
513 
514     /** {@inheritDoc} */
515     @Override
516     public final void addChangeListener(final DatasetChangeListener listener)
517     {
518         this.listeners.add(listener);
519     }
520 
521     /** {@inheritDoc} */
522     @Override
523     public final void removeChangeListener(final DatasetChangeListener listener)
524     {
525         this.listeners.remove(listener);
526     }
527 
528     /**
529      * Manually set status label from sub class. Will be overwritten by a moving mouse pointer over the axes.
530      * @param label String; label to set
531      */
532     protected final void setStatusLabel(final String label)
533     {
534         if (this.statusLabel != null)
535         {
536             this.statusLabel.setText(label);
537         }
538     }
539 
540     /**
541      * Return the caption of this graph.
542      * @return String; the caption of this graph
543      */
544     public final String getCaption()
545     {
546         return this.caption;
547     }
548 
549 }