/* ===================================================================== */
/** A plotting module. Draws a simple graphic in a canvas. Based on the
    earlier implementation in Tcl/Tk 8.0. Written for JDK 1.0. 
    
    Order of procedures: PlotBorder has to be called before any of the
    data plotting procedures. All settings have default values,
    initialized in the constructor by calling Reset().

    Viewport coordinates run from 0.0 to 1.0, and are converted into
    integers based on the physical size of the canvas. The origin is
    in the lower left corner.

    World coordinates are required to have their origin in the lower
    left corner. This is done because the procedures for plotting
    ticks can't cope with inverted axes. The change would be trivial,
    but time-consuming. 

    Zooming is supported with mouse Button 3, or the left
    mouse button and the META key (CTRL on Windows systems). The
    application generates an ACTION_EVENT with a "ZOOM" argument.
    Users should repaint, without calling SetWindow(), or they
    should call SetWindow with the settings currently stored in
    the PlotCanvas object in members wx0, wx1, wy0, wy1. Unzooming
    is supported by generating an ACTION_EVENT with an "UNZOOM"
    argument, when Button 2 is pressed, or Button 1 and the ALT key.

    @author Emmanuel Gustin
    @version 1.0
*/    
/* ===================================================================== */

import java.lang.*;
import java.awt.*;
import AuxMath;

public class PlotCanvas extends Canvas {

  /** These constants describe the plotting options for the
      border of the graph.
  */
  public static final int PLOT_LEFT   = 0x0001;   // plot borderline
  public static final int PLOT_RIGHT  = 0x0002;   // plot borderline
  public static final int TICK_LEFT   = 0x0004;   // plot ticks 
  public static final int TICK_RIGHT  = 0x0008;   // plot ticks 
  public static final int SUB_LEFT    = 0x0010;   // plot subticks 
  public static final int SUB_RIGHT   = 0x0020;   // plot subticks 
  public static final int TICK_INVERT = 0x0040;   // invert ticks
  public static final int LABEL_LEFT  = 0x0080;   // plot numeric labels 
  public static final int LABEL_RIGHT = 0x0100;   // plot numeric labels
  public static final int GRID        = 0x0200;   // plot grid
  public static final int RANGE       = 0x03FF;   // sum: allowed bits!
  public static final int DEFAULT     = 0x00BF;   // default

  /** These constants describe the plotting styles for data.
  */
  public static final int LINE         = 0x0001;   // line (default)
  public static final int SYMBOLS      = 0x0002;   // symbols
  public static final int LINE_SYMBOLS = 0x0003;   // line and symbols
  public static final int ALLOWED      = 0x0003;   // allowed bits!

  /** World coordinates */
  public double wx0, wx1, wy0, wy1; 
  /** Viewport coordinates as floating points. This is the way the
      values are entered by the user. */
  public double vx0, vx1, vy0, vy1;   
  /** Styles for plotting borders. See constants above. */
  int xstyle, ystyle;
  /** Distance between ticks on X and Y axes. */
  double xtick, ytick;
  /** Number of subdivisions of X and Y ticks. */
  int xsub, ysub;
  /** Current plotting color. */
  Color currentColor;
  /** Current font. */
  Font currentFont;
  /** Current plotting style for data. */
  int currentStyle;
  /** Current line width */
  int currentLineWidth;
  /** Current symbol type and size. */
  char currentSymbol;
  Font currentSymbolFont;
  /** Viewport coordinates in integer format. This is calculated
      from vx0, vx1, vy0, vy1. */
  int cvx0, cvx1, cvy0, cvy1;
  /** Constants for conversion from physical to world coordinates. */
  double wAx, wCx, wAy, wCy;
  /** Constants for conversion from world to physical coordinates. */
  double cAx, cCx, cAy, cCy;
  /** Whether or not to draw a crosshair cursor, and three variables
      to help draw it. */
  public boolean CrossHairs = false;
  public boolean crossDown = false;
  public int oldx, oldy;
  /** starting point for zooming in */
  public boolean zooming = false;
  public int zsx, zsy;
  /** If this is set to true, the PlotCanvas will handle the mouse
      event for the crosshair and zooming. If its set to false,
      it will not handle them, and pass them on. */
  public boolean handleMouseEvents = true;      

  // -----------------------------------------------------------
  /** Currently all data members are primitive types. Of course
      the Canvas must be allocated. Calls Reset().
      @return is a constructor
  */
  public PlotCanvas() {
    super();
    Reset();
  }

