/*  File: Simulation.java
    Contains basic classes for the physics lab simulation */
/*
    Part of the www.MyPhysicsLab.com physics simulation applet.
    Copyright (c) 2001  Erik Neumann

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

    Contact Erik Neumann at erikn@MyPhysicsLab.com or
    610 N. 65th St. Seattle WA 98103

*/

import java.util.Vector;
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.text.DecimalFormat;
import java.text.NumberFormat;

/////////////////////////////////////////////////////////////////////////////
// CoordMap class
/* Provides the coordinate mapping between screen and simulation world.
  Calculates the scaling and origin coordinates.

  QUICK GUIDE:
  new CoordMap(int y_dir, double x1, double x2, double y1, double y2, int align_x, int align_y)
  x1,x2,y1,y2 give the simulation coords of simulation area's L,R,T,B
  This is different from the screen coords!!!  We assure that the rect x1,x2,y1,y2
  fits in the screen area, but there is usually some excess area.
  So you can find the actual screen boundaries in simulation coords, but it is
  better to stick to using the simulation area -- because the screen area can change
  due to resizing of the window.  After a resize, we are guaranteed to have the same
  simulation area showing in the window.

  If you want to use the entire screen area, try using the expand() method to
  grow the simulation area.  Then you can use the whole screen area, but if there is
  any later resizing of the window your simulation area won't change.

  y_dir is either CoordMap.INCREASE_UP or CoordMap.INCREASE_DOWN
  Note that always y1<y2, so
    INCREASE_DOWN:  y1 = top,    y2 = bottom
    INCREASE_UP:    y1 = bottom, y2 = top

  align_x and align_y determine how the map is shifted when the screen area is too wide or too
  tall. That is, x1,x2,y1,y2 define a rectangle with a certain aspect ratio. The screen will
  usually have a different aspect ratio, so we have to fit the x1,x2,y1,y2 rectangle into the
  screen. The question is whether the x1,x2,y1,y2 rectangle is aligned to the left, right or
  middle.  This is specified for horizontal and vertical separately. The extra area is still
  available to the app (but see comments above) its just a question of where the origin is placed.

  If setFillScreen(true) then the aspect ratio is not maintained, and the given
  x1,x2,y1,y2 will match the screen boundaries regardless of aspect ratio.

  TO DO:
  Add some exceptions for divide by zero errors in recalc().  Eg. width=0 is a problem.
  Protect the class by hiding all the variables and only allow method access.

*/

class CoordMap
{
  // class constants for origin location
  public static final int ALIGN_MIDDLE = 0;
  public static final int ALIGN_LEFT = 1;
  public static final int ALIGN_RIGHT = 2;
  public static final int ALIGN_UPPER = 3;
  public static final int ALIGN_LOWER = 4;
  public static final int INCREASE_UP = -1;
  public static final int INCREASE_DOWN = 1;

  private int y_direction = INCREASE_DOWN;
  private int origin_x = 0;  /* CALCULATED origin in screen coords */
  private int origin_y = 0;  /* CALCULATED origin in screen coords */
  public int screen_left = 0;  /* in screen coords (pixels) */
  public int screen_top = 0;  /* in screen coords (pixels) */
  public int screen_width = 0;  /* in screen coords (pixels) */
  public int screen_height = 0;  /* in screen coords (pixels) */
  // "box" variables give simulation area in screen coords
  private int boxLeft,boxRight,boxTop,boxBottom,boxWidth,boxHeight;

  private double pixel_per_unit_x = 100;  /* CALCULATED */
  private double pixel_per_unit_y = 100;  /* CALCULATED */
  private boolean fill_screen = false;
  public double sim_x1;  /* in simulation coords, left boundary of simulation area */
  public double sim_x2;  /* in simulation coords, right boundary of simulation area */
  public double sim_y1;  /* simulation area top (INCREASE_DOWN) or bottom (INCRESE_UP) */
  public double sim_y2;  /* simulation area bottom (INCRESE_UP) or top (INCREASE_DOWN) */
  public double sim_width = 0;  /* = sim_x2 - sim_x1 */
  public double sim_height = 0;  /* = sim_y2 - sim_y1 */
  // sim coords corresponding to left,right,top,bottom of screen  DO NOT USE!!!
  /* note: It is better to use the 'box' methods than sim_left, sim_top, etc.
     because when the window is resized, the screen area changes, but the 'box' stays
     the same. */
  public double sim_left, sim_top, sim_right, sim_bottom;  /* CALCULATED -- DO NOT USE */
  private int align_x = ALIGN_LEFT;
  private int align_y = ALIGN_UPPER;

  public CoordMap(int y_dir, double x1, double x2, double y1, double y2,
                  int align_x, int align_y) {
    y_direction = y_dir;
    this.align_x = align_x;
    this.align_y = align_y;
    setRange(x1, x2, y1, y2);
  }


  public void pvars(String s)  {
    System.out.println("------- map vars: "+s+" -------");
    System.out.println("sim x1="+sim_x1+",x2="+sim_x2+",y1="+sim_y1+",y2="+sim_y2);
    System.out.println("screen left="+screen_left+",top="+screen_top+",width="+screen_width+",height="+screen_height);
    System.out.println("origin x="+ origin_x+",y="+origin_y);
    System.out.println("sim left="+sim_left+",top="+sim_top+",right="+sim_right+",bottom="+sim_bottom+")");
    System.out.println("simToScreen left="+simToScreenX(sim_left)+
      ", top="+simToScreenY(sim_top)+
      ", right="+simToScreenX(sim_right)+
      ", bottom="+ simToScreenY(sim_bottom));
    System.out.println("pixel_per_unit_x="+pixel_per_unit_x+" sim width="+sim_width+", sim_height="+sim_height+")");
    System.out.println("align x="+align_x+", y="+align_y+")");
  }

