/**  file:     ObjectDraw.java
 *   author:   Robert M. Keller
 *   copyright 1997 by Robert M. Keller, all rights reserved
 *   purpose:  Object-drawing applet (for pedagogical purposes)
 *
 *   This is a simple drawing program.  It illustrates several object-oriented
 *   ideas, including inheritance.
 *
 *   The user is able to draw various shapes with the mouse.  The shapes
 *   can be moved, grouped hierarchically, cut, and pasted, etc.
 *
 *   The user can also have a textual description (S expression) of any 
 *   object appear in a separate window.
 *
 *   Currently there are five choice menus:
 *
 *     Stay/Revert:   With Revert, the program always goes back to Move
 *                    mode after doing an operation.  With Stay, it stays
 *                    in the same mode.
 *
 *      Mode:         Selects between Move, Draw, Cut, Paste, etc.
 *
 *      Shape:        Selects the shape to be drawn.
 *
 *      Fill:         Selects whether the shape is filled or outline.
 *
 *      Color:        Selects the color
 **/

/* $Revision: 1.5 $, $Date: 1997/10/16 00:37:15 $ */

/*
    Method:

    Each type of shape is a class, as defined by the following inheritance 
    hierarchy:

                              Rectangle
                                  |
                                  |
                                Shape 
                                  |
                                  |
               ----------------------------------------
               |            |            |            |
               |            |            |            |
              Box         Oval         Line         Group


    Rectangle is the one defined in package java.awt.  It is not a graphic
    object as such, but simply defines the rectangular region which bounds
    the object.  It is used to determine where the mouse is clicked (via
    the 'inside' method).

    Class Shape is a relatively abstract class.  It carries values common to
    all shape classes, such as color and filled.

    Even though Line and Group ignore color and fill, it was decided for 
    simplicity not to make a separate node in the hierarchy for unfillable 
    objects.

    A Group contains a set of shapes, currently represented using the
    class java.util.Vector.  

    Continuous curves are represented by (generally large) groups of small
    lines.

    Each class has its own draw method, which translate into appropriate
    calls for java.awt.  In the case of Group, the draw method calls the
    draw methods for each shape in the group.

    When a shape is drawn, cut, etc. the entire image is redrawn.  This
    eliminates the need to have an erase method.
*/

import java.applet.*;           // applet classes
import java.awt.*;              // Abstract Window Toolkit classes
import java.util.*;             // utility classes

/**
 *   ObjectDraw is the applet
 **/ 