  // -----------------------------------------------------------
  /** Sets reasonable default values for all variable members.
      Called by the constructor. May be called by users.
  */
  public synchronized void Reset() {
    wx0  = 0.0;   wx1  = 1.0; wy0  = 0.0;    wy1  = 1.0; 
    cvx0 = 0;     cvx1 = 100; cvy0 = 0;      cvy1 = 100;
    vx0  = 0.0;   vx1  = 1.0; vy0  = 0.0;    vy1  = 1.0;   
    wAx  = 100.0; wCx  = 0.0; wAy  = -100.0; wCy  = 100.0;
    cAx  = 0.01;  cCx  = 0.0; cAy  = -0.01;  cCy  = -1.0;
    xtick = 1.0;  xsub = 2;
    ytick = 1.0;  ysub = 2;
    xstyle            = DEFAULT;
    ystyle            = DEFAULT;
    currentColor      = Color.black;
    currentFont       = new Font("Courier", Font.PLAIN, 12);
    currentStyle      = LINE;
    currentLineWidth  = 1;
    currentSymbol     = ':';
    currentSymbolFont = new Font("ZapfDingbats", Font.PLAIN, 12);
    crossDown  = false;
    CrossHairs = false;
    zooming    = false;
    oldx = 0; oldy = 0;
    zsx  = 0;  zsy = 0;
  }

  // -----------------------------------------------------------
  /** Define a viewport, in coordinates 0..1, with the origin
      assumed to be in the lower left corner.
      @param x0 Viewport coordinates X minimum
      @param x1 Viewport coordinates X maximum
      @param y0 Viewport coordinates Y minimum
      @param y1 Viewport coordinates Y maximum
  */
  public synchronized void SetViewport(double x0, double x1, double y0, double y1) {
    double r;
    // check 
    if (vx0 < 0.0) {vx0 = 0.0;}
    if (vx1 < 0.0) {vx1 = 0.0;}
    if (vx0 > 1.0) {vx0 = 1.0;}
    if (vx1 > 1.0) {vx1 = 1.0;}
    // check for order 
    if (x1 < x0) {r = x1; x1 = x0; x0 = r;}
    if (y1 < y0) {r = y1; y1 = y0; y0 = r;}
    // assign      
    vx0 = x0;
    vx1 = x1;
    vy0 = y0;
    vy1 = y1;
  }

  // -----------------------------------------------------------
  /** Define the world coordinates. The origin is assumed to
      be in the lower left corner. They must be in increasing
      order. Yes, that is an undesirable limitation... 
      @param x0 World coordinates X minimum
      @param x1 World coordinates X maximum
      @param y0 World coordinates Y minimum
      @param y1 World coordinates Y maximum
  */
  public synchronized void SetWindow(double x0, double x1, double y0, double y1) {
    double r;
    // check for equality
    if ((x1 == x0) || (y1 == y0)) {
      return;
    }
    // check for order 
    if (x1 < x0) {r = x1; x1 = x0; x0 = r;}
    if (y1 < y0) {r = y1; y1 = y0; y0 = r;}
    // assign
    wx0 = x0;
    wx1 = x1;
    wy0 = y0;
    wy1 = y1;
  }

  // -----------------------------------------------------------
  /** Set ticks. If the tick values are unacceptable, the plotting
      modes for the border are changed to switch ticks off. But
      correct values do not automatically switch ticks on. 
      @param xtick  X tick distance
      @param xsub   Number of subdivisions of an X tick
      @param ytick  YX tick distance
      @param ysub   Number of subdivisions of an Y tick
  */
  public synchronized void SetTicks(double xtick, int xsub, double ytick, int ysub) {
    // --- x axis ---
    if (xtick <= 0.0) {
      this.xstyle = this.xstyle & (~ TICK_LEFT) & (~ TICK_RIGHT);
    } else {
      this.xtick = xtick;
    }
    if (xsub <= 0) {
      this.xstyle = this.xstyle & (~ SUB_LEFT) & (~ SUB_RIGHT);
    } else {
      this.xsub = xsub;
    }
    // --- y axis ---
    if (ytick <= 0.0) {
      this.ystyle = this.ystyle & (~ TICK_LEFT) & (~ TICK_RIGHT);
    } else {
      this.ytick = ytick;
    }
    if (ysub <= 0) {
      this.ystyle = this.ystyle & (~ SUB_LEFT) & (~ SUB_RIGHT);
    } else {
      this.ysub = ysub;
    }
  }