  /*
  input:  must have following set:
    screen_width, screen_height, screen_left, screen_top,
    sim_width, sim_height, sim_x1, sim_y1
    y_direction, fill_screen

    additionally, if (fill_screen==false) must have:
    align_x, align_y
  */
  private void recalc() {
    if (fill_screen) {
      /* fill the screen according to chosen sim values
         calculate resulting pixel_per_unit for x & y
         calculate resulting screen coord origin location
         offsets should be zero
      */
      pixel_per_unit_x = (double)screen_width/sim_width;
      pixel_per_unit_y = (double)screen_height/sim_height;
      origin_x = screen_left - (int)(sim_x1 * pixel_per_unit_x +0.4999);
      if (y_direction == INCREASE_DOWN)
        origin_y = screen_top
              - (int)(sim_y1 * pixel_per_unit_y +0.4999);
      else
        origin_y = screen_top + screen_height
              + (int)(sim_y1 * pixel_per_unit_y +0.4999);
    } else {
      if ((sim_width == 0) || (sim_height == 0))
        return;
      if ((screen_width <= 0) || (screen_height <= 0))
        return;
      int ideal_height = (int)((double)screen_width*sim_height/sim_width);
      int ideal_width;
      int offset_x, offset_y;
      /* how to figure out location of origin:
        Imagine the rectangle (within screen) that holds the simulation, so it has width = sim_width
        and height = sim_height.  Now impose a temporary coord system over this with upperleft being
        0,0, and lowerright being 1,1. The origin location is specified in these temporary coords.
        We shift the entire rectangle according to requested alignment.  And then finally we can
        determine the screen coords of the origin.
      */
      if (screen_height < ideal_height)  {// height is limiting factor
        pixel_per_unit_y = pixel_per_unit_x = (double)screen_height/sim_height;
        offset_y = 0;
        ideal_width = (int)(sim_width*pixel_per_unit_x);
        switch (align_x) {
          case ALIGN_LEFT:  offset_x = 0; break;
          case ALIGN_RIGHT: offset_x = screen_width - ideal_width; break;
          case ALIGN_MIDDLE: offset_x = (screen_width - ideal_width)/2; break;
          default: offset_x = 0; break;
        }
      } else  {// width is limiting factor
        pixel_per_unit_y = pixel_per_unit_x = (double)screen_width/sim_width;
        offset_x = 0;
        ideal_height = (int)(sim_height*pixel_per_unit_y);
        switch (align_y) {
          case ALIGN_UPPER:  offset_y = 0; break;
          case ALIGN_MIDDLE: offset_y = (screen_height - ideal_height)/2; break;
          case ALIGN_LOWER:  offset_y = screen_height - ideal_height; break;
          default: offset_y = 0; break;
        }
      }

      origin_x = screen_left + offset_x - (int)(sim_x1*pixel_per_unit_x);
      if (y_direction == INCREASE_DOWN)
        origin_y = screen_top + offset_y - (int)(sim_y1*pixel_per_unit_y);
      else
        origin_y = screen_top + screen_height - offset_y + (int)(sim_y1*pixel_per_unit_y);

    }
    sim_left = screenToSimX(screen_left);
    sim_right = screenToSimX(screen_left + screen_width);
    sim_top = screenToSimY(screen_top);
    sim_bottom = screenToSimY(screen_top + screen_height);
    boxLeft = simToScreenX(sim_x1);
    boxRight = simToScreenX(sim_x2);
    boxTop = simToScreenY((y_direction == INCREASE_UP) ? sim_y2 : sim_y1);
    boxBottom = simToScreenY((y_direction == INCREASE_UP) ? sim_y1 : sim_y2);
    boxWidth = boxRight - boxLeft;
    boxHeight = boxBottom - boxTop;
  }

  //  If setFillScreen(true) then the aspect ratio is not maintained, and the given
  //  sim boundaries will match the screen boundaries regardless of aspect ratio.
  public void setFillScreen(boolean f)  {
    fill_screen = f;
    recalc();
  }

  // Expand the simulation boundaries to take up the entire screen
  /* Why is this useful?  When starting up, we ask for a minimum simulation boundary
     area, for example a square boundary of (-5,-5,5,5).  The recalc() procedure then figures out how
     to fit that simulation area into the current screen, which usually is rectangular with
     the width not equal to the height.  Suppose that the width is greater than the height.
     Then expand() changes the simulation area to match the maximum available
     screen area.  In our example, it might change to (-6.3, -5, 6.3, 5) where the horizontal
     boundaries got extended.
  */
  public void expand() {
    sim_x1 = screenToSimX(screen_left);
    sim_x2 = screenToSimX(screen_left + screen_width);
    sim_width = sim_x2 - sim_x1;
    sim_y1 = screenToSimY(screen_top);
    sim_y2 = screenToSimY(screen_top + screen_height);
    sim_height = sim_y2 - sim_y1;
    if (y_direction == INCREASE_UP) { // swap y1 and y2 if increase up
      double d = sim_y1;
      sim_y1 = sim_y2;
      sim_y2 = d;
      sim_height = sim_y2 - sim_y1;
    }
    recalc();
  }

  public void setRange(double xlo, double xhi, double ylo, double yhi)  {
    //System.out.println("map.setRange("+xlo+","+xhi+","+ylo+","+yhi+")");
    sim_x1 = xlo;
    sim_x2 = xhi;
    sim_y1 = ylo;
    sim_y2 = yhi;
    sim_width = xhi - xlo;
    sim_height = yhi - ylo;
    recalc();
    //pvars("setRange");
  }

  public void setScreen(int left, int top, int width, int height)  {
    //System.out.println("map.setScreen "+left+" "+top+" "+width+" "+height);
    if ((width>0) && (height>0)) {
      screen_top = top;
      screen_left = left;
      screen_width = width;
      screen_height = height;
      recalc();
      //pvars("setScreen");
    }
  }

  public int simToScreenScaleX(double x)  {
    /* does only scaling in x direction, no offsetting */
    return (int)(x*pixel_per_unit_x+0.5);
  }

  public int simToScreenX(double x)  {
    /* Returns the screen coords of x, given the various globals. */
    return origin_x + (int)(x*pixel_per_unit_x+0.5);
  }

  public int simToScreenY(double y)  {
    /* Returns the screen coords of y, given the various globals. */
    return origin_y + y_direction*(int)(y*pixel_per_unit_y+0.5);
  }

  public double screenToSimX(int scr_x)  {
    /* scr_x is in screen coords, returns simulation coords */
    return (double)(scr_x - origin_x)/pixel_per_unit_x;
  }

  public double screenToSimY(int scr_y) {
    /* scr_y is in screen coords, returns simulation coords */
    return y_direction*(double)(scr_y - origin_y)/pixel_per_unit_y;
  }

  /*  'Box' is the simulation area defined by sim_x1, sim_x2, sim_y1, sim_y2.
      The following methods return the screen coords of the box.
  */
  public int leftBox() {
    return boxLeft;
  }

  public int rightBox() {
    return boxRight;
  }

  public int topBox() {
    return boxTop;
  }

  public int bottomBox() {
    return boxBottom;
  }

  public int widthBox() {
    return boxWidth;
  }

  public int heightBox() {
    return boxHeight;
  }

  public boolean intersectRect(Rectangle r) {
    int x1, y1, x2, y2;
    x1 = r.x;
    y1 = r.y;
    x2 = x1 + r.width;
    y2 = y1 + r.height;
    int sx1 = screen_left;
    int sy1 = screen_top;
    int sx2 = screen_left + screen_width;
    int sy2 = screen_top + screen_height;
    if (sx1 >= x2)
      return false;
    if (x1 >= sx2)
      return false;
    if (sy1 >= y2)
      return false;
    if (y1 >= sy2)
      return false;
    //System.out.println(x1+" "+y1+" "+x2+" "+y2+" "+sx1+" "+sy1+" "+sx2+" "+sy2);
    return true;
  }
}

/////////////////////////////////////////////////////////////////////////////
// CElement class

