package edu.jsu.leathrum.mathlets.shared; import javax.swing.*; import java.applet.*; import java.awt.event.*; import java.awt.geom.*; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.Point; import java.awt.Cursor; import java.awt.Image; import java.awt.Rectangle; import java.awt.FontMetrics; import java.text.DecimalFormat; public abstract class AGraphPanel extends JPanel implements MouseListener, MouseMotionListener { protected final int HEIGHT = 300; protected final int WIDTH = 300; protected AGraphCanvas canvas; JButton zoomInButton = new JButton("zoomin"); JButton zoomOutButton = new JButton("zoomout"); JButton mouseButton = new JButton("mouse"); JButton windowButton = new JButton("bounds"); public boolean zoomingIn, zoomingOut, mouseTracking; ButtonList blist = new ButtonList(); // initial defaults for CoordsConverter set here public CoordsConverter c = new CoordsConverter(HEIGHT, WIDTH, -10, 10, -10, 10); // defaults for display fields set in display.initwindow() WindowPanel display = new WindowPanel(); // general model for flow of values: // initial defaults set in constructor for CoordsConverter // transferred to display fields in display.initWindow() // this method also sets up the FieldHandlers for parameter handling // after this, need to account for effects of FieldHandlers // so display fields are final arbiters of the values // values transferred to CoordsConverter with every call to redraw() method // do *not* need to transfer values back to display fields when open display Box b1 = new Box(BoxLayout.Y_AXIS); Box b2 = new Box(BoxLayout.X_AXIS); Box b3 = new Box(BoxLayout.Y_AXIS); Box b4 = new Box(BoxLayout.X_AXIS); public AGraphPanel () { zoomingIn = zoomingOut = mouseTracking = false; this.setOpaque(false); } // this method *must* be called from subclass constructor // to set up display using instantiated graph canvas protected void initPanel(AGraphCanvas canv) { canvas = canv; canvas.addMouseListener(this); canvas.addMouseMotionListener(this); b1.add(canvas); b4.add(Box.createHorizontalGlue()); b4.add(zoomInButton); zoomInButton.setOpaque(false); b3.add(b4); b4 = new Box(BoxLayout.X_AXIS); b4.add(Box.createHorizontalGlue()); b4.add(zoomOutButton); zoomOutButton.setOpaque(false); b3.add(b4); b2.add(b3); b2.add(Box.createHorizontalStrut(20)); b3 = new Box(BoxLayout.Y_AXIS); b4 = new Box(BoxLayout.X_AXIS); b4.add(mouseButton); mouseButton.setOpaque(false); b4.add(Box.createHorizontalGlue()); b3.add(b4); b4 = new Box(BoxLayout.X_AXIS); b4.add(windowButton); windowButton.setOpaque(false); b4.add(Box.createHorizontalGlue()); b3.add(b4); b2.add(b3); b1.add(b2); add(b1); blist.registerButtonAction(zoomInButton, new ZoomInAction()); blist.registerButtonAction(zoomOutButton, new ZoomOutAction()); blist.registerButtonAction(mouseButton, new MouseAction()); blist.registerButtonAction(windowButton, new WindowAction()); } class ZoomInAction implements ActionWrapper { public void doThis() { zoomingIn = true; canvas.indicateZoom(); } } class ZoomOutAction implements ActionWrapper { public void doThis() { zoomingOut = true; canvas.indicateZoom(); } } class MouseAction implements ActionWrapper { public void doThis() { mouseTracking = !mouseTracking; } } class WindowAction implements ActionWrapper { public void doThis() { doWindow(); } } // this method *must* be called after HTML parameters are read and // locale set, to specify button text given corrected locale // look at AMathlet.java for abstract method localinit() // call this from inside concrete subclass' localinit() method public void localinit() { zoomInButton.setText(AMathlet.getProperty("zoomin")); zoomOutButton.setText(AMathlet.getProperty("zoomout")); mouseButton.setText(AMathlet.getProperty("mouse")); windowButton.setText(AMathlet.getProperty("window")); // this call to display.initwindow() sets up field values and defaults // using values in CoordsConverter (which were set in constructor) display.initwindow(); } public void reapply() { // this is where field values in display are transferred to CoordsConverter display.applyValues(); // includes call to redraw() } public void redraw() { // this method does *not* change values in CoordsConverter canvas.buildFigure(); canvas.repaint(); } public void setVarLabels(String newxvar, String newyvar) { display.setVarLabels(newxvar, newyvar); } // utility method: several applets use this to determine if mouse click // is near a given point on graph // points must be transformed to pixel coords *first* using c.tr public boolean isNear(Point2D p1, Point2D p2) { return ((Math.abs(p1.getX()-p2.getX())<3) && (Math.abs(p1.getY()-p2.getY())<3)); } public void mouseClicked(MouseEvent e) { if (zoomingIn) { Point2D p = c.trinv.transform(new Point2D.Double(e.getX(), e.getY()),null); double xspan = c.xmax - c.xmin; double yspan = c.ymax - c.ymin; // make changes here to display values -- includes apply to CoordsConverter display.setCoords(p.getX()-xspan/4, p.getX()+xspan/4, p.getY()-yspan/4, p.getY()+yspan/4); // includes call to canvas.redraw() } else if (zoomingOut) { Point2D p = c.trinv.transform(new Point2D.Double(e.getX(), e.getY()),null); double xspan = c.xmax - c.xmin; double yspan = c.ymax - c.ymin; // make changes here to display values -- includes apply to CoordsConverter display.setCoords(p.getX()-xspan, p.getX()+xspan, p.getY()-yspan, p.getY()+yspan); // includes call to canvas.redraw() } else mouseClickedOther(e); zoomingIn = zoomingOut = false; } // don't override this -- instead, override mouseClickedOther() below public void mouseClickedOther(MouseEvent e) { } // here for the purpose of being overridden, see above public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { if (mouseTracking) canvas.clearCoords(); if (zoomingIn || zoomingOut) canvas.showCrosshair(-1, -1); } public void mousePressed(MouseEvent e) { } // frequently overridden public void mouseReleased(MouseEvent e) { } public void mouseDragged(MouseEvent e) { if (mouseTracking) canvas.showCoords(e.getX(), e.getY()); } // frequently overridden, but be sure to call this version first public void mouseMoved(MouseEvent e) { if (mouseTracking) canvas.showCoords(e.getX(), e.getY()); if (zoomingIn || zoomingOut) canvas.showCrosshair(e.getX(), e.getY()); } protected void doWindow() { display.pack(); display.show(); } public class CoordsConverter { public double xmin, xmax, delx, ymin, ymax, dely; int ht, wd; private double xsc, ysc; public AffineTransform tr = new AffineTransform(); public AffineTransform trinv = new AffineTransform(); public GeneralPath axpath = new GeneralPath(); public CoordsConverter(int ht0, int wd0, double x0, double x1, double y0, double y1) { ht = ht0; wd = wd0; xmin = x0; xmax = x1; ymin = y0; ymax = y1; delx = dely = 1.0; setScale(); } private void setScale() { xsc = ((double) wd)/(xmax-xmin); ysc = ((double) ht)/(ymax-ymin); tr.setToIdentity(); tr.scale(xsc,-ysc); tr.translate(-xmin,-ymax); try {trinv = tr.createInverse();} catch (NoninvertibleTransformException e) {trinv.setToIdentity();} buildAxesPath(); } public void setCoords(double x0, double x1, double y0, double y1) { xmin = x0; xmax = x1; ymin = y0; ymax = y1; setScale(); } public void setDeltas(double delxnew, double delynew) { delx = delxnew; dely = delynew; buildAxesPath(); } private void buildAxesPath() { double value = 0.0; double temp = 0.0; Point2D p2; axpath = new GeneralPath(); p2 = tr.transform(new Point2D.Double(0.0,0.0),null); axpath.append(tr.createTransformedShape( new Line2D.Double(xmin,0.0,xmax,0.0)),false); axpath.append(tr.createTransformedShape( new Line2D.Double(0.0,ymin,0.0,ymax)),false); for (value = delx; value < xmax ; value+=delx) { temp = tr.transform(new Point2D.Double(value,0.0),null).getX(); axpath.append(new Line2D.Double(temp, p2.getY()-3, temp, p2.getY()+3),false); } for (value = -delx; value > xmin ; value-=delx) { temp = tr.transform(new Point2D.Double(value,0.0),null).getX(); axpath.append(new Line2D.Double(temp, p2.getY()-3, temp, p2.getY()+3),false); } for (value = dely; value < ymax ; value+=dely) { temp = tr.transform(new Point2D.Double(0.0,value),null).getY(); axpath.append(new Line2D.Double(p2.getX()-3, temp, p2.getX()+3, temp),false); } for (value = -dely; value > ymin ; value-=dely) { temp = tr.transform(new Point2D.Double(0.0,value),null).getY(); axpath.append(new Line2D.Double(p2.getX()-3, temp, p2.getX()+3, temp),false); } } } // end class CoordsConverter, inner class of AGraphPanel protected abstract class AGraphCanvas extends JComponent { private Dimension size; public AGraphCanvas(int initialWidth, int initialHeight) { size = new Dimension(initialWidth, initialHeight); this.setBackground(Color.white); this.setOpaque(false); } public Dimension getPreferredSize() { return getMinimumSize(); } public Dimension getMinimumSize() { return size; } public void drawAxes(Graphics2D g) { g.setPaint(Color.black); g.draw(c.axpath); } public void redraw() { // alternative to repaint() buildFigure(); // to refresh stored Shapes, in cases where that is needed repaint(); } // instantiating class generally includes a Shape object(s) like GeneralPath // this method includes the code to build the Shape from applet data // empty method here, to be overridden if needed protected void buildFigure() { } // some applets need to draw something under the axes, so this is called // before the call to drawAxes() in paintComponent(), and if needed // the applet can override this method to get that in right place protected void drawFirst(Graphics2D g) { } // this class does the actual drawing or filling of the Shape(s) // including choices of colors, using g.setPaint() protected abstract void drawFigure(Graphics2D g); public void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D) g; drawFirst(g2); drawAxes(g2); drawFigure(g2); if (mouseTracking) showCoords(); } FontMetrics fm; DecimalFormat nf; int hz=0, vt=0; int xold=0, yold=0; public void showCoords() { showCoords(xold,yold); } public void showCoords(int x, int y) { Point2D p; String value1, value2; p = c.trinv.transform(new Point2D.Double(x, y),null); if (nf == null) nf = (DecimalFormat) AMathlet.getNumberFormat().clone(); xold=x; yold=y; nf.setMaximumFractionDigits(8); value1 = nf.format(p.getX()); value1=AMathlet.getProperty("xeq")+value1; value2 = nf.format(p.getY()); value2=AMathlet.getProperty("yeq")+value2; showCoords(value1, value2); } // override for different coord systems public void showCoords(String value1, String value2) { Graphics2D g2 = (Graphics2D) this.getGraphics(); g2.setPaint(this.getBackground()); g2.fill(new Rectangle2D.Double(0,0,hz,vt)); FormattedLabel flabel = new FormattedLabel(value1+"
"+value2); Dimension lsize = flabel.getPreferredSize(); flabel.setSize(lsize); // this prevents lousy line breaks flabel.paintComponent(g2.create(0,0,lsize.width,lsize.height)); hz=lsize.width; vt=lsize.height; } // this version just uses two displayed strings, override the other form public void clearCoords() { Graphics2D g2 = (Graphics2D) this.getGraphics(); g2.setPaint(this.getBackground()); g2.fill(new Rectangle2D.Double(0,0,hz,vt)); hz=0; vt=0; } // I had problems with the Cursor.CROSSHAIR_CURSOR predefined cursor type (kept getting // the text cursor instead), and also with setting up my own custom cursors (couldn't get // a transparent background), so I decided to live with the default cursor and draw // crosshairs around it instead -- here is where that code lives private final int XHAIRLEN = 10; // half of the width of the crosshair, in pixels private final int BOXLEN = 3; // half width of gap in middle of crosshair, in pixels private GeneralPath xhair = null; private void createZoomCursor() { xhair = new GeneralPath(); xhair.moveTo(-XHAIRLEN, 0); xhair.lineTo(-BOXLEN, 0); xhair.moveTo(XHAIRLEN, 0); xhair.lineTo(BOXLEN, 0); xhair.moveTo(0, -XHAIRLEN); xhair.lineTo(0, -BOXLEN); } public void indicateZoom() { if (fm == null) fm = this.getFontMetrics(this.getFont()); if (xhair == null) createZoomCursor(); Graphics2D g = (Graphics2D) this.getGraphics(); String msg = AMathlet.getProperty("clickmsg"); int ht = fm.getDescent()+fm.getHeight(); g.setPaint(this.getBackground()); g.fill(new Rectangle2D.Double(1,size.height-ht-1,fm.stringWidth(msg),ht)); g.setPaint(Color.black); g.drawString(msg,1,size.height-fm.getDescent()-1); } private Rectangle oldxhairrect = new Rectangle(); public void showCrosshair(int x, int y) { AffineTransform t = new AffineTransform(); t.setToIdentity(); t.translate(x,y); t.rotate(-Math.PI/8); Graphics2D g = (Graphics2D) this.getGraphics(); paintImmediately(oldxhairrect); oldxhairrect = new Rectangle(x-XHAIRLEN, y-XHAIRLEN, 2*XHAIRLEN, 2*XHAIRLEN); indicateZoom(); BasicStroke d = (BasicStroke) g.getStroke(); g.setStroke(new BasicStroke(1.5f)); g.setPaint(Color.black); g.draw(t.createTransformedShape(xhair)); g.setStroke(d); } } // end class AGraphCanvas, inner class of AGraphPanel private class WindowPanel extends JFrame { DoubleInputSet xminField = new DoubleInputSet("xmin=",12); DoubleInputSet xmaxField = new DoubleInputSet("xmax=",12); DoubleInputSet delxField = new DoubleInputSet("delx=",12); DoubleInputSet yminField = new DoubleInputSet("ymin=",12); DoubleInputSet ymaxField = new DoubleInputSet("ymax=",12); DoubleInputSet delyField = new DoubleInputSet("dely=",12); JButton squareButton = new JButton(""); JButton stdButton = new JButton(""); JButton okButton = new JButton(""); JButton cancelButton = new JButton(""); private String vars[] = {"x", "y"}; Box b0, b1, b2; KeyHandlerSequence kseq = new KeyHandlerSequence(); ButtonList bb = new ButtonList(); public WindowPanel () { this.getContentPane().setBackground(Color.white); b1 = new Box(BoxLayout.Y_AXIS); b2 = new Box(BoxLayout.Y_AXIS); xminField.setDefault(c.xmin); xminField.select(0,0); b0=new Box(BoxLayout.X_AXIS); b0.add(Box.createHorizontalGlue()); b0.add(xminField); b1.add(b0); xmaxField.setDefault(c.xmax); xmaxField.select(0,0); b0=new Box(BoxLayout.X_AXIS); b0.add(Box.createHorizontalGlue()); b0.add(xmaxField); b1.add(b0); delxField.setDefault(c.delx); delxField.select(0,0); b0=new Box(BoxLayout.X_AXIS); b0.add(Box.createHorizontalGlue()); b0.add(delxField); b1.add(b0); yminField.setDefault(c.ymin); yminField.select(0,0); b0=new Box(BoxLayout.X_AXIS); b0.add(Box.createHorizontalGlue()); b0.add(yminField); b1.add(b0); ymaxField.setDefault(c.ymax); ymaxField.select(0,0); b0=new Box(BoxLayout.X_AXIS); b0.add(Box.createHorizontalGlue()); b0.add(ymaxField); b1.add(b0); delyField.setDefault(c.dely); delyField.select(0,0); b0=new Box(BoxLayout.X_AXIS); b0.add(Box.createHorizontalGlue()); b0.add(delyField); b1.add(b0); b2.add(b1); b0=new Box(BoxLayout.X_AXIS); b0.add(squareButton); squareButton.setOpaque(false); b0.add(stdButton); stdButton.setOpaque(false); b0.add(okButton); okButton.setOpaque(false); b0.add(cancelButton); cancelButton.setOpaque(false); b2.add(b0); this.getContentPane().add(b2); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); } class SquareAction implements ActionWrapper { public void doThis() { Point2D p = c.tr.transform(new Point2D.Double(xminField.getValue() +delxField.getValue(), ymaxField.getValue()-delyField.getValue()),null); p = new Point2D.Double(Math.abs(p.getX()),Math.abs(p.getY())); if (p.getX() < p.getY()) { yminField.setValue(p.getY()*yminField.getValue()/p.getX()); ymaxField.setValue(p.getY()*ymaxField.getValue()/p.getX()); } else if (p.getY() < p.getX()) { xminField.setValue(p.getX()*xminField.getValue()/p.getY()); xmaxField.setValue(p.getX()*xmaxField.getValue()/p.getY()); } // again here, modifications made to field values, then applied // to the CoordsConverter applyValues(); } } class StdAction implements ActionWrapper { public void doThis() { setFieldsToDefaults(); applyValues(); } } class OkAction implements ActionWrapper { public void doThis() { applyValues(); dispose(); } } class CancelAction implements ActionWrapper { public void doThis() { dispose(); } } class KeyAction implements ActionWrapper { public void doThis() { applyValues(); } } // initwindow() *must* be called before displaying window to get locale info public void initwindow() { xminField.setName(AMathlet.getProperty(vars[0]+"mineq")); xmaxField.setName(AMathlet.getProperty(vars[0]+"maxeq")); delxField.setName(AMathlet.getProperty("del"+vars[0]+"eq")); yminField.setName(AMathlet.getProperty(vars[1]+"mineq")); ymaxField.setName(AMathlet.getProperty(vars[1]+"maxeq")); delyField.setName(AMathlet.getProperty("del"+vars[1]+"eq")); KeyAction kp = new KeyAction(); kseq.registerKeyHandler(xminField, kp); kseq.registerKeyHandler(xmaxField, kp); kseq.registerKeyHandler(delxField, kp); kseq.registerKeyHandler(yminField, kp); kseq.registerKeyHandler(ymaxField, kp); kseq.registerKeyHandler(delyField, kp); squareButton.setText(AMathlet.getProperty("square")); stdButton.setText(AMathlet.getProperty("std")); okButton.setText(AMathlet.getProperty("ok")); cancelButton.setText(AMathlet.getProperty("cancel")); bb.registerButtonAction(squareButton, new SquareAction()); bb.registerButtonAction(stdButton, new StdAction()); bb.registerButtonAction(okButton, new OkAction()); bb.registerButtonAction(cancelButton, new CancelAction()); AMathlet.setHandler(vars[0]+"min",xminField); AMathlet.setHandler(vars[0]+"max",xmaxField); AMathlet.setHandler("del"+vars[0],delxField); AMathlet.setHandler(vars[1]+"min",yminField); AMathlet.setHandler(vars[1]+"max",ymaxField); AMathlet.setHandler("del"+vars[1],delyField); // default values for fields set *from* coordsConverter xminField.setDefault(c.xmin); xmaxField.setDefault(c.xmax); delxField.setDefault(c.delx); yminField.setDefault(c.ymin); ymaxField.setDefault(c.ymax); delyField.setDefault(c.dely); setFieldsToDefaults(); } private void setFieldsToDefaults() { xminField.setToDefault(); xmaxField.setToDefault(); delxField.setToDefault(); yminField.setToDefault(); ymaxField.setToDefault(); delyField.setToDefault(); } public void setCoords(double newxmin, double newxmax, double newymin, double newymax) { // when new values need to be set (as in zoom operation), // set them here first, then apply to CoordsConverter xminField.setValue(newxmin); xmaxField.setValue(newxmax); yminField.setValue(newymin); ymaxField.setValue(newymax); applyValues(); } // this public method transfers values from fields to coordsConverter public void applyValues() { // includes call to redraw() c.setCoords(xminField.getValue(), xmaxField.getValue(), yminField.getValue(), ymaxField.getValue()); c.setDeltas(delxField.getValue(), delyField.getValue()); redraw(); } // note: these are not the full labels, or even full names of properies // but stubs used to build property names public void setVarLabels(String newxvar, String newyvar) { vars[0] = newxvar; vars[1] = newyvar; } } // end class WindowPanel, inner class of AGraphPanel } // end class AGraphPanel