public class ObjectDraw extends Applet
  {
  Graphics graphics;                        // graphics buffer
  Image image;                              // Image for graphics
  
  Vector allShapes = new Vector();          // vector of all shapes in drawing

  Shape currentShape;                       // the currently selected shape

  Shape justCut;                            // the shape just cutd

  boolean continuous;                       // used for free-form drawing
  Vector curve;

  int xDown, yDown;                         // where the mouse was pressed

  int xCut, yCut;                           // where shape was cut

  int grid = 1;                             // grid size
  int maxGrid = 64;                         // maximum grid size

  DefTable descriptions;                    // table of descriptions
  DescriptionFrame descriptionFrame;        // frame for descriptions

  // widgets

  static public Color backgroundColor = Color.white,
                      groupingColor   = Color.black;

  static public Font MainFont = new Font("Helvetica", Font.BOLD, 18);

  Slider gridSlider                        // slider for grid size
      = new Slider(this, "Grid", MainFont, 1, 1, maxGrid, 2);

  Choice shapeChoice = new Choice();        // choice menu for shape
  int chosenShape = BOX;                    // default for menu
  int drawShape = chosenShape;              // drawShape is shaped to be DRAWN
                                            // (as opposed to for grouping)

  Choice modeChoice = new Choice();         // choice menu for mode
  int chosenMode = DRAW;                    // default mode
  
  Choice colorChoice = new Choice();        // choice menu for color
  Color chosenColor = Color.black;          // default color
  Vector allColors = new Vector();          // vector of all colors in menu

  Choice fillChoice = new Choice();         // choice menu for fill
  boolean fillMode = false;

  Choice stayChoice = new Choice();         // choice menu for stay/revert
  boolean stayMode = true;

  // table of drawing modes and corresponding indices (for use in switches)
  
  static String mode[] =
    { "Move", "Draw",   "Cut", "CutAll", "Paste", "Copy", "Group", 
      "GroupAll", "Ungroup", "Lower", "Raise", "Describe" };

  static final int 
       MOVE = 0, DRAW = 1, CUT = 2, CUTALL = 3, PASTE = 4, COPY = 5, GROUP = 6,
       GROUPALL = 7, UNGROUP = 8, LOWER = 9, RAISE = 10, DESCRIBE = 11;


  // table of shapes and corresponding indices (for use in switches)

  static String shape[] = {"Box",   "Line",   "Curve",   "Oval"};
  static final int          BOX = 0, LINE = 1, CURVE = 2, OVAL = 3;

  /**
   *   Initialize the applet.
   **/

  public void init()
    {
    setLayout(new FlowLayout(FlowLayout.LEFT)); 
    setBackground(backgroundColor);             // set the background color

    image = createImage(size().width, size().height);   
    graphics = image.getGraphics();             // make a graphics buffer

    // set up widgets

    stayChoice.setFont(MainFont);               // add Stay/Revert menu
    stayChoice.addItem("Stay");
    stayChoice.addItem("Revert");
    add(stayChoice);

    modeChoice.setFont(MainFont);               // add mode choice menu
    for( int i = 0; i < mode.length; i++ )      // add mode choices
      modeChoice.addItem(mode[i]);
    setMode(chosenMode);
    add(modeChoice);				// add choice menu to applet

    shapeChoice.setFont(MainFont);              // add shape choice menu
    for( int i = 0; i < shape.length; i++ )     // add shape choices
      shapeChoice.addItem(shape[i]);
    add(shapeChoice);				// add shape menu to applet

    fillChoice.setFont(MainFont);
    fillChoice.addItem("NoFill");
    fillChoice.addItem("Fill");
    add(fillChoice);				// add fill menu to applet

    colorChoice.setFont(MainFont);              // add color choice menu
    addColor("Black",     Color.black);         // add colors to menu
    addColor("Red",       Color.red);           // and to color String table 
    addColor("Blue",      Color.blue);
    addColor("Orange",    Color.orange);
    addColor("Yellow",    Color.yellow);
    addColor("Green",     Color.green);
    addColor("Cyan",      Color.cyan);
    addColor("Magenta",   Color.magenta);
    addColor("Gray",      Color.gray);
    addColor("DarkGray",  Color.darkGray);
    addColor("LightGray", Color.lightGray);
    addColor("Pink",      Color.pink);
    addColor("White",     Color.white);
    add(colorChoice);
    }

  //
  // Clear graphics
  //

  void clearGraphics()
    {
    graphics.clearRect(0, 0, size().width, size().height);
    }


  //
  // Draw all items in the window.
  //

  void display()
    {
    for( Enumeration e = allShapes.elements(); e.hasMoreElements(); )
      {
      ((Shape)e.nextElement()).draw(graphics);
      }
    graphics.setColor(Color.black);
    graphics.drawRect(0, 0, size().width-1, size().height-1);	// border
    repaint();
    }


  //
  // Redraw all items in the window.
  //

  void redisplay()
    {
    clearGraphics();
    display();
    }


  //
  // Start drawing a new shape.
  //

  void startShape(int x, int y, Color color, boolean fillMode)
    {
    switch( chosenShape )
      {
      case BOX:   currentShape = new Box(x, y, color, fillMode);  break;

      case OVAL:  currentShape = new Oval(x, y, color, fillMode); break;

      case LINE:  currentShape = new Line(x, y, color);           break;

      case CURVE: currentShape = new Line(x, y, color);           break;
      }
    redisplay();
    currentShape.draw(graphics);
    repaint();    
    }


  //
  // Grow the current shape as the mouse is dragged.
  //

  void growShape(int x, int y)
    {
    redisplay();
    currentShape.width = x - currentShape.x;    // adjust size
    currentShape.height = y - currentShape.y;
    currentShape.draw(graphics);
    repaint();
    }


  //
  // Complete the current shape, adding it to allShapes.
  //

  void finishShape()
    {
    currentShape.complete();
    allShapes.addElement(currentShape);
    }


  // 
  // Move the shape to another location.
  // 

  void moveShape(int x, int y)
    {
    if( currentShape == null )
      return;
    currentShape.moveBy(x - xDown, y - yDown);
    xDown = x;
    yDown = y;
    }


  //
  // Set the mode as specified and set the modeChoice menu
  //

  void setMode(int i)
    {
    switch( i )
      {
      case GROUPALL:
        all();          
        group(2);
        i = chosenMode; // don't change mode for GroupAll
        break;

      case CUTALL:
        all();
        group(1);
        if( currentShape == null )
          break;
        cut(currentShape.x, currentShape.y);
        i = chosenMode; // don't change mode for CutAll
        redisplay();
        break;
      }
    chosenMode = i;
    modeChoice.select(mode[i]);
    }


  //
  // Look for shape at a given position, setting currentShape to one found.
  // Return true if found, otherwise false.
  //

  boolean findShape(int x, int y)
    {
    for (int i = allShapes.size()-1; i >= 0; i-- )  // start at end of Vector
      {
      Shape s = (Shape)allShapes.elementAt(i);
      if( s.inside(x, y) )
        {
        currentShape = s;
        return true;
        }
      }
    currentShape = null;
    return false;
    }

 
  //
  // Move current shape so that it is displayed first, selected last.
  //

  void lower()
    {
    if( currentShape == null )
      return;
    allShapes.removeElement(currentShape);
    allShapes.insertElementAt(currentShape, 0);
    }


  //
  // Move current shape so that it is displayed last, selected first.
  //

  void raise()
    {
    if( currentShape == null )
      return;
    allShapes.removeElement(currentShape);
    allShapes.addElement(currentShape);
    }


  //
  // Cut the selected shape.
  //

  void cut(int x, int y)
    {
    if( currentShape == null )
      return;
    xCut = x;
    yCut = y;
    allShapes.removeElement(currentShape);
    justCut = currentShape;
    }


  //
  // Paste pastes the most recently cut shape, if any.
  // with x and y specifying an offset from that position

  void paste(int x, int y)
    {
    if( justCut == null )
      return;
    currentShape = justCut.copy();
    allShapes.addElement(currentShape);
    currentShape.moveBy(x, y);
    }


  //
  // Group surrounded shapes into one.
  //

  void group(int minGroupSize)
    {
    Vector members = establishGroup(currentShape);
    if( members.size() < minGroupSize )
      {
      // a little warning: don't make groups of size 0 or 1
      System.out.println("group of size " + members.size() + " not created");
      currentShape = null;
      return;
      }

    // remove members in group from allShapes
    for( Enumeration e = members.elements(); e.hasMoreElements(); )
      allShapes.removeElement(e.nextElement());

    // make the group the current shape
    currentShape = new Group(currentShape.x, currentShape.y, this, members);
    allShapes.addElement(currentShape);
    }


  //
  // Ungroup selected shape if it is a group.
  //

  void ungroup()
    {
    if( currentShape == null )
      return;
    if( !(currentShape instanceof Group) )
      return;
    allShapes.removeElement(currentShape);

    Group group = (Group)currentShape;

    if( group.shared )
      {
      // clone Vector first so as not to mess up sharing
      group = group.deepCopy();
      }

    // add component members to allShapes, setting their coordinates

    for( Enumeration e = group.members.elements(); e.hasMoreElements();)
      {
      Shape s = ((Shape)e.nextElement());
      s.moveBy(currentShape.x, currentShape.y);
      allShapes.addElement(s);
      }
        
    // no shape is current after ungrouping

    currentShape = null;
    }


  //
  // Make a Vector of the shapes contained inside shape s.
  //

  Vector establishGroup(Shape s)
    {
    Vector v = new Vector();
    for( Enumeration e = allShapes.elements(); e.hasMoreElements(); )
      {
      Shape t = (Shape)e.nextElement();
      // check whether t is inside boundary of s
      if(    t.x >= s.x 
          && t.y >= s.y
          && t.x + t.width  <= s.x + s.width
          && t.y + t.height <= s.y + s.height )
        {
        v.addElement(t);        // put t on new Vector
        }
      }
    return v;
    }


  // All makes a shape which surrounds all shapes (e.g. for grouping)

  void all()
    {
    currentShape = new Box(0, 0, groupingColor, false);
    currentShape.width = size().width;
    currentShape.height = size().height;
    }


  //
  //  revert sets the mode back to Move if stayMode is false
  //

  void revert()
    {
    if( !stayMode ) 
      {
      setMode(MOVE);
      }
    }


  /**
   *   mouseDown is called when the mousebutton is depressed.
   **/

  public boolean mouseDown(Event e, int x, int y)
    {
    x = (x/grid)*grid;
    y = (y/grid)*grid;
    xDown = x; yDown = y;

    switch( chosenMode )
      {
      case MOVE:
           findShape(x, y);
           break;

      case DRAW:
           continuous = false;
           chosenShape = drawShape;
           if( chosenShape == CURVE )
             {
             continuous = true;
             curve = new Vector();
             }
           startShape(x, y, chosenColor, fillMode);
           break;

      case GROUP:     
           chosenShape = BOX;
           startShape(x, y, groupingColor, false);
           break;

      case DESCRIBE:
           findShape(x, y);
           if( currentShape == null )
             break;
           if( descriptionFrame == null )
             {
             descriptionFrame = new DescriptionFrame(this);
             }

           // create description table
           descriptions = new DefTable();

           // clear text of frame, then add descriptions
           descriptionFrame.clearText();
           currentShape.describe(descriptionFrame.shape, this);
           descriptions.describe(descriptionFrame.defs, this);
           descriptionFrame.update();
           break;
      }
    return true;
    }


  /**
   *   mouseDrag is called when the mouse is dragged.
   **/

  public boolean mouseDrag(Event e, int x, int y)
    {
    x = (x/grid)*grid;
    y = (y/grid)*grid;
    switch( chosenMode )
      {
      case MOVE:
           moveShape(x, y);
           redisplay();
           break;

      case DRAW:
           if( continuous )
             {
             // finish this shape and start a new one, adding to curve
             growShape(x, y);
             finishShape();
             curve.addElement(currentShape);
             startShape(x, y, chosenColor, fillMode);
             }
           else
             {
             growShape(x, y);
             }
           break;

      case GROUP:
           growShape(x, y);
           break;
      }
    return true;
    }


  /**
   *   mouseUp is called when the mousebutton is released.
   **/

  public boolean mouseUp(Event v, int x, int y)
    {
    x = (x/grid)*grid;
    y = (y/grid)*grid;
    switch( chosenMode )
      {
      case MOVE:
           currentShape = null;
           break;

      case DRAW:
           finishShape();
           if( continuous )
             {
             // finish the curve
             curve.addElement(currentShape);
             currentShape = 
               new Group(currentShape.x, currentShape.y, this, curve);

             // remove members in group from allShapes
             for( Enumeration e = curve.elements(); e.hasMoreElements(); )
               allShapes.removeElement(e.nextElement());

             allShapes.addElement(currentShape);
             }
           break;

      case CUT:
           if( findShape(x, y) )
             cut(x, y);
           break;

      case PASTE:
           paste(x-xCut, y-yCut);
           break;

      case COPY:
           if( findShape(x, y) )
             {
             cut(x, y);
             paste(0, 0);
             paste(grid, -grid);
             }
           break;

      case GROUP:
           group(2);
           break;

      case UNGROUP:
           findShape(x, y);
           ungroup();
           break;

      case LOWER:
           findShape(x, y);
           lower();
           findShape(x, y);
           break;

      case RAISE: 
           findShape(x, y);
           raise();
           findShape(x, y);
           break;
      }
    revert();
    redisplay();
    return true;
    }


  /**
   *   action handles events targeted for buttons, etc.
   *   These events do not go to mouseUp etc.
   **/

  public boolean action(Event event, Object arg)
    {
    if( event.target == stayChoice )
      {
      stayMode = ((String)arg == "Stay");
      }

    else if( event.target == modeChoice )
      {
      setMode(findString((String)arg, mode));
      }

    else if( event.target == shapeChoice )
      {
      drawShape = chosenShape = findString((String)arg, shape);
      setMode(DRAW);            // setting shape assumes user wants to draw
      }

    else if( event.target == fillChoice )
      {
      fillMode = ((String)arg == "Fill" );
      setMode(DRAW);            // setting fill assumes user wants to draw
      }

    else if( event.target == colorChoice )
      {
      chosenColor = colorFromString((String)arg);
      setMode(DRAW);            // setting color assumes user wants to draw
      }

    return super.action(event, arg);    // Delegate all other actions to super.
    }


  /** 
    * Set mode or perform action when user moves a slider 
   **/

  public boolean handleEvent(Event event)
    {
    if( event.target == gridSlider.scroll )
      {
      grid = (int)gridSlider.getValue(); // grid slider
      return true;
      }
    return super.handleEvent(event);    // Delegate all other actions to super.
    }


  /**
   *   update is implicitly called by repaint()
   *   It calls paint(Graphics)
   **/

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


  /**
   *   paint(Graphics) is is called by update(Graphics)
   **/

  public void paint(Graphics g)
    {
    g.drawImage(image, 0, 0, null);
    g.setColor(Color.black);
    }



  // create an entry in String-Color correspondence table

  void addColor(String name, Color color)
    {
    colorChoice.addItem(name);
    allColors.addElement(new Pair(name, color));
    }

    
  // return a Color object given a String naming the color

  public Color colorFromString(String string)
    {
    for( Enumeration e = allColors.elements(); e.hasMoreElements(); )
     {
     Pair p = (Pair)(e.nextElement());
     if( string.equals(p.first) )
       return (Color)(p.second);
     }

    System.err.println("*** color not understood: " + string + " using black");
    return Color.black;
    }


  // return a String from a Color object

  public String colorToString(Color color)
    {
    for( Enumeration e = allColors.elements(); e.hasMoreElements(); )
     {
     Pair p = (Pair)(e.nextElement());
     if( color.equals(p.second) )
       return (String)(p.first);
     }
    return color.toString();
    }


  static int findString(String toBeFound, String array[])
    {
    for( int i = 0; i < array.length; i++ )
      {
      if( toBeFound.equals(array[i]) )
        return i;
      }
    return -1;
    }


  } // ObjectDraw
    