class CElement
{
// class constants
public static final int MODE_RECT = 1;
public static final int MODE_CIRCLE = 2;
public static final int MODE_SPRING = 3;
public static final int MODE_LINE = 4;
public static final int MODE_CIRCLE_FILLED = 5;

/* About bounds (m_X1, m_Y1, m_X2, m_Y2)
  It should always be true that m_X1 < m_X2 and m_Y1 < m_Y2
  Therefore m_X1, m_Y1 is either top-left or bottom-left depending on whether the
  CoordMap has INCREASE_UP or INCREASE_DOWN set.
  */
public double m_X1, m_Y1, m_X2, m_Y2;  // bounds (simulation coords)

/* ix & ivx are for collision checking */
public int ix = -1;   //index of position in array of vars
public int ivx = -1;  //index of velocity in array of vars
public int    m_DrawMode; // drawing mode (rect, circle, ...)
public double m_Mass;   // the mass in kilograms
public boolean m_Draggable = false;  // whether it is mouse draggable
public Color m_Color;

public CElement ()
{
  m_X1 = m_Y1 = m_X2 = m_Y2 = 0;
  m_DrawMode = 0;
  m_Mass = 0;
  m_Color = Color.black;
}

public CElement (double X1, double Y1, double X2, double Y2)
{
  m_X1 = X1;
  m_Y1 = Y1;
  m_X2 = X2;
  m_Y2 = Y2;
  m_DrawMode = 0;
  m_Mass = 0;
  m_Color = Color.black;
}

/* Distance is meant to give the distance squared from the "center" of the object to the given x, y point */
public double distanceSquared(double x, double y)
{
  double dx = (m_X2 + m_X1)/2 - x;
  double dy = (m_Y2 + m_Y1)/2 - y;
  return dx*dx+dy*dy;
}

public boolean hitCheck(double x, double y)
{
  if (m_Draggable && ((x>=m_X1) && (x<=m_X2) && (y>=m_Y1) && (y<=m_Y2)))
    return true;
  else
    return false;
}

public void setX1(double p)
{
  m_X1 = p;
}

public void setX2(double p)
{
  m_X2 = p;
}

public void setY1(double p)
{
  m_Y1 = p;
}

public void setY2(double p)
{
  m_Y2 = p;
}

public void draw (Graphics g, CoordMap map)
{
}

public double getCenterX()
{
  return (m_X1 + m_X2)/2;
}

public double getCenterY()
{
  return (m_Y1 + m_Y2)/2;
}


}

/////////////////////////////////////////////////////////////////////////////
// CMass class

class CMass extends CElement
{
  public double m_Height;
  public double m_Width;
  public double m_Damping = 0;  // slowing from friction, viscosity, etc.
  public double m_Elasticity = 1;  // bounciness, from 0 (blob) to 1 (superball)

/*
  public CMass()
  {
    super(0, 0, 1, 1);
    m_Height = 1;
    m_Width = 1;
  }
*/

  public CMass (double X1, double Y1, double width, double height, int drawMode)
  {
    super(X1, Y1, X1+width, Y1+height);
    m_Height = height;
    m_Width = width;
    m_Mass = 1;
    m_DrawMode = drawMode;
    m_Draggable = true;
    m_Color = Color.red;

  }

  public void setX1(double Xpos)
  {
    m_X1 = Xpos;
    m_X2 = Xpos + m_Width;
  }

  public void setY1(double Ypos)
  {
    m_Y1 = Ypos;
    m_Y2 = Ypos + m_Height;
  }

  public void setCenterX(double Xpos)
  {
    double w = m_Width/2;
    m_X1 = Xpos - w;
    m_X2 = Xpos + w;
  }

  public void setCenterY(double Ypos)
  {
    double w = m_Width/2;
    m_Y1 = Ypos - w;
    m_Y2 = Ypos + w;
  }

  public void draw (Graphics g, CoordMap map)
  {
    int x1, y1, x2, y2;
    x1 = map.simToScreenX(m_X1);
    y1 = map.simToScreenY(m_Y1);
    x2 = map.simToScreenX(m_X2);
    y2 = map.simToScreenY(m_Y2);
    if (y2 < y1)  // adjust for INCREASE_UP mode
    {
      int d = y2;
      y2 = y1;
      y1 = d;
    }

    g.setColor(m_Color);
    switch (m_DrawMode)
    {
    case CElement.MODE_RECT:
      g.drawRect(x1, y1, x2-x1, y2-y1);
      break;
    case CElement.MODE_CIRCLE:
      g.drawOval(x1, y1, x2-x1, y2-y1);
      break;
    case CElement.MODE_CIRCLE_FILLED:
      g.fillOval(x1, y1, x2-x1, y2-y1);
      break;
    //default:
    //  throw "unknown draw mode";
    }

  }
}

/////////////////////////////////////////////////////////////////////////////
// CArc class

class CArc extends CElement
{
  // X1, Y1 are the center of the arc
  // X2, Y2 will be calculated if necessary?
  public double m_Radius;
  public double m_Angle;      // degrees
  public double m_StartAngle;  // where the curve starts from; 0 = "east"
  public double m_HeadLength = 0.2;  // length of arrowhead

  public CArc (double X1, double Y1, double r, double angle0, double angle)
  {
    super(X1, Y1, 0, 0);
    m_Radius = r;
    m_StartAngle = angle0;
    m_Angle = angle;
    m_Draggable = false;

  }

  public void setX1(double Xpos)
  {
    m_X1 = Xpos;
  }

  public void setY1(double Ypos)
  {
    m_Y1 = Ypos;
  }

  public void draw (Graphics g, CoordMap map)
  {
    int x1, y1, x2, y2, r;
    x1 = map.simToScreenX(m_X1);
    y1 = map.simToScreenY(m_Y1);
    r = map.simToScreenScaleX(m_Radius); // assumes x & y are scaled same!

    g.setColor(Color.black);
    /* parameters to drawArc are:
      x, y (top-left corner), width, height (bounding rect)
      startAngle (degrees, 0 = 3 o'clock),
      arcAngle (degrees relative to startAngle) */
    int ang = (int)(m_Angle+0.5);
    // bug:  seems that drawArc sometimes draws a circle for
    //  angle = +1 or -1 degrees!!!
    // BUG:  this bug is still happening for small radius in driven
    // pendulum (eg. radius 0.2).  Seems to be related to small radius and
    // small angle at same time.  To really check out this bug
    // would need to set up a test program where I can interactively
    // give static inputs to g.drawArc, and try to determine where the
    // bug is happening exactly.
    if (Math.abs(ang)>1)
       g.drawArc(x1-r, y1-r, 2*r, 2*r, (int)m_StartAngle, ang);

    if ((ang != 0) && (r > 0))
    {
      // arrowhead
      // find tip of arrowhead
      double x,y;
      double a0, a1, a;  // startangle & angle in radians
      a0 = Math.PI*(double)m_StartAngle/(double)180;
      a1 = Math.PI*(double)m_Angle/(double)180;
      a = -(a0 + a1);
      x = m_X1 + m_Radius*Math.cos(a);
      y = m_Y1 + m_Radius*Math.sin(a);

      double h = Math.min(m_HeadLength, 0.5*m_Radius);
      if (a1 > 0)
        h = -h;

      // find endpoint of first arrowhead, and draw it
      double xp, yp;
      xp = x + h*Math.cos(Math.PI/2 - a - Math.PI/6);
      yp = y - h*Math.sin(Math.PI/2 - a - Math.PI/6);
      x1 = map.simToScreenX(x);
      y1 = map.simToScreenY(y);
      x2 = map.simToScreenX(xp);
      y2 = map.simToScreenY(yp);
      g.drawLine(x1, y1, x2, y2);

      // find endpoint of 2nd arrowhead, and draw it
      xp = x + h*Math.cos(Math.PI/2 - a + Math.PI/6);
      yp = y - h*Math.sin(Math.PI/2 - a + Math.PI/6);
      x2 = map.simToScreenX(xp);
      y2 = map.simToScreenY(yp);
      g.drawLine(x1, y1, x2, y2);
    }
  }
}

/////////////////////////////////////////////////////////////////////////////
// CSpring class

