Integral Curves : Curve « Advanced Graphics « Java






Integral Curves

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)

Related examples in the same category

1.Spline EditorSpline Editor
2.Draw SplineDraw Spline
3.Animated GraphAnimated Graph
4.Epsilon DeltaEpsilon Delta
5.Families Of GraphsFamilies Of Graphs
6.Trace curveTrace curve
7.Input the function and draw the curveInput the function and draw the curve
8.Scatter PlotScatter Plot
9.Zoom interaction, Text background color, and the effect of transparencyZoom interaction, Text background color, and the effect of transparency