//
// The abstract class Shape
//

abstract class Shape extends java.awt.Rectangle
  {
  Color color;          // the color of the shape

  boolean filled;       // whether the shape is filled or outline

  // Constructor; width and height are set when the shape size is finally
  // determined

  Shape(int x, int y, Color color, boolean filled)
    {
    this.x = x;
    this.y = y;
    this.color = color;
    this.filled = filled;
    }

  Shape(Shape orig)  	// Copy constructor: Create a copy of a shape
    {
    x = orig.x;
    y = orig.y;
    width = orig.width;
    height = orig.height;
    color = orig.color;
    filled = orig.filled;
    }


  // move shape by indicated increment

  void moveBy(int dx, int dy)
    {
    x += dx;
    y += dy;
    }


  // complete deals with negative widths and heights when a shape is finished
  // It is overridden for Box and Oval and is a no-op for Line and Group.

  void complete()
    { }

  abstract Shape copy();                        // returns copy of shape
  abstract void draw(int x, int y, Graphics g); // draws with added offset x, y
  abstract void describe(int indentation, StringBuffer buff, ObjectDraw app);

  void draw(Graphics g)                         // draw with no offset
    {
    draw(0, 0, g);
    }

  void describe(StringBuffer buff, ObjectDraw app) // print with no indentation
    {
    describe(0, buff, app);
    } 

  static void indent(int indentation, StringBuffer buff)
    {
    for( int i = 0; i < indentation; i++ )
      buff.append(" ");
    }
  }  // Shape


