/**************************************************************************
* Copyright (c) 2001, 2005 David J. Eck                                   *
*                                                                         *
* Permission is hereby granted, free of charge, to any person obtaining   *
* a copy of this software and associated documentation files (the         *
* "Software"), to deal in the Software without restriction, including     *
* without limitation the rights to use, copy, modify, merge, publish,     *
* distribute, sublicense, and/or sell copies of the Software, and to      *
* permit persons to whom the Software is furnished to do so, subject to   *
* the following conditions:                                               *
*                                                                         *
* The above copyright notice and this permission notice shall be included *
* in all copies or substantial portions of the Software.                  *
*                                                                         *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,         *
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF      *
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  *
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY    *
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,    *
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE       *
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                  *
*                                                                         *
* ----                                                                    *
* (Released under new license, April 2012.)                               *
*                                                                         *
*             David J. Eck                                                *
*             Department of Mathematics and Computer Science              *
*             Hobart and William Smith Colleges                           *
*             300 Pulteney Street                                         *
*             Geneva, NY 14456                                            *
*             eck@hws.edu                                                 *
*             http://math.hws.edu/eck                                     *
**************************************************************************/

package edu.hws.jcm.draw;

import edu.hws.jcm.awt.*;
import java.util.StringTokenizer;
import java.util.Vector;
import java.awt.*;
import java.awt.event.*;

/**
 * A DisplayCanvas is a drawing area that can contain one or more CoordinateRects.  
 * Each CoordinateRect can, in turn, contain Drawable items.  If you only want
 * one CoordinateRect that fills the whole canvas, you can for the most part
 * ignore the CoordinateRect and work only with the canvas.
 * The option of using an offscreen image for double buffering is provided.
 * By default, this option is on.  
 * <p>If a DisplayCanvas is added to a
 * Controller, then it will take care of calling the checkInput()
 * and compute() methods of the InputObjects and Computables that
 * it contains, so there is no need to add them individually to
 * the Controller.  If the DisplayCanvas is added to a JCMPanel, it
 * is automatically added to the Controller for that JCMPanel.
 * (On the other hand, if a DisplayCanvas is added to a Controller,
 * this means that all the
 * items in the DisplayCanvas will be recomputed, even if only some
 * of them need to be.)
 * <p>The canvas can display an error message that goes away
 * when the canvas is clicked or when clearErrorMessage()
 * is called.  This allows the Canvas to be used as an 
 * ErrorReporter for a Controller or LimitControlPanel.
 * <p>When any changes are made to the contents of the Canvas,
 * doRedraw() must be called for those changes to take effect.  
 * Normally, doRedraw() is called by the CoordinateRect or by
 * one of the Drawables in a CoordinateRect.  Use doRedraw(CoordinateRect c) 
 * or doRedraw(int index) to redraw a single CoordinateRect. Note that
 * repainting the canvas is not enough, because this will not automatically
 * refresh the off-screen image.
 *
 */

