/*
 * XtangoAnimator.java                       version 1.00   4 September 1996
 *    synchronized on icon.position                  1.01  28 September 1996
 *    removed bounded buffer                         1.02   7 October   1996
 *    took out sleep(frameDelay) in doCommand()      1.03  16 October   1996
 *    single stepping button                         1.04  20 December  1996
 *    JDK 1.1 event delegation model                 1.050  7 May       1997
 *    both abstract, synchronized illegal on draw()  1.051  8 December  1997
 *
 *  This is an implementation of the Xtango animator interpreter command
 * set in Java.  See the main() method below for example use.  For more
 * information about the Xtango algorithm animation package, see
 * John T. Stasko and Doug Hayes, "XTANGO Algorithm Animation Designer's
 * Package," October 1992, available by anonymous ftp from machine
 * ftp.cc.gatech.edu (from directory pub/people/stasko, retrieve file
 * xtango.tar.Z, then uncompress and extract file xtangodoc.ps from
 * directory ./xtango/doc in the archive file xtango.tar).
 *
 * (C) 1996, 1997 Stephen J. Hartley.  All rights reserved.
 * Permission to use, copy, modify, and distribute this software for
 * non-commercial uses is hereby granted provided this notice is kept
 * intact within the source file.
 *
 * mailto:shartley@mcs.drexel.edu http://www.mcs.drexel.edu/~shartley
 * Drexel University, Math and Computer Science Department
 * Philadelphia, PA 19104 USA  telephone: +1-215-895-2678
 */

package XtangoAnimation;

import java.awt.*;
import java.awt.event.*;
import java.util.*;

public final class XtangoAnimator implements WindowListener {

   public static final int OUTLINE = 0; // for fillval in rectangle, circle,
   public static final int HALF = 1;    // triangle, fill
   public static final int SOLID = 2;

   public static final int THIN = 3;    // for widthval in line, pointLine
   public static final int MEDTHICK = 4;
   public static final int THICK = 5;

   // SYNC: the movement is completed before the method returns
   static final int SYNC = 6;
   // ASYNC: the method returns after the command is started
   static final int ASYNC = 7;       // for moveAsync, etc.

   static boolean debug = false;

   private static final int frameWidth = 640;
   private static final int frameHeight = 700;

   private AnimatorFrame af = null;

   public XtangoAnimator(int frameWidth, int frameHeight, boolean dbg) {
      debug = dbg;
      af = new AnimatorFrame(frameWidth, frameHeight, this);
      af.addWindowListener(this);
   }

   public XtangoAnimator() {
      this(frameWidth, frameHeight, false);
   }

   public XtangoAnimator(boolean dbg) {
      this(frameWidth, frameHeight, dbg);
   }

   public XtangoAnimator(int frameWidth, int frameHeight) {
      this(frameWidth, frameHeight, false);
   }

   public void windowClosed(WindowEvent evt) {
      if (debug) System.out.println("XtangoAnimator: window closed");
   }

   public void windowDeiconified(WindowEvent evt) {
      if (debug) System.out.println("XtangoAnimator: window deiconified");
   }

   public void windowIconified(WindowEvent evt) {
      if (debug) System.out.println("XtangoAnimator: window iconified");
   }

   public void windowActivated(WindowEvent evt) {
      if (debug) System.out.println("XtangoAnimator: window activated");
   }

   public void windowDeactivated(WindowEvent evt) {
      if (debug) System.out.println("XtangoAnimator: window deactivated");
   }

   public void windowOpened(WindowEvent evt) {
      if (debug) System.out.println("XtangoAnimator: window opened");
   }

   // user used window manager, not Java, to destroy the window
   public void windowClosing(WindowEvent evt) {
      if (debug) System.out.println("XtangoAnimator: window destroy");
      af.setVisible(false);
      af.dispose();
      System.exit(0);
   }

