package org.opentrafficsim.draw.graphs; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics2D; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.LinkedHashSet; import java.util.Set; import java.util.UUID; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JTextField; import javax.swing.SwingConstants; import javax.swing.filechooser.FileNameExtensionFilter; import org.djunits.value.vdouble.scalar.Duration; import org.djunits.value.vdouble.scalar.Time; import org.jfree.chart.ChartMouseListener; import org.jfree.chart.ChartPanel; import org.jfree.chart.ChartUtils; import org.jfree.chart.JFreeChart; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.title.TextTitle; import org.jfree.data.general.Dataset; import org.jfree.data.general.DatasetChangeEvent; import org.jfree.data.general.DatasetChangeListener; import org.jfree.data.general.DatasetGroup; import org.opentrafficsim.base.Identifiable; import org.opentrafficsim.core.dsol.OTSSimulatorInterface; import nl.tudelft.simulation.dsol.SimRuntimeException; import nl.tudelft.simulation.dsol.formalisms.eventscheduling.SimEventInterface; import nl.tudelft.simulation.dsol.simtime.SimTimeDoubleUnit; import nl.tudelft.simulation.event.EventType; /** * Super class of all plots. This schedules regular updates, creates menus and deals with listeners. There are a number of * delegate methods for sub classes to implement. *

* Copyright (c) 2013-2019 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See OpenTrafficSim License. *

* @version $Revision$, $LastChangedDate$, by $Author$, initial version 4 okt. 2018
* @author Alexander Verbraeck * @author Peter Knoppers * @author Wouter Schakel */ public abstract class AbstractPlot extends JFrame implements Identifiable, Dataset { /** */ private static final long serialVersionUID = 20181004L; /** * The (regular, not timed) event type for pub/sub indicating the addition of a graph. Not used internally.
* Payload: String graph caption (not an array, just a String) */ public static final EventType GRAPH_ADD_EVENT = new EventType("GRAPH.ADD"); /** * The (regular, not timed) event type for pub/sub indicating the removal of a graph. Not used internally.
* Payload: String Graph caption (not an array, just a String) */ public static final EventType GRAPH_REMOVE_EVENT = new EventType("GRAPH.REMOVE"); /** Initial upper bound for the time scale. */ public static final Time DEFAULT_INITIAL_UPPER_TIME_BOUND = Time.createSI(300.0); /** Caption. */ private final String caption; /** Update interval. */ private Duration updateInterval; /** Delay so critical future events have occurred, e.g. GTU's next move's to extend trajectories. */ private final Duration delay; /** Simulator. */ private final OTSSimulatorInterface simulator; /** Time of last data update. */ private Time updateTime; /** Number of updates. */ private int updates = 0; /** Unique ID of the chart. */ private final String id = UUID.randomUUID().toString(); /** The chart, so we can export it. */ private JFreeChart chart; /** Status label. */ private JLabel statusLabel; /** Detach menu item. */ private JMenuItem detach; /** List of parties interested in changes of this plot. */ private Set listeners = new LinkedHashSet<>(); /** Event of next update. */ private SimEventInterface updateEvent; /** * Constructor. * @param caption String; caption * @param updateInterval Duration; regular update interval (simulation time) * @param simulator OTSSimulatorInterface; simulator * @param delay Duration; delay so critical future events have occurred, e.g. GTU's next move's to extend trajectories */ public AbstractPlot(final String caption, final Duration updateInterval, final OTSSimulatorInterface simulator, final Duration delay) { this.caption = caption; this.updateInterval = updateInterval; this.simulator = simulator; this.delay = delay; update(); // start redraw chain } /** * Sets the chart and adds menus and listeners. * @param chart JFreeChart; chart */ @SuppressWarnings("methodlength") protected void setChart(final JFreeChart chart) { this.chart = chart; // make title somewhat smaller chart.setTitle(new TextTitle(chart.getTitle().getText(), new Font("SansSerif", java.awt.Font.BOLD, 16))); // default colors and zoom behavior chart.getPlot().setBackgroundPaint(Color.LIGHT_GRAY); chart.setBackgroundPaint(Color.WHITE); if (chart.getPlot() instanceof XYPlot) { chart.getXYPlot().setDomainGridlinePaint(Color.WHITE); chart.getXYPlot().setRangeGridlinePaint(Color.WHITE); } // status label this.statusLabel = new JLabel(" ", SwingConstants.CENTER); add(this.statusLabel, BorderLayout.SOUTH); // override to gain some control over the auto bounds ChartPanel chartPanel = new ChartPanel(chart) { /** */ private static final long serialVersionUID = 20181006L; /** {@inheritDoc} */ @Override public void restoreAutoDomainBounds() { super.restoreAutoDomainBounds(); if (chart.getPlot() instanceof XYPlot) { setAutoBoundDomain(chart.getXYPlot()); } } /** {@inheritDoc} */ @Override public void restoreAutoRangeBounds() { super.restoreAutoRangeBounds(); if (chart.getPlot() instanceof XYPlot) { setAutoBoundRange(chart.getXYPlot()); } } /** {@inheritDoc} This implementation adds control over the PNG image size and font size. */ @Override public void doSaveAs() throws IOException { // the code in this method is based on the code in the super implementation // create setting components JLabel fontSizeLabel = new JLabel("font size"); JTextField fontSize = new JTextField("32"); // by default, give more space for labels in a png export fontSize.setToolTipText("Font size of title (other fonts are scaled)"); fontSize.setPreferredSize(new Dimension(40, 20)); JTextField width = new JTextField("960"); width.setToolTipText("Width [pixels]"); width.setPreferredSize(new Dimension(40, 20)); JLabel x = new JLabel("x"); JTextField height = new JTextField("540"); height.setToolTipText("Height [pixels]"); height.setPreferredSize(new Dimension(40, 20)); // create file chooser with these components JFileChooser fileChooser = new JFileChooserWithSettings(fontSizeLabel, fontSize, width, x, height); fileChooser.setCurrentDirectory(getDefaultDirectoryForSaveAs()); FileNameExtensionFilter filter = new FileNameExtensionFilter(localizationResources.getString("PNG_Image_Files"), "png"); fileChooser.addChoosableFileFilter(filter); fileChooser.setFileFilter(filter); int option = fileChooser.showSaveDialog(this); if (option == JFileChooser.APPROVE_OPTION) { String filename = fileChooser.getSelectedFile().getPath(); if (isEnforceFileExtensions()) { if (!filename.endsWith(".png")) { filename = filename + ".png"; } } // get settings from setting components double fs; // relative scale try { fs = Double.parseDouble(fontSize.getText()); } catch (NumberFormatException exception) { fs = 16.0; } int w; try { w = Integer.parseInt(width.getText()); } catch (NumberFormatException exception) { w = getWidth(); } int h; try { h = Integer.parseInt(height.getText()); } catch (NumberFormatException exception) { h = getHeight(); } OutputStream out = new BufferedOutputStream(new FileOutputStream(new File(filename))); out.write(encodeAsPng(w, h, fs)); out.close(); } } }; ChartMouseListener chartListener = getChartMouseListener(); if (chartListener != null) { chartPanel.addChartMouseListener(chartListener); } // pointer handler final PointerHandler ph = new PointerHandler() { /** {@inheritDoc} */ @Override public void updateHint(final double domainValue, final double rangeValue) { if (Double.isNaN(domainValue)) { setStatusLabel(" "); } else { setStatusLabel(getStatusLabel(domainValue, rangeValue)); } } }; chartPanel.addMouseMotionListener(ph); chartPanel.addMouseListener(ph); add(chartPanel, BorderLayout.CENTER); chartPanel.setMouseWheelEnabled(true); // pop up JPopupMenu popupMenu = chartPanel.getPopupMenu(); popupMenu.add(new JPopupMenu.Separator()); this.detach = new JMenuItem("Show in detached window"); this.detach.addActionListener(new ActionListener() { @SuppressWarnings("synthetic-access") @Override public void actionPerformed(final ActionEvent e) { AbstractPlot.this.detach.setEnabled(false); JFrame window = new JFrame(AbstractPlot.this.caption); window.setDefaultCloseOperation(DISPOSE_ON_CLOSE); window.add(chartPanel, BorderLayout.CENTER); window.add(AbstractPlot.this.statusLabel, BorderLayout.SOUTH); window.addWindowListener(new WindowAdapter() { /** {@inheritDoc} */ @Override public void windowClosing(@SuppressWarnings("hiding") final WindowEvent e) { add(chartPanel, BorderLayout.CENTER); add(AbstractPlot.this.statusLabel, BorderLayout.SOUTH); AbstractPlot.this.detach.setEnabled(true); AbstractPlot.this.getContentPane().validate(); AbstractPlot.this.getContentPane().repaint(); } }); window.pack(); window.setVisible(true); AbstractPlot.this.getContentPane().repaint(); } }); popupMenu.add(this.detach); addPopUpMenuItems(popupMenu); } /** * Returns the chart as a byte array representing a png image. * @param width int; width * @param height int; height * @param fontSize double; font size (16 is the original on screen size) * @return byte[]; the chart as a byte array representing a png image * @throws IOException on IO exception */ public byte[] encodeAsPng(final int width, final int height, final double fontSize) throws IOException { // to double the font size, we halve the base dimensions // JFreeChart will the assign more area (relatively) to the fixed actual font size double baseWidth = width / (fontSize / 16); double baseHeight = height / (fontSize / 16); // this code is from ChartUtils.writeScaledChartAsPNG BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = image.createGraphics(); // to compensate for the base dimensions which are not w x h, we scale the drawing AffineTransform saved = g2.getTransform(); g2.transform(AffineTransform.getScaleInstance(width / baseWidth, height / baseHeight)); getChart().draw(g2, new Rectangle2D.Double(0, 0, baseWidth, baseHeight), null, null); g2.setTransform(saved); g2.dispose(); return ChartUtils.encodeAsPNG(image); } /** {@inheritDoc} */ @Override public final DatasetGroup getGroup() { return null; // not used } /** {@inheritDoc} */ @Override public final void setGroup(final DatasetGroup group) { // not used } /** * Overridable method to add pop up items. * @param popupMenu JPopupMenu; pop up menu */ protected void addPopUpMenuItems(final JPopupMenu popupMenu) { // } /** * Overridable; activates auto bounds on domain axis from user input. This class does not force the use of {@code XYPlot}s, * but the auto bounds command comes from the {@code ChartPanel} that this class creates. In case the used plot is a * {@code XYPlot}, this method is then invoked. Sub classes with auto domain bounds that work with an {@code XYPlot} should * implement this. The method is not abstract as the use of {@code XYPlot} is not obligated. * @param plot XYPlot; plot */ protected void setAutoBoundDomain(final XYPlot plot) { // } /** * Overridable; activates auto bounds on range axis from user input. This class does not force the use of {@code XYPlot}s, * but the auto bounds command comes from the {@code ChartPanel} that this class creates. In case the used plot is a * {@code XYPlot}, this method is then invoked. Sub classes with auto range bounds that work with an {@code XYPlot} should * implement this. The method is not abstract as the use of {@code XYPlot} is not obligated. * @param plot XYPlot; plot */ protected void setAutoBoundRange(final XYPlot plot) { // } /** * Overridable; may return a chart listener for additional functions. * @return ChartMouseListener, {@code null} by default */ protected ChartMouseListener getChartMouseListener() { return null; } /** * Return the graph type for transceiver. * @return GraphType; the graph type. */ public abstract GraphType getGraphType(); /** * Returns the status label when the mouse is over the given location. * @param domainValue double; domain value (x-axis) * @param rangeValue double; range value (y-axis) * @return String; status label when the mouse is over the given location */ protected abstract String getStatusLabel(double domainValue, double rangeValue); /** * Increase the simulated time span. * @param time Time; time to increase to */ protected abstract void increaseTime(Time time); /** * Redraws the plot and schedules the next update. */ protected void update() { this.updateTime = this.simulator.getSimulatorTime(); increaseTime(this.updateTime.minus(this.delay)); notifyPlotChange(); scheduleNextUpdateEvent(); } /** * Schedules the next update event. */ private void scheduleNextUpdateEvent() { try { this.updates++; // events are scheduled slightly later, so all influencing movements have occurred this.updateEvent = this.simulator.scheduleEventAbs( Time.createSI(this.updateInterval.si * this.updates + this.delay.si), this, this, "update", null); } catch (SimRuntimeException exception) { throw new RuntimeException("Unexpected exception while updating plot.", exception); } } /** * Notify all change listeners. */ public final void notifyPlotChange() { DatasetChangeEvent event = new DatasetChangeEvent(this, this); for (DatasetChangeListener dcl : this.listeners) { dcl.datasetChanged(event); } } /** * Sets a new update interval. * @param interval Duration; update interval */ protected final void setUpdateInterval(final Duration interval) { if (this.updateEvent != null) { this.simulator.cancelEvent(this.updateEvent); } this.updates = (int) (this.simulator.getSimulatorTime().si / interval.si); this.updateInterval = interval; this.updateTime = Time.createSI(this.updates * this.updateInterval.si); scheduleNextUpdateEvent(); } /** * Returns time until which data should be plotted. * @return Time; time until which data should be plotted */ protected final Time getUpdateTime() { return this.updateTime; } /** * Returns the chart. * @return JFreeChart; chart */ protected final JFreeChart getChart() { return this.chart; } /** {@inheritDoc} */ @Override public final String getId() { return this.id; } /** {@inheritDoc} */ @Override public final void addChangeListener(final DatasetChangeListener listener) { this.listeners.add(listener); } /** {@inheritDoc} */ @Override public final void removeChangeListener(final DatasetChangeListener listener) { this.listeners.remove(listener); } /** * Manually set status label from sub class. Will be overwritten by a moving mouse pointer over the axes. * @param label String; label to set */ protected final void setStatusLabel(final String label) { if (this.statusLabel != null) { this.statusLabel.setText(label); } } /** * Return the caption of this graph. * @return String; the caption of this graph */ public final String getCaption() { return this.caption; } }