//
// A Box shape
//

class Box extends Shape
  {
  Box(int x, int y, Color color, boolean filled)
    {
    super(x, y, color, filled);
    }

  Box(Box box)                  // copy constructor
    {
    super((Shape)box);
    }

  Shape copy()
    {
    return new Box(this);
    }

  void draw(int x, int y, Graphics g)       // draw a Box
    {
    // note that x, y, width, and height are local

    x = this.x + x;             // change from offset to absolute
    y = this.y + y;

    int width = this.width;     
    int height = this.height;

    if( width < 0 )             // accomodate negative widths and heights
      {
      width = -width;
      x -= width;
      }
    if( height < 0 )
      {
      height = -height;
      y -= height;
      }

    g.setColor(color);
    if( filled )
      g.fillRect(x, y, width, height);    
    else
      g.drawRect(x, y, width, height);    
    }

  // complete fixes width and height if either is negative.

  void complete()
    {
    if( width < 0 )
      {
      x += width;
      width = -width;
      }
    if( height < 0 )
      {
      y += height;
      height = -height;
      }
    }

  void describe(int indentation, StringBuffer buff, ObjectDraw app)
    {
    indent(indentation, buff);
    buff.append("(" + (filled ? "filled " : "")
                    + "rectangle " + app.colorToString(color) + " "
                    + x + " " + y + " "
                    + width + " " + height + ")\n"); 
    }
  }  // Box