  // -----------------------------------------------------------
  /** Set ticks. Tick values are calculated by AuxMath.TickRound.
      This version expands the world coordinates a bit to match
      the ticks. 
      @param xticks Number of X ticks
      @param yticks Number of Y ticks
  */
  public synchronized void SetTicks(double xticks, double yticks) {
    double v, w;
    int i, j;
    // --- round off ---
    v = AuxMath.TickRound((wx1-wx0) / xticks);
    i = AuxMath.GetSubTick();
    w = AuxMath.TickRound((wy1-wy0) / yticks);
    j = AuxMath.GetSubTick();
    // --- call other ticks procedure ---
    SetTicks(v, i, w, j);
    // --- expand world ---
    wx0 = xtick * Math.floor(wx0 / xtick);
    wx1 = xtick * Math.ceil( wx1 / xtick);
    wy0 = ytick * Math.floor(wy0 / ytick);
    wy1 = ytick * Math.ceil( wy1 / ytick);
  }

  // -----------------------------------------------------------
  /** Sets the border modes. The values are combinations of 
      PLOT_LEFT, PLOT_RIGHT, TICK_LEFT, TICK_RIGHT, SUB_LEFT,
      SUB_RIGHT, TICK_INVERT, LABEL_LEFT, LABEL_RIGHT. For the
      X axis, LEFT affects the bottom and RIGHT the top. Note
      that the constructor called Reset(), and Reset() set the
      styles to DEFAULT.
      @param xstyle Style for horizontal borders
      @param ystyle Style for vertical borders
  */
  public synchronized void SetBorder(int xstyle, int ystyle) {
    this.xstyle = RANGE & xstyle;
    this.ystyle = RANGE & ystyle;
  }

  // -----------------------------------------------------------
  /** Set current plotting color.
      @param col a Color object.
  */
  public synchronized void SetColor(Color col) {
    if (col == null) {return;}
    currentColor = col;
  }

  // -----------------------------------------------------------
  /** Set current line width
      @param w Integer width >= 1. 
  */
  public synchronized void SetWidth(int w) {
    if (w < 1) {
      currentLineWidth = 1;
    } else {
      currentLineWidth = w;
    }
  }

  // -----------------------------------------------------------
  /** Set current font. 
      @param font a Font object.
  */
  public synchronized void SetFont(Font font) {
    if (font == null) {return;}
    currentFont = font;
  }

  // -----------------------------------------------------------
  /** Set current plotting style
      @param style The current plotting style. 
  */
  public synchronized void SetStyle(int style) {
    currentStyle = (style & ALLOWED);
  }

  // -----------------------------------------------------------
  /** Set current symbol style
      @param ch   the symbol
      @param size the symbol size
      @param font the font of the symbol character
  */
  public synchronized void SetSymbol(char ch, Font font) {
    currentSymbol     = ch;
    currentSymbolFont = font;
  }

  // -----------------------------------------------------------
  /** Draws a fat line of indicated width. For width=1 the
      Graphics.drawLine method is used. For larger widths
      the line is defined as a filled Polygon, and
      Graphics.drawPolygon is called.
      @param g   Graphics object
      @param x0  X coordinate of first point
      @param y0  Y coordinate of first point
      @param x1  X coordinate of last point
      @param y1  Y coordinate of last point
      @param w   Line width
  */
  public void DrawFatLine(Graphics g, int x0, int y0, int x1, int y1, int w) {
    double vx, vy, r, ox, oy;
    int x[] = new int[6];
    int y[] = new int[6];

    if (w > 1) {
      g.setColor(currentColor);
      // calculate an unit vector, set length to w. 
      vx = x1 - x0;
      vy = y1 - y0;
      r = Math.sqrt(vx * vx + vy * vy);
      vx = 0.5 * w * vx / r;
      vy = 0.5 * w * vy / r;
      // an orthogonal vector
      ox = -vy;
      oy = vx;
      // points
      x[0] = (int) Math.round(x0 - vx);  y[0] = (int) Math.round(y0 - vy);
      x[1] = (int) Math.round(x0 - ox);  y[1] = (int) Math.round(y0 - oy);
      x[2] = (int) Math.round(x1 - ox);  y[2] = (int) Math.round(y1 - oy);
      x[3] = (int) Math.round(x1 + vx);  y[3] = (int) Math.round(y1 + vy);
      x[4] = (int) Math.round(x1 + ox);  y[4] = (int) Math.round(y1 + oy);
      x[5] = (int) Math.round(x0 + ox);  y[5] = (int) Math.round(y0 + oy);
      // draw
      g.fillPolygon(x, y, 6);
    } else {
      g.drawLine(x0, y0, x1, y1);
    }
    // end
    return;
  }

