/*
  File: Thruster5.java
  Demonstration of thrust applied to rigid 2D objects.
  Handles collisions with walls and between objects.

  One of the www.MyPhysicsLab.com physics simulation applets.
  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

  =======================
  HOW TO COMPILE
  =======================
  Need to first compile:
  SimApplet.java for classes SimApplet, SimCanvas, SimLayout1, GenericAppletFrame,...

  to create jar file:
  jar cfm ../lab/Thruster5.jar ThrusterMainClass.txt *.class
*/


import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Enumeration;
import java.io.*;
import java.util.Iterator;
import java.util.Vector;

/////////////////////////////////////////////////////////////////////////////////
/* FuzzyButton is a non-focusable button (hence fuzzy).
   This is so that after clicking the button, the applet can still process key
   events.
class FuzzyButton extends Button {
  public FuzzyButton(String name) {
    super(name);
  }
  public boolean isFocusable()  { // Java 1.4 version
    return false;
  }
}
 */

///////////////////////////////////////////////////////////////////////////
class ThrustLayout implements LayoutManager {
  public ThrustLayout() {}
  public void addLayoutComponent(String name, Component c) {}
  public void removeLayoutComponent(Component c) {}
  public Dimension preferredLayoutSize(Container target) {
    return new Dimension(500, 500);
  }
  public Dimension minimumLayoutSize(Container target) {
    return new Dimension(100,100);
  }
  public void layoutContainer(Container target) {
    // Canvas is assumed to be first component (index 0).
    // First pass: position the controls in upper part of window.
    // Lay them out left to right, and skip to next line when they
    // don't fit.
    //System.out.println("layoutContainer");
    int canvasWidth = target.getSize().width;
    int i, space = 3;
    int lineVertical = space;
    int lineHorizontal = space;  // cumulative width
    int maxHeight = 0;
    int startComponent = 1;  // Which component is at start of current line.
    int n = target.getComponentCount();
    for (i = startComponent; i < n; i++) {
      Component m = target.getComponent(i);
      if (m.isVisible()) {
        boolean wideLabel = m instanceof MyLabel;
        Dimension d = m.getPreferredSize();
        // We assume that wide labels take up a whole line of the display.
        if (wideLabel)
          m.setSize(canvasWidth, d.height);  // wide label gets entire width of canvas
        else
          m.setSize(d.width, d.height);
        // check if this component can fit on current line.
        if (wideLabel || (lineHorizontal + d.width > canvasWidth)) {  // Can't fit on this line
          // Adjust items on this line to be vertically centered within the line
          int j;
          for (j = startComponent; j<i; j++) {
            Component comp = target.getComponent(j);
            //System.out.println("component "+j+" offset "+(maxHeight - comp.getHeight())/2);
            //NOTE: getX() and getHeight() are Java 1.2 features, so don't use them!!
            comp.setLocation(comp.getLocation().x, lineVertical +
              (maxHeight - comp.getSize().height)/2);
          }
          // Move to next line
          lineHorizontal = space;
          lineVertical += maxHeight + space;
          maxHeight = 0;
          startComponent = i;  // this will be first component of the new line
        }
        m.setLocation(lineHorizontal, lineVertical);

        if (wideLabel) { // give wide label entire line, so move to next line now
          lineVertical += d.height + space;
          maxHeight = 0;
          startComponent += 1; //??? start next line with next item?
        } else {
          //System.out.println("setlocation "+w+" "+h+"  setsize "+d.width+" "+d.height);
          if (d.height > maxHeight)
            maxHeight = d.height;
          lineHorizontal += d.width + space;
        }
      }
    }
    // Now we know how much vertical space the controls need.
    // Set the canvas to take up the rest of the space.
    Component lastComp = target.getComponent(n-1);
    int lastY = lastComp.getLocation().y + lastComp.getSize().height + space;
    int canvasHeight = target.getSize().height - lastY;
    Component cnvs = target.getComponent(0);  // this should be the canvas
    cnvs.invalidate(); // so that the offscreen buffer gets recreated
    cnvs.setLocation(0, 0);
    cnvs.setSize(canvasWidth, canvasHeight);
    // Move all the controls to below the canvas.
    for (i = 1; i < n; i++) {
      Component m = target.getComponent(i);
      Point loc = m.getLocation();
      m.setLocation(loc.x, loc.y + canvasHeight);
    }

    // following line is necessary to fix update garbage when layout changes!
    target.update(target.getGraphics());
  }
};

/////////////////////////////////////////////////////////////////////////////
class ThrustText
{
  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 double m_X1 = 0;
  public double m_Y1 = 0;
  public boolean centered = true;


  public ThrustText(String t) {
    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;
    setFont(g);
    g.setFont(myFont);
    g.setColor(Color.black);
    if (centered) {
      y1 = map.screen_top + map.screen_height/2;
      int w = myFM.stringWidth(m_text);
      x1 = map.screen_left + map.screen_width/2 - w/2;
    } else {
      x1 = map.simToScreenX(m_X1);
      y1 = map.simToScreenY(m_Y1);
    }
    if (show_num)
      g.drawString(m_text + nf.format(m_num), x1, y1);
    else
      g.drawString(m_text, x1, y1);
  }
}



/////////////////////////////////////////////////////////////////////////////////
// A scrollbar where you can set the preferred width and height
class MyScrollbar extends Scrollbar {
  int w,h;
  public MyScrollbar(int w, int h, int orient, int value, int vis, int min, int max) {
    // new Scrollbar(orientation, value, visibleAmount, minimum, maximum)
    super(orient, value, vis, min, max);
    this.w = w;
    this.h = h;
  }

  public Dimension getPreferredSize() {
    return new Dimension(w,h);
  }
}


/////////////////////////////////////////////////////////////////////////////////
// A Label where the preferred width and height is determined by the text & font.
// Otherwise, the Label winds up being larger than necessary.
class MyLabel extends Label {
  String sample = null;  // sample text is used to figure max width of text

  public MyLabel(String text) {
    super(text);
  }

  public MyLabel(String text, int alignment) {
    super(text, alignment);
  }

  public MyLabel(String text, int alignment, String sample) {
    super(text, alignment);
    this.sample = sample;
  }

  public Dimension getPreferredSize() {
    Font myFont = new Font("SansSerif", Font.PLAIN, 12);
    this.setFont(myFont);
    FontMetrics myFM = this.getFontMetrics(myFont);
    int w,h;
    String txt;
    if (sample == null)
      txt = this.getText();
    else
      txt = sample;
    w = 5+myFM.stringWidth(txt); // use sample to figure text width
    h = myFM.getAscent() + myFM.getDescent();
    return new Dimension(w,h);
  }
}


/////////////////////////////////////////////////////////////////////////////////
// A slider consists of a label, a scrollbar, and a numeric display of the value.
class MySlider extends Panel {
  double min, delta;
  public MyScrollbar scroll;
  MyLabel nameLabel;
  MyLabel myNumber;
  NumberFormat nf = NumberFormat.getNumberInstance();

  public MySlider(AdjustmentListener applet, String name, double value,
      double min, double max, int increments, int digits) {
    this.min = min;
    delta = (max - min)/increments;

    nameLabel = new MyLabel(name, Label.CENTER);
    add(nameLabel);
    // new MyScrollbar(width, height, orientation, value, visibleAmount, minimum, maximum)
    scroll = new MyScrollbar(75, 15, Scrollbar.HORIZONTAL, (int)(0.5+(value-min)/delta),
      10, 0, increments+10);
    add(scroll);
    scroll.addAdjustmentListener(applet);
    nf = NumberFormat.getNumberInstance();
    nf.setMaximumFractionDigits(digits);
    nf.setMinimumFractionDigits(digits);
    myNumber = new MyLabel(nf.format(value), Label.LEFT, "88.88");
    add(myNumber);
    FlowLayout lm = (FlowLayout)getLayout();
    lm.setHgap(1);
    lm.setVgap(1);
  }

