package nxm.sys.libg;

import java.awt.Graphics;
import java.util.Arrays;
import java.util.Comparator;

import nxm.sys.inc.Chainable;
import nxm.sys.inc.Constants;
import nxm.sys.inc.Drawable;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MessageHandler;
import nxm.sys.inc.Tablizable;
import nxm.sys.lib.Convert;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.KeyObject;
import nxm.sys.lib.Message;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Position;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.Util;
import nxm.sys.libg.Symbol.IconSymbol;

/** A class to hold and plot a feature. Note, a feature can be of multiple types because
    of the type option masking. Hence a feature can be a box, circle, ellipse, and so on.
    All the positions are in real world coordinates (RWC) in terms of the plotter. <br>
    <br>
    There are four modes for determining the position of the feature:
    <pre>
      MODE 1: anchor=DATA and dt &lt; 0 (default)
        Feature is plotted based on center point (x,y,z) and
        extents (dx,dy,dz). The point (x,y,z) is based on the
        relative coordinates of the plot (for static plots and
        the held-value in real time plots the relative and the
        absolute values are often the same -- this is not true
        for real-time raster plots after the first pass).

        After the feature has been plotted it will remain in
        the same position even after the plot has "wrapped"
        (in the case of dynamic plots).

      MODE 2: anchor=DATA and dt &gt;= 0
        Feature is plotted based on center point (x,y',z) and
        extents (dx,dt,dz). Where y' is derived based on absolute
        time position t. (Note that dy and dt are both relative
        time positions and thus dy = dt.)

        A feature will only be plotted within when within the
        time range of the plot. After the plot wraps the
        feature will be plotted over.

      MODE 3: anchor=SCREEN
        Feature coordinates are given in percentage of the plot
        display as currently shown, measured from the top left
        corner of the screen.

      MODE 4: anchor=PIXEL
        Feature coordinates are given in pixels measured from
        the top left corner of the screen.
    </pre>
    The last two modes (SCREEN and PIXEL) are intended for use with annotations within the plot
    window (e.g. messages displayed to the user) that do directly associate with the data shown
    in the plot.

    @author Jeff Schoen / Neon Ngo / J. Czechowski IV
*/
// Tablizable since NeXtMidas 3.5.4 though the toTable() method existed long before.
public class Feature extends Object implements MessageHandler,Chainable,Drawable,Tablizable {
  private static final boolean FAST_DT2DY = true; // Fast dy=dt conversion (true in NXM 2.4.1)

  /** List of type of options supported.
      @since NeXtMidas 3.5.4
   */
  public static final String typeList = "Symbol,Data,HLine,VLine,Box,Circle,Oval,Elps,"
                                      + "Ltag,Rtag,Ttag,Btag,Dtag,Peak,IPlot,VRide,"
                                      + "OSymbol,Text,Inverse,Tri,Filled,XLine,YLine,"
                                      + "HBar,VBar,DLine,CustomTag";

  /** Type Option: Draw a fixed-size symbol at (x,y). */
  public static final int SYMBOL  = 0x000001;
  /** Type Option: Highlight enclosed data.           */
  public static final int DATA    = 0x000002;
  /** Type Option: Draws two horizontal lines (one at y=Y-DY/2 and one at y=Y+DY/2).
      (Note: If DY is zero, only one line will be drawn.)
   */
  public static final int HLINE   = 0x000004;
  /** Type Option: Draws two vertical lines (one at x=X-DX/2 and one at x=X+DX/2).
      (Note: If DX is zero, only one line will be drawn.)
   */
  public static final int VLINE   = 0x000008;
  /** Type Option: Draw a box centered at (x,y).      */
  public static final int BOX     = 0x000010;
  /** Type Option: Draw a circle centered at (x,y).   */
  public static final int CIRC    = 0x000020;
  /** Type Option: Draw an oval centered at (x,y).    */
  public static final int OVAL    = 0x000040;
  /** Type Option: Draw a geodetic ellipse.           */
  public static final int ELPS    = 0x000080;
  /** Type Option: Show text/tag along left axis.    */
  public static final int LTAG    = 0x000100;
  /** Type Option: Show text/tag along right axis.   */
  public static final int RTAG    = 0x000200;
  /** Type Option: Show text/tag along top axis.     */
  public static final int TTAG    = 0x000400;
  /** Type Option: Show text/tag along bottom axis.  */
  public static final int BTAG    = 0x000800;
  /** Type Option: Show text/tag dynamically centered at visible part of feature. Defined and implemented since NeXtMidas 3.7.0 */
  public static final int DTAG    = 0x001000;
  /** Type Option: NOT USED.                         */
  public static final int PEAK    = 0x002000;
  /** Type Option: Show text/tag inside axis.        */
  public static final int IPLT    = 0x004000;
  /** Type Option: Vertical position follows data.   */
  public static final int VRIDE   = 0x008000;
  /** Type Option: Fixed size circle. (Overlay)      */
  public static final int OSYMBOL = 0x010000;
  /** Type Option: Show text.                        */
  public static final int TEXT    = 0x020000;
  /** Type Option: Use inverse coloring.             */
  public static final int INVERSE = 0x040000;
  /** Type Option: Fixed size triangle (always filled) (DEPRECATED). */
  public static final int TRI     = 0x080000;
  /** Type Option: Fill box/circle/oval feature.     */
  public static final int FILLED  = 0x100000;
  /** Type Option: Feature that draws a two lines for a given X.
      Draws two lines with respect to the Plot's view (one at plot x=X-DX/2
      and one at plot x=X+DX/2). In the normal Y vs X plot these lines will be
      vertical, and in the rotated X vs Y Plot these lines will be horizontal.
      (Note: If DX is zero, only one line will be drawn.)
      @since 2.9.0
   */
  public static final int XLINE   = 0x200000;
  /** Type Option: Feature that draws two lines for a given Y.
      Draws two lines with respect to the Plot's view (one at plot y=Y-DY/2
      and one at plot y=Y+DY/2). In the normal Y vs X plot these lines will be
      horizontal, and in the rotated X vs Y plot these lines will be vertical.
      (Note: If DY is zero, only one line will be drawn.)
      @since 2.9.0
   */
  public static final int YLINE   = 0x400000;
  /** Type Option: Draws horizontal bar (filled box using FILLCOLOR) at
      Y-DY/2 and Y+DY/2 along the vertical axis.
      E.g. this fills in the area between the HLINE type.
      @since 3.3.1.
   */
  public static final int HBAR    = 0x800000;
  /** Type Option: Draws vertical bar (filled box using FILLCOLOR) at
      X-DX/2 and X+DX/2 along the horizontal axis.
      E.g. this fills in the area between the VLINE type.
      @since 3.3.1.
   */
  public static final int VBAR  = 0x01000000;
  /** Type Option: Draws a diagonal line
      @since 3.9.4
   */
  @InternalUseOnly
  public static final int DLINE = 0x02000000;
  /** Type Option: Show text/tag at custom location referenced to
      one of the corners (TL,TR, BL, BR) or the center and then
      offset by the x,y pixel offset
      @since 3.9.4
   */
  @InternalUseOnly
  public static final int CUSTOMTAG = 0x04000000;

  /** List of anchors supported. */
  public static final String anchorList = "Data,Screen,Pixel";

  /** Anchor Option: Data Coordinates */
  public static final int A_DATA    = 1;
  /** Anchor Option: Fraction of screen from top left */
  public static final int A_SCREEN  = 2;
  /** Anchor Option: Pixels from top left */
  public static final int A_PIXEL   = 3;

  /* The allowable gap/variation in dt during feature validation. */
  final double DT_GAP = 0.100;

