/* Lab.java */
/*
    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

*/

/*
Order to compile java files:
  1. Simulation.java  = base classes, elements
  2. sim1.java = particular simulations
  3. Lab.java = user interface stuff
 */

import java.applet.*;
import java.net.URL;
import java.awt.*;
import java.awt.event.*;
import java.util.Enumeration;
import java.util.Vector;
import java.util.StringTokenizer;
import java.text.NumberFormat;
import java.io.*;
import java.util.Iterator;


/////////////////////////////////////////////////////////////////////////////
// CControl class
//   user interface control
/* a control is hooked up to a canvas(eg. sim or graph) when it is constructed.
   control gets parameter names & values from canvas at update time.
   control area of screen is set... how?
 */

class CControl extends CCanvas
{
  /* line_height is a cludge... here's why:  I want to know how many
    lines the control needs.  But a font only tells its dimensions when we have a graphics device.  But that happens long after the layout is set up.
    We can perhaps ask to re-layout if we discover the line_height is different?
    */
  public int line_height = 10; // WARNING: only accurate after draw()
  private int columns = 1;  // number of columns
  private CCanvas canvas = null;  // the canvas being controlled
  private Font myFont = null;
  private FontMetrics myFM = null;
  private int ascent = 20;
  private int descent = 10;
  private int leading = 5;
  private NumberFormat nf = null;
  private int selected = -1;   // index of selected control
  private String keybuf = "";  // typing input buffer
  private ControlTimer myTimer = null;

//------------------- private classes ----------------------------------------
/* If user doesn't hit 'enter' key after a few seconds, then we do it for them
   in this thread.
   Whenever a field is being editted, create a ControlTimer object.  Whenever a key is
   pressed, tell ControlTimer to hit 'enter' key in 5 seconds.
 */

/*
  ControlTimer Thread class
  Make an instance, start it running with a long sleep time (eg. 60 seconds)
  the renew method should interrupt the thread and set a flag telling it to renew.
  the cancel method should interrupt and set flag to cancel.
  On interrupt, check the flag and do one or the other.
*/

private class ControlTimer extends Thread
{
  public int action = 1;   // 0 means continue, 1 means terminate
  public long delay = 50000;

  ControlTimer()
  {
    super("ControlTimer Thread");
    setDaemon(true);  // so that this thread doesn't prevent us from quitting the app
  }

  public void run()
  {
    //System.out.println("ControlTimer starting");
    do
    {
      try
      {
        {
          //System.out.println("ControlTimer going to sleep for "+delay);
          sleep(delay);
          // hit 'enter' for the lazy slow user.
          //System.out.println("ControlTimer is checking key buf");
          CControl.this.checkKeyBuf();
          action = 1;
        }
      }
      catch(InterruptedException e)
      {
        // when we are interrupted, we just go back to sleep (if action == 0)
        //System.out.println("ControlTimer interrupted, action == "+action);
      }
    } while (action != 1);
    //System.out.println("ControlTimer is quitting");
  }
}

//-----------------  CControl Class ----------------------------------------
CControl(Applet app, CCanvas cnvs, int cols)  // constructor
{
  super(app);
  name = "control";

  columns = cols;
  canvas = cnvs;

  // assume a column is 10 wide
  // CoordMap inputs are direction, x1, x2, y1, y2, align_x, align_y
  map = new CoordMap(CoordMap.INCREASE_DOWN, 0, 10*cols, 0, 10, CoordMap.ALIGN_MIDDLE, CoordMap.ALIGN_MIDDLE);

  map.setFillScreen(true);
}

public boolean acceptsKeyEvents()
{
  return true;
}

// returns needed height of control in terms of pixels.
// NOTE WARNING:  line_height is only accurate after draw() has been called
public int height()
{
  // add 3 for extra space below bottom line
  return 3 + line_height*
  (int)Math.ceil((double)canvas.params.length/(double)columns);
}

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);
  // could include code here to redo layout if new line_height is bigger than previous
  if (line_height != ascent+descent+leading)
  {
    line_height = ascent+descent+leading;
    ((Lab)my_applet).redoLayout();
    System.out.println("control line height changed "+line_height);
  }
}