   synchronized void startPushed() {notify();}
   synchronized void quitPushed() {notify();}

// Wait to start the animation until the Start button is clicked.
   public synchronized void begin() {
      System.out.println("XtangoAnimator: Push the Start button");
      try {
         wait();
      } catch (InterruptedException e) {}
   }

// Wait to terminate the animation until the Close or Quit button is clicked.
   public synchronized void end() {
      if (debug) System.out.println("XtangoAnimator end");
      af.doCommand(new Cend());
      try {
         wait();
      } catch (InterruptedException e) {}
   }

// Change the background to the given color.
// The default starter is white.
   public void bg(Color colorval) {
      if (debug) System.out.println("XtangoAnimator bg: " + colorval);
      af.doCommand(new Cbg(colorval));
   }

// Change the displayed coordinates to the given values.
// The bottom left and top right corners of the animation
// window are set to coordinates (lx,by) and (rx,ty).
   public void coords(float lx, float by, float rx, float ty) {
      if (rx - lx == 0 || ty - by == 0) {
         System.err.println
            ("XtangoAnimator coords: rx - lx == 0 || ty - by == 0");
         return;
      }
/*
 * widthCanvas and heightCanvas determine what (x,y) coordinates actually
 * show up in the AnimatorFrame window; (0,0) is by default in the lower
 * left corner; whatever is in the Canvas bounds will show up when drawn;
 * the coords command really just moves (0,0) i.e. really just sets what
 * the lower left corner is; only two numbers are needed but the Xtango
 * command has four; for simplicity rx-lx==ty-by is required; oops, the
 * coord command also sets the linear scale so just three numbers are
 * actually needed.
 */
      if (rx - lx != ty - by) {
         System.err.println
            ("XtangoAnimator coords: rx - lx != ty - by");
         return;
      }
      Ccoords c = new Ccoords(lx, by, rx, ty);
      af.doCommand(c);
      if (debug) System.out.println("XtangoAnimator coords: " + c);
   }

// Generate the given number of animation frames with no changes in them.
   public void delay(int steps) {
      if (debug) System.out.println("XtangoAnimator delay: " + steps);
      af.doCommand(new Cdelay(steps));
   }

// Create a line with one endpoint at the given position
// and of the given size.
   public void line(String id, float xpos, float ypos,
         float xsize, float ysize, Color colorval, int widthval) {
      Iline ell = new Iline(id, xpos, ypos, xsize, ysize, colorval, widthval);
      af.doCommand(ell);
      if (debug) System.out.println("XtangoAnimator line: " + ell);
   }

// Create a line with its two endpoints at the given positions
   public void pointLine(String id, float xpos1, float ypos1,
         float xpos2, float ypos2, Color colorval, int widthval) {
      Iline ell = new Iline(id, xpos1, ypos1, xpos2-xpos1, ypos2-ypos1,
         colorval, widthval);
      af.doCommand(ell);
      if (debug) System.out.println("XtangoAnimator pointLine: " + ell);
   }

// Create a rectangle with lower left corner at the given position
// and of the given size (the size must be positive).
   public void rectangle(String id, float xpos, float ypos,
         float xsize, float ysize, Color colorval, int fillval) {
      Irectangle r = new Irectangle(id, xpos, ypos, xsize, ysize,
         colorval, fillval);
      af.doCommand(r);
      if (debug) System.out.println("XtangoAnimator rectangle: " + r);
   }

// Create a circle with the given radius centered at the given position.
   public void circle(String id, float xpos, float ypos,
         float radius, Color colorval, int fillval) {
      Icircle c = new Icircle(id, xpos, ypos, radius, colorval, fillval);
      af.doCommand(c);
      if (debug) System.out.println("XtangoAnimator circle: " + c);
   }

// Create a triangle whose three vertices are located
// at the given three coordinates.
   public void triangle(String id, float v1x, float v1y, float v2x,
         float v2y, float v3x, float v3y, Color colorval, int fillval) {
      Itriangle t = new Itriangle(id, v1x, v1y, v2x, v2y, v3x, v3y,
         colorval, fillval);
      af.doCommand(t);
      if (debug) System.out.println("XtangoAnimator triangle: " + t);
   }

// Create text with lower left corner at the given position
// if centered is false.  If centered is true, the position
// arguments denote the place where the center of the text is put.
   public void text(String id, float xpos, float ypos,
         boolean centered, Color colorval, String string) {
      Itext t = new Itext(id, xpos, ypos, centered, colorval, string,
         Itext.NORMAL);
      af.doCommand(t);
      if (debug) System.out.println("XtangoAnimator text: " + t);
   }

// This works just like the text command except that
// this text is in a much larger font.
   public void bigText(String id, float xpos, float ypos,
         boolean centered, Color colorval, String string) {
      Itext t = new Itext(id, xpos, ypos, centered, colorval, string,
         Itext.BIG);
      af.doCommand(t);
      if (debug) System.out.println("XtangoAnimator bigText: " + t);
   }

// This works just like the text command except that
// this text is in a much smaller font.
   public void smallText(String id, float xpos, float ypos,
         boolean centered, Color colorval, String string) {
      Itext t = new Itext(id, xpos, ypos, centered, colorval, string,
         Itext.SMALL);
      af.doCommand(t);
      if (debug) System.out.println("XtangoAnimator smallText: " + t);
   }

// Smoothly move, via a sequence of intermediate steps,
// the object with the given id to the specified position.
   public void move(String id, float xpos, float ypos) {
      if (debug) System.out.println("XtangoAnimator move: " + id
         + ", xpos=" + xpos + ", ypos=" + ypos);
      af.doCommand(new Cmove(id, xpos, ypos, SYNC));
   }

// Smoothly move, via a sequence of intermediate steps,
// the object with the given identifier by the given relative distance.
   public void moveRelative(String id, float xdelta, float ydelta) {
      if (debug) System.out.println("XtangoAnimator moveRelative: " + id
         + ", xdelta=" + xdelta + ", ydelta=" + ydelta);
      af.doCommand(new CmoveRelative(id, xdelta, ydelta, SYNC));
   }

// Smoothly move, via a sequence of intermediate steps, the object with
// the first id to the current position of the object with the second id.
   public void moveTo(String id1, String id2) {
      if (debug)
         System.out.println("XtangoAnimator moveTo: " + id1 + " to " + id2);
      af.doCommand(new CmoveTo(id1, id2, SYNC));
   }

// Move the object with the given identifier
// to the designated position in a one frame jump.
   public void jump(String id, float xpos, float ypos) {
      if (debug) System.out.println("XtangoAnimator jump: " + id
         + ", xpos=" + xpos + ", ypos=" + ypos);
      af.doCommand(new Cjump(id, xpos, ypos));
   }

// Move the object with the given identifier
// by the provided relative distance in one jump.
   public void jumpRelative(String id, float xdelta, float ydelta) {
      if (debug) System.out.println("XtangoAnimator jumpRelative: " + id
         + ", xdelta=" + xdelta + ", ydelta=" + ydelta);
      af.doCommand(new CjumpRelative(id, xdelta, ydelta));
   }

// Move the object with the given identifier to the current position
// of the object with the second identifier in a one frame jump.
   public void jumpTo(String id1, String id2) {
      if (debug)
         System.out.println("XtangoAnimator jumpTo: " + id1 + " to " + id2);
      af.doCommand(new CjumpTo(id1, id2));
   }

// Change the color of the object with the given identifier
// to the specified color value.
   public void color(String id, Color colorval) {
      if (debug)
         System.out.println("XtangoAnimator color: " + id + ", " + colorval);
      af.doCommand(new Ccolor(id, colorval));
   }

// Permanently remove the object with the given identifier from the display,
// and remove any association of this identifier string with the object.
   public void delete(String id) {
      if (debug) System.out.println("XtangoAnimator delete: " + id);
      af.doCommand(new Cdelete(id));
   }

// Change the object with the given identifier to the designated fill value.
   public void fill(String id, int fillval) {
      if (debug) System.out.println("XtangoAnimator fill: id=" + id
         + ", fillval=" + fillval);
      af.doCommand(new Cfill(id, fillval));
   }

// Toggle the visibility of the object with the given identifier.
   public void vis(String id) {
      if (debug) System.out.println("XtangoAnimator vis: " + id);
      af.doCommand(new Cvis(id, Cvis.VIS));
   }

// Push the object with the given identifier backward to the viewing
// plane farthest from the viewer.
   public void lower(String id) {
      if (debug) System.out.println("XtangoAnimator lower: " + id);
      af.doCommand(new Cvis(id, Cvis.LOWER));
   }

// Pop the object with the given identifier forward to the viewing
// plane closest to the viewer.
   public void raise(String id) {
      if (debug) System.out.println("XtangoAnimator raise: " + id);
      af.doCommand(new Cvis(id, Cvis.RAISE));
   }

// Make the two objects specified by the given identifiers
// smoothly exchange positions.
   public void exchangePos(String id1, String id2) {
      if (debug)
         System.out.println("XtangoAnimator exchangePos: " + id1 + ", " + id2);
      af.doCommand(new CexchangePos(id1, id2, SYNC));
   }

// Make the two objects specified by the given identifiers
// exchange positions in one instantaneous jump.
   public void switchPos(String id1, String id2) {
      if (debug)
         System.out.println("XtangoAnimator switchPos: " + id1 + ", " + id2);
      af.doCommand(new CswitchPos(id1, id2));
   }

// Exchange the identifiers used to designate the two given objects.
   public void swapIds(String id1, String id2) {
      if (debug)
         System.out.println("XtangoAnimator swapIds: " + id1 + ", " + id2);
      af.doCommand(new CswapIds(id1, id2));
   }

// Smoothly move asynchronously, via a sequence of intermediate steps,
// the object with the given id to the specified position.
   public void moveAsync(String id, float xpos, float ypos) {
      if (debug) System.out.println("XtangoAnimator moveAsync: " + id
         + ", xpos=" + xpos + ", ypos=" + ypos);
      af.doCommand(new Cmove(id, xpos, ypos, ASYNC));
   }

// Smoothly move asynchronously, via a sequence of intermediate steps,
// the object with the given identifier by the given relative distance.
   public void moveRelativeAsync(String id, float xdelta, float ydelta) {
      if (debug) System.out.println("XtangoAnimator moveRelativeAsync: "
         + id + ", xdelta=" + xdelta + ", ydelta=" + ydelta);
      af.doCommand(new CmoveRelative(id, xdelta, ydelta, ASYNC));
   }

// Smoothly move asynchronously, via a sequence of intermediate steps,
// the object with the first id to the current position of the object
// with the second id.
   public void moveToAsync(String id1, String id2) {
      if (debug)
         System.out.println("XtangoAnimator moveToAsync: "
            + id1 + " to " + id2);
      af.doCommand(new CmoveTo(id1, id2, ASYNC));
   }

// Make the two objects specified by the given identifiers
// smoothly exchange positions asynchronously.
   public void exchangePosAsync(String id1, String id2) {
      if (debug)
         System.out.println("XtangoAnimator exchangePosAsync: "
            + id1 + ", " + id2);
      af.doCommand(new CexchangePos(id1, id2, ASYNC));
   }

