package nxm.sys.libg;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferedImage;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Table;

/** A drawing surface for use by Plot / MPlot. <br>
   <b>NOTE: This class is still in the experimental phase and subject to change
    without notice based on user feedback and further enhancements.</b><br>
    This class creates a BufferedImage and draws directly to that, NOT the Plot
    graphics panel. When desired, Plot then draws this image on top of the other plot data.
    The point of drawing on the canvas is that a large number of objects can be drawn on it
    while it is in the background and then only drawn once as an image. This is to
    prevent some refresh issues seen when using Features caused by re-drawing data portions
    unnecessarily.
    @author jwb
    @since NeXtMidas 2.9.3
 */
public class PlotCanvas {

  /** List of options to use when drawing on the canvas image. */
  public static final String optionsList = "Vertical,Horizontal,YAsTime,Connect";
  /** List of options to use when drawing on the canvas image.
      @deprecated Since NeXtMidas 3.3.1: use {@link #optionsList}. this will be removed in 3.5+
   */
  @Deprecated public static final String OPTIONS = optionsList;
  /** Canvas drawing option: Draw a vertical line. */
  public static final int VERTICAL    = 0x1;
  /** Canvas drawing option: Draw a horizontal line. */
  public static final int HORIZONTAL  = 0x2;
  /** Canvas drawing option: Y values are time-based. */
  public static final int YASTIME     = 0x4;
  /** Canvas drawing option: Draw lines between lines/points. */
  public static final int CONNECT     = 0x8;

  private BufferedImage canvas;  // the image to use as the canvas

  private MPlot   MP;
  private Color   color;         // used when no other color given
  private Table   objs;          // The objects to draw on the canvas
  private int     objID;
  private int[]   bounds;  // pixel bounds {X1,X2,Y1,Y2}
  private int     pointSize;  // size in pixels of points drawn on the canvas
  private Symbol  pointSymbol;   // Symbol to use for Canvas points

  private boolean needUpdate; // whether or not to re-compute the canvas objects locations and re-draw on the Canvas image context
  private boolean isPlotCanvasThatNeedsLegacyPaint; // This is to provide the user to use their own custom GWidgets that still use the paint() method

  /** If this is true, then it tells the paint() that is is being called from the new paintComponent(Graphics g) code
   * Since 3.9.0 for backwards compatibility NXM-3213
   */
  private volatile boolean inPaintComponent = false;

  /** PlotCanvas constructor
   *  @param mp MPlot
   */
  public PlotCanvas (MPlot mp) {
    this.MP = mp;
    setup();
    init();
  }

  private void init () {
    objs        = new Table();
    color       = Color.white;
    objID       = 0;
    bounds      = new int[4];
    pointSize   = 5;
    pointSymbol = Symbol.getSymbol("CROSS", pointSize);
  }

  /** Create a new Canvas */
  private void setup () {
    if (MP==null || MP.panel==null) return;
    canvas      = new BufferedImage(MP.panel.getWidth(),MP.panel.getHeight(),BufferedImage.TYPE_INT_ARGB);
    needUpdate  = false;
    isPlotCanvasThatNeedsLegacyPaint = GraphicsUtil.checkIfNeedLegacyPaint(this);
  }

  /** Set the default color to use for the Canvas objects
   *  @param c default color */
  public void setColor (Color c) {
    if (c!=null) color = c;
  }

  /** Set the color of an Object on the canvas
   *  @param id id of Object
   *  @param c  color
   */
  public void setObjColor (String id, Color c) {
    if (id==null || c==null) return;
    Table tab = objs.getTable("ID");
    tab.remove("COLOR");
    tab.put("COLOR", c);
  }

  /** Set the color to use when drawing canvas objects  Note: all canvas objects will use this color.
   *  @param c color to use when drawing canvas objects */
  public void setColor (String c) {
    setColor(MColor.getColor(c));
  }

  /** Set the color of an Object on the canvas
   *  @param id id of Object
   *  @param c  color
   */
  public void setObjColor (String id, String c) {
    setObjColor(id, MColor.getColor(c));
  }