  // -----------------------------------------------------------
  /** Calculate intermediate values, based on current viewport
      and Window settings. The user should not need to call this
      procedure directly. 
  */
  private synchronized void IntermediateCalc() {
    Rectangle bnd;
    double w, h;
    double q, p;
    // --- get size of canvas ---
    bnd = bounds();
    w = (double) (bnd.width - 1);
    h = (double) (bnd.height - 1);
    // --- get size of viewport ---
    cvx0 = (int) Math.round(vx0 * w); cvx1 = (int) Math.round(vx1 * w);
    cvy0 = (int) Math.round(vy0 * h); cvy1 = (int) Math.round(vy1 * h);
    // --- conversion of physical to world coordinates ---
    q = wx0 - wx1;              p = (double) (cvx0 - cvx1);
    wAx = q / p;                wCx = wx0 - wAx * (double) cvx0;
    q = wy0 - wy1;              p = (double) (cvy1 - cvy0);
    wAy = q / p;                wCy = wy0 - wAy * (double) cvy1;
    // --- conversion of world to physical coordinates ---
    cAx = 1.0 / wAx;            cCx = - wCx / wAx;
    cAy = 1.0 / wAy;            cCy = - wCy / wAy;
  }

  // -----------------------------------------------------------
  /** Return viewport and world coordinates as a string. 
      @return String
  */
  public String toString() {
    String ret;
    ret = "PlotCanvas (" + vx0 + "," + vx1 + ") x (" + vy0 + "," + vy1 + ") ";
    ret = ret + "-> (" + wx0 + "," + wx1 + ") x (" + wy0 + "," + wy1 + ")";
    return ret;
  }