class CSpring extends CElement
{

public double m_RestLength = 1.0; // the unstretched (slack) length in meters
public double m_Thickness = 0.5;  // the thickness (width) of coil in meters
public double m_SpringConst = 1.0;  // the spring constant in Newtons/meter
public Color m_Color2 = null;  // expanded color


public CSpring (double X1, double Y1, double restLen, double thick)
{
  // X1, and Y1 are the attachment points of the left part of spring
  // default is horizontal spring at rest
  super(X1, Y1, X1+restLen, Y1);
  m_Thickness = thick;
  m_Mass = 0;
  m_SpringConst = 1;
  m_RestLength = restLen;
  m_DrawMode = CElement.MODE_SPRING;  // default
  m_Color = Color.red;  // compressed
  m_Color2 = Color.green;
}

public void draw (Graphics g, CoordMap map)
{
  if (m_Color2 == null)
    m_Color2 = m_Color.brighter();
  int cycles = 3;
  double x1=m_X1;
  double x2=m_X2;
  double y1=m_Y1;
  double y2=m_Y2;
  double cos_theta, sin_theta;
  // find angle of rotation
  // note:  if x2==x1 then slope is infinite, which is a valid double
  {
    double theta=Math.atan((y2-y1)/(x2-x1));
    if (x2<x1)  // need to add Pi to theta
      theta+=Math.acos(-1);
    cos_theta=Math.cos(theta);
    sin_theta=Math.sin(theta);
  }
  double len=Math.sqrt((y2-y1)*(y2-y1)+(x2-x1)*(x2-x1));
  if (len == 0)
    return;  // draw nothing if length is zero
  if ((m_DrawMode == CElement.MODE_SPRING) && (m_SpringConst == 0))
    return;  // draw nothing if spring constant is zero
  double h = m_Thickness;
  double w = len/16;
  double x, y;
  int xscr0, yscr0;

  // set up points based on a horizontal orientation of length len,
  // and then rotate about the point x1, y1
  // regard the origin as being at x1,y1

  // move the pen to the starting point
  // first point is at (0,0)
  xscr0 = map.simToScreenX(x1);
  yscr0 = map.simToScreenY(y1);

  if (m_DrawMode == CElement.MODE_LINE)
  {
    // single horizontal line
    x2=len;
    y2=0;
    x = x1+cos_theta*x2-sin_theta*y2;
    y = y1+sin_theta*x2+cos_theta*y2;
    int xscr = map.simToScreenX(x);
    int yscr = map.simToScreenY(y);

    g.setColor(m_Color);
    g.drawLine(xscr0, yscr0, xscr, yscr);

  }
  else if (m_DrawMode == CElement.MODE_SPRING)
  {
    /* make a polygon object */
    int nPoints = 5+2*cycles;
    int i=0;
    // NOTE: better to allocate these arrays permanently for the object, avoid GC
    int[] xPoints = new int[nPoints];
    int[] yPoints = new int[nPoints];
    xPoints[i] = xscr0;
    yPoints[i++] = yscr0;

    /* draw pen red when compressed, green when stretched */
    if (len < m_RestLength)
      g.setColor(m_Color);  // compressed
    else
      g.setColor(m_Color2);

    // second point is at (w,0), rotated by theta
    x2=w;
    y2=0;
    x = x1+cos_theta*x2-sin_theta*y2;
    y = y1+sin_theta*x2+cos_theta*y2;
    xPoints[i] = map.simToScreenX(x);
    yPoints[i++] = map.simToScreenY(y);

    // first ramp up
    x2=2*w;
    y2=-h/2;
    x = x1+cos_theta*x2-sin_theta*y2;
    y = y1+sin_theta*x2+cos_theta*y2;
    xPoints[i] = map.simToScreenX(x);
    yPoints[i++] = map.simToScreenY(y);

    // 3 up and down cycles
    int j;
    for (j=1; j<=cycles; j++)
    {
      x2=4*j*w;
      y2=h/2;
      x = x1+cos_theta*x2-sin_theta*y2;
      y = y1+sin_theta*x2+cos_theta*y2;
      xPoints[i] = map.simToScreenX(x);
      yPoints[i++] = map.simToScreenY(y);

      x2=(4*j+2)*w;
      y2=-h/2;
      x = x1+cos_theta*x2-sin_theta*y2;
      y = y1+sin_theta*x2+cos_theta*y2;
      xPoints[i] = map.simToScreenX(x);
      yPoints[i++] = map.simToScreenY(y);
    }

    // last ramp down
    x2=(3+cycles*4)*w;
    y2=0;
    x = x1+cos_theta*x2-sin_theta*y2;
    y = y1+sin_theta*x2+cos_theta*y2;
    xPoints[i] = map.simToScreenX(x);
    yPoints[i++] = map.simToScreenY(y);

    // final horizontal line
    x2=len;
    y2=0;
    x = x1+cos_theta*x2-sin_theta*y2;
    y = y1+sin_theta*x2+cos_theta*y2;
    xPoints[i] = map.simToScreenX(x);
    yPoints[i++] = map.simToScreenY(y);

    g.drawPolyline(xPoints, yPoints, nPoints);
  }
}

}

/////////////////////////////////////////////////////////////////////////////
// CWall class

class CWall extends CElement
{
public double m_Angle;   // orientation angle in degrees... 0=vertical facing right

public CWall (double X1, double Y1, double X2, double Y2, double angle)
{
  super(X1, Y1, X2, Y2);
  m_Angle = angle;
  m_Mass = Math.exp(30);  // essentially infinite
}

public void draw (Graphics g, CoordMap map)
{
  int i;
  int x1, x2, y1, y2;
  x1 = map.simToScreenX(m_X1);
  y1 = map.simToScreenY(m_Y1);
  x2 = map.simToScreenX(m_X2);
  y2 = map.simToScreenY(m_Y2);
  g.setColor(Color.black);

  if (m_Angle==0)
  {
    g.drawLine(x2, y1, x2, y2);

    // 5 diagonal hatch marks
    int hatchHeight = (y2 - y1)/5;
    for (i=1; i<=5; i++)
      g.drawLine(x1, y1+(i*hatchHeight), x2, y1+((i-1)*hatchHeight));
  }
  else if (m_Angle==-90)
  {
    g.drawLine(x1, y1, x1, y2);

    // 5 diagonal hatch marks
    int hatchHeight = (y2 - y1)/5;
    for (i=1; i<=5; i++)
      g.drawLine(x1, y1+((i-1)*hatchHeight), x2, y1+(i*hatchHeight));
  }
}


}
/////////////////////////////////////////////////////////////////////////////
// CText class

class CText extends CElement
{
  public String m_text;
  private double m_num = 0;
  private boolean show_num = false;
  private Font myFont = null;
  private FontMetrics myFM = null;
  private int ascent = 20;
  private int descent = 10;
  private int leading = 5;
  private NumberFormat nf = null;
  public int line_height = 10; // WARNING: only accurate after draw()


public CText (double X1, double Y1, String t)
{
  super(X1, Y1, X1, Y1);
  m_text = t;
}

public void setNumber(double n)
{
  show_num = true;
  m_num = n;
}

public void setFont(Graphics g)
{
  if (myFont != null)
    return;
  myFont = new Font("Serif", Font.PLAIN, 14);
  myFM = g.getFontMetrics(myFont);
  ascent = myFM.getAscent();
  descent = myFM.getDescent();
  leading = myFM.getLeading();
  nf = NumberFormat.getNumberInstance();
  nf.setMaximumFractionDigits(5);
  if (line_height != ascent+descent+leading)
  {
    line_height = ascent+descent+leading;
  }
}

public void draw (Graphics g, CoordMap map)
{
  int x1, y1;
  x1 = map.simToScreenX(m_X1);
  y1 = map.simToScreenY(m_Y1);
  setFont(g);
  g.setFont(myFont);
  g.setColor(Color.gray);
  if (show_num)
    g.drawString(m_text + nf.format(m_num), x1, y1);
  else
    g.drawString(m_text, x1, y1);
}


}