  /** get color
   *  @return color
   */
  public Color getColor () { return color; }

  /** get Color by id
   *  @param id id of Color
   *  @return Color
   */
  public Color getObjColor (String id) {
    return (Color) objs.getTable(id).get("COLOR");
  }

  /** Get the unique identifier of the Canvas object based on its position
      @param x1 X Min
      @param y1 Y or T Min
      @param x2 X Max
      @param y2 Y or T Max
      @return unique identifier of Canvas object
   */
  public String getObjectId (double x1, double y1, double x2, double y2) {
    String[] keys = objs.getKeys();
    Table tab  = null;
    Table data = null;
    for (String key : keys) {
      tab = objs.getTable(key);
      if (tab != null) {
        data = tab.getTable("DATA");
        if (data != null) {
          if (data.getD("X1")==x1 && data.getD("X2")==x2 && data.getD("Y1")==y1 && data.getD("Y2")==y2) {
            return tab.getS("ID");
          }
        }
      }
    }
    return null;  // not found
  }

  /** Add an object to be drawn on the canvas
      @param obj Table containing information about the object<pre>
        The given table is parsed for the following keys:
        ID:    The String ID of the object
        TYPE:  'Line', 'Point', '[Circ]le', or '[Ell]ipse'
        DATA:  A table containing the specific information for the Object to be added to the canvas
        COLOR: Color to use for the object. If not given, then any color set for the whole canvas is used.
      </pre>
   */
  public void addToCanvas (Table obj) {
    if (obj==null) return;

    if (canvas==null) setup();
    String type = obj.getS("TYPE", "LINE").toUpperCase();  // default to LINE type
    String id   = obj.getS("ID");
    Table data  = obj.getTable("DATA");
    Color color = MColor.getColor(obj.get("COLOR", this.color));

    addObject(id, type, color, data);
  }

  private void addObject (String id, String type, Color color, Table data) {
    if (type==null || data==null) return;
    if (color==null) color = this.color;

    if (type.equals("LINE")) {
      addLine(id,data.getD("X1"),data.getD("Y1"),data.getD("X2"),data.getD("Y2"),data.getState("ISTIME"),color);
    }
    else if (type.equals("POINT")) {
      addPoint(id,data.getD("X1"),data.getD("Y1"),data.getState("ISTIME"),color);
    }
    else if (type.startsWith("CIRC")) {
      addCircle(id,data.getD("X1"),data.getD("Y1"),data.getD("RADIUS"),data.getL("NPTS"),data.getState("ISTIME"),color);
    }
    else if (type.startsWith("ELL")) {
      addEllipse(id, data.getD("X1"),data.getD("Y1"),data.getD("ALT"),data.getD("SEMIMAJOR"),
          data.getD("SEMIMINOR"), data.getD("TILT"), data.getL("DIV"),data.getState("ISTIME"), color);
    }
  }

  /** Add a line on the canvas at the coords specified (from x1,y1 to x2,y2).
      @param id Unique identifier of this line. If null or empty String, a generic id will be used internally
      @param x1 X Min
      @param y1 Y or T Min
      @param x2 X Max
      @param y2 Y or T Max
      @param isTime Whether or not the Y coords are Times
      @param color Color to use for the object. If null, then any color set (or default) for the whole canvas is used.
                 This will look up the Color using {@link MColor#getColor(Object)}
   */
  public void addLine (String id, double x1, double y1, double x2, double y2, boolean isTime, Object color) {
    if (canvas==null) setup();
    Color col = MColor.getColor(color);
    if (col==null) col = this.color;

    double newY1 = y1;
    double newY2 = y2;

    if (isTime) {   // for time-based plots, look up the Y value
      Layer lay = ((Layer)MP.getLayers().get(1));
      newY1 = lay.getPosAtTime(y1);
      newY2 = lay.getPosAtTime(y2);
    }
    addLine(id, x1, newY1, x2, newY2, col);
    updateObject("LINE",id,new Table("{X1="+x1+",Y1="+y1+",X2="+x2+",Y2="+y2+",ISTIME="+isTime+"}"), col);
  }