//
// an Oval Shape
//

class Oval extends Shape
  {
  Oval(int x, int y, Color color, boolean filled)
    {
    super(x, y, color, filled);
    }

  Oval(Oval oval)               // copy constructor
    {
    super((Shape)oval);
    }

  Shape copy()
    {
    return new Oval(this);
    }

  void draw(int x, int y, Graphics g)       // draw an Oval
    {
    // note that x, y, width, and height are local

    x = this.x + x;             // change from offset to absolute
    y = this.y + y;

    int width = this.width;     
    int height = this.height;

    if( width < 0 )             // accomodate negative widths and heights
      {
      width = -width;
      x -= width;
      }
    if( height < 0 )
      {
      height = -height;
      y -= height;
      }

    g.setColor(color);
    if( filled )
      g.fillOval(x, y, width, height);    
    else
      g.drawOval(x, y, width, height);    
    }

  // complete fixes width and height if either is negative.

  void complete()
    {
    if( width < 0 )
      {
      x += width;
      width = -width;
      }
    if( height < 0 )
      {
      y += height;
      height = -height;
      }
    }

  void describe(int indentation, StringBuffer buff, ObjectDraw app)
    {
    indent(indentation, buff);
    buff.append("(" + (filled ? "filled " : "")
                    + "oval " + app.colorToString(color) + " "
                    + x + " " + y + " "
                    + width + " " + height + ")\n"); 
    }
  }  // Oval