//////////////////////////////////////////////////////////////////
// CCanvas class
/*
  A drawable area.
*/


class CCanvas
{
public static final int VAR = 0;  // parameter type: variable
public static final int FNC = 1;  // parameter type: function

protected CoordMap  map = null;   // maps coords between screen and sim
protected Applet my_applet = null;  // the containing applet
public String[] params;  // names of parameters
public String name = ""; // name of this canvas
protected Vector listeners;

public CCanvas(Applet app)
{
  my_applet = app;
  listeners = new Vector(10);

}

public void addListener(CCanvas aCanvas)
{
  listeners.addElement(aCanvas);
}

public void removeListener(CCanvas aCanvas)
{
  listeners.removeElement(aCanvas);
}

public void draw(Graphics g)
{
  // set clipping & clear to white & draw a border
  g.setClip(map.screen_left, map.screen_top, map.screen_width, map.screen_height);
  g.setColor(Color.white);
  g.fillRect(map.screen_left, map.screen_top, map.screen_width, map.screen_height);
  g.setColor(Color.lightGray);
  g.drawRect(map.screen_left, map.screen_top, map.screen_width-1, map.screen_height-1);
}

public void incrementalDraw(Graphics screen, Graphics offscreen)
{
}

public void repaint()
{
  if (my_applet == null)
    return;
  /*
  if (!name.equalsIgnoreCase("sim"))
    System.out.println("repaint "+name+" "+map.screen_left+" "+map.screen_top+" "+map.screen_width+" "+map.screen_height);
  */
  my_applet.repaint(map.screen_left, map.screen_top,
            map.screen_width, map.screen_height);
}

public boolean animated()
{
  return false;
}

public void mousePressed(MouseEvent evt)
{
}

public void mouseDragged(MouseEvent evt)
{
}

public void mouseReleased(MouseEvent evt)
{
}

public void mouseClicked(MouseEvent evt)
{
}

public boolean acceptsKeyEvents()
{
  return false;
}

public void keyTyped(KeyEvent e)
{
}

public void keyPressed(KeyEvent e)
{
}


public boolean hitCheck(int scr_x, int scr_y)
{
  if (scr_x < map.screen_left)
    return false;
  if (scr_x > map.screen_left + map.screen_width)
    return false;
  if (scr_y < map.screen_top)
    return false;
  if (scr_y > map.screen_top + map.screen_height)
    return false;
  return true;
}


public boolean intersectRect(Rectangle r)
{
  boolean t = map.intersectRect(r);
  /*
  if (t && !name.equalsIgnoreCase("sim")) {
    System.out.println(name+" intersectRect "+r.x+" "+r.y+" "+r.width+" "+r.height);
    System.out.println(name+" map rect "+map.screen_left+" "+map.screen_top+" "+map.screen_width+" "+map.screen_height);
  }
  */
  return t;

}

public double get(int param)
{
  return 0;
}

public void set(int param, double value)
{
}

public void setScreen(int left, int top, int width, int height)
{
  map.setScreen(left, top, width, height);
}

public void reset()
{
}

public void connect(CCanvas c)
{
}

} // end CCanvas class

//////////////////////////////////////////////////////////////////
// CPhase class
/*
  Phase graph...  graphs one variable against another
*/