public void draw(Graphics g)
{
  int i;
  super.draw(g);

  setFont(g);
  g.setFont(myFont);

  // draw name of each parameter in current column, until full
  g.setColor(Color.black);

  if (canvas == null)
    return;
  if (canvas.params == null)
    return;
  int column = 0;
  // for each parameter
  int x = map.screen_left + 10;
  int y = map.screen_top + ascent + leading;
  for (i=0; i<canvas.params.length; i++)
  {
    //int w = myFM.stringWidth(s);
    if (y+descent > map.screen_top + map.screen_height) // column full
    {
      x += map.screen_width/columns;
      y = map.screen_top + ascent + leading;
    }

    // draw the text & value of parameter
    if (selected == i)
    {
      g.setColor(Color.red);
      if (keybuf.length() > 0)
        g.drawString(canvas.params[i] + ": " + keybuf, x, y);
      else
        g.drawString(canvas.params[i] + ": " + nf.format(canvas.get(i)), x, y);
    }
    else
    {
      g.setColor(Color.black);
      g.drawString(canvas.params[i] + ": " + nf.format(canvas.get(i)), x, y);
    }

    // draw a box around selected guy
    if (selected == i)
    {
      g.setColor(Color.red);
      g.drawRect(x-8, y-ascent-leading/2, map.screen_width/columns-3,
        ascent+descent+leading/2);
    }
    // advance to next line
    y += ascent + descent + leading;
  }

  g.setColor(Color.green);
  for (i=0; i<columns; i++)
  {
    int x1 = map.simToScreenX(i*10);
    int y1 =  map.simToScreenY(0);
    int x2 =  map.simToScreenX(10*(i+1));
    int y2 = map.simToScreenY(10);
    g.drawRect(x1, y1, x2-x1, y2-y1);
    //g.drawLine(x1, y1, x2, y2);
    //g.drawLine(x1, y2, x2, y1);
  }
}

// reset the timer so that the timer task doesn't execute until 5 seconds from now.
private void renewTimer()
{
  if (myTimer != null)
  {
    try
    {
      myTimer.action = 0;  // tells thread to continue sleeping
      myTimer.delay = 5000;  // time to sleep in milliseconds
      myTimer.interrupt();  // schedule with delay given in milliseconds
    }
    catch (SecurityException e1)
    { //if the current thread cannot modify this thread..
    }
  }
}

private void killTimer()
{
  if (myTimer != null)
  {
    try
    {
      myTimer.action = 1;  // tells thread to quit
      myTimer.interrupt();  // schedule with delay given in milliseconds
      myTimer = null;
    }
    catch (SecurityException e1)
    { //if the current thread cannot modify this thread..
    }
  }
}

public void mousePressed(MouseEvent evt)
{

  //System.out.println("mouse click in control "+evt.getX() + " "+evt.getY());
  // which column?
  int col = (evt.getX() - map.screen_left)/(map.screen_width/columns);
  // which line?
  int line = (evt.getY() - map.screen_top)/(ascent + leading + descent);
  int lines_per_col = map.screen_height/(ascent+leading+descent); //?
  int click_on = line + col*lines_per_col;
  //System.out.println("col "+col+" line "+line+" lines_per_col "+
  //  lines_per_col+" click_on "+click_on);

  if (click_on >= canvas.params.length)
  {
    checkKeyBuf();
    selected = -1;  // nothing selected
  }
  // click on a new or different cell
  else if (click_on != selected)
  {
    checkKeyBuf();
    selected = click_on;
    if (myTimer == null)
    {
      myTimer = new ControlTimer();
      myTimer.start();
    }
  }
  else  // if we click on the currently selected cell, just renew the timer.
  {
    renewTimer();
  }
  repaint();
  // if canvas has a pointer to Lab, then could repaint here!!!
}

// keyPressed is where we can capture control keys like backspace & enter
public void keyPressed(KeyEvent e)
{
  int keyCode = e.getKeyCode();
  if (keyCode == KeyEvent.VK_BACK_SPACE) // backspace
  {
    if (keybuf.length() >=1)
    {
      keybuf = keybuf.substring(0, keybuf.length()-1);
      repaint();
    }
    renewTimer();
  }
  else if (keyCode == KeyEvent.VK_ENTER) // enter key
    checkKeyBuf();
}

// 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)
{
  char c = e.getKeyChar();
  if (selected < 0)
    return;  // ignore if no one selected
  if (((c>='0')&&(c<='9')) || (c=='.') || (c=='-'))
  {
    // append the character to the keybuf
    keybuf = keybuf + c;
    repaint();
    renewTimer();
  }
}

// updates the variable from what was typed in to keybuf
// and deselects the parameter control.
void checkKeyBuf()
{
  if ((selected >= 0) && (keybuf.length() > 0))
  {
    // turn the keybuf into a double
    try
    {
      double dd = (new Double(keybuf)).doubleValue();
      // might want to put a min/max check here...
      canvas.set(selected, dd);
    }
    catch (NumberFormatException e)
    {
    }
  }
  // set so that nobody is selected
  keybuf = "";
  selected = -1;
  repaint();
  killTimer();  // destroy any outstanding timer & task
}

} // end CControl class

/////////////////////////////////////////////////////////////////////////////
// LabTimer class
class LabTimer extends Thread
{
private Vector myCanvas;
private long delay = 33;
private boolean firstTime = true;
private boolean suspendRequested = false;

  LabTimer(long dly)
  { super("LabTimer Thread");
    myCanvas = new Vector(10);
    delay = dly;
  }