  /** Mask for type SYMBOL|TEXT */
  static final int SYMBOL_AND_TEXT = SYMBOL^TEXT;
  /** Mask for all tags */
  static final int ALL_TAGS = RTAG^LTAG^TTAG^BTAG^DTAG^CUSTOMTAG;
  /** Reference locations available for custom tag TL=top left, BR=bottom right, CTR=center, etc. */
  public static enum CustomTagReference {
      /** custom tag TR=top-right */
      TR,
      /** custom tag TL=top-left */
      TL,
      /** custom tag BL=bottom-left */
      BL,
      /** custom tag BR=bottom-right */
      BR,
      /** custom tag CTR=center */
      CTR
    };
  /** Reference location of custom tag */
  private CustomTagReference customTagReference = CustomTagReference.TR;
  /** Custom tag X pixel offset */
  private int customTagPixelX = 0;
  /** Custom tag Y pixel offset */
  private int customTagPixelY = 0;

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // these fields should be protected
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Feature name (both for drawing on display and for referencing in MPlot's list of features). */
  protected String name;
  /** Enable flags. */
  protected  int enable = 0xFB; // 0XFB = |Global|Labels|Thin|Base|Zoom|FastBase|FastZoom|;
  /** For internal usage by MPlot only; true if this feature needs to be refreshed.
      As of NextMidas 2.7.1, set to false since MPlot.addFeature() sets it to true.
   */
  protected boolean needRefresh=false;
  /** Line that hold attributes like color. */
  protected Line line;
  /** Plot to which this feature belongs. */
  protected MPlot MP;

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // these fields should be private
  //////////////////////////////////////////////////////////////////////////////////////////////////
  private       Layer  layer;                    // Layer to which this feature belongs.
  private       int    type      = SYMBOL;       // Mask holding all the types of this feature
  private       int    anchor    = A_DATA;       // Anchor option for this feature
  private       String text;                     // The text to go along with feature
  private       double semiMajor = 10000;        // Ellipse Semi-major axis in meters
  private       double semiMinor =  1000;        // Ellipse Semi-minor axis in meters
  private       double tilt      =    30;        // Ellipse tilt in degrees (0=North)
  private final MPoint pix       = new MPoint(); // Feature pixel position
  private       double timeAtTop;                // Time at top of layer
  private       Object metadata;                 // Metadata object - will call toString() on this when the query command is called. since 3.9.3
  private double x, y, z, t, dx, dy, dz, dt=-1;     // x, y, z are the Real World Coordinates (RWC) of the center point
  private static boolean warningFeatureNotDrawn = false; // Enable warning for Feature present without Data
  private boolean needWarnIfFeatureNotDrawn=warningFeatureNotDrawn; // Only show Feature not supported maximum of once warning once
  private String group; // FeatureGroup to which this Feature belongs - since 3.9.3 for NXM-3471
  private boolean visible = false; // NXM-3512 is this feature visible

  /** visible DIV full area ratio at which Feature's text is visible - avoid overcrowding at low zoom levels */
  @InternalUseOnly
  private double textZoomRatio = 1;

  /** visible DIV full area ratio at which Feature is visible - allows finer Features to show when zoomed */
  @InternalUseOnly
  private double featureZoomRatio = 1;

  int ix1Last, ix2Last, iy1Last, iy2Last;

  /** Creates a new feature. */
  public Feature() {
    line = new Line(this);
    line.setSymbol("CROSS"); // default symbol type
  }

  /** Creates a new feature on the given layer.  <br>
      <b>Intended for internal use in Layer and its subclasses only.</b> This will
      create a Feature applicable to its given Layer. The constructor is public to
      facilitate use by user-defined Layers, but should not be used outside one of
      the Layer subclasses.<br>
      <br>
      Note that this constructor sets the Feature type to {@link #DATA} by default.
      This is backwards-compatible to support older versions of NeXtMidas, but most
      Layers will not want or need this set. This can be "fixed" by calling
      {@link #setType(int)} with a value of 0 right after calling the constructor
      or by using the default {@link Layer#getFeature()} implementation.
      @param layer The Layer invoking this constructor
   */
  public Feature (Layer layer) {
    this();
    type = DATA;
    this.layer = layer;
    this.MP = layer.MP;
  }

  /** Creates a new feature based on a table of keys.
   *  @param t The table of keys.
   */
  public Feature (Table t) {
    this();
    fromTable(t);
  }

  @Override
  public boolean getNeedRefresh () {
    return needRefresh || line.getNeedRefresh();
  }

  @Override
  public void clearNeedRefresh () {
    needRefresh = false;
    line.clearNeedRefresh();
  }

  /** @since NeXtMidas 3.5.4 */
  @Override
  public void draw (int mode) {
    if (MP.gc==null) return;
    if (featureZoomRatio != 1) { // NXM-3648
      double ratio = (MP.rx2 - MP.rx1) / (MP.orx2 - MP.orx1);
      if (ratio > featureZoomRatio) {
        return;
      }
    }

    int ix,iy,ix1,iy1,ix2,iy2;
    clearNeedRefresh();
    if (MP==null) return;

    // ORIG before 2.6.0: Layer lay = (layer!=null)? layer : (Layer)MP.getLayers().get(1);
    Layer lay = getLayer();
    Line  lin = line.copy();

    /* Added for BUG 2166: Allow plotting of time-based Features on Type 1000 files */
    boolean lay1d = (lay != null && lay.pf instanceof DataFile && lay.typeClass==1);
    boolean lay2d = (lay != null && lay.pf instanceof DataFile && lay.typeClass==2); // TODO: not used

    // resolve the tag name
    String tag = "";
    if (text!=null && text.length() != 0) tag=text;
    else if (name!=null) tag=name;

    // Get the y position from vertical ride
    if ((type&VRIDE)!=0 && lay!=null && lay.ndata>0) {
      try { y = lay.getNearestYValue(x); }
      // generated by x values within plot BUT outside of data set
      catch (ArrayIndexOutOfBoundsException e) { } // ignore
      catch (ArithmeticException e) { }            // ignore
    }

    // Get the y position using time line if dt >= 0
    if ((lay!=null) && (dt>=0)) {
      // code in this block re-written for Bug 2355
      double tmpVal;
      double tmpDelta;
      double startAdj = lay.pf.getStart();

      tmpVal    =  lay.getPosAtTime(t);
      tmpVal    += startAdj;
      timeAtTop =  lay.getPosAtTime(t-dt/2);
      timeAtTop += startAdj;

      if (FAST_DT2DY) {
        tmpDelta = dt;
      }
      else  {
        tmpDelta =  lay.getPosAtTime(t+dt/2);
        tmpDelta += startAdj - timeAtTop;
      }

      if (lay1d && MP.xvy) {
        x  = tmpVal;   // type 1000 time-based so set the x values
        dx = tmpDelta;
      } else {
        y  = tmpVal;
        dy = tmpDelta;
      }
    }

    // Get the pixel position of the feature (center point)
    if (anchor==A_PIXEL) {           // X, Y already in Pixels (since 3.5.0 for BUG 2784)
      ix = (int) x;
      iy = (int) y;
    } else if (anchor==A_SCREEN) {   // X and Y are a percent of Screen pixels (since 3.5.0 for BUG 2784)
      ix = (int) ((MP.ix21) * (x/100));
      iy = (int) ((MP.iy21) * (y/100));
    }
    else if ((MP.view == MPlot.V_LATLONCONTINUOUS)) { // since 3.9.0 for NXM-3284
      // continuous Lat Lon requires special handling including:
      // 1) shifting of features into visible range
      // 2) drawing wrapped features on both sides of plot
      double effectiveX = x;
      if (dx==0 && dy==0) {   // Box of size zero
        // find true center X based adjusted for current bounds
        if ((x < MP.rx1) && (x+MPlot.LONGITUDE_RANGE < MP.rx2)) {
          effectiveX+=MPlot.LONGITUDE_RANGE;
        }
        MP.getPoint(effectiveX,y,z,pix);
        ix=pix.x; // X Pixel position
        iy=pix.y; // Y Pixel position
        if (isIconAndTextOnly(line)) {
          // Icon only Features should have width/height based on file specifications NXM-3647
          IconSymbol icon = (IconSymbol)line.getSymbol();
          int halfWidthPixels  = icon.getIconWidth() / 2;
          int halfHeightPixels = icon.getIconHeight() / 2;
          ix1 = ix - halfWidthPixels;
          ix2 = ix + halfWidthPixels;
          iy1 = iy - halfHeightPixels;
          iy2 = iy + halfHeightPixels;
        }
        else {
          ix1=ix2=ix; iy1=iy2=iy;
        }
        visible = checkRangeAndDrawFeature(lay, lin, tag, lay1d, ix, ix1, ix2, iy, iy1, iy2);
        return;
      }
      else {  // Determine the size of the box from dx and dy
        // check if the feature extends around the wrap bound
        double leftPoint = x - dx/2;
        double rightPoint = x + dx/2;
        double p = 0.0001;
        if ((leftPoint > MP.orx1) && (rightPoint < MP.orx2)) {
          // can be drawn as one feature in current bounds
          getFeatureBoundingPointsAndDraw(lay,lin,tag,lay1d,effectiveX,true);
        }
        else if ((leftPoint + MPlot.LONGITUDE_RANGE > MP.orx1) && (rightPoint + MPlot.LONGITUDE_RANGE < MP.orx2)) {
          // can be drawn as one feature shifted by 360 to current bounds
          effectiveX+=MPlot.LONGITUDE_RANGE;
          getFeatureBoundingPointsAndDraw(lay,lin,tag,lay1d,effectiveX,true);
        }
        else if ((leftPoint + MPlot.LONGITUDE_RANGE < MP.orx2) && (rightPoint > MP.orx1)) {
          // feature is split across bounds - Need to add the precisison calculation so it is visible.
          getFeatureBoundingPointsAndDraw(lay,lin,tag,lay1d,x+p,false);
          getFeatureBoundingPointsAndDraw(lay,lin,tag,lay1d,x+MPlot.LONGITUDE_RANGE-p,true);
        }
        else if ((leftPoint + (2*MPlot.LONGITUDE_RANGE) < MP.orx2) && (rightPoint + MPlot.LONGITUDE_RANGE> MP.orx1)) {
          // feature is split across bounds and originally lies partially below -180
          getFeatureBoundingPointsAndDraw(lay,lin,tag,lay1d,x+MPlot.LONGITUDE_RANGE,false);
          getFeatureBoundingPointsAndDraw(lay,lin,tag,lay1d,x+2*MPlot.LONGITUDE_RANGE,true);
        }
        else {
          // feature is not in bounds
        }
        return;
      }
    }
    else   {                       // A_DATA - RWC conversion
      // Features drawn on the edges of a lat lon map do not wrap, but just extend beyond the edge
      MP.getPoint(x,y,z,pix);
      ix=pix.x; // X Pixel position
      iy=pix.y; // Y Pixel position
    }

    // check for in range to display
    if (dx==0 && dy==0) {   // Box of size zero
      if (isIconAndTextOnly(line)) {
        // Icon only Features should have width/height based on file specifications NXM-3647
        IconSymbol icon = (IconSymbol)line.getSymbol();
        int halfWidthPixels  = icon.getIconWidth() / 2;
        int halfHeightPixels = icon.getIconHeight() / 2;
        ix1 = ix - halfWidthPixels;
        ix2 = ix + halfWidthPixels;
        iy1 = iy - halfHeightPixels;
        iy2 = iy + halfHeightPixels;
      }
      else {
        ix1=ix2=ix; iy1=iy2=iy;
      }
    } else {  // Determine the size of the box from dx and dy
      MP.getPoint(x-dx/2,y-dy/2,z-dz/2,pix);
      ix1=pix.x; iy1=pix.y;
      MP.getPoint(x+dx/2,y+dy/2,z+dz/2,pix);
      ix2=pix.x; iy2=pix.y;
      if (MP.view==MPlot.V_GEODETIC) {
        // if one of the points is visible force all to be
        boolean visXY  = (ix!=0) || (iy!=0);
        boolean visXY1 = (ix1!=0) || (iy1!=0);
        boolean visXY2 = (ix2!=0) || (iy2!=0);
        boolean oneVisible = visXY || visXY1 || visXY2;
        if (oneVisible) {
          MP.tpix.flags = Pix.FULLKEEP;
          if (!visXY) {
            MP.getPoint(x,y,z,pix);
            ix=pix.x; // X Pixel position
            iy=pix.y; // Y Pixel position
          }
          if (!visXY1) {
            MP.getPoint(x-dx/2,y-dy/2,z-dz/2,pix);
            ix1=pix.x;
            iy1=pix.y;
          }
          if (!visXY2) {
            MP.getPoint(x+dx/2,y+dy/2,z+dz/2,pix);
            ix2=pix.x;
            iy2=pix.y;
          }
        }
        MP.tpix.flags = Pix.CHECK; // restore
      }
      // swap values so that ix1 <= ix2; iy1 <= iy2
      if (ix2<ix1) { int i=ix1; ix1=ix2; ix2=i; }
      if (iy2<iy1) { int i=iy1; iy1=iy2; iy2=i; }
    }

    //now call checkRangeAndDraw
    visible = checkRangeAndDrawFeature (lay, lin, tag, lay1d, ix, ix1, ix2, iy, iy1, iy2);

  }

  /** Check if this Feature is just an IconSymbol and Text, with no other Types */
  private boolean isIconAndTextOnly (Line lineCopy) {
    return ((type & SYMBOL_AND_TEXT) == SYMBOL_AND_TEXT) &&
           ((type & (SYMBOL_AND_TEXT^ALL_TAGS)) == type) &&
           (lineCopy != null) && (lineCopy.getSymbol() instanceof IconSymbol);
  }

  /** Is this feature currently visible NXM-3512 */
  boolean isVisible () {
    return visible;
  }

  /** Get the Features bounding points and draw (where bounding box is not of size zero)
   *  @param lay        the Layer
   *  @param lin        the Line
   *  @param tag        the name
   *  @param lay1d      should always be false at this point since this code is currently just used
   *                    for continuous lat lon
   * @param effectiveX  the effective value of x (actual x may be shifted to if it actually should be visible in current bounds)
   * @param rangeAdjust the rwc2pix should automatically adjust this value up by the Lat Lon range if that makes it visible
   */
  private void getFeatureBoundingPointsAndDraw (Layer lay, Line lin, String tag, boolean lay1d, double effectiveX, boolean rangeAdjust) {
    MP.getPoint(effectiveX,y,z,pix, rangeAdjust);
    int ix=pix.x; // X Pixel position
    int iy=pix.y; // Y Pixel position

    MP.getPoint(effectiveX-dx/2,y-dy/2,z-dz/2,pix,rangeAdjust);
    int ix1=pix.x;
    int iy1=pix.y;
    MP.getPoint(effectiveX+dx/2,y+dy/2,z+dz/2,pix,rangeAdjust);
    int ix2=pix.x;
    int iy2=pix.y;
    // swap values so that ix1 <= ix2; iy1 <= iy2
    if (ix2<ix1) { int i=ix1; ix1=ix2; ix2=i; }
    if (iy2<iy1) { int i=iy1; iy1=iy2; iy2=i; }

    visible = checkRangeAndDrawFeature(lay, lin, tag, lay1d, ix, ix1, ix2, iy, iy1, iy2);
  }

  /** Check to make sure this feature is in range and draw the specific type
   *  @param lay        the Layer
   *  @param lin        the Line
   *  @param tag        the name
   *  @param lay1d      true if this is a Layer1D plot
   *  @param ix         center x in pixels
   *  @param ix1        bounding box smallest x in pixels
   *  @param ix2        bounding box largest  x in pixels
   *  @param iy         center y in pixels
   *  @param iy1        bounding box smallest y in pixels
   *  @param iy2        bounding box largest  x in pixels
   *  @return true if visible and drawn
   */
  private boolean checkRangeAndDrawFeature (Layer lay, Line lin, String tag, boolean lay1d,
      int ix, int ix1, int ix2, int iy, int iy1, int iy2) {
    // Apply our range checks to RWC positions only
    boolean xok = anchor==A_DATA ? (ix2>=MP.ix1 && ix1<=MP.ix2) : true;
    boolean yok = anchor==A_DATA ? (iy2>=MP.iy1 && iy1<=MP.iy2) : true;

    // We should always render features if inside their limits, b/c they could be filled.
    if (isType(ELPS)) {
      // NTN: for faster calculation, using max of either semiMajor and semiMinor as radius,
      //      so we don't have to worry about rotation (also convert from meters to RWC length)
      int ex1, ex2, ey1, ey2; // ellipse pixel boundary
      double len = Math.max(semiMajor, semiMinor);
      double radius = Constants.METERS2DEG * len;
      MP.getPoint(x-radius,y-radius,z-dz/2,pix); ex1=pix.x; ey1=pix.y;
      MP.getPoint(x+radius,y+radius,z+dz/2,pix); ex2=pix.x; ey2=pix.y;
      // swap values so that ex1 <= ex2; ey1 <= ey2
      if (ex2<ex1) { int i=ex1; ex1=ex2; ex2=i; }
      if (ey2<ey1) { int i=ey1; ey1=ey2; ey2=i; }

      // update range check to include ellipse boundary
      xok = xok || (ex2>=MP.ix1 && ex1<=MP.ix2);
      yok = yok || (ey2>=MP.iy1 && ey1<=MP.iy2);
    }

    // FIX BUG 836 to still display circle when zoom in too much
    if (isType(CIRC)) {
      int cx1, cx2, cy1, cy2; // circle pixel boundary
      MPoint[] minMax = calcCircleMinMaxPoints();
      cx1 = minMax[0].x;
      cy1 = minMax[0].y;
      cx2 = minMax[1].x;
      cy2 = minMax[1].y;
      // update range check to include circle boundary
      xok = xok || (cx2>=MP.ix1 && cx1<=MP.ix2);
      yok = yok || (cy2>=MP.iy1 && cy1<=MP.iy2);
    }

    if (false) { // Keep this for debug
      System.out.println("Feat draw: name="+name+" top="+timeAtTop+" x="+x+" y="+y+" ix="+ix+" iy="+iy
                        +" ix1,2="+ix1+","+ix2+" iy1,2="+iy1+","+iy2+" dx="+dx+" dt="+dt+" dy="+dy
                        +" xok="+xok+" yok="+yok+" type="+getType());
    }

    // The next two lines skip drawing if off screen, HLINE/VLINE and HBAR/VBAR and DATA are special
    // since they render independent of the normal x1,x2, y1,y2 settings. (Prior to NeXtMidas 3.5.4
    // HBAR/VBAR were missing from this check, this was corrected with Bug 2915.)
    if (!xok && !isType(HLINE|HBAR     )) return false;
    if (!yok && !isType(VLINE|VBAR|DATA)) return false;

    // if time, draw only if top is valid ?
    if (dt >= 0) {
      double min =  (lay1d) ? MP.orx1 : MP.ory1;
      double max =  (lay1d) ? MP.orx2 : MP.ory2;
      if (timeAtTop < min || timeAtTop > max) return false;
    }

    Graphics gc = MP.gc;
    Graphics gt = MP.gc; // on axis (not in clip window)
    lin.updateGraphics(gc); // set color, font, stroke, etc for line

    if ((type&INVERSE)!=0) {
      gc.setXORMode(MP.theme.cfg);
      gt.setXORMode(MP.theme.cfg);
    }
    if ((type&HBAR)!=0 && yok) { // since 3.3.1: filled horizontal bar for BUG 2714
      gc.setColor(line.getFillColor());
      gc.fillRect(MP.ix1,iy1+1, MP.ix2-2,iy2-iy1-1); // since 3.6.0 fixed off-by-one problem - Bug 2914
      gc.setColor(line.getColor());
    }
    if ((type&VBAR)!=0 && xok) { // since 3.3.1: filled vertical bar for BUG 2714
      gc.setColor(line.getFillColor());
      gc.fillRect(ix1+1,MP.iy1, ix2-ix1-1,MP.iy2-2); // since 3.6.0 fixed off-by-one problem - Bug 2914
      gc.setColor(line.getColor());
    }
    if (isType(HLINE) && yok) {
      gc.drawLine(MP.ix1,iy1,MP.ix2-2,iy1);
      if (iy2!=iy1) gc.drawLine(MP.ix1,iy2,MP.ix2-2,iy2);
      if ((type&IPLT)!=0) {
        if ((type&LTAG)!=0) gt.drawString(tag,MP.ix1,iy);
        if ((type&RTAG)!=0) gt.drawString(tag,MP.ix2-MP.tw*(1+tag.length()),iy);
      } else {
        if ((type&LTAG)!=0) gt.drawString(tag,MP.ix1-MP.tw*(1+tag.length()),iy);
        if ((type&RTAG)!=0) gt.drawString(tag,MP.ix2+MP.tw,iy);
      }
    }
    if (isType(VLINE) && xok) {
      gc.drawLine(ix1,MP.iy1,ix1,MP.iy2-2);
      if (ix2!=ix1) gc.drawLine(ix2,MP.iy1,ix2,MP.iy2-2);
      int iix = ix;
      if (dx>0) iix = ix - (MP.tw*tag.length())/2;
      if ((type&IPLT)!=0) {
        if ((type&TTAG)!=0) gt.drawString(tag,iix,MP.iy1+MP.th-2);
        if ((type&BTAG)!=0) gt.drawString(tag,iix,MP.iy2);
      } else {
        if ((type&TTAG)!=0) gt.drawString(tag,iix,MP.iy1-5);
        if ((type&BTAG)!=0) gt.drawString(tag,iix,MP.iy2+MP.th);
      }
    }


    if (xok && yok) {
      int idx = ix2-ix1;
      int idy = iy2-iy1;
      int idc = Math.max(idx,idy);
      int radius = idc/2;

      if ((type&FILLED)!= 0) {
        // since 3.3.1: moved the fill draws together so that we only set the fill color once (BUG 2714)
        // NOTE: the java.awt.Graphics.fillXXX(..) methods automatically offsets one pixel inside fill region
        gc.setColor(line.getFillColor());
        if ((type&BOX) !=0) gc.fillRect(ix1,iy1,idx,idy);
        if ((type&CIRC)!=0) gc.fillOval(ix-radius,iy-radius,idc,idc);
        if ((type&OVAL)!=0) gc.fillOval(ix1,iy1,idx,idy);
        gc.setColor(line.getColor());
      }

      if ((type&BOX)    !=0) gc.drawRect(ix1,iy1,idx,idy);
      if ((type&CIRC)   !=0) gc.drawOval(ix-radius,iy-radius,idc,idc);
      if ((type&OVAL)   !=0) gc.drawOval(ix1,iy1,idx,idy);
      // Support for inverted filled Triangle, probably belongs elsewhere
      // but I put it here until features are refactored
      if ((type&TRI)    !=0) drawTriangle(ix,iy,(int)dx,gc);
      if ((type&ELPS)   !=0) MP.drawEllipse(x,y,z,semiMajor,semiMinor,90.0-tilt,-1,lin, true); // 3.9.1 enable high zoom code
      if ((type&SYMBOL) !=0) lin.getSymbol().draw(ix,iy,gc);
      if ((type&OSYMBOL)!=0) gc.drawOval(ix-2,iy-2,4,4);
      if ((type&TEXT)   !=0) drawText(gc,tag,ix,ix1,ix2,iy,iy1,iy2);
      if ((type&DLINE)   !=0) {
        if ((ix < ix2) && (ix >= ix2-ix1-1)) {
          if (dx*dy >= 0) {
            gc.drawLine(ix1,iy1,ix2,iy2);
          }
          else {
            gc.drawLine(ix2,iy1,ix1,iy2);
          }
        }
      }
    }

    if ((type&DATA)!=0 && xok && (lay != null)) {
      int npts = lay.drawDataSegment(lin, x-dx/2, x+dx/2);

      // ONLY if user has specifically enabled this warning (4.1.4 update)
      // Warn the user once that Features are present , but no data is present.
      // This is a valid condition, as Features should remain present even
      // when hidden or the pipe with the data has not yet started
      // See NXM-2710 (Bug 2802) an NXM-1375 (Bug 1458)
      if (needWarnIfFeatureNotDrawn && (npts == -1)) {
        if (lay.getClass() != Layer.class) {
          Shell.warning("Features of TYPE=|DATA| are not supported by " + lay.getClass().getSimpleName()
              + ". Feature " + getName() + " not being drawn on " + lay.getName() + ".");
        }
        else {
          Shell.warning("Feature " + getName() + " of TYPE=|DATA| not being drawn until layer present.");
        }
        //type &= ~DATA; // Leave commented out!! (Fix for Bug 2802 in 3.5.0 (undo Bug 1458 fix))
        needWarnIfFeatureNotDrawn = false; // only warning once at most
      }
    }

    ix1Last = ix1; ix2Last = ix2; iy1Last = iy1; iy2Last = iy2;
    gt.setColor(MP.theme.cfg);
    if ((type&INVERSE)!=0) {
      gc.setPaintMode();
      gt.setPaintMode();
    }

    return true;
  }

  /** Draw the text label tag at the position(s) specified in options (Left, Right, Top, Bottom, or Default)
      @param gc The Graphics context to draw on
      @param tag The text to be displayed
      @param ix The X pixel position of the feature
      @param ix1 The X min pixel position of the feature
      @param ix2 The X max pixel position of the feature
      @param iy The Y pixel position of the feature
      @param iy1 The Y min pixel position of the feature
      @param iy2 The Y max pixel position of the feature
      @since NeXtMidas 2.7.4 (BUG 2008)
   */
  private void drawText (Graphics gc,String tag, int ix, int ix1, int ix2,  int iy, int iy1, int iy2) {
    if (textZoomRatio != 1) { // NXM-3648
      double ratio = (MP.rx2 - MP.rx1) / (MP.orx2 - MP.orx1);
      if (ratio > textZoomRatio) {
        return;
      }
    }

    // Add support for multi-line tags in 4.0.0
    String[] tagLines = null;
    boolean multiLineTag = false;
    if (tag.contains("\n")) {
      tagLines = tag.split("\n");
      tag = Arrays.asList(tagLines).stream().max(Comparator.comparingInt(String::length)).get();
      multiLineTag = true;
    }
    // since 3.7.0 next 4 lines to ensure text is visible on PLOT (Feature is already within visible range) (BUG 2970)
    ix1 = Math.max(ix1, MP.ix1);
    ix2 = Math.min(ix2, MP.ix2);
    iy1 = Math.max(iy1, MP.iy1);
    iy2 = Math.min(iy2, MP.iy2);

    int posx; // starting x pos of label
    int posy; // starting y pos of label
    final int CENTER_OFFSET = 2;   // amount that the center point needs to be adjusted
    int centerx = (ix1 + (ix2-ix1) / 2) + CENTER_OFFSET;   // center X pix pos
    int centery = (iy1 + (iy2-iy1) / 2) - CENTER_OFFSET;   // center Y pix pos
    int charWidth  = MP.tw;          // width of a single character
    int textWidth  = charWidth*(1+tag.length());  // width of all the text (or longest line of multi-line tag)
    int textHeight = MP.th;          // height of a single character

    if ((type&LTAG)!=0) {     // left side of feature
      posx = ix1 - textWidth;
      posy = centery +(textHeight /2);
      if (!multiLineTag) {
        gc.drawString(tag, posx, posy);
      }
      else {
        posy -= ((tagLines.length - 1) / 2) * textHeight;
        drawMultiLineTag(gc,tagLines,posx,posy,textHeight,tag.length(),true);
      }
    }
    if ((type&RTAG)!=0) {     // right side of feature
      posx = ix2 + charWidth;
      posy = centery + (textHeight /2);
      if (!multiLineTag) {
        gc.drawString(tag, posx, posy);
      }
      else {
        posy -= ((tagLines.length - 1) / 2) * textHeight;
        drawMultiLineTag(gc,tagLines,posx,posy,textHeight,tag.length(),false);
      }
    }
    if ((type&TTAG)!=0) {    // top of feature
      posy = iy1 - 2;
      if (!multiLineTag) {
        posx = centerx - (textWidth/2);
        gc.drawString(tag, posx, posy);
      }
      else {
        posx = centerx - ((tagLines[tagLines.length-1].length()*charWidth)/2);
        posy -= (tagLines.length - 1) * textHeight;
        drawMultiLineTag(gc,tagLines,posx,posy,textHeight,tag.length(),false);
      }
    }
    if ((type&BTAG)!=0) {    // bottom of feature
      posy = iy2 + textHeight;
      if (!multiLineTag) {
        posx = centerx - (textWidth/2);
        gc.drawString(tag, posx, posy);
      }
      else {
        posx = centerx - ((tagLines[0].length()*charWidth)/2);
        drawMultiLineTag(gc,tagLines,posx,posy,textHeight,tag.length(),false);
      }
    }
    if ((type&DTAG)!=0) {    // dynamically center at visible part of feature (since 3.7.0 for BUG 2970)
      posx = centerx - (textWidth/2);
      posy = centery + (textHeight/2);
      if (!multiLineTag) {
        gc.drawString(tag, posx, posy);
      }
      else {
        posx = Math.max(ix1, posx);
        posy = posy -((tagLines.length - 1) / 2) * textHeight;
        drawMultiLineTag(gc,tagLines,posx,posy,textHeight,tag.length(),false);
      }
    }
    if ((type&CUSTOMTAG)!=0) {
      drawCustomTagText(gc, tag, multiLineTag, tagLines, ix, ix1, ix2, iy, iy1, iy2);
    }

    if ( (type&BTAG)==0 && (type&TTAG)==0 && (type&RTAG)==0 &&
         (type&LTAG)==0 && (type&DTAG)==0 && (type&CUSTOMTAG)==0) {  // default - since 3.7.0 exclude DTAG for BUG 2970
      posx = ix + CENTER_OFFSET;
      posy = iy - CENTER_OFFSET;
      if (!multiLineTag) {
        gc.drawString(tag, posx, posy);
      }
      else {
        posy -= ((tagLines.length - 1) / 2) * textHeight;
        drawMultiLineTag(gc,tagLines,posx,posy,textHeight,tag.length(),false);
      }
    }
  }

  /** Draw Multiple Line Tags
   *
   *  @param tagLines tag line
   *  @param posx        x position for leftmost character
   *  @param posy        y position for first line
   *  @param longestLine length of longest line
   *  @param leftPad     true to left pad shorter lines with spaces
   */
  private void drawMultiLineTag (Graphics gc, String[] tagLines, int posx, int posy,
      int textHeight, int longestLine, boolean leftPad) {
    for (String line : tagLines) {
      if (leftPad) {
        gc.drawString(StringUtil.padLeft(line, longestLine), posx, posy);
      }
      else {
        gc.drawString(line, posx, posy);
      }
      posy += textHeight;
    }
  }

  /** Draws text at a custom location based on reference corner (or center) and pixel offset */
  private void drawCustomTagText (Graphics gc,String tag, boolean multilineTag, String[] tagLines,
      int ix, int ix1, int ix2,  int iy, int iy1, int iy2) {
    int posx = customTagPixelX; // starting x pos of label
    int posy = customTagPixelY; // starting y pos of label
    switch (customTagReference) {
      case TL:
        posx += ix1;
        posy += iy1;
        break;
      case TR:
        posx += ix2;
        posy += iy1;
        break;
      case BL:
        posx += ix1;
        posy += iy2;
        break;
      case BR:
        posx += ix2;
        posy += iy2;
        break;
      case CTR:
        posx += ix;
        posy += iy;
        break;
    }
    if (!multilineTag) {
      gc.drawString(tag, posx, posy);
    }
    else {
      drawMultiLineTag(gc,tagLines,posx,posy,MP.th,tag.length(),false);
    }
  }

  private void drawTriangle (int ix, int iy, int size, Graphics gc) {
    int xpoints [] = new int [4];
    int ypoints [] = new int [4];
    xpoints[0] = ix-size;    ypoints[0] = iy;
    xpoints[1] = ix;         ypoints[1] = iy+size;
    xpoints[2] = ix+size;    ypoints[2] = iy;
    xpoints[3] = xpoints[0]; ypoints[3] = ypoints[0]; //Close the polygon
    gc.fillPolygon (xpoints,ypoints,xpoints.length);
  }

  @Override
  public void refresh () {
    //if (!drawn && MP!=null) MP.drawOverlay(0,this);
    //else if (layer!=null) layer.needRefresh=true;
    //else needRefresh=true;
    if (isEnabled(0)) needRefresh=true;
  }

  @Override
  public boolean reset () {
    return false; // not applicable for a Feature
  }

  /** Get the TEXT of the feature
   *  @return the TEXT of the feature */
  public String getText () { return text; }
  /** Get the TYPE name (String) of the feature
   *  @return the TYPE name (String) of the feature */
  public String getType () { return Parser.mask2s(typeList,type); }
  /** Get the Anchor mode of the feature
   *  @return the Anchor mode of the feature */
  public String getAnchor () { return Parser.get(anchorList,anchor); }
  /** Get the X POSITION of the feature
   *  @return the X POSITION of the feature */
  public double getX () { return x; }
  /** Get the Y POSITION of the feature
   *  @return the Y POSITION of the feature */
  public double getY () { return y; }
  /** Get the Z POSITION of the feature
   *  @return the Z POSITION of the feature */
  public double getZ () { return z; }
  /** Get the time of the feature.
   *  @return the time of the feature */
  public double getT () { return t; }
  /** Get the X DELTA of the feature (the width).
   *  @return the X DELTA of the feature (the width) */
  public double getDX () { return dx; }
  /** Get the Y DELTA of the feature (the height).
   *  @return the Y DELTA of the feature (the height). */
  public double getDY () { return dy; }
  /** Get the Z DELTA of the feature (the elevation).
   *  @return the Z DELTA of the feature (the elevation) */
  public double getDZ () { return dz; }
  /** Get the time delta of the feature.
   *  @return the time delta of the feature */
  public double getDT () { return dt; }
  /** Get the ellipse semi-major axis of the feature, in meters.
   *  @return the ellipse semi-major axis of the feature, in meters */
  public double getMajor () { return semiMajor; }
  /** Get the ellipse semi-major axis of the feature, in meters.
   *  @return the ellipse semi-major axis of the feature, in meters. */
  public double getSemiMajor () { return semiMajor; }
  /** Get the ellipse semi-minor axis of the feature, in meters.
   *  @return the ellipse semi-minor axis of the feature, in meters. */
  public double getMinor () { return semiMinor; }
  /** Get the ellipse semi-minor axis of the feature, in meters.
   *  @return the ellipse semi-minor axis of the feature, in meters */
  public double getSemiMinor () { return semiMinor; }
  /** Get the ellipse tilt angle of the feature, in degrees. (0=North)*
   *  @return the ellipse tilt angle of the feature, in degrees */
  public double getTilt () { return tilt; }

  /** Sets the anchor mode of this Feature
      @param value int representing the anchor mode of the Feature
   */
  public void setAnchor (String value) {
    setAnchor( Parser.find(anchorList,value,A_DATA) );
  }

  /** Sets the name of this Feature
      @param value name of this Feature
   */
  public void setName (String value) {
    if (value==null || value.length()==0 || value.equals(name)) return;
    value = value.toUpperCase();
    if (MP!=null && name!=null) MP.features.rename (name,value);
    name = value;
    refresh();
  }

  /** Set the text of the Feature.  This can be moved to left, right, top or bottom of feature.
   *  @param value The text to insert where "null" and "NULL" are treated the same as null (as this
   *               matches macro usage). Setting this to null will make the text blank.
   */
  public void setText (String value) {
    if (StringUtil.isNull(value)) value = null; // <- allows "null" and "NULL" to be treated as null

    if (!Util.equals(text,value)) { // <-- Use Util.equals(..) since either value could be null
      text = value;
      refresh();
    }
  }

  @Override
  public void setEnable (int value) {
    if (enable == value) return;  // no change
    boolean wasDisplayed = (enable&MPlot.EN_GLOBAL) !=0;
    enable = value;
    refresh();
    boolean nowDisplayed = (enable&MPlot.EN_GLOBAL) !=0;
    if ((wasDisplayed && !nowDisplayed)) needRefresh=true;   // ensure feature is removed when un-enabled (Bug 2036)
  }

  /** Sets the custom tag
      @param reference  Reference location of custom tag
      @param xPixOffset Custom tag X-pixel offset
      @param yPixOffset Custom tag Y-pixel offset
   */
  @InternalUseOnly
  public void setCustomTag (CustomTagReference reference, int xPixOffset, int yPixOffset) {
    customTagReference = reference;
    customTagPixelX = xPixOffset;
    customTagPixelY = yPixOffset;
  }

  /** set custom tag x pixel offset from reference
  *
  *  @param xPixOffset  Custom tag X-pixel offset
  */
 @InternalUseOnly
  public void setCustomTagXPix (int xPixOffset) {
    customTagPixelX = xPixOffset;
  }

  /** set custom tag y pixel offset from reference
   *
   *  @param yPixOffset  Custom tag Y-pixel offset
   */
  @InternalUseOnly
  public void setCustomTagYPix (int yPixOffset) {
    customTagPixelY = yPixOffset;
  }

  /** Sets the custom tag
      @param reference  Reference location of custom tag
   */
  @InternalUseOnly
  public void setCustomTagRef (String reference) {
    switch (reference) {
      case "TL":
        customTagReference = CustomTagReference.TL;
        break;
      case "BL":
        customTagReference = CustomTagReference.BL;
        break;
      case "BR":
        customTagReference = CustomTagReference.BR;
        break;
      case "CTR":
        customTagReference = CustomTagReference.CTR;
        break;
      default:
        customTagReference = CustomTagReference.TR;
        break;
    }
  }

  //////////////////////////////////////////////////////////////////////////////
  // The following get/set methods match those in Layer.java
  @Override public void    setEnable (String value) { setEnable(Parser.mask(MPlot.enableList,value,enable)); }
  @Override public int     getEnable ()             { return enable; }
  @Override public String  getEnableString ()       { return Parser.mask2s(MPlot.enableList, enable); }
  @Override public String  getName ()               { return name; }

  /** Gets the line that holds attributes
   *  @return the line that holds attributes
   */
  @SuppressWarnings("deprecation") // deprecated Drawable.getLine to break in dependency on libg - 3.9.2 for JDK 11 modularity
            public Line    getLine ()               { return line; }
  /** Gets the Midas Plot to which this feature belongs
   *  @return the Midas Plot to which this feature belongs
   */
  @SuppressWarnings("deprecation") // deprecated Drawable.getMPlot to break in dependency on libg - 3.9.2 for JDK 11 modularity
            public MPlot   getMPlot ()              { return MP; }          // Since NeXtMidas 3.5.4 - Changed in 3.9.1 to not override the interface definition (interface method will be removed)
  @Override public Object  getNextLink ()           { return getLine(); }   // Since NeXtMidas 3.5.4
  @Override public Object  getPrevLink ()           { return getMPlot(); }  // Since NeXtMidas 3.5.4
  //////////////////////////////////////////////////////////////////////////////
  @Override public void    findRange ()             { /* not needed */ }         // Since NeXtMidas 3.5.4
  @Override
  public double  getXMin () {
    if ((MP==null) || MP.view!=MPlot.V_3DGRAPH) return x - getMaxDX()/2;
    else return Math.min(-Math.abs(x+getMaxDX()/2), x - getMaxDX()/2) * Layer.threeDGraphMarginFactor; // added support for 3DGRAPH in 3.9.3
  }
  @Override
  public double  getXMax () {
    if ((MP==null) || MP.view!=MPlot.V_3DGRAPH) return x + getMaxDX()/2;
    else return Math.max(Math.abs(x-getMaxDX()/2), x + getMaxDX()/2) * Layer.threeDGraphMarginFactor;
  }
  @Override
  public double  getYMin () {
    if ((MP==null) || MP.view!=MPlot.V_3DGRAPH) return y - getMaxDY()/2;
    else return Math.min(-Math.abs(y+getMaxDY()/2), y - getMaxDY()/2) * Layer.threeDGraphMarginFactor;
  }
  @Override
  public double  getYMax () {
    if ((MP==null) || MP.view!=MPlot.V_3DGRAPH) return y + getMaxDY()/2;
    else return Math.max(Math.abs(y-getMaxDY()/2), y + getMaxDY()/2) * Layer.threeDGraphMarginFactor;
  }
  @Override
  public double  getZMin () {
    if ((MP==null) || MP.view!=MPlot.V_3DGRAPH) return z - dz/2;
    else return Math.min(-Math.abs(z+dz/2), z - dz/2) * Layer.threeDGraphMarginFactor;
  }
  @Override
  public double  getZMax () {
    if ((MP==null) || MP.view!=MPlot.V_3DGRAPH) return z + dz/2;
    else return Math.max(Math.abs(z-dz/2), z + dz/2) * Layer.threeDGraphMarginFactor;
  }

  /** Get maximum dx
   *  @return maximum dx (accounting for circles and ellipses)
   *  @since NeXtMidas 3.5.4
   */
  private double getMaxDX() {
    double maxDX = dx;
    if (isType(CIRC)) {
      double diameter = Math.max(dx,dy); // diameter is maximum of dx & dy
      maxDX = (diameter > maxDX)? diameter:maxDX;
    }
    if (isType(ELPS)) {
      double len = Math.max(semiMajor, semiMinor); // use outer limits of ellipse...
      double diameter = (Constants.METERS2DEG * len) * 2;  // ...in RWC (lat/lon not meters)
      maxDX = (diameter > maxDX)? diameter:maxDX;
    }
    return maxDX;
  }

  /** Get maximum dy
   *  @return maximum dy (accounting for circles and ellipses)
   *  @since NeXtMidas 3.5.4
   */
  private double getMaxDY() {
    double maxDY = dy;
    if (isType(CIRC)) {
      double diameter = Math.max(dx,dy); // diameter is maximum of dx & dy
      maxDY = (diameter > maxDY)? diameter:maxDY;
    }
    if (isType(ELPS)) {
      double len = Math.max(semiMajor, semiMinor); // use outer limits of ellipse...
      double diameter = (Constants.METERS2DEG * len) * 2;  // ...in RWC (lat/lon not meters)
      maxDY = (diameter > maxDY)? diameter:maxDY;
    }
    return maxDY;
  }

  /** set list of feature types to enable
   *  @param value list of feature types to enable */
  public void setType (String value) {
    setType(Parser.mask(typeList,value,type));
  }

  /** Sets the type of feature to plot.
   *  @param val the type of feature to plot */
  public void setType (int val) { if (type != val) { type = val; refresh(); } }

  /** Sets the anchor mode of the feature
      @param val int represening the anchor mode
   */
  public void setAnchor (int val) { if (anchor != val) { anchor = val; refresh(); } }

  /** Sets the X-coordinate (RWC) of the center point
   *  @param val the X-coordinate (RWC) of the center point */
  public void setX (double val) { if (x != val) { x = val; refresh(); }}

  /** Sets the Y-coordinate (RWC) of the center point
   *  @param val the Y-coordinate (RWC) of the center point */
  public void setY (double val) { if (y != val) { y = val; refresh(); }}

  /** Sets the Z-coordinate (RWC) of the center point
   *  @param val the Z-coordinate (RWC) of the center point */
  public void setZ (double val) { if (z != val) { z = val; refresh(); } }

  /** Sets the time of the feature
      @param val the time of the feature
   */
  public void setT (double val) { if (t != val) { t = val; refresh(); } }

  /** Sets the width delta of the feature in RWC. Note that the {@link #CIRC} feature
      uses the the larger of dx or dy when drawing.
      @param val dx width delta of the feature in RWC
   */
  public void setDX (double val) { if (dx != val) { dx = val; refresh(); } }

  /** Sets the height delta of the feature in RWC. Note that the {@link #CIRC} feature
      uses the the larger of dx or dy when drawing.
      @param val dy height delta of the feature in RWC
   */
  public void setDY (double val) { if (dy != val) { dy = val; refresh(); } }

  /** Sets the elevation delta of the feature in RWC
      @param val dz elevation delta of the feature in RWC
   */
  public void setDZ (double val) { if (dz != val) { dz = val; refresh(); } }

  /** Sets the time delta of the feature
      @param val dt time delta of the feature
   */
  public void setDT (double val) { if (dt != val) { dt = val; refresh(); } }

  /** Sets X and DX using min and max values. This is computed as:
      <pre>
        dx = max - min;
        x  = min + dx/2;
      </pre>
      @param min Minimum X value.
      @param max maximum X value.
      @since NeXtMidas 2.5.5
   */
  public void setXMinMax (double min, double max) {
    if (min > max) { double temp = min; min = max; max = temp; }
    if (dx != (max-min) || (x != (min + ((max-min)/2)) )) {
      // Needs refersh
      dx = max - min;
      x  = min + dx/2;
      refresh();
    }
  }

  /** Sets Y and DY using min and max values. This is computed as:
      <pre>
        dy = max - min;
        y  = min + dy/2;
      </pre>
      @param min Minimum Y value.
      @param max maximum Y value.
      @since NeXtMidas 2.5.5
   */
  public void setYMinMax (double min, double max) {
    if (min > max) { double temp = min; min = max; max = temp; }
    if (dy != (max-min) || (y != (min + ((max-min)/2)) )) {
      // Needs refresh
      dy = max - min;
      y  = min + dy/2;
      refresh();
    }
  }

  /** Sets T and DT using min and max values. This is computed as:
      <pre>
        dt = max - min;
        t  = min + dt/2;
      </pre>
      @param min Minimum T value.
      @param max maximum T value.
      @since NeXtMidas 2.5.5
   */
  public void setTMinMax (double min, double max) {
    if (min > max) { double temp = min; min = max; max = temp; }
    if (dt != (max-min) || (t != (min + ((max-min)/2)) )) {
      // Needs refresh
      dt = max - min;
      t  = min + dt/2;
      refresh();
    }
  }

  /** Gets the minimum T value.
      @return <code>t - dt/2</code>
      @since NeXtMidas 2.5.5
   */
  public double getTMin () { return t - dt/2; }

  /** Gets the maximum T value.
      @return <code>t + dt/2</code>
      @since NeXtMidas 2.5.5
   */
  public double getTMax () { return t + dt/2; }

  /** Set the semi-major ellipse axis in meters.
   *  @param value The axis measurement in meters (not degrees).
   */
  public void setMajor (double value) { setSemiMajor(value); }

  /** Set the semi-major ellipse axis in meters.
   *  @param value The axis measurement in meters (not degrees).
   */
  public void setSemiMajor (double value) {
    if (semiMajor != value) {
      semiMajor = value;
      refresh();
    }
  }

  /** Set the semi-minor ellipse axis in meters.
   *  @param value The axis measurement in meters (not degrees).
   */
  public void setMinor (double value) { setSemiMinor(value); }

  /** Set the semi-minor ellipse axis in meters.
   *  @param value The axis measurement in meters (not degrees).
   */
  public void setSemiMinor (double value) {
    if (semiMinor != value) {
      semiMinor = value;
      refresh();
    }
  }

  /** Set the tilt of an ellipse (0=North).
      @param value The value in degrees
   */
  public void setTilt  (double value) {
    if (tilt != value) {
      tilt = value;
      refresh();
    }
  }

  /** visible DIV full area ratio at which Feature's text is visible
   *  to avoid overcrowding at low zoom levels
   *  @param zoomRatio visible DIV full area ratio
   */
  @InternalUseOnly
  public void setTextZoomRatio (double zoomRatio) {
    if (textZoomRatio != zoomRatio) {
      textZoomRatio  = zoomRatio;
    }
  }

  /** visible DIV full area ratio at which Feature is visible
   *  to avoid overcrowding finer Features at low zoom levels
   *  @param zoomRatio visible DIV full area ratio
   */
  @InternalUseOnly
  public void setFeatureZoomRatio (double zoomRatio) {
    if (featureZoomRatio != zoomRatio) {
      featureZoomRatio  = zoomRatio;
    }
  }


  /** Removes this Feature either from the Plot it belongs to or
      the Layer it belongs to, depending on where it originated from
   */
  public void delete () {
    if (name!=null) MP.removeFeature (this.name);
    else if (layer!=null) layer.feature = null;
  }

  @Override
  public boolean isEnabled (int mode) {
    // Implements Drawable.isEnabled(..), see Drawable.java for notes about implementation.
    if ((enable&MPlot.EN_GLOBAL)==0) return false;
    if (mode==0) return true;
    else return (mode>0) ^ ((enable&MPlot.EN_BSTORE)!=0);
  }

  /** issue configuration menus */
  public void configure () {
    // JC4: 13DEC06 Removed GoToLoc from list... never implemented.
    String menuItems = "Rename,Delete,Enable,Type,Line,Anchor,Dialog,Query";
    if (metadata != null) menuItems += ",Metadata";
    new GMenu(MP,"Feature.cfg",menuItems,0,0,this);
  }

  @Override
  public int processMessage (Message msg) {
    // This function is extremely similar to the one in Layer (much is copy-paste)
    String text = Convert.o2s(msg.data);

    if (msg.name.equals("FEATURE.CFG")) {
           if (text.equals("RENAME")) new GPrompt(MP,"Name",  name,                     0,            this);
      else if (text.equals("QUERY" )) new GQuery( MP,"Feature "+name, this,             0,            this);
      else if (text.equals("ENABLE")) new GMenu(  MP,"Enable",MPlot.enableList, enable, GMenu.TOGGLE, this);
      else if (text.equals("TYPE"  )) new GMenu(  MP,"Type",  typeList,         type,   GMenu.TOGGLE, this);
      else if (text.equals("ANCHOR")) new GMenu(  MP,"Anchor",anchorList,       anchor, 0,            this);
      else if (text.equals("METADATA")) new GQuery( MP,"Feature "+name, this, "Metadata",0,this);
      else if (text.equals("DELETE")) MP.removeFeature(name);
      else if (text.equals("LINE"  )) line.configure();
      else if (text.equals("DIALOG")) dialog();
      else { Shell.warning("FEATURE: Unknown message name="+msg.getName()+" data="+msg.getData()); }
    }
    else if (msg.name.equals("ENABLE" )) { enable = msg.info; refresh(); } // TODO: Change to setEnable(msg.info)
    else if (msg.name.equals("TYPE"   )) { type   = msg.info; refresh(); } // TODO: Change to setType(msg.info)
    else if (msg.name.equals("NAME"   )) setName(text);
    else if (msg.name.equals("RENAME" )) setName(text);
    else if (msg.name.equals("NEWNAME")) setName(text);
    else if (msg.name.equals("FEATURE.DIALOG")) {
      Table  tbl    = (Table)msg.data;
      String action = tbl.getS("DIALOG.VALUE");
      setX(tbl.getD("X.VALUE"));
      setY(tbl.getD("Y.VALUE"));
      setZ(tbl.getD("Z.VALUE")); // fixed in 3.9.3 for NXM-3502
      setText(tbl.getString("TEXT.VALUE"));
      if (action.equals("ZOOMTO")) {
        double   zr   = tbl.getD("ZOOMRANGE.VALUE");
        double[] zoom = new double[] { x-zr,x+zr,y-zr,y+zr };
        MP.mh.processMessage( new Message("ZOOM",0,zoom) );
      }
    }
    else {
      Shell.warning("FEATURE: Unknown message name="+msg.getName()+" data="+msg.getData());
      return 0;
    }
    return 1;
  }

  /** present a configuration dialog */
  public void dialog () {
    String sticky = "Apply,ZoomTo", actions = "Submit,Cancel,"+sticky;
    String ptext = (text==null)? "null" : text;
    boolean geo = (MP.view==MPlot.V_LATLON) || (MP.view==MPlot.V_LATLONCONTINUOUS) ||  // V_LATLON_CONT added in 3.9.0 for NXM-3284
                  (MP.view==MPlot.V_MERCATOR) || (MP.view==MPlot.V_GEODETIC);
    GDialog gd = new GDialog (MP, "Feature.Dialog", 250,75, actions,sticky, 0,this);
    GPrompt gl = new GPrompt (gd, "Text", ptext, GWidget.NOMSG, this);
    GValue  gx = new GValue (gd, "X  ", x, MP.orx1, MP.orx2, 1.0, GWidget.NOMSG, this);
    GValue  gy = new GValue (gd, "Y  ", y, MP.ory1, MP.ory2, 1.0, GWidget.NOMSG, this);
    GValue  gz = new GValue (gd, "Z  ", z, MP.orz1, MP.orz2, 1.0, GWidget.NOMSG, this);
    double  zr = Math.min(Math.abs(MP.orx2-MP.orx1),Math.abs(MP.ory2-MP.ory1));
    GValue  gr = new GValue (gd, "ZoomRange", zr/10, zr/100, zr, zr/100, GWidget.NOMSG, this);
    if (geo) { gx.setTitle("Lon"); gy.setTitle("Lat"); gz.setTitle("Alt"); }
    gd.resize(1);
  }

  /** @since NeXtMidas 3.5.4 */
  /** set MPlot for Feature
   *  @param plot MPlot
   */
  @SuppressWarnings("deprecation")
  public void setMPlot (MPlot plot) {
    this.MP = plot;
    line.setPlot(plot);
    if (hasGroup()) {
      plot.addFeatureToGroup(this);
    }
  }

  /** Sets the plot this feature is part of.
   *  @param plot The plot this drawable is part of.
   *  @since NeXtMidas 3.5.4
   */
  public void setPlot (MPlot plot) { // TODO: Deprecate this to match Layer
    setMPlot(plot);
  }

  /** Set the Layer this feature is on by name. The named Layer must exist
   *  as in the MPlot layers array.
   *  @param layerName The name of the layer this feature belongs to.
   *  @since NeXtMidas 3.5.0
   */
  public void setLayer (String layerName) {
    if (MP==null || MP.layers==null || MP.layers.size()==0) return;
    Layer lay = (Layer) MP.layers.get(layerName);
    if (lay != null) setLayer(lay);
  }

  /** Sets the layer this feature is on.
   *  @param layer The layer this feature belongs to.
   */
  public void setLayer (Layer layer) {
    this.layer = layer;
    line.setParent(layer);
  }

  /** Gets the plot this feature is part of
      @return the plot this feature is part of
      @since NeXtMidas 3.5.4
  */
  public MPlot getPlot () {
    return getMPlot();
  }

  /** Gets the layer this feature is on. If none specified, then get the base layer from the plot
   *  (see {@link MPlot#getBaseLayer()}). The layer returned is the layer used when computing the
   *  position of features that use {@link #DATA} or time-based values.
   *  @return The layer this feature belongs to.
   */
  public Layer getLayer () {
    Layer result = layer;
    if (result==null && MP!=null) result = MP.getBaseLayer();  // Plot baseLayer concept added in 3.5.0
    return result;
    // ORIG before 2.6.0: return layer;
  }

  @Override
  public Table toTable () {
    Table ft = new Table();
    java.awt.Color color = line.getColor();
    ft.put("NAME",name);
    ft.put("TYPE",getType());
    if (isType(SYMBOL)) ft.put("SYMBOL", line.getSymbol().getName());  // added for Bug 2264
    if (anchor!=A_DATA) ft.put("ANCHOR",getAnchor());
    if (color!=null) {
      // ft.put("COLOR",Convert.l2x(color.getRGB()));  // replaced with line below for Bug 2263
      ft.put("COLOR",MColor.toString(color));
    }
    //TODO: Why do we omit 0 values? 0,0 is a legal position. The only omission should be t and dt when dt=-1
    if (x!=0)  ft.put("X",x);
    if (y!=0)  ft.put("Y",y);
    if (z!=0)  ft.put("Z",z);
    if (t!=0)  ft.put("T",t);
    if (dx!=0) ft.put("DX",dx);
    if (dy!=0) ft.put("DY",dy);
    if (dz!=0) ft.put("DZ",dz);
    if (dt!=0) ft.put("DT",dt);
    if (text!=null) {
      ft.put("TEXT",text);
    }
    if (metadata!=null) {
      ft.put("METADATA", metadata);
    }
    if (group!=null) {
      ft.put("GROUP", group);
    }
    return ft;
  }

  /** Set feature parameters from a table.
   *  @param t The table of parameters.
   */
  public void fromTable (Table t) {
    KeyObject.setKeys(this,t);
  }

  /** Checks if this feature is of the following type option (feature) mask. Note, a feature can be
   *  of multiple types (b/c of the masking, hence it can be a box, circle, ellipse, and so on. <br>
   *  <br>
   *  Since NeXtMidas 3.5.4 this method is able to map between {@link #HLINE}, {@link #VLINE}, and
   *  {@link #XLINE}, {@link #YLINE}, so that asking for one will return true if it or its proper
   *  companion is set.
   *  @param typeOptionMask Type option (feature type) to test if this feature is of that type.
   *  @return true, if this feature is of the specified type option mask, otherwise false
   *  @since NeXtMidas 2.7.1; was previously private since 2.3.1
   */
  public final boolean isType (int typeOptionMask) {
    if (MP == null) {
      // ignore
    }
    else if (MP.xvy) {
      // orientation rotated (x - vertical axis, y - horizontal axis)
      // (so a XLINE (line at a given x)is horizontal, and a YLINE is vertical
      if ((typeOptionMask & (XLINE|HLINE)) != 0) typeOptionMask |= XLINE|HLINE;
      if ((typeOptionMask & (YLINE|VLINE)) != 0) typeOptionMask |= YLINE|VLINE;
    }
    else {
      // normal orientation (y - vertical axis, x - horizontal axis)
      // (so a XLINE (line at a given x)is vertical, and a YLINE is horizontal
      if ((typeOptionMask & (XLINE|VLINE)) != 0) typeOptionMask |= XLINE|VLINE;
      if ((typeOptionMask & (YLINE|HLINE)) != 0) typeOptionMask |= YLINE|HLINE;
    }
    return (type&typeOptionMask) !=0;
  }

  //NTN: the alternate form of isType(String) might be useful for usage in a macro (2009-04-13)
  ///** Checks if this feature is of the following type list (e.g. "CIRC|ELPS").
  //    Note, a feature can be of multiple types (b/c of the masking, hence it can
  //    be a box, circle, ellipse, and so on.
  //    @see #isType(int)
  // */
  //public final boolean isType (String typeList) {
  //  return isType(Parser.mask(typeList, typeList, 0));
  //}

  /** Deprecated since NeXtMidas 2.5.0: only for internal usage by nxm.sys.intr.Feature.
   *  @param ref The reference to use.
   */
  @InternalUseOnly
  public void setRef (Object ref) {
    // The intend ref object is used when opening files (can be Midas, Command, etc.),
    // but is currently only used in Feature (it was probably copied from Layer.java).
  }

  /** Calculate Min and Max Points for Circle having maximized size based on pixels (axis)
   *  @return array of two MPoint, first (index 0) is min Point, and second (index=1) is max Point
   *  @since NeXtMidas 3.5.4
   */
  private MPoint[] calcCircleMinMaxPoints() {
    MPoint pixMin    = new MPoint(); // Feature pixel minimum
    MPoint pixMax    = new MPoint(); // Feature pixel maximum
    MP.tpix.flags = Pix.FULLKEEP; // get true values, even if not in visible range
    MP.getPoint(x-dx/2,y-dy/2,z-dz/2,pixMin);
    MP.getPoint(x+dx/2,y+dy/2,z+dz/2,pixMax);
    MP.tpix.flags = Pix.CHECK; // restore flag

    // swap values so that pixMin.x <= pixMax.x; pixMin.y <= pixMax.y
    if (pixMax.x < pixMin.x) { int i=pixMin.x; pixMin.x=pixMax.x; pixMax.x=i; }
    if (pixMax.y < pixMin.y) { int i=pixMin.y; pixMin.y=pixMax.y; pixMax.y=i; }
    int diffPixelsX = pixMax.x - pixMin.x;
    int diffPixelsY = pixMax.y - pixMin.y;
    int pixelsXGTY = diffPixelsX - diffPixelsY;
    if (pixelsXGTY > 0) {
      pixMin.y -= pixelsXGTY;
      pixMax.y += pixelsXGTY;
    }
    else {
      pixMin.x += pixelsXGTY;
      pixMax.x -= pixelsXGTY;
    }
    MPoint[] minMaxPoints = {pixMin, pixMax};
    return minMaxPoints;
  }

  /** @since NeXtMidas 3.5.4 */
  @Override
  public boolean isAtPosition (Position pos, int dpx, int dpy) {
    MPoint pixMin    = new MPoint(); // Feature pixel minimum
    MPoint pixMax    = new MPoint(); // Feature pixel maximum
    // check for in range to display
    if (getMaxDX() ==0 && getMaxDY()==0 ) {   // Box of size zero & any Circle or Ellipse has radius of 0
      pixMin.x = pixMax.x = pix.x;
      pixMin.y = pixMax.y = pix.y;
    } else {
      if (isType(CIRC)) { // range of circle dependent on axes - chooses maximum pixels (rather than larger of dx or dy)
        MPoint[] minMax = calcCircleMinMaxPoints();
        pixMin = minMax[0];
        pixMax = minMax[1];
      }
      else { // Determine the size of the box from getMaxDX() and dy
        double maxDX = getMaxDX();
        double maxDY = getMaxDY();
        MP.getPoint(x-maxDX/2, y-maxDY/2, z-dz/2, pixMin);
        MP.getPoint(x+maxDX/2, y+maxDY/2, z+dz/2, pixMax);
        // swap values so that pixMin.x <= pixMax.x; pixMin.y <= pixMax.y
        if (pixMax.x < pixMin.x) { int i=pixMin.x; pixMin.x=pixMax.x; pixMax.x=i; }
        if (pixMax.y < pixMin.y) { int i=pixMin.y; pixMin.y=pixMax.y; pixMax.y=i; }
      }
    }
    MPoint posPix = new MPoint();
    MP.getPoint(pos, posPix);

    boolean okX = (dpx == -2); // Does x-axis check pass?
    boolean okY = (dpy == -2); // Does y-axis check pass?

    if (isType(BOX|CIRC|OVAL|ELPS|HBAR|VBAR|DLINE)) {
      okX = okX || ((pixMin.x <= posPix.x) && (posPix.x <= pixMax.x)) || isType(HBAR);
      okY = okY || ((pixMin.y <= posPix.y) && (posPix.y <= pixMax.y)) || isType(VBAR);
    }
    else { // HLINE/VLINE without any dimensionality
      okX = okX || isType(HLINE);
      okY = okY || isType(VLINE);
    }

    if ((okX     ) && (okY     )) return true;  // Strict containment -> TRUE
    if ((dpx == 0) && (dpy == 0)) return false; // Strict containment -> FALSE

    int defDP = ((type & SYMBOL) == 0)? 4 : line.getSymbolSize() / 2;
    if (dpx == -1) dpx = defDP;
    if (dpy == -1) dpy = defDP;

    okX = okX || ((dpx >= 0) && (Math.abs(posPix.x - pix.x) < dpx));
    okY = okY || ((dpy >= 0) && (Math.abs(posPix.y - pix.y) < dpy));

    return okX && okY; // Containment with fudge-factor -> TRUE or FALSE
  }

  /** Is the Feature present at the specified pixel position?
   *  The 'dpx' and 'dpy' parameters specify a suggested "fudge factor" to use:
   *  <pre>
   *   N = N-pixel fudge-factor
   *   0 = No fudge factor, strict position match (not good for small features)
   *  -1 = Use default N-pixel fudge-factor
   *  -2 = Don't consider this axis.
   *  </pre>
   *  @param posPix this MPoint contains the pixel coordinates to check
   *  @param dpx x fudge factor
   *  @param dpy y fudge factor
   *  @return true if Feature is present
   *  @since NeXtMidas 3.9.4
   */
  boolean isAtPix (MPoint posPix, int dpx, int dpy) {
    MPoint pixMin    = new MPoint(); // Feature pixel minimum
    MPoint pixMax    = new MPoint(); // Feature pixel maximum
    // check for in range to display
    if (getMaxDX() ==0 && getMaxDY()==0 ) {   // Box of size zero & any Circle or Ellipse has radius of 0
      pixMin.x = pixMax.x = pix.x;
      pixMin.y = pixMax.y = pix.y;
    } else {
      if (isType(CIRC)) { // range of circle dependent on axes - chooses maximum pixels (rather than larger of dx or dy)
        MPoint[] minMax = calcCircleMinMaxPoints();
        pixMin = minMax[0];
        pixMax = minMax[1];
      }
      else { // Determine the size of the box from getMaxDX() and dy
        double maxDX = getMaxDX();
        double maxDY = getMaxDY();
        MP.getPoint(x-maxDX/2, y-maxDY/2, z-dz/2, pixMin);
        MP.getPoint(x+maxDX/2, y+maxDY/2, z+dz/2, pixMax);
        // swap values so that pixMin.x <= pixMax.x; pixMin.y <= pixMax.y
        if (pixMax.x < pixMin.x) { int i=pixMin.x; pixMin.x=pixMax.x; pixMax.x=i; }
        if (pixMax.y < pixMin.y) { int i=pixMin.y; pixMin.y=pixMax.y; pixMax.y=i; }
      }
    }

    boolean okX = (dpx == -2); // Does x-axis check pass?
    boolean okY = (dpy == -2); // Does y-axis check pass?

    if (isType(BOX|CIRC|OVAL|ELPS|HBAR|VBAR|DLINE)) {
      okX = okX || ((pixMin.x <= posPix.x) && (posPix.x <= pixMax.x)) || isType(HBAR);
      okY = okY || ((pixMin.y <= posPix.y) && (posPix.y <= pixMax.y)) || isType(VBAR);
    }
    else { // HLINE/VLINE without any dimensionality
      okX = okX || isType(HLINE);
      okY = okY || isType(VLINE);
    }

    if ((okX     ) && (okY     )) return true;  // Strict containment -> TRUE
    if ((dpx == 0) && (dpy == 0)) return false; // Strict containment -> FALSE

    int defDP = ((type & SYMBOL) == 0)? 4 : line.getSymbolSize() / 2;
    if (dpx == -1) dpx = defDP;
    if (dpy == -1) dpy = defDP;

    okX = okX || ((dpx >= 0) && (Math.abs(posPix.x - pix.x) < dpx));
    okY = okY || ((dpy >= 0) && (Math.abs(posPix.y - pix.y) < dpy));

    return okX && okY; // Containment with fudge-factor -> TRUE or FALSE
  }


  /** Sets x, dx, y, dy. This is true for the box, the circle, and the oval, similar dimensions are needed for each.
   *  <br><br>
   *  The primary intent for this method is to set the box location/dimension from the msg.data table of a DRAGBOX message.
   *  <br><br>
   *  This parameter bust be a table with at least {XMIN, XMAX, YMIN, YMAX}
   *  @param tblWithOuterBounds Message with table values as bounds.
   *  @since NeXtMidas 3.7.1
   */
  public void setBoundsFromTable (Table tblWithOuterBounds) {

    // Gets the extreme values from the table for working with
    double xmin = tblWithOuterBounds.getD("XMIN.VALUE");
    double xmax = tblWithOuterBounds.getD("XMAX.VALUE");
    double ymin = tblWithOuterBounds.getD("YMIN.VALUE");
    double ymax = tblWithOuterBounds.getD("YMAX.VALUE");

    // Sets the min and max using the values coming from the table
    setXMinMax(xmin, xmax);
    setYMinMax(ymin, ymax);
  }

  /** Sets x, dx, y, dy, t, dt. This is true for the box, the circle, and the oval, similar dimensions are needed for each.
   *  <br><br>
   *  The primary intent for this method is to set the box location/dimension from the msg.data table of a DRAGBOX message.
   *  <br><br>
   *  This parameter bust be a table with at least {XMIN, XMAX, YMIN, YMAX}
   *  @param tblWithOuterBounds Message with table values as bounds.
   */
  public void setBoundsFromTableTime (Table tblWithOuterBounds) {
    if (tblWithOuterBounds.containsKey("XMIN")) {
      // If Time values are present set them
      double tmin = tblWithOuterBounds.getD("TMIN.VALUE");
      double tmax = tblWithOuterBounds.getD("TMAX.VALUE");
      if (tmin!=tmax) { // will not get set if they do not exist or are both the same
        setTMinMax(tmin, tmax);
      }
    }
    setBoundsFromTable(tblWithOuterBounds);
  }

  /** Get the name of the group that this feature belongs
   *  @return the group
   *  @since NeXtMidas 3.9.3
   */
  public String getGroup () {
    return group;
  }

  /** Check if this feature is part of a group
   *
   *  @return True if it is part of a group
   *  @since NeXtMidas 3.9.3
   */
  public boolean hasGroup () {
    return group!=null && group!="";
  }

  /** Set the group the feature will be in
   *
   *  @param group the group to set
   *  @since NeXtMidas 3.9.3
   */
  public void setGroup (String group) {
    if (group.equals(this.group)) return;
    String oldGroup = this.group;
    this.group = group;
    if (MP!=null) {
      if (oldGroup == null || oldGroup.isEmpty()) MP.addFeatureToGroup(this); // add to group
      else MP.moveFeatureToGroup(oldGroup, this); // move from group to new group
    }
  }

  /** Set the line attributes using a Table of key - value pairs
   *
   *  @param tbl The table of attributes for the line
   *  @since NeXtMidas 3.9.3
   */
  public void setLine(Table tbl) {
    KeyObject.setKeys(getLine(), tbl);
  }

  /** Popup the metadata to the screen.
   *  @since NeXtMidas 3.9.3
   */
  @InternalUseOnly
  public void displayMetaData () {
    new GMenu(MP,"Metadata",getMetadata().toString(),0,0,this);
  }

  /** Get the metadata
   *  @return the metadata
   *  @since NeXtMidas 3.9.3
   */
  public Object getMetadata () {
    return metadata;
  }

  /** Set the metadata
   *  @param metadata the metadata to set
   *  @since NeXtMidas 3.9.3
   */
  public void setMetadata (Object metadata) {
    this.metadata = metadata;
  }

  /** This will get the current pixel location of the feature.
   *
   *  @return int array of pixel location {x,y}
   *  @since NeXtMidas 3.9.3
   */
  int[] getPixelXY () {
    return new int[] {pix.x,pix.y};
  }

  /** Set if user gets one warning when Feature not drawn
   *  @param warn true to get 1 warning
   */
  public static void setWarningFeatureNotDrawn (boolean warn) {
    Feature.warningFeatureNotDrawn = warn;
  }

  /** Get if user will get one warning when Feature not drawn
   *  @return true if will get warning
   */
  public static boolean isWarningFeatureNotDrawn () {
    return Feature.warningFeatureNotDrawn;
  }

  /** Set this Feature to be the MPlot elastic Feature
   *  This is only allowed if this is an MPlot Feature
   *  Any current MPlot.elasticFeature will not longer be elastic
   *
   *  @param stretchable true if this feature should be the elastic one
   */
  public void setElastic (boolean stretchable) {
    if (stretchable) {
      if ((MP==null) || (layer!=null)) {
        Shell.warning("setElestic not allowed on Feature " + this
            + " Only Features associated with MPlot and not with a layer are eligible to be the Mplot elastic Feature");
      }
      else {
        if (MP.elasticFeature!=null) {
          Shell.warning("Overriding current elastic Feature "+MP.elasticFeature +
              " with Feature "+this);
        }
        MP.setElasticFeature(this);
      }
    }
    else {
      if ((MP.elasticFeature!=null) && (MP.elasticFeature!=this)) {
        Shell.warning("This Feature is not the MPlot elastic Feature, so deselecting does nothing :" + this);;
      }
      else {
        MP.setElasticFeature(null);
      }
    }
  }

  public String toString () {
    return toTable().toString();
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Deprecated Fields/Methods
  //////////////////////////////////////////////////////////////////////////////////////////////////

}