//
// A straight line
//

class Line extends Shape
  {
  Line(int x, int y, Color color)
    {
    super(x, y, color, false);	        // not filled
    }

  Line(Line line)                       // copy constructor
    {
    super((Shape)line);
    }

  Shape copy()
    {
    return new Line(this);
    }

  void draw(int x, int y, Graphics g)               // draw a Line
    {
    g.setColor(color);
    g.drawLine(this.x + x, this.y + y, this.x+x+width, this.y+y+height);    
    }


  // inside is determined specially for a line, since negative width and
  // height are permitted.

  public boolean inside(int x, int y)
    {
    int x0 = this.x;
    int y0 = this.y;
    int width = this.width;
    int height = this.height;
    if( width < 0 )
      {
      width = -width;
      x0 -= width;
      }
    if( height < 0 )
      {
      height = -height;
      y0 -= height;
      }
    return  x >= x0
         && x <  x0 + width 
         && y >= y0 
         && y <  y0 + height;
    }
   

  void describe(int indentation, StringBuffer buff, ObjectDraw app)
    {
    indent(indentation, buff);
    buff.append("(line " + app.colorToString(color) + " "
                         + x + " " + y + " " 
                         + width + " " + height + ")\n"); 
    }
  }  // Line


//
// A Group is a set of shapes treated as a single Shape.
// Groups may have Groups as elements, etc.
// Groups ignore the color attribute of Shape
//