  /** Add a line on the canvas at the coords specified (from x1,y1 to x2,y2)
      @param id Unique identifier of this line. If null or empty String, a generic id will be used internally
      @param x1 X Min
      @param y1 Y Min
      @param x2 X Max
      @param y2 Y Max
      @param color Color to use when drawing the Line
   */
  private void addLine (String id, double x1, double y1, double x2, double y2, Color color) {
    if (canvas==null) setup();
    Graphics gc = canvas.createGraphics();

    double[] buf = {x1,y1,x2,y2};
    int pixFlags = MP.isMapView() ? Pix.CLIP : Pix.CHECK;
    Pix pix = new Pix(4, pixFlags);
    MP.viewer.rwc2pix(buf,0,2,2,pix);

    int pixX1 = Math.min(pix.x[0],pix.x[1]);
    int pixX2 = Math.max(pix.x[1],pix.x[0]);
    int pixY1 = Math.min(pix.y[0],pix.y[1]);
    int pixY2 = Math.max(pix.y[0],pix.y[1]);

    updateBounds(pixX1,pixY1,pixX2,pixY2);

    if (MP.isMapView()) {
      Line line = MP.getBaseLayer().getLine().copy();
      line.setColor(color); // color is passed in through line - bug 2858
      MP.drawLine(buf,0,2,2,line,Pix.CLIP,gc,true); // has logic for clipping across map lines
    }
    else {
      gc.setColor(color);
      gc.drawLine(pixX1, pixY1, pixX2, pixY2);
      gc.setColor(this.color);  // set back to default
    }
  }

  /** Add a circle on the canvas at the coords specified with the radius specified
      @param id The unique identifier of this object
      @param x The RWC X position (or lon)
      @param y The RWC Y Position (or time or lat)
      @param radius The RWC radius
      @param div The number of divisions used to draw the circle (-1==default) (only used on maps)
      @param isTime whether or not the plot Y axis is time-based
      @param color The color to assign the circle
   */
  public void addCircle (String id, double x, double y, double radius, int div, boolean isTime, Object color) {
    if (canvas==null) setup();
    Color col = MColor.getColor(color);
    if (col==null) col = this.color;
    if (div == -1) div = 90;  // unofficial plot default

    // update Y to a time if on a time based plot
    double newY = y;
    if (isTime) {   // for time-based plots, look up the Y value
      Layer lay = ((Layer)MP.getLayers().get(1));
      newY = lay.getPosAtTime(y);
    }
    addCircle(x,newY,radius,div,col);
    updateObject("CIRC",id,new Table("{X1="+x+",Y1="+y+",X2="+x+",Y2="+y+",ISTIME="+isTime+",RADIUS="+radius+",NPTS="+div+"}"), col);
  }

  /** Add a circle on the canvas at the coords specified with the radius specified
      @param x The RWC X position (or lon)
      @param y The RWC Y Position (or time or lat)
      @param radius The RWC radius
      @param div The number of points used to draw the circle (-1==default) (only used on maps)
      @param color The color to assign to the circle
   */
  private void addCircle (double x, double y, double radius, int div, Color color) {
    if (canvas==null) setup();
    Graphics gc = canvas.createGraphics();
    if (color==null) color = this.color;

    if (MP.isMapView()) {
      Line line = MP.getBaseLayer().getLine().copy();
      line.setColor(color); // color is passed in through line - bug 2858
      MP.drawCircleLonLat(x, y, radius, div, line, gc);

      double xMax = x+radius;
      double xMin = x-radius;
      double yMax = y-radius;
      double yMin = y+radius;
      double buf[] = {xMin,yMin,xMax,yMax};
      Pix pix = new Pix(4, Pix.CLIP);
      MP.viewer.rwc2pix(buf,0,2,2,pix);
      updateBounds(pix.x[0],pix.y[0],pix.x[1],pix.y[1]);
    } else {
      gc.setColor(color);
      double[] buf = {x,y,x,y+radius};
      Pix pix = new Pix(4, Pix.CHECK);
      MP.viewer.rwc2pix(buf,0,2,2,pix);
      int pixRadius = Math.abs(pix.y[1]-pix.y[0]);
      gc.drawOval(pix.x[0]-pixRadius, pix.y[0]-pixRadius, pixRadius*2, pixRadius*2);  // start point is top-left
      updateBounds(pix.x[0]+pixRadius,pix.y[0]+pixRadius,pix.x[1]-pixRadius,pix.y[1]-pixRadius);
      gc.setColor(this.color);  // set back to default
    }
  }