  public void run()
  { try
    { while (!interrupted()) // loop until interrupted
      {
        checkSuspended();
        // repaint all canvas's that want repainting...
        int i;
        int n = myCanvas.size();
        for (i=0; i<n; i++)
        {
          CCanvas c = (CCanvas)myCanvas.elementAt(i);
          c.repaint();
        }
        if (firstTime)  // let things settle down before starting
        {
          sleep(100); // milliseconds
          firstTime = false;
        } else
          sleep(delay);  // milliseconds
      }
    }
    catch(InterruptedException e)
    { System.out.println("LabTimer thread interrupted.");
    }
  }

  public void add(CCanvas c)
  {
    myCanvas.addElement(c);
  }

  public void requestSuspend()
  {
    suspendRequested = true;
  }

  private synchronized void checkSuspended()
    throws InterruptedException
  {
    while (suspendRequested)
      wait();
  }

  public synchronized void requestResume()
  {
    suspendRequested = false;
    notify();
  }

} // end LabTimer class


/////////////////////////////////////////////////////////////////////////////
// Lab class

public class Lab extends Applet
  implements MouseListener, MouseMotionListener,ComponentListener,
  KeyListener
{
// long lasting objects
private LabLayout theLayout = null;
public LabTimer timer = null;
private Vector  cnvs;  // list of canvas's (sim, graph, controls, ...)
private Image offscreen = null;
public boolean firstTime = true;  // true only during startup

// temp variables
private boolean verbose = false;   // debugging variable
private CCanvas dragCanvas = null; // the canvas we are dragging within
private CCanvas keyCanvas = null;  // the canvas getting key events (focus)


// --------------------------------------------------------------------
// why inner class for LabLayout?  To keep all the cruft from spreading!
/*
   ======= L A W S   O F    L A Y O U T  ===============
   1. A layout number specifies a certain arrangement of canvas's (sim, graph, control).

   2. The canvas (sim, graph or control) should exist before the layout operates on them, they are stored in Lab variables like sim1, graph1, control1, ...

   3. Only the layout may add or remove a canvas (sim, graph, control) from the list of canvas's.  Exceptions?  when a canvas is destroyed.

   ======== L A Y O U T    D E B A T E ===================
   1.  Should the layout create default objects if they don't exist yet?  Like a sim or graph or control?  This would make it easier to set things up, you wouldn't need to create all these things beforehand.

*/
private class LabLayout  //  inner class
{
private int layout_num = 0;
private int ctrl1_cols = 3;  // number of control columns
private String style = "";
private CSimulation sim1 = null;
private CPhase graph1 = null;
private CControl ctrl1 = null;
private CSimulation sim2 = null;
private CPhase graph2 = null;
private CControl ctrl2 = null;

// NOTE use "Lab.this" to refer to enclosing object

  LabLayout()
  {
  }
  /* NOTE:  if actually try to change/add/delete a sim while timer is running,
     you will have to stop & restart the timer, or at least get at its list
     of things that need repainting!!!
   */
  public void setSim1(String name)
  {
    System.out.println("setSim1 "+name);
    if ("springMass1".equalsIgnoreCase(name))
      sim1 = new CSpringMass1(Lab.this);
    else if ("springMass2".equalsIgnoreCase(name))
      sim1 = new CSpringMass2(Lab.this);
    else if ("pendulum1".equalsIgnoreCase(name))
      sim1 = new CPendulum1(Lab.this);
    else if ("spring2d".equalsIgnoreCase(name))
      sim1 = new CSpring2D(Lab.this);
    else if ("doubleSpring2d".equalsIgnoreCase(name))
      sim1 = new CDoubleSpring2D(Lab.this);
    else if ("collideSpring1".equalsIgnoreCase(name))
      sim1 = new CCollideSpring1(Lab.this);
    else if ("pendulum_cart".equalsIgnoreCase(name))
      sim1 = new CPendulumCart(Lab.this);
    else if ("dangle_stick".equalsIgnoreCase(name))
      sim1 = new CDangleStick(Lab.this);
    else if ("double_pendulum".equalsIgnoreCase(name))
      sim1 = new CDoublePendulum(Lab.this);
    else if ("molecule1".equalsIgnoreCase(name))
      sim1 = new CMolecule1(Lab.this);
    else if ("molecule2".equalsIgnoreCase(name))
      sim1 = new CMolecule3(Lab.this, 2);
    else if ("molecule3".equalsIgnoreCase(name))
      sim1 = new CMolecule3(Lab.this, 3);
    else if ("molecule4".equalsIgnoreCase(name))
      sim1 = new CMolecule3(Lab.this, 4);
    else if ("molecule5".equalsIgnoreCase(name))
      sim1 = new CMolecule3(Lab.this, 5);
    else if ("molecule6".equalsIgnoreCase(name))
      sim1 = new CMolecule3(Lab.this, 6);
    else if ("RollerSimpleHump".equalsIgnoreCase(name))
      sim1 = new CRoller1(Lab.this, 0);
    else if ("RollerSimpleLoop".equalsIgnoreCase(name))
      sim1 = new CRoller1(Lab.this, 1);
    else if ("RollerSimpleCircle".equalsIgnoreCase(name))
      sim1 = new CRoller1(Lab.this, 2);
    else if ("RollerSimpleInfinity".equalsIgnoreCase(name))
      sim1 = new CRoller1(Lab.this, 4);
    else if ("RollerSimpleOval".equalsIgnoreCase(name))
      sim1 = new CRoller1(Lab.this, 5);
    else if ("RollerSimpleSpiral".equalsIgnoreCase(name))
      sim1 = new CRoller1(Lab.this, 6);
    else if ("RollerSpringHump".equalsIgnoreCase(name))
      sim1 = new CRoller2(Lab.this, 0);
    else if ("RollerSpringLoop".equalsIgnoreCase(name))
      sim1 = new CRoller2(Lab.this, 1);
    else if ("RollerSpringCircle".equalsIgnoreCase(name))
      sim1 = new CRoller2(Lab.this, 2);
    else if ("RollerSpringInfinity".equalsIgnoreCase(name))
      sim1 = new CRoller2(Lab.this, 4);
    else if ("RollerSpringOval".equalsIgnoreCase(name))
      sim1 = new CRoller2(Lab.this, 5);
    else if ("RollerSpringSpiral".equalsIgnoreCase(name))
      sim1 = new CRoller2(Lab.this, 6);
    else if ("RollerDoubleHump".equalsIgnoreCase(name))
      sim1 = new CRoller3(Lab.this, 0);
    else if ("RollerDoubleLoop".equalsIgnoreCase(name))
      sim1 = new CRoller3(Lab.this, 1);
    else if ("RollerDoubleCircle".equalsIgnoreCase(name))
      sim1 = new CRoller3(Lab.this, 2);
    else if ("RollerDoubleInfinity".equalsIgnoreCase(name))
      sim1 = new CRoller3(Lab.this, 4);
    else if ("RollerDoubleOval".equalsIgnoreCase(name))
      sim1 = new CRoller3(Lab.this, 5);
    else if ("RollerDoubleSpiral".equalsIgnoreCase(name))
      sim1 = new CRoller3(Lab.this, 6);
    else if ("RollerFlightHump".equalsIgnoreCase(name))
      sim1 = new CRoller4(Lab.this);
    else if ("StringFlat".equalsIgnoreCase(name))
      sim1 = new CString(Lab.this, CString.FLAT);
    else if ("StringTriangle".equalsIgnoreCase(name))
      sim1 = new CString(Lab.this, CString.TRIANGLE);
    else if ("StringQuarterTri".equalsIgnoreCase(name))
      sim1 = new CString(Lab.this, CString.QUARTER_TRI);
    else if ("StringSquarePulse".equalsIgnoreCase(name))
      sim1 = new CString(Lab.this, CString.SQUARE_PULSE);
    else if ("StringSinePulse".equalsIgnoreCase(name))
      sim1 = new CString(Lab.this, CString.SINE_PULSE);
    else if ("StringHalfSinePulse".equalsIgnoreCase(name))
      sim1 = new CString(Lab.this, CString.HALF_SINE_PULSE);
    else if ("StringFancySine".equalsIgnoreCase(name))
      sim1 = new CString(Lab.this, CString.FANCY_SINE);
    else {
      System.out.println("default sim");
      sim1 = new CSpringMass1(Lab.this);  // default (good idea?)
    }
    // set target of graph
    if (graph1 != null)
      graph1.target(sim1);
    // recreate the controls
    makeControls();

  }

  public void setGraph1(String graphName)
  {
    System.out.println("setGraph1 "+graphName);
    if (graphName == null)
      return;
    if (graph1 != null)
    {
      graph1.disconnect();
      cnvs.removeElement(graph1);
    }
    if ("phase".equalsIgnoreCase(graphName))
      graph1 = new CPhase(Lab.this);
    else
      graph1 = null;
    if (graph1 != null)
    {
      // assume its target is sim1
      if (sim1 != null)
        graph1.target(sim1);
    }
  }

  public void setGraph1Target(String name)
  {
    System.out.println("setGraph1Target "+name);
    if (name == null)
      return;
    if (name.length() == 0)
      return;
    // what if sim1 or sim2 are null?
    if (graph1 == null)
      return;
    if ("simulation1".equalsIgnoreCase(name))
      graph1.target(sim1);
    else if ("simulation2".equalsIgnoreCase(name))
      graph1.target(sim2);
  }

  public void setGraph1XVar(String name)
  {
    System.out.println("setGraph1XVar "+name);
    if (name == null)
      return;
    if (name.length() == 0)
      return;
    int var = Integer.parseInt(name);
    if (graph1 == null)
      return;
    graph1.setXVar(var);
    graph1.repaint();
  }

  public void setGraph1YVar(String name)
  {
    System.out.println("setGraph1YVar "+name);
    if (name == null)
      return;
    if (name.length() == 0)
      return;
    int var = Integer.parseInt(name);
    if (graph1 == null)
      return;
    graph1.setYVar(var);
    graph1.repaint();
  }

  public void setGraph1Mode(String s)
  {
    System.out.println("setGraph1Mode "+s);
    if (s == null)
      return;
    if (s.length() == 0)
      return;
    StringTokenizer t = new StringTokenizer(s, " ,");
    String tok = t.nextToken();
    int mode = 0;
    if (tok.equalsIgnoreCase("dots"))
      mode = CPhase.DOTS;
    else if (tok.equalsIgnoreCase("lines"))
      mode = CPhase.LINES;
    int size = 1; // default
    if (t.hasMoreTokens())
    {
      size = Integer.parseInt(t.nextToken());
      if (size<1 || size>10)
        size = 1;
    }
    graph1.setDrawMode(mode, size);
  }

  public void setGraph1AutoScale(String s)
  {
    System.out.println("setGraph1AutoScale "+s);
    if (s == null)
      return;
    if (s.length() == 0)
      return;
    graph1.setAutoScale("true".equalsIgnoreCase(s));


  }

  public void setSim1Param(int num, String s)
  {
    System.out.println("setSim1Param "+num+" "+s);
    if (s == null)
      return;
    if (s.length() == 0)
      return;
    double val = (new Double(s)).doubleValue();  // could get an exception!!
    sim1.set(num, val);
    ctrl1.repaint();  // ensure that params are updated
  }

  public void setSim1Var(int num, String s)
  {
    System.out.println("setSim1Var "+num+" "+s);
    if (s == null)
      return;
    if (s.length() == 0)
      return;
    double val = (new Double(s)).doubleValue();  // could get an exception!!
    sim1.setVar(num, val);
  }

  public void makeControls()
  {
    if (ctrl1_cols <= 0)  // to hide it, set to zero columns
    {
      // hide the existing control
      if (ctrl1 != null)
      { cnvs.removeElement(ctrl1);
        ctrl1 = null;
      }
    } else
    {
      // what if sim1 is null?
      ctrl1 = new CControl(Lab.this, sim1, ctrl1_cols);
    }
  }

  public void setControl1Columns(String name)
  {
    System.out.println("setControl1Columns "+name);
    if (name == null)
      return;
    if (name.length() == 0)
      return;
    int crtl1_cols = Integer.parseInt(name);
    makeControls();
  }

  public void layoutCanvas()
  {
    Dimension d = Lab.this.getSize();
    System.out.println("layoutCanvas "+layout_num);

    cnvs.removeAllElements();
    switch (layout_num)
    {
      case 0: {   // sim1 gets entire screen
        if (sim1 != null) {
          sim1.setScreen(0, 0, d.width, d.height);
          cnvs.addElement(sim1);
        }
        break;
      }
      case 1: {  // sim1 gets right half, graph1 gets left half
        if (sim1 != null) {
          sim1.setScreen(d.width/2, 0, d.width/2, d.height);
          cnvs.addElement(sim1);
        }
        if (graph1 != null) {
          graph1.setScreen(0, 0, d.width/2, d.height);
          cnvs.addElement(graph1);
        } else
         System.out.println("layout: graph1 is null");
        break;
      }
      case 2: { // ctrl1 at bottom, sim1 gets right half,
             //graph1 gets left half
        int h_ctrl = 0;
        // give the controls what they need
        if (ctrl1 != null) {
          h_ctrl = ctrl1.height();
          ctrl1.setScreen(0, d.height - h_ctrl, d.width, h_ctrl);
          cnvs.addElement(ctrl1);
        }
        // divide upper evenly
        if (sim1 != null) {
          sim1.setScreen(d.width/2, 0, d.width/2, d.height - h_ctrl);
          cnvs.addElement(sim1);
        }
        if (graph1 != null) {
          graph1.setScreen(0, 0, d.width/2, d.height - h_ctrl);
          cnvs.addElement(graph1);
        }
        break;
      }
      case 3: {  // sim1 gets top, ctrl1 gets bottom
        int h_ctrl = 0;
        // give the controls bottom that they need
        if (ctrl1 != null) {
          h_ctrl = ctrl1.height();
          ctrl1.setScreen(0, d.height - h_ctrl, d.width, h_ctrl);
          cnvs.addElement(ctrl1);
        }
        // upper goes to sim1
        if (sim1 != null) {
          sim1.setScreen(0, 0, d.width, d.height - h_ctrl);
          cnvs.addElement(sim1);
        }
        break;
      }
    }

    // repaint each canvas
    int i;
    int n = Lab.this.cnvs.size();
    for (i=0; i<n; i++)
    {
      CCanvas c = (CCanvas)Lab.this.cnvs.elementAt(i);
      c.repaint();
    }

  }

  public void setLayout(String name)
  {
    if (name == null)
      return;
    if (name.length() == 0)
      return;
    int num = Integer.parseInt(name);
    layout_num = num;
    layoutCanvas();
  }


} // end LabLayout class-------------------------------

// start body of Lab class


  public static void main(String[] args)
   {
    Frame frame = new LabFrame();
      frame.show();
   }

  // following several methods are for calling by JavaScript
  // after the applet has started running...

  public void setSimulation1(String s)
  {
    stop();
    theLayout.setSim1(s);
    theLayout.layoutCanvas();
    start();
  }

  public void setGraph1(String s)
  {
    theLayout.setGraph1(s);
    theLayout.layoutCanvas();
  }

  public void setControl1Columns(String s)
  {
    theLayout.setControl1Columns(s);
    theLayout.layoutCanvas();
  }

  public void setGraph1Target(String s)
  {
    theLayout.setGraph1Target(s);
  }

  public void setGraph1XVar(String s)
  {
    theLayout.setGraph1XVar(s);
  }

  public void setGraph1YVar(String s)
  {
    theLayout.setGraph1YVar(s);
  }

  public void setGraph1Mode(String s)
  {
    theLayout.setGraph1Mode(s);
  }

  public void setGraph1AutoScale(String s)
  {
    theLayout.setGraph1AutoScale(s);
  }

  // NOTE: can't use the name 'setLayout' because that's used by some other Java classes!
  // if you use 'setLayout' then can't call this function from JavaScript.
  public void set_layout(String s)
  {
    theLayout.setLayout(s);
  }

  public void setSim1Var(int num, String s)
  {
    theLayout.setSim1Var(num, s);
  }

  public void setSim1Param(int num, String s)
  {
    theLayout.setSim1Param(num, s);
  }

  public void sim1Stop()
  {
    theLayout.sim1.stop();
  }

  public void sim1Debug()
  {
    theLayout.sim1.debug();
    System.out.println("sim1_debug()");
  }

  public void sim1ShowEnergy(boolean show)
  {
    theLayout.sim1.showEnergy(show);
    System.out.println("show_energy");
  }

  public void graph1Repaint()
  {
    theLayout.graph1.reset();
  }

  public void redoLayout()
  {
    theLayout.layoutCanvas();
  }

  public void init()   // "constructor"
  {
    if (verbose) System.out.println("Lab.init() started");
    cnvs = new Vector(10);
    theLayout = new LabLayout();

    theLayout.setSim1(getParameter("simulation1"));
    theLayout.setGraph1(getParameter("graph1"));
    theLayout.setGraph1Target(getParameter("graph1_target"));
    theLayout.setControl1Columns(getParameter("control1_columns"));
    theLayout.setGraph1XVar(getParameter("graph1_x_var"));
    theLayout.setGraph1YVar(getParameter("graph1_y_var"));
    theLayout.setGraph1Mode(getParameter("graph1_mode"));
    theLayout.setGraph1AutoScale(getParameter("auto_scale"));
    theLayout.setLayout(getParameter("layout"));
    theLayout.setSim1Param(0, getParameter("sim1_param0"));
    theLayout.setSim1Param(1, getParameter("sim1_param1"));
    theLayout.setSim1Param(2, getParameter("sim1_param2"));
    theLayout.setSim1Param(3, getParameter("sim1_param3"));
    theLayout.setSim1Param(4, getParameter("sim1_param4"));
    theLayout.setSim1Param(5, getParameter("sim1_param5"));
    theLayout.setSim1Param(6, getParameter("sim1_param6"));
    theLayout.setSim1Param(7, getParameter("sim1_param7"));
    theLayout.setSim1Var(0, getParameter("sim1_var0"));
    theLayout.setSim1Var(1, getParameter("sim1_var1"));
    theLayout.setSim1Var(2, getParameter("sim1_var2"));
    theLayout.setSim1Var(3, getParameter("sim1_var3"));
    theLayout.setSim1Var(4, getParameter("sim1_var4"));
    theLayout.setSim1Var(5, getParameter("sim1_var5"));
    theLayout.setSim1Var(6, getParameter("sim1_var6"));
    theLayout.setSim1Var(7, getParameter("sim1_var7"));

    // get parameter for "orientation" (horizontal, vertical)
    // get percentages for graph & sim (and their order?)

    // create the layout object, give it the sim, graph, orientation

    /* WARNING:  In the application, the applet has zero width & height at this point...only when the window is activated will it have a size.  I don't know what will happen in the browser... */

    addMouseListener(this);  // listen to mouse events
    addMouseMotionListener(this);  // listen to mouse motion events
    addKeyListener(this);  // listen to key events
    if (verbose) System.out.println("Lab.init() finished");
  }

  // called when user returns to browser page containing applet
  public void start()
  {
    if (verbose) System.out.println("Lab.start() started");

    // screen size should be known by now...
    if (firstTime)
      theLayout.layoutCanvas();

    if (timer == null)
    {
      /* NOTE:  if you actually try to change/add/delete a sim while timer is running,you will have to stop & restart the timer, or somehow modify its list of things that need repainting!!!
       */
      timer = new LabTimer(33);
      int i;
      int n = cnvs.size();
      for (i=0; i<n; i++)
      {
        CCanvas c = (CCanvas)cnvs.elementAt(i);
        if (c.animated())  // add animated canvas's to the timer
          timer.add(c);
      }
      timer.start();
    }

    if (verbose) System.out.println("Lab.start() finished");
  }

  // called when user leaves browser page containing applet
  public void stop()
  {
    if (verbose) System.out.println("Lab.stop() started");
    if (timer != null)
    { timer.interrupt();
      timer = null;  // destroys the thread
    }
    if (verbose) System.out.println("Lab.stop() finished");
  }

  /* override update to NOT erase the background before painting */
  public void update(Graphics g)
  {
    if (verbose) System.out.println("Lab.update() started");
    if (offscreen != null)
      paint(g);  // we have offscreen buffer, so don't erase background
    else
      super.update(g);  // no offscreen, so erase background as normal
    if (verbose) System.out.println("Lab.update() finished");
  }

   public void paint(Graphics g)
   {
    if (verbose) System.out.println("Lab.paint() started");
    // createImage doesn't work during "init()"... probably because
    // applet is zero width & height during init.
    if (offscreen == null)
      offscreen = createImage(getSize().width, getSize().height);

    if (offscreen != null)
    {
      if (verbose) System.out.println("Lab.paint() offscreen not null");
      Graphics og = offscreen.getGraphics();
      Dimension d = getSize();
      // need to remember clip bounds because we change clipping below
      Rectangle r = g.getClipBounds();
      int x1, y1, x2, y2;
      x1 = r.x;
      y1 = r.y;
      x2 = x1 + r.width;
      y2 = y1 + r.height;

      int i;
      int n = cnvs.size();
      for (i=0; i<n; i++)
      {
        CCanvas c = (CCanvas)cnvs.elementAt(i);
        /*
        clipping:  if the canvas intersects at all, then it sets clipping to its
        entire canvas area.  This changes the clipping area. Future improvement:
        could set clipping to intersection with the clipping rect, instead of entire
        canvas area.
        */
        if (c.intersectRect(r))
          c.draw(og);
      }

      // blit clipping area to screen.
      g.drawImage(offscreen, x1, y1, x2, y2, x1, y1, x2, y2, null);


      /* allow graph to do incremental drawing (regardless of what clipping area is).
      */
      for (i=0; i<n; i++)
      {
        CCanvas c = (CCanvas)cnvs.elementAt(i);
        c.incrementalDraw(g, og);
      }

      og.dispose();  // crucial!!!
    } else {
      // throw an exception here???
      System.out.println("Lab.paint() offscreen is null");
      //setBackground(Color.white);
      //super.paint(g);  // clear window to white
      //sim.draw(g);
    }

    if (verbose) System.out.println("Lab.paint() finished");

   }

  public void mousePressed(MouseEvent evt)
  {
    // determine which canvas we clicked in
    // set a class var to that canvas
    // pass message down

    int scr_x = evt.getX();  // screen coords
    int scr_y = evt.getY();
    int i;
    dragCanvas = null;
    int n = cnvs.size();
    for (i=0; i<n; i++) {
      CCanvas c = (CCanvas)cnvs.elementAt(i);
      if (c.hitCheck(scr_x, scr_y))
      {
        dragCanvas = c;
        c.mousePressed(evt);
        break;
      }
    }
  }

  public void mouseDragged(MouseEvent evt)
  {
    // pass message to canvas prev clicked on
  if (dragCanvas != null)
    dragCanvas.mouseDragged(evt);
  }

  public void mouseReleased(MouseEvent evt)
  {
    // pass message to canvas prev clicked on
  if (dragCanvas != null)
    dragCanvas.mouseReleased(evt);
  }

  public void mouseClicked(MouseEvent evt)
  {
    int scr_x = evt.getX();  // screen coords
    int scr_y = evt.getY();
    int i;
    int n = cnvs.size();
    for (i=0; i<n; i++) {
      CCanvas c = (CCanvas)cnvs.elementAt(i);
      if (c.hitCheck(scr_x, scr_y))
      {
        c.mouseClicked(evt);
        if (c.acceptsKeyEvents())
          keyCanvas = c;
        break;
      }
    }
  }

  public void mouseEntered(MouseEvent evt)
  {
  }

  public void mouseExited(MouseEvent evt)
  {
  }

  public void mouseMoved(MouseEvent evt)
  {
  }

  /* componentListener interface */
  public void componentMoved(ComponentEvent e)
  {
  }

  public void componentHidden(ComponentEvent e)
  {
  }

  public void componentResized(ComponentEvent e)
  {
    System.out.println("componentResized");
    offscreen = null;  // so that a new size offscreen is created
    theLayout.layoutCanvas();
  }

  public void componentShown(ComponentEvent e)
  {
  }

  /* keyListener interface */
  public void keyTyped(KeyEvent e)
  {
    if (keyCanvas != null)
      keyCanvas.keyTyped(e);
  }

  public void keyPressed(KeyEvent e)
  {
    if (keyCanvas != null)
      keyCanvas.keyPressed(e);
  }

  public void keyReleased(KeyEvent e)
  {
  }

  // 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;
  }

}