   public static void main(String[] args) { // for testing
      if (debug) System.out.println("XtangoAnimator: main");
      XtangoAnimator xa = new XtangoAnimator();
      xa.begin();
      xa.delay(10);
      xa.pointLine("Lthin", 0.3f, 0.2f, 0.8f, 0.7f, Color.black,
         XtangoAnimator.THIN);
      xa.delay(10);
      xa.pointLine("Lmedium", 0.4f, 0.2f, 0.9f, 0.7f, Color.black,
         XtangoAnimator.MEDTHICK);
      xa.delay(10);
      xa.pointLine("Lthick", 0.5f, 0.2f, 1.0f, 0.7f, Color.black,
         XtangoAnimator.THICK);
      xa.delay(10);
      xa.triangle("Tri1", 0.1f, 0.1f, 0.5f, 0.9f, 0.9f, 0.2f, Color.magenta,
         XtangoAnimator.OUTLINE);
      xa.delay(10);
      xa.triangle("Tri2", 0.1f, 0.3f, 0.7f, 0.9f, 0.8f, 0.3f, Color.green,
         XtangoAnimator.SOLID);
      xa.delay(10);
      xa.bg(Color.yellow);
      xa.delay(10);
      xa.text("t00", 0.0f, 0.0f, false, Color.gray, "text 0");
      xa.delay(10);
      xa.text("t01", 0.1f, 0.1f, false, Color.blue, "text 1");
      xa.delay(10);
      xa.circle("c0", 0.8f, 0.2f, 0.1f, Color.red, XtangoAnimator.SOLID);
      xa.delay(10);
      xa.bg(Color.cyan);
      xa.delay(10);
      xa.text("t02", 0.2f, 0.2f, false, Color.gray, "text 2");
      xa.delay(10);
      xa.text("t03", 0.3f, 0.3f, false, Color.gray, "text 3");
      xa.delay(10);
      xa.text("t04", 0.4f, 0.4f, false, Color.gray, "text 4");
      xa.delay(10);
      xa.text("t05", 0.5f, 0.5f, false, Color.gray, "text 5");
      xa.delay(10);
      xa.text("t06", 0.6f, 0.6f, false, Color.gray, "text 6");
      xa.delay(10);
      xa.text("t06", 0.6f, 0.6f, false, Color.gray, "TEXT 6");
      xa.delay(10);
      xa.text("t07", 0.7f, 0.7f, false, Color.gray, "text 7");
      xa.delay(10);
      xa.text("t08", 0.8f, 0.8f, false, Color.gray, "text 8");
      xa.delay(10);
      xa.text("t09", 0.9f, 0.9f, false, Color.gray, "text 9");
      xa.delay(10);
      xa.text("t10", 1.0f, 1.0f, false, Color.gray, "text10");
      xa.delay(10);
      xa.circle("c1", 0.7f, 0.7f, 0.05f, Color.black, XtangoAnimator.OUTLINE);
      xa.delay(10);
      xa.fill("c1", XtangoAnimator.SOLID);
      xa.delay(10);
      xa.fill("c1", XtangoAnimator.HALF);
      xa.delay(10);
      xa.color("t07", Color.white);
      xa.delay(10);
      xa.raise("t07");
      xa.delay(10);
      xa.swapIds("t04", "t05");
      xa.jump("t04", 0.9f, 0.1f);
      xa.delay(10);
      xa.delete("t04"); xa.delete("t05"); xa.delete("t06");
      xa.delete("t04");
      xa.delay(10);
      xa.jumpTo("c0", "t03");
      xa.delay(10);
      xa.vis("t03");    // should lower
      xa.delay(10);
      xa.vis("t03");    // should raise
      xa.delay(10);
      xa.rectangle("R", 0.5f, 0.5f, 0.1f, 0.2f, Color.black,
         XtangoAnimator.SOLID);
      xa.delay(10);
      xa.rectangle("Rhalf", 0.6f, 0.6f, 0.2f, 0.1f, Color.black,
         XtangoAnimator.HALF);
      xa.delay(10);
      xa.line("L", 0.1f, 0.1f, 0.8f, 0.8f, Color.black, XtangoAnimator.THIN);
      xa.delay(10);
      xa.circle("moveTest1", 0.1f, 0.9f, 0.05f, Color.magenta,
         XtangoAnimator.SOLID);
      xa.move("moveTest1", 0.9f, 0.1f);
      xa.circle("moveTest2", 0.9f, 0.9f, 0.05f, Color.blue,
         XtangoAnimator.OUTLINE);
      xa.move("moveTest2", 0.1f, 0.1f);
      for (int i = 0; i < 5; i++) {
         xa.rectangle("R"+i, (float)Math.random(), (float)Math.random(),
            (float)Math.random()/4,
            (float)Math.random()/4, Color.magenta, XtangoAnimator.SOLID);
         xa.delay(2);
         xa.color("R"+i, Color.orange);
         xa.delay(2);
         xa.delete("R"+i);
      }
      for (int i = 0; i < 5; i++) {
         xa.delay(2);
         float x = (float)Math.random();
         float y = (float)Math.random();
         System.out.println("jump x=" + x + ", y=" + y);
         xa.jump("c1", x, y);
      }
      xa.exchangePosAsync("moveTest1", "moveTest2");
      xa.delay(1);
      xa.bigText("ha!", 0.5f, 0.1f, true, Color.black, "ha!");
      xa.exchangePos("moveTest1", "moveTest2");
      xa.delay(1);
      xa.bigText("Ha!", 0.6f, 0.1f, true, Color.black, "Ha!");
      xa.exchangePosAsync("moveTest1", "moveTest2");
      xa.delay(5);
      xa.bigText("HA!", 0.7f, 0.1f, true, Color.black, "HA!");
      xa.delay(10);
      xa.jumpRelative("t00", 0.05f, 0.05f);
      xa.delay(10);
      xa.jumpRelative("t01", -0.05f, 0.07f);
      xa.delay(10);
      xa.jumpRelative("t02", 0.02f, -0.15f);
      xa.delay(10);
      xa.jumpRelative("t03", -0.3f, -0.3f);
      xa.delay(10);
      xa.moveRelative("moveTest1", 0.3f, 0.3f);
      xa.moveTo("moveTest2", "t00");
      xa.delay(10);
      xa.smallText("Tsmall", 0.2f, 0.9f, true, Color.magenta, "Going...");
      xa.line("L1", 0.2f, 0.85f, 0.0f, 0.1f, Color.black, XtangoAnimator.THIN);
      xa.line("L2", 0.15f, 0.9f, 0.1f, 0.0f, Color.black, XtangoAnimator.THIN);
      xa.delay(10);
      xa.text("Tnormal", 0.15f, 0.8f, true, Color.blue, "Yup, Going...");
      xa.line("L3", 0.15f, 0.75f, 0.0f, 0.1f, Color.black, XtangoAnimator.THIN);
      xa.line("L4", 0.1f, 0.8f, 0.1f, 0.0f, Color.black, XtangoAnimator.THIN);
      xa.delay(10);
      xa.bigText("Tbig", 0.1f, 0.7f, true, Color.darkGray, "Surely, GONE!");
      xa.line("L5", 0.1f, 0.65f, 0.0f, 0.1f, Color.black, XtangoAnimator.THIN);
      xa.line("L6", 0.05f, 0.7f, 0.1f, 0.0f, Color.black, XtangoAnimator.THIN);
      xa.delay(10);
      xa.coords(0.0f, 0.0f, 0.5f, 0.5f);
      xa.delay(10);
      xa.coords(0.0f, 0.5f, 0.5f, 1.0f);
      xa.delay(10);
      xa.coords(0.0f, 0.0f, 1.0f, 1.0f);
      xa.delay(10);
      xa.coords(0.0f, 0.0f, 1.0f, 0.5f);
      xa.delay(10);
      xa.coords(0.0f, 0.0f, 0.5f, 1.0f);
      xa.delay(10);
      xa.switchPos("Tbig", "Tsmall");
      xa.delay(10);
      xa.moveToAsync("Tnormal", "Tsmall");
      xa.delay(1);
      xa.moveRelativeAsync("Tbig", 0.3f, 0.0f);
      xa.moveAsync("Tsmall", 0.2f, 0.9f);
      System.out.println("DONE!");
      xa.end();
      System.exit(0);
   }
}

final class AnimatorFrame extends Frame implements ActionListener {

   private int frameWidth = 0;
   private int frameHeight = 0;
   private XtangoAnimator xa = null;
   private Panel p = null;
   private AnimatorCanvas ac = null;
   private Thread at = null; // in AnimatorCanvas
   private Vector icons = null;
   private Hashtable ht = null;
   private TextField tf = null;
   private boolean singleStep = false;
   private Button singleStepButton = null;