  /** Draw an ellipse. If the plot is a map, then this will basically use
      {@link MPlot#drawEllipse(double, double, double, double, double, double, int, Line, Graphics)},
      otherwise it will use the simple Java graphics utility to draw an ellipse.
      @param id The unique identifier for the object
      @param x The RWC X (or LAT)
      @param y The RWC Y (or LON or TIME)
      @param alt   Altitude if a map
      @param semiMajor Semi-major axis (meters for map)
      @param semiMinor Semi-minor axis (meters for map)
      @param tilt Tilt of major axis in degrees (0=North).
      @param div Number of divisions (line segments) to make an ellipse (-1=UsePlotDefault).
                       Should be a multiple of four. (Map only)
      @param isTime is the base plot layer Y coord time-based?
      @param color The color to assign the circle
   */
  public void addEllipse (String id, double x, double y, double alt, double semiMajor,
      double semiMinor, double tilt, int div, boolean isTime, Object color) {
    if (canvas==null) setup();
    Color col = MColor.getColor(color);
    if (col==null) col = this.color;

    double newY = y;
    if (isTime) {   // for time-based plots, look up the Y value
      Layer lay = ((Layer)MP.getLayers().get(1));
      newY = lay.getPosAtTime(y);
    }
    addEllipse(x,newY,alt,semiMajor,semiMinor,tilt,div,col);
    updateObject("ELLIPSE",id,new Table("{X1="+x+",Y1="+y+",X2="+x+",Y2="+y+",ALT="+alt+",TILT="+
        tilt+",ISTIME="+isTime+",DIV="+div+",SEMIMAJOR="+semiMajor+",SEMIMINOR="+semiMinor+"}"), col);
  }

  private void addEllipse (double x, double y, double alt, double semiMajor,
      double semiMinor, double tilt, int div, Color color) {

    if (canvas==null) setup();
    Graphics2D gc = canvas.createGraphics();
    if (color==null) color = this.color;
    gc.setColor(color);

    if (MP.isMapView()) { // map is easier because it uses existing MPlot code and X and Y are same units (deg)
      Line line = MP.getBaseLayer().getLine().copy();
      line.setColor(color); // color is passed in through line - bug 2858
      line.updateGraphics(gc);
      MP.drawEllipseOnMap(x, y, alt, semiMajor, semiMinor, tilt, div, line, gc);
      // update the bounds of the canvas draw area
      double bigAxis = Math.max(semiMajor, semiMinor);
      double semiMajorInDeg = bigAxis/111000;  // roughly 111,000 meters per degree for Lat
      double[] buf = {x-semiMajorInDeg,y-semiMajorInDeg,x+semiMajorInDeg,y+semiMajorInDeg}; // max possible size
      Pix pix = new Pix(4, Pix.CLIP);
      MP.viewer.rwc2pix(buf,0,2,2,pix);
      updateBounds(pix.x[0],pix.y[0],pix.x[1],pix.y[1]);
    } else {
      tilt = 360 - (90-tilt);  // alter to have 0 at top and rotate clockwise
      double buf[] = {x-semiMajor,y-semiMinor,x+semiMajor,y+semiMinor,x,y};
      Pix pix = new Pix(4, Pix.CHECK);
      MP.viewer.rwc2pix(buf,0,2,3,pix);
      int xTopPix = pix.x[0];
      int yTopPix = pix.y[0];
      int width   = pix.x[1] - pix.x[0];
      int height  = pix.y[1] - pix.y[0];
      int centerX = pix.x[2];
      int centerY = pix.y[2];
      Shape ell = new Ellipse2D.Double(xTopPix,yTopPix,width,height);
      gc.rotate(Math.toRadians(tilt), centerX, centerY);
      gc.draw(ell);
      // update bounds bookkeeping
      int maxRadius = Math.max(width, height)/2;
      updateBounds(centerX-maxRadius,centerY-maxRadius,centerX+maxRadius,centerY+maxRadius);
    }

    gc.setColor(this.color);  // set back to default
  }