  // -----------------------------------------------------------
  /** Draws the border around the plot. Calls IntermediateCalc.
      Should be called before plotting anything. 
  */
  public synchronized void PlotBorder() {
    Graphics g;
    FontMetrics fm;
    String str;
    double st, dsx, x, dx, y, dy, dsy;
    int t1, s1, et, ft, es, fs, ab, at, cx, i, w, cy;
    // --- call intermediate calculations ---
    IntermediateCalc();
    // --- get graphics environment ---
    g = getGraphics();
    g.setColor(currentColor);
    g.setFont(currentFont);
    fm = getFontMetrics(getFont());
    // --- X border processing ---
    // top and bottom lines
    if ((xstyle & PLOT_LEFT) != 0)  {DrawFatLine(g, cvx0, cvy0, cvx1, cvy0, currentLineWidth);}
    if ((xstyle & PLOT_RIGHT) != 0) {DrawFatLine(g, cvx0, cvy1, cvx1, cvy1, currentLineWidth);}
    // loop for ticks
    if ((xstyle & (TICK_LEFT | TICK_RIGHT)) != 0) {
      // set start
      st = xtick * Math.rint(wx0 / xtick);
      while (st < wx0) {st = st + xtick;}
      // set subtick distance
      dsx = xtick / (double) xsub;
      // physical dimensions of ticks.
      t1 = fm.getHeight() / 2;
      s1 = t1 / 2;
      if ((xstyle & TICK_INVERT) != 0) {
        et = cvy1 + t1; ft = cvy0 - t1; es = cvy1 + s1; fs = cvy0 - s1;
      } else {
        et = cvy1 - t1; ft = cvy0 + t1; es = cvy1 - s1; fs = cvy0 + s1;
      }
      // get physical position of numeric labels
      if ((xstyle & TICK_INVERT) != 0) {
        ab = cvy1 + fm.getHeight() + t1;
        at = cvy0 - fm.getHeight() - t1;
      } else {
        ab = cvy1 + fm.getHeight();
        at = cvy0 - fm.getHeight();
      }
      // plotting loop for main ticks and labels
      x = st;
      while (x <= wx1) {
        cx = (int) Math.round(cAx * x + cCx);
        // bottom axis
        if ((xstyle & TICK_LEFT) != 0) {
          DrawFatLine(g, cx, cvy1, cx, et, currentLineWidth);
          if ((xstyle & LABEL_LEFT) != 0) {
            str = Double.toString(x);
            w = fm.stringWidth(str);
            g.drawString(str, cx - w / 2, ab);
          }
        }
        // top axis
        if ((xstyle & TICK_RIGHT) != 0) {
          DrawFatLine(g, cx, cvy0, cx, ft, currentLineWidth);
          if ((xstyle & LABEL_RIGHT) != 0) {
            str = Double.toString(x);
            w = fm.stringWidth(str);
            g.drawString(str, cx - w / 2, at);
          }
        }
        // grid
        if ((xstyle & GRID) != 0) {
          DrawFatLine(g, cx, cvy0, cx, cvy1, currentLineWidth / 2);
        }
        // loop for subticks
        if ((xstyle & (SUB_LEFT | SUB_RIGHT)) != 0) {
          dx = x + dsx;
          i = 1;
          while ((i < xsub) && (dx < wx1)) {
            cx = (int) Math.round(cAx * dx + cCx);
            if ((xstyle & SUB_LEFT) != 0)  {DrawFatLine(g, cx, cvy1, cx, es, currentLineWidth);}
            if ((xstyle & SUB_RIGHT) != 0) {DrawFatLine(g, cx, cvy0, cx, fs, currentLineWidth);}
            dx = dx + dsx;
            i++;
          }
        }
        // next
        x = x + xtick;
      }
    }
    // --- Y border processing ---
    // left and right lines
    if ((ystyle & PLOT_LEFT) != 0)  {DrawFatLine(g, cvx0, cvy0, cvx0, cvy1, currentLineWidth);}
    if ((ystyle & PLOT_RIGHT) != 0) {DrawFatLine(g, cvx1, cvy0, cvx1, cvy1, currentLineWidth);}
    // loop for ticks
    if ((ystyle & (TICK_LEFT | TICK_RIGHT)) != 0) {
      // set start
      st = ytick * Math.rint(wy0 / ytick);
      while (st < wy0) {st = st + ytick;}
      // set subtick distance
      dsy = ytick / (double) ysub;
      // physical dimensions of ticks.
      t1 = fm.getMaxAdvance() / 2;
      s1 = t1 / 2;
      if ((ystyle & TICK_INVERT) != 0) {
        et = cvx1 + t1; ft = cvx0 - t1; es = cvx1 + s1; fs = cvx0 - s1;
      } else {
        et = cvx1 - t1; ft = cvx0 + t1; es = cvx1 - s1; fs = cvx0 + s1;
      }
      // get physical position of numeric labels
      if ((ystyle & TICK_INVERT) != 0) {
        ab = cvx0 - 2 * t1;
        at = cvx1 + 2 * t1;
      } else {
        ab = cvx0 - t1;
        at = cvx1 + t1;
      }
      // plotting loop for main ticks and labels
      y = st;
      while (y <= wy1) {
        cy = (int) Math.round(cAy * y + cCy);
        // left axis
        if ((ystyle & TICK_LEFT) != 0) {
          DrawFatLine(g, cvx0, cy, ft, cy, currentLineWidth);
          if ((ystyle & LABEL_LEFT) != 0) {
            str = Double.toString(y);
            w = fm.stringWidth(str);
            g.drawString(str, ab - w, cy);
          }
        }
        // right axis
        if ((ystyle & TICK_RIGHT) != 0) {
          DrawFatLine(g, cvx1, cy, et, cy, currentLineWidth);
          if ((ystyle & LABEL_RIGHT) != 0) {
            str = Double.toString(y);
            w = fm.stringWidth(str);
            g.drawString(str, at, cy);
          }
        }
        // grid
        if ((ystyle & GRID) != 0) {
          DrawFatLine(g, cvx0, cy, cvx1, cy, currentLineWidth / 2);
        }
        // loop for subticks
        if ((ystyle & (SUB_LEFT | SUB_RIGHT)) != 0) {
          dy = y + dsy;
          i = 1;
          while ((i < ysub) && (dy < wy1)) {
            cy = (int) Math.round(cAy * dy + cCy);
            if ((ystyle & SUB_LEFT) != 0) {DrawFatLine(g, cvx0, cy, fs, cy, currentLineWidth);}
            if ((ystyle & SUB_RIGHT) != 0) {DrawFatLine(g, cvx1, cy, es, cy, currentLineWidth);}
            dy = dy + dsy;
            i++;
          }
        }
        // next
        y = y + ytick;
      }
    }
  }

  // -----------------------------------------------------------
  /** The paint procedure does nothing.
      @param g Graphics object
  */
  public void paint(Graphics g) {
    return;
  }

  // -----------------------------------------------------------
  /** The clear procedure wipes the canvas clean. It is important
      to do this in a repaint, for it wipes the cursor lines away.
      Calls Reset();
  */
  public synchronized void clear() {
    Graphics g;
    Rectangle r;
    g = getGraphics();
    r = bounds();
    // fill background
    g.setColor(getBackground());
    g.fillRect(0, 0, r.width-1, r.height-1);
    g.setColor(getBackground());
    // reset
    Reset();
  }