class CPhase extends CCanvas
{
public static final int DOTS = 0;
public static final int LINES = 1;

private CSimulation sim = null;
private double last_x = -100000;  // last position drawn in XOR
private double last_y = -100000;
private Font numFont = null;
private FontMetrics numFM = null;
private boolean dirty = true;  // whether we need to redraw
private boolean XOR_visible = false;  // whether there is an XOR dot visible
private int x_var = 0;  // index of x variable in simulation's vars[]
private int y_var = 1;  // index of y variable in simulation's vars[]
//private double x1 = -5; // lo range of x
//private double x2 = 5;  // hi range of x
//private double y1 = -5; // lo range of y
//private double y2 = 5;  // hi range of y
private int drawMode = LINES;
private int dotSize = 1;
private DecimalFormat df = null;
private double axis_x, axis_y;  // coord position of x & y axis
private boolean auto_scale = true;
private boolean range_set = false;
private boolean range_dirty = false;
private double range_x_hi, range_x_lo, range_y_hi, range_y_lo;
private double range_time = 0;
private static final int mem_len = 1500;
private double[] mem_x = new double[mem_len];  // memory of x coords
private double[] mem_y = new double[mem_len];  // memory of y coords
private int mem_index = 0;  // index for next entry in memory list
private int mem_size = 0;  // number of items in memory list
/* how the circular memory list works:
  we write new entries into the array mem_x & mem_y until it fills.
  Then we wrap around and start writing to the beginning again.
  mem_index always points to the next location to write to.
  If mem_size < mem_len, then we have entries at 0,1,2,...,mem_index-1
  If mem_size = mem_len, then the order of entries is:
    mem_index, mem_index+1, ..., mem_len-1, 0, 1, 2, ..., mem_index-1
*/

public CPhase(Applet app)
{
  super(app);
  // CoordMap inputs are direction, x1, x2, y1, y2, align_x, align_y
  map = new CoordMap(CoordMap.INCREASE_UP, -5, 5, -5, 5, CoordMap.ALIGN_MIDDLE, CoordMap.ALIGN_MIDDLE);

  map.setFillScreen(true);
  name = "phase";
  df = new DecimalFormat("0.#");
  range_time = (double)System.currentTimeMillis()/1000;

}

public void target(CSimulation s)
{
  if (s == null)
    sim = null;
  // if target sim doesn't provide names, then don't connect to it
  else if (s.var_names == null) {
    System.out.println("graph unable to connect to sim");
    sim = null;
  }
  else
  {
    sim = s;
    sim.addListener(this);
    setXVar(x_var);  // defaults
    setYVar(y_var);
  }
}

public void disconnect()
{
  sim.removeListener(this);
}

public void setXVar(int n)
{
  x_var = n;
  if (sim == null)
    return;
  double[] r = sim.getRange(x_var);
  //x1 = r[0];
  //x2 = r[1];
  map.setRange(r[0], r[1], map.sim_y1, map.sim_y2);
  reset();
}

public void setYVar(int n)
{
  y_var = n;
  if (sim == null)
    return;
  double[] r = sim.getRange(y_var);
  //y1 = r[0];
  //y2 = r[1];
  map.setRange(map.sim_x1, map.sim_x2, r[0], r[1]);
  reset();
}

public void setDrawMode(int mode, int size)
{
  drawMode = mode;
  dotSize = size;
}

public void setAutoScale(boolean auto)
{
  System.out.println("auto scale set to "+auto);
  auto_scale = auto;
  range_set = false;
  range_dirty = false;
  range_time = (double)System.currentTimeMillis()/1000;
}

public void repaint()
{
  dirty = true;
  super.repaint();
}

public void reset()
{
  last_x = -100000;
  last_y = -100000;
  range_set = false;
  range_dirty = false;
  range_time = (double)System.currentTimeMillis()/1000;
  mem_index = mem_size = 0;  // clear out the memory
  repaint();
}

private void setFont(Graphics g)
{
  if (numFont != null)
    return;
  numFont = new Font("SansSerif", Font.PLAIN, 10);
  numFM = g.getFontMetrics(numFont);
}

public void drawArrow(Graphics g, double x, double y, double th, double len)
{
  // draw an arrow from sim point x,y at angle th of sim length len
  double x2, y2;
  x2 = x + len*Math.cos(th);
  y2 = y + len*Math.sin(th);
  g.setColor(Color.red);
  g.fillRect(map.simToScreenX(x),map.simToScreenY(y),2,2);
  g.setColor(Color.pink);
  g.drawLine(map.simToScreenX(x),
          map.simToScreenY(y),
          map.simToScreenX(x2),
          map.simToScreenY(y2));
}

public void draw(Graphics g)
{
  // copy from the offscreen buffer if we are up to date?
  XOR_visible = false;  // XOR dot will be wiped out by this draw

  // only redraw into offscreen if our state is wrong (=dirty)
  // otherwise we will just update from the offscreen
  if (!dirty)
    return;

  super.draw(g);

  if (sim==null)
  {
    dirty = false;
    return;
  }

  // figure where to draw axes
  // NOTE: need to offset from left or top so that they are visible!
  double x0, y0;  // sim coords of axes
  if ((map.sim_x1 > 0) || (map.sim_x2 < 0))
    x0 = map.sim_x1 + 0.2;
  else
    x0 = 0;
  if ((map.sim_y1 > 0) || (map.sim_y2 < 0))
    y0 = map.sim_y1 + 0.2;
  else
    y0 = 0;
  axis_x = x0;
  axis_y = y0;

  // draw horizontal axis, with different colors indicating shrink/zoom regions
  double span = map.sim_x2 - map.sim_x1;
  double a = map.sim_x1;
  g.setColor(Color.darkGray);
  g.drawLine(map.simToScreenX(a),
        map.simToScreenY(y0),
        map.simToScreenX(a + span),
        map.simToScreenY(y0));

  // draw vertical axis, with different colors indicating shrink/zoom regions
  span = map.sim_y2 - map.sim_y1;
  a = map.sim_y1;
  g.setColor(Color.darkGray);
  g.drawLine(map.simToScreenX(x0),
        map.simToScreenY(a),
        map.simToScreenX(x0),
        map.simToScreenY(a + span));

  // get a font
  setFont(g);
  g.setFont(numFont);

  // draw tick marks on horizontal axis
  // make scale*width be between 4 and 16
  //double scale = 1;
  //int w = (int)Math.ceil(map.sim_width);
  //if ((w<4)||(w>16))
  //  scale = 8.0/w;
  //int numTick = (int)Math.floor(w*scale);
  //double tickWidth = w/(double)numTick;
  //int ti;  // tick index
  // get y-coords of the tick
  int y1 = map.simToScreenY(y0) - 4;
  int y2 = y1 + 8;
  double incr = Math.ceil(map.sim_width/12);
  double x_sim = Math.ceil(map.sim_x1);
  while (x_sim < map.sim_x2)
  {
    int x_screen = map.simToScreenX(x_sim);
    g.setColor(Color.black);
    g.drawLine(x_screen,y1,x_screen,y2);  // draw tick mark
    if (Math.abs(x_sim) > .001)  // skip printing zero
    {
      g.setColor(Color.gray);
      String s;
      if (x_sim - Math.floor(x_sim) <.001)
        s = "" + (int)x_sim;  // whole number
      else
        s = df.format(x_sim); // fraction
        //s = "" + x_sim;  // fraction
      g.drawString(s, x_screen - 4, y2+10);
    }
    x_sim += incr;
  }

  // draw name of the horizontal axis
  //String hname = "position";
  String hname = sim.var_names[x_var];
  int w = numFM.stringWidth(hname);
  g.drawString(hname, map.simToScreenX(map.sim_x2) - w - 5,
          map.simToScreenY(y0) - 8);

  // draw tick marks on vertical axis
  // make scale*height be between 4 and 16
  //scale = 1;
  //w = (int)Math.ceil(map.sim_height);
  //if ((w<4)||(w>16))
  //  scale = 8.0/w;
  //numTick = (int)Math.floor(w*scale);
  //tickWidth = w/(double)numTick;
  // get x-coords of the tick
  int x1 = map.simToScreenX(x0) - 4;
  int x2 = x1 + 8;
  incr = Math.ceil(map.sim_height/12);
  //System.out.println("graph incr "+incr);
  double y_sim = Math.ceil(map.sim_y1);
  while (y_sim < map.sim_y2)
  {
    int y_screen = map.simToScreenY(y_sim);
    g.setColor(Color.black);
    g.drawLine(x1,y_screen,x2,y_screen);
    if (Math.abs(y_sim) > .001)  // skip printing zero
    {
      g.setColor(Color.gray);
      String s;
      if (y_sim - Math.floor(y_sim) <.001)
        s = "" + (int)y_sim;
      else
        s = "" + y_sim;
      g.drawString(s, x2+6, y_screen + 5);
    }
    y_sim += incr;
  }

  // draw name of the vertical axis
  String vname = sim.var_names[y_var];
  w = numFM.stringWidth(vname);
  g.drawString(vname, map.simToScreenX(x0) + 6,
    map.simToScreenY(map.sim_y2) + 13);
/*
  // draw vector field
  double dx, dy;  // increments
  double x, y;
  int xi, yi;  // counters
  double len = map.sim_width * 0.05;
  dx = map.sim_width/10;
  dy = map.sim_height/20;
  x = map.sim_x1;
  for (xi=0; xi<10; xi++)
  {
    y = map.sim_y1;
    for (yi=0; yi<20; yi++)
    {
      // need a way to communicate this equation from sim to graph!
      double th = Math.atan2(-(3.0/0.5)*(x-2.5) -0.2*y, y);
      drawArrow(g, x, y, th, len);
      y += dy;
    }
    x += dx;
  }
*/
  // draw the memorized points
  // see note above on how the circular memory list works
  if (mem_size>0)
  {
    // make list of indices into memory list, in order from oldest to newest
    int[] ind = new int[mem_size];
    int i;
    // find the first entry in the memory list
    if (mem_size < mem_len)
    {
      for (i=0; i<mem_size; i++)
        ind[i] = i;
    }
    else
    {
      for (i=0; i<mem_size; i++)
        ind[i] = (i + mem_index) % mem_size;
    }

    // draw the points in the order given by ind
    for (i=1; i<mem_size; i++)
    {
    g.setPaintMode();
    g.setColor(Color.black);
    if (drawMode == DOTS)
      g.fillRect(map.simToScreenX(mem_x[ind[i-1]]),
            map.simToScreenY(mem_y[ind[i-1]]), dotSize, dotSize);
    else
      g.drawLine(map.simToScreenX(mem_x[ind[i-1]]),
            map.simToScreenY(mem_y[ind[i-1]]),
            map.simToScreenX(mem_x[ind[i]]),
            map.simToScreenY(mem_y[ind[i]]));
    }
  }

  dirty = false;
}

public void incrementalDraw(Graphics screen, Graphics offscreen)
{
  /* do incremental drawing into both on and off screen
    need to set clipping to allow drawing in each, regardless
    of current clipping.
  */
  if (sim == null)
    return;
  screen.setClip(map.screen_left, map.screen_top,
            map.screen_width, map.screen_height);

  if (last_x > -10000)
  {
    if (XOR_visible)  // erase the last position
    {
      screen.setXORMode(Color.yellow);
      screen.fillRect(map.simToScreenX(last_x)-1,
                map.simToScreenY(last_y)-1, 4, 4);
      XOR_visible = false;
    }

    // draw a permanent pixel at last position
    screen.setPaintMode();
    screen.setColor(Color.black);
    if (drawMode == DOTS)
      screen.fillRect(map.simToScreenX(last_x),
            map.simToScreenY(last_y), dotSize, dotSize);
    else
      screen.drawLine(map.simToScreenX(last_x),
            map.simToScreenY(last_y),
            map.simToScreenX(sim.vars[x_var]),
            map.simToScreenY(sim.vars[y_var]));

    // draw permanent pixel in offscreen also
    offscreen.setClip(map.screen_left, map.screen_top,
            map.screen_width, map.screen_height);
    offscreen.setColor(Color.black);

    if (drawMode == DOTS)
      offscreen.fillRect(map.simToScreenX(last_x),
            map.simToScreenY(last_y), dotSize, dotSize);
    else
      offscreen.drawLine(map.simToScreenX(last_x),
            map.simToScreenY(last_y),
            map.simToScreenX(sim.vars[x_var]),
            map.simToScreenY(sim.vars[y_var]));

  }

  // draw current position in XOR mode
  last_x = sim.vars[x_var];
  last_y = sim.vars[y_var];
  memorize(last_x, last_y);
  if (auto_scale)
    rangeCheck(last_x, last_y);

  screen.setXORMode(Color.yellow);
  screen.fillRect(map.simToScreenX(last_x)-1,
            map.simToScreenY(last_y)-1, 4, 4);
  screen.setPaintMode();
  XOR_visible = true;

}

// remember these values in a big list
private void memorize(double last_x, double last_y)
{
  mem_x[mem_index] = last_x;
  mem_y[mem_index] = last_y;
  mem_index++;
  if (mem_size < mem_len)
    mem_size++;
  if (mem_index >= mem_len)  // wrap around at end
    mem_index = 0;
}

// for auto-scaling, see if we need to expand the range of the graph
private void rangeCheck(double last_x, double last_y)
{
  if (!range_set)
  {
    range_x_hi = last_x;
    range_x_lo = last_x;
    range_y_hi = last_y;
    range_y_lo = last_y;
    range_set = true;
  } else {
    double xspan = range_x_hi - range_x_lo;
    double yspan = range_y_hi - range_y_lo;
    if (last_x <= range_x_lo)
    {
      range_x_lo = last_x - 0.10*xspan;
      range_dirty = true;
    }
    if (last_x >= range_x_hi)
    {
      range_x_hi = last_x + 0.10*xspan;
      range_dirty = true;
    }
    if (last_y <= range_y_lo)
    {
      range_y_lo = last_y - 0.10*yspan;
      range_dirty = true;
    }
    if (last_y >= range_y_hi)
    {
      range_y_hi = last_y + 0.10*yspan;
      range_dirty = true;
    }
  }
  double now = (double)System.currentTimeMillis()/1000;
  if (range_dirty && now > range_time + 5)
  {
    range_time = now;
    //System.out.println("old range: x "+map.sim_x1+" "+map.sim_x2+" y "+map.sim_y1+" "+map.sim_y2);
    map.setRange(range_x_lo, range_x_hi, range_y_lo, range_y_hi);
    //System.out.println("new range: x "+map.sim_x1+" "+map.sim_x2+" y "+map.sim_y1+" "+map.sim_y2);
    last_x = -100000;
    last_y = -100000;
    repaint();
    range_dirty = false;
  }
}

public void mouseClicked(MouseEvent evt)
{
  System.out.println("mouse click in phase "+evt.getX() + " "+evt.getY());
  double sim_x = map.screenToSimX(evt.getX());
  double sim_y = map.screenToSimY(evt.getY());
  //sim.vars[x_var] = sim_x;
  //sim.vars[y_var] = sim_y;
  // did we click nearer to x or y axis? (in screen coords)
  int dx = Math.abs(evt.getX() - map.simToScreenX(axis_x));
  int dy = Math.abs(evt.getY() - map.simToScreenY(axis_y));
  reset();
  // if canvas has a pointer to Lab, then could repaint here!!!
}


}  // end CPhase class