  /** Draw a point on the Canvas.
      @param id Unique identifier for the point. If null or empty String, a generic id of "POINT_N" will be used.
      @param x The RWC X
      @param y The RWC Y or T
      @param isTime Denotes whether the Y value is a Time
      @param color Either a String with a color name or a Color object. In the case of a String, the color
                 is looked up via {@link MColor#getColor(Object)}
   */
  public void addPoint (String id, double x, double y, boolean isTime, Object color) {
    if (canvas==null) setup();
    Color col = MColor.getColor(color);
    if (col==null) col = this.color;  // default

    Layer lay = ((Layer)MP.getLayers().get(1));
    double newY = y;

    if (isTime) {
      newY = lay.getPosAtTime(y);
    }
    addPoint(id,x,newY,col);
    updateObject("POINT",id,new Table("{X1="+x+",Y1="+y+",X2="+x+",Y2="+y+",IST="+isTime+"}"), col);
  }

  private void addPoint (String id, double x, double y, Color color) {
    if (canvas==null) setup();
    Graphics gc = canvas.createGraphics();

    double[] buf = {x,y};
    int pixFlags = MP.isMapView() ? Pix.CLIP : Pix.CHECK;
    Pix pix = new Pix(2, pixFlags);
    MP.viewer.rwc2pix(buf,0,2,1,pix);

    int pixX = pix.x[0];
    int pixY = pix.y[0];
    updateBounds(pixX,pixY,pixX,pixY);
    gc.setColor(color);
    getPointSymbol().draw(pixX, pixY, gc);
    gc.setColor(this.color);  // set back to default
  }

  /** Update the bounds used to determine the clip area of the graphics context
      Note: both x and y values will be checked against each other to determine max/min
   */
  private void updateBounds (int x1, int y1, int x2, int y2) {
    if (x1 > x2) {
      int tmpX = x1;
      x1 = x2;
      x2 = tmpX;
    }
    if (y1 > y2) {
      int tmpY = y1;
      y1 = y2;
      y2 = tmpY;
    }
    // update x bounds
    if      (x1 < bounds[0]) bounds[0] = x1;  // min
    else if (x2 > bounds[1]) bounds[1] = x2;  // max
    // update y bounds
    if      (y1 < bounds[2]) bounds[2] = y1;  // min
    else if (y2 > bounds[3]) bounds[3] = y2;  // max
  }

  /** Paint the canvas on top of the current plot
   *  @deprecated Since NeXtMidas 3.9.0 to avoid graphical timing issues
   *  use {@link #paintComponent(Graphics)} (for NXM-3213)
   */
  @Deprecated
  public synchronized void paint() {
   if (!inPaintComponent) MP.refresh();
  }

  /** Paint the canvas on top of the current plot
   *  @param g the Graphics object used for painting
   *  @since NeXtMidas 3.9.0
   */
  @InternalUseOnly
  public synchronized void paintComponent (Graphics g) {
    if(isPlotCanvasThatNeedsLegacyPaint) {
      inPaintComponent=true;
      paint();
      inPaintComponent=false;
    }
    if (MP.panel==null || canvas==null || ((MP.holdRefresh&MPlot.HOLD_CANVAS_REFRESHES)!=0)) return;
    if (needUpdate) update();
    // keep drawing in bounds of data and as small as possible (for network traffic sakes)
    int x1 = Math.max(bounds[0],MP.ix1);
    int y1 = Math.max(bounds[2],MP.iy1);
    int x2 = Math.min(bounds[1]-bounds[0],MP.ix21);
    int y2 = Math.min(bounds[3]-bounds[2],MP.iy21);
    g.setClip(x1,y1,x2,y2);
    g.drawImage(canvas,0,0,MP);
  }

