package freegraph; import java.awt.*; import java.awt.image.*; import java.awt.event.*; import java.util.*; /** * GraphPlane is a X-Y or cartesian coordinate plane which draws the axis and a * list of GraphPlaneItems (typically instances of ExpressionEvaluator). * The GraphPlane handles the zooming with the mouse and can be monitored * with GraphPlaneListener to receive notification of axis changes or * mouse position changes.

* No Graphics2D is used to keep it compatible with Java 1.1.

*/ public class GraphPlane extends Component implements ActionListener { private double xMin = -20; private double yMin = -20; private double xMax = 20; private double yMax = 20; private double xTick = 5; private double yTick = 5; private int xZoomStart = 0; private int yZoomStart = 0; private int xZoomCurr = 0; private int yZoomCurr = 0; private boolean mouseDown = false; private int widthVar = 0; private int heightVar = 0; private Color axisColor = Color.blue; private Color outlineColor = Color.black; private Color bgColor = Color.lightGray; private Color zoomColor = Color.red; private InvertRectangle invRect; private PopupMenu pupMenuGraphPlane = new PopupMenu(); private MenuItem mniReset = new MenuItem(); private MenuItem mniZoomout = new MenuItem(); private MenuItem mniZoomin = new MenuItem(); private Vector graphPlaneItemsList = new Vector(); private Vector graphPlaneListenerList = new Vector(); private boolean drawGrid = true; public static int XMIN_DEFAULT = -20; public static int YMIN_DEFAULT = -20; public static int XMAX_DEFAULT = 20; public static int YMAX_DEFAULT = 20; /**** * constructs a new GraphPlane */ public GraphPlane() { init(); System.out.println("GraphPlane, Copyright 1998 by Martin Lovell, " + "ALL RIGHTS RESERVED"); } /*** * initializes a new GraphPlane by creating the popup menu items and * setting up mouse listeners for the component */ private void init() { mniReset.setLabel("Reset"); mniZoomout.setLabel("Zoom Out"); mniZoomin.setLabel("Zoom In"); pupMenuGraphPlane.add(mniReset); pupMenuGraphPlane.add(mniZoomout); pupMenuGraphPlane.add(mniZoomin); pupMenuGraphPlane.addActionListener(this); add(pupMenuGraphPlane); addMouseListener(new GraphPlaneMouseAdapter(this)); addMouseMotionListener(new GraphPlaneMouseMotionAdapter(this)); } /**** * returns the maximum X value for the GraphPlane */ public double getXMax() { return xMax; } /**** * returns the minimum X value for the GraphPlane */ public double getXMin() { return xMin; } /**** * returns the maximum Y value for the GraphPlane */ public double getYMax() { return yMax; } /**** * returns the minimum Y value for the GraphPlane */ public double getYMin() { return yMin; } /**** * returns an Enumeration of the GraphPlanItems */ public Enumeration graphPlaneItems() { return graphPlaneItemsList.elements(); } /** * adds a GraphPlaneItem to be drawn when the graph is drawn */ public void addGraphPlaneItem(GraphPlaneItem gp) { graphPlaneItemsList.addElement(gp); } /** * removes a GraphPlaneItem */ public void removeGraphPlaneItem(GraphPlaneItem gp) { graphPlaneItemsList.removeElement(gp); } /**** * adds a GraphPlaneListener to receive events about the mouse location and * the size of the GraphPlane. */ public void addGraphPlaneListener(GraphPlaneListener l) { graphPlaneListenerList.addElement(l); } /*** * removes a GraphPlaneListener */ public void removeGraphPlaneListener(GraphPlaneListener l) { graphPlaneListenerList.removeElement(l); } /***** * resets the GraphPlane to its default values */ public void reset() { xMin = XMIN_DEFAULT; yMin = YMIN_DEFAULT; xMax = XMAX_DEFAULT; yMax = YMAX_DEFAULT; repaint(); } /**** * zooms in the graph */ public void zoomout() { double dX = (xMax - xMin) / 2; double dY = (yMax - yMin) / 2; xMin = xMin - dX; yMin = yMin - dY; xMax = xMax + dX; yMax = yMax + dY; repaint(); } /*** * zooms out the graph */ public void zoomin() { double dX = (xMax - xMin) / 4; double dY = (yMax - yMin) / 4; xMin = xMin + dX; yMin = yMin + dY; xMax = xMax - dX; yMax = yMax - dY; repaint(); } /*** * dispatches action events from the popup menu. */ public void actionPerformed(ActionEvent e) { if (e.getActionCommand().equals("Reset")) reset(); else if (e.getActionCommand().equals("Zoom Out")) zoomout(); else if (e.getActionCommand().equals("Zoom In")) zoomin(); } /*** * sets up some instance variables used during painting */ private void setupBoundVars() { Rectangle brect = getBounds(); widthVar = brect.width - 1; heightVar = brect.height - 1; } /*** * paints the graph by drawing the axis and then drawing each * GraphPlaneItem. */ public void paint(Graphics g) { super.paint(g); drawGraph(g); for (int i = 0; i < graphPlaneItemsList.size(); i++) { ((GraphPlaneItem)graphPlaneItemsList.elementAt(i)).drawItem(this, g, 'X'); System.out.println("Drawing item: " + graphPlaneItemsList.elementAt(i)); } } /*** * converts a double to a string with at most 9 digits */ private String doubleToString(double x) { int numDigits = 9; StringBuffer str; str = new StringBuffer(Double.toString(x)); String s = str.toString(); int i = s.indexOf('E'); if (i == -1) { i = s.indexOf('.'); if (i != 0 && str.length() > i + numDigits) str.setLength(i + numDigits); } else if (i > numDigits) { int j; for (j=i; j getBounds().height) xaxis = getBounds().height / 2; if (yaxis < 0 || yaxis > getBounds().width) yaxis = getBounds().width / 2; String xmaxstr = doubleToString(xMax); int strlen = g.getFontMetrics().charsWidth(xmaxstr.toCharArray(), 0, xmaxstr.length()) + 3; g.drawString(xmaxstr, getBounds().width - strlen, xaxis); g.drawString(doubleToString(xMin), 5, xaxis); g.drawString(doubleToString(yMin), yaxis, getBounds().height - 15); g.drawString(doubleToString(yMax), yaxis, 15); } } /** * draws the axis, called by drawGraph() which is called by paint(); */ private void paintAxis(Graphics g) { g.setColor(axisColor); if (yMin * yMax < 0) { drawLine(g, xMin, 0, xMax, 0); paintXTicks(g); } if (xMin * xMax < 0) { drawLine(g, 0, yMin, 0, yMax); paintYTicks(g); } } /** * draws the X tick marks, * called by drawAxis, which is called by drawGraph(), which is * called by paint(); */ private void paintXTicks(Graphics g) { double tickPos = roundTo(xTick, xMin); double tickSize = (yMax - yMin) / 40; while (convertY(tickSize) <= 0) tickSize++; while (tickPos < xMax) { drawLine(g, tickPos, -tickSize, tickPos, tickSize); tickPos += xTick; } } /** * draws the Y tick marks, * called by drawAxis, which is called by drawGraph(), which is * called by paint(); */ private void paintYTicks(Graphics g) { double tickPos = roundTo(yTick, yMin); double tickSize = (xMax - xMin) / 40; while (convertX(tickSize) <= 0) tickSize++; while (tickPos < yMax) { drawLine(g, -tickSize, tickPos, tickSize, tickPos); tickPos += yTick; } } /** * Draws a line between two points on the plane. The points are * in "graph" coordinates. */ boolean drawLine(Graphics g, double x1, double y1, double x2, double y2) { int ix1 = convertX(x1); int iy1 = convertY(y1); int ix2 = convertX(x2); int iy2 = convertY(y2); boolean result = !(ix1 < 0 || iy1 < 0 || ix1 > getBounds().width || iy1 > getBounds().height || ix2 < 0 || iy2 < 0 || ix2 > getBounds().width || iy2 > getBounds().height); if (result) g.drawLine(ix1, iy1, ix2, iy2); return result; } /** * Draws an inverted rectangle on the graph. Used for drawing the zoom * rectangle when zooming with the mouse. See also InvertRectangle. */ private void drawInvertRect(Graphics g, int x1, int y1, int x2, int y2) { if (invRect == null) invRect = new InvertRectangle(); invRect.drawRect(g, x1, y1, x2-x1, y2-y1); } /** * Converts a graph X value to a component X value */ public int convertX(double x) { double result = (x - xMin) / (xMax - xMin) * widthVar; return (int)result; } /** * Converts from a component X value to a graph X value */ public double deconvertX(int x) { double result = x * (xMax - xMin) / widthVar + xMin; return result; } /** * Converts from a graph Y value to a component Y value */ public int convertY(double y) { double result = heightVar - (y - yMin) / (yMax - yMin) * heightVar; return (int)result; } /** * Converts from a component Y value to a graph Y value */ public double deconvertY(int y) { double result = ((heightVar - y) * (yMax - yMin)) / heightVar + yMin; return result; } /** * internally used rounding function, rounds value to the closest roundX

* roundTo(0.5, 7.624) should return 7.5. * @param roundX the number to round to * @param value the number to round */ double roundTo(double roundX, double value) { return Math.rint(value / roundX) * roundX; } public void mouseClicked(MouseEvent event) { } /** * If the left mouse button is pressed, global variables are used to * store the mouse location for use with a zoom.
* If right mouse button is pressed, the popup menu is * displayed. */ public void mousePressed(MouseEvent event) { if ((event.getModifiers() == Event.META_MASK) || (event.isPopupTrigger())) { // because e.isPopupTrigger() isn't working in 1.1.2 WinAWT pupMenuGraphPlane.show(this, event.getX(), event.getY()); } else { mouseDown = true; xZoomStart = event.getX(); yZoomStart = event.getY(); xZoomCurr = event.getX(); yZoomCurr = event.getY(); } } /** * If a zoom rectangle is being drawn, the graph's axis are changed and * the graph is updated. When zooming, the values are rounded based on * the range and domain of the graph. */ public void mouseReleased(MouseEvent event) { if (!mouseDown) return; Graphics g = getGraphics(); g.setColor(bgColor); drawInvertRect(g, xZoomStart, yZoomStart, xZoomCurr, yZoomCurr); mouseDown = false; if (!(Math.abs(xZoomStart - xZoomCurr) < 10 || Math.abs(yZoomStart - yZoomCurr) < 10)) { double xa = deconvertX(xZoomStart); double xb = deconvertX(xZoomCurr); double ya = deconvertY(yZoomStart); double yb = deconvertY(yZoomCurr); if (xa < xb) { xMin = xa; xMax = xb; } else { xMax = xa; xMin = xb; } if (ya < yb) { yMin = ya; yMax = yb; } else { yMax = ya; yMin = yb; } /** * Rounds based on the range and domain */ double r; double log10; log10 = (Math.log((xMax - xMin)/2)/Math.log(10)); if (log10 >= 0) r = 1; else r = Math.pow(10, Math.rint(log10 - 2)); xMin = roundTo(r, xMin); xMax = roundTo(r, xMax); if (xMin == xMax) xMax = xMax + r; log10 = (Math.log((yMax - yMin)/2)/Math.log(10)); if (log10 >= 0) r = 1; else r = Math.pow(10, Math.rint(log10 - 2)); yMin = roundTo(r, yMin); yMax = roundTo(r, yMax); if (yMin == yMax) yMax = yMax + r; } repaint(); } /** * draws the zoom (invert) rectangle */ public void mouseDragged(MouseEvent event) { if (mouseDown) { Graphics g = getGraphics(); if ((xZoomStart != xZoomCurr) || (yZoomStart != yZoomCurr)) { g.setColor(bgColor); drawInvertRect(g, xZoomStart, yZoomStart, xZoomCurr, yZoomCurr); drawGraph(g); } xZoomCurr = event.getX(); yZoomCurr = event.getY(); g.setColor(zoomColor); drawInvertRect(g, xZoomStart, yZoomStart, xZoomCurr, yZoomCurr); } } /** * converts the position of the mouse to graph coordinates and fires * event to GraphPlaneListeners. If the mouse position on a pixel * which could also be represented by a number more round than the * calculated value, the number is rounded. */ public void mouseMoved(MouseEvent event) { double xPos = deconvertX(event.getX()); double yPos = deconvertY(event.getY()); if (convertX(xPos) == convertX(roundTo(0.01, xPos))) xPos = roundTo(0.01, xPos); if (convertY(yPos) == convertY(roundTo(0.01, yPos))) yPos = roundTo(0.01, yPos); if (convertX(xPos) == convertX(roundTo(0.1, xPos))) xPos = roundTo(0.1, xPos); if (convertY(yPos) == convertY(roundTo(0.1, yPos))) yPos = roundTo(0.1, yPos); if (convertX(xPos) == convertX(Math.rint(xPos))) xPos = Math.rint(xPos); if (convertY(yPos) == convertY(Math.rint(yPos))) yPos = Math.rint(yPos); xPos = shortenDouble(xPos); yPos = shortenDouble(yPos); for (int i =0; i < graphPlaneListenerList.size(); i++) ((GraphPlaneListener)graphPlaneListenerList. elementAt(i)).mousePosMoved(xPos, yPos); } } /** * Adapter used to pass events to appropriate methods in GraphPlane */ class GraphPlaneMouseAdapter extends java.awt.event.MouseAdapter { GraphPlane adaptee; GraphPlaneMouseAdapter(GraphPlane graphPlane) { adaptee = graphPlane; } public void mouseClicked(MouseEvent event) { adaptee.mouseClicked(event); } public void mousePressed(MouseEvent event) { adaptee.mousePressed(event); } public void mouseReleased(MouseEvent event) { adaptee.mouseReleased(event); } } /** * Adapter used to pass events to appropriate methods in GraphPlane */ class GraphPlaneMouseMotionAdapter extends java.awt.event.MouseMotionAdapter { GraphPlane adaptee; GraphPlaneMouseMotionAdapter(GraphPlane graphPlane) { adaptee = graphPlane; } public void mouseDragged(MouseEvent event) { adaptee.mouseDragged(event); } public void mouseMoved(MouseEvent event) { adaptee.mouseMoved(event); } }