Integral Curves
/************************************************************************* * * * This source code file, and compiled classes derived from it, can * * be used and distributed without restriction, including for commercial * * use. (Attribution is not required but is appreciated.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * Geneva, New York 14456, USA * * Email: eck@hws.edu WWW: http://math.hws.edu/eck/ * * * *************************************************************************/ // This applet displays a vector field (f1(x,y),f2(x,y)) and integral curves // for that vector field (although the integral curve feature can be turned off // with an applet param). The drawing of the curves is animated; they are // drawn segment-by-segment. In the default setup, a curve is started when the // user clicks on the canvas. A curve can also be started by entering the // starting x and y coords in a pair of text input boxes and clicking a button. import java.awt.*; import java.awt.event.*; import java.applet.Applet; import java.util.*; import edu.hws.jcm.draw.*; import edu.hws.jcm.data.*; import edu.hws.jcm.functions.*; import edu.hws.jcm.awt.*; public class IntegralCurves extends GenericGraphApplet { private Variable yVar; // The seond variable, usually y. private Function xFunc,yFunc; // The functions that give the components of the vector field private ExpressionInput functionInput2; // For inputting yFunc. private VectorField field; // The vector/direction field private Animator animator; // for incrementally drawing integral curves. private Vector curves = new Vector(); // Holds the integral curves private VariableInput deltaT; // input the deltaT for the curve double dt = 0.1; // The value of delat t in the case where there is no deltaT input box private VariableInput xStart,yStart; // Starting point for curve private Choice methodChoice; // select integration method private Button startCurveButton; // user clicks to start curve from (x,y) in xStart, yStart input boxes private Button clearButton; // clears curves private Color curveColor; // color for integral curves private Draw curveDrawer = new Draw(); // A DrawTemp object that draws one segment of the integral curves. private double[] nextPoint = new double[2]; // Help in computing next point of integral curve. private double[] params = new double[2]; // ditto private static final int RK4 = 0, RK2 = 1, EULER = 2; // constants for integration methos private class Curve { // holds the data for one integral curve double dt; int method; double x,y; // point on the curve double lastX = Double.NaN, lastY; // previous point, so we can draw a line. } private class Draw implements DrawTemp { // For drawing the next segment in each integral curve (as a DrawTemp) public void draw(Graphics g, CoordinateRect coords) { int size = curves.size(); g.setColor(curveColor); for (int i = 0; i < size; i++) { Curve c = (Curve)(curves.elementAt(i)); if (! (Double.isNaN(c.x) || Double.isNaN(c.y) || Double.isNaN(c.lastX) || Double.isNaN(c.lastY)) ) { int x1 = coords.xToPixel(c.lastX); int y1 = coords.yToPixel(c.lastY); int x2 = coords.xToPixel(c.x); int y2 = coords.yToPixel(c.y); g.drawLine(x1,y1,x2,y2); } } } } protected void setUpParser() { // create the "y" variable; also set up some parameter defaults. yVar = new Variable(getParameter("Variable2","y")); parser.add(yVar); super.setUpParser(); // sets up xVar, among other things. parameterDefaults = new Hashtable(); parameterDefaults.put("FunctionLabel", " f1(" + xVar.getName() + "," + yVar.getName() + ") = "); parameterDefaults.put("FunctionLabel2", " f2(" + xVar.getName() + "," + yVar.getName() + ") = "); parameterDefaults.put("Function", " " + yVar.getName() + " - 0.1*" + xVar.getName()); parameterDefaults.put("Function2", " - " + xVar.getName() + " - 0.1*" + yVar.getName()); defaultFrameSize = new int[] { 580, 440 }; } protected void setUpCanvas() { // Override this to add more stuff to the canvas. super.setUpCanvas(); // Do the common setup: Add the axes and // set up the vector field and add it to the canvas if (functionInput != null) { xFunc = functionInput.getFunction(new Variable[] {xVar,yVar}); yFunc = functionInput2.getFunction(new Variable[] {xVar,yVar}); } else { String xFuncDef = getParameter("Function"); String yFuncDef = getParameter("Function2"); Function f = new SimpleFunction( parser.parse(xFuncDef), new Variable[] {xVar,yVar} ); xFunc = new WrapperFunction(f); f = new SimpleFunction( parser.parse(yFuncDef), new Variable[] {xVar,yVar} ); yFunc = new WrapperFunction(f); } String type = (getParameter("VectorStyle", "") + "A").toUpperCase(); int style = 0; switch (type.charAt(0)) { case 'A': style = VectorField.ARROWS; break; case 'L': style = VectorField.LINES; break; case 'S': style = VectorField.SCALED_VECTORS; break; } field = new VectorField(xFunc,yFunc,style); Color color = getColorParam("VectorColor"); if (color != null) field.setColor(color); int space = (style == VectorField.LINES)? 20 : 30; double[] d = getNumericParam("VectorSpacing"); if (d != null && d.length > 0 && d[0] >= 1) space = (int)Math.round(d[0]); field.setPixelSpacing(space); canvas.add(field); // Finally, add the graph to the canvas. curveColor = getColorParam("CurveColor", Color.magenta); // add a mouse listener to the canvas for starting curves. if ("yes".equalsIgnoreCase(getParameter("MouseStartsCurves","yes")) && "yes".equalsIgnoreCase(getParameter("DoCurves","yes"))) canvas.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent evt) { CoordinateRect coords = canvas.getCoordinateRect(); double x = coords.pixelToX(evt.getX()); double y = coords.pixelToY(evt.getY()); if (xStart != null) xStart.setVal(x); if (yStart != null) yStart.setVal(y); startCurve(x,y); } }); } // end setUpCanvas() protected void setUpBottomPanel() { // Override this to make a panel containing controls. This is complicated // because it's possible to turn off a lot of the inputs with applet params. // Check on the value of delta t, which has to be set even if there are no input controls. double[] DT = getNumericParam("DeltaT"); if ( ! (DT == null || DT.length == 0 || DT[0] <= 0) ) dt = DT[0]; boolean doCurves = "yes".equalsIgnoreCase(getParameter("DoCurves","yes")); boolean useInputs = "yes".equalsIgnoreCase(getParameter("UseFunctionInput","yes")); if (!doCurves && !useInputs) // no input controls at all. return; // make the input panel inputPanel = new JCMPanel(); inputPanel.setBackground( getColorParam("PanelBackground", Color.lightGray) ); mainPanel.add(inputPanel,BorderLayout.SOUTH); // Make the function inputs and the compute button, if these are in the configuration. JCMPanel in1 = null, in2 = null; // hold function inputs, if any if (useInputs) { if ( "yes".equalsIgnoreCase(getParameter("UseComputeButton", "yes")) ) { String cname = getParameter("ComputeButtonName", "New Functions"); computeButton = new Button(cname); computeButton.addActionListener(this); } functionInput = new ExpressionInput(getParameter("Function"),parser); in1 = new JCMPanel(); in1.add(functionInput,BorderLayout.CENTER); in1.add(new Label(getParameter("FunctionLabel")), BorderLayout.WEST); functionInput.setOnUserAction(mainController); functionInput2 = new ExpressionInput(getParameter("Function2"),parser); in2 = new JCMPanel(); in2.add(functionInput2,BorderLayout.CENTER); in2.add(new Label(getParameter("FunctionLabel2")), BorderLayout.WEST); functionInput2.setOnUserAction(mainController); } // If we're not doing curves, all we have to do is put the function inputs in the inputPanel if (!doCurves) { Panel p = new JCMPanel(2,1,3); p.add(in1); p.add(in2); inputPanel.add(p, BorderLayout.CENTER); if (computeButton != null) inputPanel.add(computeButton,BorderLayout.EAST); return; } // Now we know that doCurves is true. First, make the animator and clear button animator = new Animator(Animator.STOP_BUTTON); animator.setStopButtonName("Stop Curves"); animator.setOnChange(new Computable() { // animator drives curves public void compute() { extendCurves(); } }); mainController.add(new InputObject() { // curves must stop if main controller is triggered public void checkInput() { curves.setSize(0); animator.stop(); } public void notifyControllerOnChange(Controller c) { } }); clearButton = new Button("Clear"); clearButton.addActionListener(this); // Make a panel to contain the xStart and yStart inputs, if they are in the configuration. Panel bottom = null; if ("yes".equalsIgnoreCase(getParameter("UseStartInputs","yes"))) { xStart = new VariableInput(); xStart.addActionListener(this); yStart = new VariableInput(); yStart.addActionListener(this); bottom = new Panel(); // not a JCMPanel -- I don't want their contents checked automatically startCurveButton = new Button("Start curve at:"); startCurveButton.addActionListener(this); bottom.add(startCurveButton); bottom.add(new Label(xVar.getName() + " =")); bottom.add(xStart); bottom.add(new Label(yVar.getName() + " =")); bottom.add(yStart); } // Now, make a panel to contain the methodChoice and deltaT input if they are in the configuration. // The animator and clear button will be added to this panel if it exists. If not, and if // an xStart/yStart panel exists, then it will be added there. If neither exists, // it goes in its own panel. The variable bottom ends up pointing to a panel that // contains all the curve controls. boolean useChoice = "yes".equalsIgnoreCase(getParameter("UseMethodChoice","yes")); boolean useDelta = "yes".equalsIgnoreCase(getParameter("UseDeltaInput","yes")); if (useChoice || useDelta) { Panel top = new Panel(); // not a JCMPanel! if (useDelta) { top.add(new Label("dt =")); deltaT = new VariableInput(null,""+dt); top.add(deltaT); } if (useChoice) { top.add(new Label("Method:")); methodChoice = new Choice(); methodChoice.add("Runge-Kutta 4"); methodChoice.add("Runge-Kutta 2"); methodChoice.add("Euler"); top.add(methodChoice); } top.add(animator); top.add(clearButton); if (bottom == null) bottom = top; else { Panel p = new Panel(); p.setLayout(new BorderLayout()); p.add(top, BorderLayout.NORTH); p.add(bottom, BorderLayout.CENTER); bottom = p; } } else { if (bottom == null) bottom = new Panel(); bottom.add(animator); bottom.add(clearButton); } // Add the panels "bottom" to the inputPanel, and ruturn // if there are no function inputs. inputPanel.add(bottom, BorderLayout.CENTER); if (in1 == null) return; // Add the function inputs and compute button to the inputPanel Panel in = new JCMPanel(1,2); in.add(in1); in.add(in2); if (computeButton != null) { Panel p = new JCMPanel(); p.add(in,BorderLayout.CENTER); p.add(computeButton,BorderLayout.EAST); in = p; } inputPanel.add(in,BorderLayout.NORTH); } // end setUpBottomPanel() public void actionPerformed(ActionEvent evt) { // React if user presses return in xStart or yStart, or pass evt on to GenericGraphApplet Object src = evt.getSource(); if (src == clearButton) { canvas.clearErrorMessage(); curves.setSize(0); animator.stop(); canvas.compute(); // force recompute of off-screen canvas! } else if (src == xStart || src == yStart || src == startCurveButton) { // Start a curve from x and y values in xStart and yStart canvas.clearErrorMessage(); double x=0, y=0; try { xStart.checkInput(); x = xStart.getVal(); yStart.checkInput(); y = yStart.getVal(); startCurve(x,y); if (deltaT != null) { deltaT.checkInput(); dt = deltaT.getVal(); if (dt <= 0) { deltaT.requestFocus(); throw new JCMError("dt must be positive", deltaT); } } } catch (JCMError e) { curves.setSize(0); animator.stop(); canvas.setErrorMessage(null,"Illegal Data For Curve. " + e.getMessage()); } } else super.actionPerformed(evt); } // end actionPerfromed public void startCurve(double x, double y) { // Start an integral curve at the point (x,y) synchronized (curves) { if (deltaT != null) { try { deltaT.checkInput(); dt = deltaT.getVal(); if (dt <= 0) { deltaT.requestFocus(); throw new JCMError("dt must be positive", deltaT); } } catch (JCMError e) { curves.setSize(0); animator.stop(); canvas.setErrorMessage(null,"Illegal Data For Curve. " + e.getMessage()); return; } } Curve c = new Curve(); c.dt = dt; int method = (methodChoice == null)? RK4 : methodChoice.getSelectedIndex(); c.method = method; c.x = x; c.y = y; curves.addElement(c); animator.start(); } } public void extendCurves() { // Add the next segment to each integral curve. This function // is called repeatedly by the animator. synchronized(curves) { if (canvas == null || canvas.getCoordinateRect() == null) // can happen when frame closes return; while (canvas.getCoordinateRect().getWidth() <= 0) { // need this at startup to make sure that the canvas has appeared on the screen try { Thread.sleep(200); } catch (InterruptedException e) { } } int size = curves.size(); for (int i = 0; i < size; i++) { Curve curve = (Curve)curves.elementAt(i); curve.lastX = curve.x; curve.lastY = curve.y; nextPoint(curve.x, curve.y, curve.dt, curve.method); curve.x = nextPoint[0]; curve.y = nextPoint[1]; } CoordinateRect c = canvas.getCoordinateRect(); double pixelWidthLimit = 100000*c.getPixelWidth(); double pixelHeightLimit = 100000*c.getPixelHeight(); for (int i = size-1; i >= 0; i--) { Curve curve = (Curve)curves.elementAt(i); if (Double.isNaN(curve.x) || Double.isNaN(curve.y) || Math.abs(curve.x) > pixelWidthLimit || Math.abs(curve.y) > pixelWidthLimit) // stop processing this curve curves.removeElementAt(i); } if (curves.size() > 0) canvas.drawTemp(curveDrawer); else { animator.stop(); } } } private void nextPoint(double x, double y, double dt, int method) { // Find next point from (x,y) by applying specified method over time interval dt switch (method) { case EULER: nextEuler(x,y,dt); break; case RK2: nextRK2(x,y,dt); break; case RK4: nextRK4(x,y,dt); break; } } private void nextEuler(double x, double y, double dt) { params[0] = x; params[1] = y; double dx = xFunc.getVal(params); double dy = yFunc.getVal(params); nextPoint[0] = x + dt*dx; nextPoint[1] = y + dt*dy; } private void nextRK2(double x, double y, double dt) { params[0] = x; params[1] = y; double dx1 = xFunc.getVal(params); double dy1 = yFunc.getVal(params); double x2 = x + dt*dx1; double y2 = y + dt*dy1; params[0] = x2; params[1] = y2; double dx2 = xFunc.getVal(params); double dy2 = yFunc.getVal(params); nextPoint[0] = x + 0.5*dt*(dx1+dx2); nextPoint[1] = y + 0.5*dt*(dy1+dy2); } private void nextRK4(double x, double y, double dt) { params[0] = x; params[1] = y; double dx1 = xFunc.getVal(params); double dy1 = yFunc.getVal(params); double x2 = x + 0.5*dt*dx1; double y2 = y + 0.5*dt*dy1; params[0] = x2; params[1] = y2; double dx2 = xFunc.getVal(params); double dy2 = yFunc.getVal(params); double x3 = x + 0.5*dt*dx2; double y3 = y + 0.5*dt*dy2; params[0] = x3; params[1] = y3; double dx3 = xFunc.getVal(params); double dy3 = yFunc.getVal(params); double x4 = x + dt*dx3; double y4 = y + dt*dy3; params[0] = x4; params[1] = y4; double dx4 = xFunc.getVal(params); double dy4 = yFunc.getVal(params); nextPoint[0] = x + (dt / 6) * (dx1 + 2 * dx2 + 2 * dx3 + dx4); nextPoint[1] = y + (dt / 6) * (dy1 + 2 * dy2 + 2 * dy3 + dy4); } protected void doLoadExample(String example) { // This method is called when the user loads an example from the // example menu (if there is one). It overrides an empty method // in GenericGraphApplet. // For the IntegrapCurves applet, the example string should contain // two expression that defines the vector field, separated // by a semicolon. This can optionally // be followed by another semicolon and a list of numbers, separated by spaces and/or commas. // The first four numbers give the x- and y-limits to be used for the // example. If they are not present, then -5,5,-5,5 is used. The next number, if present, // specifies a value for delta t. If there are more numbers, they should come in pairs. // each pair specifies a point where a curve will be started when the // example is loaded. There is a 0.5 second delay between loading and starting the // curves to allow time for the redrawing (although it seems to block the redrawing, at least // on some platforms). if (animator != null) { curves.setSize(0); animator.stop(); } int pos = example.indexOf(";"); if (pos == -1) return; // illegal example -- must have two functions String example2 = example.substring(pos+1); example = example.substring(0,pos); pos = example2.indexOf(";"); double[] limits = { -5,5,-5,5 }; // x- and y-limits to use StringTokenizer toks = null; if (pos > 0) { // Get limits from example2 text. String nums = example2.substring(pos+1); example2 = example2.substring(0,pos); toks = new StringTokenizer(nums, " ,"); if (toks.countTokens() >= 4) { for (int i = 0; i < 4; i++) { try { Double d = new Double(toks.nextToken()); limits[i] = d.doubleValue(); } catch (NumberFormatException e) { } } } if (toks.hasMoreTokens()) { double d = Double.NaN; try { d = (new Double(toks.nextToken())).doubleValue(); } catch (NumberFormatException e) { } if (Double.isNaN(d) || d <= 0 || d > 100) d = 0.1; if (deltaT != null) deltaT.setVal(d); else dt = d; } } // Set up the example data and recompute everything. if (functionInput != null) { // If there is a function input box, put the example text in it. functionInput.setText(example); functionInput2.setText(example2); } else { // If there is no user input, set the function in the graph directly. try { Function f = new SimpleFunction( parser.parse(example), xVar ); ((WrapperFunction)xFunc).setFunction(f); Function g = new SimpleFunction( parser.parse(example2), xVar ); ((WrapperFunction)yFunc).setFunction(g); } catch (ParseError e) { // There should't be parse error's in the Web-page // author's examples! If there are, the function // just won't change. } } CoordinateRect coords = canvas.getCoordinateRect(0); coords.setLimits(limits); coords.setRestoreBuffer(); mainController.compute(); if (animator != null && toks != null) { // get any extra nums from the tokenizer and use them as starting points for curves int ct = 2*(toks.countTokens()/2); if (ct > 0) { synchronized(curves) { for (int i = 0; i < ct; i++) { try { double x = (new Double(toks.nextToken())).doubleValue(); double y = (new Double(toks.nextToken())).doubleValue(); startCurve(x,y); } catch (Exception e) { } } if (curves.size() > 0) { // start the curves going try { Thread.sleep(500); // wait a bit to give the canvas time to start drawing itself. } catch (InterruptedException e) { } } } } } } // end doLoadExample() public void stop() { // stop animator when applet is stopped if (animator != null) { curves.setSize(0); animator.stop(); } super.stop(); } public static void main(String[] a){ javax.swing.JFrame f = new javax.swing.JFrame(); Applet app = new IntegralCurves(); app.init(); f.getContentPane().add (app); f.pack(); f.setSize (new Dimension (500, 500)); f.setVisible(true); } } // end class IntegralCurves