  // -----------------------------------------------------------
  /** Plots data starting from 2 arrays, containing x and y, the 
      world coordinates of the datapoints. The arrays must be
      of at least length n. 
      @param x Array of X values
      @param y Array of Y values
      @param n Number of points to plot
  */
  public void Plot2Arrays(double x[], double y[], int n) {
    int px[], py[];
    int i; 
    // --- check ---
    if ((x == null) || (y == null) || (n <= 0)) {
      return;
    }
    if ((x.length < n) || (y.length < n)) {
      return;
    }
    // --- calculate arrays with physical coordinates ---
    px = new int[n];
    py = new int[n];
    for (i = 0; i < n; i++) {
      px[i] = (int) Math.round(cAx * x[i] + cCx);
      py[i] = (int) Math.round(cAy * y[i] + cCy);
    }
    // --- now plot with physical coordinates ---
    PlotwPhysicalCoords(px, py, n);
    // --- clean up ---
    px = null;
    py = null;
  }

  // -----------------------------------------------------------
  /** Plots data starting from 1 array, containing the y 
      coordinates of the datapoints, and an x0, dx definition
      of the X points. The array must be of at least length n. 
      @param x0 X start value
      @param dx X increment
      @param y Array of Y values
      @param n Number of points to plot
  */
  public void Plot1Array(double x0, double dx, double y[], int n) {
    int px[], py[];
    double x;
    int i; 
    // --- check ---
    if ((dx == 0.0) || (y == null) || (n <= 0)) {
      return;
    }
    if ((y.length < n)) {
      return;
    }
    // --- calculate arrays with physical coordinates ---
    px = new int[n];
    py = new int[n];
    x = x0; 
    for (i = 0; i < n; i++) {
      px[i] = (int) Math.round(cAx * x    + cCx);
      py[i] = (int) Math.round(cAy * y[i] + cCy);
      x = x + dx;
    }
    // --- now plot with physical coordinates ---
    PlotwPhysicalCoords(px, py, n);
    // --- clean up ---
    px = null;
    py = null;
  }

  // -----------------------------------------------------------
  /** Plots symbols at coordinates (cx, cy). This is a low-level
      plotting procedure. There should be no need to call it
      directly, but it can. The symbol is a DingBats font
      character. Useful characters are: l, m, n, s, t, u, h,
      I, 6, and :. 
      @param g  Graphics object
      @param cx[] physical X coordinates
      @param cy[] physical Y coordinates
      @param n number of data points to plot
      @param symbol Dingbats character
      @param size   Size in points
  */
  public void PlotSymbols(Graphics g, int cx[], int cy[], int n) {
    Font sft, oft;
    FontMetrics fm;
    int hw, hh, i;
    String str;

    // --- settings ---
    oft = g.getFont();
    g.setFont(currentSymbolFont);
    g.setColor(currentColor);
    fm = g.getFontMetrics();
    hw = fm.charWidth(currentSymbol) / 2;
    hh = fm.getHeight() / 2;
    str = String.valueOf(currentSymbol);
    // --- plot ---
    for (i = 0; i < n; i++) {
      g.drawString(str, cx[i] - hw, cy[i] + hh);
    }
    // --- restore font ---
    validate();
    g.setFont(oft);
  }

  // -----------------------------------------------------------
  /** Plots data starting from 2 arrays, containing px and py, the 
      physical coordinates of the datapoints. The arrays must be
      of at least length n. This procedure is not intended to be
      called by the user, but it might. 
      @param px Array of X values
      @param py Array of Y values
      @param n Number of points to plot
  */
  public void PlotwPhysicalCoords(int px[], int py[], int n) {
    Graphics g;
    Rectangle r;
    int i;
    // --- settings ---
    g = getGraphics();
    g.setColor(currentColor);
    g.setFont(currentFont);
    g.clipRect(cvx0, cvy0, cvx1 - cvx0 + 1, cvy1 - cvy0 + 1);
    // --- lines ---
    if ((currentStyle & LINE) != 0) {
      for (i = 0; i < n-1; i++) {
        DrawFatLine(g, px[i], py[i], px[i+1], py[i+1], currentLineWidth);
      }
    }
    // --- symbols ---
    if ((currentStyle & SYMBOLS) != 0) {
      PlotSymbols(g, px, py, n);
    }
  }

  // -----------------------------------------------------------
  /** Add a label. The coordinates are given as world window
      coordinates.
      @param x   X coordinate
      @param y   Y coordinate
      @param j   Justification 0..1
      @param str String label 
  */
  public void LabelWindow(double x, double y, double j, String str) {
    FontMetrics fm;
    Graphics g;
    int h, w, px, py;
    // --- info ----
    g = getGraphics();
    fm = g.getFontMetrics();
    h = fm.getHeight();
    w = fm.stringWidth(str);
    // --- coordinates ---
    px = (int) Math.round(cAx * x + cCx - j * w);
    py = (int) Math.round(cAy * y + cCy);
    // --- plot ---
    g.drawString(str, px, py);
  }

