Mega Code Archive

 
Categories / Java / Advanced Graphics
 

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                     jcm1-source.zip( 532 k)