/*************************************************************************
*                                                                        *
*  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();
   }


} // end class IntegralCurves