   AnimatorFrame(int frameWidth, int frameHeight, XtangoAnimator xa) {
      super("AnimatorFrame");
      setLayout(new BorderLayout());
      p = new Panel();
      p.setLayout(new FlowLayout());
      Button b;
      p.add(b = new Button("Start"));   b.addActionListener(this);
      p.add(b = new Button("Faster"));  b.addActionListener(this);
      tf = new TextField(5);
      tf.setEditable(false);
      p.add(tf);
      p.add(b = new Button("Slower"));  b.addActionListener(this);
      p.add(singleStepButton = new Button("Single Step Off"));
                         singleStepButton.addActionListener(this);
      p.add(b = new Button("Step"));    b.addActionListener(this);
      p.add(b = new Button("Stop"));    b.addActionListener(this);
      p.add(b = new Button("Close"));   b.addActionListener(this);
      p.add(b = new Button("Quit"));    b.addActionListener(this);
      add("North", p);
      icons = new Vector();
      ac = new AnimatorCanvas(icons);
      add("Center", ac);
      ht = new Hashtable();
      this.frameWidth = frameWidth;
      this.frameHeight = frameHeight;
      this.xa = xa;
      setSize(frameWidth, frameHeight);
      setBackground(Color.white);
      setVisible(true);
      ac.resetBounds();
      if (XtangoAnimator.debug)
         System.out.println("AnimatorFrame: constructor done");
   }

   public void actionPerformed(ActionEvent evt) {
      Object o = evt.getSource();
      if (o instanceof Button) {
         String label = ((Button) o).getLabel();
         if (label.equals("Start")) {
            tf.setText(""+ac.frameDelay);
            if (at == null) {
               at = new Thread(ac);
               at.setPriority(Thread.MAX_PRIORITY);
               at.setDaemon(true);
               at.start();
               if (XtangoAnimator.debug)
                  System.out.println("AnimatorFrame: animator thread started");
            }
            xa.startPushed();
         } else if (label.equals("Faster")) { // divide frameDelay by 2
            int newDelay = ac.frameDelay;
            newDelay /= 2;
            newDelay = newDelay < 16 ? 16 : newDelay;
            ac.frameDelay = newDelay;
            tf.setText(""+newDelay);
            if (XtangoAnimator.debug)
               System.out.println("AnimatorFrame: new frameDelay="
                  + newDelay);
         } else if (label.equals("Slower")) { // multiply frameDelay by 2
            int newDelay = ac.frameDelay;
            newDelay *= 2;
            ac.frameDelay = newDelay;
            tf.setText(""+newDelay);
            if (XtangoAnimator.debug)
               System.out.println("AnimatorFrame: new frameDelay="
                  + newDelay);
         } else if (label.equals("Single Step Off")) {
            synchronized (this) { singleStep = true; }
            singleStepButton.setLabel("Single Step On");
         } else if (label.equals("Single Step On")) {
            synchronized (this) {
               singleStep = false; // now that single stepping is off,
               this.notifyAll();   // clear out any waiting threads
            }
            singleStepButton.setLabel("Single Step Off");
         } else if (label.equals("Step")) {
            synchronized (this) { if (singleStep) this.notifyAll(); }
         } else if (label.equals("Stop")) {
            if (at != null) at.stop();
         } else if (label.equals("Close")) {
            if (at != null) at.stop();
            this.setVisible(false);
            this.dispose();
            xa.quitPushed();
         } else if (label.equals("Quit")) {
            System.exit(0);
         } else if (XtangoAnimator.debug)
            System.out.println("unknown Button label: " + label);
      } else if (XtangoAnimator.debug)
         System.out.println("ActionEvent is not a Button");
   }

   void doCommand(AnimatorCommand command) {
   // do not make this synchronized so that commands can be processed
   // in parallel, e.g. two threads both moving icons at the same time
      if (XtangoAnimator.debug)
         System.out.println("doCommand: command " + command);
      if (command instanceof AnimatorAction) {
         ((AnimatorAction) command).perform(ac, ht, icons);
      } else if (command instanceof AnimatorIcon) {
         ((AnimatorIcon) command).add(ht, icons);
      } else {
         System.err.println("doCommand: illegal command");
      }
// Make it the programmer's responsibility to call xa.delay(frames) if
// a delay is needed.  This will allow a group of commmands to be done
// nearly instantaneously.
//      try {
//         int frameDelay = ac.frameDelay;
//         Thread.sleep(frameDelay);
//      } catch (InterruptedException e) {}
      synchronized (this) {
         if (singleStep)
            try {
               wait();
            } catch (InterruptedException e) {}
      }
   }
}

final class AnimatorCanvas extends Canvas implements Runnable {

   int frameDelay = 128;
   int numMoveSteps = 10;

   private int widthCanvas = 0, heightCanvas = 0;
   private int cornerX = 0, cornerY = 0;
   private int squareCanvas = 0;
   private int excessY = 0;
   private Vector icons = null;
   private Image offscreenImage = null;
   private Graphics offscreenGraphics = null;
   private float lx = 0, by = 0, rx = 1, ty = 1; // default (0,0)..(1,1)

   AnimatorCanvas(Vector icons) {
      super();
      this.icons = icons;
   }

   synchronized void changeCoordinates
         (float lx, float by, float rx, float ty) {
      this.lx = lx;
      this.by = by;
      this.rx = rx;
      this.ty = ty;
   }

   void resetBounds() {
      Rectangle boundingBox = getBounds();
      widthCanvas = boundingBox.width;
      heightCanvas = boundingBox.height;
      squareCanvas = Math.min(widthCanvas, heightCanvas);
      excessY = heightCanvas - squareCanvas;
// take out the following line so the Canvas can utilize all
//    setSize(squareCanvas, squareCanvas);
      cornerX = boundingBox.x;
      cornerY = boundingBox.y;
      offscreenImage = createImage(widthCanvas, heightCanvas);
      offscreenGraphics = offscreenImage.getGraphics();
      if (XtangoAnimator.debug) {
         System.out.println("Canvas: cornerX=" + cornerX
            + ", cornerY=" + cornerY);
         System.out.println("Canvas: widthCanvas=" + widthCanvas
            + ", heightCanvas=" + heightCanvas + ", squareCanvas="
            + squareCanvas + ", excessY=" + excessY);
      }
   }

   public void update(Graphics g) {
      paint(g);
   }

   public synchronized void paint(Graphics g) {
      // for each icon in the icon Vector
      // icon.draw(g);
      if (XtangoAnimator.debug)
         System.out.println("AnimatorCanvas: paint");
      offscreenGraphics.setColor(getBackground());
      offscreenGraphics.fillRect(0, 0, widthCanvas, heightCanvas);
      synchronized (icons) {
         int howMany = icons.size();
         for (int i = 0; i < howMany; i++) {
            if (XtangoAnimator.debug)
               System.out.println("painting " + icons.elementAt(i));
            ((AnimatorIcon) icons.elementAt(i)).draw(this, offscreenGraphics);
         }
      }
      g.drawImage(offscreenImage, 0, 0, this);
   }

   int scaleX(float xpos) {
      return ((int)((xpos-lx)/(rx-lx)*squareCanvas));
   }

   int scaleY(float ypos) { // excessY: make sure y=0 is along bottom
      return ((int)((ty-ypos)/(ty-by)*squareCanvas))+excessY;
   }

   int scaleR(float r) { // linear scaling for radius, rectangle size,...
      return (int)(r/(rx-lx)*squareCanvas); // assumes rx-lx==ty-by
   }

   public void run() {
      while (true) {
         try {
            Thread.sleep(frameDelay);
         } catch (InterruptedException e) {}
         repaint();
      }
   }
}

abstract class AnimatorCommand {

   public abstract String toString();
}

abstract class AnimatorAction extends AnimatorCommand {

