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 }