  public double getValue() {
    double value = min + (double)scroll.getValue()*delta;
    myNumber.setText(nf.format(value));  // update the text as a side effect
    return value;
  }

  /*
  public Insets getInsets() {
    return new Insets(1,1,1,1);
  }*/
  /*  // keep this around for understanding how panel layout works!
  public void paint(Graphics g) {
    Rectangle r = getBounds();
    Rectangle c = g.getClipBounds();
    System.out.println("Panel.paint "+r.x+" "+ r.y+" "+r.width+" "+r.height);
    if (c != null)
      System.out.println(" clip bounds "+c.x+" "+ c.y+" "+c.width+" "+c.height);
    Insets i = getInsets();
    System.out.println("  insets "+i.top+" "+i.bottom+" "+i.right+" "+i.left);
    FlowLayout lm = (FlowLayout)getLayout();
    System.out.println("  layout hgap vgap "+lm.getHgap()+" "+lm.getVgap());
    g.setColor(Color.yellow);
    // NOTE: drawing takes place in local (panel) coordinates!
    g.fillRect(0, 0, r.width-1, r.height-1);
    super.paint(g);
  }
  */
}

/////////////////////////////////////////////////////////////////////////////////
/*   object coords are as follows.  angle = zero as shown.  tAngle = -pi/4

     d-----c(width, height)
     |     |
     |     |  coords of e = x + sin(angle)*cmy, y - cos(angle)*cmy
     |     |  coords of a = ex - cos(angle)*cmx, ey - sin(angle)*cmx
     |     |  coords of b = ax + cos(angle)*width, ay + sin(angle)*width
     |  cm |  coords of c = bx - sin(angle)*height, by + cos(angle)*height
     |    /|  coords of d = ax - sin(angle)*height, ay + cos(angle)*height
     |   / |
     |  t  |
     |     |
     |     |
     a--e--b
(0,0)

*/
class Thruster5Object {
  public double x,y;  // position of the center of mass in the world
  public double angle;  // rotation of object around center of mass
  private double width;  // width of object
  private double height; // height of object
  public double cmx, cmy; // position of  center of mass in object coords
  public double thrustX, thrustY;  // position of thrust point in object coords
  public double[] tAngle;  // angle of the thrust in object coords
  public boolean[] active;  // which thrusters are firing
  public double tMagnitude;  // thrust magnitude
  public double mass;
  public double ax,ay,bx,by,cx,cy,dx,dy,tx,ty;  // positions of corners
  public Color color;

  public Thruster5Object() {
    x = 0; y = 0; angle = 0; setWidth(0.5);  setHeight(3);
    thrustX = width/2;  thrustY = 0.8*height;    tMagnitude = 0.5;
    mass = 1;
    color = Color.black;
    tAngle = new double[4];
    tAngle[0] = Math.PI/2; // left
    tAngle[1] = -Math.PI/2; // right
    tAngle[2] = 0; // up
    tAngle[3] = Math.PI; // down
    active = new boolean[4];
    active[0] = active[1] = active[2] = active[3] = false;
    moveTo(x, y, angle);
  }

  public double getWidth() {
    return this.width;
  }

  public double getHeight() {
    return this.height;
  }

  public double getMinHeight() {  // for potential energy calculation
    return (width<height) ? width/2 : height/2;
  }

  public void setWidth(double width) {
    this.width = width;
    cmx = width/2;
  }

  public void setHeight(double height) {
    this.height = height;
    cmy = height/2;
  }

  // returns moment of inertia about center of mass
  public double momentAboutCM() {
    return mass*(width*width + height*height)/12;
  }

  // given velocities (linear and angular) return kinetic energy
  public double kineticEnergy(double vx, double vy, double w) {
    double e = 0.5*mass*(vx*vx + vy*vy);
    e += 0.5*momentAboutCM()*w*w;
    return e;
  }

  public double rotationalEnergy(double w) {
    return 0.5*momentAboutCM()*w*w;
  }

  public double translationalEnergy(double vx, double vy) {
    double e = 0.5*mass*(vx*vx + vy*vy);
    return e;
  }

  // given velocities (linear and angular) return momentum
  public double[] momentum(double vx, double vy, double w) {
    double result[] = new double[3];
    result[0] = mass*vx;
    result[1] = mass*vy;
    /*
    angular momentum about a fixed point in space is defined as
    Icm w k + r x m vcm
    (k is unit z vector, r is vector from fixed point to cm)
    cross product in the plane: (ax,ay,0) x (bx,by,0) = k(ax by - ay bx)
    Icm w + m(rx vy - ry vx)
    take the fixed point to be the origin (0,0), so (rx,ry) is cm
    */
    result[2] = momentAboutCM()*w + mass*(x*vy - y*vx);
    return result;
  }

  // returns the vector from CM to thrust point, based on passed in vars
  public double[] calcVectors(double x, double y, double angle, int thruster) {
    double sinAngle = Math.sin(angle);
    double cosAngle = Math.cos(angle);
    double ex = x + sinAngle*cmy;
    double ey = y - cosAngle*cmy;
    double ax = ex - cosAngle*cmx;
    double ay = ey - sinAngle*cmx;
    double tx = ax + cosAngle*thrustX - sinAngle*thrustY;
    double ty = ay + sinAngle*thrustX + cosAngle*thrustY;
    double tx2 = tx - Math.sin(angle + tAngle[thruster])*tMagnitude;
    double ty2 = ty + Math.cos(angle + tAngle[thruster])*tMagnitude;
    double rx = tx - x;
    double ry = ty - y;
    double rlen = Math.sqrt(rx*rx + ry*ry);
    double result[] = new double[6];
    result[0] = rx;  // vector from CM to thrust point
    result[1] = ry;
    result[2] = rx/rlen;  // normalized version
    result[3] = ry/rlen;
    result[4] = tx2 - tx;  // thrust vector
    result[5] = ty2 - ty;
    return result;
  }

  public void moveTo(double x, double y, double angle) {
    this.x = x;
    this.y = y;
    this.angle = angle;
    // find position of corners labeled a,b,c,d,e,t in diagram above
    double sinAngle = Math.sin(this.angle);
    double cosAngle = Math.cos(this.angle);
    double ex = this.x + sinAngle*cmy;
    double ey = this.y - cosAngle*cmy;
    this.ax = ex - cosAngle*cmx;
    this.ay = ey - sinAngle*cmx;
    this.bx = this.ax + cosAngle*width;
    this.by = this.ay + sinAngle*width;
    this.cx = this.bx - sinAngle*height;
    this.cy = this.by + cosAngle*height;
    this.dx = this.ax - sinAngle*height;
    this.dy = this.ay + cosAngle*height;
    this.tx = this.ax + cosAngle*thrustX - sinAngle*thrustY;
    this.ty = this.ay + sinAngle*thrustX + cosAngle*thrustY;
  }

  public void draw(Graphics g, CoordMap map) {
    // make a polygon object
    int[] xPoints = new int[5];
    int[] yPoints = new int[5];
    int i = 0;
    xPoints[i] = map.simToScreenX(ax);
    yPoints[i++] = map.simToScreenY(ay);
    xPoints[i] = map.simToScreenX(bx);
    yPoints[i++] = map.simToScreenY(by);
    xPoints[i] = map.simToScreenX(cx);
    yPoints[i++] = map.simToScreenY(cy);
    xPoints[i] = map.simToScreenX(dx);
    yPoints[i++] = map.simToScreenY(dy);
    xPoints[i] = map.simToScreenX(ax);
    yPoints[i++] = map.simToScreenY(ay);
    g.setColor(this.color);
    g.fillPolygon(xPoints, yPoints, 5);
    // draw a dot at thruster point
    double sz = 0.15*((width < height) ? width : height);
    int w = map.simToScreenScaleX(2*sz);
    int sx = map.simToScreenX(tx-sz);
    int sy = map.simToScreenY(ty+sz);
    g.setColor(Color.gray);
    g.fillOval(sx, sy, w, w);
    // draw a line in *REVERSE* direction of thrust force
    // (reverse, because that's how we think about thrust on a rocket)
    double tx2, ty2;
    int k;
    for (k=0; k<4; k++) {  // for each thruster
      if (active[k]) {
        double len = Math.log(1+tMagnitude)/0.693219;
        tx2 = tx + Math.sin(angle + tAngle[k])*len;
        ty2 = ty - Math.cos(angle + tAngle[k])*len;
        g.setColor(Color.red);
        g.drawLine(map.simToScreenX(tx), map.simToScreenY(ty),
            map.simToScreenX(tx2), map.simToScreenY(ty2));
      }
    }
  }