   abstract void perform(AnimatorCanvas ac, Hashtable ht, Vector icons);

   public abstract String toString();
}

class Cbg extends AnimatorAction {

   private Color colorval = null;

   Cbg(Color colorval) {
      this.colorval = colorval;
   }

   void perform(AnimatorCanvas ac, Hashtable ht, Vector icons) {
      ac.setBackground(colorval);
   }

   public String toString() {
      return "Cbg: colorval=" + colorval;
   }
}

class Ccoords extends AnimatorAction {

   private float lx = 0, by = 0, rx = 1, ty = 1;

   Ccoords(float lx, float by, float rx, float ty) {
      this.lx = lx;
      this.by = by;
      this.rx = rx;
      this.ty = ty;
   }

   void perform(AnimatorCanvas ac, Hashtable ht, Vector icons) {
      ac.changeCoordinates(lx, by, rx, ty);
   }

   public String toString() {
      return "Ccoords: (lx,by,rx,ty)="+lx+","+by+","+rx+","+ty;
   }
}

class Cdelay extends AnimatorAction {

   private int steps = 0;

   Cdelay(int steps) {
      this.steps = steps;
   }

   void perform(AnimatorCanvas ac, Hashtable ht, Vector icons) {
      if (XtangoAnimator.debug)
         System.out.println("Cdelay: napping " + steps + " steps");
      try {
         Thread.sleep(steps*ac.frameDelay);
      } catch (InterruptedException e) {}
   }

   public String toString() {
      return "Cdelay: steps=" + steps;
   }
}

class Ccolor extends AnimatorAction {

   private String id = null;
   private Color colorval = null;

   Ccolor(String id, Color colorval) {
      this.id = id;
      this.colorval = colorval;
   }

   void perform(AnimatorCanvas ac, Hashtable ht, Vector icons) {
      Object icon = null;
      synchronized (ht) {
         if ((icon = ht.get(id)) == null) {
            System.err.println("Ccolor hashtable: no such id=" + id);
            return;
         }
      }
      ((AnimatorIcon) icon).colorval(colorval);
   }

   public String toString() {
      return "Ccolor: colorval=" + colorval;
   }
}

class Cfill extends AnimatorAction {

   private String id = null;
   private int fillval = XtangoAnimator.OUTLINE;

   Cfill(String id, int fillval) {
      this.id = id;
      this.fillval = fillval;
   }

   void perform(AnimatorCanvas ac, Hashtable ht, Vector icons) {
      Object icon = null;
      synchronized (ht) {
         if ((icon = ht.get(id)) == null) {
            System.err.println("Cfill hashtable: no such id=" + id);
            return;
         }
      }
      if (icon instanceof AnimatorShape) {
         if (fillval == XtangoAnimator.SOLID
               || fillval == XtangoAnimator.HALF
               || fillval == XtangoAnimator.OUTLINE)
            ((AnimatorShape) icon).fillval(fillval);
         else System.err.println
            ("Cfill: fillval=" + fillval + " not implemented");
      } else {
         System.err.println("Cfill: icon is not fillable, icon=" + icon);
      }
   }

   public String toString() {
      return "Cfill: fillval=" + fillval;
   }
}

class Cdelete extends AnimatorAction {

   private String id = null;

   Cdelete(String id) {
      this.id = id;
   }

   void perform(AnimatorCanvas ac, Hashtable ht, Vector icons) {
      Object icon = null;
      synchronized (ht) {
         if ((icon = ht.remove(id)) == null) {
            System.err.println("Cdelete hashtable: no such id=" + id);
            return;
         }
      }
      synchronized (icons) {
         if(!icons.removeElement(icon)) {
            System.err.println("Cdelete vector: no such icon=" + icon);
            return;
         }
      }
   }

   public String toString() {
      return "Cdelete: id=" + id;
   }
}

class Cmove extends AnimatorAction implements Runnable {

   protected final static int ABSOLUTE = 1, RELATIVE = 2, ICON = 3;

   protected String id = null, id2 = null;
   protected float xpos = 0, ypos = 0;
   protected float xdelta = 0, ydelta = 0;
   protected AnimatorIcon icon = null, icon2 = null;
   protected int syncMode = 0;
   protected int moveMode = 0;
   protected int numMoveSteps = 0, frameDelay = 0;

   Cmove(String id, float xpos, float ypos, int syncMode,
      String id2, float xdelta, float ydelta, int moveMode) {
      this.id = id;
      this.id2 = id2;
      this.xpos = xpos;
      this.ypos = ypos;
      this.xdelta = xdelta;
      this.ydelta = ydelta;
      this.syncMode = syncMode;
      this.moveMode = moveMode;
   }

   Cmove(String id, float xpos, float ypos, int syncMode) {
      this(id, xpos, ypos, syncMode, null, 0.0f, 0.0f, ABSOLUTE);
   }

   void perform(AnimatorCanvas ac, Hashtable ht, Vector icons) {
      numMoveSteps = ac.numMoveSteps;
      frameDelay = ac.frameDelay;
      synchronized (ht) {
         if ((icon = (AnimatorIcon) ht.get(id)) == null) {
            System.err.println("Cjump hashtable: no such id=" + id);
            return;
         }
         if (id2 != null) {
            if ((icon2 = (AnimatorIcon) ht.get(id2)) == null) {
               System.err.println("CmoveTo hashtable: no such id2=" + id2);
               return;
            }
         }
      }
      if (syncMode == XtangoAnimator.SYNC) run();
      else if (syncMode == XtangoAnimator.ASYNC)
         (new Thread(this)).start();
      else
         System.err.println("Cmove: illegal syncMode=" + syncMode);
   }

   public void run() {
/*
 * When moving an icon, we need to synchronize on the icon's position so
 * that an asynchronous move followed closely by a move of the same icon
 * does not interleave.
 */
      synchronized (icon.position) {     // for ASYNC move, there may be
         Position p = icon.position();   // a race condition locking
         if (moveMode == RELATIVE) {     // the position with another
            xpos = (xdelta + p.x);       // move of same icon next
            ypos = (ydelta + p.y);
         } else if (moveMode == ICON) {
            Position p2 = null;
            synchronized (icon2.position) {
               p2 = icon2.position();
            }
            xpos = p2.x;
            ypos = p2.y;
         } else if (moveMode == ABSOLUTE) {
         } else {
            System.err.println("Cmove: illegal moveMode=" + moveMode);
         }
         float xchange = (xpos - p.x)/numMoveSteps;
         float ychange = (ypos - p.y)/numMoveSteps;
         for (int i = 1; i < numMoveSteps; i++) {
            icon.positionRel(xchange, ychange);
            try {
               Thread.sleep(frameDelay);
            } catch (InterruptedException e) {}
         }
         icon.position(xpos, ypos); // avoid summing round-off errors
      }
   }

   public String toString() {
      return "Cmove: id="+id+", xpos="+xpos+", ypos="+ypos
         +", syncMode="+syncMode;
   }
}

class CmoveRelative extends Cmove {

   CmoveRelative(String id, float xdelta, float ydelta, int syncMode) {
      super(id, 0.0f, 0.0f, syncMode, null, xdelta, ydelta, RELATIVE);
   }

   public String toString() {
      return "CmoveRelative: id="+id+", xdelta="+xdelta+", ydelta="+ydelta
         +", syncMode="+syncMode;
   }
}

class CmoveTo extends Cmove {

   CmoveTo(String id, String id2, int syncMode) {
      super(id, 0.0f, 0.0f, syncMode, id2, 0.0f, 0.0f, ICON);
   }

   public String toString() {
      return "CmoveTo: id="+id+", id2="+id2+", syncMode="+syncMode;
   }
}

class Cjump extends AnimatorAction {

   private String id = null;
   private float xpos = 0, ypos = 0;

   Cjump(String id, float xpos, float ypos) {
      this.id = id;
      this.xpos = xpos;
      this.ypos = ypos;
   }