public class DisplayCanvas extends Canvas
                 implements ErrorReporter, InputObject, Computable {


   private Vector coordinateRects;  
      // Contains all the CoordinateRects that have been added to this canvas.
      // The elements are members of the private static nested class CRData.
   
   private boolean useOffscreenCanvas = true;  // If true, double buffering is used.
   
   private boolean handleMouseZooms = false;   // If true, clicking on a CoordinateRect
                                               //   will zoom in on the clicked point, or
                                               //   will zoom out if the shift key is down.
   
   /**
    * Create a DisplayCanvas with a white background containing no CoordinateRects.
    */   
   public DisplayCanvas() {
      setBackground(Color.white);
      enableEvents(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
   }
   
   /**
    * Create a DisplayCanvas with a white background and containing the
    * specified CoordinateRect.  The CoordinateRect fills the entire canvas.
    *
    * @param c The CoordinateRect that will fill the canvas.  If c is null, no CoordinateRect is added to the canvas.
    */   
   public DisplayCanvas(CoordinateRect c) {
      this();
      if (c != null)
         addCoordinateRect(c);
   }

   /**
    * Release the memory used the by the off-screen image, if any, that is used for
    * double-buffering.  It's a good idea to call this if the DisplayCanvas is in
    * an applet and the applet is stopped.
    */   
   public void releaseResources() {
      OSC = null;
      OSG = null;
   }
   

   /**
    * Set the "handleMouseZooms" property of this DisplayCanvas.  IF the value is true,
    * then clicking on the canvas will zoom in on the point that is clicked and shift-clicking
    * will zoom out from that point.  Only the CoordinateRect, if any, that contains the point
    * is zoomed.  Furthermore, if the user clicks-and-drags, a rectangle
    * is drawn.  When the mouse is released, the interior of the rectangle is zoomed to fill
    * the CoordinateRect.  This property is false by default.
    */
   public void setHandleMouseZooms(boolean handle) {
      handleMouseZooms = handle;
   }
   
   /**
    * Get the "handleMouseZooms" property of this DisplayCanvas, which determines whether the
    * canvas reacts to mouse events by zooming the CoordinateRect that is clicked.
    */
   public boolean getHandleMouseZooms() {
      return handleMouseZooms;
   }
   
   /**
    * Set the "useOffscreenCanvas" property of this DisplayCanvas.  IF the value is true,
    * an off-screen image is used for double buffering.  This property is true by default.
    */
   public boolean getUseOffscreenCanvas() { 
      return useOffscreenCanvas;
   }

   /**
    * Get the "useOffscreenCanvas" property of this DisplayCanvas, which determines whether
    * double-buffering is used.
    */
   public void setUseOffscreenCanvas(boolean use) {
      useOffscreenCanvas = use;
      if (!use) {
         OSC = null;
         OSG = null;
      }
   }
   
   // ----------- For managing CoordinateRects ------------------------
     
   /**
    * This private subclass of DisplayCanvas holds the data for one CoordinateRect 
    * contained in a DisplayCanvas.
    */
   private static class CRData implements java.io.Serializable {  
            // Data for one coordinate rect
      CoordinateRect coords;
      double xmin, xmax, ymin, ymax; 
               // Values between 0 and 1 that
               // specify the region of the canvas occupied by this
               // CoordinateRect.
      Color background;  // Color to fill area with before drawing.
                         // If it's null, no fill is done.  (The display color
                         // of the Canvas shows through.)
   }
   
   /**
    * Add the specified Drawable item to the first CoordinateRect in this DisplayCanvas.
    * If no CoordinateRect is associated with the canvas, one is created to fill the
    * entire canvas.
    */
   public void add(Drawable d) {
      if (coordinateRects == null)
         addCoordinateRect(new CoordinateRect());
      CoordinateRect c = ((CRData)coordinateRects.elementAt(0)).coords;
      c.add(d);
   }
   
   /**
    * Add a Drawable item to one of the CoordinateRects associated with the Canvas.
    *
    * @param d The Drawable item to be added to a CoordinateRect
    * @param coordRectIndex The index of the CoordinateRect, where the index of the first
    *    CoordinateRect that was added to the cavas is zero, the index of the second is one,
    *    and so on.  A CoordinateRect with the specified index must already exist in the
    *    canvas, or an IllegalArgumentException is thrown.
    */
   public void add(Drawable d, int coordRectIndex) {
      if (coordinateRects == null || coordRectIndex < 0 || coordRectIndex >= coordinateRects.size())
         throw new IllegalArgumentException("Internal programming error:  CoordinateRect index (" + coordRectIndex + ")out of range.");
      CoordinateRect c = ((CRData)coordinateRects.elementAt(coordRectIndex)).coords;
      c.add(d);
   }

   /**
    * Add the specified CoordinateRect to this DisplayCanvas, filling the entire canvas,
    * and with background color equal to the background color of the canvas.
    * 
    * @param c the CoordinateRect to be added.  If null, an IllegalArgumentException is thrown.
    */
   public void addCoordinateRect(CoordinateRect c) {
      addCoordinateRect(c,0,1,0,1,null);
   }

   /**
    * Add a CoordinateRect to the canvas, occupying a specified region of the canvas.
    *
    * @param coords The CoordinateRect to be added.  If this is null, an IllegalArgumentExceptionis thrown.
    * @param hmin Specifies the left edge of the CoordinateRect in the canvas, as a fraction of the size of the canvas.
    *             This must be in the range form 0 to 1, or an IllegalArgumentException is thrown.
    * @param hmax Specifies the right edge of the CoordinateRect in the canvas, as a fraction of the size of the canvas.
    *             This must be in the range form 0 to 1 and must be strictly greater than hmin, or an IllegalArgumentException is thrown.
    * @param vmin Specifies the top edge of the CoordinateRect in the canvas, as a fraction of the size of the canvas.
    *             This must be in the range form 0 to 1, or an IllegalArgumentException is thrown.
    * @param vmax Specifies the bottom edge of the CoordinateRect in the canvas, as a fraction of the size of the canvas.
    *             This must be in the range form 0 to 1 and must be strictly greater than vmin, or an IllegalArgumentException is thrown.
    * @param background The background color of the CoordinateRect.  The CoordinateRect is filled with this color
    *             before the Drawables that it contains are drawn.  If background is null, no filling takes place
    *             and the canvas shows through.
    */
   public void addCoordinateRect(CoordinateRect coords, double hmin, double hmax,
                                     double vmin, double vmax, Color background) {
      if (hmin < 0 || hmin > 1 || hmax < 0 || hmax > 1 || hmin >= hmax ||
               vmin < 0 || vmin > 1 || vmax < 0 || vmax > 1 || vmin >= vmax)
         throw new IllegalArgumentException("Illegal values for area covered by CoordinateRect.");
      if (coords == null)
         throw new IllegalArgumentException("Can't add null CoordinateRect to DisplayCanvas.");
      CRData c = new CRData();
      c.coords = coords;
      c.xmin = hmin;
      c.xmax = hmax;
      c.ymin = vmin;
      c.ymax = vmax;
      c.background = background;
      if (coordinateRects == null)
         coordinateRects = new Vector();
      coordinateRects.addElement(c);
      coords.setOwner(this);
   }
   
   /**
    * Add a newly created CoordinateRect covering the specified section of
    * the canvas.  hmin, hmax, vmin, vmax must be in the range 0 to 1.
    * The index of the new CoordinateRect is returned.
    */
   public int addNewCoordinateRect(double hmin, double hmax, double vmin, double vmax) {
      CoordinateRect c = new CoordinateRect();
      addCoordinateRect(c,hmin,hmax,vmin,vmax,null);   
      return coordinateRects.size() - 1;
   }
   
   /**
    * Add a newly created CoordinateRect covering the specified section of
    * the canvas, with the specfied background color.  hmin, hmax, vmin, vmax must be in the range 0 to 1.
    * The index of the new CoordinateRect is returned.
    */
   public int addNewCoordinateRect(double hmin, double hmax, double vmin, double vmax, Color background) {
      CoordinateRect c = new CoordinateRect();
      addCoordinateRect(c,hmin, hmax, vmin, vmax,background);   
      return coordinateRects.size() - 1;
   }
   
   /**
    * Get the first CoordinateRect in this canvas.  (If none exists, one is created and
    * added to the canvas.)
    */
   public CoordinateRect getCoordinateRect() {
      return getCoordinateRect(0);
   }

   /**
    * Get the i-th CoordinateRect in this DisplayCanvas.  They are numbered staring from zero.
    * If there is no i-th rect, null is returned, except that if there
    * are NO coordinate rects and a request for rect 0 is received,
    * then a new CoordinateRect is added to fill the entire canvas.
    */
   public CoordinateRect getCoordinateRect(int i) {
      if (i == 0 && (coordinateRects == null || coordinateRects.size() == 0))
         addNewCoordinateRect(0,1,0,1);
      if (coordinateRects == null || i < 0 || i >= coordinateRects.size())
         return null;
      else
         return ((CRData)coordinateRects.elementAt(i)).coords;
   }
    
   /**  
    * Return CoordinateRect that contains the specified pixel, or
    * null if there is none.  The CoordinateRects are searched in
    * reverse order, so that the "top" CoordinateRect at that point is
    * returned.  Note that this method only makes sense if the canvas
    * has already been displayed.
    * (Mostly, this is for internal use in this class.)
    */
   public CoordinateRect findCoordinateRectAt(int pixelX, int pixelY) {
      if (coordinateRects == null)
         return null;
      for (int i = coordinateRects.size() - 1; i >= 0; i--) {
         CRData c = (CRData)coordinateRects.elementAt(i);
         int width = getSize().width;
         if (width <= 0)
            return null;
         int height = getSize().height;
         int x = (int)(c.xmin*width);
         int y = (int)(c.ymin*height);
         int r = (int)(c.xmax*width);
         int b = (int)(c.ymax*height);
         if (pixelX >= x && pixelX < r && pixelY >= y && pixelY < b)
            return c.coords;
      }
      return null;
   }
    
   /**
    * Should be called whenever the contents of the canvas have changed and so
    * it needs to need to be redrawn.  (This causes the off-screen image to be redrawn.
    * A simple call to repaint() does not do this.)
    * If only one CoordinateRect needs to be repainted, you can call doRedraw(int i) or
    * or doRedraw(CoordinateRect c), which can be more efficient than redrawing the whole canvas.
    * If an error message is displayed, it will be cleared.
    */
   synchronized public void doRedraw() {
        // Should be called whenever the coordinate rects need to be repainted.
        // If only one needs to be repainted, you can call doRedraw(int i) or
        // or doRedraw(CoordinateRect c), which can be more efficient.
        // If an error message is displayed, this will not take effect until
        // the error message is cleared.
      OSCvalid = false;
      if (errorMessage != null)
         clearErrorMessage();  // does repaint
      else
         repaint();
   }
   
   /**
    * To be called when the contents of one of the CordinateRects have changed and so
    * it needs to need to be redrawn.  (This causes the off-screen image to be redrawn.
    * A simple call to repaint() does not do this.)
    * If an error message is displayed, it will be cleared.
    *
    * @param coordRectIndex The index of the CoordinateRect to be redrawn, where the first CoordinateRect is at index zero.
    *        If there is no such CoordinateRect, then nothing is done.
    */
   synchronized public void doRedraw(int coordRectIndex) {
      if (coordinateRects != null && coordRectIndex >= 0 && coordRectIndex < coordinateRects.size()) {
         CRData c = (CRData)coordinateRects.elementAt(coordRectIndex);
         OSCvalid = false;
         if (errorMessage != null) 
            clearErrorMessage();  // does repaint
         else {
            int width = getSize().width;
            int height = getSize().height;
            int x = (int)(c.xmin*width);
            int y = (int)(c.ymin*height);
            int w = (int)(c.xmax*width) - x;
            int h = (int)(c.ymax*height) - y;
            repaint(x,y,w,h);
         }
      }
   }
   
   /**
    * To be called when the contents of one of the CordinateRects have changed and so
    * it needs to need to be redrawn.  (This causes the off-screen image to be redrawn.
    * A simple call to repaint() does not do this.)
    * If an error message is displayed, it will be cleared.
    *
    * @param coords The CoordinateRect to be redrawn.  If coords is not in this DisplayCanvas, nothing is done.
    */
   synchronized public void doRedraw(CoordinateRect coords) {
      int size = (coordinateRects == null)? -1 : coordinateRects.size();
      for (int i = 0; i < size; i++)
         if (((CRData)coordinateRects.elementAt(i)).coords == coords) {
            doRedraw(i);
            break;
         }
   }
   

//------------------- InputObject/Computable interfaces ---------------------
   
   /**
    * This is generally called by a Controller.  It calls the checkInput() method of
    * any InputObject displayed on this Canvas.
    */
   synchronized public void checkInput() {
      if (coordinateRects != null) {
         int top = coordinateRects.size();
         for (int i = 0; i < top; i++)
            ((CRData)coordinateRects.elementAt(i)).coords.checkInput();
      }
   }
   
   /**
    * This is generally called by a Controller.  It calls the compute() method of
    * any InputObject displayed on this Canvas.
    */
   synchronized public void compute() {
      if (coordinateRects != null) {
         int top = coordinateRects.size();
         for (int i = 0; i < top; i++)
            ((CRData)coordinateRects.elementAt(i)).coords.compute();
      }
   }
   
   /**
    * Method required by InputObject interface; in this class, calls the same method
    * recursively on any CoordinateRects contained in this DisplayCanvas.  This is meant to 
    * be called by JCMPanel.gatherInputs().
    */
    public void notifyControllerOnChange(Controller c) {
      if (coordinateRects != null) {
         int top = coordinateRects.size();
         for (int i = 0; i < top; i++)
            ((CRData)coordinateRects.elementAt(i)).coords.notifyControllerOnChange(c);
      }
    }

//-------------------- Error Reporter Stuff ------------------------------------

   /** 
    * Get color that is used as a background when the canvas displays an error message.
    */
   public Color getErrorBackground() { 
      return errorBackground; 
   }
 
   /** 
    * Set color to be used as a background when the canvas displays an error message.
    * The default is a light green.  If the specified Color value is null, nothing is done.
    */
   public void setErrorBackground(Color c) { 
      if (c != null)
         errorBackground = c; 
   }
   
   /** 
    * Get color that is used for the text when the canvas displays an error message.
    */
   public Color getErrorForeground() { 
     return errorForeground; 
   }
   
   /** 
    * Set color to be used for the text when the canvas displays an error message.
    * The default is a dark green.  If the specified Color value is null, nothing is done.
    */
   public void setErrorForeground(Color c) { 
      if (c != null)
         errorForeground = c; 
   }

   /**
    * Get the error message that is currently displayed on the canvas.  If no error
    * is displyed, the return value is null.
    */
   synchronized public String getErrorMessage() { 
      return errorMessage;
   }

   /**
    * Set an error message to be displayed on the canvas.  This method is generally called by
    * a Controller or a LimitControlPanel.  If you call it directly, use null as the first parameter.
    * 
    * @param c The Controller, if any, that is calling this routine.  This controller will be notified
    *             when the error message is cleared.  If the method is not being called by a contrller, this
    *             parameter should be set to null.
    * @param message The error message to be displayed.  If the value is null or is a blank string, 
    *                the current message, if any, is cleared.
    */
   synchronized public void setErrorMessage(Controller c, String message) { 
      if (message == null || message.trim().length() == 0) {
         if (errorMessage != null) {
               clearErrorMessage();
            if (errorSource != c)
               errorSource.errorCleared();
            repaint();
         }
      }
      else {
         errorMessage = message.trim();
         errorSource = c;
         OSCvalid = false;
         repaint();
      }
   }

   /**
    * Clear the error message, if any, that is currently displayed on the canvas.
    */
   synchronized public void clearErrorMessage() {
      if (errorMessage == null)
         return;
      errorMessage = null;
      if (errorSource != null)
         errorSource.errorCleared();
      errorSource = null;
      repaint();
   }

   // ---------------------- ErrorReporter Implementation ----------------------------

   private Color errorBackground = new Color(220,255,220);
   private Color errorForeground = new Color(0,120,0);
   private String errorMessage;
   private Controller errorSource;
   private Draggable dragged;  // The draggable object, if any, that is being dragged.

   /**
    * This has been overridden to handle the mouse zoom feature.
    * Not meant to be called directly.
    */
   public void processMouseEvent(MouseEvent evt) {
         // If an error message is displayed, get rid of it and
         // ignore the mouse click.
      if (evt.getID() == MouseEvent.MOUSE_PRESSED) {
         dragging = false;  // Shouldn't be possible, but some old, buggy versions of Java made it so.
         dragged = null;
         if (errorMessage != null) {
            if (errorSource != null)
               errorSource.errorCleared();
            errorSource = null;
            errorMessage = null;
            repaint();
            evt.consume();
            return;
         }
         CoordinateRect c = findCoords(evt);
         if (c != null)
            dragged = c.checkDraggables(evt);
         if (dragged != null)
            return;
         if (handleMouseZooms && !(evt.getClickCount() > 1 || evt.isAltDown() || evt.isMetaDown() || evt.isControlDown())) {
            super.processMouseEvent(evt);
            if (!evt.isConsumed())
               doMouseZoom_pressed(evt);
            return;
         }
      }
      else if (evt.getID() == MouseEvent.MOUSE_RELEASED && handleMouseZooms && dragged != null) {
         dragged.finishDrag(evt);
         dragged = null;
         return;
      }
      else if (evt.getID() == MouseEvent.MOUSE_RELEASED && handleMouseZooms && dragging) {
         doMouseZoom_released(evt);
         return;
      }
      super.processMouseEvent(evt);
   }
   
   /**
    * This has been overridden to handle the mouse zoom feature.
    * Not meant to be called directly.
    */
   public void processMouseMotionEvent(MouseEvent evt) {
      if (dragged != null && evt.getID() == MouseEvent.MOUSE_DRAGGED)
         dragged.continueDrag(evt);
      else if (dragging && evt.getID() == MouseEvent.MOUSE_DRAGGED)
         doMouseZoom_moved(evt);
      else
         super.processMouseMotionEvent(evt);
   }

   private void drawErrorMessage(Graphics g) {
      if (errorMessage == null)
         return;

      Font font = new Font("Helvetica",Font.BOLD,12);
      FontMetrics fm = g.getFontMetrics(font);
      
      int width = getSize().width;
      int height = getSize().height;
      int lineHeight = fm.getHeight();
      int leading = fm.getLeading();
      int messageWidth = width - 80;
      int left = 30;
      int maxLines = (height - 60 - lineHeight) / lineHeight;
      if (maxLines <= 0)
         maxLines = 1;
            
      StringTokenizer t = new StringTokenizer(errorMessage, " \t\r\n");
      int lineCt = 0;
      String[] errorMessageList = new String[maxLines];
      String line = "   ";  // indent first line
      while (t.hasMoreTokens())  {
          String word = t.nextToken();
          if (fm.stringWidth(word) > messageWidth) {
              String w = "";
              int dots = fm.stringWidth("...");
              for (int c = 0; c < word.length(); c++) {
                 w += word.charAt(c);
                 if (fm.stringWidth(w) + dots > messageWidth)
                    break;
              }
              word = w;
          }
          String linePlusWord = line + " " + word;
          if (fm.stringWidth(linePlusWord) > messageWidth) {
             errorMessageList[lineCt] = line;
             lineCt++;
             if (lineCt == maxLines)
                break;
             line = word;
          }
          else {
             line = linePlusWord;
          }
      }
      if (lineCt < maxLines) {
         errorMessageList[lineCt] = line;
         lineCt++;
      }
      if (lineCt == 1)
         errorMessageList[0] += "    ";  // for proper centering
      
      int boxWidth = width - 60;
      int boxHeight = (lineCt+1)*lineHeight + 50;
      int top = height/2 - boxHeight/2;
      if (top < 0)
         top = 0;
      
      g.setColor(getBackground());
      g.fillRect(0,0,width,height);
      g.setColor(errorBackground);
      g.fillRect(left,top,boxWidth,boxHeight);
      g.setColor(errorForeground);
      g.drawRect(left,top,boxWidth,boxHeight);
      g.drawRect(left+1,top+1,boxWidth-2,boxHeight-2);
      g.drawLine(left,top + 23 + lineHeight, left + boxWidth - 2, top + 23 + lineHeight); 
      g.drawLine(left,top + 24 + lineHeight, left + boxWidth - 2, top + 24 + lineHeight);
      g.setFont(font);
      g.drawString("ERROR MESSAGE",width/2 - fm.stringWidth("(Error Message)")/2, top + 10 + lineHeight); 
      if (lineCt == 1)
         g.drawString(errorMessageList[0], width/2 - fm.stringWidth(errorMessageList[0])/2, top + 35 + 2*lineHeight);
      else {
         for (int i = 0; i < lineCt; i++) {
            g.drawString(errorMessageList[i], left + 10, top + 35 + (i+2)*lineHeight - leading);
         }
      }
   }  // end drawErrorMessage();
   

   // ------------ Handle Mouse Zooming -----------------------------------------
   
   private transient boolean dragging, draggingZoomWindow;
   private transient CRData draggingInRect;
   private transient int dragXmax, dragXmin, dragYmax, dragYmin;
   private transient int lastX, lastY, startX, startY;
   
   private CoordinateRect findCoords(MouseEvent evt) {
          // Find coord rect containing the mouse.
      int xMouse = evt.getX();
      int yMouse = evt.getY();
      int size = (coordinateRects == null)? -1 : coordinateRects.size();
      int width = getSize().width;
      int height = getSize().height;
      for (int i = size-1; i >= 0; i--) {
         CRData c = (CRData)coordinateRects.elementAt(i);
         double xmin = (int)(c.xmin*width);
         double ymin = (int)(c.ymin*height);
         double xmax = (int)(c.xmax*width) - 1;
         double ymax = (int)(c.ymax*height) - 1;
         if (xMouse >= xmin && xMouse <= xmax && yMouse >= ymin && yMouse <= ymax)
            return c.coords;
      }
      return null;
   }
   
   private synchronized void doMouseZoom_pressed(MouseEvent evt) {
         // Called from processMouseEvent, above.
         // Ignore multiple clicks and clicks with other than button 1
         // and clicks modified with any key except shift.
      if (evt.getClickCount() > 1 || evt.isAltDown() || evt.isMetaDown() || evt.isControlDown())
         return;
      int xMouse = evt.getX();
      int yMouse = evt.getY();
      int size = (coordinateRects == null)? -1 : coordinateRects.size();
      int width = getSize().width;
      int height = getSize().height;
      for (int i = size-1; i >= 0; i--) {
         CRData c = (CRData)coordinateRects.elementAt(i);
         dragXmin = (int)(c.xmin*width);
         dragYmin = (int)(c.ymin*height);
         dragXmax = (int)(c.xmax*width) - 1;
         dragYmax = (int)(c.ymax*height) - 1;
         if (xMouse >= dragXmin && xMouse <= dragXmax && yMouse >= dragYmin && yMouse <= dragYmax) {
            dragging = true;
            draggingZoomWindow = false;
            draggingInRect = c;
            startX = xMouse;
            startY = yMouse;
            lastX = xMouse;
            lastY = yMouse;
            break;
         } 
      }
   }
   
   private synchronized void doMouseZoom_released(MouseEvent evt) {
      Graphics g = getGraphics();
      putDragRect(g);
      g.dispose();
      CoordinateRect c = draggingInRect.coords;
      if ( (Math.abs(lastX - startX) < 4 && Math.abs(lastY - startY) < 4)
                || Math.abs(startX - lastX) < 2 || Math.abs(startY - lastY) < 2 ) {
         if (draggingZoomWindow)
             return;
         if (evt.isShiftDown())
             c.zoomOutFromPixel(startX,startY);
         else
             c.zoomInOnPixel(startX,startY);
      }
      else {
         c.setLimits( c.pixelToX(startX), c.pixelToX(lastX), c.pixelToY(startY), c.pixelToY(lastY) );
      }
      dragging = false;
   }
   
   private synchronized void doMouseZoom_moved(MouseEvent evt) {
      Graphics g = getGraphics();
      putDragRect(g);
      lastX = evt.getX();
      lastY = evt.getY();
      putDragRect(g);
      g.dispose();
   }
   
   private void putDragRect(Graphics g) {  // (Assume dragging = true)
      if (lastX < dragXmin)
         lastX = dragXmin;
      if (lastX > dragXmax)
         lastX = dragXmax;
      if (lastY < dragYmin)
         lastY = dragYmin;
      if (lastY > dragYmax)
         lastY = dragYmax;
      if ( (Math.abs(startX - lastX) < 4 && Math.abs(startY - lastY) < 4)
                || Math.abs(startX - lastX) < 2 || Math.abs(startY - lastY) < 2 )
         return;  
      draggingZoomWindow = true;
       Color bc = draggingInRect.background;
       if (bc == null)
          bc = getBackground();
       g.setXORMode(bc);
       if (bc.getRed() <= 100 && bc.getGreen() <= 100 && bc.getBlue() <= 150)
          g.setColor(Color.white);
       else
          g.setColor(Color.black);
       int x, y, w, h;
       if (startX < lastX) {
          x = startX;
          w = lastX - startX;
       }
       else {
          x = lastX;
          w = startX - lastX;
       }
       if (startY < lastY) {
          y = startY;
          h = lastY - startY;
       }
       else {
          y = lastY;
          h = startY - lastY;
       }
       g.drawRect(x,y,w,h);
       g.setPaintMode();
   }
   
   //--------- More implementation details... ----------------------------------
   
   /**
    * This has been overridden to return a default size of 350-by-350 pixels.
    * Not usually called directly.
    */
   public Dimension getPreferredSize() {
      return new Dimension(350,350);
   }

   private transient Image OSC;
   private transient Graphics OSG;
   private transient boolean OSCvalid;
   private transient int OSCwidth = -1, OSCheight = -1;
      
   private void drawCoordinateRects(Graphics g, int width, int height, Rectangle clip) {
      g.setColor(getBackground());
      g.fillRect(0,0,width,height);
      int count = (coordinateRects == null)? -1 : coordinateRects.size();
      try {
         for (int i = 0; i < count; i++) {
            CRData c = (CRData)coordinateRects.elementAt(i);
            Rectangle bounds = new Rectangle();
            bounds.x = (int)(c.xmin*width);
            bounds.y = (int)(c.ymin*height);
            bounds.width = (int)(c.xmax*width) - bounds.x;
            bounds.height =  (int)(c.ymax*height) - bounds.y;
            Rectangle clipThisRect = (clip == null)? bounds : bounds.intersection(clip);
            if (clip == null || !clipThisRect.isEmpty()) {
               g.setClip(clipThisRect);
               if (c.background != null) {
                  g.setColor(c.background);
                  g.fillRect(bounds.x,bounds.y,bounds.width,bounds.height);
               }
               c.coords.draw(g,bounds.x,bounds.y,bounds.width,bounds.height);
            }
         }
      }
      finally {
         g.setClip(clip);
      }
   }
   
   /**
    * This has been overridden to implemnt double-buffering.
    * Not meant to be called directly.
    */
   public void update(Graphics g) {
      paint(g);
   }

   /**
    * Draw the contents of the DisplayCanvas.
    * Not usually called directly.
    */
   synchronized public void paint(Graphics g) {
      if (errorMessage == null) {
         try {
            checkOSC();
            if (OSC != null)
               g.drawImage(OSC,0,0,this);
            else 
               drawCoordinateRects(g,getSize().width,getSize().height,g.getClipBounds());
            if (dragging)
               putDragRect(g);
         }
         catch (RuntimeException e) {
            errorMessage = "Internal Error? (stack trace on System.out):  " + e.toString();
            e.printStackTrace();
            g.setClip(0,0,getSize().width,getSize().height);
         }
      }
      if (errorMessage != null) {
         drawErrorMessage(g);
         OSCvalid = false;
         return;
      }
   }
   
   /**
    * Draws the specified item in the first CoordinateRect in this canvas.
    * It is drawn on screen and on the off-screen canvas, if there is one.
    * However, no information is kept about this item, so the drawing will
    * disappear the next time the off-screen canvas is re-drawn (if there
    * is an off-screen canvas) or the next time the canvas is repainted
    * (is there is no off-screen canvas).  If the canvas contains no
    * CoordinateRect when this is called, a new one is added.  Note that
    * this method should only be called after the canvas has appeared on 
    * the screen.
    */
   public void drawTemp(DrawTemp drawItem) {
      if ( coordinateRects == null || coordinateRects.size() == 0)
         addCoordinateRect(new CoordinateRect());
      drawTemp(drawItem,0);
   }
   
   /**
    * Draws the specified item in the specified CoordinateRect in this canvas.
    * It is drawn on screen and on the off-screen canvas, if there is one.
    * However, no information is kept about this item, so the drawing will
    * disappear the next time the off-screen canvas is re-drawn (if there
    * is an off-screen canvas) or the next time the canvas is repainted
    * (is there is no off-screen canvas).   Note that
    * this method should only be called after the canvas has appeared on 
    * the screen.
    *
    * @param drawItem The non-null object that is to be drawn
    * @param coordRectIndex The index of the CoordinateRect in which it 
    *        is to be drawn, where the index of the fist CoordinateRect
    *        added to the canvas is zero, and so on.  If there is
    *        no CoordinateRect with the specified index, an IllegalArgumentException
    *        is thrown.
    */
   synchronized public void drawTemp(DrawTemp drawItem, int coordRectIndex) {
      if (coordRectIndex < 0 || coordRectIndex >= coordinateRects.size())
         throw new IllegalArgumentException("Invalid CoordinateRect index, " + coordRectIndex);
      Graphics g = getGraphics();
      if (g == null)
         return;
      CRData c = (CRData)coordinateRects.elementAt(coordRectIndex);
      Rectangle bounds = new Rectangle();
      bounds.x = (int)(c.xmin*getSize().width);
      bounds.y = (int)(c.ymin*getSize().height);
      bounds.width = (int)(c.xmax*getSize().width) - bounds.x;
      bounds.height =  (int)(c.ymax*getSize().height) - bounds.y;
      g.setClip(bounds);
      drawItem.draw(g,c.coords);
      g.dispose();
      if (useOffscreenCanvas && OSCvalid && OSC != null) {
         g = OSC.getGraphics();
         g.setClip(bounds);
         drawItem.draw(g,c.coords);
         g.dispose();
      }
   }
   
   
   synchronized private void checkOSC() {  // make off-screen image, if necessary
      if (!useOffscreenCanvas || (OSCvalid == true && OSC != null && 
                                    OSCwidth == getSize().width && OSCheight == getSize().height))
         return;
      int width = getSize().width;
      int height = getSize().height;
      if (OSC == null || width != OSCwidth || height != OSCheight) {
         OSCvalid = false;
         OSCwidth = width;
         OSCheight = height;
         try {
            OSC = createImage(OSCwidth,OSCheight);
            OSG = OSC.getGraphics();
         }
         catch (OutOfMemoryError e) {
            OSC = null;
            OSG = null;
         }
      }
      if (OSC == null || OSCvalid)
         return;
      OSCvalid = true;
      OSG.setClip(0,0,width,height);
      drawCoordinateRects(OSG,width,height,null);
   }


} // end class DisplayCanvas