  /** enable need update */
  public void needUpdate () {
    needUpdate = true;
  }

  /** Update the drawn canvas objects so that they are drawn in the correct spot (such as after a resize or zoom) */
  public void update () {
    undraw();  // remove existing artifacts
    String[] keys = objs.getKeys();
    String type;
    Table tab;
    Table data = null;
    for (String key : keys) {
      tab = objs.getTable(key);
      if (tab==null || tab.toString().equals("{}")) continue;
      type = tab.getS("TYPE","LINE").toUpperCase();
      data = tab.getTable("DATA");
      Color color = MColor.getColor(tab.get("COLOR", this.color));
      addObject(key, type, color, data);
    }

    needUpdate = false;
  }

  /** This will null out the canvas image, removing any canvas drawings. <br>
      Note 1: This does not remove the defined Canvas Objects, which will be drawn
      when the canvas is no longer null. <br>
      Note 2: This does not call a refresh/repaint.
  */
  public void clear () {
    canvas = null;
  }

  /** Remove existing items from the canvas  */
  public void undraw () {
    if (canvas==null) return;  // nothing to do
    clear();
    Graphics gc = MP.panel.getGraphics();
    gc.setClip(bounds[0], bounds[2], bounds[1]-bounds[0], bounds[3]-bounds[2]);
    MP.panel.repaint();  // this is smoother and faster than setting needRefresh
    for (int i=0; i<4; i++) bounds[i] = 0;
  }

  /** Draw multiple objects (points or lines) to the Canvas.
      @param buf Array of points
      @param dim
      <pre>Dimension of how to read the buffer.
    dim == 1   =&gt; draw points, e.g. {pt1_x,pt1_y,pt2_x,pt2_y,...}
    dim == 2   =&gt; read the buffer as either horizontal or vertical lines across the plot, e.g.  {line1_y,line2_y,...}
                  Horizontal or Vertical is determined by the flag mask (see {@link #optionsList}) [def=horizontal if dim=2]
    dim == 4   =&gt; draw lines from x1,y1 to x2,y2, e.g. {pt1_x1,pt1_y1,pt1_x2,pt1_y2,pt2_x1,pt2_y1,pt2_x2,pt2_y2,...}
      </pre>
      @param offset   Starting offset into the buffer array
      @param npts     How many points/lines to pull from the buffer. Use -1 for "all".
      @param idPrefix Unique identifier (prefix) for each canvas object drawn. If null of empty String (""), a generic
                      id will be assigned based on the dim type. (e.g "LINE_1,LINE_2,...) for dim of 2 or 4.
      @param options  mask of options to use when drawing. See {@link #optionsList}
      @param color    Color to use when drawing the Object(s). Color determined using {@link MColor#getColor(Object)}
   */
  public void draw (double[] buf, int dim, int offset, int npts, String idPrefix, int options, Object color) {
    Color col = MColor.getColor(color);
    if (col==null) col = this.color;
    boolean isTime = (options&YASTIME) != 0;
    int pts = (npts == -1)? buf.length - offset : npts;   // -1 means go tot the end of the buffer

    if (dim==1) {
      for (int i = offset; i < (pts*2); i+=2) {
        addPoint(idPrefix+"_"+objID++, buf[i], buf[i+1], isTime, col);
      }
    }
    else if (dim==2 || dim==4) {  // line of some sort
      if (dim == 2) {  // strict horizontal or vertical line
        boolean vert = (options&VERTICAL) != 0;  // vertical line
        if (vert) {
          for (int i = offset; i < pts; i++) {
            addLine(idPrefix+"_"+objID++, buf[i], MP.ry1, buf[i], MP.ry2, isTime, col);
          }
        } else {   // horizontal line
          for (int i = offset; i < pts; i++) {
            addLine(idPrefix+"_"+objID++, MP.rx1, buf[i], MP.rx2, buf[i], isTime, col);
          }
        }
      }
      else {
        for (int i = offset; i < (pts*4); i+=4) {
          addLine(idPrefix+"_"+objID++, buf[i], buf[i+1], buf[i+2], buf[i+3], isTime, col);
          if ((CONNECT&options) !=0) addLine(idPrefix+"_"+objID++, buf[i+2], buf[i+3],buf[i+4], buf[i+5], isTime, col);  // connecting line to next point
        }
      }
    }
  }