   void perform(AnimatorCanvas ac, Hashtable ht, Vector icons) {
      AnimatorIcon icon = null;
      synchronized (ht) {
         if ((icon = (AnimatorIcon) ht.get(id)) == null) {
            System.err.println("Cjump hashtable: no such id=" + id);
            return;
         }
      }
      synchronized (icon.position) {
         icon.position(xpos, ypos);
      }
   }

   public String toString() {
      return "Cjump: id="+id+", xpos="+xpos+", ypos="+ypos;
   }
}

class CjumpRelative extends AnimatorAction {

   private String id = null;
   private float xdelta = 0, ydelta = 0;

   CjumpRelative(String id, float xdelta, float ydelta) {
      this.id = id;
      this.xdelta = xdelta;
      this.ydelta = ydelta;
   }

   void perform(AnimatorCanvas ac, Hashtable ht, Vector icons) {
      AnimatorIcon icon = null;
      synchronized (ht) {
         if ((icon = (AnimatorIcon) ht.get(id)) == null) {
            System.err.println("CjumpRelative hashtable: no such id=" + id);
            return;
         }
      }
      synchronized (icon.position) {
         icon.positionRel(xdelta, ydelta);
      }
   }

   public String toString() {
      return "CjumpRelative: id="+id+", xdelta="+xdelta+", ydelta="+ydelta;
   }
}

class CjumpTo extends AnimatorAction {

   private String id1 = null;
   private String id2 = null;

   CjumpTo(String id1, String id2) {
      this.id1 = id1;
      this.id2 = id2;
   }

   void perform(AnimatorCanvas ac, Hashtable ht, Vector icons) {
      AnimatorIcon icon1 = null, icon2 = null;
      Position p = null;
      synchronized (ht) {
         if ((icon1 = (AnimatorIcon) ht.get(id1)) == null) {
            System.err.println("CjumpTo hashtable: no such id1=" + id1);
            return;
         }
         if ((icon2 = (AnimatorIcon) ht.get(id2)) == null) {
            System.err.println("CjumpTo hashtable: no such id2=" + id2);
            return;
         }
      }
      synchronized (icon2.position) {
         p = icon2.position();
      }
      synchronized (icon1.position) {
         icon1.position(p.x, p.y);
      }
   }

   public String toString() {
      return "CjumpTo: id1="+id1+", id2="+id2;
   }
}

class CexchangePos extends AnimatorAction implements Runnable {

   private String id1 = null;
   private String id2 = null;
   private int syncMode = 0;
   private int numMoveSteps = 0, frameDelay = 0;
   private AnimatorIcon icon1 = null, icon2 = null;

   CexchangePos(String id1, String id2, int syncMode) {
      this.id1 = id1;
      this.id2 = id2;
      this.syncMode = syncMode;
   }

   void perform(AnimatorCanvas ac, Hashtable ht, Vector icons) {
      numMoveSteps = ac.numMoveSteps;
      frameDelay = ac.frameDelay;
      synchronized (ht) {
         if ((icon1 = (AnimatorIcon) ht.get(id1)) == null) {
            System.err.println("CexchangePos hashtable: no such id1=" + id1);
            return;
         }
         if ((icon2 = (AnimatorIcon) ht.get(id2)) == null) {
            System.err.println("CexchangePos hashtable: no such id2=" + id2);
            return;
         }
      }
      if (syncMode == XtangoAnimator.SYNC) run();
      else if (syncMode == XtangoAnimator.ASYNC)
         (new Thread(this)).start();
      else
         System.err.println("Cmove: illegal syncMode=" + syncMode);
   }

   public void run() {
      synchronized (icon1.position) {  // deadlock possible!
         synchronized (icon2.position) {
            Position p1 = icon1.position();
            Position p2 = icon2.position();
            float xchange = (p2.x - p1.x)/numMoveSteps;
            float ychange = (p2.y - p1.y)/numMoveSteps;
            for (int i = 1; i < numMoveSteps; i++) {
               icon1.positionRel(xchange, ychange);
               icon2.positionRel(-xchange, -ychange);
               try {
                  Thread.sleep(frameDelay);
               } catch (InterruptedException e) {}
            }
            icon1.position(p2.x, p2.y); // avoid summing round-off errors
            icon2.position(p1.x, p1.y);
         }
      }
   }

   public String toString() {
      return "CexchangePos: id1="+id1+",id2="+id2+", syncMode="+syncMode;
   }
}

class Cvis extends AnimatorAction {

   static final int VIS = 0;
   static final int RAISE = 1;
   static final int LOWER = 2;

   private String id = null;
   private int action = Cvis.VIS;

   Cvis(String id, int action) {
      this.id = id;
      this.action = action;
   }

   void perform(AnimatorCanvas ac, Hashtable ht, Vector icons) {
      Object icon = null;
      int newVisibility = Cvis.VIS;
      synchronized (ht) {
         if ((icon = ht.get(id)) == null) {
            System.err.println("Cvis hashtable: no such id=" + id);
            return;
         }
      }
      if (action == Cvis.VIS) { // move to end opposite of current visibility
         int oldVisibility = ((AnimatorIcon) icon).visibility();
         if (oldVisibility == Cvis.RAISE) newVisibility = Cvis.LOWER;
         else if (oldVisibility == Cvis.LOWER) newVisibility = Cvis.RAISE;
         else System.err.println("Cvis: oldVisibility=" + oldVisibility
            + " is incorrect");
      } else if (action == Cvis.RAISE) { // move to end of icons
         newVisibility = Cvis.RAISE;
      } else if (action == Cvis.LOWER) { // move to beginning of icons
         newVisibility = Cvis.LOWER;
      } else {
         System.err.println("Cvis: action=" + action + " not implemented");
      }
      ((AnimatorIcon) icon).visibility(newVisibility);
      synchronized (icons) {
         if (newVisibility == Cvis.RAISE) {
            if (icons.removeElement(icon)) {
               icons.addElement(icon);
            } else System.err.println("Cvis: missing icon=" + icon);
         } else if (newVisibility == Cvis.LOWER) {
            if (icons.removeElement(icon)) {
               icons.insertElementAt(icon, 0);
            } else System.err.println("Cvis: missing icon=" + icon);
         } else System.err.println("Cvis: newVisibility=" + newVisibility
            + " is incorrect (should not happen)");
      }
   }

   public String toString() {
      return "Cvis: action=" + action;
   }
}

class CswitchPos extends AnimatorAction {

   private String id1 = null, id2 = null;

   CswitchPos(String id1, String id2) {
      this.id1 = id1;
      this.id2 = id2;
   }

   void perform(AnimatorCanvas ac, Hashtable ht, Vector icons) {
      AnimatorIcon icon1 = null, icon2 = null;
      Position p1 = null, p2 = null;
      synchronized (ht) {
         if ((icon1 = (AnimatorIcon) ht.get(id1)) == null) {
            System.err.println("CswitchPos hashtable: no such id1=" + id1);
            return;
         }
         if ((icon2 = (AnimatorIcon) ht.get(id2)) == null) {
            System.err.println("CswitchPos hashtable: no such id2=" + id2);
            return;
         }
      }
      synchronized (icons) {  // sync on icons since we want both positions
                              // changed before allowing drawing again
         synchronized (icon1.position) {
            p1 = icon1.position();
         }
         synchronized (icon2.position) {
            p2 = icon2.position();
         }
         icon1.position(p2.x, p2.y);
         icon2.position(p1.x, p1.y);
      }
   }

   public String toString() {
      return "CswitchPos: id1="+id1+",id2="+id2;
   }
}

class CswapIds extends AnimatorAction {

   private String id1 = null;
   private String id2 = null;

   CswapIds(String id1, String id2) {
      this.id1 = id1;
      this.id2 = id2;
   }

