import      java.lang.Math;
import      java.util.*;

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

/** StarField routes the various Events to StarMarkers.

 * The 1.0.2 version extends Canvas,
 * need not (can not) implement event listeners,
 * propagates resulting new events only to parent.

 * MOMENTARY, ACTION, SINGLE, MULTIPLE select modes.
 * Also ENTER, EXIT of individual StarMarkers.

 * ActionEvent for all of these,
 * reported by the StarField to parent,
 * with the target being the individual StarMarker,
 * command field shows out what changed.

 * IPD Inc Newport RI
 * Jan 27 1999 */

public class StarField extends Canvas
implements Runnable
{

    private float minX, maxX, minY, maxY;

    private float minZoomX, maxZoomX, minZoomY, maxZoomY;

    private Vector MV;
    private StarMarker[] markers = null;

    private int zMode = 0;

    private int   mode;

    public static final int
            MOMENTARY = 0,
	    ACTION = 1,
	    SINGLE = 2,
	    MULTIPLE = 3;

    protected RadioGroup RG = null;

    protected Font
	normalFont = null,
	boldFont = null;

    protected boolean
	paintEnabled = true,
	enterExpand = false,
	enterHighlight = false;

/** Show when we are visited */

    protected boolean active = false;

/** Constructors */

    StarField () {
        this (0, 1, 0, 1);
    }

    StarField (int mode) {
        this (mode, 0, 1, 0, 1);
    }

    StarField (float minX, float maxX, float minY, float maxY) {
        this (SINGLE, minX, maxX, minY, maxY);
    }

    StarField (int mode, float minX, float maxX, float minY, float maxY) {

        if ((MOMENTARY != mode)
          && (ACTION != mode)
          && (SINGLE != mode)
          && (MULTIPLE != mode))
            throw new IllegalArgumentException ("Invalid select mode");

        this.mode = mode;

        if (SINGLE == mode)
            RG = new RadioGroup ();

/* It will grow if needed -- this seems a good start */

        MV = new Vector (100);

        setXAxis (minX, maxX);
        setYAxis (minY, maxY);
    }

/** Remove all Markers from this StarField */

    public void clear () {
        MV.removeAllElements ();
	markers = null;
	repaint (100);
    }

    protected int [] rankX = null, rankY = null;

  // protected float compressX = (float)0.5, compressY = (float)0.5;
    protected float compressX = 0, compressY = 0;

/** Set Compression of Marker distribution on X and Y axes.
 * Zero Compression positions linearly according to X and Y values,
 * which accurately displays the relative values,
 * but may crowd some Markers and leave other space empty.
 * One Compression positions in equal steps by ranking not values,
 * making best use of all the space,
 * but showing only ranks and not the relative values.
 * Intermediate Compression combines the two. */

    public void setCompression (float compressX, float compressY) {
	this.compressX = compressX;
	this.compressY = compressY;
    }

/** Set both Axes to include all current Markers.
 * In case of only one Marker put it in the middle not corner. */

    public void scaleAxes () {
        if ((MV.isEmpty()))
            return;

        StarMarker[] markers = getStarMarkers ();

        float
	  minX = markers[0].getX(),
	  maxX = markers[0].getX(),
	  minY = markers[0].getY(),
	  maxY = markers[0].getY();

        for (int ii = 1;
             ii < markers.length;
             ii++) {

             if (minX > markers[ii].getX())
		 minX = markers[ii].getX();

             if (maxX < markers[ii].getX())
		 maxX = markers[ii].getX();

             if (minY > markers[ii].getY())
		 minY = markers[ii].getY();

             if (maxY < markers[ii].getY())
		 maxY = markers[ii].getY();
        }

/* create rankings in X and Y -- we may use these to unclutter. */

 // System.out.println (" create rankings");

        rankX = new int [markers.length];
        rankY = new int [markers.length];

        for (int ii = 0;
             ii < markers.length;
             ii++) {
	    int eqsX = 0, ltsX = 0, eqsY = 0, ltsY = 0;

            for (int jj = 0;
                 jj < markers.length;
                 jj++) {
                 if (markers[jj].getX() < markers[ii].getX()) ltsX++;
                 if (markers[jj].getY() < markers[ii].getY()) ltsY++;
	     }

            for (int jj = ii+1;
                 jj < markers.length;
                 jj++) {
                 if (markers[jj].getX() == markers[ii].getX()) eqsX++;
                 if (markers[jj].getY() == markers[ii].getY()) eqsY++;
	     }

	     if (eqsX > ltsX)
		 rankX[ii] = eqsX+1;
	     else
		 rankX[ii] = ltsX;
	     if (eqsY > ltsY)
		 rankY[ii] = eqsY+1;
	     else
		 rankY[ii] = ltsY;
	 }

 // System.out.println ("done create rankings");

        setXAxis (minX, maxX);
        setYAxis (minY, maxY);
    }

/** Set X Axis,
 * and the limits of Zoom,
 * and rescale any current contents */

    public void setXAxis (float minX, float maxX) {
        this.minX = minX;
        this.minZoomX = minX;
        this.maxX = maxX;
        this.maxZoomX = maxX;

// System.out.println ("setXAxis"+" "+minX+" "+maxX);
        if (!(MV.isEmpty()))
            scaleCoords ();
    }

/** Set Y Axis,
 * and the limits of Zoom,
 * and rescale any current contents */

    public void setYAxis (float minY, float maxY) {
        this.minY = minY;
        this.minZoomY = minY;
        this.maxY = maxY;
        this.maxZoomY = maxY;

// System.out.println ("setYAxis"+" "+minY+" "+maxY);
        if (!(MV.isEmpty()))
            scaleCoords ();
    }

/** Set zoom as bounding percentages,
 * measured from Left and from Top.
 * @param L -- percentage of X span for Left edge inset
 * @param W -- percentage of X span for Width
 * @param T -- percentage of Y span for Top edge inset
 * @param H -- percentage of Y span for Height
 */

    public void setZoom (int L, int W, int T, int H) {
	if ((L < 0) || (L > 100)
	 || (W < 0) || (W > 100)
	 || (T < 0) || (T > 100)
	 || (H < 0) || (H > 100))
	     return;

// System.out.println ("setZoom"+" "+L+" "+W+" "+T+" "+H);

        minZoomX = minX + (L * (maxX-minX) / 100);
        maxZoomX = minZoomX + (W * (maxX-minX) / 100);

        maxZoomY = maxY - (T * (maxY-minY) / 100);
	minZoomY = maxZoomY - (H * (maxY-minY) / 100);

// System.out.println ("Zoom"+" "+minZoomX+" "+maxZoomX+" "+minZoomY+" "+maxZoomY);
        if (!(MV.isEmpty()))
            scaleCoords ();
    }

/** Zoom In,
 * to show two-thirds current span,
 * centered at given position,
 * in axis units. */

    public void zoomIn (float zoomX, float zoomY) {

        float Xhalf = (maxZoomX - minZoomX) / 3;
	minZoomX = zoomX - Xhalf;
	maxZoomX = zoomX + Xhalf;

        float Yhalf = (maxZoomY - minZoomY) / 3;
	minZoomY = zoomY - Yhalf;
	maxZoomY = zoomY + Yhalf;

        if (!(MV.isEmpty()))
            scaleCoords ();
    }

/** We could (but have not) make these Colors settable */

    protected Color [] barColors = { Color.gray, Color.white, Color.gray };

    protected float XBarStarts [] = new float [4];
    protected float YBarStarts [] = new float [4];

/** Reset Zoom to full given axes */

    public void zoomHome () {
        minZoomX = minX;
        minZoomY = minY;
        maxZoomX = maxX;
        maxZoomY = maxY;

       zoom_left = 0;
       zoom_visible = 100;
       zoom_top = 0;

       if (null != doZoomIn) doZoomIn.enable ();
       if (null != doZoomOut) doZoomOut.disable ();

/* Set fixed ends and adjustable values between them.
 * starts [0] minX and starts [1] minZoomX etc
 * User changes directly update the starts [] array,
 * and produce an event that comes back to us.
 * We must then make use of them. */

       if (null != BarControlX) {
	   float margin = (maxX - minX) / 30;
	   XBarStarts [0] = minX - margin;
	   XBarStarts [1] = minZoomX;
	   XBarStarts [2] = maxZoomX;
	   XBarStarts [3] = maxX + margin;
	   BarControlX.setBar (barColors, XBarStarts);
       }

/* Remember Y runs top-to-bottom */

       if (null != BarControlY) {
	   float margin = (maxY - minY) / 30;
	   YBarStarts [3] = minY - margin;
	   YBarStarts [2] = minZoomY;
	   YBarStarts [1] = maxZoomY;
	   YBarStarts [0] = maxY + margin;
	   BarControlY.setBar (barColors, YBarStarts);
       }

        if (!(MV.isEmpty()))
            scaleCoords ();
    }

    public void doHome () {  zoomHome (); }

/* Scrollbars are integer,
 * we always use them to represent percentage,
 * so min and max are 0 to 100,
 * regardless of the scale values or widget size.
 * value and visible width or height are percentages,
 * that must add up to 100 or less.
 * Zoom In / Out modify the visible width or height,
 * and the value by half as much,
 * to keep current center. */

   private Scrollbar
       ScrollX, ScrollY;

   private int
       zoom_left, zoom_visible, zoom_top;

   private Component
      doZoomOut, doZoomIn;

/** Note the controls created by and belonging to the parent,
 * for which Events will be handled here.
 * This form uses two Scrollbars to pan at existing scale,
 * and a pair of Buttons for scaling In and Out. */

    public void setZoomControls (Scrollbar ScrollX, Scrollbar ScrollY, Component doZoomOut, Component doZoomIn) {
	this.ScrollX = ScrollX;
	this.ScrollY = ScrollY;
	this.doZoomOut = doZoomOut;
	this.doZoomIn = doZoomIn;
    }

   private BarControl
       BarControlX, BarControlY;

/** Note the controls created by and belonging to the parent,
 * for which Events will be handled here.
 * This form uses two BarControls,
 * each controlling both limits of an axis,
 * so that further In and Out controls are not needed. */

    public void setZoomControls (BarControl BarControlX, BarControl BarControlY) {
	this.BarControlX = BarControlX;
	this.BarControlY = BarControlY;
	this.doZoomOut = null;
	this.doZoomIn = null;
    }

/** Chance for the StarField to handle any Zoom Control Events,
 * originating in controls it has been told about,
 * let others go by for normal handling.
 * Call this early within parent.handleEvent.
 * @return true if the event is handled here,
 * false if event remains to be handled by others. */

    public boolean handleZoomEvents (Event event) {

       if (event.target == BarControlX) {
	   minZoomX = XBarStarts [1];
	   maxZoomX = XBarStarts [2];

        if (!(MV.isEmpty()))
            scaleCoords ();
           return true;
       }

       if (event.target == BarControlY) {
	   minZoomY = YBarStarts [2];
	   maxZoomY = YBarStarts [1];

        if (!(MV.isEmpty()))
            scaleCoords ();
           return true;
       }

       if ((event.target == doZoomOut)
	&& (event.id == Event.ACTION_EVENT)) {
	   doZoomIn.enable ();
	   if (zoom_visible > 80) return true;

	   zoom_visible += 20;
	   zoom_left -= 10;
	   zoom_top -= 10;
	   ScrollX.setValues (zoom_left, zoom_visible, 0, 100);
	   ScrollY.setValues (zoom_top, zoom_visible, 0, 100);
	   doZoom ();
           return true;
       }

       if ((event.target == doZoomIn)
	&& (event.id == Event.ACTION_EVENT)) {
	   doZoomOut.enable ();
	   if (zoom_visible < 25) return true;

	   zoom_visible -= 20;
	   zoom_left += 10;
	   zoom_top += 10;
	   ScrollX.setValues (zoom_left, zoom_visible, 0, 100);
	   ScrollY.setValues (zoom_top, zoom_visible, 0, 100);
	   doZoom ();
           return true;
       }

       if (event.target == ScrollX) {
	   zoom_left = ScrollX.getValue ();
	   zoom_visible = ScrollX.getVisible ();
	   doZoom ();
           return true;
       }

       if (event.target == ScrollY) {
	   zoom_top = ScrollY.getValue ();
	   zoom_visible = ScrollY.getVisible ();
	   doZoom ();
           return true;
       }

       return false;
    }  

/* Some platforms always show a minimal size thumb,
 * which can be moved further than should be allowed.
 * We try to protect ourselves against that here. */

    protected void doZoom () {
       if (zoom_visible < 10)
	   zoom_visible = 10;
       if (zoom_visible > 100)
	   zoom_visible = 100;

       if (zoom_left < 0)
	   zoom_left = 0;
       if (zoom_left > 90)
	   zoom_left = 90;

       if (zoom_top < 0)
	   zoom_top = 0;
       if (zoom_top > 90)
	   zoom_top = 90;

       setZoom (zoom_left, zoom_visible, zoom_top, zoom_visible);
    }

 /** Method to completely manage zoom and pan w/out other controls,
 * except some Button or Menu choice that comes here,
 * first changes cursor to cross-hairs,
 * next Click changes the view and restores the default cursor. */

    public void activateZoomIn () {
        zMode = 1;
	setCursor (Cursor.getPredefinedCursor (Cursor.CROSSHAIR_CURSOR));
	repaint ();
    }

    protected Color colorPoly = null;
    protected float xPoly [] = null, yPoly [] = null;

/** Set of points defining a Polygon (last equals first)
 * to be drawn as user assist backdrop to the data.
 * Same units as data axes.
 * Sorry about separate x and y arrays,
 * we need a RealPoint class. */

    public void setPoly (Color color, float xPoly [], float yPoly []) {
     ////////// nothing done yet
    }

/** Set false before a batch of changes and true when done.
 * This avoids flicker and also gives a faster finished result. */

    public void setPaintEnabled (boolean bv) {
	paintEnabled = bv;
	if (bv) repaint (100);
    }

    protected boolean dragEnabled = false;

/** Set policy to allow Markers to be dragged by user,
 * if the individual Marker also allows it. */

    public void setDragEnabled (boolean bv) {
	dragEnabled = bv;
    }

    public boolean getDragEnabled () { return dragEnabled; }

/** Set policy to Expand a Marker on Enter */

    public void setEnterExpand (boolean bv) { enterExpand = bv; }

/** Set policy to Highlight a Marker on Enter */

    public void setEnterHighlight (boolean bv) { enterHighlight = bv; }

/** Add a StarMarker to this StarField */

    public StarMarker add (StarMarker marker) {

        MV.addElement (marker);
	markers = null;

        marker.setParent (this);

/* assign it a Listener according to the StarField mode,
 * but we will have to route the events to them,
 * because this is not a Container,
 * and markers are not Children. */

        if (MOMENTARY == mode)
            marker.setListener (new MomentaryButtonLikeListener(marker));

        if (ACTION == mode)
            marker.setListener (new SpringyButtonLikeListener(marker));

        if (SINGLE == mode) {
            marker.setListener (new StickyButtonLikeListener(marker));
	    marker.setRadioGroup (RG);
        }

        if (MULTIPLE == mode)
            marker.setListener (new StickyButtonLikeListener(marker));

        return marker;
    }

/** Get array of current StarMarkers or null if not any */

    public StarMarker[] getStarMarkers () {
        if (MV.isEmpty())
            return null;

       if (null == markers) {
            markers = new StarMarker[MV.size ()];
	    MV.copyInto (markers);
       }

        return markers;
    }

/* Works but not terribly handy */

    protected boolean mouseZoom (int x, int y) {
	if (0 == zMode)
	   return false;

        Dimension d = getSize ();

        float Xspan = maxZoomX - minZoomX;
        float Yspan = maxZoomY - minZoomY;

/* Remember that x pixels left to right
 * and y top to bottom */

        float
	    fx = minZoomX + ((Xspan * x) / d.width),
	    fy = maxZoomY - ((Yspan * y) / d.height);

        if (1 == zMode)
	    zoomIn (fx, fy);

        zMode = 0;
       setCursor (Cursor.getPredefinedCursor (Cursor.DEFAULT_CURSOR));
       repaint ();
        return true;
    }

/** Pass mousePressed to containing StarMarker if any */

    public synchronized boolean mouseDown (Event me, int x, int y) {

	if (0 != zMode)
	    return true;

        if (null != (has_mouse = handleMouse (me)))
            has_mouse.getListener ().mousePressed (me);

        old_mouse = has_mouse;

	return true;
    }

 /** None of Listeners really needed Drag anyway,
  * they just disarm / arm as pressed mouse exits and enters,
  * and we can (optionally) let user Drag the Marker.
  *
  * Drag starts with Down and ends with Up,
  * so it also has the effect of a Click.
  * Maybe we should prevent that but we do not. */

    public synchronized boolean mouseDrag (Event me, int mx, int my) {

	if (0 != zMode)
	    return true;

        if (!getDragEnabled ())
	    return true;

        handleMouse  (me);

   //// if (null != has_mouse) has_mouse.getListener ().mouseDragged (me);

/* OH OH compression and ranking???
 * Dragging a Marker past others changes the rankings,
 * which contribute to their pixel positions,
 * if compression is greater than zero,
 * which means other Markers all must move.
 * So just consider these features mutually exclusive. */

        if ((null != has_mouse)
         && (compressX == 0) && (compressY == 0)
	 && (has_mouse.getDragEnabled ())) {

            Dimension d = getSize ();

            float Xspan = maxZoomX - minZoomX;
            float Yspan = maxZoomY - minZoomY;

/* Remember that x pixels left to right
 * and y top to bottom */

            float
	        fx = minZoomX + ((Xspan * mx) / d.width),
	        fy = maxZoomY - ((Yspan * my) / d.height);

/* Hack instead of proper width and height... old position */

	    repaint (50, has_mouse.x-40, has_mouse.y-20, 80, 40);

/* Don't bother scaling these back to pixels... */

	    has_mouse.changeXY (fx, fy);

/* These are of course exactly the right pixel values */

            has_mouse.move (mx, my);

/* Hack instead of proper width and height... new position */

	    repaint (50, mx-40, my-20, 80, 40);

            processDrag (has_mouse);
        }

	return true;
    }

/** Pass mouseClicked to containing StarMarker if any */

    public synchronized boolean mouseClick (Event me, int x, int y) {

	if (mouseZoom (x, y))
	    return true;

        if (null != (has_mouse = handleMouse (me)))
            has_mouse.getListener ().mouseClicked (me);

        old_mouse = has_mouse;

	return true;
    }

/** Pass mouseReleased to StarMarker of record */

    public synchronized boolean mouseUp (Event me, int x, int y) {

	if (mouseZoom (x, y))
	    return true;

        handleMouse  (me);
        if (null != has_mouse)
            has_mouse.getListener ().mouseReleased (me);

        old_mouse = has_mouse;

	return true;
    }

/** Possibly change mouseMove to mouseEnter / mouseExit */

    public synchronized boolean mouseMove (Event me, int x, int y) {
        if (null != (has_mouse = handleMouse (me))) {
            if (old_mouse == has_mouse)
                has_mouse.getListener ().mouseMoved (me);

            else {
                if (null != old_mouse) {
                    old_mouse.getListener ().mouseExited (me);
		    processExit (old_mouse);
		}
                has_mouse.getListener ().mouseEntered (me);
		processEnter (has_mouse);
            }
        }

	else if (null != old_mouse) {
            old_mouse.getListener ().mouseExited (me);
	    processExit (old_mouse);
        }

        old_mouse = has_mouse;

	return true;
    }

    public synchronized boolean mouseEnter (Event me, int x, int y) {
        setActive (true);

	requestFocus ();

	return true;
    }

    public synchronized boolean mouseExit (Event me, int x, int y) {
        if (null != old_mouse) {
            old_mouse.getListener ().mouseExited (me);
	    processExit (old_mouse);
	}
	old_mouse = null;
        setActive (false);

	return true;
    }

/* There is no keyClick just Down and Up.
 * Report Up after Down on same StarMarker. */

    public boolean keyDown (Event evt, int key) {
	key_mouse = has_mouse;
	return true; /// return super.handleEvent (evt);
    }

    public boolean keyUp (Event evt, int key) {
	if ((null != has_mouse)
	 && (key_mouse == has_mouse))
            processKeypress (key_mouse, key);

	return true; /// return super.handleEvent (evt);
    }

    private StarMarker
	key_mouse = null,
	has_mouse = null,
	old_mouse = null;

/** Find containing Marker if any for routing the Event.
 * Make that containing Marker the event.target,
 * but do --not-- translate coordinates,
 * because Markers use our coordinates not their own. */

    protected StarMarker handleMouse (Event me) {
        Point mep = new Point (me.x, me.y);

        StarMarker[] markers = getStarMarkers ();

        if (null == markers)
            return null;

        for (int ii = 0; ii < markers.length; ii++) {
            if (markers[ii].isVisible ()
             && markers[ii].isEnabled ()
             && markers[ii].contains(mep)) {
		me.target = markers[ii];
                return markers[ii];
            }
        }
        return null;
    }

    private StarMarker actionMarker = null;
    private String actionString = null;

/** Report "Keypress:C:NN" on a StarMarker to parent.
 * C is String [1] for the key character if defined.
 * NN is String [2 or more] for the numeric key code. */

    public void processKeypress (StarMarker sm, int key) {
	actionMarker = sm;
	actionString = "Keypress:"+String.valueOf ((char)key)+":"+String.valueOf (key);
        run (); //// new Thread (this).start();
    }

/** Report "Action" on a StarMarker to parent */

    public void processAction (StarMarker sm) {
	actionMarker = sm;
	actionString = "Action";
        run (); //// new Thread (this).start();
    }

/** Report "Drag" on a StarMarker to parent */

    public void processDrag (StarMarker sm) {
	actionMarker = sm;
	actionString = "Drag";
        run (); //// new Thread (this).start();
    }

/** Report "Enter" on a StarMarker to parent */

    public void processEnter (StarMarker sm) {

	if (enterExpand)
	    sm.setExpanded (true);

	if (enterHighlight)
	    sm.setHighlighted (true);

	actionMarker = sm;
	actionString = "Enter";
        run (); //// new Thread (this).start();
    }

/** Report "Exit" on a StarMarker to parent */

    public void processExit (StarMarker sm) {

	if (enterExpand)
	    sm.setExpanded (false);

	if (enterHighlight)
	    sm.setHighlighted (false);

	actionMarker = sm;
	actionString = "Exit";
        run (); //// new Thread (this).start();
    }

/** Report "Select" or "Deselect" on a StarMarker to parent */

    public void processStateChange (StarMarker sm, boolean state) {
        if (null != RG) {
            if (state)
                RG.setSelected (sm);
            else if (RG.getSelected() == sm) {
                sm.setState (true);
                return;
            }
        }
        actionMarker = sm;
	if (state)
	    actionString = "Select";
	else
	    actionString = "Deselect";
        run (); //// new Thread (this).start();
    }

/** In 1.1 version a separate thread delivers the Action to parent,
 * to avoid a problem when events cause lengthy processing.
 * In 1.0.2 the problem is not present,
 * and works better w/out a separate thread. */

    public void run () {

        try
	{ getParent ().postEvent
           (new Event
	       (actionMarker, Event.ACTION_EVENT, actionString));

        } catch (Exception exc) {
            System.out.println ("failed getParent.postEvent:" + exc);
            exc.printStackTrace ();
        }
    }

    protected int
	nRows = 30,
	nCols = 30;

/** Used for establish min and pref sizes based on font metrics. */

    public void setRowsCols (int nRows, int nCols) {
	this.nRows = nRows;
	this.nCols = nCols;
    }

/** MinimumSize based on current Font point size.
 * Still may have to make do with less of course... */

    public Dimension getMinimumSize () {

        int points = 12;
        Font oFont = getFont ();

        if (null != oFont)
            points = oFont.getSize ();

        return new Dimension (nCols * points, nRows * points);
    }

    public Dimension minimumSize () { return getMinimumSize (); }

/** PreferredSize based on current Font point size. */

    public Dimension getPreferredSize () {

        int points = 12;
        Font oFont = getFont ();

        if (null != oFont)
            points = oFont.getSize ();

        return new Dimension (nCols * points, nRows * points);
    }

    public Dimension preferredSize () { return getPreferredSize (); }

    public void setBounds (int x, int y, int w, int h) {
        super.setBounds (x, y, w, h);
        scaleCoords ();
    }

    public void setBounds (Rectangle r) {
        super.setBounds (r);
        scaleCoords ();
    }

    public void setSize (int w, int h) {
        super.setSize (w, h);
        scaleCoords ();
    }

    public void setSize (Dimension d) {
        super.setSize (d);
        scaleCoords ();
    }

    public void reshape (int x, int y, int w, int h) {
	super.reshape (x, y, w, h);
        scaleCoords ();
    }

    public void reshape (Rectangle r) {
	super.reshape (r.x, r.y, r.width, r.height);
	scaleCoords ();
    }

    public void resize (int w, int h) {
	super.resize (w, h);
	scaleCoords ();
    }

    public void resize (Dimension d) {
	super.resize (d);
	scaleCoords ();
    }

    public Dimension getSize () { return size (); }

/** Map all StarMarkers onto current size of StarField */

    protected void scaleCoords () {
        if ((MV.isEmpty()))
            return;

        Dimension d = getSize ();

        if ((null == d)
         || (0 == d.width)
         || (0 == d.height))
             return;

        StarMarker[] markers = getStarMarkers ();
        if ((null == markers) || (0 == markers.length))
	     return;

        float Xspan = maxZoomX - minZoomX;
        float Yspan = maxZoomY - minZoomY;

        float
	   xdr = (maxX - minX) / markers.length,
	   ydr = (maxY - minY) / markers.length;

        for (int ii = 0;
             ii < markers.length;
             ii++) {

           float xx, yy;

      xx = markers[ii].getX ();
      yy = markers[ii].getY ();

 if ((null != rankX) && (null != rankY)) {
 xx = (compressX * (minX + (rankX[ii] * xdr)))
  + ((1-compressX) * markers[ii].getX ());

 yy = (compressY * (minY + (rankY[ii] * ydr)))
  + ((1-compressY) * markers[ii].getY ());
 }

/* Remember that x pixels left to right
 * and y top to bottom */

            markers[ii].move
            (10+(int) ((d.width-20) * (xx - minZoomX) / Xspan),
             10+(int) ((d.height-20) * (maxZoomY - yy) / Yspan));
        }

        repaint (100);   
    }

/** Scale the float X Y of given StarMarker to pixel coords.
 * ONLY VALID for zero compression on both axes.
 * Otherwise would either need rankings on both axes,
 * or would have to change them along with pixel coords. */

    public void reScale (StarMarker sm) {

        Dimension d = getSize ();

        if ((null == d)
         || (0 == d.width)
         || (0 == d.height))
             return;

        float Xspan = maxZoomX - minZoomX;
        float Yspan = maxZoomY - minZoomY;

        float
	   xdr = (maxX - minX) / markers.length,
	   ydr = (maxY - minY) / markers.length;

        float xx, yy;

        xx = sm.getX ();
        yy = sm.getY ();

/* Remember that x pixels left to right
 * and y top to bottom */

        sm.move
           (10+(int) ((d.width-20) * (xx - minZoomX) / Xspan),
            10+(int) ((d.height-20) * (maxZoomY - yy) / Yspan));
    }

    public void setActive (boolean bv) {
        active = bv;
        repaint (100);
    }

    public void setFont (Font f) {
       super.setFont (f);
       normalFont = f;
       boldFont = new Font
	(f.getName (),
	 Font.BOLD,
	 f.getSize ());
    }

    public void paint (Graphics g) {

        Dimension d = getSize ();

	if ((null == boldFont)
	 || (null == normalFont)) {
	    normalFont  = getFont ();
	    boldFont = new Font
	       (normalFont.getName (),
	       Font.BOLD,
	       normalFont.getSize ());
        }

/* Paint all visible markers.
 * Skip any that are Highlighted,
 * and get them in a second loop,
 * to help them show up in clutter. */

	if (paintEnabled) {
            StarMarker[] markers = getStarMarkers ();

        if (null != markers) {

            g.setFont (normalFont);
            for (int ii = 0;
                 ii < markers.length;
                 ii++)
		 if (!markers[ii].isHighlighted ())
                markers[ii].paint (g);

            g.setFont (boldFont);
            for (int ii = 0;
                 ii < markers.length;
                 ii++)
		 if (markers[ii].isHighlighted ())
                markers[ii].paint (g);
            }
        }

/* Show when we are visited */

        if (active) {
            g.setColor (Color.black);
            g.drawRect (0, 0, d.width - 1, d.height - 1);
        }
    }

   public void waitForImage (Image image) 
   {
      MediaTracker tracker = new MediaTracker (this);
      try {
         tracker.addImage (image, 0);
         tracker.waitForID (0);
      } catch (InterruptedException e) { e.printStackTrace (); }
   }

    protected Color fgHLColor = Color.black, bgHLColor = Color.white;

    protected int borderThickness = 2;

    protected int borderHLThickness = 4;

    public void setBorderHLThickness (int borderHLThickness) {
        if (this.borderHLThickness == borderHLThickness) return;

        this.borderHLThickness =  borderHLThickness;
    }

    public int getBorderHLThickness () { return borderHLThickness; }

    public void setBorderThickness (int borderThickness) {
        if (this.borderThickness == borderThickness) return;

        this.borderThickness =  borderThickness;
    }

    public int getBorderThickness () { return borderThickness; }

    public void setHLForeground (Color set_fgHLColor) {
        if (fgHLColor == set_fgHLColor) return;

        fgHLColor = set_fgHLColor;
    }

    public Color getHLForeground () { return fgHLColor; }

    public void setHLBackground (Color set_bgHLColor) {
        if (bgHLColor == set_bgHLColor) return;

        bgHLColor = set_bgHLColor;
    }

    public Color getHLBackground () { return bgHLColor; }

}