class Group extends Shape
  {
  Vector members;                       // members of the group

  boolean shared = false;               // indicates whether members shared

  Group(int x, int y, ObjectDraw app, Vector shapes)
    {
    super(x, y, null, false);	       // no color, not filled
    members = initMembers(shapes, app.currentShape);
    }

  Group(Group group)                    // copy constructor
    {                                   // note: does not run initMembers
    super((Shape)group);
    members = group.members;
    shared = group.shared = true;       // indicate sharing
    }

  Shape copy()
    {
    return new Group(this);
    }


  Group deepCopy()
    {
    Group result = (Group)copy();
    Vector old = result.members;
    result.members = new Vector();
    for( Enumeration e = old.elements(); e.hasMoreElements(); )
      {
      result.members.addElement(((Shape)e.nextElement()).copy());
      }
    return result;
    }


  // initialize the members of a group by setting their coordinates to be
  // relative.

  Vector initMembers(Vector members, Shape wrapper)
    {
    Vector v = new Vector();
    
    // find maxes and mins
    int xmin = x+wrapper.width, ymin = y+wrapper.height, xmax = -1, ymax = -1;
    for( Enumeration e = members.elements(); e.hasMoreElements(); )
      {
      Shape s = (Shape)e.nextElement();

      if( s.width >= 0 )
        {
        xmin = Math.min(xmin, s.x);
        xmax = Math.max(xmax, s.x+s.width-1);
        }
      else
        {
        xmin = Math.min(xmin, s.x+s.width+1);
        xmax = Math.max(xmax, s.x);
        }
                
      if( s.height >= 0 )
        {
        ymin = Math.min(ymin, s.y);
        ymax = Math.max(ymax, s.y+s.height-1);
        }
      else
        {
        ymin = Math.min(ymin, s.y+s.height+1);
        ymax = Math.max(ymax, s.y);
        }
      }

    x = xmin; 
    y = ymin;
    width  = xmax - xmin + 1; 
    height = ymax - ymin + 1;

    // adjust origins
    for( Enumeration e = members.elements(); e.hasMoreElements(); )
      {
      Shape s = (Shape)e.nextElement();
      s.x -= xmin;
      s.y -= ymin;
      v.addElement(s);
      }
    return v;
    }


  // draw a Group by drawing its members, with offset

  void draw(int x, int y, Graphics g)
    {
    for( Enumeration e = members.elements(); e.hasMoreElements(); )
      {
      ((Shape)e.nextElement()).draw(this.x + x, this.y + y, g);
      }
    }


  void describe(int indentation, StringBuffer buff, ObjectDraw app)
    {
    indent(indentation, buff);
    buff.append("(group " + x + " " + y);
    if( shared )
      {
      int i = app.descriptions.getRef(members);
      buff.append(" (ref " + i +"))\n");
      }
    else
      {
      buff.append("\n");
      for( Enumeration e = members.elements(); e.hasMoreElements(); )
        {
        ((Shape)(e.nextElement())).describe(indentation+2, buff, app);
        }
      indent(indentation, buff);
      buff.append(")\n");
      }
    }
  }  // Group


// Pair is just a pair of two objects

class Pair
  {
  Object first;
  Object second;

  Pair(Object first, Object second)
    {
    this.first = first;
    this.second = second; 
    }
  }  // Pair


/**
  *  A DefTable is a table of object definitions
 **/