//////////////////////////////////////////////////////////////////
// CSimulation class
/*

*/


class CSimulation extends CCanvas
{
public static final double NONSENSE = -999999.99;
public static final int MAX_VARS = 40; // MOVE TO SUBCLASS!!!
public static final int CLOCKS_PER_SEC = 1000;
public double    m_time = -1; // time of last simulation step
public double  sim_time = 0;  // simulation time (cumulative)
public double  time_offset = 0; // offset of simtime from real-time
public int        numVars;    // number of variables in vars[]
public double[]    vars;  // array of variables used in the diffeqs
public double[] old_vars;  // (only used in modifyobjects)

public boolean[]  calc;  // for each var, true if calc'ed, false if dragging
public String[] var_names; // names of these variables
public Vector m_oba;  // object-array: contains elements (eg. mass, spring...)
public boolean    m_Animating;    // set to false to halt animation
public boolean initial_draw = true; // true during the first time drawing
private CElement dragObj = null;
private int offset_x = 0;
private int offset_y = 0;
public boolean m_debug = false;


public CSimulation(Applet app)
{
  super(app);
  m_oba = new Vector(10);
  numVars = 0;
  m_Animating = true;
  name = "sim";
  vars = new double[CSimulation.MAX_VARS];
  old_vars = new double[CSimulation.MAX_VARS];
  calc = new boolean[CSimulation.MAX_VARS];
  int i;
  for (i=0;i<CSimulation.MAX_VARS;i++)
    calc[i] = true;
}

public void stop()
{
}

public void debug()
{
  m_debug = !m_debug;
  System.out.println("super debug="+m_debug);
}

public void showEnergy(boolean show)
{
}

public void setVar(int var, double val)
{
  vars[var] = val;
}

public void draw(Graphics g)
{
  if (initial_draw)
    initial_draw = false;
  else
    modifyObjects();  // calculate state of the world now, and say if draw needed
  super.draw(g);
  int i;
  int num_obs = m_oba.size();
  for (i=0; i<num_obs; i++) {
    CElement pElement = (CElement)m_oba.elementAt(i);
    pElement.draw(g, map);
  }
}

public double[] getRange(int var)
{
  double[] r = {0, 0};
  return r;
}

// Prepend puts the element at the front of the list
public void prepend(CElement e)
{
  m_oba.insertElementAt(e, 0);
}

public void add(CElement e)
{
  m_oba.addElement(e);
}

public void remove(CElement e)
{
  m_oba.removeElement(e);
}

public boolean contains(CElement e)
{
  return m_oba.contains(e);
}

/* Explanation of how to code up differential equations:
  The variables are stored in the vars[] array.
  Let y = var[0], w = var[1], z = var[2], etc.
  The differential equations must all be first order, in the form:
    y' = f(t, y, w, z, ...)
    w' = g(t, y, w, z, ...)
    z' = h(t, y, w, z, ...)
    ...
  You will have as many equations as there are variables

  diffeq0 returns the right hand side of the first equation
    y' = f(t, y, w, z, ...)
  diffeq1 returns the right hand side of the second equation
    w' = g(t, y, w, z, ...)
*/

public double diffeq0(double t, double[] x)
{ return 0; }
public double diffeq1(double t, double[] x)
{ return 0; }
public double diffeq2(double t, double[] x)
{ return 0; }
public double diffeq3(double t, double[] x)
{ return 0; }
public double diffeq4(double t, double[] x)
{ return 0; }
public double diffeq5(double t, double[] x)
{ return 0; }
public double diffeq6(double t, double[] x)
{ return 0; }
public double diffeq7(double t, double[] x)
{ return 0; }


// executes the i-th diffeq
// i = which diffeq,  t=time,  x= array of variables
public double evaluate(int i, double t, double[] x)
{
  switch (i)
  { case 0:  return diffeq0(t, x);
    case 1:  return diffeq1(t, x);
    case 2:  return diffeq2(t, x);
    case 3:  return diffeq3(t, x);
    case 4:  return diffeq4(t, x);
    case 5:  return diffeq5(t, x);
    case 6:  return diffeq6(t, x);
    case 7:  return diffeq7(t, x);
    default:
      System.out.println("throw?  problem in evaluate");
      return 0;
  }
}


// A version of Runge-Kutta method using arrays
// Calculates the values of the variables at time t+h
// t = last time value
// h = time increment
// vars = array of variables
// N = number of variables in x array
public void solve(double t, double h)
{
  int N = numVars;
  int i;
  // NOTE: more efficient to allocate these arrays ONCE!!! store in sim object
  // (But this way we can dynamically change the number of variables easily.)
  double[] inp = new double[N];
  double[] k1 = new double[N];
  double[] k2 = new double[N];
  double[] k3 = new double[N];
  double[] k4 = new double[N];
  for (i=0; i<N; i++)
    k1[i] = evaluate(i,t,vars);     // evaluate at time t
  for (i=0; i<N; i++)
    inp[i] = vars[i]+k1[i]*h/2; // set up input to diffeqs
  for (i=0; i<N; i++)
    k2[i] = evaluate(i,t+h/2,inp);  // evaluate at time t+h/2
  for (i=0; i<N; i++)
    inp[i] = vars[i]+k2[i]*h/2; // set up input to diffeqs
  for (i=0; i<N; i++)
    k3[i] = evaluate(i,t+h/2,inp);  // evaluate at time t+h/2
  for (i=0; i<N; i++)
    inp[i] = vars[i]+k3[i]*h; // set up input to diffeqs
  for (i=0; i<N; i++)
    k4[i] = evaluate(i,t+h,inp);    // evaluate at time t+h
  for (i=0; i<N; i++)
    if (calc[i])
      vars[i] = vars[i]+(k1[i]+2*k2[i]+2*k3[i]+k4[i])*h/6;
}


public boolean animated()
{ //m_time = -1;  // reset time because we are about to restart after long pause
  return true;
}

public void modifyObjects()
{
  if (m_Animating)
  {
    double now = (double)System.currentTimeMillis()/1000;
    /* figure out how much time has passed since last simulation step */
    /* m_time is used to figure how much real-time has passed since last simulation step */
    /* sim_time is a cumulative time counter in "simulation" time */
    double h;
    if (m_time < 0)
    {
      sim_time = 0;
      h = 0.05;   // assume that a small time has passed at start
    }
    else
    {
      h = now - m_time;
      if (h == 0)
      {
        //System.out.println("no time passed!");
        return;  // does this ever happen?
      }
      // Deal with long delays here... This causes time slippage & animation will stutter
      // It will look like the animation "paused" during the delay, but I think its
      // better than having the animation do a huge discontinuous jump.
      if (h > 0.25)
        h = 0.25;
    }
    if ((h<0) || (h>3))
    {
      System.out.println("*** trouble with time step h = "+h+" ***");
      h = 0.1;  // pick a more reasonable number
    }
    // record time of this simulation step
    m_time = now;

    /* In this loop, we attempt to step directly forward by time h.
       If a collision happened over that time-step, then we back up and re-advance to
       the time of the collision.
       Continue this process until we have advanced the entire time-step by h.
     */
    int ctr = 0;
    do
    {
      // save variables
      int i = numVars;
      while (i-- > 0)
        old_vars[i] = vars[i];
      // advance vars by delta h
      solve(sim_time, h);
      sim_time += h;
      // collision?  returns tc = estimated time of collision
      double tc = collisionTime(old_vars, h);
      if ((tc >= 0) && (tc < h))
      {
        /* collision detected, so back up to that earlier time */
        ctr++;
        // reset variables to start time
        i = numVars;
        while (i-- > 0)
          vars[i] = old_vars[i];
        sim_time -= h;
        // advance to time of collision
        if (tc > 0)
          solve(sim_time, tc);
        else
          System.out.println("zero collision "+ctr);
        sim_time += tc;
        h -= tc;

        // adjust vars to reflect the collision
        adjustCollision();
      }
      else // no collision, so done
      {
        h = 0;
      }
    }
    while (h>0);

    if (ctr > 1)
      System.out.println(ctr+" collisions");

    // Modify the Element objects based on the new values in vars
    doModifyObjects();
  }
}

public void adjustCollision()
{
}

public void doModifyObjects()
{}

public double collisionTime(double[] ov, double h)
{
  return 99999999;
}

public void startDrag(CElement e)
{
  m_Animating = false;
}

public CElement hitCheckElements(double x, double y)
{
  // Returns the draggable object that is NEAREST the specified point
  double distance = 1000000000;
  int ind = m_oba.size();
  CElement nearest = null;
  CElement pElement;
  while (--ind >= 0)
  {
    pElement = (CElement)m_oba.elementAt(ind);
    if (pElement.m_Draggable)
    {
      double d = pElement.distanceSquared(x,y);
      if (distance > d)
      {
        distance = d;
        nearest = pElement;
      }
    }
  }
  return nearest;
}

/* set the given element to the given x & y position, subject to
  whatever constraints that the simulation wants to impose.
 */
void constrainedSet(CElement e, double x, double y)
{
}

public void mousePressed(MouseEvent evt)
{
  int scr_x = evt.getX();  // screen coords
  int scr_y = evt.getY();
  // which object did mouse click on?
  double sim_x = map.screenToSimX(scr_x);  // simulation coords
  double sim_y = map.screenToSimY(scr_y);
  dragObj = hitCheckElements(sim_x, sim_y);
  if (dragObj != null)
  {
    startDrag(dragObj);
    offset_x = scr_x - map.simToScreenX(dragObj.m_X1);
    offset_y = scr_y - map.simToScreenY(dragObj.m_Y1);
  }
  //System.out.println("mouse pressed at "+scr_x+" "+scr_y);
}

public void mouseDragged(MouseEvent evt)
{
  if (dragObj != null)
  {
    double sim_x = map.screenToSimX(evt.getX() - offset_x);
    double sim_y = map.screenToSimY(evt.getY() - offset_y);
    // sim_x, sim_y should correspond to the new m_X1, m_Y1 for obj
    // let the simulation modify the object
    constrainedSet(dragObj, sim_x, sim_y);
  }
  //System.out.println("mouse dragged to "+x+" "+y);
}

public void mouseReleased(MouseEvent evt)
{
  if (dragObj != null)
  {
    m_Animating = true;   // turn animation on
    m_time = -1;  // ERN: NEEDED? restarts simulation from current state
    // reset the globals used in animation
    resetVariables(dragObj);
    dragObj = null;

    // go back to RK calculation for all vars
    int i;
    for (i=0;i<CSimulation.MAX_VARS;i++)
      calc[i] = true;

  }
}

// Causes graph to refresh after dragging...
public void resetVariables(CElement p)
{
  int i;
  int n = listeners.size();
  for (i=0; i<n; i++) {
    CCanvas c = (CCanvas)listeners.elementAt(i);
    c.reset();
  }

}


}
