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  
55  
56  
57  
58  
59  
60  
61  
62  
63  
64  
65  public abstract class AbstractPlot extends JFrame implements Identifiable, Dataset
66  {
67  
68      
69      private static final long serialVersionUID = 20181004L;
70  
71      
72  
73  
74  
75      public static final EventType GRAPH_ADD_EVENT = new EventType("GRAPH.ADD");
76  
77      
78  
79  
80  
81      public static final EventType GRAPH_REMOVE_EVENT = new EventType("GRAPH.REMOVE");
82  
83      
84      public static final Time DEFAULT_INITIAL_UPPER_TIME_BOUND = Time.createSI(300.0);
85  
86      
87      private final String caption;
88  
89      
90      private Duration updateInterval;
91  
92      
93      private final Duration delay;
94  
95      
96      private final OTSSimulatorInterface simulator;
97  
98      
99      private Time updateTime;
100 
101     
102     private int updates = 0;
103 
104     
105     private final String id = UUID.randomUUID().toString();
106 
107     
108     private JFreeChart chart;
109 
110     
111     private JLabel statusLabel;
112 
113     
114     private JMenuItem detach;
115 
116     
117     private Set<DatasetChangeListener> listeners = new LinkedHashSet<>();
118 
119     
120     private SimEventInterface<SimTimeDoubleUnit> updateEvent;
121 
122     
123 
124 
125 
126 
127 
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(); 
137     }
138 
139     
140 
141 
142 
143     @SuppressWarnings("methodlength")
144     protected void setChart(final JFreeChart chart)
145     {
146         this.chart = chart;
147 
148         
149         chart.setTitle(new TextTitle(chart.getTitle().getText(), new Font("SansSerif", java.awt.Font.BOLD, 16)));
150 
151         
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         
161         this.statusLabel = new JLabel(" ", SwingConstants.CENTER);
162         add(this.statusLabel, BorderLayout.SOUTH);
163 
164         
165         ChartPanel chartPanel = new ChartPanel(chart)
166         {
167             
168             private static final long serialVersionUID = 20181006L;
169 
170             
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             
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             
193             @Override
194             public void doSaveAs() throws IOException
195             {
196                 
197 
198                 
199                 JLabel fontSizeLabel = new JLabel("font size");
200                 JTextField fontSize = new JTextField("32"); 
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                 
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                     
232                     double fs; 
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         
272         final PointerHandler ph = new PointerHandler()
273         {
274             
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         
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                     
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 
332 
333 
334 
335 
336 
337 
338     public byte[] encodeAsPng(final int width, final int height, final double fontSize) throws IOException
339     {
340         
341         
342         double baseWidth = width / (fontSize / 16);
343         double baseHeight = height / (fontSize / 16);
344         
345         BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
346         Graphics2D g2 = image.createGraphics();
347         
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     
357     @Override
358     public final DatasetGroup getGroup()
359     {
360         return null; 
361     }
362 
363     
364     @Override
365     public final void setGroup(final DatasetGroup group)
366     {
367         
368     }
369 
370     
371 
372 
373 
374     protected void addPopUpMenuItems(final JPopupMenu popupMenu)
375     {
376         
377     }
378 
379     
380 
381 
382 
383 
384 
385 
386     protected void setAutoBoundDomain(final XYPlot plot)
387     {
388         
389     }
390 
391     
392 
393 
394 
395 
396 
397 
398     protected void setAutoBoundRange(final XYPlot plot)
399     {
400         
401     }
402 
403     
404 
405 
406 
407     protected ChartMouseListener getChartMouseListener()
408     {
409         return null;
410     }
411 
412     
413 
414 
415 
416     public abstract GraphType getGraphType();
417 
418     
419 
420 
421 
422 
423 
424     protected abstract String getStatusLabel(double domainValue, double rangeValue);
425 
426     
427 
428 
429 
430     protected abstract void increaseTime(Time time);
431 
432     
433 
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 
445 
446     private void scheduleNextUpdateEvent()
447     {
448         try
449         {
450             this.updates++;
451             
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 
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 
475 
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 
491 
492 
493     protected final Time getUpdateTime()
494     {
495         return this.updateTime;
496     }
497 
498     
499 
500 
501 
502     protected final JFreeChart getChart()
503     {
504         return this.chart;
505     }
506 
507     
508     @Override
509     public final String getId()
510     {
511         return this.id;
512     }
513 
514     
515     @Override
516     public final void addChangeListener(final DatasetChangeListener listener)
517     {
518         this.listeners.add(listener);
519     }
520 
521     
522     @Override
523     public final void removeChangeListener(final DatasetChangeListener listener)
524     {
525         this.listeners.remove(listener);
526     }
527 
528     
529 
530 
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 
542 
543 
544     public final String getCaption()
545     {
546         return this.caption;
547     }
548 
549 }