/* * A labeled box for signal plots. * * @Copyright (c) 1997-2008 The Regents of the University of California. All * rights reserved. * * Permission is hereby granted, without written agreement and without license * or royalty fees, to use, copy, modify, and distribute this software and its * documentation for any purpose, provided that the above copyright notice and * the following two paragraphs appear in all copies of this software. * * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT * OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY OF * CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN * "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATION TO PROVIDE * MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. * * PT_COPYRIGHT_VERSION_2 COPYRIGHTENDKEY */ package ptolemy.plot; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.FlowLayout; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.image.BufferedImage; import java.awt.print.PageFormat; import java.awt.print.Printable; import java.awt.print.PrinterException; import java.awt.print.PrinterJob; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Writer; import java.net.URL; import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.Vector; import javax.print.attribute.HashPrintRequestAttributeSet; import javax.print.attribute.PrintRequestAttributeSet; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.SwingUtilities; import ptolemy.plot.zoomBox.ZoomRectangle; import ptolemy.util.FileUtilities; import ptolemy.util.StringUtilities; // TO DO: // - Augment getColorByName to support a full complement of colors // (get the color list from Tycho). // //////////////////////////////////////////////////////////////////////// // // PlotBox /** * This class provides a labeled box within which to place a data plot. A title, * X and Y axis labels, tick marks, and a legend are all supported. Zooming in * and out is supported. To zoom in, drag the mouse downwards to draw a box. To * zoom out, drag the mouse upward. * <p> * The box can be configured either through a file with commands or through * direct invocation of the public methods of the class. * <p> * When calling the methods, in most cases the changes will not be visible until * paintComponent() has been called. To request that this be done, call * repaint(). * <p> * A small set of key bindings are provided for convenience. They are: * <ul> * <li>Cntrl-c: Export the plot to the clipboard (in PlotML). * <li>D: Dump the plot to standard output (in PlotML). * <li>E: Export the plot to standard output in EPS format. * <li>F: Fill the plot. * <li>H or ?: Display a simple help message. * <li>Cntrl-D or Q: quit * </ul> * These commands are provided in a menu by the PlotFrame class. Note that * exporting to the clipboard is not allowed in applets (it used to be), so this * will result in an error message. * <p> * At this time, the two export commands produce encapsulated postscript tuned * for black-and-white printers. In the future, more formats may supported. * Exporting to the clipboard and to standard output, in theory, is allowed for * applets, unlike writing to a file. Thus, these key bindings provide a simple * mechanism to obtain a high-resolution image of the plot from an applet, * suitable for incorporation in a document. However, in some browsers, * exporting to standard out triggers a security violation. You can use the JDK * appletviewer instead. * <p> * To read commands from a file or URL, the preferred technique is to use one of * the classes in the plotml package. That package supports both PlotML, an XML * extension for plots, and a historical file format specific to ptplot. The * historical file format is understood by the read() method in this class. The * syntax of the historical format, documented below, is rudimentary, and will * probably not be extended as ptplot evolves. Nonetheless, we document it here * since it is directly supported by this class. * <p> * The historical format for the file allows any number commands, one per line. * Unrecognized commands and commands with syntax errors are ignored. Comments * are denoted by a line starting with a pound sign "#". The recognized commands * include: * * <pre> * TitleText: <i>string</i> * XLabel: <i>string</i> * YLabel: <i>string</i> * </pre> * * These commands provide a title and labels for the X (horizontal) and Y * (vertical) axes. A <i>string</i> is simply a sequence of characters, possibly * including spaces. There is no need here to surround them with quotation * marks, and in fact, if you do, the quotation marks will be included in the * labels. * <p> * The ranges of the X and Y axes can be optionally given by commands like: * * <pre> * XRange: <i>min</i>, <i>max</i> * YRange: <i>min</i>, <i>max</i> * </pre> * * The arguments <i>min</i> and <i>max</i> are numbers, possibly including a * sign and a decimal point. If they are not specified, then the ranges are * computed automatically from the data and padded slightly so that datapoints * are not plotted on the axes. * <p> * The tick marks for the axes are usually computed automatically from the * ranges. Every attempt is made to choose reasonable positions for the tick * marks regardless of the data ranges (powers of ten multiplied by 1, 2, or 5 * are used). However, they can also be specified explicitly using commands * like: * * <pre> * XTicks: <i>label position, label position, ...</i> * YTicks: <i>label position, label position, ...</i> * </pre> * * A <i>label</i> is a string that must be surrounded by quotation marks if it * contains any spaces. A <i>position</i> is a number giving the location of the * tick mark along the axis. For example, a horizontal axis for a frequency * domain plot might have tick marks as follows: * * <pre> * XTicks: -PI -3.14159, -PI/2 -1.570795, 0 0, PI/2 1.570795, PI 3.14159 * </pre> * * Tick marks could also denote years, months, days of the week, etc. * <p> * The X and Y axes can use a logarithmic scale with the following commands: * * <pre> * XLog: on * YLog: on * </pre> * * The grid labels represent powers of 10. Note that if a logarithmic scale is * used, then the values must be positive. Non-positive values will be silently * dropped. Note further that when using logarithmic axes that the log of input * data is taken as the data is added to the plot. This means that * * <pre> * XLog: on * </pre> * * or * * <pre> * YLog: on * </pre> * * should appear before any data. Also, the value of the XTicks, YTicks, XRange * or YRange directives should be in log units. So, * * <pre> * XTicks: 1K 3 * </pre> * * will display the string * * <pre> * 1K * </pre> * * at the 1000 mark. * <p> * By default, tick marks are connected by a light grey background grid. This * grid can be turned off with the following command: * * <pre> * Grid: off * </pre> * * It can be turned back on with * * <pre> * Grid: on * </pre> * * Also, by default, the first ten data sets are shown each in a unique color. * The use of color can be turned off with the command: * * <pre> * Color: off * </pre> * * It can be turned back on with * * <pre> * Color: on * </pre> * * Finally, the rather specialized command * * <pre> * Wrap: on * </pre> * * enables wrapping of the X (horizontal) axis, which means that if a point is * added with X out of range, its X value will be modified modulo the range so * that it lies in range. This command only has an effect if the X range has * been set explicitly. It is designed specifically to support oscilloscope-like * behavior, where the X value of points is increasing, but the display wraps it * around to left. A point that lands on the right edge of the X range is * repeated on the left edge to give a better sense of continuity. The feature * works best when points do land precisely on the edge, and are plotted from * left to right, increasing in X. * <p> * All of the above commands can also be invoked directly by calling the the * corresponding public methods from some Java procedure. * <p> * This class uses features of JDK 1.2, and hence if used in an applet, it can * only be viewed by a browser that supports JDK 1.2, or a plugin. * * @author Edward A. Lee, Christopher Brooks, Contributors: Jun Wu * ([email protected]), William Wu, Robert Kroeger * @version $Id: PlotBox.java,v 1.283.4.3 2008/04/01 01:19:02 cxh Exp $ * @since Ptolemy II 0.2 * @Pt.ProposedRating Yellow (cxh) * @Pt.AcceptedRating Yellow (cxh) */ public class PlotBox extends JPanel implements Printable { // ///////////////////////////////////////////////////////////////// // // constructor //// private static final long serialVersionUID = 1L; protected boolean plotBoxDrawsZoomRect = true; /** Construct a plot box with a default configuration. */ public PlotBox() { // If we make this transparent, the background shows through. // However, we assume that the user will set the background. // NOTE: A component is transparent by default (?). // setOpaque(false); setOpaque(true); // Create a right-justified layout with spacing of 2 pixels. setLayout(new FlowLayout(FlowLayout.RIGHT, 2, 2)); addMouseListener(new ZoomListener()); addKeyListener(new CommandListener()); addMouseMotionListener(new DragListener()); // Request the focus so that key events are heard. // NOTE: no longer needed? // requestFocus(); revalidate(); repaint(); } // ///////////////////////////////////////////////////////////////// // // public methods //// /** * Add a legend (displayed at the upper right) for the specified data set * with the specified string. Short strings generally fit better than long * strings. If the string is empty, or the argument is null, then no legend * is added. * * @param dataset * The dataset index. * @param legend * The label for the dataset. * @see #renameLegend(int, String) */ public synchronized void addLegend(int dataset, String legend) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; if ((legend == null) || legend.equals("")) { return; } _legendStrings.addElement(legend); _legendDatasets.addElement(Integer.valueOf(dataset)); } /** * Specify a tick mark for the X axis. The label given is placed on the axis * at the position given by <i>position</i>. If this is called once or more, * automatic generation of tick marks is disabled. The tick mark will appear * only if it is within the X range. * * @param label * The label for the tick mark. * @param position * The position on the X axis. */ public synchronized void addXTick(String label, double position) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; if (_xticks == null) { _xticks = new Vector<Double>(); _xticklabels = new Vector<String>(); } _xticks.addElement(Double.valueOf(position)); _xticklabels.addElement(label); } /** * Specify a tick mark for the Y axis. The label given is placed on the axis * at the position given by <i>position</i>. If this is called once or more, * automatic generation of tick marks is disabled. The tick mark will appear * only if it is within the Y range. * * @param label * The label for the tick mark. * @param position * The position on the Y axis. */ public synchronized void addYTick(String label, double position) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; if (_yticks == null) { _yticks = new Vector<Double>(); _yticklabels = new Vector<String>(); } _yticks.addElement(Double.valueOf(position)); _yticklabels.addElement(label); } /** * If the argument is true, clear the axes. I.e., set all parameters * controlling the axes to their initial conditions. For the change to take * effect, call repaint(). If the argument is false, do nothing. * * @param axes * If true, clear the axes parameters. */ public synchronized void clear(boolean axes) { // We need to repaint the offscreen buffer. _plotImage = null; _xBottom = Double.MAX_VALUE; _xTop = -Double.MAX_VALUE; _yBottom = Double.MAX_VALUE; _yTop = -Double.MAX_VALUE; if (axes) { // Protected members first. _yMax = 0; _yMin = 0; _xMax = 0; _xMin = 0; _xRangeGiven = false; _yRangeGiven = false; _originalXRangeGiven = false; _originalYRangeGiven = false; _rangesGivenByZooming = false; _xlog = false; _ylog = false; _grid = true; _wrap = false; _usecolor = true; // Private members next... _xlabel = null; _ylabel = null; _title = null; _legendStrings = new Vector<String>(); _legendDatasets = new Vector<Integer>(); _xticks = null; _xticklabels = null; _yticks = null; _yticklabels = null; } } /** * Clear all legends. This will show up on the next redraw. */ public synchronized void clearLegends() { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _legendStrings = new Vector<String>(); _legendDatasets = new Vector<Integer>(); } /** * If this method is called in the event thread, then simply execute the * specified action. Otherwise, if there are already deferred actions, then * add the specified one to the list. Otherwise, create a list of deferred * actions, if necessary, and request that the list be processed in the * event dispatch thread. * * Note that it does not work nearly as well to simply schedule the action * yourself on the event thread because if there are a large number of * actions, then the event thread will not be able to keep up. By grouping * these actions, we avoid this problem. * * This method is not synchronized, so the caller should be. * * @param action * The Runnable object to execute. */ public void deferIfNecessary(Runnable action) { // In swing, updates to showing graphics must be done in the // event thread. If we are in the event thread, then proceed. // Otherwise, queue a request or add to a pending request. if (EventQueue.isDispatchThread()) { action.run(); } else { if (_deferredActions == null) { _deferredActions = new LinkedList<Runnable>(); } // Add the specified action to the list of actions to perform. _deferredActions.add(action); // If it hasn't already been requested, request that actions // be performed in the event dispatch thread. if (!_actionsDeferred) { Runnable doActions = new Runnable() { @Override public void run() { _executeDeferredActions(); } }; try { // NOTE: Using invokeAndWait() here risks causing // deadlock. Don't do it! SwingUtilities.invokeLater(doActions); } catch (Exception ex) { // Ignore InterruptedException. // Other exceptions should not occur. } _actionsDeferred = true; } } } /** * Export a description of the plot. Currently, only EPS is supported. But * in the future, this may cause a dialog box to open to allow the user to * select a format. If the argument is null, then the description goes to * the clipboard. Otherwise, it goes to the specified file. To send it to * standard output, use <code>System.out</code> as an argument. * * @param out * An output stream to which to send the description. */ public synchronized void export(OutputStream out) { try { EPSGraphics g = new EPSGraphics(out, _width, _height); _drawPlot(g, false); g.showpage(); } catch (RuntimeException ex) { String message = "Export failed: " + ex.getMessage(); JOptionPane.showMessageDialog(this, message, "Ptolemy Plot Message", JOptionPane.ERROR_MESSAGE); // Rethrow the exception so that we don't report success, // and so the stack trace is displayed on standard out. throw (RuntimeException) ex.fillInStackTrace(); } } // CONTRIBUTED CODE. // I wanted the ability to use the Plot object in a servlet and to // write out the resultant images. The following routines, // particularly exportImage(), permit this. I also had to make some // minor changes elsewhere. Rob Kroeger, May 2001. // NOTE: This code has been modified by EAL to conform with Ptolemy II // coding style. /** * Create a BufferedImage and draw this plot to it. The size of the returned * image matches the current size of the plot. This method can be used, for * example, by a servlet to produce an image, rather than requiring an * applet to instantiate a PlotBox. * * @return An image filled by the plot. */ public synchronized BufferedImage exportImage() { Rectangle rectangle = new Rectangle(_preferredWidth, _preferredHeight); return exportImage(new BufferedImage(rectangle.width, rectangle.height, BufferedImage.TYPE_INT_ARGB), rectangle, _defaultImageRenderingHints(), false); } /** * Create a BufferedImage the size of the given rectangle and draw this plot * to it at the position specified by the rectangle. The plot is rendered * using anti-aliasing. * * @param rectangle * The size of the plot. This method can be used, for example, by * a servlet to produce an image, rather than requiring an applet * to instantiate a PlotBox. * @return An image containing the plot. */ public synchronized BufferedImage exportImage(Rectangle rectangle) { return exportImage(new BufferedImage(rectangle.width, rectangle.height, BufferedImage.TYPE_INT_ARGB), rectangle, _defaultImageRenderingHints(), false); } /** * Draw this plot onto the specified image at the position of the specified * rectangle with the size of the specified rectangle. The plot is rendered * using anti-aliasing. This can be used to paint a number of different * plots onto a single buffered image. This method can be used, for example, * by a servlet to produce an image, rather than requiring an applet to * instantiate a PlotBox. * * @param bufferedImage * Image onto which the plot is drawn. * @param rectangle * The size and position of the plot in the image. * @param hints * Rendering hints for this plot. * @param transparent * Indicator that the background of the plot should not be * painted. * @return The modified bufferedImage. */ public synchronized BufferedImage exportImage(BufferedImage bufferedImage, Rectangle rectangle, RenderingHints hints, boolean transparent) { Graphics2D graphics = bufferedImage.createGraphics(); graphics.addRenderingHints(_defaultImageRenderingHints()); if (!transparent) { graphics.setColor(Color.white); // set the background color graphics.fill(rectangle); } _drawPlot(graphics, false, rectangle); return bufferedImage; } /** * Draw this plot onto the provided image. This method does not paint the * background, so the plot is transparent. The plot fills the image, and is * rendered using anti-aliasing. This method can be used to overlay multiple * plots on the same image, although you must use care to ensure that the * axes and other labels are identical. Hence, it is usually better to * simply combine data sets into a single plot. * * @param bufferedImage * The image onto which to render the plot. * @return The modified bufferedImage. */ public synchronized BufferedImage exportImage(BufferedImage bufferedImage) { return exportImage(bufferedImage, new Rectangle(bufferedImage.getWidth(), bufferedImage.getHeight()), _defaultImageRenderingHints(), true); } /** * Rescale so that the data that is currently plotted just fits. This is * done based on the protected variables _xBottom, _xTop, _yBottom, and * _yTop. It is up to derived classes to ensure that variables are valid. * This method calls repaint(), which eventually causes the display to be * updated. */ public synchronized void fillPlot() { // NOTE: These used to be _setXRange() and _setYRange() to avoid // confusing this with user-specified ranges. But we want to treat // a fill command as a user specified range. // EAL, 6/12/00. setXRange(_xBottom, _xTop); setYRange(_yBottom, _yTop); repaint(); // Reacquire the focus so that key bindings work. // NOTE: no longer needed? // requestFocus(); } /** * Return whether the plot uses color. * * @return True if the plot uses color. */ public boolean getColor() { return _usecolor; } /** * Get the point colors. * * @return Array of colors * @see #setColors(Color[]) */ public Color[] getColors() { return _colors; } /** * Convert a color name into a Color. Currently, only a very limited set of * color names is supported: black, white, red, green, and blue. * * @param name * A color name, or null if not found. * @return An instance of Color. */ public static Color getColorByName(String name) { try { // Check to see if it is a hexadecimal if (name.startsWith("#")) { name = name.substring(1); } Color col = new Color(Integer.parseInt(name, 16)); return col; } catch (NumberFormatException e) { } // FIXME: This is a poor excuse for a list of colors and values. // We should use a hash table here. // Note that Color decode() wants the values to start with 0x. String[][] names = { { "black", "00000" }, { "white", "ffffff" }, { "red", "ff0000" }, { "green", "00ff00" }, { "blue", "0000ff" } }; for (int i = 0; i < names.length; i++) { if (name.equals(names[i][0])) { try { Color col = new Color(Integer.parseInt(names[i][1], 16)); return col; } catch (NumberFormatException e) { } } } return null; } /** * Return whether the grid is drawn. * * @return True if a grid is drawn. */ public boolean getGrid() { return _grid; } /** * Get the legend for a dataset, or null if there is none. The legend would * have been set by addLegend(). * * @param dataset * The dataset index. * @return The legend label, or null if there is none. */ public synchronized String getLegend(int dataset) { int idx = _legendDatasets.indexOf(Integer.valueOf(dataset), 0); if (idx != -1) { return _legendStrings.elementAt(idx); } return null; } /** * Given a legend string, return the corresponding dataset or -1 if no * legend was added with that legend string The legend would have been set * by addLegend(). * * @param legend * The String naming the legend * @return The legend dataset, or -1 if not found. * @since Ptplot 5.2p1 */ public synchronized int getLegendDataset(String legend) { int index = _legendStrings.indexOf(legend); if (index == -1) { return -1; } return _legendDatasets.get(index).intValue(); } /** * If the size of the plot has been set by setSize(), then return that size. * Otherwise, return what the superclass returns (which is undocumented, but * apparently imposes no maximum size). Currently (JDK 1.3), only BoxLayout * pays any attention to this. * * @return The maximum desired size. */ // public synchronized Dimension getMaximumSize() { // if (_sizeHasBeenSet) { // return new Dimension(_preferredWidth, _preferredHeight); // } else { // return super.getMaximumSize(); // } // } /** * Get the minimum size of this component. This is simply the dimensions * specified by setSize(), if this has been called. Otherwise, return * whatever the base class returns, which is undocumented. * * @return The minimum size. */ // public synchronized Dimension getMinimumSize() { // if (_sizeHasBeenSet) { // return new Dimension(_preferredWidth, _preferredHeight); // } else { // return super.getMinimumSize(); // } // } /** * Get the current plot rectangle. Note that Rectangle returned by this * method is calculated from the values of {@link #_ulx}, {@link #_uly}, * {@link #_lrx} and {@link #_lry}. The value passed in by * setPlotRectangle() is not directly used, thus calling getPlotRectangle() * may not return the same rectangle that was passed in with * setPlotRectangle(). * * @return Rectangle * @see #setPlotRectangle(Rectangle) */ public Rectangle getPlotRectangle() { return new Rectangle(_ulx, _uly, _lrx - _ulx, _lry - _uly); } /** * Get the preferred size of this component. This is simply the dimensions * specified by setSize(), if this has been called, or the default width and * height otherwise (500 by 300). * * @return The preferred size. */ @Override public synchronized Dimension getPreferredSize() { return new Dimension(_preferredWidth, _preferredHeight); } /** * Get the title of the graph, or an empty string if there is none. * * @return The title. */ public synchronized String getTitle() { if (_title == null) { return ""; } return _title; } /** * Get the range for X values of the data points registered so far. Usually, * derived classes handle managing the range by checking each new point * against the current range. * * @return An array of two doubles where the first element is the minimum * and the second element is the maximum. * @see #getXRange() */ public synchronized double[] getXAutoRange() { double[] result = new double[2]; result[0] = _xBottom; result[1] = _xTop; return result; } /** * Get the label for the X (horizontal) axis, or null if none has been set. * * @return The X label. */ public synchronized String getXLabel() { return _xlabel; } /** * Return whether the X axis is drawn with a logarithmic scale. * * @return True if the X axis is logarithmic. */ public boolean getXLog() { return _xlog; } /** * Get the X range. If {@link #setXRange(double, double)} has been called, * then this method returns the values passed in as arguments to * setXRange(double, double). If setXRange(double, double) has not been * called, then this method returns the range of the data to be plotted, * which might not be all of the data due to zooming. * * @return An array of two doubles where the first element is the minimum * and the second element is the maximum. * @see #getXAutoRange() */ public synchronized double[] getXRange() { double[] result = new double[2]; if (_xRangeGiven) { result[0] = _xlowgiven; result[1] = _xhighgiven; } else { // Have to first correct for the padding. result[0] = _xMin + ((_xMax - _xMin) * _padding); result[1] = _xMax - ((_xMax - _xMin) * _padding); } return result; } /** * Get the X ticks that have been specified, or null if none. The return * value is an array with two vectors, the first of which specifies the X * tick locations (as instances of Double), and the second of which * specifies the corresponding labels. * * @return The X ticks. */ public synchronized Vector<?>[] getXTicks() { if (_xticks == null) { return null; } Vector<?>[] result = new Vector[2]; result[0] = _xticks; result[1] = _xticklabels; return result; } /** * Get the range for Y values of the data points registered so far. Usually, * derived classes handle managing the range by checking each new point * against the range. * * @return An array of two doubles where the first element is the minimum * and the second element is the maximum. * @see #getYRange() */ public synchronized double[] getYAutoRange() { double[] result = new double[2]; result[0] = _yBottom; result[1] = _yTop; return result; } /** * Get the label for the Y (vertical) axis, or null if none has been set. * * @return The Y label. */ public String getYLabel() { return _ylabel; } /** * Return whether the Y axis is drawn with a logarithmic scale. * * @return True if the Y axis is logarithmic. */ public boolean getYLog() { return _ylog; } /** * Get the Y range. If {@link #setYRange(double, double)} has been called, * then this method returns the values passed in as arguments to * setYRange(double, double). If setYRange(double, double) has not been * called, then this method returns the range of the data to be plotted, * which might not be all of the data due to zooming. * * @return An array of two doubles where the first element is the minimum * and the second element is the maximum. * @see #getYAutoRange() */ public synchronized double[] getYRange() { double[] result = new double[2]; if (_yRangeGiven) { result[0] = _ylowgiven; result[1] = _yhighgiven; } else { // Have to first correct for the padding. result[0] = _yMin + ((_yMax - _yMin) * _padding); result[1] = _yMax - ((_yMax - _yMin) * _padding); ; } return result; } /** * Get the Y ticks that have been specified, or null if none. The return * value is an array with two vectors, the first of which specifies the Y * tick locations (as instances of Double), and the second of which * specifies the corresponding labels. * * @return The Y ticks. */ public synchronized Vector<?>[] getYTicks() { if (_yticks == null) { return null; } Vector<?>[] result = new Vector<?>[2]; result[0] = _yticks; result[1] = _yticklabels; return result; } /** * Paint the component contents, which in this base class is only the axes. * * @param graphics * The graphics context. */ @Override public synchronized void paintComponent(Graphics graphics) { // super.paintComponent(graphics); // _drawPlot(graphics, true); BufferedImage newPlotImage = _plotImage; if (newPlotImage == null) { Rectangle bounds = getBounds(); newPlotImage = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_3BYTE_BGR); _plotImage = newPlotImage; offScreenGraphics = newPlotImage.createGraphics(); offScreenGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); offScreenGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED); super.paintComponent(offScreenGraphics); _drawPlot(offScreenGraphics, true); } // Blit the offscreen image onto the screen. graphics.drawImage(newPlotImage, 0, 0, null); // Acquire the focus so that key bindings work. // NOTE: no longer needed? // requestFocus(); } /** * Print the plot to a printer, represented by the specified graphics * object. * * @param graphics * The context into which the page is drawn. * @param format * The size and orientation of the page being drawn. * @param index * The zero based index of the page to be drawn. * @return PAGE_EXISTS if the page is rendered successfully, or NO_SUCH_PAGE * if pageIndex specifies a non-existent page. * @exception PrinterException * If the print job is terminated. */ @Override public synchronized int print(Graphics graphics, PageFormat format, int index) throws PrinterException { if (graphics == null) { return Printable.NO_SUCH_PAGE; } // We only print on one page. if (index >= 1) { return Printable.NO_SUCH_PAGE; } Graphics2D graphics2D = (Graphics2D) graphics; // Scale the printout to fit the pages. // Contributed by Laurent ETUR, Schlumberger Riboud Product Center double scalex = format.getImageableWidth() / getWidth(); double scaley = format.getImageableHeight() / getHeight(); double scale = Math.min(scalex, scaley); graphics2D.translate((int) format.getImageableX(), (int) format.getImageableY()); graphics2D.scale(scale, scale); _drawPlot(graphics, true); return Printable.PAGE_EXISTS; } /** * Read commands and/or plot data from an input stream in the old (non-XML) * file syntax. To update the display, call repaint(), or make the plot * visible with setVisible(true). * <p> * To read from standard input, use: * * <pre> * read(System.in); * </pre> * * To read from a url, use: * * <pre> * read(url.openStream()); * </pre> * * To read a URL from within an applet, use: * * <pre> * URL url = new URL(getDocumentBase(), urlSpec); * read(url.openStream()); * </pre> * * Within an application, if you have an absolute URL, use: * * <pre> * URL url = new URL(urlSpec); * read(url.openStream()); * </pre> * * To read from a file, use: * * <pre> * read(new FileInputStream(filename)); * </pre> * * @param in * The input stream. * @exception IOException * If the stream cannot be read. */ public synchronized void read(InputStream in) throws IOException { try { // NOTE: I tried to use exclusively the jdk 1.1 Reader classes, // but they provide no support like DataInputStream, nor // support for URL accesses. So I use the older classes // here in a strange mixture. BufferedReader din = new BufferedReader(new InputStreamReader(in)); try { String line = din.readLine(); while (line != null) { _parseLine(line); line = din.readLine(); } } finally { din.close(); } } catch (IOException e) { _errorMsg = new String[2]; _errorMsg[0] = "Failure reading input data."; _errorMsg[1] = e.getMessage(); throw e; } } /** * Read a single line command provided as a string. The commands can be any * of those in the ASCII file format. * * @param command * A command. */ public synchronized void read(String command) { _parseLine(command); } /** * Remove the legend (displayed at the upper right) for the specified data * set. If the dataset is not found, nothing will occur. The PlotBox must be * repainted in order for this to take effect. * * @param dataset * The dataset index. */ public synchronized void removeLegend(int dataset) { final int len = _legendDatasets.size(); int foundIndex = -1; boolean found = false; for (int i = 0; (i < len) && !found; ++i) { if (_legendDatasets.get(i).intValue() == dataset) { foundIndex = i; found = true; } } if (found) { _legendDatasets.remove(foundIndex); _legendStrings.remove(foundIndex); } } /** * Rename a legend. * * @param dataset * The dataset of the legend to be renamed. If there is no * dataset with this value, then nothing happens. * @param newName * The new name of legend. * @see #addLegend(int, String) */ public synchronized void renameLegend(int dataset, String newName) { int index = _legendDatasets.indexOf(Integer.valueOf(dataset), 0); if (index != -1) { _legendStrings.setElementAt(newName, index); // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; } } /** * Reset the X and Y axes to the ranges that were first specified using * setXRange() and setYRange(). If these methods have not been called, then * reset to the default ranges. This method calls repaint(), which * eventually causes the display to be updated. */ public synchronized void resetAxes() { setXRange(_originalXlow, _originalXhigh); setYRange(_originalYlow, _originalYhigh); repaint(); } /** * Do nothing in this base class. Derived classes might want to override * this class to give an example of their use. */ public void samplePlot() { // Empty default implementation. } /** * Set the background color. * * @param background * The background color. */ @Override public synchronized void setBackground(Color background) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _background = background; super.setBackground(_background); } /** * Move and resize this component. The new location of the top-left corner * is specified by x and y, and the new size is specified by width and * height. This overrides the base class method to make a record of the new * size. * * @param x * The new x-coordinate of this component. * @param y * The new y-coordinate of this component. * @param width * The new width of this component. * @param height * The new height of this component. */ @Override public synchronized void setBounds(int x, int y, int width, int height) { _width = width; _height = height; // Resizing the component means we need to redraw the buffer. _plotImage = null; super.setBounds(x, y, _width, _height); } /** * If the argument is true, make a fill button visible at the upper right. * This button auto-scales the plot. NOTE: The button may infringe on the * title space, if the title is long. In an application, it is preferable to * provide a menu with the fill command. This way, when printing the plot, * the printed plot will not have a spurious button. Thus, this method * should be used only by applets, which normally do not have menus. This * method should only be called from within the event dispatch thread, since * it interacts with swing. */ public synchronized void setButtons(boolean visible) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; if (_printButton == null) { // Load the image by using the absolute path to the gif. URL img = null; try { // FindBugs: Usage of GetResource may be unsafe if // class is extended img = FileUtilities.nameToURL("$CLASSPATH/ptolemy/plot/img/print.gif", null, null); } catch (IOException ex) { ex.printStackTrace(); } if (img != null) { ImageIcon printIcon = new ImageIcon(img); _printButton = new JButton(printIcon); _printButton.setBorderPainted(false); } else { // Backup in case something goes wrong with the // class loader. _printButton = new JButton("P"); } // FIXME: If we failed to get an image, then the letter "P" // Is not likely to fit into a 20x20 button. _printButton.setPreferredSize(new Dimension(20, 20)); _printButton.setToolTipText("Print the plot."); _printButton.addActionListener(new ButtonListener()); add(_printButton); } _printButton.setVisible(visible); if (_resetButton == null) { // Load the image by using the absolute path to the gif. URL img = null; try { // FindBugs: Usage of GetResource may be unsafe if // class is extended img = FileUtilities.nameToURL("$CLASSPATH/ptolemy/plot/img/reset.gif", null, null); } catch (IOException ex) { ex.printStackTrace(); } if (img != null) { ImageIcon resetIcon = new ImageIcon(img); _resetButton = new JButton(resetIcon); _resetButton.setBorderPainted(false); } else { // Backup in case something goes wrong with the // class loader. _resetButton = new JButton("R"); } // FIXME: If we failed to get an image, then the letter "R" // Is not likely to fit into a 20x20 button. _resetButton.setPreferredSize(new Dimension(20, 20)); _resetButton.setToolTipText("Reset X and Y ranges to their original values"); _resetButton.addActionListener(new ButtonListener()); add(_resetButton); } _resetButton.setVisible(visible); if (_formatButton == null) { // Load the image by using the absolute path to the gif. URL img = null; try { // FindBugs: Usage of GetResource may be unsafe if // class is extended img = FileUtilities.nameToURL("$CLASSPATH/ptolemy/plot/img/format.gif", null, null); } catch (IOException ex) { ex.printStackTrace(); } if (img != null) { ImageIcon formatIcon = new ImageIcon(img); _formatButton = new JButton(formatIcon); _formatButton.setBorderPainted(false); } else { // Backup in case something goes wrong with the // class loader. _formatButton = new JButton("S"); } // FIXME: If we failed to get an image, then the letter "S" // Is not likely to fit into a 20x20 button. _formatButton.setPreferredSize(new Dimension(20, 20)); _formatButton.setToolTipText("Set the plot format"); _formatButton.addActionListener(new ButtonListener()); add(_formatButton); } _formatButton.setVisible(visible); if (_fillButton == null) { // Load the image by using the absolute path to the gif. URL img = null; try { // FindBugs: Usage of GetResource may be unsafe if // class is extended img = FileUtilities.nameToURL("$CLASSPATH/ptolemy/plot/img/fill.gif", null, null); } catch (IOException ex) { ex.printStackTrace(); } if (img != null) { ImageIcon fillIcon = new ImageIcon(img); _fillButton = new JButton(fillIcon); _fillButton.setBorderPainted(false); } else { // Backup in case something goes wrong with the // class loader. _fillButton = new JButton("F"); } // FIXME: If we failed to get an image, then the letter "F" // Is not likely to fit into a 20x20 button. _fillButton.setPreferredSize(new Dimension(20, 20)); _fillButton.setToolTipText("Rescale the plot to fit the data"); _fillButton.addActionListener(new ButtonListener()); add(_fillButton); } _fillButton.setVisible(visible); // Request the focus so that key events are heard. // NOTE: no longer needed? // requestFocus(); } /** * If the argument is false, draw the plot without using color (in black and * white). Otherwise, draw it in color (the default). * * @param useColor * False to draw in back and white. */ public synchronized void setColor(boolean useColor) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _usecolor = useColor; } /** * Set the point colors. Note that the default colors have been carefully * selected to maximize readability and that it is easy to use colors that * result in a very ugly plot. * * @param colors * Array of colors to use in succession for data sets. * @see #getColors() */ public synchronized void setColors(Color[] colors) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _colors = colors; } /** * Set the foreground color. * * @param foreground * The foreground color. */ @Override public synchronized void setForeground(Color foreground) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _foreground = foreground; super.setForeground(_foreground); } /** * Control whether the grid is drawn. * * @param grid * If true, a grid is drawn. */ public synchronized void setGrid(boolean grid) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _grid = grid; } /** * Set the label font, which is used for axis labels and legend labels. The * font names understood are those understood by java.awt.Font.decode(). * * @param name * A font name. */ public synchronized void setLabelFont(String name) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _labelFont = Font.decode(name); _labelFontMetrics = getFontMetrics(_labelFont); } /** * Set the plot rectangle inside the axes. This method can be used to create * two plots that share the same axes. * * @param rectangle * Rectangle space inside axes. * @see #getPlotRectangle() */ public synchronized void setPlotRectangle(Rectangle rectangle) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _specifiedPlotRectangle = rectangle; } /** * Set the size of the plot. This overrides the base class to make it work. * In particular, it records the specified size so that getMinimumSize() and * getPreferredSize() return the specified value. However, it only works if * the plot is placed in its own JPanel. This is because the JPanel asks the * contained component for its preferred size before determining the size of * the panel. If the plot is placed directly in the content pane of a * JApplet, then, mysteriously, this method has no effect. * * @param width * The width, in pixels. * @param height * The height, in pixels. */ @Override public synchronized void setSize(int width, int height) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _width = width; _height = height; _preferredWidth = width; _preferredHeight = height; // _sizeHasBeenSet = true; super.setSize(width, height); } /** * Set the title of the graph. * * @param title * The title. */ public synchronized void setTitle(String title) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _title = title; } /** * Set the title font. The font names understood are those understood by * java.awt.Font.decode(). * * @param name * A font name. */ public synchronized void setTitleFont(String name) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _titleFont = Font.decode(name); _titleFontMetrics = getFontMetrics(_titleFont); } /** * Specify whether the X axis is wrapped. If it is, then X values that are * out of range are remapped to be in range using modulo arithmetic. The X * range is determined by the most recent call to setXRange() (or the most * recent zoom). If the X range has not been set, then use the default X * range, or if data has been plotted, then the current fill range. * * @param wrap * If true, wrapping of the X axis is enabled. */ public synchronized void setWrap(boolean wrap) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _wrap = wrap; if (!_xRangeGiven) { if (_xBottom > _xTop) { // have nothing to go on. setXRange(0, 0); } else { setXRange(_xBottom, _xTop); } } _wrapLow = _xlowgiven; _wrapHigh = _xhighgiven; } /** * Set the label for the X (horizontal) axis. * * @param label * The label. */ public synchronized void setXLabel(String label) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _xlabel = label; } /** * Specify whether the X axis is drawn with a logarithmic scale. If you * would like to have the X axis drawn with a logarithmic axis, then * setXLog(true) should be called before adding any data points. * * @param xlog * If true, logarithmic axis is used. */ public synchronized void setXLog(boolean xlog) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _xlog = xlog; } /** * Set the X (horizontal) range of the plot. If this is not done explicitly, * then the range is computed automatically from data available when the * plot is drawn. If min and max are identical, then the range is * arbitrarily spread by 1. * * @param min * The left extent of the range. * @param max * The right extent of the range. */ public synchronized void setXRange(double min, double max) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _xRangeGiven = true; _xlowgiven = min; _xhighgiven = max; _setXRange(min, max); fireAction(Action.MANUAL_ZOOM_ACTIVATED); } /** * Set the label for the Y (vertical) axis. * * @param label * The label. */ public synchronized void setYLabel(String label) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _ylabel = label; } /** * Specify whether the Y axis is drawn with a logarithmic scale. If you * would like to have the Y axis drawn with a logarithmic axis, then * setYLog(true) should be called before adding any data points. * * @param ylog * If true, logarithmic axis is used. */ public synchronized void setYLog(boolean ylog) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _ylog = ylog; } /** * Set the Y (vertical) range of the plot. If this is not done explicitly, * then the range is computed automatically from data available when the * plot is drawn. If min and max are identical, then the range is * arbitrarily spread by 0.1. * * @param min * The bottom extent of the range. * @param max * The top extent of the range. */ public synchronized void setYRange(double min, double max) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _yRangeGiven = true; _ylowgiven = min; _yhighgiven = max; _setYRange(min, max); fireAction(Action.MANUAL_ZOOM_ACTIVATED); } /** * Write the current data and plot configuration to the specified stream in * PlotML syntax. PlotML is an XML extension for plot data. The written * information is standalone, in that it includes the DTD (document type * definition). This makes is somewhat verbose. To get smaller files, use * the two argument version of write(). The output is buffered, and is * flushed and closed before exiting. Derived classes should override * writeFormat and writeData rather than this method. * * @param out * An output stream. */ public void write(OutputStream out) { write(out, null); } /** * Write the current data and plot configuration to the specified stream in * PlotML syntax. PlotML is an XML scheme for plot data. The URL (relative * or absolute) for the DTD is given as the second argument. If that * argument is null, then the PlotML PUBLIC DTD is referenced, resulting in * a file that can be read by a PlotML parser without any external file * references, as long as that parser has local access to the DTD. The * output is buffered, and is flushed and closed before exiting. Derived * classes should override writeFormat and writeData rather than this * method. * * @param out * An output stream. * @param dtd * The reference (URL) for the DTD, or null to use the PUBLIC * DTD. */ public synchronized void write(OutputStream out, String dtd) { write(new OutputStreamWriter(out), dtd); } /** * Write the current data and plot configuration to the specified stream in * PlotML syntax. PlotML is an XML scheme for plot data. The URL (relative * or absolute) for the DTD is given as the second argument. If that * argument is null, then the PlotML PUBLIC DTD is referenced, resulting in * a file that can be read by a PlotML parser without any external file * references, as long as that parser has local access to the DTD. The * output is buffered, and is flushed before exiting. * * @param out * An output writer. * @param dtd * The reference (URL) for the DTD, or null to use the PUBLIC * DTD. */ public synchronized void write(Writer out, String dtd) { // Auto-flush is disabled. PrintWriter output = new PrintWriter(new BufferedWriter(out), false); if (dtd == null) { output.println("<?xml version=\"1.0\" standalone=\"yes\"?>"); output.println("<!DOCTYPE plot PUBLIC \"-//UC Berkeley//DTD PlotML 1//EN\""); output.println(" \"http://ptolemy.eecs.berkeley.edu/xml/dtd/PlotML_1.dtd\">"); } else { output.println("<?xml version=\"1.0\" standalone=\"no\"?>"); output.println("<!DOCTYPE plot SYSTEM \"" + dtd + "\">"); } output.println("<plot>"); output.println("<!-- Ptolemy plot, version " + PTPLOT_RELEASE + " , PlotML format. -->"); writeFormat(output); writeData(output); output.println("</plot>"); output.flush(); // NOTE: We used to close the stream, but if this is part // of an exportMoML operation, that is the wrong thing to do. // if (out != System.out) { // output.close(); // } } /** * Write plot data information to the specified output stream in PlotML. In * this base class, there is no data to write, so this method returns * without doing anything. * * @param output * A buffered print writer. */ public synchronized void writeData(PrintWriter output) { } /** * Write plot format information to the specified output stream in PlotML. * Derived classes should override this method to first call the parent * class method, then add whatever additional format information they wish * to add to the stream. * * @param output * A buffered print writer. */ public synchronized void writeFormat(PrintWriter output) { // NOTE: If you modify this, you should change the _DTD variable // accordingly. if (_title != null) { output.println("<title>" + _title + "</title>"); } if (_xlabel != null) { output.println("<xLabel>" + _xlabel + "</xLabel>"); } if (_ylabel != null) { output.println("<yLabel>" + _ylabel + "</yLabel>"); } if (_xRangeGiven) { output.println("<xRange min=\"" + _xlowgiven + "\" max=\"" + _xhighgiven + "\"/>"); } if (_yRangeGiven) { output.println("<yRange min=\"" + _ylowgiven + "\" max=\"" + _yhighgiven + "\"/>"); } if ((_xticks != null) && (_xticks.size() > 0)) { output.println("<xTicks>"); int last = _xticks.size() - 1; for (int i = 0; i <= last; i++) { output.println(" <tick label=\"" + _xticklabels.elementAt(i) + "\" position=\"" + _xticks.elementAt(i) + "\"/>"); } output.println("</xTicks>"); } if ((_yticks != null) && (_yticks.size() > 0)) { output.println("<yTicks>"); int last = _yticks.size() - 1; for (int i = 0; i <= last; i++) { output.println(" <tick label=\"" + _yticklabels.elementAt(i) + "\" position=\"" + _yticks.elementAt(i) + "\"/>"); } output.println("</yTicks>"); } if (_xlog) { output.println("<xLog/>"); } if (_ylog) { output.println("<yLog/>"); } if (!_grid) { output.println("<noGrid/>"); } if (_wrap) { output.println("<wrap/>"); } if (!_usecolor) { output.println("<noColor/>"); } } /** * Zoom in or out to the specified rectangle. This method calls repaint(). * * @param lowx * The low end of the new X range. * @param lowy * The low end of the new Y range. * @param highx * The high end of the new X range. * @param highy * The high end of the new Y range. */ public synchronized void zoom(double lowx, double lowy, double highx, double highy) { setXRange(lowx, highx); setYRange(lowy, highy); repaint(); } // ///////////////////////////////////////////////////////////////// // // public variables //// public static final String PTPLOT_RELEASE = "5.7"; // ///////////////////////////////////////////////////////////////// // // protected methods //// /** * Draw the axes using the current range, label, and title information. If * the second argument is true, clear the display before redrawing. This * method is called by paintComponent(). To cause it to be called you would * normally call repaint(), which eventually causes paintComponent() to be * called. * <p> * Note that this is synchronized so that points are not added by other * threads while the drawing is occurring. This method should be called only * from the event dispatch thread, consistent with swing policy. * * @param graphics * The graphics context. * @param clearfirst * If true, clear the plot before proceeding. */ protected synchronized void _drawPlot(Graphics graphics, boolean clearfirst) { Rectangle bounds = getBounds(); _drawPlot(graphics, clearfirst, bounds); } /** * Draw the axes using the current range, label, and title information, at * the size of the specified rectangle. If the second argument is true, * clear the display before redrawing. This method is called by * paintComponent(). To cause it to be called you would normally call * repaint(), which eventually causes paintComponent() to be called. * <p> * Note that this is synchronized so that points are not added by other * threads while the drawing is occurring. This method should be called only * from the event dispatch thread, consistent with swing policy. * * @param graphics * The graphics context. * @param clearfirst * If true, clear the plot before proceeding. * @param drawRect * A specification of the size. */ protected synchronized void _drawPlot(Graphics graphics, boolean clearfirst, Rectangle drawRect) { // Ignore if there is no graphics object to draw on. if (graphics == null) { return; } graphics.setPaintMode(); /* * NOTE: The following seems to be unnecessary with Swing... if * (clearfirst) { // NOTE: calling clearRect() here permits the * background // color to show through, but it messes up printing. // * Printing results in black-on-black title and axis labels. * graphics.setColor(_background); graphics.drawRect(0, 0, * drawRect.width, drawRect.height); graphics.setColor(Color.black); } */ // If an error message has been set, display it and return. if (_errorMsg != null) { int fheight = _labelFontMetrics.getHeight() + 2; int msgy = fheight; graphics.setColor(Color.black); for (int i = 0; i < _errorMsg.length; i++) { graphics.drawString(_errorMsg[i], 10, msgy); msgy += fheight; System.err.println(_errorMsg[i]); } return; } // Make sure we have an x and y range if (!_xRangeGiven) { if (_xBottom > _xTop) { // have nothing to go on. _setXRange(0, 0); } else { _setXRange(_xBottom, _xTop); } } if (!_yRangeGiven) { if (_yBottom > _yTop) { // have nothing to go on. _setYRange(0, 0); } else { _setYRange(_yBottom, _yTop); } } // If user specified a plot rectangle, compute // a working plot rectangle which lies inside the // drawRect at the user specified coordinates Rectangle workingPlotRectangle = null; if (_specifiedPlotRectangle != null) { workingPlotRectangle = new Rectangle(Math.max(0, _specifiedPlotRectangle.x), Math.max(0, _specifiedPlotRectangle.y), Math.min(drawRect.width, _specifiedPlotRectangle.width), Math.min( drawRect.height, _specifiedPlotRectangle.height)); } // Vertical space for title, if appropriate. // NOTE: We assume a one-line title. int titley = 0; int titlefontheight = _titleFontMetrics.getHeight(); if (_title == null) { // NOTE: If the _title is null, then set it to the empty // string to solve the problem where the fill button overlaps // the legend if there is no title. The fix here would // be to modify the legend printing text so that it takes // into account the case where there is no title by offsetting // just enough for the button. _title = ""; } if ((_title != null) || (_yExp != 0)) { titley = titlefontheight + _topPadding; } // Number of vertical tick marks depends on the height of the font // for labeling ticks and the height of the window. Font previousFont = graphics.getFont(); graphics.setFont(_labelFont); graphics.setColor(_foreground); // foreground color not set here --Rob. int labelheight = _labelFontMetrics.getHeight(); int halflabelheight = labelheight / 2; // Draw scaling annotation for x axis. // NOTE: 5 pixel padding on bottom. int ySPos = drawRect.height - 5; int xSPos = drawRect.width - _rightPadding; if (_xlog) { _xExp = (int) Math.floor(_xtickMin); } if ((_xExp != 0) && (_xticks == null)) { String superscript = Integer.toString(_xExp); xSPos -= _superscriptFontMetrics.stringWidth(superscript); graphics.setFont(_superscriptFont); if (!_xlog) { graphics.drawString(superscript, xSPos, ySPos - halflabelheight); xSPos -= _labelFontMetrics.stringWidth("x10"); graphics.setFont(_labelFont); graphics.drawString("x10", xSPos, ySPos); } // NOTE: 5 pixel padding on bottom _bottomPadding = ((3 * labelheight) / 2) + 5; } // NOTE: 5 pixel padding on the bottom. if ((_xlabel != null) && (_bottomPadding < (labelheight + 5))) { _bottomPadding = labelheight + 5; } // Compute the space needed around the plot, starting with vertical. // NOTE: padding of 5 pixels below title. if (workingPlotRectangle != null) { _uly = workingPlotRectangle.y; } else { _uly = titley + 5; } // NOTE: 3 pixels above bottom labels. if (workingPlotRectangle != null) { _lry = workingPlotRectangle.y + workingPlotRectangle.height; } else { _lry = drawRect.height - labelheight - _bottomPadding - 3; } int height = _lry - _uly; _yscale = height / (_yMax - _yMin); _ytickscale = height / (_ytickMax - _ytickMin); // //////////////// vertical axis // Number of y tick marks. // NOTE: subjective spacing factor. int ny = 2 + (height / (labelheight + 10)); // Compute y increment. double yStep = _roundUp((_ytickMax - _ytickMin) / ny); // Compute y starting point so it is a multiple of yStep. double yStart = yStep * Math.ceil(_ytickMin / yStep); // NOTE: Following disables first tick. Not a good idea? // if (yStart == _ytickMin) yStart += yStep; // Define the strings that will label the y axis. // Meanwhile, find the width of the widest label. // The labels are quantized so that they don't have excess resolution. int widesty = 0; // These do not get used unless ticks are automatic, but the // compiler is not smart enough to allow us to reference them // in two distinct conditional clauses unless they are // allocated outside the clauses. String[] ylabels = new String[ny]; int[] ylabwidth = new int[ny]; int ind = 0; if (_yticks == null) { Vector<Double> ygrid = null; if (_ylog) { ygrid = _gridInit(yStart, yStep, true, null); } // automatic ticks // First, figure out how many digits after the decimal point // will be used. int numfracdigits = _numFracDigits(yStep); // NOTE: Test cases kept in case they are needed again. // System.out.println("0.1 with 3 digits: " + _formatNum(0.1, 3)); // System.out.println("0.0995 with 3 digits: " + // _formatNum(0.0995, 3)); // System.out.println("0.9995 with 3 digits: " + // _formatNum(0.9995, 3)); // System.out.println("1.9995 with 0 digits: " + // _formatNum(1.9995, 0)); // System.out.println("1 with 3 digits: " + _formatNum(1, 3)); // System.out.println("10 with 0 digits: " + _formatNum(10, 0)); // System.out.println("997 with 3 digits: " + _formatNum(997, 3)); // System.out.println("0.005 needs: " + _numFracDigits(0.005)); // System.out.println("1 needs: " + _numFracDigits(1)); // System.out.println("999 needs: " + _numFracDigits(999)); // System.out.println("999.0001 needs: "+_numFracDigits(999.0001)); // System.out.println("0.005 integer digits: " + // _numIntDigits(0.005)); // System.out.println("1 integer digits: " + _numIntDigits(1)); // System.out.println("999 integer digits: " + _numIntDigits(999)); // System.out.println("-999.0001 integer digits: " + // _numIntDigits(999.0001)); double yTmpStart = yStart; if (_ylog) { yTmpStart = _gridStep(ygrid, yStart, yStep, _ylog); } for (double ypos = yTmpStart; ypos <= _ytickMax; ypos = _gridStep(ygrid, ypos, yStep, _ylog)) { // Prevent out of bounds exceptions if (ind >= ny) { break; } String yticklabel; if (_ylog) { yticklabel = _formatLogNum(ypos, numfracdigits); } else { yticklabel = _formatNum(ypos, numfracdigits); } ylabels[ind] = yticklabel; int lw = _labelFontMetrics.stringWidth(yticklabel); ylabwidth[ind++] = lw; if (lw > widesty) { widesty = lw; } } } else { // explicitly specified ticks Enumeration<String> nl = _yticklabels.elements(); while (nl.hasMoreElements()) { String label = nl.nextElement(); int lw = _labelFontMetrics.stringWidth(label); if (lw > widesty) { widesty = lw; } } } // Next we do the horizontal spacing. if (workingPlotRectangle != null) { _ulx = workingPlotRectangle.x; } else { if (_ylabel != null) { _ulx = widesty + _labelFontMetrics.stringWidth("W") + _leftPadding; } else { _ulx = widesty + _leftPadding; } } int legendwidth = _drawLegend(graphics, drawRect.width - _rightPadding, _uly); if (workingPlotRectangle != null) { _lrx = workingPlotRectangle.x + workingPlotRectangle.width; } else { _lrx = drawRect.width - legendwidth - _rightPadding; } int width = _lrx - _ulx; _xscale = width / (_xMax - _xMin); _xtickscale = width / (_xtickMax - _xtickMin); // Background for the plotting rectangle. // Always use a white background because the dataset colors // were designed for a white background. graphics.setColor(Color.white); graphics.fillRect(_ulx, _uly, width, height); graphics.setColor(_foreground); graphics.drawRect(_ulx, _uly, width, height); // NOTE: subjective tick length. int tickLength = 5; int xCoord1 = _ulx + tickLength; int xCoord2 = _lrx - tickLength; if (_yticks == null) { // auto-ticks Vector<Double> ygrid = null; double yTmpStart = yStart; if (_ylog) { ygrid = _gridInit(yStart, yStep, true, null); yTmpStart = _gridStep(ygrid, yStart, yStep, _ylog); ny = ind; } ind = 0; // Set to false if we don't need the exponent boolean needExponent = _ylog; for (double ypos = yTmpStart; ypos <= _ytickMax; ypos = _gridStep(ygrid, ypos, yStep, _ylog)) { // Prevent out of bounds exceptions if (ind >= ny) { break; } int yCoord1 = _lry - (int) ((ypos - _ytickMin) * _ytickscale); // The lowest label is shifted up slightly to avoid // colliding with x labels. int offset = 0; if ((ind > 0) && !_ylog) { offset = halflabelheight; } graphics.drawLine(_ulx, yCoord1, xCoord1, yCoord1); graphics.drawLine(_lrx, yCoord1, xCoord2, yCoord1); if (_grid && (yCoord1 != _uly) && (yCoord1 != _lry)) { graphics.setColor(Color.lightGray); graphics.drawLine(xCoord1, yCoord1, xCoord2, yCoord1); graphics.setColor(_foreground); } // Check to see if any of the labels printed contain // the exponent. If we don't see an exponent, then print it. if (_ylog && (ylabels[ind].indexOf('e') != -1)) { needExponent = false; } // NOTE: 4 pixel spacing between axis and labels. graphics.drawString(ylabels[ind], _ulx - ylabwidth[ind++] - 4, yCoord1 + offset); } if (_ylog) { // Draw in grid lines that don't have labels. Vector<Double> unlabeledgrid = _gridInit(yStart, yStep, false, ygrid); if (unlabeledgrid.size() > 0) { // If the step is greater than 1, clamp it to 1 so that // we draw the unlabeled grid lines for each // integer interval. double tmpStep = (yStep > 1.0) ? 1.0 : yStep; for (double ypos = _gridStep(unlabeledgrid, yStart, tmpStep, _ylog); ypos <= _ytickMax; ypos = _gridStep( unlabeledgrid, ypos, tmpStep, _ylog)) { int yCoord1 = _lry - (int) ((ypos - _ytickMin) * _ytickscale); if (_grid && (yCoord1 != _uly) && (yCoord1 != _lry)) { graphics.setColor(Color.lightGray); graphics.drawLine(_ulx + 1, yCoord1, _lrx - 1, yCoord1); graphics.setColor(_foreground); } } } if (needExponent) { // We zoomed in, so we need the exponent _yExp = (int) Math.floor(yTmpStart); } else { _yExp = 0; } } // Draw scaling annotation for y axis. if (_yExp != 0) { graphics.drawString("x10", 2, titley); graphics.setFont(_superscriptFont); graphics.drawString(Integer.toString(_yExp), _labelFontMetrics.stringWidth("x10") + 2, titley - halflabelheight); graphics.setFont(_labelFont); } } else { // ticks have been explicitly specified Enumeration<Double> nt = _yticks.elements(); Enumeration<String> nl = _yticklabels.elements(); while (nl.hasMoreElements()) { String label = nl.nextElement(); double ypos = (nt.nextElement()).doubleValue(); if ((ypos > _yMax) || (ypos < _yMin)) { continue; } int yCoord1 = _lry - (int) ((ypos - _yMin) * _yscale); int offset = 0; if (ypos < (_lry - labelheight)) { offset = halflabelheight; } graphics.drawLine(_ulx, yCoord1, xCoord1, yCoord1); graphics.drawLine(_lrx, yCoord1, xCoord2, yCoord1); if (_grid && (yCoord1 != _uly) && (yCoord1 != _lry)) { graphics.setColor(Color.lightGray); graphics.drawLine(xCoord1, yCoord1, xCoord2, yCoord1); graphics.setColor(_foreground); } // NOTE: 3 pixel spacing between axis and labels. graphics.drawString(label, _ulx - _labelFontMetrics.stringWidth(label) - 3, yCoord1 + offset); } } // ////////////////// horizontal axis int yCoord1 = _uly + tickLength; int yCoord2 = _lry - tickLength; int charwidth = _labelFontMetrics.stringWidth("8"); if (_xticks == null) { // auto-ticks // Number of x tick marks. // Need to start with a guess and converge on a solution here. int nx = 10; double xStep = 0.0; int numfracdigits = 0; if (_xlog) { // X axes log labels will be at most 6 chars: -1E-02 nx = 2 + (width / ((charwidth * 6) + 10)); } else { // Limit to 10 iterations int count = 0; while (count++ <= 10) { xStep = _roundUp((_xtickMax - _xtickMin) / nx); // Compute the width of a label for this xStep numfracdigits = _numFracDigits(xStep); // Number of integer digits is the maximum of two endpoints int intdigits = _numIntDigits(_xtickMax); int inttemp = _numIntDigits(_xtickMin); if (intdigits < inttemp) { intdigits = inttemp; } // Allow two extra digits (decimal point and sign). int maxlabelwidth = charwidth * (numfracdigits + 2 + intdigits); // Compute new estimate of number of ticks. int savenx = nx; // NOTE: 10 additional pixels between labels. // NOTE: Try to ensure at least two tick marks. nx = 2 + (width / (maxlabelwidth + 10)); if (((nx - savenx) <= 1) || ((savenx - nx) <= 1)) { break; } } } xStep = _roundUp((_xtickMax - _xtickMin) / nx); numfracdigits = _numFracDigits(xStep); // Compute x starting point so it is a multiple of xStep. double xStart = xStep * Math.ceil(_xtickMin / xStep); // NOTE: Following disables first tick. Not a good idea? // if (xStart == _xMin) xStart += xStep; Vector<Double> xgrid = null; double xTmpStart = xStart; if (_xlog) { xgrid = _gridInit(xStart, xStep, true, null); // xgrid = _gridInit(xStart, xStep); xTmpStart = _gridRoundUp(xgrid, xStart); } // Set to false if we don't need the exponent boolean needExponent = _xlog; // Label the x axis. The labels are quantized so that // they don't have excess resolution. for (double xpos = xTmpStart; xpos <= _xtickMax; xpos = _gridStep(xgrid, xpos, xStep, _xlog)) { String xticklabel; if (_xlog) { xticklabel = _formatLogNum(xpos, numfracdigits); if (xticklabel.indexOf('e') != -1) { needExponent = false; } } else { xticklabel = _formatNum(xpos, numfracdigits); } xCoord1 = _ulx + (int) ((xpos - _xtickMin) * _xtickscale); graphics.drawLine(xCoord1, _uly, xCoord1, yCoord1); graphics.drawLine(xCoord1, _lry, xCoord1, yCoord2); if (_grid && (xCoord1 != _ulx) && (xCoord1 != _lrx)) { graphics.setColor(Color.lightGray); graphics.drawLine(xCoord1, yCoord1, xCoord1, yCoord2); graphics.setColor(_foreground); } int labxpos = xCoord1 - (_labelFontMetrics.stringWidth(xticklabel) / 2); // NOTE: 3 pixel spacing between axis and labels. graphics.drawString(xticklabel, labxpos, _lry + 3 + labelheight); } if (_xlog) { // Draw in grid lines that don't have labels. // If the step is greater than 1, clamp it to 1 so that // we draw the unlabeled grid lines for each // integer interval. double tmpStep = (xStep > 1.0) ? 1.0 : xStep; // Recalculate the start using the new step. xTmpStart = tmpStep * Math.ceil(_xtickMin / tmpStep); Vector<Double> unlabeledgrid = _gridInit(xTmpStart, tmpStep, false, xgrid); if (unlabeledgrid.size() > 0) { for (double xpos = _gridStep(unlabeledgrid, xTmpStart, tmpStep, _xlog); xpos <= _xtickMax; xpos = _gridStep( unlabeledgrid, xpos, tmpStep, _xlog)) { xCoord1 = _ulx + (int) ((xpos - _xtickMin) * _xtickscale); if (_grid && (xCoord1 != _ulx) && (xCoord1 != _lrx)) { graphics.setColor(Color.lightGray); graphics.drawLine(xCoord1, _uly + 1, xCoord1, _lry - 1); graphics.setColor(_foreground); } } } if (needExponent) { _xExp = (int) Math.floor(xTmpStart); graphics.setFont(_superscriptFont); graphics.drawString(Integer.toString(_xExp), xSPos, ySPos - halflabelheight); xSPos -= _labelFontMetrics.stringWidth("x10"); graphics.setFont(_labelFont); graphics.drawString("x10", xSPos, ySPos); } else { _xExp = 0; } } } else { // ticks have been explicitly specified Enumeration<Double> nt = _xticks.elements(); Enumeration<String> nl = _xticklabels.elements(); // Code contributed by Jun Wu ([email protected]) double preLength = 0.0; while (nl.hasMoreElements()) { String label = nl.nextElement(); double xpos = (nt.nextElement()).doubleValue(); // If xpos is out of range, ignore. if ((xpos > _xMax) || (xpos < _xMin)) { continue; } // Find the center position of the label. xCoord1 = _ulx + (int) ((xpos - _xMin) * _xscale); // Find the start position of x label. int labxpos = xCoord1 - (_labelFontMetrics.stringWidth(label) / 2); // If the labels are not overlapped, proceed. if (labxpos > preLength) { // calculate the length of the label preLength = xCoord1 + (_labelFontMetrics.stringWidth(label) / 2) + 10; // Draw the label. // NOTE: 3 pixel spacing between axis and labels. graphics.drawString(label, labxpos, _lry + 3 + labelheight); // Draw the label mark on the axis graphics.drawLine(xCoord1, _uly, xCoord1, yCoord1); graphics.drawLine(xCoord1, _lry, xCoord1, yCoord2); // Draw the grid line if (_grid && (xCoord1 != _ulx) && (xCoord1 != _lrx)) { graphics.setColor(Color.lightGray); graphics.drawLine(xCoord1, yCoord1, xCoord1, yCoord2); graphics.setColor(_foreground); } } } } ZoomRectangle z = zoomBox; if (plotBoxDrawsZoomRect && z != null) { z.paintZoomRectangle(this, graphics); } // ////////////////// Draw title and axis labels now. // Center the title and X label over the plotting region, not // the window. graphics.setColor(_foreground); if (_title != null) { graphics.setFont(_titleFont); int titlex = _ulx + ((width - _titleFontMetrics.stringWidth(_title)) / 2); graphics.drawString(_title, titlex, titley); } graphics.setFont(_labelFont); if (_xlabel != null) { int labelx = _ulx + ((width - _labelFontMetrics.stringWidth(_xlabel)) / 2); graphics.drawString(_xlabel, labelx, ySPos); } int charcenter = 2 + (_labelFontMetrics.stringWidth("W") / 2); if (_ylabel != null) { int yl = _ylabel.length(); if (graphics instanceof Graphics2D) { int starty = (_uly + ((_lry - _uly) / 2) + (_labelFontMetrics.stringWidth(_ylabel) / 2)) - charwidth; Graphics2D g2d = (Graphics2D) graphics; // NOTE: Fudge factor so label doesn't touch axis labels. int startx = (charcenter + halflabelheight) - 2; g2d.rotate(Math.toRadians(-90), startx, starty); g2d.drawString(_ylabel, startx, starty); g2d.rotate(Math.toRadians(90), startx, starty); } else { // Not graphics 2D, no support for rotation. // Vertical label is fairly complex to draw. int starty = (_uly + ((_lry - _uly) / 2)) - (yl * halflabelheight) + labelheight; for (int i = 0; i < yl; i++) { String nchar = _ylabel.substring(i, i + 1); int cwidth = _labelFontMetrics.stringWidth(nchar); graphics.drawString(nchar, charcenter - (cwidth / 2), starty); starty += labelheight; } } } graphics.setFont(previousFont); } /** * @return the _ulx */ public int get_ulx() { return _ulx; } /** * @return the _uly */ public int get_uly() { return _uly; } /** * @return the _lrx */ public int get_lrx() { return _lrx; } /** * @return the _lry */ public int get_lry() { return _lry; } /** * Put a mark corresponding to the specified dataset at the specified x and * y position. The mark is drawn in the current color. In this base class, a * point is a filled rectangle 6 pixels across. Note that marks greater than * about 6 pixels in size will not look very good since they will overlap * axis labels and may not fit well in the legend. The <i>clip</i> argument, * if <code>true</code>, states that the point should not be drawn if it is * out of range. * * Note that this method is not synchronized, so the caller should be. * Moreover this method should always be called from the event thread when * being used to write to the screen. * * @param graphics * The graphics context. * @param dataset * The index of the data set. * @param xpos * The X position. * @param ypos * The Y position. * @param clip * If true, do not draw if out of range. */ protected void _drawPoint(Graphics graphics, int dataset, long xpos, long ypos, boolean clip) { // Ignore if there is no graphics object to draw on. if (graphics == null) { return; } boolean pointinside = (ypos <= _lry) && (ypos >= _uly) && (xpos <= _lrx) && (xpos >= _ulx); if (!pointinside && clip) { return; } graphics.fillRect((int) xpos - 6, (int) ypos - 6, 6, 6); } /** * Display basic information in its own window. */ protected void _help() { String message = "Ptolemy plot package\n" + "By: Edward A. Lee\n" + "and Christopher Brooks\n" + "Version " + PTPLOT_RELEASE + ", Build: $Id: PlotBox.java,v 1.283.4.3 2008/04/01 01:19:02 cxh Exp $\n\n" + "Key bindings:\n" + " Cntrl-c: copy plot to clipboard (EPS format), if permitted\n" + " D: dump plot data to standard out\n" + " E: export plot to standard out (EPS format)\n" + " F: fill plot\n" + " H or ?: print help message (this message)\n" + " Cntrl-D or Q: quit\n" + "For more information, see\n" + "http://ptolemy.eecs.berkeley.edu/java/ptplot\n"; JOptionPane.showMessageDialog(this, message, "Ptolemy Plot Help Window", JOptionPane.INFORMATION_MESSAGE); } /** * Parse a line that gives plotting information. In this base class, only * lines pertaining to the title and labels are processed. Everything else * is ignored. Return true if the line is recognized. It is not * synchronized, so its caller should be. * * @param line * A line of text. */ protected boolean _parseLine(String line) { // If you modify this method, you should also modify write() // We convert the line to lower case so that the command // names are case insensitive. String lcLine = line.toLowerCase(); if (lcLine.startsWith("#")) { // comment character return true; } else if (lcLine.startsWith("titletext:")) { setTitle((line.substring(10)).trim()); return true; } else if (lcLine.startsWith("title:")) { // Tolerate alternative tag. setTitle((line.substring(6)).trim()); return true; } else if (lcLine.startsWith("xlabel:")) { setXLabel((line.substring(7)).trim()); return true; } else if (lcLine.startsWith("ylabel:")) { setYLabel((line.substring(7)).trim()); return true; } else if (lcLine.startsWith("xrange:")) { int comma = line.indexOf(",", 7); if (comma > 0) { String min = (line.substring(7, comma)).trim(); String max = (line.substring(comma + 1)).trim(); try { Double dmin = Double.valueOf(min); Double dmax = Double.valueOf(max); setXRange(dmin.doubleValue(), dmax.doubleValue()); } catch (NumberFormatException e) { // ignore if format is bogus. } } return true; } else if (lcLine.startsWith("yrange:")) { int comma = line.indexOf(",", 7); if (comma > 0) { String min = (line.substring(7, comma)).trim(); String max = (line.substring(comma + 1)).trim(); try { Double dmin = Double.valueOf(min); Double dmax = Double.valueOf(max); setYRange(dmin.doubleValue(), dmax.doubleValue()); } catch (NumberFormatException e) { // ignore if format is bogus. } } return true; } else if (lcLine.startsWith("xticks:")) { // example: // XTicks "label" 0, "label" 1, "label" 3 _parsePairs(line.substring(7), true); return true; } else if (lcLine.startsWith("yticks:")) { // example: // YTicks "label" 0, "label" 1, "label" 3 _parsePairs(line.substring(7), false); return true; } else if (lcLine.startsWith("xlog:")) { if (lcLine.indexOf("off", 5) >= 0) { _xlog = false; } else { _xlog = true; } return true; } else if (lcLine.startsWith("ylog:")) { if (lcLine.indexOf("off", 5) >= 0) { _ylog = false; } else { _ylog = true; } return true; } else if (lcLine.startsWith("grid:")) { if (lcLine.indexOf("off", 5) >= 0) { _grid = false; } else { _grid = true; } return true; } else if (lcLine.startsWith("wrap:")) { if (lcLine.indexOf("off", 5) >= 0) { _wrap = false; } else { _wrap = true; } return true; } else if (lcLine.startsWith("color:")) { if (lcLine.indexOf("off", 6) >= 0) { _usecolor = false; } else { _usecolor = true; } return true; } return false; } /** * Set the padding multiple. The plot rectangle can be "padded" in each * direction -x, +x, -y, and +y. If the padding is set to 0.05 (and the * padding is used), then there is 10% more length on each axis than set by * the setXRange() and setYRange() methods, 5% in each direction. * * @param padding * The padding multiple. */ protected void _setPadding(double padding) { // Changing legend means we need to repaint the offscreen buffer. _plotImage = null; _padding = padding; } // ///////////////////////////////////////////////////////////////// // // protected variables //// // The range of the data to be plotted. protected transient double _yMax = 0; // ///////////////////////////////////////////////////////////////// // // protected variables //// // The range of the data to be plotted. protected transient double _yMin = 0; // ///////////////////////////////////////////////////////////////// // // protected variables //// // The range of the data to be plotted. protected transient double _xMax = 0; // ///////////////////////////////////////////////////////////////// // // protected variables //// // The range of the data to be plotted. protected transient double _xMin = 0; /** * The factor we pad by so that we don't plot points on the axes. */ protected double _padding = 0.05; // Whether the ranges have been given. protected transient boolean _xRangeGiven = false; protected transient boolean _yRangeGiven = false; protected transient boolean _rangesGivenByZooming = false; /** * @serial The given X and Y ranges. If they have been given the top and * bottom of the x and y ranges. This is different from _xMin and * _xMax, which actually represent the range of data that is * plotted. This represents the range specified (which may be * different due to zooming). */ protected double _xlowgiven; /** * @serial The given X and Y ranges. If they have been given the top and * bottom of the x and y ranges. This is different from _xMin and * _xMax, which actually represent the range of data that is * plotted. This represents the range specified (which may be * different due to zooming). */ protected double _xhighgiven; /** * @serial The given X and Y ranges. If they have been given the top and * bottom of the x and y ranges. This is different from _xMin and * _xMax, which actually represent the range of data that is * plotted. This represents the range specified (which may be * different due to zooming). */ protected double _ylowgiven; /** * @serial The given X and Y ranges. If they have been given the top and * bottom of the x and y ranges. This is different from _xMin and * _xMax, which actually represent the range of data that is * plotted. This represents the range specified (which may be * different due to zooming). */ protected double _yhighgiven; /** @serial The minimum X value registered so for, for auto ranging. */ protected double _xBottom = Double.MAX_VALUE; /** @serial The maximum X value registered so for, for auto ranging. */ protected double _xTop = -Double.MAX_VALUE; /** @serial The minimum Y value registered so for, for auto ranging. */ protected double _yBottom = Double.MAX_VALUE; /** @serial The maximum Y value registered so for, for auto ranging. */ protected double _yTop = -Double.MAX_VALUE; /** @serial Whether to draw the axes using a logarithmic scale. */ protected boolean _xlog = false; /** @serial Whether to draw the axes using a logarithmic scale. */ protected boolean _ylog = false; // For use in calculating log base 10. A log times this is a log base 10. protected static final double _LOG10SCALE = 1 / Math.log(10); /** @serial Whether to draw a background grid. */ protected boolean _grid = true; /** @serial Whether to wrap the X axis. */ protected boolean _wrap = false; /** @serial The high range of the X axis for wrapping. */ protected double _wrapHigh; /** @serial The low range of the X axis for wrapping. */ protected double _wrapLow; /** @serial Color of the background, settable from HTML. */ protected Color _background = Color.white; /** @serial Color of the foreground, settable from HTML. */ protected Color _foreground = Color.black; /** * @serial Top padding. Derived classes can increment these to make space * around the plot. */ protected int _topPadding = 10; /** * @serial Bottom padding. Derived classes can increment these to make space * around the plot. */ protected int _bottomPadding = 5; /** * @serial Right padding. Derived classes can increment these to make space * around the plot. */ protected int _rightPadding = 10; /** * @serial Left padding. Derived classes can increment these to make space * around the plot. */ protected int _leftPadding = 10; // The naming convention is: "_ulx" = "upper left x", where "x" is // the horizontal dimension. /** The x value of the upper left corner of the plot rectangle in pixels. */ protected int _ulx = 1; /** The y value of the upper left corner of the plot rectangle in pixels. */ protected int _uly = 1; /** * The x value of the lower right corner of the plot rectangle in pixels. */ protected int _lrx = 100; /** * The y value of the lower right corner of the plot rectangle in pixels. */ protected int _lry = 100; /** * User specified plot rectangle, null if none specified. * * @see #setPlotRectangle(Rectangle) */ protected Rectangle _specifiedPlotRectangle = null; /** * Scaling used for the vertical axis in plotting points. The units are * pixels/unit, where unit is the units of the Y axis. */ protected double _yscale = 1.0; /** * Scaling used for the horizontal axis in plotting points. The units are * pixels/unit, where unit is the units of the X axis. */ protected double _xscale = 1.0; /** @serial Indicator whether to use _colors. */ protected boolean _usecolor = true; // Default _colors, by data set. // There are 11 colors so that combined with the // 10 marks of the Plot class, we can distinguish 110 // distinct data sets. static protected Color[] _colors = { new Color(0xff0000), // red new Color(0x0000ff), // blue new Color(0x00aaaa), // cyan-ish new Color(0x000000), // black new Color(0xffa500), // orange new Color(0x53868b), // cadetblue4 new Color(0xff7f50), // coral new Color(0x45ab1f), // dark green-ish new Color(0x90422d), // sienna-ish new Color(0xa0a0a0), // grey-ish new Color(0x14ff14), // green-ish }; /** @serial Width and height of component in pixels. */ protected int _width = 500; /** @serial Width and height of component in pixels. */ protected int _height = 300; /** @serial Width and height of component in pixels. */ protected int _preferredWidth = 500; /** @serial Width and height of component in pixels. */ protected int _preferredHeight = 300; /** @serial Indicator that size has been set. */ // protected boolean _sizeHasBeenSet = false; // ///////////////////////////////////////////////////////////////// // // private methods //// /* * Draw the legend in the upper right corner and return the width (in * pixels) used up. The arguments give the upper right corner of the region * where the legend should be placed. */ private int _drawLegend(Graphics graphics, int urx, int ury) { // Ignore if there is no graphics object to draw on. if (graphics == null) { return 0; } // FIXME: consolidate all these for efficiency Font previousFont = graphics.getFont(); graphics.setFont(_labelFont); int spacing = _labelFontMetrics.getHeight(); Enumeration<String> v = _legendStrings.elements(); Enumeration<Integer> i = _legendDatasets.elements(); int ypos = ury + spacing; int maxwidth = 0; while (v.hasMoreElements()) { String legend = v.nextElement(); // NOTE: relies on _legendDatasets having the same num. of entries. int dataset = i.nextElement().intValue(); if (dataset >= 0) { if (_usecolor) { // Points are only distinguished up to the number of colors int color = dataset % _colors.length; graphics.setColor(_colors[color]); } _drawPoint(graphics, dataset, urx - 3, ypos - 3, false); graphics.setColor(_foreground); int width = _labelFontMetrics.stringWidth(legend); if (width > maxwidth) { maxwidth = width; } graphics.drawString(legend, urx - 15 - width, ypos); ypos += spacing; } } graphics.setFont(previousFont); return 22 + maxwidth; // NOTE: subjective spacing parameter. } /** * @return the zoomBox */ protected ZoomRectangle getZoomBox() { return zoomBox; } /** * @param zoomBox * the zoomBox to set */ protected void setZoomBox(ZoomRectangle zoomBox) { this.zoomBox = zoomBox; } // Execute all actions pending on the deferred action list. // The list is cleared and the _actionsDeferred variable is set // to false, even if one of the deferred actions fails. // This method should only be invoked in the event dispatch thread. // It is synchronized, so the integrity of the deferred actions list // is ensured, since modifications to that list occur only in other // synchronized methods. private synchronized void _executeDeferredActions() { try { Iterator<Runnable> actions = _deferredActions.iterator(); while (actions.hasNext()) { Runnable action = actions.next(); action.run(); } } finally { _actionsDeferred = false; _deferredActions.clear(); } } /* * Return the number as a String for use as a label on a logarithmic axis. * Since this is a log plot, number passed in will not have too many digits * to cause problems. If the number is an integer, then we print 1e<num>. If * the number is not an integer, then print only the fractional components. */ private String _formatLogNum(double num, int numfracdigits) { String results; int exponent = (int) num; // Determine the exponent, prepending 0 or -0 if necessary. if ((exponent >= 0) && (exponent < 10)) { results = "0" + exponent; } else { if ((exponent < 0) && (exponent > -10)) { results = "-0" + (-exponent); } else { results = Integer.toString(exponent); } } // Handle the mantissa. if (num >= 0.0) { if ((num - (int) (num)) < 0.001) { results = "1e" + results; } else { results = _formatNum(Math.pow(10.0, (num - (int) num)), numfracdigits); } } else { if ((-num - (int) (-num)) < 0.001) { results = "1e" + results; } else { results = _formatNum(Math.pow(10.0, (num - (int) num)) * 10, numfracdigits); } } return results; } /* * Return a string for displaying the specified number using the specified * number of digits after the decimal point. NOTE: java.text.NumberFormat in * Netscape 4.61 has a bug where it fails to round numbers instead it * truncates them. As a result, we don't use java.text.NumberFormat, instead * We use the method from Ptplot1.3 */ private String _formatNum(double num, int numfracdigits) { // When java.text.NumberFormat works under Netscape, // uncomment the next block of code and remove // the code after it. // Ptplot developers at UCB can access a test case at: // http://ptolemy.eecs.berkeley.edu/~ptII/ptIItree/ptolemy/plot/adm/trunc/trunc-jdk11.html // The plot will show two 0.7 values on the x axis if the bug // continues to exist. // if (_numberFormat == null) { // // Cache the number format so that we don't have to get // // info about local language etc. from the OS each time. // _numberFormat = NumberFormat.getInstance(); // } // _numberFormat.setMinimumFractionDigits(numfracdigits); // _numberFormat.setMaximumFractionDigits(numfracdigits); // return _numberFormat.format(num); // The section below is from Ptplot1.3 // First, round the number. double fudge = 0.5; if (num < 0.0) { fudge = -0.5; } String numString = Double.toString(num + (fudge * Math.pow(10.0, -numfracdigits))); // Next, find the decimal point. int dpt = numString.lastIndexOf("."); StringBuffer result = new StringBuffer(); if (dpt < 0) { // The number we are given is an integer. if (numfracdigits <= 0) { // The desired result is an integer. result.append(numString); return result.toString(); } // Append a decimal point and some zeros. result.append("."); for (int i = 0; i < numfracdigits; i++) { result.append("0"); } return result.toString(); } else { // There are two cases. First, there may be enough digits. int shortby = numfracdigits - (numString.length() - dpt - 1); if (shortby <= 0) { int numtocopy = dpt + numfracdigits + 1; if (numfracdigits == 0) { // Avoid copying over a trailing decimal point. numtocopy -= 1; } result.append(numString.substring(0, numtocopy)); return result.toString(); } else { result.append(numString); for (int i = 0; i < shortby; i++) { result.append("0"); } return result.toString(); } } } /* * Determine what values to use for log axes. Based on initGrid() from * xgraph.c by David Harrison. */ private Vector<Double> _gridInit(double low, double step, boolean labeled, Vector<Double> oldgrid) { // How log axes work: // _gridInit() creates a vector with the values to use for the // log axes. For example, the vector might contain // {0.0 0.301 0.698}, which could correspond to // axis labels {1 1.2 1.5 10 12 15 100 120 150} // // _gridStep() gets the proper value. _gridInit is cycled through // for each integer log value. // // Bugs in log axes: // * Sometimes not enough grid lines are displayed because the // region is small. This bug is present in the oriignal xgraph // binary, which is the basis of this code. The problem is that // as ratio gets closer to 1.0, we need to add more and more // grid marks. Vector<Double> grid = new Vector<Double>(10); // grid.addElement(Double.valueOf(0.0)); double ratio = Math.pow(10.0, step); int ngrid = 1; if (labeled) { // Set up the number of grid lines that will be labeled if (ratio <= 3.5) { if (ratio > 2.0) { ngrid = 2; } else if (ratio > 1.26) { ngrid = 5; } else if (ratio > 1.125) { ngrid = 10; } else { ngrid = (int) Math.rint(1.0 / step); } } } else { // Set up the number of grid lines that will not be labeled if (ratio > 10.0) { ngrid = 1; } else if (ratio > 3.0) { ngrid = 2; } else if (ratio > 2.0) { ngrid = 5; } else if (ratio > 1.125) { ngrid = 10; } else { ngrid = 100; } // Note: we should keep going here, but this increases the // size of the grid array and slows everything down. } int oldgridi = 0; for (int i = 0; i < ngrid; i++) { double gridval = (i * 1.0) / ngrid * 10; double logval = _LOG10SCALE * Math.log(gridval); if (logval == Double.NEGATIVE_INFINITY) { logval = 0.0; } // If oldgrid is not null, then do not draw lines that // were already drawn in oldgrid. This is necessary // so we avoid obliterating the tick marks on the plot borders. if ((oldgrid != null) && (oldgridi < oldgrid.size())) { // Cycle through the oldgrid until we find an element // that is equal to or greater than the element we are // trying to add. while ((oldgridi < oldgrid.size()) && (oldgrid.elementAt(oldgridi).doubleValue() < logval)) { oldgridi++; } if (oldgridi < oldgrid.size()) { // Using == on doubles is bad if the numbers are close, // but not exactly equal. if (Math.abs(oldgrid.elementAt(oldgridi).doubleValue() - logval) > 0.00001) { grid.addElement(Double.valueOf(logval)); } } else { grid.addElement(Double.valueOf(logval)); } } else { grid.addElement(Double.valueOf(logval)); } } // _gridCurJuke and _gridBase are used in _gridStep(); _gridCurJuke = 0; if (low == -0.0) { low = 0.0; } _gridBase = Math.floor(low); double x = low - _gridBase; // Set gridCurJuke so that the value in grid is greater than // or equal to x. This sets us up to process the first point. for (_gridCurJuke = -1; ((_gridCurJuke + 1) < grid.size()) && (x >= grid.elementAt(_gridCurJuke + 1).doubleValue()); _gridCurJuke++) { } return grid; } /* * Round pos up to the nearest value in the grid. */ private double _gridRoundUp(Vector<Double> grid, double pos) { double x = pos - Math.floor(pos); int i; for (i = 0; (i < grid.size()) && (x >= grid.elementAt(i).doubleValue()); i++) { } if (i >= grid.size()) { return pos; } return Math.floor(pos) + grid.elementAt(i).doubleValue(); } /* * Used to find the next value for the axis label. For non-log axes, we just * return pos + step. For log axes, we read the appropriate value in the * grid Vector, add it to _gridBase and return the sum. We also take care to * reset _gridCurJuke if necessary. Note that for log axes, _gridInit() must * be called before calling _gridStep(). Based on stepGrid() from xgraph.c * by David Harrison. */ private double _gridStep(Vector<Double> grid, double pos, double step, boolean logflag) { if (logflag) { if (++_gridCurJuke >= grid.size()) { _gridCurJuke = 0; _gridBase += Math.ceil(step); } if (_gridCurJuke >= grid.size()) { return pos + step; } return _gridBase + grid.elementAt(_gridCurJuke).doubleValue(); } return pos + step; } /* * Return the number of fractional digits required to display the given * number. No number larger than 15 is returned (if more than 15 digits are * required, 15 is returned). */ private int _numFracDigits(double num) { int numdigits = 0; while ((numdigits <= 15) && (num != Math.floor(num))) { num *= 10.0; numdigits += 1; } return numdigits; } /* * Return the number of integer digits required to display the given number. * No number larger than 15 is returned (if more than 15 digits are * required, 15 is returned). */ private int _numIntDigits(double num) { int numdigits = 0; while ((numdigits <= 15) && ((int) num != 0.0)) { num /= 10.0; numdigits += 1; } return numdigits; } /* * Parse a string of the form: "word num, word num, word num, ..." where the * word must be enclosed in quotes if it contains spaces, and the number is * interpreted as a floating point number. Ignore any incorrectly formatted * fields. I <i>xtick</i> is true, then interpret the parsed string to * specify the tick labels on the x axis. Otherwise, do the y axis. */ private void _parsePairs(String line, boolean xtick) { // Clear current ticks first. if (xtick) { _xticks = null; _xticklabels = null; } else { _yticks = null; _yticklabels = null; } int start = 0; boolean cont = true; while (cont) { int comma = line.indexOf(",", start); String pair = null; if (comma > start) { pair = (line.substring(start, comma)).trim(); } else { pair = (line.substring(start)).trim(); cont = false; } int close = -1; int open = 0; if (pair.startsWith("\"")) { close = pair.indexOf("\"", 1); open = 1; } else { close = pair.indexOf(" "); } if (close > 0) { String label = pair.substring(open, close); String index = (pair.substring(close + 1)).trim(); try { double idx = (Double.valueOf(index)).doubleValue(); if (xtick) { addXTick(label, idx); } else { addYTick(label, idx); } } catch (NumberFormatException e) { System.err.println("Warning from PlotBox: " + "Unable to parse ticks: " + e.getMessage()); // ignore if format is bogus. } } start = comma + 1; comma = line.indexOf(",", start); } } /** * Return a default set of rendering hints for image export, which specifies * the use of anti-aliasing. */ private RenderingHints _defaultImageRenderingHints() { RenderingHints hints = new RenderingHints(null); hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); return hints; } /* * Given a number, round up to the nearest power of ten times 1, 2, or 5. * * Note: The argument must be strictly positive. */ private double _roundUp(double val) { int exponent = (int) Math.floor(Math.log(val) * _LOG10SCALE); val *= Math.pow(10, -exponent); if (val > 5.0) { val = 10.0; } else if (val > 2.0) { val = 5.0; } else if (val > 1.0) { val = 2.0; } val *= Math.pow(10, exponent); return val; } /* * Internal implementation of setXRange, so that it can be called when * autoranging. */ private void _setXRange(double min, double max) { // We check to see if the original range has been given here // because if we check in setXRange(), then we will not catch // the case where we have a simple plot file that consists of just // data points // // 1. Create a file that consists of two data points // 1 1 // 2 3 // 2. Start up plot on it // $PTII/bin/ptplot foo.plt // 3. Zoom in // 4. Hit reset axes // 5. The bug is that the axes do not reset to the initial settings // Changing the range means we have to replot. _plotImage = null; if (!_originalXRangeGiven) { _originalXlow = min; _originalXhigh = max; _originalXRangeGiven = true; } // If values are invalid, try for something reasonable. if (min > max) { min = -1.0; max = 1.0; } else if (min == max) { min -= 1.0; max += 1.0; } // if (_xRangeGiven) { // The user specified the range, so don't pad. // _xMin = min; // _xMax = max; // } else { // Pad slightly so that we don't plot points on the axes. _xMin = min - ((max - min) * _padding); _xMax = max + ((max - min) * _padding); // } // Find the exponent. double largest = Math.max(Math.abs(_xMin), Math.abs(_xMax)); _xExp = (int) Math.floor(Math.log(largest) * _LOG10SCALE); // Use the exponent only if it's larger than 1 in magnitude. if ((_xExp > 1) || (_xExp < -1)) { double xs = 1.0 / Math.pow(10.0, _xExp); _xtickMin = _xMin * xs; _xtickMax = _xMax * xs; } else { _xtickMin = _xMin; _xtickMax = _xMax; _xExp = 0; } } /* * Internal implementation of setYRange, so that it can be called when * autoranging. */ private void _setYRange(double min, double max) { // See comment in _setXRange() about why this is necessary. // Changing the range means we have to replot. _plotImage = null; if (!_originalYRangeGiven) { _originalYlow = min; _originalYhigh = max; _originalYRangeGiven = true; } // If values are invalid, try for something reasonable. if (min > max) { min = -1.0; max = 1.0; } else if (min == max) { min -= 0.1; max += 0.1; } // if (_yRangeGiven) { // The user specified the range, so don't pad. // _yMin = min; // _yMax = max; // } else { // Pad slightly so that we don't plot points on the axes. _yMin = min - ((max - min) * _padding); _yMax = max + ((max - min) * _padding); // } // Find the exponent. double largest = Math.max(Math.abs(_yMin), Math.abs(_yMax)); _yExp = (int) Math.floor(Math.log(largest) * _LOG10SCALE); // Use the exponent only if it's larger than 1 in magnitude. if ((_yExp > 1) || (_yExp < -1)) { double ys = 1.0 / Math.pow(10.0, _yExp); _ytickMin = _yMin * ys; _ytickMax = _yMax * ys; } else { _ytickMin = _yMin; _ytickMax = _yMax; _yExp = 0; } } /* * Zoom in or out based on the box that has been drawn. The argument gives * the lower right corner of the box. This method is not synchronized * because it is called within the UI thread, and making it synchronized * causes a deadlock. * * @param x The final x position. * * @param y The final y position. */ void _zoomEnd(int x, int y) { ZoomRectangle selectedZoom = zoomBox; zoomBox = null; Graphics graphics = offScreenGraphics; // Ignore if there is no graphics object to draw on. if (graphics == null) { return; } if (selectedZoom != null) { selectedZoom.setEnd(x, y); if (selectedZoom.isZoomingIn()) { // NOTE: ignore if total drag less than 5 pixels. if ((Math.abs(selectedZoom.getX1() - selectedZoom.getX2()) > 5) && (Math.abs(selectedZoom.getY1() - selectedZoom.getY2()) > 5)) { double a = _xMin + ((selectedZoom.getX1() - _ulx) / _xscale); double b = _xMin + ((selectedZoom.getX2() - _ulx) / _xscale); // NOTE: It used to be that it was problematic to set // the X range here because it conflicted with the wrap // mechanism. But now the wrap mechanism saves the state // of the X range when the setWrap() method is called, // so this is safe. // EAL 6/12/00. if (a < b) { setXRange(a, b); } else { setXRange(b, a); } a = _yMax - ((selectedZoom.getY1() - _uly) / _yscale); b = _yMax - ((selectedZoom.getY2() - _uly) / _yscale); if (a < b) { setYRange(a, b); } else { setYRange(b, a); } } } else if (selectedZoom.isZoomingOut()) { // Calculate zoom factor. double a = Math.abs(selectedZoom.getX1() - selectedZoom.getX2()) / 10.0 - 1; double b = Math.abs(selectedZoom.getY1() - selectedZoom.getY2()) / 10.0 - 1; double newx1 = _xMax + ((_xMax - _xMin) * a / 4); double newx2 = _xMin - ((_xMax - _xMin) * a / 4); // NOTE: To limit zooming out to the fill area, uncomment // this... // if (newx1 > _xTop) newx1 = _xTop; // if (newx2 < _xBottom) newx2 = _xBottom; double newy1 = _yMax + ((_yMax - _yMin) * b / 4); double newy2 = _yMin - ((_yMax - _yMin) * b / 4); // NOTE: To limit zooming out to the fill area, uncomment // this... // if (newy1 > _yTop) newy1 = _yTop; // if (newy2 < _yBottom) newy2 = _yBottom; zoom(newx2, newy2, newx1, newy1); } } _plotImage = null; repaint(); } /* * Draw a box for an interactive zoom box. The starting point (the upper * left corner of the box) is taken to be that specified by the startZoom() * method. The argument gives the lower right corner of the box. If a * previous box has been drawn, erase it first. This method is not * synchronized because it is called within the UI thread, and making it * synchronized causes a deadlock. * * @param x The x position. * * @param y The y position. */ void _zoomBox(int x, int y) { if (zoomBox != null) { zoomBox.setEnd(x, y); _plotImage = null; repaint(); } } /* * Set the starting point for an interactive zoom box (the upper left * corner). This method is not synchronized because it is called within the * UI thread, and making it synchronized causes a deadlock. * * @param x The x position. * * @param y The y position. */ void _zoomStart(int x, int y) { zoomBox = new ZoomRectangle(this, x, y); } // ///////////////////////////////////////////////////////////////// // // private variables //// private ZoomRectangle zoomBox; /** @serial Indicator of whether actions are deferred. */ private boolean _actionsDeferred = false; /** @serial List of deferred actions. */ private List<Runnable> _deferredActions; /** * @serial The range of the plot as labeled (multiply by 10^exp for actual * range. */ private double _ytickMax = 0.0; /** * @serial The range of the plot as labeled (multiply by 10^exp for actual * range. */ private double _ytickMin = 0.0; /** * @serial The range of the plot as labeled (multiply by 10^exp for actual * range. */ private double _xtickMax = 0.0; /** * @serial The range of the plot as labeled (multiply by 10^exp for actual * range. */ private double _xtickMin = 0.0; /** * @serial The power of ten by which the range numbers should be multiplied. */ private int _yExp = 0; /** * @serial The power of ten by which the range numbers should be multiplied. */ private int _xExp = 0; /** @serial Scaling used in making tick marks. */ private double _ytickscale = 0.0; /** @serial Scaling used in making tick marks. */ private double _xtickscale = 0.0; /** @serial Font information. */ private Font _labelFont = null; /** @serial Font information. */ private final Font _superscriptFont = null; /** @serial Font information. */ private Font _titleFont = null; /** @serial FontMetric information. */ private FontMetrics _labelFontMetrics = DefaultFonts.LABElFONtMETRICS; /** @serial FontMetric information. */ private final FontMetrics _superscriptFontMetrics = DefaultFonts.SUPErSCRIPtFONtMETRICS; /** @serial FontMetric information. */ private FontMetrics _titleFontMetrics = DefaultFonts.TITLeFONtMETRICS; // Number format cache used by _formatNum. // See the comment in _formatNum for more information. // private transient NumberFormat _numberFormat = null; // Used for log axes. Index into vector of axis labels. private transient int _gridCurJuke = 0; // Used for log axes. Base of the grid. private transient double _gridBase = 0.0; // An array of strings for reporting errors. private transient String[] _errorMsg; /** @serial The title and label strings. */ private String _xlabel; /** @serial The title and label strings. */ private String _ylabel; /** @serial The title and label strings. */ private String _title; /** @serial Legend information. */ private Vector<String> _legendStrings = new Vector<String>(); /** @serial Legend information. */ private Vector<Integer> _legendDatasets = new Vector<Integer>(); /** @serial If XTicks or YTicks are given/ */ private Vector<Double> _xticks = null; /** @serial If XTicks or YTicks are given/ */ private Vector<String> _xticklabels = null; /** @serial If XTicks or YTicks are given/ */ private Vector<Double> _yticks = null; /** @serial If XTicks or YTicks are given/ */ private Vector<String> _yticklabels = null; // A button for filling the plot private transient JButton _fillButton = null; // A button for formatting the plot private transient JButton _formatButton = null; // Indicator of whether X and Y range has been first specified. boolean _originalXRangeGiven = false; // Indicator of whether X and Y range has been first specified. boolean _originalYRangeGiven = false; // First values specified to setXRange() and setYRange(). double _originalXlow = 0.0; // First values specified to setXRange() and setYRange(). double _originalXhigh = 0.0; // First values specified to setXRange() and setYRange(). double _originalYlow = 0.0; // First values specified to setXRange() and setYRange(). double _originalYhigh = 0.0; // An offscreen buffer for improving plot performance. protected transient BufferedImage _plotImage = null; // A button for printing the plot private transient JButton _printButton = null; // A button for filling the plot private transient JButton _resetButton = null; protected Graphics2D offScreenGraphics; // // inner classes //// class ButtonListener implements ActionListener { @Override public void actionPerformed(ActionEvent event) { if (event.getSource() == _fillButton) { fillPlot(); } else if (event.getSource() == _printButton) { // FIXME: Code duplication with PlotFrame._printCrossPlatform PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet(); PrinterJob job = PrinterJob.getPrinterJob(); // [email protected]: Get the Page Format and use it. // PageFormat format = job.pageDialog(job.defaultPage()); // job.setPrintable(PlotBox.this, format); job.setPrintable(PlotBox.this); if (job.printDialog(aset)) { try { job.print(aset); } catch (Exception ex) { Component ancestor = getTopLevelAncestor(); JOptionPane.showMessageDialog(ancestor, "Printing failed:\n" + ex.toString(), "Print Error", JOptionPane.WARNING_MESSAGE); } } } else if (event.getSource() == _resetButton) { resetAxes(); } else if (event.getSource() == _formatButton) { PlotFormatter fmt = new PlotFormatter(PlotBox.this); fmt.openModal(); } } } public class ZoomListener implements MouseListener { /* * (non-Javadoc) * * @see * java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent) */ @Override public void mouseClicked(MouseEvent event) { requestFocus(); } /* * (non-Javadoc) * * @see * java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent) */ @Override public void mouseEntered(MouseEvent event) { } /* * (non-Javadoc) * * @see * java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent) */ @Override public void mouseExited(MouseEvent event) { } /* * (non-Javadoc) * * @see * java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent) */ @Override public void mousePressed(final MouseEvent event) { // http://developer.java.sun.com/developer/bugParade/bugs/4072703.html // BUTTON1_MASK still not set for MOUSE_PRESSED events // suggests: // Workaround // Assume that a press event with no modifiers must be button 1. // This has the serious drawback that it is impossible to be sure // that button 1 hasn't been pressed along with one of the other // buttons. // This problem affects Netscape 4.61 under Digital Unix and // 4.51 under Solaris if (((event.getModifiers() & InputEvent.BUTTON1_MASK) != 0) || (event.getModifiers() == 0)) { PlotBox.this._zoomStart(event.getX(), event.getY()); } } /* * (non-Javadoc) * * @see * java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent) */ @Override public void mouseReleased(final MouseEvent event) { if (((event.getModifiers() & InputEvent.BUTTON1_MASK) != 0) || (event.getModifiers() == 0)) { PlotBox.this._zoomEnd(event.getX(), event.getY()); } } } /** * The {@code DragListener} calls PlotBox._zoomBox whenever the mouse is * moved while the left button is pressed. * * @author reimann * */ public class DragListener implements MouseMotionListener { /* * (non-Javadoc) * * @see * java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent * ) */ @Override public void mouseDragged(final MouseEvent event) { if (((event.getModifiers() & InputEvent.BUTTON1_MASK) != 0) || (event.getModifiers() == 0)) { PlotBox.this._zoomBox(event.getX(), event.getY()); } } /* * (non-Javadoc) * * @see * java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent * ) */ @Override public void mouseMoved(MouseEvent event) { } } class CommandListener implements KeyListener { @Override public void keyPressed(KeyEvent e) { int keycode = e.getKeyCode(); switch (keycode) { case KeyEvent.VK_CONTROL: _control = true; break; case KeyEvent.VK_SHIFT: _shift = true; break; case KeyEvent.VK_C: if (_control) { // The "null" sends the output to the clipboard. export(null); String message = "Encapsulated PostScript (EPS) " + "exported to clipboard."; JOptionPane.showMessageDialog(PlotBox.this, message, "Ptolemy Plot Message", JOptionPane.INFORMATION_MESSAGE); } break; case KeyEvent.VK_D: if (!_control && _shift) { write(System.out); String message = "Plot data sent to standard out."; JOptionPane.showMessageDialog(PlotBox.this, message, "Ptolemy Plot Message", JOptionPane.INFORMATION_MESSAGE); } if (_control) { // xgraph and many other Unix apps use Control-D to exit StringUtilities.exit(1); } break; case KeyEvent.VK_E: if (!_control && _shift) { export(System.out); String message = "Encapsulated PostScript (EPS) " + "exported to standard out."; JOptionPane.showMessageDialog(PlotBox.this, message, "Ptolemy Plot Message", JOptionPane.INFORMATION_MESSAGE); } break; case KeyEvent.VK_F: if (!_control && _shift) { fillPlot(); } break; case KeyEvent.VK_H: if (!_control && _shift) { _help(); } break; case KeyEvent.VK_Q: if (!_control) { // xv uses q to quit. StringUtilities.exit(1); } break; case KeyEvent.VK_SLASH: if (_shift) { // Question mark is SHIFT-SLASH _help(); } break; default: // None } } @Override public void keyReleased(KeyEvent e) { int keycode = e.getKeyCode(); switch (keycode) { case KeyEvent.VK_CONTROL: _control = false; break; case KeyEvent.VK_SHIFT: _shift = false; break; default: // None } } // The keyTyped method is broken in jdk 1.1.4. // It always gets "unknown key code". @Override public void keyTyped(KeyEvent e) { } private boolean _control = false; private boolean _shift = false; } public void reactivateAutoScale() { _xRangeGiven = false; _yRangeGiven = false; revalidate(); repaint(); fireAction(Action.AUTOMATIC_ZOOM_ACTIVATED); } public static enum Action { MANUAL_ZOOM_ACTIVATED, AUTOMATIC_ZOOM_ACTIVATED; } public void registerListener(ActionListener actionListener) { listeners.add(actionListener); } public void removeListener(ActionListener actionListener) { listeners.remove(actionListener); } protected void fireAction(Action action) { ActionEvent actionEvent = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, action.toString()); for (ActionListener actionListener : listeners) { actionListener.actionPerformed(actionEvent); } } private final Set<ActionListener> listeners = new HashSet<ActionListener>(); }