   void perform(AnimatorCanvas ac, Hashtable ht, Vector icons) {
      AnimatorIcon icon1 = null, icon2 = null;
      synchronized (ht) {
         if ((icon1 = (AnimatorIcon) ht.get(id1)) == null) {
            System.err.println("CswapIds hashtable: no such id1=" + id1);
            return;
         }
         if ((icon2 = (AnimatorIcon) ht.get(id2)) == null) {
            System.err.println("CswapIds hashtable: no such id2=" + id2);
            return;
         }
         if (id1.equals(id2)) return;
         if (ht.remove(id1) == null)
            System.err.println("CswapIds: internal remove error, id1=" + id1);
         if (ht.remove(id2) == null)
            System.err.println("CswapIds: internal remove error, id2=" + id2);
         if (ht.put(id1, icon2) != null)
            System.err.println("CswapIds: internal put error, id1=" + id1);
         if (ht.put(id2, icon1) != null)
            System.err.println("CswapIds: internal put error, id2=" + id2);
      }
   }

   public String toString() {
      return "CswapIds: id1="+id1+",id2="+id2;
   }
}

class Cend extends AnimatorAction {

   Cend() { super(); }

   void perform(AnimatorCanvas ac, Hashtable ht, Vector icons) {
      System.out.println("XtangoAnimator: Push the Close or Quit button");
   }

   public String toString() {
      return "Cend";
   }
}

class Position { // point on canvas (0,0)..(1,1)

   float x = 0;
   float y = 0;

   Position() {
      this(0, 0);
   }

   Position(float x, float y) {
      super();
      this.x = x;
      this.y = y;
   }

   public synchronized String toString() {
      return "xpos=" + x + ", ypos " + y;
   }
}

class Size {

   float w = 0;
   float h = 0;

   Size() {
      this(0, 0);
   }

   Size(float w, float h) {
      super();
      this.w = w;
      this.h = h;
   }

   public synchronized String toString() {
      return ", xsize=" + w + ", ysize=" + h;
   }
}

abstract class AnimatorIcon extends AnimatorCommand {
                     // an icon can be a command to draw the icon
   protected String id = null;
   final Position position = new Position(); // need to synchronize on this
   protected Color colorval = Color.black;
   protected Color colorvalHALF = Color.gray; // only used in triangles now
   protected int visibility = Cvis.RAISE;

   protected AnimatorIcon(String id, float xpos, float ypos,
         Color colorval) {
      this.id = id;
      this.position.x = xpos; this.position.y = ypos;
      this.colorval = colorval;
      int R = colorval.getRed();
      int G = colorval.getGreen();
      int B = colorval.getBlue();
      float[] HSB = Color.RGBtoHSB(R, G, B, new float[3]);
      this.colorvalHALF =
/*
 * Half the saturation with same hue (of course) and brightness works fine
 * for red, green, blue, yellow, cyan, magenta, but not so well with pink
 * and not at all for black on Windows 95.  Use only for HALF fillval in
 * triangles for now.
 */
         new Color(Color.HSBtoRGB(HSB[0], 0.5f*HSB[1], HSB[2]));
   }

   synchronized void colorval(Color colorval) {
      this.colorval = colorval;
   }

   synchronized int visibility() { return visibility; }

   synchronized void visibility(int visibility) {
      this.visibility = visibility;
   }

   synchronized Color colorval() { return colorval; }

   synchronized Position position()       // return consistent snapshot of
      { return new Position(position.x, position.y); } // current position

   synchronized void position(float xpos, float ypos) {
      position.x = xpos;  position.y = ypos;
   }

   synchronized void positionRel(float xdelta, float ydelta) {
      position.x += xdelta;  position.y += ydelta;
   }

   void add(Hashtable ht, Vector icons) {
      Object old = null;
      synchronized (ht) {
         if (ht.containsKey(id)) {
            System.err.println
               ("CommandThread add: ignoring duplicate id=" + id);
            return;
         }
         old = ht.put(id, this);
      }
      if (old == null) {
         synchronized (icons) {
            icons.addElement(this);
         }
      } else {
         System.err.println
            ("CommandThread add: non-null old should not have happened!!");
      }
   }

      // JLS says cannot have both abstract and synchronized (page 157).
      // Funny: Microsoft JVM catches this but Sun's JDK <=1.1.4 does not.
   abstract /* synchronized */ void draw(AnimatorCanvas ac, Graphics g);

   public synchronized String toString() {
      return "id="+id+", "+position+", colorval="+colorval;
   }
}

abstract class AnimatorShape extends AnimatorIcon {

   protected static int numHalf = 10;

   protected int fillval = XtangoAnimator.SOLID;

   protected AnimatorShape(String id, float xpos, float ypos,
         Color colorval, int fillval) {
      super(id, xpos, ypos, colorval);
      this.fillval = fillval;
   }

   synchronized void fillval(int fillval) {
      this.fillval = fillval;
   }

   synchronized int fillval() { return fillval; }

   abstract /* synchronized */ void draw(AnimatorCanvas ac, Graphics g);

   public synchronized String toString() {
      return super.toString()+", fillval="+fillval;
   }
}

class Icircle extends AnimatorShape {

   protected float radius = 0;

   Icircle(String id, float xpos, float ypos, float radius,
         Color colorval, int fillval) {
      super(id, xpos, ypos, colorval, fillval);
      this.radius = radius;
   }

   synchronized void draw(AnimatorCanvas ac, Graphics g) {
      g.setColor(colorval);
      if (fillval == XtangoAnimator.SOLID) { // translate from center
         g.fillOval(                         // to upper-left
            ac.scaleX(position.x-radius), ac.scaleY(position.y+radius),
            ac.scaleR(2*radius), ac.scaleR(2*radius));
      } else if (fillval == XtangoAnimator.OUTLINE) {
         g.drawOval(
            ac.scaleX(position.x-radius), ac.scaleY(position.y+radius),
            ac.scaleR(2*radius), ac.scaleR(2*radius));
      } else if (fillval == XtangoAnimator.HALF) {
         float inc = radius/(float)numHalf;
         for (int i = 1; i < numHalf; i++) {
            float rad = inc*i;
            g.drawOval(
               ac.scaleX(position.x-rad), ac.scaleY(position.y+rad),
               ac.scaleR(2*rad), ac.scaleR(2*rad));
         }
         g.drawOval(
            ac.scaleX(position.x-radius), ac.scaleY(position.y+radius),
            ac.scaleR(2*radius), ac.scaleR(2*radius));
      } else {
         System.err.println("Icircle.draw(): fillval=" + fillval
            + " not yet implemented");
      }
   }

   public synchronized String toString() {
      return super.toString()+", radius="+radius;
   }
}

class Iline extends AnimatorIcon {

   protected Size size = null;
   protected int widthval = XtangoAnimator.THIN;

   Iline(String id, float xpos, float ypos, float xsize, float ysize,
         Color colorval, int widthval) {
      super(id, xpos, ypos, colorval);
      this.size = new Size(xsize, ysize);
      this.widthval = widthval;
   }