  /** Move a current Canvas object to a new location on the plot. Uses the ID of the Canvas object.
      The objects (points, lines, ovals) are generally referenced by X1,Y1 - X2,Y2. In the case of points and ovals, the
      two sets of numbers are the same.
      @param id  The id of the object
      @param newX1 The new X1
      @param newY1 The new Y1
      @param newX2 The new X2
      @param newY2 The new Y2
      @param doRepaint Whether or not to repaint the canvas
      @return true if the id was found and moved, else false
   */
  public boolean moveObject (String id, double newX1, double newY1, double newX2, double newY2, boolean doRepaint) {
    if (objs == null || id == null) return false;
    Table tab = objs.getTable(id);
    if (tab != null) {
      Table data = tab.getTable("DATA");
      if (data != null) {
        data.remove("X1");
        data.remove("X2");
        data.remove("Y1");
        data.remove("Y2");
        data.put("X1", newX1);
        data.put("X2", newX2);
        data.put("Y1", newY1);
        data.put("Y2", newY2);
        needUpdate = true;
        //if (doRepaint) paint();
        if (doRepaint) MP.refresh();  // call refresh rather than paint directly since 3.7.2 for NXM-2841
        return true;
      }
    }
    return false;
  }

  /** Move a Canvas object to a new location. This method looks up the id of the
      object that matches "old" coords given, if there is one,
      and then calls {@link #moveObject(String, double, double, double, double, boolean)}.
      @param oldX1 Current RWC X1
      @param oldY1 Current RWC Y1
      @param oldX2 Current RWC X2
      @param oldY2 Current RWC Y2
      @param newX1 New RWC X1
      @param newY1 New RWC Y1
      @param newX2 New RWC X2
      @param newY2 New RWC Y2
      @param doRepaint Whether or not to repaint the canvas
      @return true if the object was found, else false
   */
  public boolean moveObject (double oldX1, double oldY1, double oldX2, double oldY2,
    double newX1, double newY1, double newX2, double newY2, boolean doRepaint) {
    String[] keys = objs.getKeys();
    Table tab  = null;
    Table data = null;
    for (String key : keys) {
      tab = objs.getTable(key);
      if (tab != null) {
        data = tab.getTable("DATA");
        if (data != null) {
          if (data.getD("X1")==oldX1 && data.getD("X2")==oldX2 && data.getD("Y1")==oldY1 && data.getD("Y2")==oldY2) {
            return moveObject(tab.getS("ID"),newX1,newY1,newX2,newY2,doRepaint);
          }
        }
      }
    }
    return false; // not found
  }

  /** Remove an Object from the Canvas
      @param id The id of the Object to remove
   */
  public void removeObject (String id) {
    if (objs != null) {
      if (objs.containsKey(id))  objs.remove(id);
      else Shell.warning("Could not remove Canvas object. Id ["+id+"] does not exist.");
      needUpdate = true;
    }
  }

  /** Remove a point from the Canvas at a given location, if one exists
      @param x The RWC X Coord
      @param y The RWC Y coord
      @return true if a Point was removed, else false
   */
  public boolean removePoint (double x, double y) {
    return removeObj("POINT",x,y,x,y);
  }