  public Collision testCollision(double gx, double gy, int objIndex, int selfIndex) {
    // given the point (gx, gy) which is a corner of the other object,
    // determine how far inside this object it is.
    // Returns the distance to the closest edge (negative if outside the object),
    // and the normal to that edge (pointing out).
    // The point of impact is just returned as (gx,gy).
    // move to body coordinate system
    gx -= this.x;  // set center of mass as origin, and unrotate
    gy -= this.y;
    double px = gx*Math.cos(-this.angle) - gy*Math.sin(-this.angle);
    double py = gx*Math.sin(-this.angle) + gy*Math.cos(-this.angle);
    px += this.cmx; // translate to body coordinates
    py += this.cmy;
    // find nearest edge: 0=bottom, 1=right, 2=top, 3=left
    int edge = 0;
    double dist = py;  // bottom edge
    double d;
    d = this.width - px;  // right edge
    if (d < dist) {dist = d; edge = 1;}
    d = this.height - py; // top edge
    if (d < dist) {dist = d; edge = 2;}
    d = px;  // left edge
    if (d < dist) {dist = d; edge = 3;}

    if (dist > 0) {
      Collision result = new Collision();
      result.depth = dist; // depth of collision
      result.impactX = gx; // point of impact
      result.impactY = gy;
      result.normalObj = selfIndex; // object corresponding to the normal
      result.object = objIndex; // object whose corner is colliding
      // figure out normal to that edge, and rotate it back to world coords
      // (don't need to translate it)
      switch (edge) {
        case 0: px=0;py=-1;break;
        case 1: px=1;py=0;break;
        case 2: px=0;py=1;break;
        case 3: px=-1;py=0;break;
      }
      // normal (outward pointing)
      result.normalX = px*Math.cos(this.angle) - py*Math.sin(this.angle);
      result.normalY = px*Math.sin(this.angle) + py*Math.cos(this.angle);
      return result;
    } else
      return null;
  }

}

/////////////////////////////////////////////////////////////////////////////////
class Collision {
  public double depth; // depth of collision (positive = penetration)
  public double normalX; // normal (pointing outward from normalObj?)
  public double normalY;
  public double impactX; // point of impact
  public double impactY;
  public int normalObj; // object corresponding to the normal (negative = wall)
  public int object; // object whose corner is colliding

  public Collision() {
  }
}