  // -----------------------------------------------------------
  /** Add a label. The coordinates are given as viewport window
      coordinates. That is, they run 0..1 for the viewport, as
      defined in SetViewport, with the origin in the lower left
      corner of the Viewport. Note that you can use coordinates
      smaller than 0.0 or larger than 1.0, if you want to
      plot something outside the viewport. 
      @param x   X coordinate
      @param y   Y coordinate
      @param j   Justification 0..1
      @param str String label 
  */
  public void LabelViewport(double x, double y, double j, String str) {
    FontMetrics fm;
    Graphics g;
    int h, w, px, py;
    // --- info ----
    g = getGraphics();
    fm = g.getFontMetrics();
    h = fm.getHeight();
    w = fm.stringWidth(str);
    // --- coordinates ---
    px = (int) Math.round(cvx0 + x * (cvx1 - cvx0) - j * w);
    py = (int) Math.round(cvy1 - y * (cvy1 - cvy0));
    // --- plot ---
    g.drawString(str, px, py);
  }

  // -----------------------------------------------------------
  /** Convert x physical coordinate to x world coordinate.
      @param x physical coordinate
      @return x world coordinate
  */
  public double toWorldX(int x) {
    return(wAx * (double) x + wCx);
  }

  // -----------------------------------------------------------
  /** Convert y physical coordinate to y world coordinate.
      @param y physical coordinate
      @return y world coordinate
  */
  public double toWorldY(int y) {
    return(wAy * (double) y + wCy);
  }

  // -----------------------------------------------------------
  /** Convert x world coordinate to x physical coordinate.
      @param x world coordinate
      @return x physical coordinate
  */
  public int toPhysX(double x) {
    return((int) Math.round(cAx * x + cCx));
  }

  // -----------------------------------------------------------
  /** Convert y world  coordinate to y physical coordinate.
      @param y world coordinate
      @return y physical coordinate
  */
  public int toPhysY(double y) {
    return((int) Math.round(cAy * y + cCy));
  }

  // -----------------------------------------------------------
  /** Set drawing of crosshairs on/off. 
  */
  public void toggleCrossHairs() {
    Graphics g;
    if (CrossHairs) {
      if (crossDown) {
        g = getGraphics();
        g.setXORMode(Color.yellow);
        g.drawLine(cvx0, oldy, cvx1, oldy);
        g.drawLine(oldx, cvy0, oldx, cvy1);
        crossDown = false;
      }
    }
    CrossHairs = ! CrossHairs;
  }

  // -----------------------------------------------------------
  /** Event handler. Deals with:
      MOUSE_MOVE, draws crosshair cursor if CrossHair is true
      MOUSE_DOWN, starts zooming when Button 3 is pressed
      MOUSE_DRAG, draws zooming rectangle
      MOUSE_UP, ends zooming
      @param evt event to handle
  */
  public boolean handleEvent(Event evt) {
    Event ne;
    boolean b;
    switch (evt.id) {
    case Event.MOUSE_MOVE:
      if (handleMouseEvents) {
        if (CrossHairs) MoveCross(evt.x, evt.y);
        return(true);
      }
    case Event.MOUSE_DOWN:
      if (handleMouseEvents) {
        if ((evt.modifiers & Event.META_MASK) != 0) {
          ZoomStart(evt.x, evt.y);
          return(true);
        } else if ((evt.modifiers & Event.ALT_MASK) != 0) {
          ne = new Event(this, Event.ACTION_EVENT, "UNZOOM");
          b = postEvent(ne);
          return(true);
        }
        break;
      }
    case Event.MOUSE_DRAG:
      if (handleMouseEvents) {
        if (zooming) {
          ZoomDrag(evt.x, evt.y);
          return(true);
        }
      }
      break;
    case Event.MOUSE_UP:
      if (handleMouseEvents) {
        if (zooming) {
          ZoomEnd(evt.x, evt.y);
          ne = new Event(this, Event.ACTION_EVENT, "ZOOM");
          b = postEvent(ne);
          return(true);
        }
      }
    }
    return(false);
  }