   synchronized void draw(AnimatorCanvas ac, Graphics g) {
      g.setColor(colorval);
      if (widthval == XtangoAnimator.THIN) {
         g.drawLine(
            ac.scaleX(position.x), ac.scaleY(position.y),
            ac.scaleX(position.x+size.w), ac.scaleY(position.y+size.h));
      } else if (widthval == XtangoAnimator.MEDTHICK) {
         g.drawLine(
            ac.scaleX(position.x), ac.scaleY(position.y),
            ac.scaleX(position.x+size.w), ac.scaleY(position.y+size.h));
         g.drawLine(
            ac.scaleX(position.x)+1, ac.scaleY(position.y)+1,
            ac.scaleX(position.x+size.w)+1, ac.scaleY(position.y+size.h)+1);
         g.drawLine(
            ac.scaleX(position.x)+1, ac.scaleY(position.y)-1,
            ac.scaleX(position.x+size.w)+1, ac.scaleY(position.y+size.h)-1);
         g.drawLine(
            ac.scaleX(position.x)-1, ac.scaleY(position.y)-1,
            ac.scaleX(position.x+size.w)-1, ac.scaleY(position.y+size.h)-1);
         g.drawLine(
            ac.scaleX(position.x)-1, ac.scaleY(position.y)+1,
            ac.scaleX(position.x+size.w)-1, ac.scaleY(position.y+size.h)+1);
      } else if (widthval == XtangoAnimator.THICK) {
         g.drawLine(
            ac.scaleX(position.x), ac.scaleY(position.y),
            ac.scaleX(position.x+size.w), ac.scaleY(position.y+size.h));
         g.drawLine(
            ac.scaleX(position.x)+1, ac.scaleY(position.y)+1,
            ac.scaleX(position.x+size.w)+1, ac.scaleY(position.y+size.h)+1);
         g.drawLine(
            ac.scaleX(position.x)+1, ac.scaleY(position.y)-1,
            ac.scaleX(position.x+size.w)+1, ac.scaleY(position.y+size.h)-1);
         g.drawLine(
            ac.scaleX(position.x)-1, ac.scaleY(position.y)-1,
            ac.scaleX(position.x+size.w)-1, ac.scaleY(position.y+size.h)-1);
         g.drawLine(
            ac.scaleX(position.x)-1, ac.scaleY(position.y)+1,
            ac.scaleX(position.x+size.w)-1, ac.scaleY(position.y+size.h)+1);
         g.drawLine(
            ac.scaleX(position.x)+2, ac.scaleY(position.y)+2,
            ac.scaleX(position.x+size.w)+2, ac.scaleY(position.y+size.h)+2);
         g.drawLine(
            ac.scaleX(position.x)+2, ac.scaleY(position.y)-2,
            ac.scaleX(position.x+size.w)+2, ac.scaleY(position.y+size.h)-2);
         g.drawLine(
            ac.scaleX(position.x)-2, ac.scaleY(position.y)-2,
            ac.scaleX(position.x+size.w)-2, ac.scaleY(position.y+size.h)-2);
         g.drawLine(
            ac.scaleX(position.x)-2, ac.scaleY(position.y)+2,
            ac.scaleX(position.x+size.w)-2, ac.scaleY(position.y+size.h)+2);
      } else {
         System.err.println("Iline.draw(): widthval=" + widthval
            + " not yet implemented");
      }
   }

   public synchronized String toString() {
      return super.toString()+size+", widthval="+widthval;
   }
}

class Irectangle extends AnimatorShape {

   protected Size size = null;

   Irectangle(String id, float xpos, float ypos, float xsize, float ysize,
         Color colorval, int fillval) {
      super(id, xpos, ypos, colorval, fillval);
      this.size = new Size(xsize, ysize);
   }

   synchronized void draw(AnimatorCanvas ac, Graphics g) {
      g.setColor(colorval);
      if (fillval == XtangoAnimator.SOLID) { // translate from lower-left
         g.fillRect(                         // corner to upper-left
            ac.scaleX(position.x), ac.scaleY(position.y+size.h),
            ac.scaleR(size.w), ac.scaleR(size.h));
      } else if (fillval == XtangoAnimator.HALF) {
         float incw = size.w/(float)numHalf;
         float inch = size.h/(float)numHalf;
         for (int i = 1; i < numHalf; i++) {
            float sizew = incw*i;
            float sizeh = inch*i;
            g.drawRect(
               ac.scaleX(position.x), ac.scaleY(position.y+sizeh),
               ac.scaleR(sizew), ac.scaleR(sizeh));
         }
         g.drawRect(
            ac.scaleX(position.x), ac.scaleY(position.y+size.h),
            ac.scaleR(size.w), ac.scaleR(size.h));
      } else if (fillval == XtangoAnimator.OUTLINE) {
         g.drawRect(
            ac.scaleX(position.x), ac.scaleY(position.y+size.h),
            ac.scaleR(size.w), ac.scaleR(size.h));
      } else {
         System.err.println("Irectangle.draw(): fillval=" + fillval
            + " not yet implemented");
      }
   }

   public synchronized String toString() {
      return super.toString()+size;
   }
}

class Itriangle extends AnimatorShape {

   private Position p2 = null, p3 = null;

   Itriangle(String id, float v1x, float v1y, float v2x, float v2y,
         float v3x, float v3y, Color colorval, int fillval) {
      super(id, v1x, v1y, colorval, fillval);
      this.p2 = new Position(v2x, v2y);
      this.p3 = new Position(v3x, v3y);
   }

   synchronized void position(float xpos, float ypos) { // override
      // update p2, p3
      float xdelta = xpos - position.x;
      float ydelta = ypos - position.y;
      p2.x += xdelta; p2.y += ydelta;
      p3.x += xdelta; p3.y += ydelta;
      position.x = xpos;  position.y = ypos;
   }

   synchronized void positionRel(float xdelta, float ydelta) { // override
      position.x += xdelta;  position.y += ydelta;
      p2.x += xdelta;  p2.y += ydelta;
      p3.x += xdelta;  p3.y += ydelta;
   }

   synchronized void draw(AnimatorCanvas ac, Graphics g) {
/*
 * For triangles, in contrast to circles and rectangles where concentric
 * outlines are done for HALF, we will try half the saturation.
 */
      if (fillval == XtangoAnimator.HALF) g.setColor(colorvalHALF);
      else g.setColor(colorval);
      int[] X = {ac.scaleX(position.x), ac.scaleX(p2.x), ac.scaleX(p3.x),
                 ac.scaleX(position.x)};
      int[] Y = {ac.scaleY(position.y), ac.scaleY(p2.y), ac.scaleY(p3.y),
                 ac.scaleY(position.y)};
      if (fillval == XtangoAnimator.SOLID) {
         g.fillPolygon(X, Y, 4);
      } else if (fillval == XtangoAnimator.HALF) {
         g.fillPolygon(X, Y, 4);
      } else if (fillval == XtangoAnimator.OUTLINE) {
         g.drawPolygon(X, Y, 4);
      } else {
         System.err.println("Irectangle.draw(): fillval=" + fillval
            + " not yet implemented");
      }
   }

   public synchronized String toString() {
      return super.toString()+p2+p3;
   }
}

class Itext extends AnimatorIcon {

   public static final int SMALL = 0;
   public static final int NORMAL = 1;
   public static final int BIG = 2;

   protected static final Font fSMALL  =
      new Font("TimesRoman", Font.PLAIN, 10);
   protected static final Font fNORMAL =
      new Font("TimesRoman", Font.PLAIN, 16);
   protected static final Font fBIG    =
      new Font("TimesRoman", Font.PLAIN, 22);

   protected boolean centered = false;
   protected String string = null;
   protected int size = Itext.NORMAL;

   Itext(String id, float xpos, float ypos, boolean centered,
         Color colorval, String string, int size) {
      super(id, xpos, ypos, colorval);
      this.centered = centered;
      this.string = string;
      this.size = size;
   }

   synchronized void string(String string) {
      this.string = string;
   }

   synchronized String string() { return string; }

   synchronized void draw(AnimatorCanvas ac, Graphics g) {
      g.setColor(colorval);
      if (size == Itext.SMALL) g.setFont(fSMALL);
      else if (size == Itext.NORMAL) g.setFont(fNORMAL);
      else if (size == Itext.BIG) g.setFont(fBIG);
      else System.err.println("Itext draw(): no size=" + size);
      if (centered) {
         FontMetrics fm = g.getFontMetrics();
         int length = fm.stringWidth(string);
         int height = fm.getAscent() - fm.getDescent();
         g.drawString(string, ac.scaleX(position.x)-length/2,
            ac.scaleY(position.y)+height/2);
      } else {
         g.drawString(string, ac.scaleX(position.x), ac.scaleY(position.y));
      }
   }

   public synchronized String toString() {
      return super.toString()+", centered="+centered+", string="+string
         +", size="+size;
   }
}
