/** * Copyright 2004-2006 DFKI GmbH. * All Rights Reserved. Use is subject to license terms. * * This file is part of MARY TTS. * * MARY TTS is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ package marytts.signalproc.display; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.ScrollPaneConstants; import marytts.util.string.PrintfFormat; /** * @author Marc Schröder * */ public class FunctionGraph extends JPanel implements CursorSource, CursorListener { public static final int DEFAULT_WIDTH = 640; public static final int DEFAULT_HEIGHT = 480; public static final int DRAW_LINE = 1; public static final int DRAW_DOTS = 2; public static final int DRAW_LINEWITHDOTS = 3; public static final int DRAW_HISTOGRAM = 4; public static final int DOT_FULLCIRCLE = 1; public static final int DOT_FULLSQUARE = 2; public static final int DOT_FULLDIAMOND = 3; public static final int DOT_EMPTYCIRCLE = 11; public static final int DOT_EMPTYSQUARE = 12; public static final int DOT_EMPTYDIAMOND = 13; protected int paddingLeft = 40; protected int paddingRight = 10; protected int paddingTop = 10; protected int paddingBottom = 40; protected double x0; protected double xStep; protected List<double[]> dataseries = new ArrayList<double[]>(); protected double ymin; protected double ymax; protected boolean showXAxis = true; protected boolean showYAxis = true; protected BufferedImage graphImage = null; protected Color backgroundColor = Color.WHITE; protected Color axisColor = Color.BLACK; protected List<Color> graphColor = new ArrayList<Color>(); protected Color histogramBorderColor = Color.BLACK; protected List<Integer> graphStyle = new ArrayList<Integer>(); protected List<Integer> dotStyle = new ArrayList<Integer>(); protected int dotSize = 6; protected int histogramWidth = 10; protected boolean autoYMinMax = true; // automatically determine ymin and ymax // data to be used for drawing cursor et al on the GlassPane: // x and y coordinates, in data space protected DoublePoint positionCursor = new DoublePoint(); protected DoublePoint rangeCursor = new DoublePoint(); protected List cursorListeners = new ArrayList(); /** * Display a 2d graph showing y(x), with labelled scales. This constructor is for subclasses only, which may need to perform * some operations before calling initialise(). */ protected FunctionGraph() { super(); } /** * Display a 2d graph showing y(x), with labelled scales. * * @param x0 * x0 * @param xStep * xStep * @param y * y */ public FunctionGraph(double x0, double xStep, double[] y) { this(DEFAULT_WIDTH, DEFAULT_HEIGHT, x0, xStep, y); } /** * Display a 2d graph showing y(x), with labelled scales. * * @param width * width * @param height * height * @param x0 * x0 * @param xStep * xStep * @param y * y */ public FunctionGraph(int width, int height, double x0, double xStep, double[] y) { super(); initialise(width, height, x0, xStep, y); } public void initialise(int width, int height, double newX0, double newXStep, double[] data) { setPreferredSize(new Dimension(width, height)); setOpaque(true); this.addMouseListener(new MouseListener() { public void mouseClicked(MouseEvent e) { // System.err.println("Mouse clicked"); if (e.getButton() == MouseEvent.BUTTON1) { // left mouse button // set position cursor; if we are to the right of rangeCursor, // delete rangeCursor. positionCursor.x = imageX2X(e.getX() - paddingLeft); positionCursor.y = imageY2Y(getHeight() - paddingBottom - e.getY()); if (!Double.isNaN(rangeCursor.x) && positionCursor.x > rangeCursor.x) { rangeCursor.x = Double.NaN; } } else if (e.getButton() == MouseEvent.BUTTON3) { // right mouse button // set range cursor, but only if we are to the right of positionCursor rangeCursor.x = imageX2X(e.getX() - paddingLeft); rangeCursor.y = imageY2Y(getHeight() - paddingBottom - e.getY()); if (positionCursor.x > rangeCursor.x) { rangeCursor.x = Double.NaN; } } FunctionGraph.this.notifyCursorListeners(); FunctionGraph.this.requestFocusInWindow(); } public void mousePressed(MouseEvent e) { } public void mouseReleased(MouseEvent e) { } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } }); updateData(newX0, newXStep, data); // set styles for primary data series: graphColor.add(Color.BLUE); graphStyle.add(DRAW_LINE); dotStyle.add(DOT_FULLCIRCLE); } /** * Replace the previous data with the given new data. Any secondary data series added using { * {@link #addDataSeries(double[], Color, int, int)} are removed. * * @param newX0 * x position of first data point * @param newXStep * distance between data points on X axis * @param data * all data points */ public void updateData(double newX0, double newXStep, double[] data) { if (newXStep <= 0) { throw new IllegalArgumentException("newXStep must be >0"); } if (data == null || data.length == 0) { throw new IllegalArgumentException("No data"); } this.x0 = newX0; this.xStep = newXStep; double[] series = new double[data.length]; System.arraycopy(data, 0, series, 0, data.length); // Do not allow old secondary data sets with a new primary one: while (dataseries.size() > 0) { dataseries.remove(0); } // Also remove the styles of the secondary data sets: while (graphColor.size() > 1) { graphColor.remove(1); } while (graphStyle.size() > 1) { graphStyle.remove(1); } while (dotStyle.size() > 1) { dotStyle.remove(1); } this.dataseries.add(0, series); if (autoYMinMax) { ymin = Double.NaN; ymax = Double.NaN; for (int i = 0; i < data.length; i++) { if (Double.isNaN(data[i])) // missing value -- skip continue; if (Double.isNaN(ymin)) { assert Double.isNaN(ymax); ymin = data[i]; ymax = data[i]; continue; } if (data[i] < ymin) ymin = data[i]; else if (data[i] > ymax) ymax = data[i]; } // If the x axis is painted in the middle (ymin << 0), // we need much less paddingBottom: if (ymin < 0) { paddingBottom = paddingTop; } } // And invalidate any previous graph image: graphImage = null; } public void setPrimaryDataSeriesStyle(Color newGraphColor, int newGraphStyle, int newDotStyle) { graphColor.remove(0); graphColor.add(0, newGraphColor); graphStyle.remove(0); graphStyle.add(0, newGraphStyle); dotStyle.remove(0); dotStyle.add(0, newDotStyle); } /** * Manually set the min and max values for the y axis. * * @param theYMin * the Y min * @param theYMax * the Y max */ public void setYMinMax(double theYMin, double theYMax) { autoYMinMax = false; ymin = theYMin; ymax = theYMax; // If the x axis is painted in the middle (ymin << 0), // we need much less paddingBottom: if (ymin < 0) { paddingBottom = paddingTop; } } /** * Add a secondary data series to this graph. * * @param data * the function data, which must be of same length as the original data. * {@link #updateData(double, double, double[])} * @param newGraphColor * a colour * @param newGraphStyle * the style for painting this data series. One of {@link #DRAW_LINE}, {@link #DRAW_DOTS}, * {@value #DRAW_LINEWITHDOTS}, {@link #DRAW_HISTOGRAM}. * @param newDotStyle * the shape of any dots to use (meaningful only with newGraphStyle == {@link #DRAW_DOTS} or * {@link #DRAW_LINEWITHDOTS}). One of {@link #DOT_EMPTYCIRCLE}, {@link #DOT_EMPTYDIAMOND}, * {@link #DOT_EMPTYSQUARE}, {@link #DOT_FULLCIRCLE}, {@link #DOT_FULLDIAMOND}, {@link #DOT_FULLSQUARE}. For other * graph styles, this is ignored, and it is recommended to set it to -1 for clarity. */ public void addDataSeries(double[] data, Color newGraphColor, int newGraphStyle, int newDotStyle) { if (data == null) throw new NullPointerException("Cannot add null data"); if (dataseries.get(0).length != data.length) throw new IllegalArgumentException( "Can only add data of the exact same length as the original data series; len(orig)=" + dataseries.get(0).length + ", len(data)=" + data.length); double[] series = new double[data.length]; System.arraycopy(data, 0, series, 0, data.length); dataseries.add(series); graphColor.add(newGraphColor); graphStyle.add(newGraphStyle); dotStyle.add(newDotStyle); if (autoYMinMax) { for (int i = 0; i < data.length; i++) { if (Double.isNaN(data[i])) // missing value -- skip continue; if (Double.isNaN(ymin)) { assert Double.isNaN(ymax); ymin = data[i]; ymax = data[i]; continue; } if (data[i] < ymin) ymin = data[i]; else if (data[i] > ymax) ymax = data[i]; } // If the x axis is painted in the middle (ymin << 0), // we need much less paddingBottom: if (ymin < 0) { paddingBottom = paddingTop; } } // And invalidate any previous graph image: graphImage = null; } public double getZoomX() { double[] data = dataseries.get(0); double zoom = ((double) getPreferredSize().width - paddingLeft - paddingRight) / data.length; // System.err.println("Current Zoom: " + zoom + "(pref. size: " + getPreferredSize().width + "x" + // getPreferredSize().height + ")"); return zoom; } /** * Set the zoom of the X * * @param factor * the zoom factor for X; 1 means that each data point corresponds to one pixel; 0.5 means that 2 data points are * mapped onto one pixel; etc. */ public void setZoomX(double factor) { // System.err.println("New zoom factor requested: " + factor); // Old visible rectangle: Rectangle r = getVisibleRect(); int oldWidth = getPreferredSize().width; double[] data = dataseries.get(0); int newWidth = (int) (data.length * factor) + paddingLeft + paddingRight; if (isVisible()) { setVisible(false); setPreferredSize(new Dimension(newWidth, getPreferredSize().height)); // Only scroll to center of what was previous visible if not at left end: if (r.x != 0) { Rectangle newVisibleRect = new Rectangle((r.x + r.width / 2 - paddingLeft) * newWidth / oldWidth - r.width / 2 + paddingLeft, r.y, r.width, r.height); scrollRectToVisible(newVisibleRect); } setVisible(true); } else { setPreferredSize(new Dimension(newWidth, getPreferredSize().height)); createGraphImage(); } // System.err.print("updated "); getZoomX(); } protected void createGraphImage() { graphImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB); if (graphImage == null) { throw new NullPointerException("Cannot create image for drawing graph"); } Graphics2D g = (Graphics2D) graphImage.createGraphics(); double width = getWidth(); double height = getHeight(); int image_fromX = 0; int image_toX = (int) width; g.setBackground(backgroundColor); g.clearRect(0, 0, (int) width, (int) height); g.setFont(new Font("Courier", 0, 10)); // Now reduce the drawing area: int startX = paddingLeft; int startY = (int) height - paddingBottom; width -= paddingLeft + paddingRight; height -= paddingTop + paddingBottom; // Make sure we are not trying to draw the function outside its area: if (image_fromX < startX) image_fromX = startX; if (image_toX > startX + width) image_toX = (int) (startX + width); int image_y_origin; if (getYRange() == 0) image_y_origin = startY; else image_y_origin = startY - (int) ((-ymin / getYRange()) * height); int image_x_origin = startX + (int) ((-x0 / getXRange()) * width); // Draw the function itself: if (getYRange() > 0) { for (int s = 0; s < dataseries.size(); s++) { drawData(g, image_fromX - startX, image_toX - startX, startX, image_y_origin, startY, (int) height, dataseries.get(s), graphColor.get(s), graphStyle.get(s), dotStyle.get(s)); } } // Draw the x axis, if requested: if (showXAxis) { if (startY >= image_y_origin && image_y_origin >= startY - height) { drawXAxis(g, width, startX, startY, image_y_origin); } else { // draw x axis at the bottom, even if that is not 0: drawXAxis(g, width, startX, startY, startY); } } // Draw the y axis, if requested: if (showYAxis) { if (image_fromX <= image_x_origin && image_x_origin <= image_toX) { drawYAxis(g, height, startX, startY, image_x_origin); } else { // draw y axis at the left, even if that is not 0: drawYAxis(g, height, startX, startY, startX); } } } /** * While painting the graph, draw the actual function data. * * @param g * the graphics2d object to paint in * @param image_fromX * first visible X coordinate of the Graph display area (= after subtracting space reserved for Y axis) * @param image_toX * last visible X coordinate of the Graph display area (= after subtracting space reserved for Y axis) * @param image_refX * X coordinate of the origin, in the display area * @param image_refY * Y coordinate of the origin, in the display area * @param startY * the start position on the Y axis (= the lower bound of the drawing area) * @param image_height * the height of the drawable region for the y values * @param data * data * @param currentGraphColor * current graph color * @param currentGraphStyle * current graph style * @param currentDotStyle * current dot style */ protected void drawData(Graphics2D g, int image_fromX, int image_toX, int image_refX, int image_refY, int startY, int image_height, double[] data, Color currentGraphColor, int currentGraphStyle, int currentDotStyle) { int index_fromX = imageX2indexX(image_fromX); if (index_fromX < 0) index_fromX = 0; int index_toX = imageX2indexX(image_toX); if (index_toX < data.length) index_toX += 20; if (index_toX > data.length) index_toX = data.length; // System.err.println("Drawing values " + index_fromX + " to " + index_toX + " of " + y.length); double xo = 0.0; double yo = 0.0; double xp = 0.0; double yp = 0.0; g.setColor(currentGraphColor); for (int i = index_fromX; i < index_toX; i++) { if (!Double.isNaN(data[i])) { xp = indexX2imageX(i); yp = y2imageY(data[i]); // System.err.println("Point "+i+": ("+(image_refX+(int)xp)+","+(image_refY-(int)yp)+")"); if (currentGraphStyle == DRAW_LINE || currentGraphStyle == DRAW_LINEWITHDOTS) { g.drawLine(image_refX + (int) xo, image_refY - (int) yo, image_refX + (int) xp, image_refY - (int) yp); } if (currentGraphStyle == DRAW_DOTS || currentGraphStyle == DRAW_LINEWITHDOTS) { drawDot(g, image_refX + (int) xp, image_refY - (int) yp, currentDotStyle); } if (currentGraphStyle == DRAW_HISTOGRAM) { int topY = image_refY; if (yp > 0) topY = image_refY - (int) yp; int histHeight = (int) Math.abs(yp); // cut to drawing area if x axis not at y==0: if (topY + histHeight > startY) { histHeight = startY - topY; } g.setColor(currentGraphColor); g.fillRect(image_refX + (int) xp - histogramWidth / 2, topY, histogramWidth, histHeight); g.setColor(histogramBorderColor); g.drawRect(image_refX + (int) xp - histogramWidth / 2, topY, histogramWidth, histHeight); } xo = xp; yo = yp; } } } protected void drawDot(Graphics2D g, int x, int y, int currentDotStyle) { switch (currentDotStyle) { case DOT_FULLCIRCLE: g.fillOval(x - dotSize / 2, y - dotSize / 2, dotSize, dotSize); break; case DOT_FULLSQUARE: g.fillRect(x - dotSize / 2, y - dotSize / 2, dotSize, dotSize); break; case DOT_FULLDIAMOND: g.fillPolygon(new int[] { x - dotSize / 2, x, x + dotSize / 2, x }, new int[] { y, y - dotSize / 2, y, y + dotSize / 2 }, 4); break; case DOT_EMPTYCIRCLE: g.drawOval(x - dotSize / 2, y - dotSize / 2, dotSize, dotSize); break; case DOT_EMPTYSQUARE: g.drawRect(x - dotSize / 2, y - dotSize / 2, dotSize, dotSize); break; case DOT_EMPTYDIAMOND: g.drawPolygon(new int[] { x - dotSize / 2, x, x + dotSize / 2, x }, new int[] { y, y - dotSize / 2, y, y + dotSize / 2 }, 4); break; default: break; } } protected void drawYAxis(Graphics2D g, double height, int startX, int startY, int image_x_origin) { g.setColor(axisColor); double yRange = getYRange(); g.drawLine(image_x_origin, startY, image_x_origin, startY - (int) height); // Do not try to draw units if yRange is 0: if (yRange == 0) return; // Units on the y axis: // major units with labels every 50-100 pixels int unitOrder = (int) Math.floor(Math.log(yRange / 5) / Math.log(10)); double unitDistance = Math.pow(10, unitOrder); double image_unitDistance = unitDistance / yRange * height; if (image_unitDistance < 20) { unitDistance *= 5; } else if (image_unitDistance < 50) { unitDistance *= 2; } double unitStart = ymin; double modulo = ymin % unitDistance; if (modulo != 0) { if (modulo > 0) unitStart += unitDistance - modulo; else // < 0 unitStart += Math.abs(modulo); } PrintfFormat labelFormat; if (unitOrder > 0) { labelFormat = new PrintfFormat("%.0f"); } else { labelFormat = new PrintfFormat("%." + (-unitOrder) + "f"); } boolean intLabels = ((int) unitDistance == (int) Math.ceil(unitDistance)); // System.err.println("y axis: yRange=" + yRange + ", unitDistance=" + unitDistance + ", unitStart=" + unitStart + // ", ymin=" + ymin + ", ymin%unitDistance=" + (ymin%unitDistance)); for (double i = unitStart; i < ymax; i += unitDistance) { double yunit = (i - ymin) / yRange * height; g.drawLine(image_x_origin + 5, startY - (int) yunit, image_x_origin - 5, startY - (int) yunit); // labels to the left of y axis: g.drawString(labelFormat.sprintf(i), image_x_origin - 30, startY - (int) yunit + 5); } } protected void drawXAxis(Graphics2D g, double width, int startX, int startY, int image_y_origin) { g.setColor(axisColor); double xRange = getXRange(); // System.err.println("Drawing X axis from " + startX + " to " + startX+(int)width + // "; startY="+startY+", image_y_origin="+image_y_origin); g.drawLine(startX, image_y_origin, startX + (int) width, image_y_origin); // Units on the x axis: // major units with labels every 50-100 pixels int nUnits = (int) width / 50; int unitOrder = (int) Math.floor(Math.log(xRange / nUnits) / Math.log(10)); double unitDistance = Math.pow(10, unitOrder); double image_unitDistance = unitDistance / xRange * width; if (image_unitDistance < 20) { unitDistance *= 5; } else if (image_unitDistance < 50) { unitDistance *= 2; } double unitStart = x0; double modulo = x0 % unitDistance; if (modulo != 0) { if (modulo > 0) unitStart += unitDistance - modulo; else // < 0 unitStart += Math.abs(modulo); } PrintfFormat labelFormat; if (unitOrder > 0) { labelFormat = new PrintfFormat("%.0f"); } else { labelFormat = new PrintfFormat("%." + (-unitOrder) + "f"); } // System.err.println("x axis: xRange=" + xRange + ", unitDistance=" + unitDistance + ", unitStart=" + unitStart + ", x0=" // + x0 + ", image_unitDistance=" + image_unitDistance); for (double i = unitStart; i < x0 + xRange; i += unitDistance) { double xunit = (i - x0) / xRange * width; // System.err.println("Drawing unit at " + (startX+(int)xunit)); g.drawLine(startX + (int) xunit, image_y_origin + 5, startX + (int) xunit, image_y_origin - 5); // labels below x axis: g.drawString(labelFormat.sprintf(i), startX + (int) xunit - 10, image_y_origin + 20); } } public void paintComponent(Graphics gr) { if (graphImage == null || getWidth() != graphImage.getWidth() || getHeight() != graphImage.getHeight()) { createGraphImage(); } Graphics2D g = (Graphics2D) gr; g.drawImage(graphImage, null, null); } protected int imageX2indexX(int imageX) { if (dataseries.isEmpty()) return 0; double[] data = dataseries.get(0); if (data == null) return 0; double xScaleFactor = ((double) getWidth() - paddingLeft - paddingRight) / data.length; return (int) (imageX / xScaleFactor); } protected double imageX2X(int imageX) { double[] data = dataseries.get(0); double xScaleFactor = ((double) getWidth() - paddingLeft - paddingRight) / (data.length * xStep); return x0 + imageX / xScaleFactor; } protected int indexX2imageX(int indexX) { if (dataseries.isEmpty()) return 0; double[] data = dataseries.get(0); if (data == null) return 0; double xScaleFactor = ((double) getWidth() - paddingLeft - paddingRight) / data.length; return (int) (indexX * xScaleFactor); } protected int X2imageX(double x) { double[] data = dataseries.get(0); double xScaleFactor = ((double) getWidth() - paddingLeft - paddingRight) / (data.length * xStep); return (int) ((x - x0) * xScaleFactor); } protected int X2indexX(double x) { return (int) ((x - x0) / xStep); } protected double imageY2Y(int imageY) { double yScaleFactor = ((double) getHeight() - paddingTop - paddingBottom) / getYRange(); return imageY / yScaleFactor; } protected int y2imageY(double y) { double yScaleFactor = ((double) getHeight() - paddingTop - paddingBottom) / getYRange(); return (int) (y * yScaleFactor); } protected double getYRange() { double yRange = ymax - ymin; if (Double.isNaN(yRange)) yRange = 0; return yRange; } protected double getXRange() { double[] data = dataseries.get(0); double xRange = data.length * xStep; return xRange; } public CursorDisplayer.CursorLine getPositionCursor() { if (Double.isNaN(positionCursor.x)) return null; return new CursorDisplayer.CursorLine(this, paddingLeft + X2imageX(positionCursor.x), paddingTop, getHeight() - paddingBottom); } public CursorDisplayer.CursorLine getRangeCursor() { if (Double.isNaN(rangeCursor.x)) return null; int imageX = X2imageX(rangeCursor.x); return new CursorDisplayer.CursorLine(this, paddingLeft + X2imageX(rangeCursor.x), paddingTop, getHeight() - paddingBottom, Color.YELLOW); } public CursorDisplayer.Label getValueLabel() { if (Double.isNaN(positionCursor.x)) return null; int imageX = X2imageX(positionCursor.x) + 10; int imageY = paddingTop + 10; return new CursorDisplayer.Label(this, getLabel(positionCursor.x, positionCursor.y), imageX, imageY); } public void addCursorListener(CursorListener l) { cursorListeners.add(l); } public CursorListener[] getCursorListeners() { return (CursorListener[]) cursorListeners.toArray(new CursorListener[0]); } public boolean removeCursorListener(CursorListener l) { return cursorListeners.remove(l); } protected void notifyCursorListeners() { for (Iterator it = cursorListeners.iterator(); it.hasNext();) { CursorListener l = (CursorListener) it.next(); l.updateCursorPosition(new CursorEvent(this)); } } /** * Used when keeping several FunctionGraphs' cursor positions in synchrony. Register each other as cursor listeners before the * glass pane; whichever gets clicked causes the others to be updated. Make sure to add any peers _before_ any displaying * cursor listeners, to make sure all are in line before being displayed. * * @param e * e cursor event */ public void updateCursorPosition(CursorEvent e) { FunctionGraph source = e.getSource(); positionCursor.x = source.positionCursor.x; rangeCursor.x = source.rangeCursor.x; } public JFrame showInJFrame(String title, boolean allowZoom, boolean exitOnClose) { return showInJFrame(title, DEFAULT_WIDTH, DEFAULT_HEIGHT + 50, allowZoom, true, exitOnClose); } public JFrame showInJFrame(String title, boolean allowZoom, boolean showControls, boolean exitOnClose) { return showInJFrame(title, DEFAULT_WIDTH, DEFAULT_HEIGHT + 50, allowZoom, showControls, exitOnClose); } public JFrame showInJFrame(String title, int width, int height, boolean allowZoom, boolean exitOnClose) { return showInJFrame(title, width, height, allowZoom, true, exitOnClose); } public JFrame showInJFrame(String title, int width, int height, boolean allowZoom, boolean showControls, boolean exitOnClose) { final JFrame main = new JFrame(title); int mainWidth = width; JScrollPane scroll = new JScrollPane(this); scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); // JLayeredPane layers = new JLayeredPane(); // layers.add(scroll, new Integer(1)); // scroll.setBounds(0, 0, this.getPreferredSize().width, this.getPreferredSize().height); // glass.setBounds(0, 0, this.getPreferredSize().width, this.getPreferredSize().height); // layers.add(glass, new Integer(50)); main.getContentPane().add(scroll, BorderLayout.CENTER); final CursorDisplayer glass = new CursorDisplayer(); main.setGlassPane(glass); glass.setVisible(true); glass.addCursorSource(this); this.addCursorListener(glass); if (allowZoom) { JPanel zoomPanel = new JPanel(); zoomPanel.setLayout(new BoxLayout(zoomPanel, BoxLayout.Y_AXIS)); main.getContentPane().add(zoomPanel, BorderLayout.WEST); zoomPanel.add(Box.createVerticalGlue()); JButton zoomIn = new JButton("Zoom In"); zoomIn.setAlignmentX(CENTER_ALIGNMENT); zoomIn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { setZoomX(getZoomX() * 2); FunctionGraph.this.requestFocus(); } }); zoomPanel.add(zoomIn); JButton zoomOut = new JButton("Zoom Out"); zoomOut.setAlignmentX(CENTER_ALIGNMENT); zoomOut.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { setZoomX(getZoomX() * 0.5); FunctionGraph.this.requestFocus(); } }); zoomPanel.add(zoomOut); if (showControls) { JPanel controls = getControls(); if (controls != null) { zoomPanel.add(Box.createVerticalGlue()); controls.setAlignmentX(CENTER_ALIGNMENT); zoomPanel.add(controls); } } mainWidth += zoomPanel.getPreferredSize().width + 30; zoomPanel.add(Box.createVerticalGlue()); } main.setSize(mainWidth, height); if (exitOnClose) { main.addWindowListener(new java.awt.event.WindowAdapter() { public void windowClosing(java.awt.event.WindowEvent evt) { System.exit(0); } }); } main.setVisible(true); this.requestFocus(); return main; } /** * Subclasses may provide specific controls here. * * @return a JPanel filled with the controls, or null if none are to be provided. */ protected JPanel getControls() { return null; } protected String getLabel(double x, double y) { // be about one order of magnitude less precise than there are pixels int pixelPrecisionX = 2; if (graphImage != null) { pixelPrecisionX = (int) (Math.log(graphImage.getWidth() / getXRange()) / Math.log(10)); } int precisionX = -(int) (Math.log(getXRange()) / Math.log(10)) + pixelPrecisionX; if (precisionX < 0) precisionX = 0; // ignore imageY int precisionY = -(int) (Math.log(getYRange()) / Math.log(10)) + 2; if (precisionY < 0) precisionY = 0; int indexX = X2indexX(x); double[] data = dataseries.get(0); return "f(" + new PrintfFormat("%." + precisionX + "f").sprintf(x) + ")=" + new PrintfFormat("%." + precisionY + "f").sprintf(data[indexX]); } public class DoublePoint { public DoublePoint() { this(Double.NaN, Double.NaN); } public DoublePoint(double x, double y) { this.x = x; this.y = y; } double x; double y; } }