class DefTable extends java.util.Vector
  {
  // getRef gets a table index for a Vector of shapes

  int getRef(Vector members)
    {
    int i = indexOf(members);
    if( i < 0 )
      {
      i = size();
      // definition not already in table, put there now
      addElement(members);
      }
    return i;
    }


  // show all definitions in table

  void describe(StringBuffer buff, ObjectDraw app)
    {
    int i = 0;
    for( Enumeration e = elements(); e.hasMoreElements(); )
      {
      buff.append("(def " + i +"\n");
      describe1(i++, (Vector)e.nextElement(), buff, app);
      buff.append(")\n");
      }      
    }


  // show one definition in table

  void describe1(int i, Vector V, StringBuffer buff, ObjectDraw app)
    {
    for( Enumeration e = V.elements(); e.hasMoreElements(); )
      {
      ((Shape)e.nextElement()).describe(2, buff, app);
      }      
    }
  }  // DefTable


/**
  *  A DescriptionFrame is a frame containing text descriptions
 **/

class DescriptionFrame extends Frame
  {
  StringBuffer shape;                           // description of a shape
  StringBuffer defs;                            // auxiliary definitions

  TextArea textArea = new TextArea();           // text area within frame

  Button clearButton = new Button("Clear");     // button to clear description

  Choice fontChoice = new Choice();

  static int fontSize[] = {8, 10, 12, 14, 18, 24}; // font sizes available

  DescriptionFrame(ObjectDraw app)              // constructor
    {
    super("Description");
    setLayout(new FlowLayout(FlowLayout.LEFT));

    add(clearButton);

    // establish fontChoice menu
    for( int i = 0; i < fontSize.length; i++ )
      fontChoice.addItem(new Integer(fontSize[i]).toString());

    fontChoice.select("18");
    textArea.setFont(new Font("Helvetica", Font.BOLD, 18));
    fontChoice.setFont(app.MainFont);   // Font of the menu, not selected font
    add(fontChoice);

    // establish textArea
    setBackground(app.backgroundColor);
    add(textArea);

    resize(app.size().width, 2*app.size().height/3);
    clearButton.setFont(app.MainFont);  // Font of the button
    }

  void clearText()
    {
    shape = new StringBuffer();
    defs = new StringBuffer();
    }

  void update()
    {
    textArea.appendText(defs.toString());
    textArea.appendText(shape.toString());
    show();
    }

  public boolean action(Event event, Object arg)
    {
    if( event.target == clearButton )
      {
      textArea.setText("");             // clear description text
      }

    else if( event.target == fontChoice )
      {
      textArea.setFont(new Font("Helvetica", 
                                Font.BOLD, 
                                new Integer((String)arg).intValue()));
      }

    return super.action(event, arg);    // Delegate all other actions to super.
    }
  }  // DescriptionFrame


/**
  *  A Slider is a combination of a Label, a Scrollbar, and a TextField.
 **/

class Slider
  {
  double Value;                 // Value maintained by the slider

  Label label;                  // label for the slider
  Scrollbar scroll;             // the slider itself
  TextField field;              // field showing value of slider


  /**
    *  Create a slider.
   **/

  Slider(Applet app,            // Applet in which to place slider
         String lab,            // Label for the slider
         Font font,             // Font for Label and TextField
         double initial,        // Initial value
         int min,               // minimum value of the slider
         int max,               // maximum
         int fieldSize)         // number of characters in TextField
    {
    label = new Label(lab);
    label.setFont(font);
    app.add(label);             // Add the label.

    scroll = new Scrollbar(Scrollbar.VERTICAL, (int)initial, 100, min, max);
    scroll.setFont(font);       
    app.add(scroll);            // Add the scrollbar.

    this.Value = initial;       // Initialize the value.

    field = new TextField(fieldSize);
    field.setText(new Double(initial).toString());
    field.setFont(font);
    field.setEditable(false);
    app.add(field);             // Add the TextField.
    }


  /**
    *  Set the value of the slider programmatically.
   **/

  void setValue(double Value)
    {
    this.Value = Value;
    field.setText(new Double(Value).toString());
    }


  /**
    *  Update the value of the slider when the scrollbar is adjusted
    *  and return the value (must be called by handleEvent).
   **/

  double getValue()
    {
    setValue(scroll.getValue());
    return Value;
    }

  }  // class Slider