class LabWindowAdapter extends WindowAdapter
{
  Lab myLab = null;
  public LabWindowAdapter(Lab thisLab)
  {
    myLab = thisLab;
  }

  public void windowIconified(WindowEvent e)
  {
    System.out.println("windowIconified()");
    myLab.stop();
  }

  public void windowDeiconified(WindowEvent e)
  {
    System.out.println("windowDeiconified()");
    myLab.start();
  }

  public void windowDeactivated(WindowEvent e)
  {
    System.out.println("windowDeactivated()");
    //myLab.stop();  // disable this line when using jdb
  }

  public void windowActivated(WindowEvent e)
  {
    System.out.println("windowActivated()");
    myLab.start();
    // The firstTime flag prevents doing layout when window activates (after first time).
    // The problem is that layout calls repaint which causes CPhase graph to redraw
    // instead of using its offscreen buffer.
    myLab.firstTime = false;

  }

  public void windowClosing(WindowEvent e)
  {
    myLab.stop();
    System.exit(0);
  }
}

class LabFrame extends Frame
   implements AppletStub, AppletContext
{
  public LabFrame()  // constructor
   {
    setTitle("Physics Lab");
      //addWindowListener(new WindowAdapter()
      //   {  public void windowClosing(WindowEvent e)
      //      {  System.exit(0);
      //      }
      //   } );
    Toolkit tk = Toolkit.getDefaultToolkit();
    Dimension d = tk.getScreenSize();
    setSize(640, 480);
    setLocation(d.width/10, d.height/10);
    Lab applet = new Lab();
        applet.setStub(this);
    addWindowListener(new LabWindowAdapter(applet));
    add(applet);
    applet.init();
    applet.start();
    addComponentListener(applet);  // applet listens for resize events
   }

   // implement AppletStub & AppletContext so that we can call
   // methods like getParameter() in applet mode, and not crash
   // if we are in application mode.


   // AppletStub methods
   public boolean isActive() { return true; }
   public URL getDocumentBase() { return null; }
   public URL getCodeBase() { return null; }
   public String getParameter(String name)
   {
    // here we establish settings for the application version
    // *************  LOOK HERE ****************
    if ("simulation1".equalsIgnoreCase(name))
      return "molecule5";
    else if ("graph1".equalsIgnoreCase(name))
      return "phase";
    else if ("control1_columns".equalsIgnoreCase(name))
      return "3";
    else if ("graph1_x_var".equalsIgnoreCase(name))
      return "0";
    else if ("graph1_y_var".equalsIgnoreCase(name))
      return "1";
    else if ("graph1_draw_mode".equalsIgnoreCase(name))
      return "lines";
    else if ("layout".equalsIgnoreCase(name))
      return "3";  // 0=sim, 1=sim+graph, 2=sim+graph+ctrl, 3=sim+ctrl
    else if ("auto_scale".equalsIgnoreCase(name))
      return "true";
    else
      return "";
  }

   public AppletContext getAppletContext() { return this; }
   public void appletResize(int width, int height) {}

   // AppletContext methods
   public AudioClip getAudioClip(URL url) { return null; }
   public Image getImage(URL url) { return null; }
   public Applet getApplet(String name) { return null; }
   public Enumeration getApplets() { return null; }
   public void showDocument(URL url) {}
   public void showDocument(URL url, String target) {}
   public void showStatus(String status) {}

  // setStream, getStream, getStreamKeys are for Java 1.4 compatibility
  // comment out these methods to compile with Java 1.3
   public void setStream(String key,
                         InputStream stream)
             throws IOException
             {}
   public InputStream getStream(String key) { return null; }
   public Iterator getStreamKeys() { return null; }
}