  // ------------------------------------------------------------------
  /** This will draw with XOR a crosshair cursor on the plot. It uses
      crossDown, oldx and oldy to store data for the next iteration. It
      is called by handleEvent when CrossHair is true.
      @param x physical x coordinate
      @param y physical y coordinate
  */      
  public boolean MoveCross(int x, int y) {
    Graphics g;
    // graphics object
    g = getGraphics();
    g.setXORMode(Color.yellow);
    // draw
    CrossUp(g);
    CrossDown(g, x, y);
    return(true);
  }

  // ------------------------------------------------------------------
  /** Remove the crosshairs from display, if they are there.
  */
  public void CrossUp(Graphics g) {
    // delete old cross
    if (crossDown) {
      g.drawLine(cvx0, oldy, cvx1, oldy);
      g.drawLine(oldx, cvy0, oldx, cvy1);
    }
  }

  // ------------------------------------------------------------------
  /** Put the crosshairs down at the specified position, if CrossHairs
      is true.
  */
  public void CrossDown(Graphics g, int x, int y) {
    boolean inside;
    if (CrossHairs) {
      // current position inside or not
      inside = (cvx0 <= x) && (x <= cvx1) && (cvy0 <= y) && (y <= cvy1);
      // draw new cross
      if (inside) {
        g.drawLine(cvx0, y, cvx1, y);
        g.drawLine(x, cvy0, x, cvy1);
        oldx = x;
        oldy = y;
        crossDown = true;
      } else {
        crossDown = false;
      }
    }
  }

  // ---------------------------------------------------------------------
  /** Starts a zoom. Deletes the crosshair cursor, and draws the selection
      rectangle. 
      @param x physical x coordinate
      @param y physical y coordinate
  */      
  public void ZoomStart(int x, int y) {
    Graphics g;
    // graphics object
    g = getGraphics();
    g.setXORMode(Color.yellow);
    // current position inside or not
    // delete cross if it is there
    CrossUp(g);
    // Store this as starting point. Note that one can start zooming
    // outside the current dimensions of the viewport!
    zooming = true;
    zsx = x;
    zsy = y;
    // store the old coordinates
    oldx = x;
    oldy = y;
  }

  // ----------------------------------------------------------------------
  /** Zoom. Resizes the selection rectangle, and stores the position for the
      next iteration. 
      @param x physical x coordinate
      @param y physical y coordinate
  */      
  public void ZoomDrag(int x, int y) {
    Graphics g;
    // graphics object
    g = getGraphics();
    g.setXORMode(Color.yellow);
    // draw rectangle
    if (zooming) {
      // delete old one
      GenDrawRect(g, zsx, zsy, oldx, oldy);
      // draw new one
      GenDrawRect(g, zsx, zsy, x, y);
      // copy position
      oldx = x;
      oldy = y;
    }
  }

  // ---------------------------------------------------------------------
  /** Ends a zoom. Deletes the selection rectangle, changes the world
      coordinates wx0, wx1, wy0, wy1, and posts and ACTION_EVENT
      with an argument String "ZOOM".
      @param x physical x coordinate
      @param y physical y coordinate
  */      
  public void ZoomEnd(int x, int y) {
    Event ne;
    Graphics g;
    boolean inside, b;
    int r;
    // --- graphics object ---
    g = getGraphics();
    g.setXORMode(Color.yellow);
    // --- delete rectangle ---
    if (zooming) {
      // delete rectangle
      GenDrawRect(g, zsx, zsy, x, y);
      zooming = false;
    }
    // --- draw new crosshairs ---
    CrossDown(g, x, y);
    // --- change world coords ---
    if ((zsx != x) && (zsy != y)) {
      if (zsx > x) {r = x; x = zsx; zsx = r;}
      if (zsy < y) {r = y; y = zsy; zsy = r;}  // inverted Y axis!
      wx0 = toWorldX(zsx);
      wx1 = toWorldX(  x);
      wy0 = toWorldY(zsy);
      wy1 = toWorldY(  y);
    }
    return;
  }

  // --- Draw a general x0, y0, x1, y1 rectangle --------------------------
  public static void GenDrawRect(Graphics g, int x0, int y0, int x1, int y1) {
    if        ((x1 >= x0) && (y1 >= y0)) {
      g.drawRect(x0, y0, x1-x0+1,y1-y0+1);
    } else if ((x0 >= x1) && (y1 >= y0)) {
      g.drawRect(x1, y0, x0-x1+1, y1-y0+1);
    } else if ((x1 >= x0) && (y0 >= y1)) {
      g.drawRect(x0, y1, x1-x0+1, y0-y1+1);
    } else if ((x0 >= x1) && (y0 >= y1)) {
      g.drawRect(x1, y1, x1-x0+1, y0-y1+1);
    }
  }

}

/* ===================================================================== */