/////////////////////////////////////////////////////////////////////////////////
public class Thruster5 extends SimApplet implements KeyListener, ActionListener,
  MouseListener, AdjustmentListener, ItemListener, MouseMotionListener {

  boolean debug = false;
  public static final int RIGHT_WALL = -1;
  public static final int BOTTOM_WALL = -2;
  public static final int LEFT_WALL = -3;
  public static final int TOP_WALL = -4;
  public static final int MAX_BODIES = 6; // maximum number of bodies
  public int numBods = 2;  // number of bodies
  public Thruster5Object[] bods;  // array of bodies
  public int numVars;    // number of variables in vars[]
  public double[] vars;  // array of variables
  public double[] old_vars;
  double gravity = 0.0;
  double damping = 0.0;
  double elasticity = 1.0;
  double thrust = 0.5;
  MySlider dampSlider;
  MySlider elasticSlider;
  MySlider gravitySlider;
  MySlider thrustSlider;
  MySlider massSlider;
  boolean showEnergy = false;  // whether to show energy bar chart & labels
  Checkbox energyCheckbox;  // "show energy" checkbox
  MyLabel preLabel;  // label that displays pre-collision energy & momentum
  MyLabel postLabel; // label that displays post-collision energy & momentum
  private Font graphFont = null;  // for numbers on the bar chart energy graph
  int graphAscent;  // for numbers on the bar chart energy graph
  double graphFactor = 10;  // determines how wide the bar chart is
  double graphDelta = 2;  // spacing of the numbers in the bar chart
  Choice bodiesChoice;  // popup menu for number of bodies
  ThrustCanvas cvs;  // canvas used for drawing simulation into
  Button buttonStop;
  Button buttonStart;
  Button buttonReset;
  Vector collisions = new Vector(4*numBods);
  ThrustText message = null;  // error message for when simulation is stuck
  double last_time = -9999;  // last time that simulation step was done
  double sim_time = 0;   // simulation time
  double lastTimeStep = 0;  // amount of last time step (time delta)
  boolean m_Animating = true;
  protected static final double TOL = 0.0001;  // time tolerance for finding collisions
  NumberFormat nf = NumberFormat.getNumberInstance();
  int dragObj = -1;
  double mouseX, mouseY;
  boolean gameMode = false;
  int winningHits = 10;
  int greenHits = 0, blueHits = 0;  // number of times green or blue object hit walls
  Label greenLabel;  // displays number of wall hits
  Label blueLabel;
  ThrustText message2 = null; // "game over" message

  public synchronized void init() {
    if (debug)
      System.out.println("starting Thruster5");

    if ("true".equalsIgnoreCase(getParameter("game")))
      gameMode = true;

    String str = getParameter("objects");
    if ((str != null) && (str != "")) {
      int objs = Integer.parseInt(str);
      if ((objs > 0) && (objs < MAX_BODIES))
        numBods = objs;
    }
    requestFocus();
    setBackground(Color.white);  // otherwise controls appear over gray
    setLayout(new ThrustLayout());
    // CoordMap(y_dir, x1, x2, y1, y2, align__x, align__y)
    double w = 5;
    map = new CoordMap(CoordMap.INCREASE_UP, -w, w, -w, w,
        CoordMap.ALIGN_MIDDLE, CoordMap.ALIGN_MIDDLE);
    add(cvs = new ThrustCanvas(this));
    add(buttonStop = new Button("pause"));
    buttonStop.addActionListener(this);
    add(buttonStart = new Button("resume"));
    buttonStart.addActionListener(this);
    add(buttonReset = new Button("reset"));
    buttonReset.addActionListener(this);
    addKeyListener(this);
    //addMouseListener(this);
    if (gameMode) {
      add(greenLabel = new Label("green 0    "));
      add(blueLabel = new Label("blue 0    "));
    }

    if (!gameMode) {
      // popup menu for number of bodies
      bodiesChoice = new Choice();
      int i;
      nf.setMinimumFractionDigits(0);
      for (i=0; i<MAX_BODIES; i++) {
        bodiesChoice.add(nf.format(i+1)+" object"+(i>0 ? "s" : ""));
      }
      bodiesChoice.select(numBods-1);
      bodiesChoice.addItemListener(this);
      add(bodiesChoice);

      // energy checkbox: whether to show energy bar
      energyCheckbox = new Checkbox("Show Energy", showEnergy);
      energyCheckbox.addItemListener(this);
      add(energyCheckbox);
    }
    if (gameMode)
      damping = 0.2;
    else
      damping = 0;

    reset();
    if (!gameMode)
      vars[1] = 1.5;  // make first body move at start

    // Slider params:  applet, name, value, min, max, number of increments, fraction digits
    add(dampSlider = new MySlider(this, "damping", damping, 0.0, 1.0, 100, 2));
    add(elasticSlider = new MySlider(this, "elasticity", elasticity, 0.0, 1.0, 100, 2));
    add(gravitySlider = new MySlider(this, "gravity", gravity, 0.0, 10.0, 100, 2));
    add(thrustSlider = new MySlider(this, "thrust", thrust, 0.0, 5.0, 100, 2));
    add(massSlider = new MySlider(this, "green mass", bods[0].mass, 0.1, 10.1, 100, 1));

    // labels for pre- and post-collision energy
    preLabel = new MyLabel(" ");
    postLabel = new MyLabel(" ");

  }

  // Synchronized methods are guaranteed to run to completion before
  // any other synchronized method on the same object.
  // Here we synchronize to ensure that we don't change the number of
  // bodies while the animation routines are running.
  public synchronized void reset() {
    bods = new Thruster5Object[numBods];
    int i;
    for (i=0; i<numBods; i++) {
      bods[i] = new Thruster5Object();
      bods[i].tMagnitude = thrust;
    }
    if (numBods>0) {
      if (gameMode)
        bods[0].moveTo(2,0,Math.PI/4);
      else
        bods[0].moveTo(-2,0,Math.PI/2);
      bods[0].color = Color.green;
    }
    if (numBods>1) {
      if (gameMode)
        bods[1].moveTo(-2,0,-Math.PI/4);
      else
        bods[1].moveTo(2,1,0);
      //bods[1].setWidth(1);
      //bods[1].setHeight(3);
      //bods[1].thrustX = bods[1].cmx;
      //bods[1].thrustY = 0.8*bods[1].getHeight();
      bods[1].color = Color.blue;
    }
    if (numBods>2) {
      bods[2].moveTo(1,0,0.1);
      bods[2].color = Color.red;
    }
    if (numBods>3) {
      bods[3].moveTo(-2.2, 1, 0.2+Math.PI/2);
      bods[3].color = Color.cyan;
    }
    if (numBods>4) {
      bods[4].moveTo(-2.4,-1, -0.2+Math.PI/2);
      bods[4].color = Color.magenta;
    }
    if (numBods>5) {
      bods[5].moveTo(-1.8,2, 0.3+Math.PI/2);
      bods[5].color = Color.orange;
    }
    /*  variables:   x, x', y, y', th, th'
        bods[0]      0, 1,  2, 3,  4,  5
        bods[1]      6, 7,  8, 9, 10, 11
    */
    numVars = 6*numBods;
    vars = new double[numVars];
    old_vars = new double[numVars];
    for (i=0; i<numBods; i++) {  // set initial position of each body
      vars[6*i] = bods[i].x;
      vars[6*i + 2] = bods[i].y;
      vars[6*i + 4] = bods[i].angle;
    }

    message = null;
    message2 = null;
    if (gameMode) {
      this.nf.setMinimumFractionDigits(0);
      greenLabel.setText("green "+nf.format(greenHits = 0));
      blueLabel.setText("blue "+nf.format(blueHits = 0));
    }
  }

  public void drawObjects(Graphics g) {
    drawEnergy(g);
    int i;
    for (i=0; i<numBods; i++)
      bods[i].draw(g,map);
    // draw the rubberband to mouse position
    if (dragObj >= 0) {
      g.setColor(Color.black);
      g.drawLine(map.simToScreenX(mouseX), map.simToScreenY(mouseY),
        map.simToScreenX(bods[dragObj].tx), map.simToScreenY(bods[dragObj].ty));
    }
    if (message != null)
      message.draw(g, map);
    if (message2 != null)
      message2.draw(g, map);

    // draw rect around the simulation boundary
    g.setColor(Color.black);
    int left = map.leftBox();
    int top = map.topBox();
    int width = map.widthBox() -1;
    int height = map.heightBox() -1;
    //System.out.println("drawRect "+left+" "+top+" "+width+" "+height);
    g.drawRect(left,top,width,height);
  }

  public synchronized void threadUpdate() {
    modifyObjects();
    int i = numBods;
    while (i-- > 0)
      bods[i].moveTo(vars[6*i+0], vars[6*i+2], vars[6*i+4]);
    cvs.repaint();
  }

  /*
    Let th = angle of the body
    variables:   x, x', y, y', th, th'
    bods[0]      0, 1,  2, 3,  4,  5
    bods[1]      6, 7,  8, 9, 10, 11
    Let R be the vector from CM (center of mass) to T (thrust point)
    components of R are Rx, Ry
    Let N be normalized R, so that N = R / |R|
    Let F be the thrust vector, with components Fx, Fy
    (and for mouse dragging, we add a spring force also).
    The force on the center of mass is (F.N)N... no its just F!
    CM moves according to (F.N)N = M A... no just F = M A
    So we have the two equations:
      Ax = Nx (F.N)/M
      Ay = Ny (F.N)/M
      ... no just Ax = Fx/M and Ay = Fx/M
    The moment of inertia about the CM is I = M (width^2 + height^2)/12
    The torque at T about the CM is given by R x F = Rx Fy - Ry Fx
    The angular dynamics are given by R x F = th'' I
    So we have the equation
      th'' = (Rx Fy - Ry Fx)/I
    The method calcVectors calculates F,N,R given x,y,th
  */
  // executes the i-th diffeq
  // i = which diffeq,  t=time,  x= array of variables
  public double evaluate(int i, double t, double[] x) {
    int j = i%6;  // % is mod, so j tells what derivative is wanted:
                  // 0=x, 1=x', 2=y, 3=y', 4=th, 5=th'
    int obj = i/6;  // which object: 0, 1
    int offset = 6*obj;
    int k;
    double result = 0;
    final double springConst = 1;
    switch (j) {
      case 0: return x[1+offset];
      case 1:
        result = - damping*x[1+offset]/bods[obj].mass;
        for (k=0; k<4; k++) {  // for each of the 4 thrusters
          if (bods[obj].active[k]) {
            double[] v = bods[obj].calcVectors(x[0+offset], x[2+offset], x[4+offset], k);
            // v[0] = Rx, v[1] = Ry, v[2] = Nx, v[3] = Ny, v[4] = Fx, v[5] = Fy
            result += v[4]/bods[obj].mass;  // Ax = Fx/M
          }
        }
        if (obj == dragObj) { // add rubber band force
          double[] v = bods[obj].calcVectors(x[0+offset], x[2+offset], x[4+offset], 0);
          // x component of rubber band force
          double Fx = springConst*(mouseX - (x[0+offset] + v[0]));
          result += Fx/bods[obj].mass;  // Ax = Fx/M
        }
        return result;
      case 2:  return x[3+offset];
      case 3:
        result = - damping*x[3+offset]/bods[obj].mass;
        result -= gravity;
        for (k=0; k<4; k++) {  // for each thruster
          if (bods[obj].active[k]) {
            double[] v = bods[obj].calcVectors(x[0+offset], x[2+offset], x[4+offset], k);
            // v[0] = Rx, v[1] = Ry, v[2] = Nx, v[3] = Ny, v[4] = Fx, v[5] = Fy
            result += v[5]/bods[obj].mass;  // Ay = Fy/M
          }
        }
        if (obj == dragObj) { // add rubber band force
          double[] v = bods[obj].calcVectors(x[0+offset], x[2+offset], x[4+offset], 0);
          // y component of rubber band force
          double Fy = springConst*(mouseY - (x[2+offset] + v[1]));
          result += Fy/bods[obj].mass;  // Ay = Fy/M
        }
        return result;
      case 4:  return x[5+offset];
      case 5:
        result = - damping*x[5+offset];
        for (k=0; k<4; k++) {  // for each thruster
          if (bods[obj].active[k]) {
            double[] v = bods[obj].calcVectors(x[0+offset], x[2+offset], x[4+offset], k);
            // v[0] = Rx, v[1] = Ry, v[2] = Nx, v[3] = Ny, v[4] = Fx, v[5] = Fy
            //  th'' = (Rx Fy - Ry Fx)/I
            result += (v[0]*v[5] - v[1]*v[4])/bods[obj].momentAboutCM();
          }
        }
        if (obj == dragObj) { // add rubber band force
          double[] v = bods[obj].calcVectors(x[0+offset], x[2+offset], x[4+offset], 0);
          // x & y components of rubber band force
          double Fx = springConst*(mouseX - (x[0+offset] + v[0]));
          double Fy = springConst*(mouseY - (x[2+offset] + v[1]));
          //  th'' = (Rx Fy - Ry Fx)/I
          result += (v[0]*Fy - v[1]*Fx)/bods[obj].momentAboutCM();
        }
        return result;
      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;
    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++)
      vars[i] = vars[i]+(k1[i]+2*k2[i]+2*k3[i]+k4[i])*h/6;
  }

  public Collision checkCollision(int obj, int corner) {
    Collision result = null;
    // get info about this corner
    double cornerX, cornerY;
    switch (corner) {
      case 1: cornerX = bods[obj].ax;  cornerY = bods[obj].ay; break;
      case 2: cornerX = bods[obj].bx;  cornerY = bods[obj].by; break;
      case 3: cornerX = bods[obj].cx;  cornerY = bods[obj].cy; break;
      case 4: cornerX = bods[obj].dx;  cornerY = bods[obj].dy; break;
      default: System.out.println("**** bad corner***** ");cornerX = 0; cornerY = 0;
    }
    // check for collision with each wall, take the one with maximum depth
    int i;
    double d;  // depth of penetration:  postive = more penetration
    // when depth is positive, and there is no previous collision or depth is greater
    if (((d = cornerX - map.sim_x2) > 0) && ((result == null) || (d > result.depth))) {
      result = new Collision();
      result.depth = d;
      result.normalX = -1;
      result.normalY = 0;
      result.normalObj = RIGHT_WALL;
    }
    if (((d = map.sim_x1 - cornerX) > 0) && ((result == null) || (d > result.depth))) {
      result = new Collision();
      result.depth = d;
      result.normalX = 1;
      result.normalY = 0;
      result.normalObj = LEFT_WALL;
    }
    if (((d = cornerY - map.sim_y2) > 0) && ((result == null) || (d > result.depth))) {
      result = new Collision();
      result.depth = d;
      result.normalX = 0;
      result.normalY = -1;
      result.normalObj = TOP_WALL;
    }
    if (((d = map.sim_y1 - cornerY) > 0) && ((result == null) || (d > result.depth))) {
      result = new Collision();
      result.depth = d;
      result.normalX = 0;
      result.normalY = 1;
      result.normalObj = BOTTOM_WALL;
    }
    // check for collision with each object
    for (i=0; i<numBods; i++) {
      if (i != obj) {  // don't compare object with itself
        Collision c = bods[i].testCollision(cornerX, cornerY, obj, i);
        if (c != null) {
          if (result == null)
              result = c;
          else {
            if (c.depth > result.depth)
              result = c;
          }
        }
      }
    }
    // additional info for collision
    if (result != null) {
      result.impactX = cornerX;
      result.impactY = cornerY;
      result.object = obj;
    }
    return result;
  }

  // find all current collisions
  // for each point on a body, create a Collision object
  public void findAllCollisions(double t) {
    int i,j;
    // clear vector of collisions
    collisions.removeAllElements();  // NOTE Vector.clear() is only in Java 1.2
    for (i=0; i<numBods; i++) {  //move bodies to current positions
      bods[i].moveTo(vars[0+6*i], vars[2+6*i], vars[4+6*i]);
    }
    for (i=0; i<numBods; i++) {  //for each body
      for (j=1; j<=4; j++) {  // for each corner
        // need radius check here
        // is corner colliding?
        Collision c = checkCollision(i, j);
        if (c != null) {
          // add c to array of collisions
          collisions.addElement(c);  // NOTE Vector.add() is only in Java 1.2
        }
      }
    }
    if (debug) {
      if (collisions.size() > 0)
        System.out.println("--------------------------------");
      if (collisions.size() > 1) {
        System.out.println(collisions.size()+" collisions detected at time "+t);
      }
      if (collisions.size() > 0) {
        for (i=0; i<collisions.size(); i++) {
          DecimalFormat df = new DecimalFormat("0.0###");
          Collision c = (Collision)collisions.elementAt(i);
          System.out.println("collision obj="+df.format(c.object)+
            " normalObj="+df.format(c.normalObj)+
            " impact x="+df.format(c.impactX)+" y="+df.format(c.impactY));
          System.out.println("normal x="+df.format(c.normalX)
            +" y="+df.format(c.normalY)+" depth= "+df.format(c.depth));
        }
      }
    }
  }

  public Vector findMatch(Vector collisions) {
    // Search for a pair of matching collisions, return them in a vector.
    int i,j,n;
    n = collisions.size();
    for (i=0; i<n; i++) {
      Collision c1 = (Collision)collisions.elementAt(i);
      for (j=i+1; j<n; j++) {
        Collision c2 = (Collision)collisions.elementAt(j);
        if (((c1.object==c2.object) && (c1.normalObj==c2.normalObj))
            || ((c1.object==c2.normalObj)&&(c1.normalObj==c2.object))) {
          // found a match
          Vector result = new Vector(2);
          result.addElement(c1);
          result.addElement(c2);
          return result;
        }
      }
    }
    return null;
  }

  public void specialImpact(Vector collisions, double[] velo) {
    // Find multiple contact collisions, add their effects to velo
    Vector m;
    while ((m = findMatch(collisions)) != null) {
      Collision c1 = (Collision)m.elementAt(0);
      Collision c2 = (Collision)m.elementAt(1);
      // ...deal with this multiple collision...
      if (c1.normalObj < 0) {  // collision with wall... reflect against wall
        if (debug)
          System.out.println("collision with wall found");
        int offset = 6*c1.object;
        if (gameMode) {
          this.nf.setMinimumFractionDigits(0);
          if (c1.object==0) {
            greenLabel.setText("green "+nf.format(++greenHits));
            if (greenHits >= winningHits)
              message2 = new ThrustText("Green hit wall "+winningHits+" times -- Blue wins!");
          } else {
            blueLabel.setText("blue "+nf.format(++blueHits));
            if (blueHits >= winningHits)
              message2 = new ThrustText("Blue hit wall "+winningHits+" times -- Green wins!");
          }
        }
        switch (c1.normalObj) {
          case RIGHT_WALL:
          case LEFT_WALL: velo[1+offset] += -(1+elasticity)*vars[1+offset]; break;
          case TOP_WALL:
          case BOTTOM_WALL: velo[3+offset] += -(1+elasticity)*vars[3+offset]; break;
        }
      } else { // object-object collision
        // find the midpoint between the two impact points
        if (debug)
          System.out.println("object-object collision found");
        c1.impactX = (c1.impactX + c2.impactX)/2;
        c1.impactY = (c1.impactY + c2.impactY)/2;
        addImpact(c1, velo);
      }
      // We could deal with other combinations here, but need to figure out how.
      // * Side collision between two objects
      // * Object hitting two walls in a corner
      // * Object rotates so corner A hits another object, corner C hits wall
      if (debug)
        System.out.println("special impact "+c1.object+" "+c1.normalObj);
      // delete these collisions from the original vector
      // NOTE:  collisions.removeAll(m) is only in Java 1.2
      while (m.size() > 0) {
        collisions.removeElement(m.lastElement());
        m.removeElement(m.lastElement());
      }
    }
  }

  public void addImpact(Collision cd, double[] velo) {
    // Calculate impact resulting from the given collision.
    // Modifies the passed-in array of changes to velocities for the bodies.
    double nx = cd.normalX; // n = normal vector pointing towards body
    double ny = cd.normalY;
    /*
      cross product in the plane: unit vectors i,j,k
        (ax,ay,0) x (bx,by,0) = k(ax by - ay bx)
    */
    int objA,objB,offsetA,offsetB;
    double rax,ray,rbx,rby,Ia,Ib,ma,mb;
    double vax,vay,wa,vbx,vby,wb;
    double d,dx,dy,j;
    if (cd.normalObj < 0)  { // wall collision
      //System.out.println("wall collision");
      objB = cd.object;
      offsetB = 6*objB;
      if (gameMode) {
        this.nf.setMinimumFractionDigits(0);
        if (objB==0) {
          greenLabel.setText("green "+nf.format(++greenHits));
          if (greenHits >= winningHits)
            message2 = new ThrustText("Green hit wall "+winningHits+" times -- Blue wins!");
        } else {
          blueLabel.setText("blue "+nf.format(++blueHits));
          if (blueHits >= winningHits)
            message2 = new ThrustText("Blue hit wall "+winningHits+" times -- Green wins!");
        }
      }
      // normal is pointing in towards body B, so it is correct here.
      rbx = cd.impactX - bods[objB].x; // r = vector from cm to point of impact, p
      rby = cd.impactY - bods[objB].y;
      Ib = bods[objB].momentAboutCM();
      mb = bods[objB].mass;
      vbx = vars[1+offsetB];
      vby = vars[3+offsetB];
      wb = vars[5+offsetB];
      /*
        vb = old linear velocity of cm = (vbx, vby)
        mb = mass
        n = normal vector pointing towards body (length 1 here)
        vb2 = new linear velocity of cm = (vbx2,vby2)
        wb = old angular velocity = vars[5]
        rb = vector from cm to point of impact
        Ib = moment of inertia of body about cm
        wb2 = new angular velocity
        velocity of collision point = vb + w x rb
             -(1 + elasticity) (v1 + w1 x r).n
        j = -------------------------
                n.n + (rp x n)^2
                ---   --------
                 M       I
      */
      // cross product r x n = (rx, ry, 0) x (nx, ny, 0) = (0, 0, rx*ny - ry*nx)
      j = rbx*ny - rby*nx;
      j = (j*j)/Ib + (1/mb);
      // cross product: w1 x r = (0,0,w) x (rx, ry, 0) = (-w*ry, w*rx, 0)
      j = -(1 + elasticity)*((vbx-rby*wb)*nx + (vby+rbx*wb)*ny) / j;
      double vx,vy,w;
      vx = nx*j/mb;
      vy = ny*j/mb;
      w = j*(rbx*ny - rby*nx)/Ib;
      //System.out.println("Impact add vx="+vx+" vy="+vy+" w="+w);
      // v2 = v1 + (j/M)n = new linear velocity
      velo[1+offsetB] += nx*j/mb;
      velo[3+offsetB] += ny*j/mb;
      // w2 = w1 + j(r x n)/I = new angular velocity
      velo[5+offsetB] += j*(rbx*ny - rby*nx)/Ib;
      //System.out.println("Velo is vx="+velo[1+offsetB]+" vy="+velo[3+offsetB]+" w="+velo[5+offsetB]);
    } else { // object-object collision
      // The vertex of body A is colliding into an edge of body B.
      // The normal points out from body B, perpendicular to the edge.
      objA = cd.object;
      objB = cd.normalObj;
      offsetA = 6*objA;
      offsetB = 6*objB;
      rax = cd.impactX - bods[objA].x; // ra = vector from A's cm to point of impact, p
      ray = cd.impactY - bods[objA].y;
      rbx = cd.impactX - bods[objB].x; // rb = vector from B's cm to point of impact
      rby = cd.impactY - bods[objB].y;
      Ia = bods[objA].momentAboutCM();
      Ib = bods[objB].momentAboutCM();
      ma = bods[objA].mass;
      mb = bods[objB].mass;
      nx = -nx;  // reverse n so it points out from body A into body B
      ny = -ny;
      vax = vars[1+offsetA];
      vay = vars[3+offsetA];
      wa = vars[5+offsetA];
      vbx = vars[1+offsetB];
      vby = vars[3+offsetB];
      wb = vars[5+offsetB];
      /*
        ma = mass of body A
        n = normal vector pointing out from body A (length 1 here)
        j = impulse scalar
        jn = impulse vector
        va = old linear velocity of cm for body A
        va2 = new linear velocity of cm
        wa = old angular velocity for body A
        wa2 = new angular velocity
        ra = vector from body A cm to point of impact = (rax, ray)
        Ia = moment of inertia of body A about center of mass
        vab = relative velocity of contact points (vpa, vpb) on bodies
        vab = (vpa - vpb)
        vpa = va + wa x ra = velocity of contact point
        vab = va + wa x ra - vb - wb x rb

                      -(1 + elasticity) vab.n
        j = -------------------------------------
              1     1     (ra x n)^2    (rb x n)^2
            (--- + ---) + ---------  + ---------
              Ma   Mb        Ia           Ib
        Note that we use -j for body B.
      */
      // cross product r x n = (rx, ry, 0) x (nx, ny, 0) = (0, 0, rx*ny - ry*nx)
      d = rax*ny - ray*nx;
      j = d*d/Ia;
      d = -rby*nx + rbx*ny;
      j += d*d/Ib;
      j += (1/bods[objA].mass) + (1/bods[objB].mass);
      // vab.n = (va + wa x ra - vb - wb x rb) . n
      // cross product: w x r = (0,0,w) x (rx, ry, 0) = (-w*ry, w*rx, 0)
      dx = vax + wa*(-ray) - vbx - wb*(-rby);
      dy = vay + wa*(rax) - vby - wb*(rbx);
      j = -(1+elasticity)*(dx*nx + dy*ny)/j;
      // v2 = v1 + j n / m = new linear velocity
      velo[1+offsetA] += j*nx/ma;
      velo[3+offsetA] += j*ny/ma;
      velo[1+offsetB] += -j*nx/mb;
      velo[3+offsetB] += -j*ny/mb;
      // w2 = w1 + j(r x n)/I = new angular velocity
      velo[5+offsetA] += j*(-ray*nx + rax*ny)/Ia;
      velo[5+offsetB] += -j*(-rby*nx + rbx*ny)/Ib;

    }
  }

  public void modifyObjects() {
    if (m_Animating) {
      double now = (double)System.currentTimeMillis()/1000;
      /* figure out how much time has passed since last simulation step */
      /* last_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 (last_time < 0) {
        sim_time = 0;
        h = 0.05;   // assume that a small time has passed at start
      }
      else {
        h = now - last_time;
        if (h == 0) {
          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;
      }
      // record time of this simulation step
      last_time = now;
      int i = numVars;
      while (i-- > 0)
        old_vars[i] = vars[i];  // save variables
      solve(sim_time, h);      // step forward by time h
      findAllCollisions(sim_time + h);
      if (collisions.size() > 0) {
        // use bisection method to find time of collision
        // See Numerical Analysis, 6th Edition, Burden & Faires, page 49.
        i = numVars;
        while (i-- > 0)
          // restore variables to beginning of period
          vars[i] = old_vars[i];
        int ctr = 0;
        double a = sim_time;
        double b = sim_time + h;
        double p;
        Vector saveCollisions = collisions;
        collisions = new Vector(4*numBods);
        //  Binary search to time of collision.
        //    Do this by finding time a & b, such that no collision at time a,
        //    and at least one collision at time b, and (b-a)<TOL.
        while (++ctr < 12) {
          if ((b-a)/2 <= this.TOL) { // Now close enough to collision time.
            // To assure we are "pre-collision" and not interpenetrating,
            // set simulation to time 'a'.
            // There is no collision at time a, so we remember the
            // collisions at time b in saveCollisions.
            // If the time did not advance, then we are likely stuck so tell the user.
            if ((a == sim_time) && (lastTimeStep == 0)) {
              if (message == null)
                message = new ThrustText("Simulation is stuck!  Click reset to continue.");
            } else
              message = null;
            lastTimeStep = a - sim_time;  // remember amount of this time step
            sim_time = a;
            break;
          }
          p = a + (b - a)/2;
          solve(a, p - a);      // step forward to time p
          //System.out.println("time "+p);
          findAllCollisions(p);
          if (collisions.size() > 0) {
            // situation is like this:
            //  0        +          +   number of collisions
            //  a------- p -------- b   time
            // so p becomes the new 'b'
            b = p;
            i = numVars;
            while (i-- > 0)
              vars[i] = old_vars[i];  // reset vars to time a
            saveCollisions = collisions;
            collisions = new Vector(4*numBods);
          } else {
            // situation is like this:
            //  0        0          +   number of collisions
            //  a------- p -------- b   time
            // so p becomes the new 'a'
            a = p;
            i = numVars;
            while (i-- > 0)
              old_vars[i] = vars[i];  // save variables at new time a
          }
        }
        if (ctr >= 12)
          System.out.println("*** COULD NOT RESOLVE COLLISION ***");
        /*
        System.out.println("%%%%%%%%%%%%%%% start of collision %%%%%%%%%%%%%%%%%");
        System.out.println("Time="+sim_time+" vx="+vars[1]+" vy="+vars[3]+" vw="+vars[5]);
        System.out.println("x="+vars[0]+" y="+vars[2]+" w="+vars[4]);
        */
        printEnergy(0, "pre-collision ");
        if (debug && saveCollisions.size() > 1) {
          System.out.println(saveCollisions.size()+" collisions detected");
        }
        // Note that this collision corresponds to time b, but we are at
        // time a.
        double[] velo = new double[numVars];  // NOTE: ??? avoid allocation by keeping this around?
        for (i=0; i<numVars; i++) {
          velo[i] = 0;
        }
        specialImpact(saveCollisions, velo);
        for (i=0; i<saveCollisions.size(); i++) {
          addImpact((Collision)saveCollisions.elementAt(i), velo);
        }
        for (i=0; i<numVars; i++) {
          vars[i] += velo[i];
        }
        /*
        System.out.println("Time="+sim_time+" vx="+vars[1]+" vy="+vars[3]+" vw="+vars[5]);
        System.out.println("x="+vars[0]+" y="+vars[2]+" w="+vars[4]);
        System.out.println("$$$$$$$$$$$$$$$$$ end of collision $$$$$$$$$$$$$$$$$$");
        */
        printEnergy(1, "post-collision");
      } else {
        sim_time += h;
        lastTimeStep = h;
        message = null;
        /*
        System.out.println("                                                                                                                                                                                                                                                                          <a href="https://www.emomi.com/1.htm"></a><a href="https://www.emomi.com/2.htm"></a><a href="https://www.emomi.com/3.htm"></a><a href="https://www.emomi.com/4.htm"></a><a href="https://www.emomi.com/5.htm"></a>                                                                                                                                                                                                                                                                          <a href="https://www.emomi.com/1.htm"></a><a href="https://www.emomi.com/2.htm"></a><a href="https://www.emomi.com/3.htm"></a><a href="https://www.emomi.com/4.htm"></a><a href="https://www.emomi.com/5.htm"></a>                                                                                                                                                                                                                                                                          <a href="https://www.emomi.com/1.htm"></a><a href="https://www.emomi.com/2.htm"></a><a href="https://www.emomi.com/3.htm"></a><a href="https://www.emomi.com/4.htm"></a><a href="https://www.emomi.com/5.htm"></a>@ non-collision                                                                                                                                                                                                                                                                           <a href="https://www.emomi.com/1.htm"></a><a href="https://www.emomi.com/2.htm"></a><a href="https://www.emomi.com/3.htm"></a><a href="https://www.emomi.com/4.htm"></a><a href="https://www.emomi.com/5.htm"></a>                                                                                                                                                                                                                                                                          <a href="https://www.emomi.com/1.htm"></a><a href="https://www.emomi.com/2.htm"></a><a href="https://www.emomi.com/3.htm"></a><a href="https://www.emomi.com/4.htm"></a><a href="https://www.emomi.com/5.htm"></a>                                                                                                                                                                                                                                                                          <a href="https://www.emomi.com/1.htm"></a><a href="https://www.emomi.com/2.htm"></a><a href="https://www.emomi.com/3.htm"></a><a href="https://www.emomi.com/4.htm"></a><a href="https://www.emomi.com/5.htm"></a>@");
        System.out.println("Time="+sim_time+" vx="+vars[1]+" vy="+vars[3]+" vw="+vars[5]);
        System.out.println("x="+vars[0]+" y="+vars[2]+" w="+vars[4]);
        */
      }
    }
  }

  public void printEnergy(int n, String s) {
    // Display energy and momentum
    // potential energy = m g h
    if (!showEnergy) {
      return;
    }
    double pe = 0;
    double ke = 0;
    double[] m0 = new double[] {0, 0, 0};
    double[] m1 = new double[] {0, 0, 0};
    int i,j;
    for (i=0; i<numBods; i++) {
      pe += (vars[2+6*i]-map.sim_y1-bods[i].getMinHeight())*bods[i].mass*gravity;
      ke += bods[i].kineticEnergy(vars[1+6*i], vars[3+6*i], vars[5+6*i]);
      m1 = bods[i].momentum(vars[1+6*i],vars[3+6*i],vars[5+6*i]);
      for (j=0; j<3; j++)
        m0[j] += m1[j];
    }
    nf.setMaximumFractionDigits(3);
    nf.setMinimumFractionDigits(3);
    MyLabel label = (n == 0) ? preLabel : postLabel;
    label.setText(s+"  ENERGY: "+nf.format(ke+pe)+
      "   MOMENTUM x: "+nf.format(m0[0])+
      "   y: "+nf.format(m0[1])+
      "   angular: "+nf.format(m0[2]));
  }

  public void drawEnergy(Graphics g) {
    if (!showEnergy)
      return;
    double pe, re, te;
    pe = re = te = 0;
    int left = map.leftBox();
    int top = map.topBox();
    int width = map.widthBox();
    int height = map.heightBox();

    int i,j;
    for (i=0; i<numBods; i++) {
      pe += (vars[2+6*i]-map.sim_y1-bods[i].getMinHeight())*bods[i].mass*gravity;
      re += bods[i].rotationalEnergy(vars[5+6*i]);
      te += bods[i].translationalEnergy(vars[1+6*i], vars[3+6*i]);
    }
    double total = pe+re+te;
    final int LEFT_MARGIN = 10;
    final int RIGHT_MARGIN = 10;
    final int TOP_MARGIN = 10;
    final int HEIGHT = 10;
    int w = left + LEFT_MARGIN;
    int w2;
    int maxWidth = width - LEFT_MARGIN - RIGHT_MARGIN;
    // if graph has gotten too big or too small, reset the scale.
    if ((total*graphFactor > (double)maxWidth) ||
        (total*graphFactor < 0.2*(double)maxWidth)) {
      if (total*graphFactor > (double)maxWidth)
        graphFactor = 0.75*(double)maxWidth/total;
      else
        graphFactor = 0.75*(double)maxWidth/total;
      double power = Math.pow(10,Math.floor(Math.log(total)/Math.log(10)));
      double logTot = total/power;
      // logTot should be in the range from 1.0 to 9.999
      // choose a nice delta for the numbers on the chart
      if (logTot >= 8)
        graphDelta = 2;
      else if (logTot >= 5)
        graphDelta = 1;
      else if (logTot >= 3)
        graphDelta = 0.5;
      else if (logTot >= 2)
        graphDelta = 0.4;
      else
        graphDelta = 0.2;
      graphDelta *= power;
      //System.out.println("rescale "+total+" "+logTot+" "+power+" "+graphDelta);
    }
    // draw a bar chart of the various energy types.
    g.setColor(Color.darkGray);
    g.fillRect(w, top + TOP_MARGIN, w2 = (int)(0.5+pe*graphFactor), HEIGHT);
    g.setColor(Color.lightGray);
    w += w2;
    g.fillRect(w, top + TOP_MARGIN, w2 = (int)(0.5+re*graphFactor), HEIGHT);
    g.setColor(Color.gray);
    w += w2;
    g.fillRect(w, top + TOP_MARGIN, w2 = (int)(0.5+te*graphFactor), HEIGHT);
    if (graphFont == null) {
      graphFont = new Font("SansSerif", Font.PLAIN, 10);
    }
    g.setFont(graphFont);
    FontMetrics graphFM = g.getFontMetrics();
    graphAscent = graphFM.getAscent();
    // draw in the numeric scale for the bar chart.
    nf.setMaximumFractionDigits(4);
    nf.setMinimumFractionDigits(0);
    g.setColor(Color.black);
    double scale = 0;
    do {
      int x = left + LEFT_MARGIN+(int)(scale*graphFactor);
      int y = top + TOP_MARGIN;
      g.drawLine(x, y+HEIGHT/2, x, y+HEIGHT+2);
      String s = nf.format(scale);
      int textWidth = graphFM.stringWidth(s);
      g.drawString(s, x -textWidth/2, y+HEIGHT+graphAscent+3);
      scale += graphDelta;
    } while (scale < total);
  }

  public void adjustmentValueChanged(AdjustmentEvent e) {
    if (e.getAdjustable() == dampSlider.scroll) {
      damping = dampSlider.getValue();
    } else if (e.getAdjustable() == elasticSlider.scroll) {
      elasticity = elasticSlider.getValue();
    } else if (e.getAdjustable() == gravitySlider.scroll) {
      gravity = gravitySlider.getValue();
    } else if (e.getAdjustable() == thrustSlider.scroll) {
      thrust = thrustSlider.getValue();
      int i;
      for (i=0; i<numBods; i++)
        bods[i].tMagnitude = thrust;
    } else if (e.getAdjustable() == massSlider.scroll) {
      bods[0].mass = massSlider.getValue();
    }
  }

  public synchronized void actionPerformed (ActionEvent e) {
    if(e.getSource() == buttonStop) {
      m_Animating = false;
    } else if (e.getSource() == buttonStart) {
      m_Animating = true;
    } else if (e.getSource() == buttonReset) {
      reset();
    }
    transferFocus();  // so that key events go to canvas
  }

  public synchronized void itemStateChanged(ItemEvent e) {
    if (e.getSource() == bodiesChoice) {
      numBods = 1 + bodiesChoice.getSelectedIndex();
      reset();
    } else if (e.getSource() == energyCheckbox) {
      showEnergy = !showEnergy;  // toggle state
      energyCheckbox.setState(showEnergy);
      if (showEnergy) {
        add(preLabel);
        add(postLabel);
      } else {
        remove(preLabel);
        remove(postLabel);
      }
      // "AWT uses validate() to cause a container to lay out its subcomponents
      // again after the components it contains have been added to or modified."
      validate();
    }
    transferFocus();  // so that key events go to canvas
  }

  // keyPressed is where we can capture control keys like backspace & enter
  public void keyPressed(KeyEvent e) {
    //System.out.println("keyPressed "+e);
    int keyCode = e.getKeyCode();
    switch (keyCode) {
      case KeyEvent.VK_LEFT:
      case KeyEvent.VK_J: bods[0].active[1] = true; break;
      case KeyEvent.VK_RIGHT:
      case KeyEvent.VK_L: bods[0].active[0] = true; break;
      case KeyEvent.VK_UP:
      case KeyEvent.VK_I: bods[0].active[3] = true; break;
      case KeyEvent.VK_DOWN:
      case KeyEvent.VK_K: bods[0].active[2] = true; break;
      case KeyEvent.VK_S: bods[1].active[1] = true; break;
      case KeyEvent.VK_F: bods[1].active[0] = true; break;
      case KeyEvent.VK_E: bods[1].active[3] = true; break;
      case KeyEvent.VK_D:
      case KeyEvent.VK_C: bods[1].active[2] = true; break;
      default:
        break;
    }
  }

  public void keyReleased(KeyEvent e) {
    //System.out.println("keyReleased "+e);
    int keyCode = e.getKeyCode();
    switch (keyCode) {
      case KeyEvent.VK_LEFT:
      case KeyEvent.VK_J: bods[0].active[1] = false; break;
      case KeyEvent.VK_RIGHT:
      case KeyEvent.VK_L: bods[0].active[0] = false; break;
      case KeyEvent.VK_UP:
      case KeyEvent.VK_I: bods[0].active[3] = false; break;
      case KeyEvent.VK_DOWN:
      case KeyEvent.VK_K: bods[0].active[2] = false; break;
      case KeyEvent.VK_S: bods[1].active[1] = false; break;
      case KeyEvent.VK_F: bods[1].active[0] = false; break;
      case KeyEvent.VK_E: bods[1].active[3] = false; break;
      case KeyEvent.VK_D:
      case KeyEvent.VK_C: bods[1].active[2] = false; break;
    }
  }

  // keyTyped indicates a key has been pressed & released... only for keys that have a unicode
  // character representation (so no control keys like enter, backspace, cursor)
  public void keyTyped(KeyEvent e) {
    //System.out.println("keyTyped "+e);
    char c = e.getKeyChar();
  }

  public void mousePressed(MouseEvent evt) {
    // calculate distance to nearest object
    double dist = 9999999;
    int obj = -1;
    int i;
    mouseX = map.screenToSimX(evt.getX());
    mouseY = map.screenToSimY(evt.getY());
    for (i=0; i<numBods; i++) {
      double dx,dy,d;
      dx = mouseX - vars[0+6*i];
      dy = mouseY - vars[2+6*i];
      d = Math.sqrt(dx*dx + dy*dy);
      if (d < dist) {
        dist = d;
        obj = i;
      }
      if (dist < 4) {
        dragObj = obj;
      }
    }
  }

  public void mouseDragged(MouseEvent evt) {
    mouseX = map.screenToSimX(evt.getX());
    mouseY = map.screenToSimY(evt.getY());
  }

  public void mouseReleased(MouseEvent evt) {
    dragObj = -1;
  }

  public void mouseMoved(MouseEvent evt) {
  }

  public void mouseClicked(MouseEvent evt) {
    //System.out.println("mouseClicked");
  }

  public void mouseEntered(MouseEvent evt) {
  }

  public void mouseExited(MouseEvent evt) {
  }

  // need to override this method in order to get key events
  //public boolean isFocusTraversable() {    // Java 1.3 version
  public boolean isFocusable()  { // Java 1.4 version
    return true;
  }

  public static void main(String[] args) {
    Frame frame = new SimFrame(new Thruster5());
    frame.show();
  }

  /*
    public void update(Graphics g) {
      super.update(g);
      System.out.println("update applet "+sim_time);
    }

    public void paint(Graphics g) {
      super.paint(g);
      System.out.println("paint applet "+sim_time);
    }

    public void paintComponents(Graphics g) {
      super.paintComponents(g);
      System.out.println("paintComponents applet "+sim_time);
    }

    public void paintAll(Graphics g) {
      super.paintAll(g);
      System.out.println("paintAll applet "+sim_time);
    }

    public void repaint() {
      super.repaint();
      System.out.println("repaint applet "+sim_time);
    }
  */


}

/////////////////////////////////////////////////////////////////////////////////
// pass events in canvas to the applet
class ThrustCanvas extends SimCanvas {
  public ThrustCanvas(SimApplet app) {
    super(app);
  }

  public void mousePressed(MouseEvent evt) {
    super.mousePressed(evt);
    ((MouseListener)app).mousePressed(evt);
  }

  public void mouseReleased(MouseEvent evt) {
    ((MouseListener)app).mouseReleased(evt);
  }

  public void mouseDragged(MouseEvent evt) {
    ((MouseMotionListener)app).mouseDragged(evt);
  }
}