  /** Remove a line from the Canvas at a given location, if one exists
      @param x1 The RWC X1
      @param y1 The RWC Y1
      @param x2 The RWC X2
      @param y2 The RWC Y2
      @return true if a Line was removed, else false
   */
  public boolean removeLine (double x1, double y1, double x2, double y2) {
    return removeObj("LINE",x1,y1,x2,y2);
  }

  /** remove a circle at the given RWC
   *  @param x x coordinate
   *  @param y y coordinate
   *  @return true if circle removed
   */
  public boolean removeCircle (double x, double y) {
    return removeObj("CIRC",x,y,x,y);
  }

  /** remove an ellipse at the given RWC
   *  @param x x coordinate
   *  @param y y coordinate
   *  @return true if ellipse removed
   */
  public boolean removeEllipse (double x, double y) {
    return removeObj("ELLIPSE",x,y,x,y);
  }

  /** Remove an Object from the Canvas at a given location, if one exists
      @param x1 The RWC X1
      @param y1 The RWC Y1
      @param x2 The RWC X2
      @param y2 The RWC Y2
      @return True if an Obj was removed, else false
   */
  private boolean removeObj (String type, double x1, double y1, double x2, double y2) {
    boolean removed = false;
    if (objs != null) {
      String[] keys = objs.getKeys();
      for (String key : keys) {
        Table t = objs.getTable(key);
        if (t != null) {
          if (!t.getS("TYPE").equals(type)) continue;
          Table d = t.getTable("DATA");
          if (d != null) {
            if (x1==d.getD("X1") && x2==d.getD("X2") && y1==d.getD("Y1") && y2==d.getD("Y2")) {
              removeObject(key);
              removed = true;
            }
          }
        }
      }
    }
    if (!removed) Shell.warning("Could not remove Canvas object. Object with coords "+x1+","+y1+" "+x2+","+y2+" does not exist.");
    else {
      needUpdate=true;
    }
    return removed;
  }

  /** Keep track of canvas objects */
  private void updateObject (String type, String id, Table data, Color color) {
    if (objs==null) objs = new Table();
    if (type==null || type.equals("")) type = "LINE";   // default
    type = type.toUpperCase();
    if (id==null || id.equals("")) id = type+"_"+ (++objID);
    id = id.toUpperCase();
    Table tab = new Table();
    tab.put("TYPE", type);
    tab.put("ID", id);
    tab.put("DATA", data);
    tab.put("COLOR", color);

    objs.remove(id); // remove any existing object with same id
    objs.put(id, tab);
  }

  /** Get the table that details the items currently being drawn to the Canvas.
      @return Table {ID=&lt;String&gt;,TYPE=&lt;String&gt;,COLOR=&lt;color&gt;,DATA={X1=&lt;d&gt;,Y1=&lt;d&gt;,X2=&lt;d&gt;,Y2=&lt;d&gt;,IST=&lt;bool&gt;}}
   */
  public Table getObjects () {
    return objs;
  }

  /** Set the size (in pixels) of a point drawn on the canvas
   *  @param pix size in pixels */
  public void setPointSize (int pix) {
    if (pix>-1) pointSize = pix;
  }

  /** Get the size (in pixels) of a point drawn on the canvas
   *  @return size (in pixels)
   */
  public int getPointSize () {
    return pointSize;
  }

  /** Set the Symbol used when drawing points on the Canvas
      @param sym Must be in {@link Symbol#symbolList}
   */
  public void setPointSymbol (String sym) {
    if (Symbol.symbolList.toLowerCase().indexOf(sym.toLowerCase())>=0) {
      setPointSymbol(Symbol.get(sym, getPointSize()));
    }
    else Shell.warning("Unknown symbol type: ["+sym+"]. Symbol must be in "+Symbol.symbolList);
  }

  /** Set the Symbol used when drawing points on the Canvas
   *  @param sym Symbol used when drawing points on the Canvas */
  public void setPointSymbol (Symbol sym) {
    pointSymbol = sym;
  }

  /** get point symbol
   *  @return point Symbol */
  public Symbol getPointSymbol () {
    return pointSymbol;
  }

}
