package nxm.sys.libg;

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.IllegalComponentStateException;
import java.awt.Image;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

import nxm.sys.inc.Chainable;
import nxm.sys.inc.Constants;
import nxm.sys.inc.DataTypes;
import nxm.sys.inc.Drawable;
import nxm.sys.inc.IDable;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MessageHandler;
import nxm.sys.inc.PlotFile;
import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.inc.Units;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.FileName;
import nxm.sys.lib.FileName.FNCase;
import nxm.sys.lib.Format;
import nxm.sys.lib.GeodeticUtil;
import nxm.sys.lib.KeyVector;
import nxm.sys.lib.MFormat;
import nxm.sys.lib.Message;
import nxm.sys.lib.MessageQueue;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Position;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Table;
import nxm.sys.lib.TextFile;
import nxm.sys.lib.Time;
import nxm.sys.lib.Transform;
import nxm.sys.lib.TuneAdjust;
import nxm.sys.lib.XmlFile;
import nxm.sys.libm.Tolerance;

/** A class to open and maintain a graphical plot. <br>
    <br>
    <a id="transform"><b><u>Mathematical Transformations Used</u></b></a><br>
    The following transformations are used to convert between the differing coordinate systems:
    <pre>
      1. Transformation matrix used to convert between the RWC position
         (x,y) and screen/pixel position (x',y'):
              x' = mxx*x + mxb        x = (x'-mxb)/mxx
              y' = myy*y + myb        y = (y'-myb)/myy

              UNITS:
                x,  y  - as specified in the file for the base layer (usually first layer being plotted)
                x', y' - in pixels (relative screen position)
    </pre>
    <a id="nomenclature"><b><u>Nomenclature</u></b></a><br>
    The following terms are used in the documentation for the various constants:
    <pre>
      OPTION       - A bit flag. Multiple bit flags can be combined by
                     or'ing them together (e.g. G_SHOW|G_GRID).
      OPTION GROUP - A set of bit flags that have been combined to form
                     a compound option.
      SETTING      - An enumerated option. Settings are mutually exclusive
                     and cannot be combined.
    </pre>

    @author  Jeff Schoen / Jay Hallam / J. Czechowski IV / Neon Ngo
*/
public class MPlot extends MWindow
    implements MessageHandler,DataTypes,Constants,Chainable,ImageObserver,MouseWheelListener {

  /** Current plot view (see {@link #viewList}) */
  @InternalUseOnly("since 3.3.1") public int view = 0;

  /** Mask value for axis options (see {@link #axisList} and {@link #getAxis()}). private since 3.3.1, was deprecated in 2.8.0. */
  private int axis = 0;

  /** Current plot mode (see {@link #modeList}). */
  @InternalUseOnly("since 3.3.1") public int mode = 0;
  /** Mask value for scale options (see {@link #scaleList}). */
  @InternalUseOnly("since 3.3.1") public int scale = 0;

  /** Mask value for readout options (see {@link #readoutList} and {@link #getReadout()}). private since 3.3.1, was deprecated in 2.8.0. */
  private int readout = 0; // The first readout menu
  /** Mask value for extended readout options (see {@link #readoutListExt} and {@link #getReadoutExt()}) */
  private int readoutExt = 0; // currently only contains options that display on right side - will need code mods if includes left
  private int READOUT_OPTION_MAIN = 1; // used to tell setReadout to update the first readout menu
  private int READOUT_OPTION_EXT = 2; // used to tell setReadout to update the second readout menu
  private double readoutExtTime = 0; // NXM-3628 3.9.4

  /** Mask value for plot options (see {@link #optionsList}).
      @deprecated Since NeXtMidas 3.3.1: this was intended for INTERNAL USE ONLY and will become package-private in 3.7+
   */
  @Deprecated public int options = 0;

  /** Last view.
      @deprecated Since NeXtMidas 3.3.1: this was intended for INTERNAL USE ONLY and will become package-private in 3.7+
   */
  @Deprecated public int viewLast;
  /** Last mode.
      deprecated Since NeXtMidas 3.3.1: this was intended for INTERNAL USE ONLY and will become package-private in 3.7+
   */
  @InternalUseOnly("since 3.3.1")
  public int modeLast;

  /** This is the width  of text in the panel. */
  @InternalUseOnly("since 3.3.1") public int tw; // TODO: make package-private in 3.7+?
  /** This is the height of text in the panel. */
  @InternalUseOnly("since 3.3.1") public int th; // TODO: make package-private in 3.7+?
  /** This is the accent of text in the panel. private since 3.3.1, was deprecated in 2.8.0. */
  private int ta;

  /** Smallest visible X-axis value in pixels (see rx1)  */  public int    ix1;
  /** Largest  visible X-axis value in pixels (see rx2)  */  public int    ix2;
  /** Range of visible X-axis value in pixels (ix2-ix1)  */  public int    ix21;
  /** Smallest visible Y-axis value in pixels (see ry1)  */  public int    iy1;
  /** Largest  visible Y-axis value in pixels (see ry2)  */  public int    iy2;
  /** Range of visible Y-axis value in pixels (iy2-iy1)  */  public int    iy21;
  /** Smallest visible Z-axis value in pixels (see rz1)  */  public int    iz1;
  /** Largest  visible Z-axis value in pixels (see rz2)  */  public int    iz2;

  /** private since 3.3.1, was deprecated in 2.8.0. ***NOT USED*** */
  private int iz21;

  /** Axis divisions along the X-axis (used for the axis tics). private since 3.3.1, was deprecated in 2.8.0. */
  private int ndx;

  /** Axis divisions along the Y-axis (used for the axis tics). private since 3.3.1, was deprecated in 2.8.0. */
  private int ndy;

  /** Axis divisions along the Z-axis (used for the axis tics). private since 3.3.1, was deprecated in 2.8.0. */
  private int ndz;

  /** Cursor X-axis in RWC. private since 3.3.1, was deprecated in 2.8.0. This is not used. */
  private double rx;

  /** Smallest visible X-axis value in RWC (see ix1,orx1) */  public double rx1;
  /** Largest  visible X-axis value in RWC (see ix2,orx2) */  public double rx2;

  /** Cursor Y-axis in RWC. private since 3.3.1, was deprecated in 2.8.0. This is not used. */
  private double ry;

  /** Smallest visible Y-axis value in RWC (see iy1,ory1) */ public double ry1;
  /** Largest  visible Y-axis value in RWC (see iy2,ory2) */ public double ry2;

  /** Cursor Z-axis in RWC. private since 3.3.1, was deprecated in 2.8.0. This is not used. */
  private double rz;

  /** Smallest visible Z-axis value in RWC (see iy1,ory1). */ public double rz1;
  /** Largest  visible Z-axis value in RWC (see iy2,ory2). */ public double rz2;

  /** Pixel delta in pixels per RWC along X-axis (numXPixels/(rx2-rx1)). */ public double mxx;
  /** Pixel delta in pixels per RWC along Y-axis (numYPixels/(ry2-ry1)). */ public double myy;
  /** Offset of first pixel in RWC along X-axis (rx1*mxx).   */             public double mxb;
  /** Offset of first pixel in RWC along Y-axis (ry1*myy).   */             public double myb;

  /** 3-D rotation matrix. private since 3.3.1, was deprecated in 2.8.0. */
  private double mzz;

  /** 3-D rotation matrix. private since 3.3.1, was deprecated in 2.8.0. */
  private double mzb;

  /** Drag Box X-axis start value in RWC. private since 3.3.1, was deprecated in 2.8.0. */
  private double brx;

  /** Drag Box Y-axis start value in RWC. private since 3.3.1, was deprecated in 2.8.0. */
  private double bry;

  /** Drag Box X-axis delta value in RWC. private since 3.3.1, was deprecated in 2.8.0. */
  private double brxd;

  /** Drag Box Y-axis delta value in RWC. private since 3.3.1, was deprecated in 2.8.0. */
  private double bryd;

  /**  Original value used for rx1  */  public double orx1;
  /**  Original value used for rx2  */  public double orx2;
  /**  Original value used for ry1  */  public double ory1;
  /**  Original value used for ry2  */  public double ory2;
  /**  Original value used for rz1  */  public double orz1;
  /**  Original value used for rz2  */  public double orz2;

  /** Full/File value used for rx1
      @deprecated Since NeXtMidas 2.8.0. This was not meant to be public and will be changed to private in a future release.
   */
  @Deprecated public double frx1; // note: used by WORLDWIND option tree

  /** Full/File value used for rx2
      @deprecated Since NeXtMidas 2.8.0. This was not meant to be public and will be changed to private in a future release.
   */
  @Deprecated public double frx2;

  /** Full/File value used for ry1
      @deprecated Since NeXtMidas 2.8.0. This was not meant to be public and will be changed to private in a future release.
   */
  @Deprecated public double fry1;

  /** Full/File value used for ry2
      @deprecated Since NeXtMidas 2.8.0. This was not meant to be public and will be changed to private in a future release.
   */
  @Deprecated public double fry2;

  /** Full/File value used for rz1
      @deprecated Since NeXtMidas 2.8.0. This was not meant to be public and will be changed to private in a future release.
   */
  @Deprecated public double frz1;

  /** Full/File value used for rz2
      @deprecated Since NeXtMidas 2.8.0. This was not meant to be public and will be changed to private in a future release.
   */
  @Deprecated public double frz2;

  /** Minimum range for autoscaling. private since 3.3.1, was deprecated in 2.8.0.  */
  private double minRange = 50;

  /** Maximum range for autoscaling. private since 3.3.1, was deprecated in 2.8.0.  */
  private double maxRange = 100;

  /** Time for animation. private since 3.3.1, was deprecated in 2.8.0.  */
  private double time;

  /** Date Format code indicating that a valid SimpleDateFormat will be used */
  public static int DATE_FORMAT_SIMPLEDATEFORMAT = 97;

  /** Date Format code indicating that system should derive the date format */
  public static int DATE_FORMAT_AUTO = 98;

  /** Date Format code indicating that system should use default date formats */
  public static int DATE_FORMAT_DEFAULT = 99;

  /** Format to display time in readout and axis.
      Valid values include the int values mapped {@link Time#formatList} as well
      as DATE_FORMAT_SIMPLEDATEFORMAT, which represents any pattern accepted by
      {@link java.textSimpleDateFormat}. In addition, DATE_FORMAT_AUTO and
      DATE_FORMAT_DEFAULT are valid.
   */
  private int dateFormat = DATE_FORMAT_DEFAULT;

  /** Maintains a valid pattern accepted by {@link java.textSimpleDateFormat}.
      Used for the case when DATE_FORMAT_SIMPLEDATEFORMAT is the define dateFormat.
   */
  private MFormat dateMFormat;

  /** Graphics instance used to draw the data. */
  @InternalUseOnly
  public Graphics gc;

  /** default zoom in/out for incremental zooms */
  private static final float DEF_ZOOM_PERCENTAGE = 0.5f;

  /** percentage to adjust Zoomed view when using arrow keys. 0.1 == 10%. */
  private float   zoomAdjust     = 0.1f;
  /** Amount to multiply the zoom adjustment (for page up/down, home, and end). */
  private float   zoomMult       = 5.0f;
  /** allow double-click zooming. Def=FALSE because of users using clicks .*/
  private boolean dblClickZoom   = false;
  /** allow mouse wheel zoom in/out. */
  private boolean mouseWheelZoom = false;  // changed to false per BUG 2104
  /** percentage to adjust Zoomed view when using ZOOMIN/OUT, '+'/'-' keys, etc */
  private float   zoomPercent    = DEF_ZOOM_PERCENTAGE;  // since 3.3.2 - Bug 2722
  /** keep zoom level when new layer added */
  private boolean zoomKeep       = false;

  /** Holds keyboard mapping of keys to use for zooming in/out and moving view. */
  private Table keyMap;

  /** Used to differentiate between single clicks and the first click of a double-click. */
  private int     clickCount = 0;
  /** keep track of when the user has zoomed/unzoomed using the keyboard/mouse. */
  private boolean zoomed     = false;
  /** Counter value to let MWindow know that it contains a popup menu Since 3.8.1 */
  public volatile int popupCount = 0;
  /** Table containing any persistent adjustments the user has made to the X|Y|Z view or bounds. These values
      will continue to apply to any files opened after this table is set (unless it is cleared) */
  @InternalUseOnly
  public  Table   outerLimits;
  /** Table containing any non-persistent adjustments the user has made to the X|Y|Z view or bounds.These values
      will will apply to the current file only. */
  @InternalUseOnly
  public  Table   viewLimits;

  /** whether to over-ride the START and DELTA axis values from the file. See {@link #setAxisIndex(boolean)} and {@link #isAxisIndex()}. */
  private boolean axisIndex = false;

  /** Allow drag and drop functionality by default. Override using /DRAGDROP or {@link #setDragAndDrop(boolean, MessageQueue)}. @since 2.8.0 */
  private boolean allowDragAndDrop = false;

  /** Target of where to put the String in Drag & Drop. @since NeXtMidas 2.8.0 */
  private StringDropTarget stringDropTarget = null;

  private int      maxcmp = 128;           // maximum x-axis points/pixel before use compression
  private double[] bvp    = new double[3]; // Viewer position for PANOBS when zmode==PANGEO.
  private double[] nvp    = new double[3]; // Viewer position for PANOBS when zmode==PAN3D (ELEV,ROLL,AZIM).

  private DecimalFormat nf  = new DecimalFormat(READOUT_FORMAT_DEFAULT); // The numeric format used by most of MPlot.
  private Format        xnf = new Format(READOUT_FORMAT_DEFAULT);        // The numeric format used for the X-values.
  private Format        ynf = new Format(READOUT_FORMAT_DEFAULT);        // The numeric format used for the Y-values.

  private int userView = 0; // The view as set by the user (see also view).
  private int xMult    = 0; // Multiplier for x-axis readout.
  private int yMult    = 0; // Multiplier for y-axis readout.
  private int precision= 3; // # places after decimal for readout values
  private Position mp = new Position();
  private Position mt = new Position();
  private Position mm;      // Measure position, used for dx,dy computation.
  private double xOff = 0;  // X Offset for Axis and Readout values for external accounting
  private double yOff = 0;  // Y Offset for Axis and Readout values for external accounting

  // CONE AND ELLIPSE DRAWING
  private double[]  edata;     // Data points used when drawing an ellipse.
  private double[]  cdata;     // Data points used when drawing a cone.
  private Transform et;        // Transform used when drawing ellipse
  private Transform ctp;       // Transform used when drawing cones (position)
  private Transform ctq;       // Transform used when drawing cones (target)
  private int       ediv = 60; // Default number of divisions (line segments) to make an ellipse.

  private int crossX;          // crosshairs X Value
  private int crossY;          // crosshairs Y Value
  private int scroll;          // mask for displaying scroll bars (1= Horz, 2=Vert, 3=Both)
  private int isx;             // X pos for the Horz scroll bar
  private int isxd;            // X delta for the Horz scroll bar
  private int isxy;            // Y Pos for Horz scroll bar
  private int isy;             // Y start pos for the Vert scroll bar
  private int isyd;            // Y delta for the Vert scroll bar
  private int isyx;            // X Pos for the Vert scroll bar
  private boolean isLowHScroll  = false; // horizontal scroll actually beyond bounds on low side
  private boolean isHighHScroll = false; // horizontal scroll actually beyond bounds on high side
  private boolean isLowVScroll  = false; // vertical scroll actually beyond bounds on low side
  private boolean isHighVScroll = false; // vertical scroll actually beyond bounds on high side

  private Line      lastLine;  // The last Line drawn
  private Color     lastColor; // The last color drawn
  private LayerGrid layerGrid; // The underlying Grid layer which all plots have ( set in Open() )
  private Layer     baseLayer; // the base Layer used for Bounds - since 3.5.0
  private GLegend   legend;    // The legend for the plot.
  private String    userCursor= null; // keep track of cursor set by user (Bug 2249)
  private boolean   showDeltas;// whether or not to always show dx/dy/drange from last mouse click (Bug 1383)

  private boolean cutsOnPipedLayer2D = false; // whether or not cuts are allowed on piped Layer2D plots (is the /cuts switch present?)

  // The archived frames array is used mainly for Layer2D. However, frame size changes cause the Layer2D instance to reset,
  // thus causing the archived buffer array to reset every single time. Keeping it in MPlot will help get around this issue.
  float[][] archivedFrames         = null; // circular buffer array to hold archived frames, used mainly for cuts on piped 2D plots
  int       archivedFramesWriteIdx = 0;    // keeps track of the current location of the circular buffer array

  /** Autoscale length for exponential avg'ing (in frames).
      package-private since 3.3.2, use {@link #getScaleAvg()} and {@link #setScaleAvg(int)} instead.
   */
  int scaleAvg = 8;
  /** The maximum autolength in NOAVERAGE mode is capped*/
  final static int MAX_NOAVERAGE_AUTOL = 256;  // since 3.7.0 bug 2901

  /** The zoom level params (x1,x2,y1,y2,z1,z2,view) */ private double[] rstk = new double[MAXLEV*LEVPARAM];

  /** The backing store images   */ private Image[]  images = new Image[MAXLEV+1];
  /** The double buffer images   */ private Image    dbimage;
  /** Double buffer image backup */ private Image    dbimageBackup; // backup of latest dbimage
  /** Can use backup dbimage     */ private boolean  useDBImageBackup = true;
  /** Static override on dbimage backup */ private static boolean  neverUseDBImageBackup = false;

  /** Vector of layers to plot.     */ @InternalUseOnly("since 3.5.1") public KeyVector    layers;
  /** Vector of features to plot.   */ @InternalUseOnly("since 3.5.1") public KeyVector    features;

  /** View Handler: ___             */ @InternalUseOnly("since 3.5.1") public View         viewer;
  /** View Handler: 2D views.       */ @InternalUseOnly("since 3.5.1") public View         view2D;
  /** View Handler: 3D views.       */ @InternalUseOnly("since 3.5.1") public View3D       view3D;
  /** View Handler: Geodetic views. */ @InternalUseOnly("since 3.5.1") public ViewGeo      viewGeo;
  /** View Handler: Polar views.    */ @InternalUseOnly("since 3.5.1") public ViewPolar    viewPolar;
  /** View Handler: Mercator views. */ @InternalUseOnly("since 3.5.1") public ViewMercader viewMerc;

  /** Indicates if the plot is an X-vs-Y plot (either XY or IXIY) (xvy=true).
   *  The PLOT default is Y-vs-X (xvy=false).
   */
  @InternalUseOnly("since 3.5.1") public boolean xvy;
  /** Indicates if the plot is a 2D plot.   */
  @InternalUseOnly("since 3.5.1") public boolean is2D;
  /** Cancels a refresh that may be in progress. */
  @InternalUseOnly("since 3.5.1") public boolean cancel;

  /** Indicates if the plot needs a refresh. private since 3.3.1: was deprecated in 2.8.0. */
  private boolean needRefresh;

  /** Indicates if the plot is refreshing. private since 3.3.1: was deprecated in 2.8.0. */
  private boolean refreshing;

  /** Comma-separated list of possible values for {@link #holdRefresh}, {@link #setHoldRefresh(int)}, and {@link #setHoldRefresh(String)}. */
  public static final String holdRefreshList = "Features,Layers,Canvas";
  /** Do not allow Feature refreshes */
  public static final int HOLD_FEATURE_REFRESHES = 0x01;
  /** Do not allow Layer refreshes */
  public static final int HOLD_LAYER_REFRESHES   = 0x02;
  /** Do not allow Canvas refreshes */
  public static final int HOLD_CANVAS_REFRESHES  = 0x04;  // since 2.9.2 (Bug 2266)

  /** Used to allow user to block refreshes, make a multitude of changes, then
      re-allow refreshes, effectively 'committing' many changes at once'
      @since NeXtMidas 2.7.2
   */
  int holdRefresh = 0;

  /** True to always clear maps during refresh, to support partially transparent objects */
  static boolean mapRefreshShouldClear = true;

  /** temporary pixel buffer   */  public Pix pix  = new Pix(MAXPIX,0);
  /** temporary pixel buffer   */  public Pix tpix = new Pix(4,Pix.CHECK);

  /** Whether or not to draw the plot on a Swing panel */
  private boolean isSwing = Theme.getOption(Theme.SWING);

  /** What type of Z is displayed (XY,XYZ,PANGEO,PAN3D,etc) */ private int     zmode      = 0;
  /** Indicates the size is being set/reset                 */ private boolean resizing;
  /** The X-axis units (see {@link Units})                  */ private int     xUnitsID   = Units.NONE;
  /** What type of Z is displayed (XY,XYZ,PANGEO,PAN3D,etc) */ private int     yUnitsID   = Units.NONE;
  /** The Z-axis units (see {@link Units})                  */ private int     zUnitsID   = Units.NONE;
  /** Indicates if the units are init'ed                    */ private boolean unitsInit  = false;
  /** box surrounding the On Axis Readout                   */ private MBox oarbox = new MBox();
//JGS: why are we double book keeping along with ix1,iy1,ix21,iy21? private MBox databox = new MBox(); // box surrounding the Data area
  /** use Backing Store                          */ private boolean useBS;
  /** <not used>                                 */ private boolean useNBS;
  /** Use DBUFFER                                */ private boolean useDB;
  /** How much of the data / plot to refresh     */ private int    refreshMode;
  /** Information about the plot for the readout */ private String plotInfo;

  private static final String READOUT_FORMAT_3PLACES = "###0.###";
  private static final String READOUT_FORMAT_6PLACES = "###0.######";
  private static final String READOUT_FORMAT_DEFAULT = READOUT_FORMAT_3PLACES;

  /** default minimum for X, Y and Z bounds */
  private final double DEF_MIN_BOUNDS = 0.0;
  /** default max bounds for X, Y and Z */
  private final double DEF_MAX_BOUNDS = 1.0;

  /** {@link #drawCrossHairs(int)} mode: Undraw the existng corsshairs */                        public static final int CH_UNDRAW = -1;
  /** {@link #drawCrossHairs(int)} mode: Draw the crosshairs at the mouse pos */                 public static final int CH_DRAW   =  1;
  /** {@link #drawCrossHairs(int)} mode: Undraw existing crosshairs, then draw at mouse pos */   public static final int CH_REDRAW =  0;

  private boolean drawnCH     = false;  // the drawing state of the crosshairs  (Bug 2327)
  private boolean needsDrawCH = false;  // do the crosshairs need to be drawn   (Bug 2327)

  private double axisBuffer = 1e-12;  // amount to adjust X/Y Min/Max bounds to display inclusive values (since 3.1.0 - Bug 2478)

  /** From Bug 467....
      Mask for initial values of various plot options.
      Used to determine if a User has set the option or it is default.
   */
  @InternalUseOnly("since 3.3.1") public int initValues = 0; // public ONLY for use by PLOT
  // Possible Settings for initValues mask
  /** Set the first time user sets Axis    */ public static final int I_AXIS    = 0x0001;
  /** Set the first time user sets View    */ public static final int I_VIEW    = 0x0002;
  /** Set the first time user sets Mode    */ public static final int I_MODE    = 0x0004;
  /** Set the first time user sets Scale   */ public static final int I_SCALE   = 0x0008;
  /** Set the first time user sets Time    */ public static final int I_TIME    = 0x0010;
  /** Set the first time user sets Readout */ public static final int I_READOUT = 0x0020;

  /** Plot options constants string list which corresponds with the General Plot Options. <br>
      Possible values = <code>{@value}</code>
   */
  public static final String optionsList =
    "BStore,Axes,Mark,Contrast,CrossHairs,XCut,YCut,ZCut,StillZoom,BlockZoom,Pager,All,Auto2D,DBuffer,NearInt,DragZoom";

  /** General Plot Option: Use Backing Store.   */
  public static final int O_BSTORE   = 0x0001;
  /** General Plot Option: Show axes.           */
  public static final int O_AXES     = 0x0002;
  /** General Plot Option: Show marks.          */
  public static final int O_MARK     = 0x0004;
  /** General Plot Option: Use high contrast mode. */
  public static final int O_CONTRAST = 0x0008;
  /** General Plot Option: Show crosshairs.     */
  public static final int O_CROSS    = 0x0010;

  /** General Plot Option: ___                  */
  public static final int O_XCUT     = 0x0020;
  /** General Plot Option: ___                  */
  public static final int O_YCUT     = 0x0040;
  /** General Plot Option: ___                  */
  public static final int O_ZCUT     = 0x0080;
  /** General Plot Option: Use still zoom.      */
  public static final int O_STILL    = 0x0100;
  /** General Plot Option: Use block zoom.      */
  public static final int O_BLOCK    = 0x0200;
  /** General Plot Option: Use pager.           */
  public static final int O_PAGER    = 0x0400;
  /** General Plot Option: All available data   */
  public static final int O_ALL      = 0x0800;
  /** General Plot Option: Auto Geodetic To Mercator for speed where appropriate */
  public static final int O_AUTO2D   = 0x1000;
  /** General Plot Option: Use Double Buffering */
  public static final int O_DBUFFER  = 0x2000;
  /** General Plot Option: Use Nearest Integral Scaling */
  public static final int O_NEARINT  = 0x4000;
  /** General Plot Option: Allow ZOOMIO (Zooming using right-mouse button) */
  public static final int O_DRAGZOOM = 0x8000;   // Added for BUG 2183

  /** Default Options list */
  public static final int O_DEFAULT   = 0;

  /** Plot view setting string list which corresponds to the Plot View Settings. <br>
      Possible values = <code>{@value}</code>
   */
  public static final String viewList = "YX,iYX,YiX,iYiX,XY,iXY,XiY,iXiY,XYZ,Polar,LatLon,Mercator,Geodetic,LatLonContinuous,3DGraph";

  /** Plot View Setting: Y by X. */
  public static final int V_YX       =  1;
  /** Plot View Setting: Y inverse by X . */
  public static final int V_IYX      =  2;
  /** Plot View Setting: Y  by X inverse. */
  public static final int V_YIX      =  3;
  /** Plot View Setting: Y inverse by X inverse. */
  public static final int V_IYIX     =  4;
  /** Plot View Setting: X  by Y. */
  public static final int V_XY       =  5;
  /** Plot View Setting: X inverse by Y. */
  public static final int V_IXY      =  6;
  /** Plot View Setting: X  by Y inverse. */
  public static final int V_XIY      =  7;
  /** Plot View Setting: X inverse by Y inverse. */
  public static final int V_IXIY     =  8;
  /** Plot View Setting: X by Y by Z. */
  public static final int V_XYZ      =  9;
  /** Plot View Setting: polar. */
  public static final int V_POLAR    = 10;
  /** Plot View Setting: LatLon projection. */
  public static final int V_LATLON   = 11;
  /** Plot View Setting: Mercator projection. */
  public static final int V_MERCATOR = 12;
  /** Plot View Setting: Geodetic (3D) projection. */
  public static final int V_GEODETIC = 13;
  /** Plot View Setting: LatLon continuous projection.
      Allows continuous panning of longitude, but always keeps the range between -180 and 540.
      Actual data is left in the -180 to 180 range, but when needed a temporary copy is adjusted
      by adding 360 to it before calculating its pixel location.<br>
      Checks for whether a value is in range also need adjustment.
      For example an viewable range of 160 to 200, an x1 of -170 is in range because -170 + 360 = 190,
      which falls between 160 and 200.
      @since NeXtMidas 3.9.0 */
  public static final int V_LATLONCONTINUOUS = 14;
  /** Plot View Setting: graphing X by Y by Z. */
  public static final int V_3DGRAPH =  15;

  /** Plot mode setting string list which corresponds to the Plot Mode Settings. <br>
      Possible values = <code>{@value}</code>
   */
  public final static String modeList = "Mag,Phase,Real,Imag,RnI,RvI,10Log,20Log,X,Y,Z";
  /** Plot Mode Setting: Magnitude.               */
  public static final int M_MAG      = 1;
  /** Plot Mode Setting: Phase.                   */
  public static final int M_PHASE    = 2;
  /** Plot Mode Setting: Real.                    */
  public static final int M_REAL     = 3;
  /** Plot Mode Setting: Imaginary.               */
  public static final int M_IMAG     = 4;
  /** Plot Mode Setting: RnI (Real and Imaginary) */
  public static final int M_RNI      = 5;
  /** Plot Mode Setting: RvI (Real vs Imaginary)  */
  public static final int M_RVI      = 6;
  /** Plot Mode Setting: 10Log.                   */
  public static final int M_10LOG    = 7;
  /** Plot Mode Setting: 20Log.                   */
  public final static int M_20LOG    = 8;
  /** Plot Mode Setting: X-Ordinate.              */
  public final static int M_VX       = 9;
  /** Plot Mode Setting: Y-Ordinate.              */
  public final static int M_VY       = 10;
  /** Plot Mode Setting: Z-Ordinate.              */
  public final static int M_VZ       = 11;

  /** List of dRange Possible Units of Measurement. */
  public static final String drangeunitsList = "M,KM,NMI";
  /** @deprecated Since NeXtMidas 3.3.1: use {@link #drangeunitsList}. */
  @Deprecated public static final String dRangeUnitsList = drangeunitsList; // TODO: remove in 3.7.x+
  /** dRange Unit: Meters */
  public static final int U_METERS         = 1;
  /** dRange Unit: Kilometers */
  public static final int U_KILOMETERS     = 2;
  /** dRange Unit: Nautical Miles */
  public static final int U_NAUTICAL_MILES = 3;
  /** Default units to display in the dRange */
  public static final int DEF_DRANGE_UNITS = U_METERS;  // read in Plot open()
  /** Value for the dRange Units (see {@link #drangeunitsList}).
      @deprecated Since NeXtMidas 3.3.1: use {@link #getDRangeUnitsInt()}
   */
  @Deprecated public int dRangeUnits = DEF_DRANGE_UNITS; // TODO: this should be made private in 3.7.x+

  /** List of plot readout options. <br>
      Possible values = <code>{@value}</code>
   */
  public static final String readoutList = "Show,onStatus,onAxis,onTitle,X,dX,1/X,iX,Y,dY,dY/dX,"
       + "iY,Z,dZ,dZ/dX,iZ,T,dT,1/T,1/dT,dRange,ExtPrec,Index,DMS,Units,View,DateString,Scientific,"
       + "Azimuth,AlwaysDeltas,dRangeUnits>";
  /** Extended Readout Options if have LayerDrawable or WMS Layer*/
  public static final String readoutListExt = "Show,Features,Points,WMS_level,RF";
  /** Extended Readout Options always available */
  public static final String shortReadoutListExt = "Show,Features";
  /** Readout Option: Show readout.             */
  public static final int R_SHOW     =    0x0001;
  /** Readout Option: Show status bar readout   */
  public static final int R_ONSTATUS =    0x0002;
  /** Readout Option: Show on-axis readout.     */
  public static final int R_ONAXIS   =    0x0004;
  /** Readout Option: Show on-title readout.    */
  public static final int R_ONTITLE  =    0x0008;
  /** Readout Option: Show X.                   */
  public static final int R_X        =   0x00010;
  /** Readout Option: Show DX.                  */
  public static final int R_DX       =   0x00020;
  /** Readout Option: Show X Inverse.           */
  public static final int R_XINV     =   0x00040;
  /** Readout Option: Show X Index (ele number).*/
  public static final int R_IX       =   0x00080;
  /** Readout Option: Show Y.                   */
  public static final int R_Y        =   0x00100;
  /** Readout Option: Show DY.                  */
  public static final int R_DY       =   0x00200;
  /** Readout Option: Show DYDX.                */
  public static final int R_DYDX     =   0x00400;
  /** Readout Option: Show Y Index (ele number).*/
  public static final int R_IY       =   0x00800;
  /** Readout Option: Show Z.                   */
  public static final int R_Z        =   0x01000;
  /** Readout Option: Show DZ.                  */
  public static final int R_DZ       =   0x02000;
  /** Readout Option: Show DZDX.                */
  public static final int R_DZDX     =   0x04000;
  /** Readout Option: Show Z Index (ele number).*/
  public static final int R_IZ       =   0x08000;
  /** Readout Option: Show T.                   */
  public static final int R_T        =   0x10000;
  /** Readout Option: Show DT.                  */
  public static final int R_DT       =   0x20000;
  /** Readout Option: Show T Inverse.           */
  public static final int R_TINV     =   0x40000;
  /** Readout Option: Show Frequency.           */
  public static final int R_FREQ     =   0x80000;
  /** Readout Option: Show dRange.              */
  public static final int R_DR       =  0x100000;
  /** Readout Option: Show extended precision (ExtPrec), 6 decimal places */
  public static final int R_EP       =  0x200000;
  /** Readout Option: Show Index values.        */
  public static final int R_INDEX    =  0x400000;
  /** Readout Option: Show Lat/Lon in DMS form. */
  public static final int R_DMS      =  0x800000;
  /** Readout Option: Show units in readout.    */
  public static final int R_UNITS    = 0x1000000;
  /** Readout Option: Show current view in readout.    */
  public static final int R_VIEW     = 0x2000000;
  /** Readout Option: Use date string when units are time. */
  public static final int R_DATESTRING = 0x4000000;
  /** Readout Option: Use scientific notation in readout.  */
  public static final int R_SCIENTIFIC = 0x8000000;
  /** Readout Option: Show Azimuth between two points in readout. */
  public static final int R_AZIMUTH     = 0x10000000;
  /** Readout Option: Keep Deltas active, when enabled, rather than toggling on and off */
  public static final int R_ALWAYSDELTAS  = 0x20000000; // since 3.9.2 for NXM-3453
  /** Extended Readout Option: Display number of visible Features */
  public static final int R_EXT_SHOW   = 0x0001;; // since 3.9.3
  /** Extended Readout Option: Display number of visible Features */
  public static final int R_EXT_FEATURE   = 0x0002;; // since 3.9.3
  /** Extended Readout Option: Display number of visible Points on right side */
  public static final int R_EXT_POINT     = 0x0004;; // since 3.9.3
  /** Extended Readout Option: Display WMS level based on pixel-to-degrees */
  public static final int R_EXT_WMS_LEVEL = 0x0008;; // since 4.0.1
  /** Extended Readout Option: Display RF Readout on raster plots */
  public static final int R_EXT_RASTER_RF = 0x0010;; // since 4.1.3

  /** Readout Option Group: Default readout.    */
  public static final int R_DEFAULT  = (R_SHOW|R_ONTITLE|R_ONSTATUS|R_X|R_DX|
        R_Y|R_DY|R_DR|R_VIEW|R_SCIENTIFIC|R_DATESTRING); // DATESTRING added in 4.1.4

  /** The list of the names of the valid auto-scale options. <br>
      Possible values = <code>{@value}</code>
   */
  public static final String scaleList  = "AutoMin,AutoMax,MinRange,MaxRange,MinZRange,MaxZRange,NoAverage,ExpandOnly";

  /** Scale Option: Auto-scale data minimum.  */
  public static final int S_MIN       = 0x1;
  /** Scale Option: Auto-scale data maximum.  */
  public static final int S_MAX       = 0x2;
  /** Scale Option: Set Min Auto-scale range. */
  public static final int S_MINRANGE  = 0x4;
  /** Scale Option: Set Max Auto-scale range. */
  public static final int S_MAXRANGE  = 0x8;
  /** Scale Option: Set Min Auto-scale range for Data (Type 2000 files). */
  public static final int S_MINZRANGE = 0x10;  // Since 2.9.0: Added for Bug 2187
  /** Scale Option: Set Max Auto-scale range for Data (Type 2000 files). */
  public static final int S_MAXZRANGE = 0x20;  // Since 2.9.0: Added for Bug 2187
  /** Scale Option: Set Auto-scale not to do exponential averaging (sliding window average), e.g.
      this will autoscale on the actual/true/full min/max value for {@link MPlot#scaleAvg} frames.
      @since NeXtMidas 3.3.3
   */
  public static final int S_NOAVERAGE = 0x40; // Since 3.3.3: Added for Bug 1807
  /** Scale Option: Set Auto-scale to expand only (e.g. no shrink). Since NeXtMidas 3.4.2.  */
  public static final int S_EXPANDONLY = 0x80; // Since 3.4.2: Added for Bug 1807

  /** Scale Option Group: Auto min/max.       */
  public static final int S_AUTO      = (S_MIN|S_MAX);
  /** Scale Option Group: Range min/max.      */
  public static final int S_RANGE     = (S_MINRANGE|S_MAXRANGE);

  /** Scale Option Group: Default Scale option. */
  public static final int S_DEFAULT  = S_AUTO;

  /** Plot options constants string list which corresponds with the Plot Enabling Options. <br>
      Possible values = <code>{@value}</code>
   */
  public static final String enableList = "Global,Labels,BStore,Thin,Base,Zoom,FastBase,FastZoom";

  /** Plot Enabling Option: ___                 */
  public static final int EN_GLOBAL  = 0x01;
  /** Plot Enabling Option: Plot labels.        */
  public static final int EN_LABELS  = 0x02;
  /** Plot Enabling Option: Backing store.      */
  public static final int EN_BSTORE  = 0x04;
  /** Plot Enabling Option: File thinning.      */
  public static final int EN_THIN    = 0x08;
  /** Plot Enabling Option: ___                 */
  public static final int EN_BASE    = 0x10;
  /** Plot Enabling Option: Zooming.            */
  public static final int EN_ZOOM    = 0x20;
  /** Plot Enabling Option: ___                 */
  public static final int EN_FASTBASE= 0x40;
  /** Plot Enabling Option: Fast zooming.       */
  public static final int EN_FASTZOOM= 0x80;

  /** Plot Enabling Option Group: Defaults.     */
  public static final int EN_DEFAULT  = 0xFF;

  /** Plot options constants string list which corresponds with the Plot Axis Options. <br>
      Possible values = <code>{@value}</code>
   */
  public static final String axisList = "Show,Grid,Hscroll,Vscroll,"+
    "Laxis,Ltics,Lnumbers,Llabels,Raxis,Rtics,Rnumbers,Rlabels,"+
    "Taxis,Ttics,Tnumbers,Tlabels,Baxis,Btics,Bnumbers,Blabels,"+
    "Oaxis,Otics,Onumbers,Olabels,gridFront,RotateVals,AutoRotate";

  /** Plot Axis Option: Show axis.              */
  public static final int G_SHOW     = 0x00000001;
  /** Plot Axis Option: Show grid.              */
  public static final int G_GRID     = 0x00000002;
  /** Plot Axis Option: Enable horizontal scroll. */
  public static final int H_SBAR     = 0x00000004;
  /** Plot Axis Option: Enable vertical scroll. */
  public static final int V_SBAR     = 0x00000008;
  /** Plot Axis Option: Show LEFT axis.         */
  public static final int L_AXIS     = 0x00000010;
  /** Plot Axis Option: Show LEFT tics.         */
  public static final int L_TICS     = 0x00000020;
  /** Plot Axis Option: Show LEFT numbers       */
  public static final int L_NUMS     = 0x00000040;
  /** Plot Axis Option: Show LEFT labels.       */
  public static final int L_LABS     = 0x00000080;
  /** Plot Axis Option: Show RIGHT axis.        */
  public static final int R_AXIS     = 0x00000100;
  /** Plot Axis Option: Show RIGHT tics.        */
  public static final int R_TICS     = 0x00000200;
  /** Plot Axis Option: Show RIGHT numbers.     */
  public static final int R_NUMS     = 0x00000400;
  /** Plot Axis Option: Show RIGHT labels.      */
  public static final int R_LABS     = 0x00000800;
  /** Plot Axis Option: Show TOP axis.          */
  public static final int T_AXIS     = 0x00001000;
  /** Plot Axis Option: Show TOP tics.          */
  public static final int T_TICS     = 0x00002000;
  /** Plot Axis Option: Show TOP numbers.       */
  public static final int T_NUMS     = 0x00004000;
  /** Plot Axis Option: Show TOP labels.        */
  public static final int T_LABS     = 0x00008000;
  /** Plot Axis Option: Show BOTTOM axis.       */
  public static final int B_AXIS     = 0x00010000;
  /** Plot Axis Option: Show BOTTOM tics.       */
  public static final int B_TICS     = 0x00020000;
  /** Plot Axis Option: Show BOTTOM numbers.    */
  public static final int B_NUMS     = 0x00040000;
  /** Plot Axis Option: Show BOTTOM labels.     */
  public static final int B_LABS     = 0x00080000;
  /** Plot Axis Option: Show ___ axis.          */
  public static final int O_AXIS     = 0x00100000;
  /** Plot Axis Option: Show ___ tics.          */
  public static final int O_TICS     = 0x00200000;
  /** Plot Axis Option: Show ___ numbers.       */
  public static final int O_NUMS     = 0x00400000;
  /** Plot Axis Option: Show ___ labels.        */
  public static final int O_LABS     = 0x00800000;
  /** Plot Axis Option: Plot grid layer in the front.*/
  public static final int G_GRIDFRONT= 0x01000000;
  /** Plot Axis Option: Plot rotate Y axis tic value labels (always).*/
  public static final int O_ROTATEVALS= 0x02000000;  // since 3.1.0 (Bug 2505)
  /** Plot Axis Option: Plot rotate Y axis tic value labels, if needed.*/
  public static final int O_AUTOROTATE= 0x04000000;    // since 3.1.0 (Bug 2505)

  /** List of click action options (value = {@value}). @since NeXtMidas 3.5.2 Bug  */
  public static final String buttonActionList = "NONE,MENU,MARK_MESSAGE,UNZOOM";
  //Note:The "MARK_MESSAGE" is a temporary message that gets converted into a "MARK"
  // message in plot, after more processing is done.  Since the buttonActionList name
  // starts with "mark", a user setting a button's action to MARK will get the desired result.
  private static final int BTN_NONE         = 1;// Button Action: show/hide menu
  private static final int BTN_MENU         = 2;// Button Action: show/hide menu
  private static final int BTN_MARK_MESSAGE = 3; // Button Action: mark location
  private static final int BTN_UNZOOM       = 4; // Button Action: zoom out
  private static final int LMB_DEFAULT   = BTN_MARK_MESSAGE; // Default LMB action
  private static final int MMB_DEFAULT   = BTN_MENU;         // Default MMB action
  private static final int RMB_DEFAULT   = BTN_UNZOOM;       // Default RMB action

  private int lmbButton = LMB_DEFAULT; // Action for LMB click
  private int mmbButton = MMB_DEFAULT; // Action for MMB click
  private int rmbButton = RMB_DEFAULT; // Action for RMB click

  /** List of options to show in the Axis menu. <br>
      Possible values = <code>{@value}</code>
   */
  public static final String axisMenu = "Default,Unlabeled,Numbered,Frame,TFrame,BFrame,TRFrame,"
                                      + "BRFrame,TLFrame,BLFrame,~Grid,~Show,Modify,~Gridfront,~RotateVals,~AutoRotate";

  /** Plot Axis Option Group: Show all axis. */
  public static final int ALL_AXIS = 0x00011110;
  /** Plot Axis Option Group: Show vertical tics. */
  public static final int V_TICS   = 0x00000662;
  /** Plot Axis Option Group: Show horizontal tics. */
  public static final int H_TICS   = 0x00066002;

  /** Plot Axis Option Group: Default axis options. */
  private static final int A_DEFAULT = (G_SHOW|H_SBAR|V_SBAR|L_AXIS|L_TICS|L_LABS|R_AXIS|R_TICS
                                              |R_NUMS|T_AXIS|T_TICS|T_LABS|B_AXIS|B_TICS|B_NUMS);

  /** Plot Axis Option Group: Default axis options for "Unlabeled". */
  private static final int A_UNLABELED = (G_SHOW|H_SBAR|V_SBAR|L_AXIS|L_TICS|R_AXIS|R_TICS|R_NUMS
                                                |T_AXIS|T_TICS|B_AXIS|B_TICS|B_NUMS);

  /** Plot Axis Option Group: Default axis options for "Framed". */
  private static final int A_FRAMED = (G_SHOW|H_SBAR|V_SBAR|L_AXIS|L_TICS|R_AXIS|R_TICS|T_AXIS
                                             |T_TICS|B_AXIS|B_TICS);

  /** Plot Axis Option Group: Default axis options for "Numbered". */
  private static final int A_NUMBERED = (G_SHOW|G_GRID|H_SBAR|V_SBAR|L_NUMS|R_NUMS|T_NUMS|B_NUMS);

  private static final int A_TFRAME  = (A_FRAMED|T_NUMS);        // Show axis values on top.
  private static final int A_BFRAME  = (A_FRAMED|B_NUMS);        // Show axis values on bottom.
  private static final int A_BRFRAME = (A_FRAMED|B_NUMS|R_NUMS); // Show axis values on bottom/right.
  private static final int A_TRFRAME = (A_FRAMED|T_NUMS|R_NUMS); // Show axis values on top/right.
  private static final int A_BLFRAME = (A_FRAMED|B_NUMS|L_NUMS); // Show axis values on bottom/left.
  private static final int A_TLFRAME = (A_FRAMED|T_NUMS|L_NUMS); // Show axis values on top/left.

  private String customXAxisLabel = null;  // when not null, displays user-defined String for label in X Axis (Since 3.3.1 - Bug 945)
  private String customYAxisLabel = null;  // when not null, displays user-defined String for label in Y Axis (Since 3.3.1 - Bug 945)
  private String customZAxisLabel = null;  // when not null, displays user-defined String for label in Z Axis (Since 3.3.1 - Bug 945)

  static final int DO_CHECK      =   0;         // doDraw(..) Flag: 'Blank' flag used to not influence checks on other flags
  static final int DO_DRAW       = 0x1;         // doDraw(..) Flag: Whether to draw
  static final int DO_BS         = 0x2;         // doDraw(..) Flag: Whether to use Backing Store
  static final int DO_NBS        = 0x4;         // doDraw(..) Flag: Do NOT use Backing Store
  static final int DO_FORCE_DRAW = 0x8;         // doDraw(..) Flag: Draw regardless of backing store checks (Since - 3.5.4 Bug 1913)

  // Possible zmodes
  private static final int PANX     = 1;  // Pan to given X coord
  private static final int PANY     = 2;  // Pan to given Y coord
  private static final int PANXY    = 3;  // Pan to given x,y coords
  private static final int SCROLLX  = 4;  // PANXY with Horz scrollbar
  private static final int SCROLLY  = 5;  // PANXY with Vert Scrollbar
  private static final int PAN3D    = 6;  // Used when in XYZ mode (Lat,Lon,Alt)
  private static final int PANGEO   = 7;  // Used when in Geodetic mode
  private static final int ZOOM     = 8;  // Not used
  private static final int ZOOMIO   = 9;  // PANXY with zoom factor
  private static final int PAN3DGRAPH = 10;  // Used when in 3D graph mode

  /** The current zoom level in use */ private int level;
  /** The previous zoom level used  */ private int lastLevel;
  /** X-AXIS zoom mode              */ public  static final int X_ZOOM  = 0x1;
  /** Y-AXIS zoom mode              */ public  static final int Y_ZOOM  = 0x2;
  /** X and Y-AXIS zoom mode        */ private static final int XY_ZOOM  = X_ZOOM + Y_ZOOM;

  /** Rotated Text: A 90 Deg rotation         */ private static final double VERT_ROTATION = -PI/2;
  /** Rotated Text: The char width            */ private static final int    CHAR_WIDTH    = 6;
  /** Rotated Text: The char height           */ private static final int    CHAR_HEIGHT   = 10;
  /** Number of params to define a zoom level */ private static final int    LEVPARAM      = 7;
  /** Number of params to define a zoom level */ private static final int    MAXLEV        = 15;  // moved from 9 to 15 in 3.3.0
  /** Max number of pixels in buffer          */ private static final int    MAXPIX        = 4096;

  /** Status of "Message Consumed'            */         static final int    MSG_CONSUMED  = 1;

  /** image to draw objects on that is then drawn at once (Since NeXtMidas 2.9.2, Bug 2266). */
  private PlotCanvas  canvas;

  private boolean useMousePosition = false; // NXM-3261 3.8.1 use mouse position in zoom

  /** Minimum Longitude for regular Lat Lon mode
   *  @since NeXtMidas 3.9.0 */
  static int LONGITUDE_DEFAULT_MIN    = -180;
  /** Maximum Longitude for regular Lat Lon mode
   *  @since NeXtMidas 3.9.0 */
  static int LONGITUDE_DEFAULT_MAX    =  180;
  /** Minimum Longitude for continuous Lat Lon mode - matches WMS LOOSE minimum
   *  @since NeXtMidas 3.9.0 */
  static int LONGITUDE_CONTINUOUS_MIN = -180;
  /** Maximum Longitude for continuous Lat Lon mode - matches WMS LOOSE maximum
   *  @since NeXtMidas 3.9.0 */
  static int LONGITUDE_CONTINUOUS_MAX =  540;
  /** Longitude Range
   *  @since NeXtMidas 3.9.0 */
  public static int LONGITUDE_RANGE   =  360;
  /** Minimum Latitude for regular Lat Lon mode
   *  @since NeXtMidas 3.9.0 */
  static int LATITUDE_DEFAULT_MIN     = -90;
  /** Maximum Latitude for regular Lat Lon mode
   *  @since NeXtMidas 3.9.0 */
  static int LATITUDE_DEFAULT_MAX     =  90;
  /** Latitude Range
   *  @since NeXtMidas 3.9.0 */
  public static int LATITUDE_RANGE    = 180;

  private boolean extendedRange = false;

  private FeatureGroupMap featureGroups;

  // *** PROTOTYPE ONLY: stretchable Feature fields section
  Feature elasticFeature = null;
  private boolean elasticFeatureSelected = false;
  private ResizeBox.Handle elasticHandle = ResizeBox.Handle.NONE;

  /** track the location of the current middle mouse button location for feature selection
   * @since NeXtMidas 3.9.3
   */
  private Point currentMMBClickedLocation;

  /** Percent of screen width can be off from center if not within object */
  @InternalUseOnly
  public static double FEATURE_SELECT_TOLERANCE = 0.05;

  // Initialize double click interval to Java default if available and otherwise to 400ms. (Since 3.5.2 - Bug 2865)
  private static final int doubleClickInterval;

  /** first LayerDrawable - used for count readout - NXM-3512 */
  private LayerDrawable layerDrawable = null;

  /** number of visible MPlot Features - does not include those associates with LayerDrawble */
  private int numVisibleFeatures = 0;

  /** WMS level used to set layer transitions. -1 means inactive */
  private double wmsLevel = -1;

  /** mxx corresponding to high enough zoom level that special code to avoid integer saturation will be used (5965232) */
  static final double HIGH_ZOOM_MXX = Integer.MAX_VALUE/LONGITUDE_RANGE; // since 3.9.1 for NXM-3131
  static final double HIGH_ZOOM_GEO_MXX = Integer.MAX_VALUE/2.4;
  static final double HIGH_ZOOM_GEO_MIN_SEMIS = 1E5;

  static {                       // Since 3.5.2 for NXM-2773
    Object val = null;
    if (!Shell.isHeadless()) {   // added in 3.7.1 to avoid exception when there is no display server (see NXM-2725)
      val = java.awt.Toolkit.getDefaultToolkit().getDesktopProperty("awt.multiClickInterval");
    }
    if (val instanceof Number) {
      doubleClickInterval = ((Number) val).intValue();
    } else {
      doubleClickInterval = 400; // sane default
    }
  }

  // LatLonContinuous need to save the zoom = 0 values - since 3.9.3
  // (otherwise, setting view limits leaves user unable to zoom out NXM-3578)
  private double llc_orx1 = LONGITUDE_DEFAULT_MAX;
  private double llc_orx2 = LONGITUDE_DEFAULT_MIN;

  /** Have DRAGBOX zoom result in maximizing map area, rather than centered - i.e. minimize margins*/
  private static boolean maxMapOnDragBox = true; // 3.9.3 for NXM-3581 can change back with setter

  /** Used along with maximize map on dragbox */
  private boolean dragboxAdjustment = false;

  /** Count of number of WMS layers. ReadoutExt uses this to decide if to display WMS_level as an option */
  private int wmsLayerCnt;

  /** keep safety features if Layer2D or LayerImage base layer */
  private boolean baseIsLayer2DorImage;

  /** ellipse axis for this ellipsoid slice */
  public enum ellipseAxis {
    /** The Ellipse X-Axis */ X,
    /** The Ellipse Y-Axis */ Y,
    /** The Ellipse Z-Axis */ Z
    };

  /** Creates a new instance.
      @param name    The name for this plot.
      @param mh      The message handler to be passed any messages not processed by this class.
   */
  public MPlot (String name, MessageHandler mh) {
    super(name,mh);
  }

  /** Sets up the plot and opens the plot window. */
  @Override
  public void open () {
    super.open ();
    addMouseWheelListener();
    if (fm != null) { // Since NeXtMidas 3.5.0. Skip if in Headless state. BUG 2817
      tw = fm.charWidth('0');
      th = fm.getHeight();
      ta = fm.getAscent();
    }
    level = lastLevel = 0;
    view = 0;
    ndx=ndy=ndz = 5;
    rx1=ry1=rz1 =orx1=ory1=orz1 =frx1=fry1=frz1 = DEF_MIN_BOUNDS;
    rx2=ry2=rz2 =orx2=ory2=orz2 =frx2=fry2=frz2 = DEF_MAX_BOUNDS;
    outerLimits = new Table();
    viewLimits   = new Table();
    layers = new KeyVector(16);
    features = new KeyVector(16);
    featureGroups = new FeatureGroupMap();
    layerGrid = new LayerGrid();
    addLayer ("GRID",layerGrid);
    viewer = view2D = new View(this);
    setView(0);            // 0 = none
    setMode(0);            // 0 = none
    setAxis(A_DEFAULT);
    setTime(0);            // 0 = none
    setScale(S_DEFAULT);
    int smartDefault = Theme.getOption(Theme.SWINGDB) ? O_DBUFFER : 0;  // 0 = none
    setOptions(smartDefault);   // Revert back to original behavior in 3.8.1, except when are using SwingDB
    setReadout(R_DEFAULT, READOUT_OPTION_MAIN);
    setup();
    initValues = 0;   // Bug 467. Set initValues mask to 0
  }

  /** Close the plot window. */
  @Override
  public void close () {
    if (legend != null) legend.close();
    legend = null;
    super.close();
  }

  /** Get {@link #baseLayer} if it has been set, otherwise get {@link #layers}[1]
      @return Layer that is functioning as Plot Base Layer
      @since 3.5.0
   */
  public Layer getBaseLayer () {
    if (baseLayer != null) return baseLayer;
    else                   return (Layer)this.getLayers().get(1);  // fallback to old way
  }

  /** Sets the given Layer to be the Plot Base Layer.<br><br>
      Note: This does NOT call refresh or setup() to adjust the view and bounds.
      This method only informs the current layer that it is the baseLayer.
      USE nxm.sys.prim.plot#setBaseLayer(Layer) to have the view and bounds adjusted
      @param lay The Layer to set as the Base Layer
      @since NeXtMidas 3.5.0
   */
  @InternalUseOnly
  public void setBaseLayer (Layer lay) {
    if (baseLayer != null && baseLayer.equals(lay)) return;   // nothing to do
    baseIsLayer2DorImage = "Layer2D".equals(lay.getClassName()) || "LayerImage".equals(lay.getClassName());
    baseLayer = lay;
  }

  /** Checks if the name of the Layer passed in is the name of the acting baselayer
   *  @param lay The Layer to be compared
   *  @return If the name of the Layer passed in matches the name of the baselayer
   */
  boolean isActingBaseLayer (Layer lay) {
    if ((lay==null) || (lay.name==null)) return false;
    if (baseLayer != null) return lay.name.equals(baseLayer.name);
    KeyVector layers = this.getLayers();
    Layer layer1 = (layers!=null) ? (Layer)layers.get(1) : null;
    if (layer1==null) return false;
    return lay.name.equals(layer1.name);
  }

  /** This will return the plot's layers.
      @return The plot's layers.
   */
  public KeyVector getLayers () { return layers; }

  /** This will return the plot's features.
      @return The plot's features.
   */
  public KeyVector getFeatures () { return features; }

  /** Utility to find the closest feature to the current middle mouse button click
   *  and is within 2.5 percent of the plot away from the point.
   *  Note - This is using the pixel locations vs the rwc
   *  @return The feature that is closest to MMB
   *  @since NeXtMidas 3.9.3
   */
  public Feature getSelectedFeature () {
    if (currentMMBClickedLocation == null) return null;
    MPoint pix1 = new MPoint(currentMMBClickedLocation.x,currentMMBClickedLocation.y);
    int[] pixLoc = new int[] {pix1.x,pix1.y};
    Feature closest = null;
    double closestDist = Double.MAX_VALUE;
    double maxDist = distanceBetween2DPoints(new int[] {ix1, iy1},new int[] {ix2,iy2}) * FEATURE_SELECT_TOLERANCE;

    for(int i = 0; i< features.size(); i++) {
      Feature currFeature = (Feature) features.get(i);
      double distance = distanceBetween2DPoints(pixLoc,currFeature.getPixelXY());
      if (distance < closestDist) {
        closest = currFeature;
        closestDist = distance;
      }
    }
    boolean within = (closest != null) ? closest.isAtPix(pix1, -1, -1) : false;
    return ((closestDist < maxDist) || within) ? closest : null;
  }

  private double distanceBetween2DPoints (int[] p1, int[] p2) {
    int xdelta = p1[0] - p2[0];
    int ydelta = p1[1] - p2[1];
    return Math.sqrt(xdelta*xdelta + ydelta*ydelta);
  }

  /** Gets the current state of whether or not to draw the plot on a Swing panel
      @return the current ate of drawing the plot on a Swing panel
   */
  public boolean isSwing () {
    return isSwing;
  }

  /** Set the Axis index (whether to use Metadata START/DELTA values or START=0, DELTA=1) */
  //public void setIndex (boolean val) { index = val; }

  /** Get the Axis index (whether to use Metadata START/DELTA values or START=0, DELTA=1).
      @return whether to over-ride the START and DELTA axis values from the file
      @since NeXtMidas 2.7.2
   */
  public boolean isAxisIndex () { return axisIndex; }

  /** Set the Axis index (whether to use Metadata START/DELTA values or START=0, DELTA=1).
      @param val whether to over-ride the START and DELTA axis values from the file
      @since NeXtMidas 2.7.2
   */
  public void setAxisIndex ( boolean val) {
    axisIndex = val;
  }

  /** Set the plot view.
      Note: As of 2.7.0, when {@link #setView(int)} is called, it in turn calls
      {@link #setView(int, boolean, boolean)} with  both doReset and forceReset
      set to TRUE. This means that the view will refresh even if the View has not
      changed. If you do not want this behavior, use {@link #setView(int, boolean)}.
      @param arg The view format to use for the plot (see {@link #viewList}).
      @return Returns true if the view has been changed, false if not.
   */
  public boolean setView (int arg) {
    return setView(arg, true, false);
  }

  /** Set the plot view.
      @param arg The view format to use for the plot (see {@link #viewList}).
      @param doReset Set this to false to disable normal zoom and reset operations. This is only
                     set to false when automatically switching between GEODETIC and LATLON
      @return Returns true if the view has been changed, false if not.
   */
  public boolean setView (int arg, boolean doReset) {
    return setView(arg, doReset, false);
  }

  /** Set the plot view.
      @param arg     The view format to use for the plot (see {@link #viewList}).
      @param doReset Set this to false to disable normal zoom and reset operations. This is only
                     set to false when automatically switching between GEODETIC and LATLON.
      @param forceReset Set to true to force the view to be reset even if the view is the same.
                     This is set to true when the method is called through {@link #setView(int)}
                     and false if called via the old two-arg implementation. See Bug 467.
      @return Returns true if the view has been changed, false if the new view is already in use.
      @since NeXtMidas 2.7.0
   */
  public boolean setView (int arg, boolean doReset, boolean forceReset) {
    // if ((arg==view) && (!doReset || (userView == view))) return false; // since 2.7.0: Bug 467. Changed to allow forced resetting of images
    initValues |= I_VIEW;     // since 2.7.0 for Bug 467
    if ((arg==view) && (!doReset || (userView == view)) && (!forceReset)) return false;
    view=arg;
    if (doReset) {
      userView = view;
      unzoom(0, true);
      reset(-1);
    }
         if (view==V_GEODETIC) { if (viewGeo==null) viewGeo = new ViewGeo(this); viewer=viewGeo; }
    else if (view==V_POLAR)    { if (viewPolar==null) viewPolar = new ViewPolar(this); viewer=viewPolar; }
    else if (view==V_XYZ || view==V_3DGRAPH)  {
      if (view3D==null) view3D = new View3D(this);
      viewer=view3D;
      if (view==V_3DGRAPH) {
        // rotate graph to normal XYZ view NXM-3500
        view3D.rotate(new double[] {60,180,110});
      }
    }
    else if (view==V_MERCATOR) { viewMerc = new ViewMercader(this); viewer=viewMerc; }
    else viewer=view2D;
    is2D = (viewer==view2D || viewer==viewMerc);
    xvy = (view>=V_XY && view<=V_IXIY);
    setup();
    for (int i=0; i<layers.size(); i++) ((Layer)layers.get(i)).update();
    // TODO: change the code below to use rescale(boolean) and see if we need the setup() call 2 lines above
    findRange();
    if (doReset) {
      useRange();
      // resetting xUnits & yUnits added for Bug 2494 to reset axis when view changes
      // Switch to using new 4 parameter method refresh=false  - Since 3.6.0 for Bugs 2894 & 2431
      // TODO: Should zUnits be reset too?
      setXYZUnits(-1,-1,this.zUnitsID, false);
    }
//    layerGrid.update();

    if (view != MPlot.V_LATLONCONTINUOUS) extendedRange=false;
    setup();
    refresh();
    viewLast = view;
    return true;
  }

  /** Set the plot view.
      @param arg The view format to use for the plot (see {@link #viewList}).
      @return Returns true if the view has been changed, false if the new view is already in use.
   */
  public boolean setView (String arg) {
    int v = Parser.find(viewList, arg, 0, 0, Parser.E_ERROR); // since 3.3.1: errors if given invalid value
    return setView(v);
  }

  /** Set the plot mode
      @param arg The value for the Mode (see {@link #modeList} )
      @return true is Mode updated
   */
  public boolean setMode (int arg) {
    return setMode(arg, true);
  }

  /** Set the plot mode and (if boolean is true), trigger / force a rescale
      @param arg The value for the Mode (see {@link #modeList} )
      @param rescale Whether or not to trigger / force a rescale
      @return true is Mode updated
      @since NeXtMidas 2.8.1 - added the boolean flag
   */
  public boolean setMode (int arg, boolean rescale) {
    initValues |= I_MODE;
    mode = arg;
    if (mode == modeLast) return false;
    for (int i=0; i<layers.size(); i++) ((Layer)layers.get(i)).update();
    if (rescale) rescale(true);
    modeLast = mode;
    return true;
  }
  /** Set the plot mode.
      Calls {@link #setMode(int, boolean)} with value fo true for rescale
      @param arg The mode you want to set
      @return true is Mode updated
   */
  public boolean setMode (String arg) {
    int m = Parser.find(modeList, arg, 0, 0, Parser.E_ERROR);  // since 3.3.1: errors if given invalid value
    return setMode(m,true);
  }

  /** Rescale and refresh the plot.<br>
      This can be called to force new or updated view bounds to take effect.
      @param doReset reset zoom levels
      @since NeXtMidas 2.8.1
   */
  public void rescale (boolean doReset) {
    findRange();
    if (doReset) { useRange(); reset(-1); }
    //setup();   // JWB: commented this out because it is redundant. useRange() calls setup()
    refresh();
  }

  /** Get the time for animations
      @return time  time for animations
   */
  public double getTime () { return time; }

  /** Set the plot animation time.
      @param arg The new time to use.
      @return <code>true</code> if the time is updated and <code>false</code> if the time
               is not updated because the new time is the same as the existing time.
   */
  public boolean setTime (double arg) {
    initValues |= I_TIME;
    if (arg == time) return false;
    for (int i=0; i<layers.size(); i++) ((Layer)layers.get(i)).setTime(arg);
    time = arg; refresh(); return true;
  }

  /** Set the plot date format. It is better to use setDateFormat(String)
     @param arg Acceptable values can be either a standard Midas format {@link Time}
                or MPlot.DATE_FORMAT_AUTO, MPlot.DATE_FORMAT_DEFAULT
   */
  public void setDateFormat(int arg) {
    dateFormat = arg;
  }

  /** Set the plot date format
   * @param arg  Acceptable formats can be either a standard Midas format {@link Time#formatList}
                 or they can be any of the patterns accepted by the java.text.SimpleDateFormat class.
                 In addition, "AUTO" and "DEFAULT" wil be accepted.
   */
  public void setDateFormat(String arg) {

    if ( "AUTO".equals(arg.toUpperCase())) {
      dateFormat = DATE_FORMAT_AUTO;
    }
    else if ( "DEFAULT".equals(arg.toUpperCase())) {
      dateFormat = DATE_FORMAT_DEFAULT;
    }
    else {
      // Check for standard Midas Time format
      dateFormat = Parser.find(Time.formatList,arg,0);
      if (dateFormat < 0) {
        dateFormat = DATE_FORMAT_SIMPLEDATEFORMAT;
        dateMFormat = MFormat.getDateFormatFor(arg);
      }
    }
  }

  /** Get the date format
   * @return int containing the current dateFormat
   */
  public int getDateFormat() {
    return dateFormat;
  }

  /** Derive and return the appropriate MFormat object
   * @param deriveFormat If true derive the MFormat, otherwise return default MFormat;
   * @param defaultFormat Time format to use if deriveFormat is false.
   * @return see {@link MFormat}
   */
  private MFormat getDateFormat(boolean deriveFormat, int defaultFormat, int dp ) {

    MFormat returnFormat;
    if ( deriveFormat ) {
      if ( dateFormat == DATE_FORMAT_AUTO) {

        /*
         * If time of the visible plot spans across more then one day, then use
         * Standard format, otherwise use Hour-Min-Sec time format.
         */

        // Get YYMMDD format of min and max corrdinates to determine if they are the same day
        String tmin = Time.toString(getTMin(), Time.YMD);
        String tmax = Time.toString(getTMax(), Time.YMD);
        if (tmin.equals(tmax)) {
          returnFormat = MFormat.getDateFormatFor(Parser.get(Time.formatList, Time.HMS),dp);
        }
        else {
          returnFormat = MFormat.getDateFormatFor(Parser.get(Time.formatList, Time.STD),dp);
        }

      }
      else if ( dateFormat == DATE_FORMAT_DEFAULT) {
        returnFormat = MFormat.getDateFormatFor(Parser.get(Time.formatList, defaultFormat),dp);
      }
      else if ( dateFormat == DATE_FORMAT_SIMPLEDATEFORMAT) {
        returnFormat = dateMFormat;
      }
      else {
        returnFormat = MFormat.getDateFormatFor(Parser.get(Time.formatList, dateFormat),dp);
      }
    } else {
      returnFormat = MFormat.getDateFormatFor(Parser.get(Time.formatList, defaultFormat),dp);
    }
    return returnFormat;
  }

  /** Get the mask value for the axis options
      @return mask value for the axis options
      @see #axisList
   */
  public int getAxis () { return axis; }

  /** set the axis flags
      @param arg mask value for the axis options
      @return true if axis flags updated
   */
  public boolean setAxis (int arg) {
    //if (arg == 0) arg = A_DEFAULT;   // BUG 1835 - '0' should be 'NONE', not default
    initValues |= I_AXIS;  // since 2.7.0 Bug 467
    if (arg == axis) return false;
    axis = arg;
    setup();
    refresh();
    return true;
  }

  /** Set the axis flags with a string mask.  The string mask must follow the
      NeXtMidas option convention of separating the mask fields with a pipe
      delimiter (for example: aaa|bbb ...)
      @param arg string mask of axis flags
      @return true if axis flags updated
   */
  public boolean setAxis (String arg) {
    if (arg.equals("MODIFY")) {
      configure("AXIS.CFG");
      return false;
    }
    int flag = 0;
    int i = arg.indexOf('|');
         if (arg.startsWith("DEF")) flag = (A_DEFAULT);
    else if (arg.startsWith("UNL")) flag = (A_UNLABELED);
    else if (arg.startsWith("NUM")) flag = (A_NUMBERED);
    else if (arg.startsWith("FRA")) flag = (A_FRAMED);
    else if (arg.startsWith("TF"))  flag = (A_TFRAME);
    else if (arg.startsWith("BF"))  flag = (A_BFRAME);
    else if (arg.startsWith("TRF")) flag = (A_TRFRAME);
    else if (arg.startsWith("BRF")) flag = (A_BRFRAME);
    else if (arg.startsWith("TLF")) flag = (A_TLFRAME);
    else if (arg.startsWith("BLF")) flag = (A_BLFRAME);

    // if not a combo mode, parse with current as default
    if (flag==0) flag = Parser.mask(axisList,arg,axis);
    // if combo mode | options, parse rest as additions to combo
    else if (i>0) flag = Parser.mask(axisList,"+"+arg.substring(i),flag);
    return setAxis(flag);
  }

  /** Get the value of the int mask for the readout.
      @return int mask for the readout
      @see #readoutList
   */
  public int getReadout () { return readout; }

  /** Get the value of the String mask for the readout.
      @return String mask for the readout
      @see #readoutList
      @since NeXtMidas 3.7.3 for NXM-3158
   */
  public String getReadoutStr () {
    return Parser.mask2s(readoutList, readout);
  }

  /** set the readout flags with mask number.
      @param arg readout option mask
      @return true if readout options has changed
   */
  public boolean setReadout (int arg) {
    return setReadout(arg, READOUT_OPTION_MAIN);
  }

  private boolean setReadout (int arg, int readoutOption) {
    int readout = 0;
    if (readoutOption==READOUT_OPTION_MAIN) readout = this.readout;
    else if (readoutOption==READOUT_OPTION_EXT) readout = this.readoutExt;
    initValues |= I_READOUT;
    boolean anyChanges;
    int axisFlag = readout&R_ONAXIS;
    //if (arg == 0) arg = R_DEFAULT;  // BUG 1835 - '0' should be 'NONE', not default
    if (arg == readout) {
      anyChanges = false;
    }
    else {
      // If Scientific is turned off, make sure the values show on the axis (Bug 2740 - 3.3.3)
      if ((readout&R_SCIENTIFIC)!=0  && (arg&R_SCIENTIFIC)==0 && (axis&O_AUTOROTATE)==0 ) {
        setAxis("+autorotate");
      }

      // Set the readout format shown to user on status or title bar...
      if ((readout&R_EP)==R_EP) {   // currently in ExtPrec mode
        if ((arg&R_EP)!=R_EP) {     // turn off ExtPrec mode
          setPrecision(3);
        }
      } else {                      // currently not in ExtPrec mode
        if ((arg&R_EP)==R_EP) {     // turn on ExtPrec mode
          setPrecision(6);
        }
      }
      readout = arg;
      anyChanges = true;
      if (readoutOption==READOUT_OPTION_MAIN) this.readout = readout;
      else if (readoutOption==READOUT_OPTION_EXT) this.readoutExt = readout;
    }

    //if user toggled onAxis Readout, refresh display
    if ((readout&R_ONAXIS)!=axisFlag && this.status != INIT)  hardRefresh();
    formatReadout(0);
    return anyChanges;
  }

  /** Set the readout flags with string mask.  For example: x|y|dx|dy).
      @param arg string mask containing readout flags
      @return true if readout options have changed
      @see #readoutList
   */
  public boolean setReadout (String arg) {
    return setReadout(Parser.mask(readoutList,arg,readout), READOUT_OPTION_MAIN);
  }

  /** Set the extended readout flags with string mask.  For example: x|y|dx|dy).
      @param arg string mask containing extended readout flags
      @return true if extended readout options have changed
      @see #readoutListExt
   */
  public boolean setReadoutExt (String arg) {
    return setReadout(Parser.mask(readoutListExt, arg,readoutExt), READOUT_OPTION_EXT);
  }

  /** Get the value of the int mask for the extended readout.
      (includes point count readout on right hand side)
      @return int mask for the extended readout
      @see #readoutListExt
      @since NeXtMidas 3.9.4
  */
  public int getReadoutExt () {
    return readoutExt;
  }

  /** set the readout format with a JAVA style format string. DEFAULT/default indicates
      to reset to the default string.  If the format string is illegal, it is left as is.
      @param formatString JAVA style format string to set the readout format
      @return true if format string was okay
   */
  public boolean setReadoutFormat (String formatString) {
    boolean ok = true;
    if (formatString.equalsIgnoreCase("default")) {
        setPrecision(3);    //Set to the default
    } else {
      try {
        nf =  new DecimalFormat(formatString);
      } catch (IllegalArgumentException e) {
        ok = false; // Leave format as is
      }
    }
    return ok;
  }

  /**
   * Gets current dRange units setting
   * @return int specifying units displayed by dRange
   * @see #getDRangeUnits()
   */
  public int getDRangeUnitsInt () { return dRangeUnits; } // since 3.3.1 to provide user workaround for change of return type to getDRangeUnits()
  /**
   * Gets current dRange units setting.
   * @return String specifying units displayed by dRange
   * @since 2.7.2
   * */
  public String getDRangeUnits () { // in 3.3.1: changed to return String to be more user friendly and consistent
    return Parser.get(drangeunitsList, dRangeUnits);
  }

  /** Set the units displayed by dRange. Note: This method does not call refresh.
      @param units The name of the units must be in {@link #drangeunitsList}
      @return true if successfully set units
      @since NeXtMidas 2.7.1
   */
  public boolean setDRangeUnits (int units) {
    boolean ok = false;
    if (units > 0) {
      dRangeUnits = units;
      ok = true;
    }
    return ok;
  }

  /** Set the units displayed by dRange.
      @param units The name of the units must be in {@link #drangeunitsList}
      @return true if successfully set units
      @since NeXtMidas 2.7.1
   */
  public boolean setDRangeUnits (String units) {
    int iUnits = Parser.find(drangeunitsList, units, 0, 0, Parser.E_ERROR); // since 3.3.1: errors if given invalid value
    return setDRangeUnits (iUnits);
  }

  /** get the precision (number of places after decimal point) used in readout.
      @return precision (number of places after decimal point)
   */
  public int getPrecision () {
    return precision;
  }

  /** set the precision (number of places after decimal point) used in readout.
      @param prec precision (number of places after decimal point)
      @return true if precision is changed - Since 3.7.0
   */
  public boolean setPrecision (int prec) {
    if (precision == prec) return false; // Since 3.7.0
    precision = prec;
    if (prec == 6) readout |= R_EP;
    else           readout &= (~R_EP);
    if (prec <= 0) nf = new DecimalFormat("###0");
    else {
      String tmp = "###0.";
      while (prec-- > 0) tmp += "#";
      nf = new DecimalFormat(tmp);
    }
    return true;
  }

  /** set the scale flags
      @param arg scale flags
      @return true if scale flags changed
   */
  public boolean setScale (int arg) {
    initValues |= I_SCALE;
    if (arg == scale) return false;
    scale = arg;
    setup();
    refresh();
    return true;
  }

  /** Set the scale of the plot based on a String value.
      @param arg String form of scale setting. Supports anything in {@link #scaleList} or the words:
      <pre>
      FIX   : removes any autoscaling (same as "NONE")
      FULL  : sets to {@link #S_MIN}|{@link #S_MAX} (same as {@link #S_AUTO})
      AUTO  : sets to {@link #S_AUTO}
      RANGE : sets {@link #S_RANGE}
      </pre>
      @return true if scale flags changed
   */
  public boolean setScale (String arg) {
    int flag;
         if (arg.length()==0    ) flag = scale;
    else if (arg.equals("FIX"  )) flag = 0;
    else if (arg.equals("FULL" )) flag = S_MIN|S_MAX;
    else if (arg.equals("AUTO" )) flag = S_AUTO;  // since 2.9.1 for Bug 1519 (same as FULL)
    else if (arg.equals("RANGE")) flag = S_RANGE; // since 2.9.1: changed from S_MAX|S_MAXRANGE to S_RANGE for Bug 1519
    else                          flag = Parser.mask(scaleList,arg,scale);
    return setScale(flag);
  }

  /** Set the {@link #scale} mask to include {@link #S_MAXRANGE} and call {@link #setMaxRange(double)}
      @param arg The value to set it to
      @return True if MaxRange is updated, false if the values were the same
   */
  public boolean setRange (double arg) {
    if ((scale&(S_MAXRANGE|S_MAXZRANGE))==0) scale |= S_MAXRANGE;
    return setMaxRange(arg);
  }

  /** Get the max range used for autoscaling
      @return maximum range
   */
  public double getMaxRange () { return maxRange; }

  /** Sets maximum range for autoscaling.
      @param arg maximum range
      @return True if it updated the Max Range, False if the values were the same
   */
  public boolean setMaxRange (double arg) {
    if (Tolerance.equals(arg,maxRange)) return false;  // using Tolerance.equals since 3.7.1 for NXM-2937
    maxRange = arg;
    return true;
  }

  /** Get the min range used for autoscaling
      @return    minimum range
   */
  public double getMinRange () { return minRange; }

  /** Sets minimum range for autoscaling.
      @param arg minimum range
      @return True if MinRange is updated, False if the values were the same
   */
  public boolean setMinRange (double arg) {
    if (Tolerance.equals(arg,minRange)) return false;  // using Tolerance.equals since 3.7.1 for NXM-2937
    minRange = arg;
    return true;
  }

  /** apply real-time auto-scaling for specified layers.
      @param mask  mask of layers to include in autoscale (-1 for all layers)
      @param eval  true to invoke {@link Layer#findRange()} for those that layers included in autoscaling
      @return true if rescaled
    */
  public boolean autoScale (int mask, boolean eval) {
    double  a1      = 0;
    double  a2      = 0;
    boolean zaxis   = false;
    boolean rescale = false;
    Layer   lay;

    for (int n=1; n<layers.size(); n++) {
      lay = (Layer)layers.get(n);
      if (lay==null || (scale&lay.scaleable)==0 || (mask&(1<<n))==0) continue;
      if (eval) lay.findRange();
      if (n==1) { a1=lay.a1; a2=lay.a2; }
      else { a1=Math.min(a1,lay.a1); a2=Math.max(a2,lay.a2); }
      zaxis |= lay.zaxis;
    }

    // NOTE: this matches logic in useRange()
    if ((scale&S_RANGE)==S_RANGE) {                         // bound a1 by both MaxRange & MinRange
      if (a2-a1>maxRange) a1 = a2-maxRange;
      if (a2-a1<minRange) a1 = a2-minRange;
    }
    else if ((scale&S_RANGE)==S_MAXRANGE) a1 = a2-maxRange; // bound a1 by MaxRange
    else if ((scale&S_RANGE)==S_MINRANGE) a2 = a1+minRange; // bound a2 by MinRange

    if (zaxis) {
      double r = 0.05*(orz2-orz1);
      if ((scale&(S_MAX|S_RANGE))==0) {
        // not auto scaling on max or range
      } else if ((scale & S_NOAVERAGE)!= 0) { // follow layer averaging directly (no additional) - since 3.7.0 for bug 2901
        rz2 = orz2=a2;     rescale=true;
      } else if (a2 > orz2) {
        rz2 = orz2 = a2+r; rescale=true;
      } else if (a2 < orz2-r*2) {
        rz2 = orz2 = a2+r; rescale=true;
      } else if (!Tolerance.equalsWithTolerance(rz2, orz2)) { // since 3.3.2 to fix autoscale issue (BUG 2734) - using Tolerance.equals since 3.7.1 for NXM-2937
        rz2 = orz2;        rescale=true;
      }
      if ((scale&(S_MIN|S_RANGE))==0) {
        // not auto scaling on min or range
      } else if ((scale & S_NOAVERAGE)!= 0) { // follow layer averaging directly (no additional) - since 3.7.0 for bug 2901
        rz1 = orz1=a1;     rescale=true;
      } else if (a1 < orz1) {
        rz1 = orz1 = a1-r; rescale=true;
      } else if (a1 > orz1+r*2) {
        rz1 = orz1 = a1-r; rescale=true;
      } else if (!Tolerance.equalsWithTolerance(rz2, orz2)) { // since 3.3.2 to fix autoscale issue for BUG 2734 using Tolerance.equals since 3.7.1 for NXM-2937
        rz1 = orz1;        rescale=true;
      }
      if (rz1 == rz2) {         // since 3.3.2 matches logic in useRange() for BUG 2734
        // adjust values by the greater of 1 or Math.ulp - since 3.7.1 for NXM-2892
        double zUlp=Math.ceil(Math.ulp((float)rz1));
        rz1 -= zUlp; rz2 += zUlp;
      }

    } else { // y-axis
      boolean xmargin = (rx2 > orx2 + 0.01); // check with tolerance - 4.0.0 new tolerance
      double r = 0.05*(ory2-ory1); // 5% of current range
      if ((scale&(S_MAX|S_RANGE))==0) {
        // not auto scaling on max or range
      } else if (a2 > ory2) {     // 1. increase max
        ry2 = ory2 = a2+r; rescale=true;
      } else if (a2 < ory2-r*2) { // 2. reduce max: new max is less than (ory2 - 2r)
        ry2 = ory2 = a2+r; rescale=true;
      } else if ((ry2 > ory2 + 0.00001) && xmargin) {
        // 3. Do not rescale just based on a y axis with a margin in a Map View.
        //    Rescale is needed if visible y axis does not match original and the view is
        //    a non-map view or there is a visible y margin and a x margin.
        //    (logic updated in 3.7.1 for NXM-2937 and updated in 3.9.3 (flickering issue)) - NXM-3579
        ry2 = ory2;        rescale=true;
      }
      if ((scale&(S_MIN|S_RANGE))==0) {
        // not auto scaling on min or range
      } else if (a1 < ory1) {     // 1. reduce min
        ry1 = ory1 = a1-r; rescale=true;
      } else if (a1 > ory1+r*2) { // 2. increase min: new min is greater than (ory1 + 2r)
        ry1 = ory1 = a1-r; rescale=true;
      } else if ((ry1 < ory1 - 0.00001) && xmargin) {
        // 3. Do not rescale just based on a y axis% with a margin in a Map View.
        //    Rescale is needed if visible axis does not match original and the view is
        //    a non-map view or there is a visible y margin and a x margin.
        //    (logic updated in 3.7.1 for NXM-2937 and again in 3.9.3 (flickering issue)) - NXM-3579
        ry1 = ory1;        rescale=true;
      }
      if (ry1 == ry2) {  // since 3.3.2 matches logic in useRange() for BUG 2734 3.7.1 NXM-2892
        // adjust values by the greater of 1 or Math.ulp - since 3.7.1 for NXM-2892
        double yUlp=Math.ceil(Math.ulp((float)ry1));
        ry1 -= yUlp; ry2 += yUlp;
        // FYI: not updating ory1/2 since that causes the y-axis to jump constantly for horizontal line
      }
    }
    rescale = rescale && !zaxis && level==0;
    if (rescale || (view==V_3DGRAPH)) setup(); // 3dgraph needs to go through setup so that it does not start stretching off screen.
    return rescale;
  }

  /** set the options flags
      @param arg options flags (int)
      @return true if options were changed
   */
  public boolean setOptions (int arg) {
    if (arg == options) return false;
    if (checkForCut(arg)) return true;
    if ((arg&O_BSTORE)!=0) {    // Warning added for Bug 1516 (updated through Bug 1399 Aug 2012)
      for (int i=1; i<layers.size(); i++) {
        Layer lay = (Layer)layers.get(i);
        if (lay.realtime && lay.isEnabled(-1)) {
          Shell.warning("Backing Store not recommended for Realtime (piped) data due to potential Graphics artifacts! lay = "
            + lay + " enable = " + lay.getEnableString());
        }
      }
    }
    options = arg;
    if (((options&O_DBUFFER)==0) && !(Theme.getOption(Theme.REALTIME))) {
      // Warn when image double-buffering is turned off - since 3.8.1 for NXM-3213
      Shell.warning("Turning off image double buffering when running with regular "+
                    "Swing graphics may cause graphical lock-up or sluggishness - "+
                    "options are now set to: "+getOptions()+" for "+title);
    }
    setup(); return true;
  }

  /**
   * Helper function to return a list of the options that are set for MPlot
   * @return Pipe separated list of the MPlot options that are set.
   * @since NeXtMidas 3.8.1
   */
  private String getOptions() {
    return Parser.mask2s(optionsList, options);
  }

  /**
   * Will check to see if the options will be overriding previously set options. Will send a warning message is it is.
   * @param arg Pipe separated list of options that are being added or removed
   * @since NeXtMidas 3.8.1
   */
  private void validateOptions(String arg) {
    String[] arr = arg.split("\\|");
    for(String value : arr) {
      if(value.length()<1) continue;
      if(!value.substring(0, 1).matches("(\\+|\\-|\\~).*")) {
        // And previous options were turned off during override
        if ((Parser.mask(optionsList,arg,options)&options) != options) {
          Shell.warning("MPlot.setOptions: Setting options without +|-|~, overriding previous option of "+getOptions()+" with "+arg);
        }
        break;
      }
    }
  }

  /** set the options flags
      @param arg options flags as String
      @return true if options were changed
   */
  public boolean setOptions (String arg) {
    if ((warningsMask & OVERRIDING_EXISTING_PLOT_OPTIONS) != 0) validateOptions(arg);  // 3.8.1 allow option of having warnings when options are overridden
    return setOptions(Parser.mask(optionsList,arg,options));
  }

  /** Check whether a given option is set (in {@link #options} )
      @param mask option to check if set (in mask form)
      @return true if given option is set
   */
  public boolean getOption (int mask) {
    return ((options&mask)!=0);
  }

  /** Check to see if the user has selected a Cut message from the Options menu. If so,
      send the appropriate Cut message to plot. Should only be called by setOptions(arg).
      @param arg The option in int form
      @return True if it sends a message to plot, false otherwise
      @since NeXtMidas 2.7.4 (moved code block from setOptions(int), where it was added in 2.7.3)
   */
  private boolean checkForCut (int arg) {
    boolean isXCut = (arg & O_XCUT) != 0;
    boolean isYCut = (arg & O_YCUT) != 0;
    if (isXCut || isYCut) {
      Message msg = null;
      Table data = new Table("{IX="+px+", IY="+py+"}");
      if (isXCut)    { msg = new Message ("XCUT",0,data ); }
      else if (isYCut) { msg = new Message ("YCUT",0,data ); }
      if (msg != null) { mh.processMessage(msg);  return true; }
    }
    return false;
  }

  @Override
  public void setBounds (int x, int y, int w, int h) {
    super.setBounds (x,y,w,h);
    if (level==0) useRange();
    setup();
  }

  /** Take an input String key and convert to uppercase and convert MIN to 1 and MAX to 2
      @param key The input
      @return "X1", "X2", "Y1", "Y2", "Z1", "Z2", or the original value (if invalid input)
      @since NeXtMidas 3.1.0 (Bug 2504)
   */
  private String translateLimitsKey (String key) {
    String res = key.toUpperCase();

    if (res.equals("X1")||res.equals("X2")||res.equals("Y1")||res.equals("Y2")||res.equals("Z1")||res.equals("Z2")) {
      return res; // nothing to do
    }

    if      (res.endsWith("MIN")) res = res.charAt(0)+"1";
    else if (res.endsWith("MAX")) res = res.charAt(0)+"2";
    else                          res = key;  // unknown so revert to original

    return res;
  }

  /** Update the userBounds table, which keeps track of changes to the
      outer plot bounds, as set by the user
      @param key Must be X1, X2, Y1, Y2, Z1, Z2
      @param value Double containing the sticky value
      @since NeXtMidas 2.7.3
   */
  public void setOuterLimits (String key, double value) {
    //key = key.toUpperCase();
    key = translateLimitsKey(key);
    setUserLimits(key, value, true);
  }

  /** Update the outerLimits table, which keeps track of changes to the
      outer plot bounds, as set by the user. Keys must be in  X1, X2, Y1, Y2, Z1, Z2
      @param tbl outerLimits table
      @since NeXtMidas 2.7.3
   */
  public void setOuterLimits (Table tbl) {
    // this method re-written in 3.1.0 to facilitate Bug 2478
    for (String key : tbl.getKeys()) {
      //key = key.toUpperCase();
      key = translateLimitsKey(key);
      setOuterLimits(key,tbl.getD(key));
    }
  }

  /** Get the table containing the user-set 'sticky' bounds
      @return outerLimits table
   */
  public Table getOuterLimits () { return outerLimits; } // restored in 3.9.4

  /** Get the current outer bounds, which may have been shifted
   *  if in LatLonContinuous mode
   *  @return current outer bounds as table
   */
  public Table getOuterBounds () {
    Table outerBounds = new Table();
    outerBounds.put("X1",orx1);
    outerBounds.put("X2",orx2);
    outerBounds.put("Y1",ory1);
    outerBounds.put("Y2",ory2);
    return outerBounds;
   }

  /** Get a single value from the sticky (persistent) bounds values set by the user
      @param key Must be X1, X2, Y1, Y2, Z1, Z2
      @return The value (if it exists)
      @since NeXtMidas 2.7.3
   */
  public double getOuterLimits (String key) { return outerLimits.getD(key); }

  /** Update the viewLimits table, which keeps track of changes to the
      plot viewport, as set by the user
      @param key Must be X1, X2, Y1, Y2, Z1, or Z2
      @param value Double containing the bounds value
      @since NeXtMidas 2.7.3
  */
  public void setViewLimits (String key, double value) {
    //key = key.toUpperCase();
    key = translateLimitsKey(key);
    setUserLimits(key, value, false);
  }

  /** Update the viewLimits table, which keeps track of changes to the
      outer plot bounds, as set by the user. Keys must be in  X1, X2, Y1, Y2, Z1, Z2
      @param tbl viewLimits table
      @since NeXtMidas 2.7.3
   */
  public void setViewLimits (Table tbl) {
    // this method re-written in 3.1.0 to facilitate Bug 2478
    for (String key : tbl.getKeys()) {
      //key = key.toUpperCase();
      key = translateLimitsKey(key);
      setViewLimits(key,tbl.getD(key));
    }
  }

  /** Gets the table containing the user-set 'view' bounds
      @return viewLimits table
      @since NeXtMidas 3.9.0
  */
   public Table getViewLimits () { return viewLimits; }

  /** Get a single value form the v bounds values set by the user
      @param key X1, X2, Y1, Y2, Z1, or Z2
      @return The value (if it exists)
      @since NeXtMidas 2.7.3
   */
  public double getViewLimits (String key) { return viewLimits.getD(key); }

  /** Set the bounds of the plot
      @param key Must be X1, X2, Y1, Y2, Z1, or Z2
      @param value the value to set to
      @param outer Whether or not to apply these changes to all files opened after
             the current file (TRUE), or just to this file only (FALSE)
      @since NeXtMidas 2.7.3
   */
  public void setUserLimits (String key, double value, boolean outer) {
    //key = key.toUpperCase();
    key = translateLimitsKey(key);

    // ensure the bounds are inclusive (Bug 2478)
    if (key.equals("X1")  || key.equals("Y1") ) {
      value -= axisBuffer;
    } else if (key.equals("X2")  || key.equals("Y2")) {
      value += axisBuffer;
    }

    if (key.equals("X1")  || key.equals("X2")  || key.equals("Y1")  || key.equals("Y2")  ||
        key.equals("Z1")  || key.equals("Z2")) {
      if (outer) { outerLimits.put(key, value); }
      else       { viewLimits.put(key, value);  }
    } else {
      Shell.warning("Invalid bounds identifier ["+key+"]. Must be in "+
      "X1, X2, Y1, Y2, Z1, Z2, XMIN, XMAX, YMIN, YMAX, ZMIN, or ZMAX.");
    }
  }

  /** Remove a particular key from the outer or viewport bounds Table.
      This is done so that the value is not applied when rescaling or opening files.
      @param key X1, X2, Y1, Y2, Z1, or Z2
      @param outer TRUE = outer limit, FALSE = viewport limit
      @since NeXtMidas 2.7.3
   */
  public void removeLimit (String key, boolean outer) {
    //key = key.toUpperCase();
    key = translateLimitsKey(key);

    if (key.equals("X1")  || key.equals("X2")  || key.equals("Y1")  || key.equals("Y2")  ||
        key.equals("Z1")  || key.equals("Z2")) {
      if (outer) { outerLimits.remove(key); }
      else       { viewLimits.remove(key);   }
    } else {
      Shell.warning("Invalid bounds identifier ["+key+"]. Must be in "+
      "X1, X2, Y1, Y2, Z1, Z2, XMIN, XMAX, YMIN, YMAX, ZMIN, or ZMAX.");
    }
  }

  /** Load features from a file.
      @param filename The name of the file.
    */
    public void loadLimits (String filename) {
      FileName fnFilename = new FileName(filename,FNCase.KeepCase);
      TextFile tf = new TextFile(fnFilename);
      Table t = new Table();
      t.fromTextFile(tf);
      loadLimits(t);
    }

/** Load features from a table.
    @param table The table of features.
  */
  public void loadLimits (Table table) {
    if (table == null) return;
    Object outerBoundsTbl = table.get("OUTERBOUNDS");
    Object viewLimitsTbl = table.get("VIEWLIMITS");
    if (outerBoundsTbl instanceof Table) {
      setOuterLimits((Table)outerBoundsTbl);
      applyUserBounds("OUTER");
    }
    if (viewLimitsTbl instanceof Table) {
      setViewLimits((Table)viewLimitsTbl);
      applyUserBounds("VIEW");
    }
  }

/** Save features to a file.
    @param filename The name of the file.
    @since NeXtMidas 3.9.0
  */
  public void saveLimits (String filename) {
    if (filename.indexOf('.')<0) filename += ".tbl";   // add .tbl if no extension given (Bug 2263)

    Table lt = new Table();
    lt.put("OUTERBOUNDS", getOuterBounds());
    lt.put("VIEWLIMITS", viewLimits);

    FileName fnFilename = new FileName(filename,FNCase.KeepCase);
    TextFile tf = new TextFile(fnFilename);
    lt.toTextFile(tf);
    Shell.info("Saved OUTERBOUNDS and VIEWLIMITS to: "+tf.getURL());
  }

  /** Get the outer (also referred to as 'Original' or 'File')
      XMin (also referred to as 'X1')
      @return outer XMin
      @see #orx1
   */
  public double getOuterXMin () { return orx1; }

  /** Get the outer (also referred to as 'Original' or 'File')
      XMax (also referred to as 'X2' or 'ORX2')
      @return outer XMax
      @see #orx2
   */
  public double getOuterXMax () { return orx2; }

  /** Set the value to add to the axis in order to show values that would fall exactly on the MIN/MAX
      @param value value to add to the axis in order to show values that would fall exactly on the MIN/MAX
      @since NeXtMidas 3.1.0
   */
  public void setAxisBuffer (double value) { axisBuffer = value; }  // Bug 2478

  /** Get the value to add to the axis in order to show values that would fall exactly on the MIN/MAX
      @return value to add to the axis in order to show values that would fall exactly on the MIN/MAX
      @since NeXtMidas 3.1.0
   */
  public double getAxisBuffer () { return axisBuffer; }   // Bug 2478

  /** Set the plot X bounds min
      @param r1 The min value
      @since NeXtMidas 1.9.5
  */
  public void setXMin (double r1) {
    setXBounds(r1,r1+rx2-rx1, true, false); // changed to add booleans for Bug 2248
  }

  /** Set the plot X bounds min
      @param r1 The min value
      @see #setXMin(double)
   */
  public void setX1 (double r1) { setXMin(r1); }

  /** Set the plot X bounds max keeping the current XMin
      @param r2 The max value
      @since NeXtMidas 1.9.5
  */
  public void setXMax (double r2) { setXBounds(rx1,r2); }

  /** Set the plot X bounds max keeping the current XMin
      @param r2 The max value
      @see #setXMax(double)
   */
  public void setX2 (double r2) { setXMax(r2); }

  /** Sets the Limits of the X Range.
      If given in the wrong order (r1&gt;r2) the values will be swapped automatically.
      @param r1 The min X limit
      @param r2 The max X limit
   */
  public void setXBounds (double r1, double r2) {
    setXBounds(r1, r2, true, true); // changed to add booleans for Bug 2248
  }

  /** Sets the Limits of the X Range.
      If given in the wrong order (r1>r2) the values will be swapped automatically.
      @param r1 The min X limit
      @param r2 The max X limit
      @param applyX1 Whether or not to apply the X1 value to the User bounds table
      @param applyX2 Whether or not to apply the X2 value to the User bounds table
      @since NeXtMidas 2.9.0 (Bug 2248)
   */
  private void setXBounds (double r1, double r2, boolean applyX1, boolean applyX2) {
    /* Default min and max are checked in case the user wants to keep the default bounds, no matter the file data */
    if (r1==rx1 && rx1!=DEF_MIN_BOUNDS && r2==rx2 && rx2!=DEF_MAX_BOUNDS) return;
    double tmprx1 = Math.min(r1,r2);
    double tmprx2 = Math.max(r1,r2);

    // Set the values and update the temporary user bounds table
    if (tmprx1 == DEF_MIN_BOUNDS || !Tolerance.equals(tmprx1,rx1)) { // using Tolerance.equals since 3.7.1 for NXM-2937
      rx1 = tmprx1;
      if (applyX1) setUserLimits("X1", rx1, false);
    }
    if (tmprx2 == DEF_MAX_BOUNDS || !Tolerance.equals(tmprx2,rx2)) { // using Tolerance.equals since 3.7.1 for NXM-2937
      rx2 = tmprx2;
      if (applyX2)  setUserLimits("X2", rx2, false);
    }

    checkBounds("RX", true);  // check for equality (Bug 2479)

    setup();
    refresh();
  }

  /** Get the limits of the viewable area for X.
      @return A double array containing {xmin, xmax}
      @since NeXtMidas 2.7.3
   */
  public double[] getXBounds () {
    double[] xview = new double[2];
    xview[0] = rx1;
    xview[1] = rx2;
    return xview;
  }

  /** Set the plot X bounds min and max
      @param r1 The min X limit
      @param r2 The max X limit
      @deprecated Since NeXtMidas 1.9.5 Use the {@link #setXBounds(double,double)} method
   */
  @Deprecated public void setX (double r1, double r2) {
    setXBounds(r1, r2);
  }

  /** Get the plot X bounds min
      @return plot X bounds min
      @since NeXtMidas 1.9.5
   */
  public double getXMin () { return rx1; }

  /** Get the plot X bounds min
      @return plot X bounds min
      @see #getXMin()
   */
  public double getX1 () { return getXMin(); }

  /** Get the plot X bounds max
      @return plot X bounds max
      @since NeXtMidas 1.9.5
   */
  public double getXMax () { return rx2; }

  /** Get the plot X bounds max
      @return plot X bounds max
      @see  #getXMax()
    */
  public double getX2 () { return getXMax(); }

  /** set the Y Min
      @param r1 plot Y bounds min
   */
  public void setY1 (double r1) { setYMin(r1); }
  /** set the Y Min
      @param r1 plot Y bounds min
   */
  public void setYMin (double r1) { setYBounds(r1,r1+ry2-ry1,true,false); } // changed to add booleans for Bug 2248
  /** Set the Y Max Bounds
      @param r2 plot Y bounds max
   */
  public void setY2 (double r2) { setYMax(r2); }
  /** Set the Y Max Bounds
      @param r2 plot Y bounds max
    */
  public void setYMax (double r2) { setYBounds(ry1,r2); }

  /** Set the plot Y outer bounds min and max
      @param r1 The min Y limit
      @param r2 The max Y limit
      @deprecated Since 2.7.2, use the {@link #setYBounds(double, double)} method
   */
  @Deprecated public void setY (double r1, double r2) { setYBounds(r1,r2); }

  /** Sets the Limits of the Y Range. If given in the wrong order (r1&gt;r2) the values will be
      swapped automatically.
      @param r1 The min Z limit
      @param r2 The max Z limit
   */
  public void setYBounds (double r1, double r2) {
    setYBounds(r1,r2,true,true);  // changed to add booleans for Bug 2248
  }

  /** Sets the Limits of the Y Range. If given in the wrong order (r1&gt;r2) the values will be
      swapped automatically.
      @param r1 The min Z limit
      @param r2 The max Z limit
      @param applyY1 Whether or not to apply YMin to {@link #viewLimits}
      @param applyY2 Whether or not to apply YMax to {@link #viewLimits}
      @since NeXtMidas 2.9.0
   */
  private void setYBounds (double r1, double r2, boolean applyY1, boolean applyY2) {
    /* Default min and max are checked in case the user wants to keep the default bounds, no matter the file data */
    if (r1==ry1 && ry1!=DEF_MIN_BOUNDS && r2==ry2 && ry2!=DEF_MAX_BOUNDS) return;
    double tmpry1 = Math.min(r1,r2);
    double tmpry2 = Math.max(r1,r2);

    // Set the values and update the temporary user bounds table
    if (tmpry1 == DEF_MIN_BOUNDS || !Tolerance.equals(tmpry1,ry1)) { // using Tolerance.equals since 3.7.1 for NXM-2937
      ry1 = tmpry1;
      if (applyY1) setUserLimits("Y1", ry1, false);
    }
    if (tmpry2 == DEF_MAX_BOUNDS || !Tolerance.equals(tmpry2,ry2)) { // using Tolerance.equals since 3.7.1 for NXM-2937
      ry2 = tmpry2;
      if (applyY2) setUserLimits("Y2", ry2, false);
    }

    checkBounds("RY", true);

    setup();
    refresh();
  }

  /** Get the Limits of Y viewable area. Note: this is different than {@link #getYBounds()}, which
      returns the outer Y limits.
      @return A doubel array containing {ymin,ymax} of the viewable area
      @since NeXtMidas 2.7.3
   */
  public double[] getYBounds () {
    double[] yview = new double[2];
    yview[0] = ry1;
    yview[1] = ry2;
    return yview;
  }

  /** get the Y Min
      @return plot Y bounds min
   */
  public double getY1 () { return getYMin(); }
  /** get the Y Min
      @return plot Y bounds min
   */
  public double getYMin () { return ry1; }
  /** get the Y Max
      @return plot Y bounds max
   */
  public double getY2 () { return getYMax(); }
  /** get the Y Max
      @return plot Y bounds max
   */
  public double getYMax () { return ry2; }

  /** get the time Min bounds
      @return time Min bounds
   */
  public double getT1 () { return getTMin();  }

  /** get the time Min bounds
      @return time Min bounds
   */
  public double getTMin () {
    double t = ry1;
    Layer lay = getBaseLayer();  // since 3.5.0
    if (lay!=null) t = lay.getTime(ix1,iy1);
    return t;
  }

  /** get the time Max bounds
      @return time Max bounds
   */
  public double getT2 () { return getTMax(); }

  /** get the time Max bounds
      @return time Max bounds
   */
  public double getTMax () {
    double t = ry2;
    Layer lay = getBaseLayer();
    if (lay!=null) t = lay.getTime(ix2,iy2);
    return t;
  }

  /** Set the viewable area
      @param x1 min X for viewable area
      @param x2 max X for viewable area
      @param y1 min Y for viewable area
      @param y2 max Y for viewable area
   */
  public void setXYBounds (double x1, double x2, double y1, double y2) {
    setXYBounds(x1, x2, y1, y2, true);
  }

  /** Set the viewable area. If the min and max values are flipped, this will correct them first.
      @param x1 min X for viewable area
      @param x2 max X for viewable area
      @param y1 min Y for viewable area
      @param y2 max Y for viewable area
      @param refresh if true do a refresh after the values are updated.
   */
  public void setXYBounds (double x1, double x2, double y1, double y2, boolean refresh) {
    // get the values in the correct order
    double tmprx1 = Math.min(x1,x2);
    double tmprx2 = Math.max(x1,x2);
    double tmpry1 = Math.min(y1,y2);
    double tmpry2 = Math.max(y1,y2);

    // These values will be used to set the view limits
    double tmprx1ForBounds = tmprx1;
    double tmprx2ForBounds = tmprx2;
    double tmpry1ForBounds = tmpry1;
    double tmpry2ForBounds = tmpry2;
    // If we are in lat lon continuous, the view limits will need to be the rx or the orx values based on what is actually visible.
    // TODO - make this the only way it works.
    if (view == V_LATLONCONTINUOUS) {
      tmprx1ForBounds = Math.max(tmprx1ForBounds, orx1);
      tmprx2ForBounds = Math.min(tmprx2ForBounds, orx2);
      tmpry1ForBounds = Math.max(tmpry1ForBounds, ory1);
      tmpry2ForBounds = Math.min(tmpry2ForBounds, ory2);
    }

    /* Set the values and update the temporary user bounds table. Default min and max are checked
       in case the user wants to keep the default bounds, no matter the file data */
    if (tmprx1 == DEF_MIN_BOUNDS || !Tolerance.equals(rx1,tmprx1)) { // using Tolerance.equals since 3.7.1 for NXM-2937
      rx1 = tmprx1; setUserLimits("X1", tmprx1ForBounds, false);
    }
    if (tmprx2 == DEF_MAX_BOUNDS || !Tolerance.equals(rx2,tmprx2)) { // using Tolerance.equals since 3.7.1 for NXM-2937
      rx2 = tmprx2; setUserLimits("X2", tmprx2ForBounds, false);
    }
    if (tmpry1 == DEF_MIN_BOUNDS || !Tolerance.equals(ry1,tmpry1)) { // using Tolerance.equals since 3.7.1 for NXM-2937
      ry1 = tmpry1; setUserLimits("Y1", tmpry1ForBounds, false);
    }
    if (tmpry2 == DEF_MAX_BOUNDS || !Tolerance.equals(ry2,tmpry2)) { // using Tolerance.equals since 3.7.1 for NXM-2937
      ry2 = tmpry2; setUserLimits("Y2", tmpry2ForBounds, false);
    }

    checkBounds("RX", true);  // check for equality (Bug 2479)
    checkBounds("RY", true);  // check for equality (Bug 2479)

    if (refresh) {
      setup();
      refresh();
    }
  }

  /** Set the XY bounds, if the min and max values are flipped, this will correct them first. This
      method will ALWAYS refresh the plot when called.
      @param x1 min X for viewable area
      @param x2 max X for viewable area
      @param y1 min Y for viewable area
      @param y2 max Y for viewable area
   */
  public void setXY (double x1, double x2, double y1, double y2) {
    setXYBounds(x1, x2, y1, y2, true);
  }

  /** Set the XY bounds, if the min and max values are flipped, this will correct them first. If
      refresh is false then the plot will NOT be refreshed after the values are updated.
     @param x1 min X for viewable area
      @param x2 max X for viewable area
      @param y1 min Y for viewable area
      @param y2 max Y for viewable area
      @param refresh if true do a refresh after the values are updated.
   */
  public void setXY (double x1, double x2, double y1, double y2, boolean refresh) {
    setXYBounds(x1, x2, y1, y2, refresh);
  }

  /** Get the XY bounds
      @return An array with { x1, x2, y1, y2 }
   */
  public double[] getXY () { return getXYBounds(); }

  /** Get the XY bounds
      @return An array with { x1, x2, y1, y2 }
   */
  public double[] getXYBounds () {
    double[] nbox = new double[4];
    nbox[0] = rx1;
    nbox[1] = rx2;
    nbox[2] = ry1;
    nbox[3] = ry2;
    return nbox;
  }

  /** Get the XY bounds
      @param gx1 x min
      @param gx2 x max
      @param gy1 y min
      @param gy2 y max
      @return An array with { x1, x2, y1, y2 }
   */
  public double[] getXYGrid (double gx1, double gx2, double gy1, double gy2) {
    double ax1=rx1, ax2=rx2, ay1=ry1, ay2=ry2;
    if (view==V_LATLON || view==V_MERCATOR) {
      ax1=Math.max(ax1,-180);
      ax2=Math.min(ax2, 180);
      ay1=Math.max(ay1,-90);
      ay2=Math.min(ay2, 90);
    }
    else if (view==V_LATLONCONTINUOUS) { // NXM 3.9.0 for NXM-3284
      ax1=Math.max(ax1, orx1);
      ax2=Math.min(ax2, orx2);
      ay1=Math.max(ay1,-90);
      ay2=Math.min(ay2, 90);
    }
    else if (view==V_GEODETIC) {
      Position p;
      p = getPosition (ix1,iy1);
      ax1=ax2=p.lon;
      ay1=ay2=p.lat;
      p = getPosition (ix2,iy1);
      ax1=Math.min(ax1,p.lon); ax2=Math.max(ax2,p.lon);
      ay1=Math.min(ay1,p.lat); ay2=Math.max(ay2,p.lat);
      p = getPosition (ix2,iy2);
      ax1=Math.min(ax1,p.lon); ax2=Math.max(ax2,p.lon);
      ay1=Math.min(ay1,p.lat); ay2=Math.max(ay2,p.lat);
      p = getPosition (ix1,iy2);
      ax1=Math.min(ax1,p.lon); ax2=Math.max(ax2,p.lon);
      ay1=Math.min(ay1,p.lat); ay2=Math.max(ay2,p.lat);
      int ixh = (ix1+ix2)/2, iyh=(iy1+iy2)/2;
      p = getPosition (ixh,iy1);
      ax1=Math.min(ax1,p.lon); ax2=Math.max(ax2,p.lon);
      ay1=Math.min(ay1,p.lat); ay2=Math.max(ay2,p.lat);
      p = getPosition (ixh,iy2);
      ax1=Math.min(ax1,p.lon); ax2=Math.max(ax2,p.lon);
      ay1=Math.min(ay1,p.lat); ay2=Math.max(ay2,p.lat);
      p = getPosition (ix1,iyh);
      ax1=Math.min(ax1,p.lon); ax2=Math.max(ax2,p.lon);
      ay1=Math.min(ay1,p.lat); ay2=Math.max(ay2,p.lat);
      p = getPosition (ix2,iyh);
      ax1=Math.min(ax1,p.lon); ax2=Math.max(ax2,p.lon);
      ay1=Math.min(ay1,p.lat); ay2=Math.max(ay2,p.lat);
      if (ax2-ax1 > 180) {  // got a pole
        ax1=-180; ax2=180;
        //double ay = (ay1+ay2)/2;
        if (viewGeo.lat>0) {
          ay2=90;
        } else {
          ay1=-90;
        }
      }
    }
    // intersect with gridable region
    double[] gridBox = new double[4];
    gridBox[0] = Math.max(gx1,ax1);
    gridBox[1] = Math.min(gx2,ax2);
    gridBox[2] = Math.max(gy1,ay1);
    gridBox[3] = Math.min(gy2,ay2);

    return gridBox;
  }

  /** set the Z minimum bound
   *  @param r1 the Z minimum bound */
  public void setZ1 (double r1) { setZMin(r1); }
  /** set the Z minimum bound
   *  @param r1 the Z minimum bound */
  public void setZMin (double r1) {
    setZBounds(r1,r1+rz2-rz1,true,false);  // changed to add booleans for Bug 2248
  }
  /** set the Z maximum bound
   *  @param r2 the Z maximum bound */
  public void setZ2 (double r2) { setZMax(r2); }
  /** set the Z maximum bound
   *  @param r2 the Z maximum bound */
  public void setZMax (double r2) {
    setZBounds(rz1,r2,false,true);
  }

  /** Sets the Limits of the Z Range.
      If given in the wrong order (r1&gt;r2) the values will be swapped automatically.
      @param r1 The min Z limit
      @param r2 The max Z limit
  */
  public void setZBounds (double r1, double r2) {
    setZBounds(r1,r2,true,true);  // changed to add booleans for Bug 2248
  }

  /** Sets the Limits of the Z Range.
      If given in the wrong order (r1>r2) the values will be swapped automatically.
      @param r1 The min Z limit
      @param r2 The max Z limit
      @param applyZ1 Whether or not to add the ZMin to {@link #viewLimits}
      @param applyZ2 Whether or not to add the ZMax to {@link #viewLimits}
      @since NeXtMidas 2.9.0. (bug 2248)
  */
  private void setZBounds (double r1, double r2, boolean applyZ1, boolean applyZ2) {
    double tmprz1 = Math.min(r1,r2);
    double tmprz2 = Math.max(r1,r2);

    // Default min and max are checked in case the user wants to keep
    // the default bounds, no matter the file data
    // (Note: using Tolerance.equals since 3.7.1 for NXM-2937)
    if (tmprz1 == DEF_MIN_BOUNDS || !Tolerance.equals(rz1,tmprz1)) { rz1 = tmprz1; setUserLimits("Z1", rz1, false); }
    if (tmprz2 == DEF_MAX_BOUNDS || !Tolerance.equals(rz2,tmprz2)) { rz2 = tmprz2; setUserLimits("Z2", rz2, false); }

    // Instruct the layers to use MPlot's new Z values
    for (int i=1; i<layers.size(); i++) {
      Layer l = ((Layer)layers.get(i));
      if (applyZ1) l.setUseMPlotZMin(true);
      if (applyZ2) l.setUseMPlotZMax(true);
    }

    setup();
    refresh();
  }

  /** Get the limits of the viewable Z area. Note: This is different than {@link #getZBounds()},
      which returns the outer Z bounds.
      @return A double array containing {zMin, zMax} of the viewable area
      @since NeXtMidas 2.7.3
   */
  public double[] getZBounds () {
    double[] zview = new double[2];
    zview[0] = rz1;
    zview[1] = rz2;
    return zview;
  }

  /** set the Z bounds for minimum and maximum
      @param r1 minimum z
      @param r2 maximum z
      @deprecated Since 2.7.3, use the {@link #setZBounds(double, double)} method
   */
  @Deprecated public void setZ (double r1, double r2) { setZBounds(r1,r2); }

  /** Get the minimum real world Z bound.
   *  @return the minimum real world Z bound */
  public double getZ1 () { return getZMin(); }
  /** Get the minimum real world Z bound.
   *  @return the minimum real world Z bound */
  public double getZMin () { return rz1; }
  /** Get the maximum real world Z bound.
   *  @return the maximum real world Z bound */
  public double getZ2 () { return getZMax(); }
  /** Get the maximum real world Z bound.
   *  @return the maximum real world Z bound */
  public double getZMax () { return rz2; }

  /** Set whether or not to align the Layers by the XStart of the base layer.
      Note: This does not call refresh().
      <br><br>
      As of NeXtMidas 3.1.2 this only applies to time-base type 1000 plots.
      @param val true if should align the Layers by the XStart of the base layer
      @since NeXtMidas 3.1.2
   */
  public void setTimeAlign (boolean val) {
    for (int i=2; i<getLayers().getSize(); i++) {  // start at 2 to skip grid and base layer, which don't need this set
      Layer lay = (Layer)getLayers().get(i);
      if (lay!=null) lay.setTimeAlign(val);
    }
  }

  /** Get whether the plot is aligned by the XStart of the base Layer.
      As of NeXtMidas 3.1.2, this only applies to time-based type 1000 plots.
      @return true if plot is aligned
      @since NeXtMidas 3.1.2
   */
  public boolean isTimeAlign () {
    if (getLayers().getSize()<3) return false;  // only one layer + grid
    for (int i=2; i<getLayers().getSize(); i++) {
      Layer lay = (Layer)getLayers().get(i);
      if (lay.isTimeAlign()) return true;
    }
    return false;
  }

  /** Set the axis divisions
      @param x  x axis divisions
      @param y  y axis division
      @param z  z axis division
   */
  public void setDiv (int x, int y, int z) {
    setAxisDiv(x,y,z);
  }
  /** Set the axis divisions
      @param x  x axis divisions
      @param y  y axis division
      @param z  z axis division
   */
  public void setAxisDiv (int x, int y, int z) {
    ndx = x; ndy = y; ndz = z;
  }

  /** Set the frame size
      @param frame  frame size
   */
  public void setFS (int frame) { setFrameSize(frame); }

  /** Set the frame size
      @param frame  frame size
   */
  public void setFrameSize (int frame) {
    Layer lay = getBaseLayer();
    if (lay==null) return;
    for (int i=0; i<layers.size(); i++) {
      lay = (Layer)layers.get(i);
      lay.setFrame(frame);
    }
    refresh();
  }

  /** Set the axis delta of MPlot and its main layers by rate change
      @param freq  frequency
   */
  public void setRXD (double freq) { setRXDelta(freq); }

  /** Set the axis delta of MPlot and its main layers by rate change.
      This method calls {@link #setXDelta(double)} with an input of the
      one-half the freq/frame size.
      @param freq  frequency
   */
  public void setRXDelta (double freq) {
    Layer lay = getBaseLayer();
    if (lay==null) return;
    int frame = lay.frame & 0xFFFFFFFE; // round down to nearest EVEN frame size
    setXDelta(freq/2/frame);
  }

  /** Set the axis start of MPlot and its main layers. This method calls {@link #setXMin(double)}
      and then iterates over all of the layers calling their {@link Layer#setXAxis(double,double)}
      methods.
      @param value new x axis start value
      @see #setXMin(double)
   */
  public void setXStart (double value) {
    if (layers==null) {
      Shell.warning("No Layer plotted. Cannot set XStart.");
      return;
    }
    Layer lay = getBaseLayer();
    if (lay==null) return;
    unzoom(0);

    // call to setXMin below changed to setXBounds(..) to better keep track of user desires (Bug 2248)
    //setXMin(value);
    setXBounds(value,value+Math.abs(rx2-rx1),true,false);
    for (int i=0; i<layers.size(); i++) {
      lay = (Layer)layers.get(i);
      lay.setXAxis (value,lay.xdelta);
    }
    refresh();
  }

  /** Get the X Axis Start from the base layer.
      @return X axis start for the base layer
   */
  public double getXStart () {
    if (layers==null || getBaseLayer()==null) {
      Shell.warning("MPlot: No Layer plotted. Cannot get XStart value. Returning NaN");
      return Double.NaN;
    }
    return getBaseLayer().getXStart();
  }

  /** Get the X Axis Start for the given layer.
      @param layer index of layer to get the X axis start
      @return X axis delta for the given layer
   */
  public double getXStart (int layer) {
    return ((Layer)layers.get(layer)).getXStart();
  }

  /** Set the axis start of MPlot and its main layers. This method calls {@link #setY1(double)}
      and then iterates over all of the layers calling their {@link Layer#setYAxis(double,double)}
      methods. When done it refreshes the plot.
      @param value y axis start
      @see #setYMin(double)
      @since NeXtMidas 2.7.1
  */
  public void setYStart (double value) {
    Layer lay = getBaseLayer();
    if (lay==null) return;
    unzoom(0);
    // call to setYMin() below changed to call setYBounds(..) to keep better track of user desires (Bug 2248)
    //setYMin(value);
    setYBounds(value,value+Math.abs(ry2-ry1),true,false);
    for (int i=0; i<layers.size(); i++) {
      lay = (Layer)layers.get(i);
      lay.setYAxis (value,lay.ydelta);
    }
    refresh();
  }

  /** Get the Y Axis Start from the base layer.
      @return Y axis delta for the base layer
   */
  public double getYStart () {
    return getBaseLayer().getYStart();
  }

  /** Get the Y Axis Start for the given layer.
      @param layer index of layer to get the Y axis start
      @return Y axis delta for the given layer
   */
  public double getYStart (int layer) {
    return ((Layer)layers.get(layer)).getYStart();
  }

  /** Set the axis start of MPlot and its main layers. This method calls {@link #setZ1(double)}
      and then iterates over all of the layers calling their {@link Layer#setZAxis(double,double)}
      methods. When done it refreshes the plot.
      @param value z axis start
      @see #setZMin(double)
      @since NeXtMidas 2.7.1
  */
  public void setZStart (double value) {
    Layer lay = getBaseLayer();
    if (lay==null) return;
    unzoom(0);
    setZMin(value);
    for (int i=0; i<layers.size(); i++) {
      lay = (Layer)layers.get(i);
      lay.setYAxis (value,lay.zdelta);
    }
    //refresh();
    needRefresh = true;
  }

  /** Get the Z Axis Start from the base layer.
      @return Z axis start for the base layer
   */
  public double getZStart () {
    return getBaseLayer().getZStart();
  }

  /** Get the Z Axis Start for the given layer.
      @param layer index of layer to get the Y axis start
      @return Z axis start for the give layer
   */
  public double getZStart (int layer) {
    return ((Layer)layers.get(layer)).getZStart();
  }

  /** Set the axis delta of MPlot and its main layers by delta change.
      This method will also set the bounds of the plot to be XMin = (Layer XStart)
      and XMax = (XMin + XDelta*Frame Size). <br>
      Note: The bounds updates that take place in this method do not get applied
      to the {@link #outerLimits} and {@link #viewLimits} user bounds tables
      because it is not "evident" that the user explicitly wanted to do so.
      @param delta x axis value
   */
  public void setXDelta (double delta) {
    Layer lay = getBaseLayer();
    if (lay==null) return;
    unzoom(0);
    int frame = lay.frame & 0xFFFFFFFE; // round down to nearest EVEN frame size
    double xd = delta;
    double xs = lay.xstart;
    if (xs<0) xs = -xd*frame/2;
    orx1=rx1=xs;
    orx2=rx2=rx1 + xd*lay.frame;
    setup();
    for (int i=0; i<layers.size(); i++) ((Layer)layers.get(i)).setXAxis(xs,xd);
    refresh();
  }

  /** Get the X Axis Delta for the base layer.
      @return X axis delta for the base layer
   */
  public double getXDelta () {
    return getBaseLayer().getXDelta();
  }

  /** Get the X Axis Delta for the given layer.
      @param layer index of layer to get the X axis delta
      @return X axis delta for the given layer
   */
  public double getXDelta (int layer) {
    return ((Layer)layers.get(layer)).getXDelta();
  }

  /** Get the plot's current real world coordinate (RWC) X and Y bounds as a DragBox.
      This is the same as <code>getZoomBounds(getZoomLevel())</code>.
      @return DragBox with the current real world bounds of the plot
      @since NeXtMidas 2.5.0
   */
  public final DragBox getZoomBounds () {
    return getZoomBounds(level);
  }

  /** Get the plot's real world coordinate (RWC) X and Y bounds for a given zoom level as a DragBox.
      @param zoomLevel desired zoom level to retrieve the bounds of
      @return DragBox with the current real world bounds of the plot, or null if zoomLevel is invalid.
      @since NeXtMidas 2.5.0
   */
  public DragBox getZoomBounds (int zoomLevel) {
    DragBox boxObj = null;
    if (zoomLevel == level) {
      boxObj = new DragBox(rx1, rx2, ry1, ry2);
    } else if ( (zoomLevel >= 0) && (zoomLevel > level) ) {
      int i = level*LEVPARAM;
      boxObj = new DragBox(rstk[i+0], rstk[i+1], rstk[i+2], rstk[i+3]);
    }
    return boxObj;
  }

  /** Return current zoom level. There are no corresponding setter method, use
      {@link #zoom(Object, int)}, {@link #unzoom()}, or {@link #unzoom(int)}
      to change the zoom level.
      @return current zoom level (0 if not zoomed in).
      @since NeXtMidas 2.5.0
   */
  public int getZoomLevel () { return level; }

  /** Resize the plot area */
  public void resize () {
    reset(-1);
    resizing=true;
    setup();
    resizing=false;
    refresh();

    // This is done to fix the cutoff of text and graphical menu anomalies when not having an MW since 3.8.1 NXM-3213
    if(MW!=null) {
      MW.panel.revalidate();
    }
    else {
      panel.revalidate();
    }
  }

  /** Re-calculate the axis regions. */
  @Override
  public void setup () {
    viewer.setup();
    ix1 = 0;
    ix2 = pos.w;
    iy1 = 0;
    iy2 = pos.h;
    oarbox.setBounds(0,pos.h-th,pos.w,th);
    if ((axis&G_SHOW) != 0) {
      if ((axis&(L_AXIS|L_NUMS|L_LABS)) != 0) ix1 += 6; // allow space for labels
      if ((axis&(R_AXIS|R_NUMS|R_LABS)) != 0) ix2 -= 6;
      if ((axis&(T_AXIS|T_NUMS|T_LABS)) != 0) iy1 += 6;
      if ((axis&(B_AXIS|B_NUMS|B_LABS)) != 0) iy2 -= 6;
      if ((axis&L_NUMS) != 0) ix1 += tw*4;
      if ((axis&L_LABS) != 0) ix1 += th+4;
      if ((axis&R_NUMS) != 0) ix2 -= tw*4;
      if ((axis&R_LABS) != 0) ix2 -= th;
      if ((axis&T_NUMS) != 0) iy1 += th;
      if ((axis&T_LABS) != 0) iy1 += th;
      if ((axis&B_NUMS) != 0) iy2 -= th;
      if ((axis&B_LABS) != 0) iy2 -= th;
      // Leave enough room at bottom of plot to display onaxis readout
      if ((readout&R_ONAXIS) != 0) iy2 -= th;
    }
    ix21 = ix2-ix1;
    iy21 = iy2-iy1;
//JGS, why are we double book keeping along with ix1,iy1,ix21,iy21? databox.setBounds(ix1,iy1,ix21,iy21);
    setAspect();
    // x' = mxx*x + mxb     x = (x'-mxb)/mxx
    if (xvy) {
      myy = (ix2-ix1) / (ry2 - ry1);
      myb = ix1 - ry1*myy;
      if (view==V_XIY||view==V_IXIY) { myy = -myy; myb = ix2 - ry1*myy; }
      mxx = (iy1-iy2) / (rx2 - rx1);
      mxb = iy2 - rx1*mxx;
      if (view==V_IXY||view==V_IXIY) { mxx = -mxx; mxb = iy1 - rx1*mxx; }
    } else {
      // since 3.9.0 for NXM-3284 - allow limit to be less than -180
      double p = .12;  // precision
      if (view == MPlot.V_LATLONCONTINUOUS) {
        if (((rx1 < LONGITUDE_DEFAULT_MIN-p) ||
            (rx2 > LONGITUDE_DEFAULT_MAX+p)) && (view == MPlot.V_LATLONCONTINUOUS)) {
          extendedRange = true;
        }
        // since 3.9.2 for NXM-3417, adjust bounds so proper map section renders, rather than blank margin area
        if (((rx1 < orx1-p) || (rx2 > orx2+p))) {
          // going beyond current sliding 360 range
          if ((rx2 - rx1) < MPlot.LONGITUDE_RANGE - p) {
            // we should be rendering the whole area
            // we may need to shift to keep within our continuous range
            if (rx1 < MPlot.LONGITUDE_CONTINUOUS_MIN - p) {
              rx1 = orx1 = rx1 + MPlot.LONGITUDE_RANGE;
              rx2 = rx2 + MPlot.LONGITUDE_RANGE;
              orx2 = rx1 + MPlot.LONGITUDE_RANGE; // full range above adjusted rx1
            }
            else if (rx2 > MPlot.LONGITUDE_CONTINUOUS_MAX + p) {
              rx2 = orx2 = rx2 - MPlot.LONGITUDE_RANGE;
              rx1 = rx1 - MPlot.LONGITUDE_RANGE;
              orx1 = rx2 - MPlot.LONGITUDE_RANGE; // full range below adjusted rx2
            }
            // otherwise we may need to just shift out outerlimits
            else if (rx1 < orx1-p) {
              orx1 = rx1;
              orx2 = orx1 + MPlot.LONGITUDE_RANGE;
            }
            else if (rx2 > orx2+p) {
              orx2 = rx2;
              orx1 = orx2 - MPlot.LONGITUDE_RANGE;
            }
          }
          else if ((orx2-orx1) < (rx2-rx1-p)) {
            // Here the view width is greater than or equal to 360 (i.e. longitude
            // range), as we did not go into the if block
            // When map outer bounds width is less than the view width, we
            // need to center the visible map within the view bounds. This will
            // mean that the outer bounds need to be adjusted based on this center.
            // View bounds might also need to be adjusted, so bounds are within
            // continuous longitude "bounds" - since 4.1.3 for NXM-3981 - thanks Jeremy!
            double midX = (rx2+rx1)/2;
            if (midX < 0) {
              midX += MPlot.LONGITUDE_RANGE;
              rx1  += MPlot.LONGITUDE_RANGE;
              rx2  += MPlot.LONGITUDE_RANGE;
            }
            else if (midX > (MPlot.LONGITUDE_CONTINUOUS_MAX-MPlot.LONGITUDE_DEFAULT_MAX)) {
              midX -= MPlot.LONGITUDE_RANGE;
              rx1  -= MPlot.LONGITUDE_RANGE;
              rx2  -= MPlot.LONGITUDE_RANGE;
            }
            orx1 = midX + MPlot.LONGITUDE_DEFAULT_MIN;
            orx2 = midX + MPlot.LONGITUDE_DEFAULT_MAX;
          }
        }
            }
      mxx = (ix2-ix1) / (rx2 - rx1);
      mxb = ix1 - rx1*mxx;

      if (view==V_YIX||view==V_IYIX) { mxx = -mxx; mxb = ix2 - rx1*mxx; }
      myy = (iy1-iy2) / (ry2 - ry1);
      myb = iy2 - ry1*myy;
      if (view==V_IYX||view==V_IYIX) { myy = -myy; myb = iy1 - ry1*myy; }
    }
    mzz = (iz2-iz1) / (rz2 - rz1);
    mzb = iz1 - rz1*mzz;

    formatReadout(0);
    // calculate H&V scroll bar regions
    double sxm = ((rx1+rx2)*.5-frx1)/(frx2-frx1);
    double sxd = ((rx2-rx1)*.5)/(frx2-frx1);
    double sym = ((ry1+ry2)*.5-fry1)/(fry2-fry1);
    double syd = ((ry2-ry1)*.5)/(fry2-fry1);
    if (xvy) {
      isxd=(int)(syd*ix21); isyd=(int)(sxd*iy21);
      if (view==V_XIY||view==V_IXIY) isx=ix2-(int)(sym*ix21); else isx=ix1+(int)(sym*ix21);
      if (view==V_IXY||view==V_IXIY) isy=iy1+(int)(sxm*iy21); else isy=iy2-(int)(sxm*iy21);
    } else {
      isxd=(int)(sxd*ix21); isyd=(int)(syd*iy21);
      if (view==V_YIX||view==V_IYIX) isx=ix2-(int)(sxm*ix21); else isx=ix1+(int)(sxm*ix21);
      if (view==V_IYX||view==V_IYIX) isy=iy1+(int)(sym*iy21); else isy=iy2-(int)(sym*iy21);
    }
    isxd = Math.max(isxd,10); isyd = Math.max(isyd,10);
    scroll = 0;
    Layer lay = getBaseLayer();
    if (lay!=null && lay.isPaged(1) && ((orx1!=frx1) || (orx2!=frx2))) scroll |= (xvy)? 2:1;
    if (lay!=null && lay.isPaged(2) && ((ory1!=fry1) || (ory2!=fry2))) scroll |= (xvy)? 1:2;
    if (level>0 || zoomed) scroll = 3; // level>0 means zoomed via box, 'zoomed' means zoomed via keyboard/mouse
    reset(level);
    if (canvas!=null) canvas.needUpdate();    // update the items on the canvas (since 2.9.2)
  }

  /**
   *  Set Aspect Ratio - primary use is for map views (including when leaving a map view)
   */
  public void setAspect () {
    double xx,yy, ratio, d1,d2;
    if ((options&O_NEARINT)!=0) {
      double pdelta,rdelta;
      Layer lay = getBaseLayer();
      if (lay==null) return; // too early
      rdelta = Math.abs(lay.xdelta);
      pdelta = Math.abs((rx2-rx1)/(ix2-ix1));
      ratio  = rdelta/pdelta;
      if (ratio>=1) pdelta = rdelta / Math.floor(rdelta/pdelta);   // expansion
      else          pdelta = rdelta * Math.floor(pdelta/rdelta+1); // compression
      rx2 = rx1 + (ix2-ix1)*pdelta*((rx2>rx1)?1:-1);
      rdelta = Math.abs(lay.ydelta);
      pdelta = Math.abs((ry2-ry1)/(iy2-iy1));
      ratio  = rdelta/pdelta;
      if (ratio>=1) pdelta = rdelta / Math.floor(rdelta/pdelta);   // expansion
      else          pdelta = rdelta * Math.floor(pdelta/rdelta);   // compression
      ry2 = ry1 + (iy2-iy1)*pdelta*((ry2>ry1)?1:-1);
    }
    if (view<V_XYZ) return; // If view is anything but a map view, there is no more to do

    //*** Handle Aspect Ratios on Map Views ***********************************

    // When resizing, the calculation of percentage of the y-axis filled needs the smallest
    // visible y axis value in terms of Latitude, so save that off before switching to the
    // cylindrical projection value for Mercator views - Since 3.7.1 for NXM-2919
    double lat_ry1 = ry1;
    double lat_ry2 = ry2;

    // Determine the dimension that is having the greater change proportionally
    // ratio > 1 means greater y expansion than x, ratio <1 means greater x expansion than y
    if (view==V_MERCATOR) {              // Use Mercator cylindrical projection value for ratio to match visible axis on plot
      ry1 = ViewMercader.lat2mer(ry1);
      ry2 = ViewMercader.lat2mer(ry2);
    }
    xx = (ix2-ix1) / (rx2 - rx1); // actual x values / visible x axis values
    yy = (iy1-iy2) / (ry2 - ry1); // actual y values / visible y axis values
    ratio  = -yy/xx;              // greater than 1 means getting larger in y direction proportional to change in x direction
    // cover extreme resizing cases (no visible axis) 0/0, etc.
    if (Double.isNaN(ratio)) ratio = 1;
    ratio = Math.max(-Double.MAX_VALUE, ratio);
    ratio = Math.min(Double.MAX_VALUE, ratio);
    if (ratio == 0) ratio = Double.MIN_VALUE;

    if (!resizing) {              // initial plot set-up, not resizing
      if (ratio>1.01) {
        d1  = (ry1+ry2)/2;
        d2  = (ry2-ry1)/2;
        ry1 = d1-d2*ratio;
        ry2 = d1+d2*ratio;
      }
      else if (ratio<0.99) {
        d1 = (rx1+rx2)/2;
        d2 = (rx2-rx1)/2;
        rx1 = d1-d2/ratio;
        rx2 = d1+d2/ratio;
      }
    }
    else {                        // resizing plot

      // Resize the axes such than the entire plot is displayed, even if that means there
      // is an extra margin (i.e. the plot is "oversized") in one direction to make the
      // full other dimension fit. If the plot has had its outer limits altered or a zoom
      // has taken place, then resizing is done such that those bounds always display and
      // extra margin/map is shown if needed.  (Scale the vertical and horizontal axis
      // based on the smallest dimension) - Since 3.7.1 for NXM-2919
      //
      // The chart below shows what actions need to be taken based on the ratio (i.e. which
      // dimension is expanding greater proportionally) and whether there is excess margin
      // on either the x or y axis.  If both dimensions need adjustment, then the ratio
      // is effectively split between a y portion to apply to the y axis and an x portion
      // to apply to the x axis..
      // _______________________________________________________________________________
      // |                       ||  x axis oversized  |  y axis oversized  | balanced  |
      // |                       || (has extra margin) | (has extra margin) |           |
      // |-----------------------||--------------------|--------------------|-----------|
      // |-----------------------||--------------------|--------------------|-----------|
      // |                       || Total change can   |                    |           |
      // |                       || be accomplished by |                    | expand    |
      // |                       || modifying x axis   |                    | y axis    |
      // | ratio>1.01 (i.e. y    || IF ratio less than | Only modify y axis | only      |
      // | dimension  expanding  || 1/xPercentFilled   | (y dimension just  | (results  |
      // | more than x dimension || (i.e. only need to | gets an even       | in y axis |
      // | - taller or narrower) || reduce x margin),  | larger margin)     | with      |
      // |                       || OTHERWISE both x & |                    | extra     |
      // |                       || y need adjustment. |                    | margin)   |
      // |-----------------------||--------------------|--------------------|-----------|
      // |                       ||                    | Total change can   |           |
      // |                       ||                    | be accomplished by | expand    |
      // |                       ||                    | modifying x axis   | x axis    |
      // | ratio<0.99 (i.e. x    || Only modify x axis | IF ratio less than | only      |
      // | dimension expanding   || (x dimension just  | yPercentFilled     | (results  |
      // | more than y dimension || gets an even       | (i.e. only need to | in x axis |
      // | - shorter or wider)   || larger margin)     | reduce y margin)   | with      |
      // |                       ||                    | OTHERWISE both y & | extra     |
      // |                       ||                    | x need adjustment. | margin)   |
      // |_____________________________________________|____________________|___________|
      //

      // Determine the bounds that need to be visible (Full, Outer Limits, or View/Zoom Limits)
      // get the outer limits (if separate outer limits were set) or otherwise the "full" limits of the plot
      double outrMap_rx1 = (outerLimits.containsKey("X1")) ? outerLimits.getD("X1") : frx1;
      double outrMap_rx2 = (outerLimits.containsKey("X2")) ? outerLimits.getD("X2") : frx2;
      double outrMap_ry1 = (outerLimits.containsKey("Y1")) ? outerLimits.getD("Y1") : fry1;
      double outrMap_ry2 = (outerLimits.containsKey("Y2")) ? outerLimits.getD("Y2") : fry2;
      // determine portion of map that needs to remain visible (zoomed in portion or outer/full limits)
      double vMap_rx1 = (viewLimits.containsKey("X1")) ? viewLimits.getD("X1") : outrMap_rx1;
      double vMap_rx2 = (viewLimits.containsKey("X2")) ? viewLimits.getD("X2") : outrMap_rx2;
      double vMap_ry1 = (viewLimits.containsKey("Y1")) ? viewLimits.getD("Y1") : outrMap_ry1;
      double vMap_ry2 = (viewLimits.containsKey("Y2")) ? viewLimits.getD("Y2") : outrMap_ry2;

      // Get the Range that need to be visible and the percent of the current axis that is filled with that range
      double xFullRange  = vMap_rx2-vMap_rx1;
      double yFullRange  = vMap_ry2-vMap_ry1;
      // percent of x axis "filled" (rest is extra margin due to that dimension being oversized)
      // (Note: if "right" border has been moved to the left of the "left" left border, x is filled)
      boolean visibleXAxis   = ix2 > ix1;
      boolean xAxisPositive  = rx2 > rx1 && visibleXAxis;
      double  xPercentFilled = xAxisPositive ? xFullRange/(rx2-rx1) : 1;
      // percent of y axis "filled" (rest is extra margin due to that dimension being oversized)
      boolean visibleYAxis   = iy2 > iy1;
      boolean yAxisPositive = lat_ry2 > lat_ry1  && visibleYAxis;
      double  yPercentFilled = yAxisPositive ? yFullRange/(lat_ry2-lat_ry1) : 1;
      // if one of the axis visible range is 0 or less, the actual plot will not be rendered
      boolean bothAxesVisible = visibleXAxis && visibleYAxis;

      if (yPercentFilled > 0.99) yPercentFilled = 1;  // doubles are not stored exactly, so allow a margin
      if (xPercentFilled > 0.99) xPercentFilled = 1;  // doubles are not stored exactly, so allow a margin
      boolean yOversized = yPercentFilled < 1;        // y dimension is oversized (has extra margin) if not completely filled
      boolean xOversized = xPercentFilled < 1;        // x dimension is oversized (has extra margin) if not completely filled

      // Split the ratio into a y and an x portion, if both axes need adjustment due to the resizing
      double ratio_y_portion = 1;
      double ratio_x_portion = 1;
      if (Math.abs(ratio) > 1.001 && bothAxesVisible) {
        // getting larger in y direction proportional to change in x direction
        // (take absolute value to account for cases where "Max" boundary is less than "Min" boundary)
        if (ratio < 1/xPercentFilled) {
          ratio_x_portion = ratio;        // all the adjustment made in getting rid of excess x margin
          ratio_y_portion = 1;            // no adjustment
        }
        else if (ratio >= 1/xPercentFilled && xPercentFilled > 0){
          ratio_x_portion = 1/xPercentFilled;
          ratio_y_portion = ratio / xPercentFilled;
        }
      }
      else if (Math.abs(ratio) < 0.999 && bothAxesVisible) {
        //getting larger in x direction proportional to change in y direction
        // (take absolute value to account for cases where "Max" boundary is less than "Min" boundary)
        if (ratio > yPercentFilled) {
          ratio_y_portion = ratio;        // all the adjustment made in getting rid of excess x margin
          ratio_x_portion = 1;            // no adjustment
        }
        else if (ratio < yPercentFilled){
          ratio_y_portion = yPercentFilled;
          ratio_x_portion = ratio / yPercentFilled;
        }
      }
      else if (!bothAxesVisible  || ratio < 0) {
        ratio_x_portion = ratio; //default to adjusting only x axis if the plot is not visible
      }

      // Adjust the x and y axis as needed (see chart above)
      if (ratio_y_portion>1.01 || (yOversized && (ratio_y_portion<0.99))) { // Note: the 1.01 and 0.99 are because doubles are not stored as exact values
        d1  = (ry1+ry2)/2;
        d2  = (ry2-ry1)/2;
        if (d2 == 0) d2 = Double.MIN_VALUE;  // allow recovery after y-axis display of 0
        ry1 = d1-d2*ratio_y_portion;
        ry2 = d1+d2*ratio_y_portion;
        // limit values on cases where one axis is collapsed to the point it is no longer visible
        ry1 = Math.max(-Double.MAX_VALUE/2, ry1);
        ry1 = Math.min(Double.MAX_VALUE/2, ry1);
        ry2 = Math.min(Double.MAX_VALUE/2, ry2);
        ry2 = Math.max(-Double.MAX_VALUE/2, ry2);
        if ((ry1 <= ory1) && (ry2 >= ory2)) { // since 3.9.3 add safeguard against unbalanced y margins NXM-3597 3.9.3
          double margin = ((ory1-ry1) + (ry2-ory2))/2;
          ry1 = ory1 - margin;
          ry2 = ory2 + margin;
        }
      }
      if (ratio_x_portion<0.99 || (xOversized && ratio_x_portion>1.01)) { // Note: the 1.01 and 0.99 are because doubles are not stored as exact values
        d1 = (rx1+rx2)/2;
        d2 = (rx2-rx1)/2;
        if (d2 == 0) d2 = Double.MIN_VALUE;  // allow recovery after x-axis display of 0
        rx1 = d1-d2/ratio_x_portion;
        rx2 = d1+d2/ratio_x_portion;
        // limit values on cases where one axis is collapsed to the point it is no longer visible
        rx1 = Math.max(-Double.MAX_VALUE/2, rx1);
        rx1 = Math.min(Double.MAX_VALUE/2, rx1);
        rx2 = Math.min(Double.MAX_VALUE/2, rx2);
        rx2 = Math.max(-Double.MAX_VALUE/2, rx2);
        if ((rx1 <= orx1) && (rx2 >= orx2)) { // since 3.9.3 add safeguard against unbalanced x margins NXM-3597 3.9.3
          double margin = ((orx1-rx1) + (rx2-orx2))/2;
          rx1 = orx1 - margin;
          rx2 = orx2 + margin;
        }
      }
    } // end else resizing

    // Added option to adjust zoom area to maximize map - since 3.9.3 for NXM-3581
    if (maxMapOnDragBox && isLatLonView() && dragboxAdjustment) shiftMoreLatLonMap(); // 4.0.0 only do this on dragbox adjustment, LATLON may not even be map
    dragboxAdjustment = false; // through action based on that adjustment

    if (view==V_MERCATOR) {
      ry1 = ViewMercader.mer2lat(ry1); // Restore visible axis values to RWC
      ry2 = ViewMercader.mer2lat(ry2);
    }

  }

  /** Display plot configuration menu.
      @see #configure(String)
   */
  public void configure () {
    new GMenu(this,"MPlot.cfg", "View,Scale,CxMode,Axis,Readout,ReadoutExt,Options",0,0, this);
  }

  /** This processes the messages associated with the configuration menu.
      @param menuname configuration menu name
      @see #processMessage(Message)
      @see #configure()
   */
  public void configure (String menuname) {
    if (menuname.equals("VIEW")) new GMenu (this, "View", viewList, view, 0, this);
    else if (menuname.equals("SCALE")) {
      int gf = GWidget.NOMSG | GWidget.VBOXMO;
      GDialog gd = new GDialog(null, "ScaleDialog", 340, 120, "Apply,Submit,Cancel", "Apply", 0, this);
      gd.needsResize=false; // Need to prevent hitting the event dispatch thread when adding each item Since 3.9.0
      new GMenu( gd, "Flags   ", scaleList, scale,gf|GMenu.TOGGLE|GMenu.TLEFT|GMenu.NCOL,this);
      new GValue(gd, "XMin",     orx1, DMIN, DMAX, 1.0, gf|GWidget.CONCAT, this);
      new GValue(gd, "XMax",     orx2, DMIN, DMAX, 1.0, gf,                this);
      new GValue(gd, "YMin",     ory1, DMIN, DMAX, 1.0, gf|GWidget.CONCAT, this);
      new GValue(gd, "YMax",     ory2, DMIN, DMAX, 1.0, gf,                this);
      new GValue(gd, "ZMin",     orz1, DMIN, DMAX, 1.0, gf|GWidget.CONCAT, this);
      new GValue(gd, "ZMax",     orz2, DMIN, DMAX, 1.0, gf,                this);
      new GValue(gd, "ViewXMin", rx1,  DMIN, DMAX, 1.0, gf|GWidget.CONCAT, this);
      new GValue(gd, "ViewXMax", rx2,  DMIN, DMAX, 1.0, gf,                this);
      new GValue(gd, "ViewYMin", ry1,  DMIN, DMAX, 1.0, gf|GWidget.CONCAT, this);
      new GValue(gd, "ViewYMax", ry2,  DMIN, DMAX, 1.0, gf,                this);
      new GValue(gd, "ViewZMin", rz1,  DMIN, DMAX, 1.0, gf|GWidget.CONCAT, this);
      new GValue(gd, "ViewZMax", rz2,  DMIN, DMAX, 1.0, gf,                this);
      new GValue(gd, "MinRange",minRange,0.0,1.e5,1.0, gf|GWidget.CONCAT,this);
      new GValue(gd, "MaxRange",maxRange,0.0,1.e5,1.0, gf,this);
      new GValue(gd, "ScaleAvg",scaleAvg,0,100,1, gf,this);
      new GMenu( gd, "Force Rescale", "TRUE, FALSE", 2, gf|GMenu.CHOICE|GMenu.TLEFT|GMenu.NCOL,this);
      // Since NXM 3.8.1 This is used to increment the amount of popups contained inside of MPLot.
      // Usually done in the super, done here because we send in null parent
      popupCount++;
      gd.needsResize=true; // Need to hit the event dispatch thread now. Since 3.9.0
      gd.addTo(this);
    }
    else if (menuname.equals("MODE"))     new GMenu (this, "Mode",     modeList,    mode,    0,            this);
    else if (menuname.equals("AXIS"))     new GMenu (this, "Axis",     axisMenu,    0,       0,            this);
    else if (menuname.equals("AXIS.CFG")) new GMenu (this, "Axis.cfg", axisList,    axis,    GMenu.TOGGLE, this);
    else if (menuname.equals("READOUT"))  new GMenu (this, "Readout",  readoutList, readout, GMenu.TOGGLE, this);
    else if (menuname.equals("READOUTEXT")) {
      String currentList = shortReadoutListExt;
      if (layerDrawable != null || wmsLayerCnt > 0) currentList += ",Points";
      if (wmsLayerCnt > 0)       currentList += ",WMS_level";
      new GMenu (this, "ReadoutExt", currentList, readoutExt, GMenu.TOGGLE, this);
    }
    else if (menuname.equals("OPTIONS"))  new GMenu (this, "Options",  optionsList, options, GMenu.TOGGLE, this);
    else if (menuname.equals("LEGEND")) createLegend();
    else if (menuname.equals("LAYERS")) createLegend();
  }

  /** Creates a new legend for the plot. <br>
      <br>
      <i>Since NeXtMidas 1.9.3:</i> If an old legend is still around, it will be closed before
      the new one is opened. Since updates to the legend are only sent to the last one opened, it
      is necessary to close any old ones so they don't get out-of-synch.
      @return The new legend.
   */
  public GWidget createLegend () {
    if ((legend != null) && (legend.getStatus() != EXIT)) {
      legend.close(); /* Prevent old legends from staying up. */
    }
    legend = new GLegend(this, "Legend", this, 0, this);
    return legend;
  }

  /** Returns the legend, if there is one, or null otherwise.
   *  @return the legend, if there is one, or null otherwise */
  public GLegend getLegend () {
    return legend;
  }

  /** Call message handler method to process a message. This override of <code>
      MWindow.process </code> is used to force a Plot out of the INIT state so
      that data can get processed even if it is not displayed.
      @param name The Message name.
      @param info The Message info.
      @param data The Message data.
      @return The status of the message processing (see {@link MessageHandler#processMessage}).
      @since NeXtMidas 2.7.2
      @deprecated since 3.9.0 for graphical timing issues (NXM-3213)
   */
  @Deprecated
  @Override
  public int process (String name, int info, Object data) {
    // BUG 1905 - The code below was moved to this method from plot.open so that
    //            the display have time to finish being opened, otherwise they
    //            may return FALSE to isPanelVisible()
    if (status==INIT || status==BACK) {
      if (isPanelVisible()) {
        if (parent instanceof Frame) getFocus();
        status = FRONT;
      }
      else status = BACK;
    }
    return super.process(name, info, data);
  }

  @Override
  public int preprocess (String name, int info, Object data) {
    return preprocess (name,info, data, null);
  }

  @Override
  public int preprocess (String name, int info, Object data, MouseEvent e) {
    Object origData = data;
    switch (name) {
      case "BUTTON":
        zmode=0;
        switch (info) {
          case LEFT_MOUSE_BUTTON:
            // set name to message click action, but change if scrolling
            name = Parser.get(buttonActionList, lmbButton); // Bug 2868
            if ((elasticFeature!=null) && "MARK_MESSAGE".contentEquals(name)) {  // PROTOTYPE capability - NXM-3702
              elasticFeatureSelected = elasticFeature.isAtPosition(getPosition(),0,0);
              needRefresh = true;
            }
            if ((scroll&1)!=0 && (py>iy2 || py<iy1)) {
              name = (xvy)? "PANY":"PANX";
              data = pan2rwc((ix2+ix1)/2-px,0);
            }
            if ((scroll&2)!=0 && (px<ix1 || px>ix2)) {
              name = (xvy)? "PANX":"PANY";
              data = pan2rwc(0,(iy2+iy1)/2-py);
            }
            if (px>ix1 && px<ix2 && py>iy1 && py<iy2) {
              if (mm!=null && !showDeltas) mm = null;   // showdeltas check added in 2.9.0 (Bug 1383)
              else mm = getPosition().cloneOf();
              formatReadout(0);
            }
            break;
          case MIDDLE_MOUSE_BUTTON:
            name = Parser.get(buttonActionList, mmbButton); // Bug 2868
            if (data instanceof MouseEvent) {
              // Save off the location of the MMB click for use with selecting feature
              // moved here in 3.9.4 to handle case of custom Message Handler
              setCurrentMMBClickedLocation(((MouseEvent)data).getPoint());
            }
            break;
          case RIGHT_MOUSE_BUTTON:
            if (!dblClickZoom) name = Parser.get(buttonActionList, rmbButton); // Bug 2868
            break;
        }
        if (name.equals("NONE")) return MSG_CONSUMED; //ignore mouse button message when action is NONE - Since 3.5.2 Bug 2868
        break;
      case "POINTER":
        if (elasticFeatureSelected) { // PROTOTYPE - not officially supported - NXM-3702
          elasticHandle = ResizeBox.getHandle(elasticFeature, px, py);
          String cursor = ResizeBox.cursorFromHandle(elasticHandle);
          setCursor(cursor);
        }
        break;
      case "DRAGBOX":
        if (dragging==-3 && view==V_LATLONCONTINUOUS) name="NOOP"; // stop processing, set to NOOP because we just finished panning, not drawing a dragbox since 3.9.1
        box.reform();
        data = box2rwc(box);
        zmode=dragging=0;
        if (elasticHandle!=ResizeBox.Handle.NONE) {
          elasticHandle = ResizeBox.Handle.NONE;
          data = elasticFeature.toTable();
          name = "RESIZEBOX";
          info = 0;
        }
        // NOTE: Pre Nxm 2.3.0, ZOOM and DRAGBOX messages were addressed index based. This
        //       whole section was redone so the macro user can access with XMIN, XMAX, etc.
        else if (view==V_GEODETIC) {
          double[] rwc1 = new double[3];
          double[] rwc2 = new double[3];
          MPoint pix1 = new MPoint(box.x,box.y);
          MPoint pix2 = new MPoint(box.x+box.w,box.y+box.h);
          viewGeo.pix2rwc(pix1,rwc1);
          viewGeo.pix2rwc(pix2,rwc2);
          // WARNING: Min and Max Y pixels are top to bottom, in RWC it is bottom to top.
          //          This is why rwc2[1], rwc1[1] are in the order below.
          DragBox boxObj = new DragBox(rwc1[0], rwc2[0], rwc2[1], rwc1[1], (double[])data);
          data = boxObj;
        } else {
          DragBox boxObj = new DragBox((double[])data);
          double top = getPosition(box.x, box.y).t;
          double bot = getPosition(box.x+box.w, box.y+box.h).t;
          boxObj.setT1(top);   // added in NeXtMidas 2.9.0 (Bug 2093)
          boxObj.setT2(bot);   // added in NeXtMidas 2.9.0 (Bug 2093)
          boxObj.setTMin((bot<top)? bot : top); // For rasters with embedded time
          boxObj.setTMax((top>bot)? top : bot); // in the frames
          data = boxObj;
        }
        if (info==LEFT_MOUSE_BUTTON) name = "ZOOM";
        break;
      case "DRAG":
        int buttNum = info % 100; // button number
        if (elasticFeatureSelected && (buttNum==LEFT_MOUSE_BUTTON) &&
            (elasticHandle!=ResizeBox.Handle.NONE))  { // Internal use prototype NXM-3702
          int isConsumed = ResizeBox.processDrag(elasticFeature, elasticHandle);
          if (isConsumed==MSG_CONSUMED) return MSG_CONSUMED;
        }
        double factor;
        if (zmode==0 && info>0) {
          boolean dragWhenUnzoomed = ( buttNum==MIDDLE_MOUSE_BUTTON) && view==V_LATLONCONTINUOUS;
               if (buttNum==MIDDLE_MOUSE_BUTTON) zmode = PANXY;
          else if (buttNum!=MIDDLE_MOUSE_BUTTON && (scroll&1)!=0 && Math.abs(box.y-isxy)<8 &&
              (isHighHScroll || box.x>isx-isxd) && (isLowHScroll || box.x<isx+isxd)) {
            zmode=SCROLLX;
          }
          else if (buttNum!=MIDDLE_MOUSE_BUTTON && (scroll&2)!=0 && Math.abs(box.x-isyx)<8 &&
              (isHighVScroll || box.y>isy-isyd) && (isLowVScroll ||box.y<isy+isyd)) {
            zmode=SCROLLY;
          }
          else if (buttNum==RIGHT_MOUSE_BUTTON && (options&O_DRAGZOOM)!=0 && (double)box.x/(double)pos.w > 0.8) zmode=ZOOMIO;
          else if (buttNum==RIGHT_MOUSE_BUTTON && view==V_GEODETIC) zmode=PANGEO;
          else if (buttNum==RIGHT_MOUSE_BUTTON && is3DView()) zmode=PAN3D;
          if (scroll==0 && view!=V_GEODETIC && !is3DView() && !dragWhenUnzoomed) zmode=0; // added middle mouse button check so that we can pan with the middle mouse
          if (zmode==0) zmode = -info;
          if (zmode!=0) {
            dragging = -zmode;
            brx = (rx1+rx2)/2;
            bry = (ry1+ry2)/2;
            brxd = (rx2-rx1)/2;
            bryd = (ry2-ry1)/2;
            viewer.getObserver(bvp);
          }
        }
        switch (zmode) {
          case PANXY:
            name = "PANXY";
            data = panb2rwc(box.w,box.h);
            break;
          case SCROLLX:
            isScrolling=true;
            name = "PANXY";
            data = scroll2rwc(box.w,0);
            break;
          case SCROLLY:
            isScrolling=true;
            name = "PANXY";
            data = scroll2rwc(0,box.h);
            break;
          case ZOOMIO:
            name = "PANXY";
            factor = box.h/(double)Math.abs(iy2-iy1);
            data = panz2rwc(-4*factor);
            break;
          case PANGEO:
            name = "PANOBS";
            data = nvp;
            factor = 180.0/pos.h;
            nvp[0] = bvp[0]-box.w*factor;
            nvp[1] = bvp[1]+box.h*factor;
            nvp[2] = bvp[2];
            break;
          case PAN3D:
            name = "PANOBS";
            data = null;
            factor = 1.0/Math.min(ix21,iy21);
            double x = 2.0*(px-ix1)/ix21-1;
            double y = 2.0*(py-iy1)/iy21-1;
            double z = Math.max( 0.0, 1-Math.sqrt(x*x+y*y) );
            nvp[1] = z*180*(box.x-px)*factor;           // ELEV
            nvp[2] = z*180*(py-box.y)*factor;           // ROLL
            nvp[0] = (z-1)*180*((box.x-px)*y+(py-box.y)*x)*factor;  // AZIM
            box.x=px;
            box.y=py;
            view3D.rotate(nvp);
            break;
        }
        break;
    }
    // send off everything from here to make code simpler
    //Include original message data in quals table - Since NeXtMidas 3.5.4 Bug 2927
    if (mh != null && !name.equals("NOOP")) { // If the name is set to NOOP we do not want to do any more processing of the message since 3.9.1
      Table quals = new Table();
      quals.put("ORIG_MSG_DATA", origData);
      if ((e!=null) && (origData != null) && (origData instanceof MBox)) {
          quals.put("MODIFIERS", e.getModifiersEx()); // NXM-4028 add modifiers to quals table
      }
      mh.processMessage(new Message(name,info,data,mh,this,quals));
    }
    return MSG_CONSUMED;
  }

  /** Assign a Feature to be MPlot's stretchable feature
   * If this Feature is not already one of MPlot's features, add it
   * @param feature feature that is to be stretchable
   */
  @InternalUseOnly("Prototype in progress")
  public void setElasticFeature (Feature feature) {
    if ((feature!=null) && (features.findKey(feature.getName())==-1)) {
      // need to add this Feature to MPlot Features
      addFeature(feature);
    }
    elasticFeature = feature;
    elasticFeatureSelected = false;
    elasticHandle = ResizeBox.Handle.NONE;
    needRefresh = true;
  }

  /** Clear / Remove any Elastic Feature */
  @InternalUseOnly("Prototype in progress")
  public void clearElasticFeature () {
    elasticFeature = null;
    elasticFeatureSelected = false;
    elasticHandle = ResizeBox.Handle.NONE;
    needRefresh = true;
  }

  @Override
  protected void stopScroll() {
    isScrolling=false;
    zmode=dragging=0;
  }

  /** Process a message from parent process or config menu. This is the
      same as calling <code>processMessage(msg,null,null)</code>.
      @param msg The message to process.
      @see #processMessage(Message,Object,Object)
   */
  @Override
  public int processMessage (Message msg) {
    return processMessage(msg,null,null);
  }

  /** Process a message from parent process or config menu.
   *  @param msg    The message to process.
   *  @param msgid  <i>This parameter is ignored.</i>
   *  @param parent <i>This parameter is ignored.</i>
   *  @return The status, see {@link #processMessage(Message)} for details.
   */
  public int processMessage (Message msg, Object msgid, Object parent) {
    Layer lay;

    switch (msg.name) {
      case "MPLOT.CFG":    configure(Convert.o2s(msg.data));        return MSG_CONSUMED;
      case "VIEW":         setView(Convert.o2s(msg.data));          return MSG_CONSUMED;
      case "AXIS":         setAxis(Convert.o2s(msg.data));          return MSG_CONSUMED;
      case "AXIS.CFG":     setAxis(msg.info);                       return MSG_CONSUMED;
      case "MODE":         setMode(Convert.o2s(msg.data));          return MSG_CONSUMED;
      case "SCALE":        setScale(Convert.o2s(msg.data));         return MSG_CONSUMED;
      case "SCALEDIALOG":  applyScaleValues(Convert.o2t(msg.data)); return MSG_CONSUMED;
      case "DRANGEUNITS":  setDRangeUnits((String)msg.data);        return MSG_CONSUMED;
      case "OPTIONS":      if (setOptions(msg.info)) refresh();     return MSG_CONSUMED;

      case "READOUTEXT":
        if (setReadout(msg.info, READOUT_OPTION_EXT)) {
          refresh();
        }
        return MSG_CONSUMED;
      case "READOUT":
        if ("DRANGEUNITS>".equals(msg.data)) {
          // make a dRange Units menu
          new GMenu(this,"dRangeUnits",drangeunitsList,dRangeUnits,0,this);
        }
        else if (setReadout(msg.info, READOUT_OPTION_MAIN)) {
          refresh();
        }
        return MSG_CONSUMED;
      case "LEGEND.ALT":
        lay = (Layer)layers.get(Convert.o2s(msg.data));
        if (lay!=null) {
          lay.configure();
        }
        return MSG_CONSUMED;

      case "LEGEND":
        lay = (Layer)layers.get(Convert.o2s(msg.data));
          if (lay!=null) {
          if ((lay.enable&EN_BSTORE)!=0) reset(-1);
          lay.enable ^= EN_GLOBAL;
          refresh();
        }
        return MSG_CONSUMED;

      default:
          return super.processMessage(msg);
    }
  }

  /** Apply a table of values (typically from the scale dialog menu)
      Note: These values are NOT checked for consistency and can result in a bad state
      (ex: viewLimits outside the range of outerLimits and cases where clicking both
      "Apply" and "Submit" gives different results than just clicking one of the buttons.)
      @since NeXtMidas 2.7.3 (code in processMessage)
      @since NeXtMidas 2.7.4 (this method created)
   */
  private void applyScaleValues (Table t) {
    // the values below are set to false by default otherwise pushing the 'apply'
    // button in the dialog causes a rescale
    boolean doFindRange = false;
    boolean doUseRange  = false;

    int tmpScale = ((GMenu)t.get("FLAGS")).getItemIndex();
    if (tmpScale != scale) { scale = tmpScale; doFindRange=true; doUseRange=true; }

    // read the bounds values from the table
    double tmporx1 = Convert.o2d( ((GValue)t.get("XMIN")).getValue() );
    double tmporx2 = Convert.o2d( ((GValue)t.get("XMAX")).getValue() );
    double tmpory1 = Convert.o2d( ((GValue)t.get("YMIN")).getValue() );
    double tmpory2 = Convert.o2d( ((GValue)t.get("YMAX")).getValue() );
    double tmporz1 = Convert.o2d( ((GValue)t.get("ZMIN")).getValue() );
    double tmporz2 = Convert.o2d( ((GValue)t.get("ZMAX")).getValue() );
    double tmprx1 = Convert.o2d( ((GValue)t.get("VIEWXMIN")).getValue() );
    double tmprx2 = Convert.o2d( ((GValue)t.get("VIEWXMAX")).getValue() );
    double tmpry1 = Convert.o2d( ((GValue)t.get("VIEWYMIN")).getValue() );
    double tmpry2 = Convert.o2d( ((GValue)t.get("VIEWYMAX")).getValue() );
    double tmprz1 = Convert.o2d( ((GValue)t.get("VIEWZMIN")).getValue() );
    double tmprz2 = Convert.o2d( ((GValue)t.get("VIEWZMAX")).getValue() );

    // if input bounds values are different than current, set the values and update the user bounds settings table
    // (Note: not using Tolerance.equals(double,double) for now as this is called due to user input from a scale dialog menu)
    if (tmporx1 != orx1) { orx1 = tmporx1; setUserLimits("X1", orx1, true); doFindRange = false; doUseRange = true;}
    if (tmporx2 != orx2) { orx2 = tmporx2; setUserLimits("X2", orx2, true); doFindRange = false; doUseRange = true;}
    if (tmpory1 != ory1) { ory1 = tmpory1; setUserLimits("Y1", ory1, true); doFindRange = false; doUseRange = true;}
    if (tmpory2 != ory2) { ory2 = tmpory2; setUserLimits("Y2", ory2, true); doFindRange = false; doUseRange = true;}
    if (tmporz1 != orz1) { orz1 = tmporz1; setUserLimits("Z1", orz1, true); doFindRange = false; doUseRange = true;}
    if (tmporz2 != orz2) { orz2 = tmporz2; setUserLimits("Z2", orz2, true); doFindRange = false; doUseRange = true;}
    if (tmprx1 != rx1) { rx1 = tmprx1; setUserLimits("X1", rx1, false); doUseRange = false;}
    if (tmprx2 != rx2) { rx2 = tmprx2; setUserLimits("X2", rx2, false); doUseRange = false;}
    if (tmpry1 != ry1) { ry1 = tmpry1; setUserLimits("Y1", ry1, false); doUseRange = false;}
    if (tmpry2 != ry2) { ry2 = tmpry2; setUserLimits("Y2", ry2, false); doUseRange = false;}
    if (tmprz1 != rz1) { rz1 = tmprz1; setUserLimits("Z1", rz1, false); doUseRange = false;}
    if (tmprz2 != rz2) { rz2 = tmprz2; setUserLimits("Z2", rz2, false); doUseRange = false;}

    minRange = Convert.o2d( ((GValue)t.get("MINRANGE")).getValue() );
    maxRange = Convert.o2d( ((GValue)t.get("MAXRANGE")).getValue() );
    scaleAvg = Convert.o2l( ((GValue)t.get("SCALEAVG")).getValue() );

    boolean forceRescale = Convert.o2s(((GMenu)t.get("FORCE RESCALE")).getValue()).equals("TRUE");  // rescale means reset all bounds also
    if (forceRescale || doFindRange) findRange();  // resets the orx|y|z values (outer bounds)
    if (forceRescale || doUseRange)  useRange();   // resets the  rx|y|z values (view bounds)
    setup();
    refresh();
  }

  /** Sets the indicator to show that the plot needs to be refreshed.  */
  @Override
  public void refresh () {
    cancel=true;
    needRefresh=true;
  }

  /** Set whether to hold any refreshes from occurring.<br>
      Allows users to block refreshes, make many changes, then unblock
      refreshes to commit/draw all changes at once.
      @param arg Value must be from {@link #holdRefreshList}
      @return true if it changes/sets the holdRefresh, else false.
      @since NeXtMidas 2.7.2
   */
  public boolean setHoldRefresh (String arg) {
    return setHoldRefresh(Parser.mask(holdRefreshList, arg, holdRefresh));
  }

  /** Set whether to hold any refreshes from occurring.<br>
      Allows users to block refreshes, make many changes, then unblock
      refreshes to commit/draw all changes at once.
      @param arg Int form of value from {@link #holdRefreshList}
      @return true if it changes/sets the holdRefresh, else false.
      @since NeXtMidas 2.7.2
   */
  public boolean setHoldRefresh (int arg) {
    if (arg != holdRefresh) {
      holdRefresh = arg;
      return true;
    }
    return false;
  }

  /** Get what refreshes are being held using {@link #holdRefresh}
      @return Value(s) from {@link #holdRefreshList}
      @since NeXtMidas 2.7.4 (replaced earlier version that returned int)
   */
  public String getHoldRefresh () {
    String res = Parser.mask2s(holdRefreshList, holdRefresh);
    if (res.equals("|")) res = "NONE";
    return res;
  }

  /** Gets a copy of the buttonAction table currently in use.
    @return buttonAction table
    @since NeXtMidas 3.5.2  Bug 2868
  */
  public Table getButtonAction () {
    Table tbl = new Table();
    tbl.put("LMB", Parser.get(buttonActionList, lmbButton));
    tbl.put("MMB", Parser.get(buttonActionList, mmbButton));
    tbl.put("RMB", Parser.get(buttonActionList, rmbButton));
    return tbl;
  }

  /** Sets the click actions to use. The table should be in the form:
    <pre>
      {LMB=[action],MMB=[action]RMB=[action]}
    </pre>
    Where each <code>[action]</code> is one of the items on the {@link #buttonActionList} or
    <code>"DEFAULT"</code> to use the PLOT default.
    @param tbl The click action table.
    @since NeXtMidas 3.5.2  Bug 2868
  */
  public void setButtonAction (Table tbl) {
    if (tbl != null) {
      lmbButton = findButtonAction(tbl.getS("LMB"), lmbButton, LMB_DEFAULT);
      mmbButton = findButtonAction(tbl.getS("MMB"), mmbButton, MMB_DEFAULT);
      rmbButton = findButtonAction(tbl.getS("RMB"), rmbButton, RMB_DEFAULT);
    }
  }

  /** Only used by setButtonAction(..).
      @param val new button action (from table) in String form
      @param current this button's current action
      @param def default action for this button
      @return integer version of buttonAction
      @since NeXtMidas 3.5.2  Bug 2868
   */
  private int findButtonAction(String val, int current, int def) {
    int action = current;
    if (val != null) {
      if (val.equalsIgnoreCase("DEFAULT")) {
        action = def;

      } else {
        action = Parser.find(buttonActionList, val, def);
        if (action < 0) {
          throw new MidasException("MPlot: Invalid buttonAction '"+val+"', valid options are '"
                                  +buttonActionList+"'.");
        }
      }
    }
    return action;
  }

  /** Pan to the given Time Window. The argument object must contain 2 doubles.
      These are offset and duration in seconds.  If duration is positive, offset is
      the start of the window.  If duration is negative, offset is the end of the window.
      @param rtw 2 doubles that are offset and duration in seconds
      @param mode unused
      @return true if pan successful
   */
  public boolean pantw (Object rtw, int mode) { return panTimeWindow(rtw,mode); }

  /** Pan to the given Time Window. The argument object must contain 2 doubles.
      These are offset and duration in seconds.  If duration is positive, offset is
      the start of the window.  If duration is negative, offset is the end of the window.
      @param rtw  time window
      @param mode unused
      @return true if panned
   */
  public boolean panTimeWindow (Object rtw, int mode) {
    double t1,t2;
    t1 = Convert.o2d(rtw,0,0.0);
    t2 = Convert.o2d(rtw,1,1.0);
    Layer ly = getBaseLayer();
    if (ly != null && ly.pf instanceof DataFile) {
      DataFile df = (DataFile)ly.pf;
      if (t1==0 || t2==0) { // show all
        t1 = df.getStart();
        t2 = t1 + df.getDelta()*df.getSize();
      } else {          // show region
        t1 = df.getStart() + df.getDelta()*df.getIndexAt(t1);
        if (t2>0) t2=t1+t2; else { t1=t1+t2; t2=t1-t2; }
      }
      if (df.typeClass==2) setYBounds(t1,t2); else setXBounds(t1,t2);
      return true;
    }
    return false;
  }

  /** Pan to the given X and Y windows.
      The argument object must contain 4 doubles.  They are X1, X2, Y1, and Y2.
      If mode=1, only the X window is changed. If mode=2, only the Y window is changed.
      @param rxy given X and Y windows
      @param mode 1 for setting x bounds, 2 for setting y bounds
      @return true if panned
  */
  public boolean panxy (Object rxy, int mode) {
    double x1 = Convert.o2d(rxy,0,rx1);
    double x2 = Convert.o2d(rxy,1,rx2);
    double y1 = Convert.o2d(rxy,2,ry1);
    double y2 = Convert.o2d(rxy,3,ry2);

    // Pan limiting of map views and Layer2D base layers
    if (baseIsLayer2DorImage || isMapView()) {
      if (baseIsLayer2DorImage) {
        // limit range to data bounds or outer bounds, whichever is larger
        x1 = Math.max(Math.min(orx1, frx1), x1);
        x2 = Math.min(Math.max(orx2, frx2), x2);
        y1 = Math.max(Math.min(ory1, fry1), y1);
        y2 = Math.min(Math.max(ory2, fry2), y2);
      }
      else if (!isLatLonView()) {
        // Try to avoid panning too far off screen. It can get lost.
        // Values were determined by trial and error
        // The map views can go on forever still if zoomed enough
        // V_3DGRAPH needs better fix as only limits first direction
        double range_multiple_allowed;
        switch (view) {
          case V_GEODETIC: range_multiple_allowed = 2.7; break;
          case V_MERCATOR: range_multiple_allowed = 2;   break;
          case V_POLAR:    range_multiple_allowed = 2;   break;
          case V_XYZ:      range_multiple_allowed = 2.5; break;
          case V_3DGRAPH:  range_multiple_allowed = 1;   break;
          default:         range_multiple_allowed = 1;   break;
        }
        if ((x1 < Math.min(frx1*range_multiple_allowed, rx1)) || (x2 > Math.min(x2, Math.max(frx2*range_multiple_allowed, rx2)))) {
          x1 = rx1;
          x2 = rx2;
        }
        if ((y1 < Math.min(fry1*range_multiple_allowed, ry1)) || (y2 > Math.min(y2, Math.max(fry2*range_multiple_allowed, ry2)))) {
          y1 = ry1;
          y2 = ry2;
        }


      }
      // the flat views should only allow panning within the range of visible bounds
      else {
        // do not allow panning beyond bounds, but account for existing margins 3.9.3
        if ((view == V_LATLON) && ((x1 < Math.min(orx1, rx1)) || (x2 > Math.min(x2, Math.max(orx2, rx2))))) {
          x1 = rx1;
          x2 = rx2;
        }
        if ((y1 < Math.min(ory1, ry1)) || (y2 > Math.min(y2, Math.max(ory2,ry2)))) {
          y1 = ry1;
          y2 = ry2;
        }

        // LatLon views need to stay within outer bounds, except LatLonContinuous which allows
        // longitude outer bounds to be shifted
        if((view == V_LATLONCONTINUOUS) || ((x2-x1 > LONGITUDE_RANGE) && (y2-y1 > LATITUDE_RANGE))) {
          // Since 3.9.1 convert the values provided to the correct adjusted value based on view
          double[] convertedValues = adjustBorders(x1,x2,y1,y2);
          x1 = convertedValues[0];
          x2 = convertedValues[1];
          y1 = convertedValues[2];
          y2 = convertedValues[3];
        }
      }
    }

    if (mode==1) setXBounds(x1,x2);
    else if (mode==2) setYBounds(y1,y2);
    else setXYBounds(x1,x2,y1,y2);
    return true;
  }

  // Not sure if this method is still used ??
  /** Pan to the given X and Y windows
      Please refer to {@link #panxy(Object, int)}
      for a more current version of this method
      @param rxy given X and Y windows
      @param mode 1 for setting x bounds, 2 for setting y bounds
      @return true if panned
   */
  @InternalUseOnly
  public boolean panxy_orig (Object rxy, int mode) {
    double x1 = Convert.o2d(rxy,0,rx1);
    double x2 = Convert.o2d(rxy,1,rx2);
    double y1 = Convert.o2d(rxy,2,ry1);
    double y2 = Convert.o2d(rxy,3,ry2);

    // Pan limiting of standard views
    if (isStandardView()) {
      if (!isLatLonView()) {
        // Limit scrolling to full range

        if (isStandardXYView()) {
          if ((x1 < Math.min(frx1, rx1)) || (x2 > Math.min(x2, Math.max(frx2, rx2)))) {
            x1 = rx1;
            x2 = rx2;
          }
          if ((y1 < Math.min(fry1, ry1)) || (y2 > Math.min(y2, Math.max(fry2, ry2)))) {
            y1 = ry1;
            y2 = ry2;
          }
        }
        else {
          // Try to avoid panning too far off screen. It can get lost.
          // Values were determined by trial and error
          // The map views can go on forever still if zoomed enough
          // V_3DGRAPH needs better fix as only limits first direction
          double range_multiple_allowed;
          switch (view) {
            case V_GEODETIC: range_multiple_allowed = 2.7; break;
            case V_MERCATOR: range_multiple_allowed = 2;   break;
            case V_POLAR:    range_multiple_allowed = 2;   break;
            case V_XYZ:      range_multiple_allowed = 2.5; break;
            case V_3DGRAPH:  range_multiple_allowed = 2;   break;
            default:         range_multiple_allowed = 1;   break;
          }

          if(x1<frx1*range_multiple_allowed) return false;
          if(x2>frx2*range_multiple_allowed) return false;
          if(y1<fry1*range_multiple_allowed) return false;
          if(y2>fry2*range_multiple_allowed) return false;
        }
      }
      // the flat views should only allow panning within the range of visible bounds
      else {
        // do not allow panning beyond bounds, but account for existing margins 3.9.3
        if ((view == V_LATLON) && ((x1 < Math.min(orx1, rx1)) || (x2 > Math.min(x2, Math.max(orx2, rx2))))) {
          x1 = rx1;
          x2 = rx2;
        }
        if ((y1 < Math.min(ory1, ry1)) || (y2 > Math.min(y2, Math.max(ory2,ry2)))) {
          y1 = ry1;
          y2 = ry2;
        }

        // LatLon views need to stay within outer bounds, except LatLonContinuous which allows
        // longitude outer bounds to be shifted
        if((view == V_LATLONCONTINUOUS) || ((x2-x1 > LONGITUDE_RANGE) && (y2-y1 > LATITUDE_RANGE))) {
          // Since 3.9.1 convert the values provided to the correct adjusted value based on view
          double[] convertedValues = adjustBorders(x1,x2,y1,y2);
          x1 = convertedValues[0];
          x2 = convertedValues[1];
          y1 = convertedValues[2];
          y2 = convertedValues[3];
        }
      }
    }


    if (mode==1) setXBounds(x1,x2);
    else if (mode==2) setYBounds(y1,y2);
    else setXYBounds(x1,x2,y1,y2);
    return true;
  }


  /** Pan to the given X window.
      The argument object must contain 2 doubles.  They are X1 and X2.
      @param rx X window
      @return true if panned
  */
  public boolean panx (Object rx) {
    double x1 = Convert.o2d(rx,0,rx1);
    double x2 = Convert.o2d(rx,1,rx2);
    setXBounds(x1,x2);
    return true;
  }

  /** Pan to the given Y window.
      The argument object must contain 2 doubles.  They are Y1 and Y2.
      @param ry Y window
      @return true if panned
  */
  public boolean pany (Object ry) {
    double y1 = Convert.o2d(ry,0,ry1);
    double y2 = Convert.o2d(ry,1,ry2);
    setYBounds(y1,y2);
    return true;
  }

  /** Pan to the given 3D perspective.
      The argument object must contain 3 doubles.
      They are lat,lon,alt in GEO mode,
      or alpha, beta, and distance in XYZ mode.
      @param arg object with 3 doubles
      @return true if panned
  */
  public boolean panobs (Object arg) {
    reset(-1);
    return viewer.setObserver(Convert.o2da(arg));
  }

  private double[] box2rwc (MBox box) {
    double[] nbox = new double[4];
    nbox[0] = getrx(box.x,box.y);
    nbox[1] = getrx(box.x+box.w,box.y+box.h);
    nbox[2] = getry(box.x,box.y);
    nbox[3] = getry(box.x+box.w,box.y+box.h);
    return nbox;
  }

  private double[] panb2rwc (int dx, int dy) {
    double drx,dry;
    double[] nbox = new double[4];
    if (xvy) {
      drx = -(double)dy/mxx;
      dry = -(double)dx/myy;
    } else {
      drx = -(double)dx/mxx;
      dry = -(double)dy/myy;
    }
    nbox[0] = brx+drx-brxd;
    nbox[1] = brx+drx+brxd;
    nbox[2] = bry+dry-bryd;
    nbox[3] = bry+dry+bryd;
    return nbox;
  }

  private double[] scroll2rwc (int dx, int dy) {
    double drx,dry;
    double[] nbox = new double[4];
    double fx = (rx2-rx1)/(frx2-frx1);
    double fy = (ry2-ry1)/(fry2-fry1);
    if (xvy) {
      drx = dy/(mxx*fx);
      dry = dx/(myy*fy);
    } else {
      drx = dx/(mxx*fx);
      dry = dy/(myy*fy);
    }
    nbox[0] = brx+drx-brxd;
    nbox[1] = brx+drx+brxd;
    nbox[2] = bry+dry-bryd;
    nbox[3] = bry+dry+bryd;
    if (baseIsLayer2DorImage || isMapView()) {
      // limit scrolling to plot bounds, so scroll bar does not disappear - since 3.9.1 for NXM-3332
      double minX = Math.min(frx1, orx1);
      double maxX = Math.max(frx2, orx2);
      double minY = Math.min(fry1, ory1);
      double maxY = Math.max(fry2, ory2);
      if (nbox[0] < minX) {
        nbox[0] = minX;
        nbox[1] = minX + (rx2-rx1);
      }
      else if (nbox[1] > maxX) {
        nbox[1] = maxX;
        nbox[0] = maxX - (rx2-rx1);
      }
      if (nbox[2] < minY) {
        nbox[2] = minY;
        nbox[3] = minY + (ry2-ry1);
      }
      else if (nbox[3] > maxY) {
        nbox[3] = maxY;
        nbox[2] = maxY - (ry2-ry1);
      }
    }
    return nbox;
  }

  private double[] pan2rwc (int dx, int dy) {
    double drx,dry;
    double[] nbox = new double[4];
    if (xvy) {
      drx = -(double)dy/mxx;
      dry = -(double)dx/myy;
    } else {
      drx = -(double)dx/mxx;
      dry = -(double)dy/myy;
    }
    nbox[0] = rx1+drx;
    nbox[1] = rx2+drx;
    nbox[2] = ry1+dry;
    nbox[3] = ry2+dry;
    return nbox;
  }

  private double[] panz2rwc (double factor) {
    double[] nbox = new double[4];
    if (factor>0) factor = (1+factor);
    else factor = 1/(1-factor);
    nbox[0] = brx-brxd*factor;
    nbox[1] = brx+brxd*factor;
    nbox[2] = bry-bryd*factor;
    nbox[3] = bry+bryd*factor;
    return nbox;
  }

  // TODO: not used, delete?
  private double getrdx () {
    return getrx(px,py)-getrx(box.x,box.y);
  }

  // TODO: not used, delete?
  private double getrx () {
    return getrx(px,py);
  }

  /** Converts X-axis pixel measures to RWC values
    @param ix X-axis pixel measures
    @param iy Y-axis pixel measures
    @return RWC values for X-axis pixel measures
   */
  public double getrx (int ix, int iy) {
    int i = (xvy)? iy:ix;
    if (mxx==0.0) return 0.0;
    return ((i-mxb)/mxx);
  }

  // TODO: not used, delete?
  private double getrdy () {
    return getry(px,py)-getry(box.x,box.y);
  }

  // TODO: not used, delete?
  private double getry () {
    return getry(px,py);
  }

  /** Converts Y-axis pixel measures to RWC values
      @param ix X-axis pixel measures
      @param iy Y-axis pixel measures
      @return RWC values for Y-axis pixel measures
   */
  public double getry (int ix, int iy) {
    int i = (xvy)? ix:iy;
    if (myy==0.0) return 0.0;
    if (view==V_MERCATOR) return viewMerc.iy2ry(iy);
    return ((i-myb)/myy);
  }

  // TODO: not used, delete?
  private double[] getrxyz (int px, int py) {
    double[] buf = new double[4];
    buf[0] = getrx(px,py);
    buf[1] = getry(px,py);
    buf[2] = 0;
    buf[3] = 0;
    Layer lay = getBaseLayer();
    if (lay!=null) buf[3] = lay.getTime(px,py);
    return buf;
  }

  /** Indicates if a given point is in view on the plot. This allows for optimizations within a
      layer such that points not visible can be omitted.
      @param x The x coordinate (pixel coordinates).
      @param y The x coordinate (pixel coordinates).
      @return true if visible, false if not.
      @since NeXtMidas 2.5.4
   */
  public boolean isInRange (int x, int y) {
    return (x>ix1 && x<ix2 && y>iy1 && y<iy2);
  }

  /** Add a mouseWheelListener.
      This is added here instead of MWindow due to Sun JAVA Bug 6480024 which
      was causing a stack overflow in GDialog if using a Java version earlier than 1.6_10.
      @since NeXtMidas 2.7.2
   */
  private void addMouseWheelListener () {
    if (panel != null) { // Since NeXtMidas 3.5.0. Skip if in Headless state. BUG 2817
      panel.addMouseWheelListener(this);
    }
  }

  /** Capture movements of the mouse wheel, which are used for zooming in and zooming out,
      unless the mouse wheel zooms are turned off via {@link #mouseWheelZoom} being set to FALSE.
      @since NeXtMidas 2.7.2
   */
  @Override
  public void mouseWheelMoved (MouseWheelEvent e) {
    if ((eventFilter&NOMOUSE)!=0 || mouseWheelZoom == false) return;
    // ability to adjust percentage zoom added in 3.3.2 for Bug 2722
    float percent = getZoomPercentage();
    Message msg = new Message("ZOOMIN",0,percent,new Table("{PERCENT="+percent+"}"));
    if (e.getWheelRotation() > 0) msg.name = "ZOOMOUT";
    mh.processMessage(msg);
  }

  /** Override of {@link MWindow#mouseClicked(MouseEvent)} to catch double-clicks
      on the plot so we can zoom / unzoom to that point.
      @since NeXtMidas 2.7.2
   */
  @Override
  public void mouseClicked (MouseEvent e) {
    if ((eventFilter&NOMOUSE)!=0) return;
    super.mouseClicked(e); // this needs to be first to set px and py.
    clickCount = e.getClickCount();
    if (dblClickZoom) {
      if (clickCount == 1) {
        Timer zoomTimer = new Timer(doubleClickInterval, new ActionListener() {
          @Override
          public void actionPerformed(ActionEvent e) { checkClickCount(button); }
        });
        zoomTimer.setRepeats(false); // fire timer only the first time and then stop
        zoomTimer.start();           // start timer
      }
    }
  }

// TODO: This is never used, delete?
//  /** Parse the MouseEvent and put into a Table
//      @param e The mouseEvent
//      @return Table containing the more relevant info from the Event (position, button, click count)
//      @since NeXtMidas 2.9.0.
//   */
//  private Table parseMouseEvent (MouseEvent e) {
//    Table data = new Table();
//    int x = e.getX();
//    int y = e.getY();
//    double[] rwc = new double[3];
//    MPoint mp = new MPoint(x,y);
//    viewer.pix2rwc(mp, rwc);
//    data.put("RWCX", rwc[0]);
//    data.put("RWCY", rwc[1]);
//    data.put("XPOS", x);
//    data.put("YPOS", y);
//    data.put("BUTTON", e.getButton());
//    data.put("CLICKCOUNT", e.getClickCount());
//    data.put("XONSCREEN", e.getXOnScreen()); // Java 6 API
//    data.put("YONSCREEN", e.getYOnScreen());
//
//    return data;
//  }

  private void checkClickCount (int button) {
    if (clickCount==1 && button==3) {
      if (mh != null) mh.processMessage(new Message ("UNZOOM",button,null,mh,this));
    }
    else if (clickCount==2) {
      if (dblClickZoom) {
        String msgName;
        if      (button == 1) { zoomInAtMouse();  msgName="DBLCLICKZOOM";   } // LMB
        else if (button == 3) { zoomOutAtMouse(); msgName="DBLCLICKUNZOOM"; } // RMB
        else return; // ignore MMB
        Table data = new Table("XMIN="+rx1+", XMAX="+rx2+", YMIN="+ry1+", YMAX="+ry2);
        if (mh != null) mh.processMessage(new Message(msgName, 0, data));
      }
    }
  }

  /** Override of {@link MWindow#keyPressed(KeyEvent)} to catch arrow keys
      or plus/minus keys, which are used for zoomin in/out and moving the
      viewable area of a zoomed-in plot. Also catches XCUT / YCUT keys being.
      @since NeXtMidas 2.7.2
   */
  @Override
  public void keyPressed (KeyEvent e) {
    super.keyPressed(e);
    if ((eventFilter&NOKEY)!=0) return;
    String dir = null;
    float mult = 1.0f;
    Message msg = null;
    String text = KeyEvent.getKeyText(keycode).toUpperCase();
    if      (text.equals(keyMap.getS("LEFT")))  dir = "LEFT";
    else if (text.equals(keyMap.getS("RIGHT"))) dir = "RIGHT";
    else if (text.equals(keyMap.getS("UP")))    dir = "UP";
    else if (text.equals(keyMap.getS("DOWN")))  dir = "DOWN";
    else if (text.equals(keyMap.getS("PAGEUP")))   { dir="UP";    mult = zoomMult; }
    else if (text.equals(keyMap.getS("PAGEDOWN"))) { dir="DOWN";  mult = zoomMult; }
    else if (text.equals(keyMap.getS("HOME")))     { dir="LEFT";  mult = zoomMult; }
    else if (text.equals(keyMap.getS("END")))      { dir="RIGHT"; mult = zoomMult; }

    if (dir != null) {
      adjustZoom(dir, zoomAdjust*mult);
      return;
    }

    if (text.equals(keyMap.getS("IN")) || text.equals(keyMap.getS("ADD")) || text.equals(keyMap.getS("PLUS"))) {
      msg = new Message("ZOOMIN",0,getZoomPercentage(),new Table("{PERCENT="+getZoomPercentage()+"}"));
    } else if (text.equals(keyMap.getS("OUT")) || text.equals(keyMap.getS("MINUS")) || text.equals(keyMap.getS("SUBTR"))) {
      msg = new Message("ZOOMOUT",0,getZoomPercentage(),new Table("{PERCENT="+getZoomPercentage()+"}"));
    } else  if (text.equals(keyMap.getS("XCUT"))) {    // look for XCUT in keyMap table
      Table data = new Table("{IX="+px+", IY="+py+"}");
      msg = new Message ("XCUT",0,data );
    } else if (text.equals(keyMap.getS("YCUT"))) {     // look for YCUT in keyMap table
      Table data = new Table("{IX="+px+", IY="+py+"}");
      msg = new Message ("YCUT",0,data );
    } else if (text.equals("X")) {
      Table data = new Table("{IX="+px+", IY="+py+"}");
      msg = new Message ("XCUT",0,data );
    } else if (text.equals("Y")) {
      Table data = new Table("{IX="+px+", IY="+py+"}");
      msg = new Message ("YCUT",0,data );
    }

    if (msg != null) mh.processMessage(msg);
  }

  /** Set the values for the mapping of the keyboard keys.
      This method will check the given Table for certain keys and assign them to
      the keyMap table. If those keys are not found, then default mappings are used.
      @param tab table used to populate keyMap
      @since NeXtMidas 2.7.2
   */
  public void setKeyMapTable (Table tab) {
    keyMap = new Table();
    if (tab == null) tab = new Table();
    keyMap.put("LEFT",     tab.getS("LEFT",     KeyEvent.getKeyText(KeyEvent.VK_LEFT)).toUpperCase());
    keyMap.put("RIGHT",    tab.getS("RIGHT",    KeyEvent.getKeyText(KeyEvent.VK_RIGHT)).toUpperCase());
    keyMap.put("UP",       tab.getS("UP",       KeyEvent.getKeyText(KeyEvent.VK_UP)).toUpperCase());
    keyMap.put("DOWN",     tab.getS("DOWN",     KeyEvent.getKeyText(KeyEvent.VK_DOWN)).toUpperCase());
    keyMap.put("IN",       tab.getS("IN",       KeyEvent.getKeyText(KeyEvent.VK_ADD)).toUpperCase());
    keyMap.put("PLUS",     tab.getS("PLUS",     KeyEvent.getKeyText(KeyEvent.VK_EQUALS)).toUpperCase());
    keyMap.put("ADD",      tab.getS("ADD",      KeyEvent.getKeyText(KeyEvent.VK_ADD)).toUpperCase());
    keyMap.put("OUT",      tab.getS("OUT",      KeyEvent.getKeyText(KeyEvent.VK_SUBTRACT)).toUpperCase());
    keyMap.put("MINUS",    tab.getS("MINUS",    KeyEvent.getKeyText(KeyEvent.VK_MINUS)).toUpperCase());
    keyMap.put("SUBTR",    tab.getS("SUBTR",    KeyEvent.getKeyText(KeyEvent.VK_SUBTRACT)).toUpperCase());
    keyMap.put("PAGEUP",   tab.getS("PAGEUP",   KeyEvent.getKeyText(KeyEvent.VK_PAGE_UP)).toUpperCase());
    keyMap.put("HOME",     tab.getS("HOME",     KeyEvent.getKeyText(KeyEvent.VK_HOME)).toUpperCase());
    keyMap.put("END",      tab.getS("END",      KeyEvent.getKeyText(KeyEvent.VK_END)).toUpperCase());
    keyMap.put("PAGEDOWN", tab.getS("PAGEDOWN", KeyEvent.getKeyText(KeyEvent.VK_PAGE_DOWN)).toUpperCase());

    // only put xcut and ycut in if they are given. Otherwise, they default to CTRL+X|Y in keyPressed method
    String xcut = tab.getS("XCUT");
    String ycut = tab.getS("YCUT");
    if (xcut != null) { keyMap.put("XCUT", xcut.toUpperCase()); }
    if (ycut != null) { keyMap.put("YCUT", ycut.toUpperCase()); }
  }

  /** Centers the plot at the current mouse pos and zooms in by the current {@link #zoomAdjust}
   *  @return true if zooms */
  public boolean zoomInAtMouse () {
    Position p = getPosition(px, py);
    return zoomInOutAtPoint(p.x, p.y, zoomAdjust);
  }

  /** Centers the plot at the RWC coords given and zooms in by the percent given.
   *  @param x       real world x coordinate about which to center zoom
   *  @param y       real world y coordinate about which to center zoom
   *  @param percent zoom percent
   *  @return true if zoomed
   */
  public boolean zoomInAtPoint (double x, double y, float percent) {
    percent = checkZoomPercent(percent);
    return zoomInOutAtPoint (x, y, percent);
  }

  /** Centers the plot at the current mouse pos and zooms out by the current {@link #zoomAdjust}
   *  @return true if zoomed */
  public boolean zoomOutAtMouse () {
    Position p = getPosition(px, py);
    return zoomInOutAtPoint(p.x, p.y, 1/zoomAdjust);
  }

  /** Centers the plot at the RWC coords given and zooms out by the percent given.
   *  @param x       real world x coordinate about which to center zoom
   *  @param y       real world y coordinate about which to center zoom
   *  @param percent zoom percent
   *  @return true if zoomed
   */
  public boolean zoomOutAtPoint (double x, double y, float percent) {
    percent = checkZoomPercent(percent);
    return zoomInOutAtPoint(x, y, 1/percent);
  }

  /** Centers the plot at the RWC coords given and zooms the plot by the percent given.
      This works for both zooming in and zooming out because the zoom out
      percentage has been converted prior to calling this method.
      @since NeXtMidas 2.7.2
   */
  private boolean zoomInOutAtPoint (double x, double y, float percent) {
    double oldXDelta = rx2 - rx1;
    double oldYDelta = ry2 - ry1;
    double newXDelta = oldXDelta * percent;
    double newYDelta = oldYDelta * percent;

    rx1 = x - (newXDelta / 2);
    rx2 = x + (newXDelta / 2);
    ry1 = y - (newYDelta / 2);
    ry2 = y + (newYDelta / 2);

    if (rx1 < orx1) { rx1 = orx1; }
    if (rx2 > orx2) { rx2 = orx2; }
    if (ry1 < ory1) { ry1 = ory1; }
    if (ry2 > ory2) { ry2 = ory2; }

    setup();
    refresh();
    return true;
  }

  /** Sets the state for whether or not mouse-wheel zooming is allowed
      @param val the new state of mouse-wheel zooming
   */
  public void setMouseWheelZoom (boolean val) { mouseWheelZoom = val; }

  /** Gets the current state of whether or not mouse-wheel zooming is allowed
      @return the current state of mouse-wheel zooming
   */
  public boolean getMouseWheelZoom () { return mouseWheelZoom; }

  /** Sets the state for whether or not double-click zooming is allowed
      @param val the new state of double-click zooming
   */
  public void setDblClickZoom (boolean val) { dblClickZoom = val; }

  /** Gets the current state for whether or not double-click zooming is allowed
      @return the current state of double-click zooming
   */
  public boolean getDblClickZoom () { return dblClickZoom; }

  /** Sets the new percentage to adjust Zoomed view when using arrow keys
      @param value the new percentage to adjust Zoomed view when using arrow keys
   */
  public void setZoomAdjust (float value) { zoomAdjust = value; }

  /** Gets the current percentage to adjust Zoomed view when using arrow keys
      @return the current percentage to adjust Zoomed view when using arrow keys
   */
  public float getZoomAdjust () { return zoomAdjust; }

  /** The percentage by which to ZOOM in/out when doing a percentage
      ZOOM (ZOOMIN, ZOOMOUT, '+'/'-' keypresses, etc).
      @return The percentage to zoom in or out (.1=10%, .5=50%, etc)
      @since neXtMidas 3.3.2
   */
  public float getZoomPercentage () { return zoomPercent; }

  /** Set the percentage by which to ZOOM in/out when doing a percentage
      ZOOM (ZOOMIN, ZOOMOUT, '+'/'-' keypresses, etc).
      @param percent Percentage by which to zoom (must be between 0 and 1, with .1=10%, .5=50%, etc)
      @since neXtMidas 3.3.2
   */
  public void setZoomPercentage (float percent) { if (percent>0 && percent<=1) zoomPercent = percent; }

  /** Sets the amount to multiply the zoom adjustment
      @param mult the new amount to multiply the zoom adjustment
   */
  public void setZoomMult (float mult) { zoomMult = mult; }

  /** Gets the current amount to multiply the zoom adjustment
      @return the current amount to multiply the zoom adjustment
   */
  public float getZoomMult () { return zoomMult; }

  /** Sets whether or not to keep the zoom level when a new layer is added
      @param keep new state for whether or not to keep the zoom level when a new layer is added
   */
  @InternalUseOnly
  public void setZoomKeep (boolean keep) { zoomKeep = keep; }

  /** Gets the current state for whether or not to keep the zoom level when a new layer is added
      @return whether or not to keep the zoom level when a new layer is added
   */
  @InternalUseOnly
  public boolean getZoomKeep () { return zoomKeep; }

  /** Move the viewable area of the zoomed plot
      @param dir the direction to move
      @return true if moved
      @since NeXtMidas 2.7.2
   */
  public boolean adjustZoom (String dir) {
    return adjustZoom(dir, zoomAdjust);
  }

  /** Move the viewable area of the zoomed plot. creates a PANXY message.
      @param dir the direction to move
      @param percent the percentage to move
      @return true if moved
      @since NeXtMidas 2.7.2
   */
  public boolean adjustZoom (String dir, float percent) {
    if (Tolerance.equals(rx1,orx1) && Tolerance.equals(rx2,orx2) &&
        Tolerance.equals(ry1,ory1) && Tolerance.equals(ry2,ory2)) return false;  // not zoomed - since 3.7.1 for NXM-2937
    percent = checkZoomPercent(percent);
    double rx1Temp = rx1;
    double rx2Temp = rx2;
    double ry1Temp = ry1;
    double ry2Temp = ry2;
    double rxDelta = rx2 - rx1;
    double ryDelta = ry2 - ry1;
    double amtX    = rxDelta * percent;
    double amtY    = ryDelta * percent;
    double[] obj   = new double[4];

    if (dir.equals("UP")) {
      ry1Temp += amtY;
      ry2Temp += amtY;
    } else if (dir.equals("DOWN")) {
      ry1Temp -= amtY;
      ry2Temp -= amtY;
    } else if (dir.equals("LEFT")) {
      rx1Temp -= amtX;
      rx2Temp -= amtX;
    } else if (dir.equals("RIGHT")) {
      rx1Temp += amtX;
      rx2Temp += amtX;
    }

    obj[0] = rx1Temp;
    obj[1] = rx2Temp;
    obj[2] = ry1Temp;
    obj[3] = ry2Temp;
    Message msg = new Message("PANXY",2,obj);  // convert to PANXY msg for consistency
    mh.processMessage(msg);

    return true;
  }

  /** The bounds will be adjusted based on view mode.
   *  Generally dragboxes are cut off to outer bounds of plot
   *  ONLY should be used for LatLon Views at this point
   *
   *  @param rx1Temp - The in process rx1 value
   *  @param rx2Temp - The in process rx2 value
   *  @param ry1Temp - The in process ry1 value
   *  @param ry2Temp - The in process ry2 value
   *  @return An array of doubles of the adjusted borders in order {rx1Temp, rx2Temp, ry1Temp, ry2Temp}
   *  @since NeXtMidas 3.9.1 pulled out and corrected
   */
  private double[] adjustBorders (double rx1Temp, double rx2Temp, double ry1Temp, double ry2Temp ) {
    if (!isLatLonView()) return new double[] {rx1Temp, rx2Temp, ry1Temp, ry2Temp};
    double rxDelta  = rx2 - rx1;
    double ryDelta  = ry2 - ry1;
    double orxDelta = orx2 - orx1;
    double oryDelta = ory2 - ory1;
    double margin = orx1 - rx1;

    if (!(view==MPlot.V_LATLONCONTINUOUS)) {
      // Ensure the values do not move beyond the original borders
      if (rx1Temp < orx1) { rx1Temp = orx1; rx2Temp = rx1Temp +  (Math.min(orxDelta, rxDelta)); }
      if (rx2Temp > orx2) { rx2Temp = orx2; rx1Temp = rx2Temp -  (Math.min(orxDelta, rxDelta)); }
      if (ry1Temp < ory1) { ry1Temp = ory1; ry2Temp = ry1Temp +  (Math.min(oryDelta, ryDelta)); }
      if (ry2Temp > ory2) { ry2Temp = ory2; ry1Temp = ry2Temp -  (Math.min(oryDelta, ryDelta)); }
    }
    else {
      // Never go below -180 or greater than 540 - Lat Lon Continouous added in 3.9.0 for NXM-3284
      if ((rx2Temp - rx1Temp) > rxDelta+.0001) {
        if (rx2Temp > MPlot.LONGITUDE_CONTINUOUS_MAX ) {
          rx2Temp = rx2Temp - LONGITUDE_RANGE;
          rx1Temp = rx1Temp - LONGITUDE_RANGE;
        }
        else if (rx1Temp < MPlot.LONGITUDE_CONTINUOUS_MIN) {
          rx1Temp = rx1Temp + LONGITUDE_RANGE;
          rx2Temp = rx2Temp + LONGITUDE_RANGE;
        }
        else {
          rx2Temp = rx1Temp + LONGITUDE_RANGE;
        }
        orx1 = rx1Temp;
        orx2 = rx1Temp + LONGITUDE_RANGE;
        rstk[0] = rx1; // need to reset outer bounds of zoom out
        rstk[1] = rx2;
      }
      else if (rx1Temp < orx1 - margin) {
        orx1 = rx1Temp + margin;
        while (orx1 < LONGITUDE_CONTINUOUS_MIN) {
          orx1 += LONGITUDE_RANGE;
        }
        rx1Temp = orx1 - margin;
        rx2Temp = rx1Temp + rxDelta;
        orx2 = orx1 + LONGITUDE_RANGE;
        rstk[0] = rx1Temp; // need to reset outer bounds of zoom out
        rstk[1] = rx2Temp;
      }
      else if (rx2Temp > orx2 + margin) {
        orx2 = rx2Temp - margin;
        while (orx2 > LONGITUDE_CONTINUOUS_MAX) {
          orx2 -= LONGITUDE_RANGE;
        }
        rx2Temp = orx2+margin;
        rx1Temp = rx2Temp - rxDelta;
        orx1 = orx2 - LONGITUDE_RANGE;
        rx2Temp = orx2 + margin;
        rx1Temp = rx2Temp - rxDelta;
        rstk[0] = rx1Temp; // need to reset outer bounds of zoom out
        rstk[1] = rx2Temp;
      }
      if (ry1Temp < ory1) { ry1Temp = ory1; ry2Temp = ry1Temp + (Math.min(oryDelta, ryDelta)); }
      if (ry2Temp > ory2) { ry2Temp = ory2; ry1Temp = ry2Temp - (Math.min(oryDelta, ryDelta)); }

      // since 3.9.3 - update limits table so openfile does not reset bounds NXM-3577
      setOuterLimits(getOuterBounds());
      setViewLimits(getViewLimits());
    }
    return new double[] {rx1Temp, rx2Temp, ry1Temp, ry2Temp};
  }

  /** Zoom In incrementally
      @return TRUE
      @since NeXtMidas 2.7.2
      @see MPlot#getZoomPercentage()
   */
  public boolean zoomIn () {
    // changed in 3.3.2 to have adjustable percentage
    return zoomIn(getZoomPercentage());
  }

  /** Zoom In incrementally by the given amount
      @param percent The percentage to zoom in (number between 0 and 1)
      @return TRUE
      @since NeXtMidas 2.7.2

   */
  public boolean zoomIn (float percent) {
    percent = checkZoomPercent(percent);  // check for invalid values
    percent = 1 - percent;  // convert to percentage
    return zoomInOut(percent);
  }

  /** Zoom Out incrementally
      @return FALSE if attempting to ZOOM OUT beyond borders of the file, else TRUE
      @since NeXtMidas 2.7.2
      @see MPlot#getZoomPercentage()
   */
  public boolean zoomOut () {
    return zoomOut(getZoomPercentage());  // changed in 3.3.2 to have adjustable percentage
  }

  /** Zoom Out incrementally by the given percentage.
      Since NeXtMidas 3.3.2: zooming all the way out (-1) will clear/reset the zoom level history.
      @param percent The percent to ZOOM out by or -1 to zoom all the way out
      @return FALSE if attempting to ZOOM OUT beyond borders of the file, else TRUE
      @since NeXtMidas 2.7.2
   */
  public boolean zoomOut (float percent) {
    double p = .00001;  // precision
    if (rx1 < (orx1+p) && rx1 > (orx1-p) && rx2 < (orx2+p) && rx2 > (orx2-p) &&
        ry1 < (ory1+p) && ry1 > (ory1-p) && ry2 < (ory2+p) && ry2 > (ory2-p)) {
      return false;
    }
    if (percent == -1) return zoomInOut(percent); // Allow -1 for ZOOM completely out
    else percent = checkZoomPercent(percent);  // check for invalid values
    percent = 1 / (1 - percent);  // percent converted to 1/percent when zooming out
    return zoomInOut(percent);
  }

  /** Method that performs the incremental ZOOM IN/OUT.
      Since NeXtMidas 3.3.2: zooming all the way out (-1) will clear/reset the zoom level history.
      @param  percent percentage to ZOOM IN/OUT by or -1 to zoom all the way out
      @return FALSE   if attempting to ZOOM OUT beyond borders of the file, else TRUE
      @since NeXtMidas 2.7.2
   */
  private boolean zoomInOut (float percent) {
    boolean ok = true;

    // -1 == zoom all the way out. Reset values
    if (percent == -1) {
      rx1 = rstk[0]; rx2 = rstk[1];
      ry1 = rstk[2]; ry2 = rstk[3];
      rz1 = rstk[4]; rz2 = rstk[5];
      setViewLimits("X1", rx1);  setViewLimits("X2", rx2);
      setViewLimits("Y1", ry1);  setViewLimits("Y2", ry2);
      setViewLimits("Z1", rz1);  setViewLimits("Z2", rz2);
      zoomed = false; // zoomed all the way out
      setup();
      refresh();
      reset(-1);   // reset zoom level
      level = lastLevel = 0; // since 3.3.2: reset zoom level history (BUG 2743)
      return true;
    }

    double oldXDelta = Math.abs(rx2-rx1);
    double oldYDelta = Math.abs(ry2-ry1);

    double centerX = (rx1 + (oldXDelta / 2));
    double centerY = (ry1 + (oldYDelta / 2));

    // New delta is scaled based on percent given
    double newXDelta = oldXDelta * percent;
    double newYDelta = oldYDelta * percent;

    boolean centeringOnMouse = false; // default to old behavior of centering on map center
    double rx1New=rx1, rx2New=rx2, ry1New=ry1, ry2New=ry2; // use temp values until we are sure this zoom will work

    // /USEMOUSEPOSITION switch is set
    if (useMousePosition) {
      // Note: Even with key stroke panning and zooms, the values obtained here should be the same as px,py
      // It would take something like resizing the screen without moving the mouse to need the mouse
      // pixel position to be recalculated - since 3.9.1 for NXM-3339
      int mousePixelX = (int) MouseInfo.getPointerInfo().getLocation().getX() - panel.getLocationOnScreen().x;
      int mousePixelY = (int) MouseInfo.getPointerInfo().getLocation().getY() - panel.getLocationOnScreen().y;
      // Convert pixel coordinates to RWC
      Position mousePos = getPosition(mousePixelX,mousePixelY);

      // If we are inside the plot, we will zoom on the point. We use rx1, rx2, ry1, and ry2 to account for being on the
      // actual plot and not the x and y text area
      // Only center on Mouse if inside plot
      centeringOnMouse = ((mousePos.x > Math.max(orx1, rx1)) && (mousePos.x < Math.min(orx2, rx2)) &&
                          (mousePos.y > Math.max(ory1, ry1)) && (mousePos.y < Math.min(ory2, ry2))); // 3.9.1 do not allow centering beyond bounds

      if (centeringOnMouse) {
       // Do Google style zooming, rather than center zoom
        boolean onLeft = (mousePos.x - rx1) < (rx2 - mousePos.x);
        boolean onTop = (mousePos.y - ry1) < (ry2 - mousePos.y);
        if (onLeft) {
          rx1New = mousePos.x - (percent) * (mousePos.x - rx1);
          rx2New = rx1New + newXDelta;
        }
        else {
          rx2New = mousePos.x + ((percent) * (rx2 - mousePos.x));
          rx1New = rx2New - newXDelta;
        }
        if (onTop) {
          ry1New = mousePos.y - (percent) * (mousePos.y - ry1);
          ry2New = ry1New + newYDelta;
        }
        else {
          ry2New = mousePos.y + ((percent) * (ry2 - mousePos.y));
          ry1New = ry2New - newYDelta;
        }
      }
    }

    // Centering on center of plot is the default
    if (!centeringOnMouse) {
     // Set coords to new points based on centering zoom on map center
      rx1New = centerX - (newXDelta / 2);
      rx2New = centerX + (newXDelta / 2);
      ry1New = centerY - (newYDelta / 2);
      ry2New = centerY + (newYDelta / 2);
    }

    // Zooming in beyond pixel resolution was causing the zoom to get lost
    // The switch /USEMOUSEPOSITION made this issue easier to experience
    // Do not keep zooming when we have zoomed in the maximum (or a little too far)
    // Since 3.9.1 for NXM-3339
    if (((ry1New-ry2New)==0) || ((rx1New-rx2New)==0)) {
      return true; // since false means zooming beyond outside borders
    }
    else {
      // now set the values, since we know they are good
      rx1=rx1New;
      rx2=rx2New;
      ry1=ry1New;
      ry2=ry2New;
    }

    // do not zoom out beyond the original boundaries
    if (rstk[0] < rstk[1]) {     // orig x1 < x2
      if (rx1 < rstk[0]) { rx1 = rstk[0]; ok = false; }
      if (rx2 > rstk[1]) { rx2 = rstk[1]; ok = false; }

    } else {                     // orig x1 > x2
      if (rx1 > rstk[0]) { rx1 = rstk[0]; ok = false; }
      if (rx2 < rstk[1]) { rx2 = rstk[1]; ok = false; }
    }
    if (rstk[2] < rstk[3]) {     // orig y1 < y2
      if (ry1 < rstk[2]) { ry1 = rstk[2]; }
      if (ry2 > rstk[3]) { ry2 = rstk[3]; }
    } else {                     // orig y1 > y2
      if (ry1 > rstk[2]) { ry1 = rstk[2]; }
      if (ry2 < rstk[3]) { ry2 = rstk[3]; }
    }

    // update the temporary bounds table
    setViewLimits("X1", rx1);
    setViewLimits("X2", rx2);
    setViewLimits("Y1", ry1);
    setViewLimits("Y2", ry2);

    // remake borders and repaint
    zoomed = ok;
    setup();
    refresh();
    return ok;
  }

  /** Check the given incremental ZOOM IN/OUT percentage to ensure it is 0 < n < 1
      @param percent The given float percentage
      @return The given percentage if valid, or {@link #DEF_ZOOM_PERCENTAGE} if not
      @since NeXtMidas 2.7.2
   */
  private float checkZoomPercent (float percent) {
    if (percent <= 0 || percent >= 1) {
      Shell.warning("Invalid percentage: "+percent+". ZOOM IN/OUT percentages must "+
          "be greater than 0 and less than 1. Setting to default: "+DEF_ZOOM_PERCENTAGE);
      percent = DEF_ZOOM_PERCENTAGE;
    }
    return percent;
  }

  /** Zoom into the plot. No zoom takes place once the max zoom level is reached. See {@link #zoom(Object, int, boolean)}
      for what types of Objects can be passed.
      @param rxy  the zoom parameters
      @param mode indicates axis to zoom
      @return true when the zoom is done, false if unable to zoom (usually due to level &gt; max).
   */
  public boolean zoom (Object rxy, int mode) {
    return zoom(rxy,mode,true);
  }

  /** Zoom into the plot.
      @param rxy The zoom parameters :<pre>
                A {@link DragBox} object:
                     (values stored internal to the {@link DragBox})

                or a Table (XMIN,XMAX syntax): [Since NeXtMidas 2.3.0]
                     {XMIN=val1,XMAX=val2,YMIN=val3,YMAX=val4}

                or a Table (X1,X2,Y1,Y2 syntax):
                     {X1=val1,X2=val2,Y1=val3,Y2=val4}

                or a double array:
                     rxy[0] = x1,   rxy[1] = x2
                     rxy[2] = y1,   rxy[3] = y2
                     rxy[4] = rx1*, rxy[5] = rx2*
                     rxy[6] = ry1*, rxy[7] = ry2*
                 * When in GEODETIC mode the first four values are "special values" for the
                   GEODETIC viewer to use and the last four values are the corresponding Lat/Lon
                   values. With a {@link DragBox} or a table, these "special values" are
                   automatically computed based on the Lat/Lon values.
              </pre>
      @param mode Indicates axis to zoom: <pre>
                    0 - Same as 3 (this is the default)
                    1 - X-Axis Only
                    2 - Y-Axis Only
                    3 - X-Axis and Y-Axis</pre>
      @param history Whether or not to apply the zoom to the zoom history. If this is FALSE, then
             the zoom level and history are not updated, therefore any UNZOOMs will only apply to
             ZOOMs that WERE recorded in the history. When ZOOMing outside of the history, instead of UNZOOMs,
             users should just call this method again with the desired coordinates or {@link #zoomOut(float)}
             with a -1 (to zoom all the way out).
      @return true when the zoom is done, false if unable to zoom (usually due to level &gt; max).
      @since NeXtMidas 3.3.2 extracted from {@link #zoom(Object, int)} with history parameter.
   */
  public boolean zoom (Object rxy, int mode, boolean history) {
    if (level>=MAXLEV) return false;
    double x1   =  rx1; // Min X-axis value.
    double x2   =  rx2; // Max X-axis value.
    double y1   =  ry1; // Min Y-axis value.
    double y2   =  ry2; // Max Y-axis value.
    double lon1 = -180; // Min longitude (GEODETIC view only)
    double lon2 =  180; // Max longitude (GEODETIC view only)
    double lat1 =  -90; // Min latitude (GEODETIC view only)
    double lat2 =   90; // Max latitude (GEODETIC view only)
    if (mode == 0) mode = XY_ZOOM;
    boolean xZoom = (mode & X_ZOOM) != 0;
    boolean yZoom = (mode & Y_ZOOM) != 0;
    boolean cutOffZoomToVisibleArea = false;

    // Get new values and use current values as defaults
    if (rxy instanceof DragBox) {
      dragboxAdjustment = true;
      DragBox dragbox = (DragBox)rxy;
      cutOffZoomToVisibleArea = true; // since 3.9.2 - added to prevent drag box zoom outside of visible area
      if (view == V_GEODETIC) {
        x1   = dragbox.getGeoXMin();
        x2   = dragbox.getGeoXMax();
        y1   = dragbox.getGeoYMin();
        y2   = dragbox.getGeoYMax();
        lon1 = dragbox.getXMin();
        lon2 = dragbox.getXMax();
        lat1 = dragbox.getYMin();
        lat2 = dragbox.getYMax();
      } else {
        x1 = dragbox.getXMin();
        x2 = dragbox.getXMax();
        y1 = dragbox.getYMin();
        y2 = dragbox.getYMax();
      }
    }
    else if ((rxy instanceof Data) || (rxy instanceof double[])) { // legacy version (deprecate?)
      x1 = Convert.o2d(rxy,0,rx1);
      x2 = Convert.o2d(rxy,1,rx2);
      y1 = Convert.o2d(rxy,2,ry1);
      y2 = Convert.o2d(rxy,3,ry2);
      if (view == V_GEODETIC) {
        lon1 = Convert.o2d(rxy, 4, -180);
        lon2 = Convert.o2d(rxy, 5,  180);
        lat1 = Convert.o2d(rxy, 7,  -90);
        lat2 = Convert.o2d(rxy, 6,   90);
      }
    }
    else {
      Table tab = Convert.o2t(rxy);
      if (tab != null) {
        x1 = tab.getD("XMIN",tab.getD("X1",rx1));
        x2 = tab.getD("XMAX",tab.getD("X2",rx2));
        y1 = tab.getD("YMIN",tab.getD("Y1",ry1));
        y2 = tab.getD("YMAX",tab.getD("Y2",ry2));
      }
    }
    int oldView = view;
    if ((view == V_GEODETIC) && (options&O_AUTO2D) != 0) {
      // JC4: Check to see if the user has zoomed far enough that we can flip to 2D mode
      //      without them seeing it (the <= 15.0 was an arbitrary choice). This way we can
      //      plot the NGA data without projecting it onto the globe.
      double LIMIT     = 15.0;
      double LON_LIMIT = 180.0 - LIMIT - 2.5;
      double LAT_LIMIT =  90.0 - LIMIT - 2.5;

      if (((Math.abs(lon1-lon2) <= LIMIT) && (Math.abs(lat1-lat2) <= LIMIT))) {
        if ((Math.abs(lon1) > LON_LIMIT)  || (Math.abs(lon2) > LON_LIMIT) ||
            (Math.abs(lat1) > LAT_LIMIT)  || (Math.abs(lat2) > LAT_LIMIT) ) {
          // Too close to edge of LatLon/Mercator map.
        } else {
          setView(V_MERCATOR, false);
          x1 = lon1; x2 = lon2;
          y1 = lat1; y2 = lat2;
        }
      }
    }

    // In 4.1.1 restricting this to the main need of LatLon views
    // A better fix will need to be found if we expand this to X/Y views,
    // as it breaks zooming in after scrolling on a large 1000/2000 file NXM-3846
    if(isLatLonView() && cutOffZoomToVisibleArea) {
      double zoomBoundXMin = Math.max(Math.min(rx1, rx2), Math.min(orx1, orx2));
      double zoomBoundXMax = Math.min(Math.max(rx1, rx2), Math.max(orx1, orx2));
      double zoomBoundYMin = Math.max(Math.min(ry1, ry2), Math.min(ory1, ory2));
      double zoomBoundYMax = Math.min(Math.max(ry1, ry2), Math.max(ory1, ory2));
      // Prevent zooming outside of plot boundaries. Since 3.9.1 - fixed in 3.9.3 for ZOOMX
      if ((xZoom && ((x2<zoomBoundXMin) || (x1>zoomBoundXMax))) ||
          (yZoom && ((y2<zoomBoundYMin) || (y1>zoomBoundYMax)))) return false;
      // Do not zoom past the bounds since 3.9.1
      if (level == 0 && view == V_LATLONCONTINUOUS) {
        // need to save off originals in LatLonContinous, since shift boundaries
        llc_orx1 = orx1;
        llc_orx2 = orx2;
      }
      x1 = Math.max(x1, Math.max(rx1, orx1)); // 3.9.1 changed to have cutoff at visible limits
      x2 = Math.min(x2, Math.min(rx2, orx2)); // 3.9.1 changed to have cutoff at visible limits
      y1 = Math.max(y1, Math.max(ry1, ory1)); // 3.9.1 changed to have cutoff at visible limits
      y2 = Math.min(y2, Math.min(ry2, ory2)); // 3.9.1 changed to have cutoff at visible limits
    }
    if (history) {
      lastLevel = level;
      int i = level*LEVPARAM;

      if (level>0 || !zoomed) {    // BUG 72 / 2104: Level 0 is now set in useRange()
        rstk[i+0] = rx1; rstk[i+1] = rx2;
        rstk[i+2] = ry1; rstk[i+3] = ry2;
        rstk[i+4] = rz1; rstk[i+5] = rz2;
      }
      rstk[i+6] = oldView;
      level++;
      lastLevel = level;
    }

    if (xZoom) {
      rx1 = Math.min(x1,x2);
      rx2 = Math.max(x1,x2);

      // update the temporary user bounds table
      setUserLimits("X1", rx1, false);
      setUserLimits("X2", rx2, false);
    }
    if (yZoom) {
      ry1 = Math.min(y1,y2);
      ry2 = Math.max(y1,y2);

      // update the temporary user bounds table
      setUserLimits("Y1", ry1, false);
      setUserLimits("Y2", ry2, false);
    }
    setup();
    return true;
  }

  /** Adjust view bounds to include maximum map
   *  Designed only for LatLon and LatLonContinuous
   *  Must be called after Aspect is set
   */
  private void shiftMoreLatLonMap () {
    double xRange = rx2 - rx1;
    double yRange = ry2 - ry1;
    double xOuterRange = orx2-orx1;
    double yOuterRange = ory2-ory1;
    boolean xRangeNotFull = xRange < xOuterRange;
    boolean yRangeNotFull = yRange < yOuterRange;
    if (view ==V_LATLON) {
      if (xRangeNotFull) {
        // shift x values, so we are not showing margin
        // this does not apply to LatLonContinuous, as the map is just shifted
        if (rx2 > orx2) {
          rx1 = orx2 - xRange;
          rx2 = orx2;
        }
        else if (rx1 < orx1) {
          rx1 = orx1;
          rx2 = orx1 + xRange;
        }
      }
      else {
        // still minimize margin area by centering
        double margin = (xRange - xOuterRange)/2;
        rx1 = orx1 - margin;
        rx2 = orx2 + margin;
      }
    }
    if ((view==V_LATLON) || (view==V_LATLONCONTINUOUS)) {
      if (yRangeNotFull) {
        // shift y values, so we are not showing margin
        if (ry2 > ory2) {
          ry1 = ory2 - yRange;
          ry2 = ory2;
        }
        else if (ry1 < ory1) {
          ry1 = ory1;
          ry2 = ory1 + xRange;
        }
      }
      else {
        // still minimize margin area by centering
        double margin = (yRange - yOuterRange)/2;
        ry1 = ory1 - margin;
        ry2 = ory2 + margin;
      }
    }
  }

  /** Unzoom to the previous zoom level.
      @return false if no zoom is required, true if a zoom has been done.
   */
  public boolean unzoom () { return unzoom(level-1); }

  /** Unzoom to a given zoom level.
      @param newlevel The zoom level.
      @return false if no zoom is required, true if a zoom has been done.
   */
  public boolean unzoom (int newlevel) {
    return unzoom(newlevel, false);
  }

  /** Unzoom to a given zoom level.
      @param newlevel The zoom level.
      @param doReset Set this to true prevent the restoration of any previous view options.
      @return false if no zoom is required, true if a zoom has been done.
   */
  private boolean unzoom (int newlevel, boolean doReset) {
    if (level<=0 || level<=newlevel) return false;
    lastLevel = level;
    level = newlevel;
    int i = level*LEVPARAM;
    rx1 = rstk[i+0]; rx2 = rstk[i+1];
    ry1 = rstk[i+2]; ry2 = rstk[i+3];
    rz1 = rstk[i+4]; rz2 = rstk[i+5];
    if(view==V_LATLONCONTINUOUS) {
      // Since 3.9.1 when zoomed all the way out change the values to be the original "or" values.
      if(level<=0) {
        rx1 = orx1 = llc_orx1; // since outer boundaries are adjusted during LatLonContinuous zooms - fixed 3.9.3
        rx2 = orx2 = llc_orx2;
        ry1 = ory1;
        ry2 = ory2;
      }
      reset(-1); // Needed to clear backing store when zooming out. The backing store image may not be lined up with the new lat lon values.
    }

    viewLimits.clear();      // remove any settings from the user bounds table

    setup();
    // JC4: Restore view if it was GEODETIC, usually setView returns false and refresh() is set.
    if (!doReset) if (!(setView ((int)rstk[i+6], false))) refresh();
    lastLevel = level;
    return true;
  }


  /**
   * Add a Layer
   * @param name  name of the Layer
   * @param layer Layer to add
   */
  public void addLayer (String name, Layer layer) {
    layer.setName(name.toUpperCase());
    addLayer (layer);
  }

  /**
   * Add a Layer
   * @param layer Layer to add
   */
  public void addLayer (Layer layer) {
    layer.setMPlot(this);
    if ((layerDrawable == null) && (layer instanceof LayerDrawable)) {
      layerDrawable = (LayerDrawable)layer;
    }
    // If the Layer is part of a LayerDrawable group, add it to
    //  the LayerDrawable. If not, add it to the MPlot layers array.
    String parentName = layer.getGroup();
    if (parentName==null || parentName.isEmpty()) {
      addToLayersArray(layer);
    }
    else {
      LayerDrawable parent = (LayerDrawable) layers.get(parentName);
      if (parent != null) {
        parent.addDrawable(layer);
        if (baseLayer==null) layer.makeBaseLayer();
      }
      else {
        addToLayersArray(layer);;  // in case parent does not exist
      }
    }
    if (!zoomKeep) reset(-1); // 3.9.4

    /* Set the units based on the base layer but do so only once. */
    if ((!unitsInit) && layer.isBaseLayer()) {
      setXYZUnits(layer.getXUnits(),layer.getYUnits(),layer.getZUnits(),true);  // use new 4 parameter method with refresh=true - Since 3.6.0 for Bugs 2894 & 2431
      unitsInit = true;
    }
  }

  private void addToLayersArray (Layer lay) {
    layers.put(lay.name,lay);
    if (lay.getClassName().equals("LayerWMS")) wmsLayerCnt++;

    // If the Base Layer is not set, or if it has been set to the same file, then reset it (needed because plot calls this twice)
    if ((baseLayer==null || (baseLayer!=null && baseLayer.name.equals(lay.name))) && layers.size()>=2) {
      lay.makeBaseLayer();  // Base Layer concept added in 3.5.0 for Bug 2792
    }
  }

  /**
   * Remove a Layer
   * @param name  name of Layer to remove
   */
  public void removeLayer (String name) {
    if(name == null) { // since 3.6.0 for Bug 2911: if a layer name is null, don't try to remove it
      return;
    }
    name = name.toUpperCase();
    if (name.equals("GRID")) return;
    Layer lay = (Layer)layers.get(name);
    if (lay.isBaseLayer()) nullArchivedFrames(); // If the baseLayer is being updated, set the archivedFrames buffer array to null
    if (lay!=null) {
      if ("LayerWMS".equals(lay.getClassName())) wmsLayerCnt--;
      lay.close();
    }
    if (lay.isBaseLayer()) baseLayer = null;  //Since 3.6.0 for Bugs 2894 and 2431
    layers.remove(name);
    if (!zoomKeep) reset(-1);
  }

  /** Sets the name of a feature and adds a feature to the plot.
      @param name    The name of the feature.
      @param feature The feature.
      @see #addFeature(Feature)
   */
  public void addFeature (String name, Feature feature) {
    feature.setName(name.toUpperCase());
    addFeature(feature);
  }

  /** Adds a feature to the plot.
      @param feature The feature.
      @see #addFeature(String,Feature)
      @see #addFeature(Table)
      @see #removeFeature(String)
   */
  public void addFeature (Feature feature) {
    Layer lay = getBaseLayer();
    PlotFile pf=null;
    if (lay != null) pf = lay.pf;
    // Warning below can be removed after BUG 2007 gets a final resolution
    if ( (lay != null) && (lay.typeClass==1) && (feature.isType(Feature.DATA))
      && (pf != null) && getOption(O_ALL) && (lay.frame < lay.pf.getSize())  ) {
      double elems = lay.pf.getSize();
      int frames = lay.frame;
      Shell.warning("FrameSize="+frames+" and No. of Elements="+elems+". When using '/ALL' switch, Data Features on "+
          "Layer 1D plots do not display if frame size is less than number of elements in file. "+
          "Use '/FS=' to increase frame size greater than "+elems+", if possible.");
    }
    feature.setPlot(this);
    // If the feature is part of a LayerDrawable group, add it to that LayerDrawable,
    //  or else add it to the MPlot features array.
    Layer featLayer = feature.getLayer();
    if (featLayer instanceof LayerDrawable) {
      ((LayerDrawable)featLayer).addDrawable(feature);
    }
    else {
      features.put(feature.getName().toUpperCase(),feature);
    }
    needRefresh = true;
  }

  /** Adds the feature to the group set in the Feature object.
   *
   *  @param feature The feature to add to the group
   *  @since NeXtMidas 3.9.3
   */
  public void addFeatureToGroup (Feature feature) {
    String groupName = feature.getGroup();
    if (groupName==null) {
      Shell.warning("Feature need group name to added to FeatureGroup "+feature);
    }
    else if (featureGroups.get(feature.getGroup())!=null) {
      // this Feature is all set and knows the group it belongs to
      featureGroups.addFeatureToGroup(feature, featureGroups.get(feature.getGroup()));
    }
    else {
      // The Feature needs a group created with the group name
      featureGroups.addFeatureToGroup(feature, new FeatureGroup(feature));
    }
  }

  /** Move Feature from one group to another
   *
   *  @param oldGroup The group to move the feature from
   *  @param feature The feature - the new group is in the feature object
   *  @since NeXtMidas 3.9.3
   */
  @InternalUseOnly
  public void moveFeatureToGroup (String oldGroup, Feature feature) {
    featureGroups.moveFeatureToGroup(oldGroup, feature);
  }

  /** Move all features from one group to another
   *
   *  @param oldGroup The source group of features
   *  @param newGroup The destination group of features
   *  @since NeXtMidas 3.9.3
   */
  @InternalUseOnly
  public void moveAllFeatures (String oldGroup, String newGroup) {
    featureGroups.moveAllFeatures(oldGroup, newGroup);
  }

  /** Get the Feature Groups
   *
   *  @return the Feature Groups
   *  @since NextMidas 3.9.3
   */
  @InternalUseOnly
  public Map<String,FeatureGroup> getFeatureGroups () {
    return featureGroups;
  }

  /** Add a Table of features to a plot.
      @param tfeats Table of features to add.
      @see #addFeature(Feature)
   */
  public void addFeature (Table tfeats) {
    Table.Iterator ti=tfeats.iterator();
    for (int i=0; i<ti.size; i++) {
      addFeature((Feature)ti.values[i]);
    }
  }

  /** Removes a feature from the plot.
      @param name The name of the feature.
      @see #addFeature(String, Feature)
   */
  public void removeFeature (String name) {
    name = name.toUpperCase();
    Feature feature = (Feature) features.get(name);
    if (feature == null) return;
    // need to remove the feature from group since 3.9.3
    if (feature.getGroup()!=null && !feature.getGroup().isEmpty()) {
      FeatureGroup fg = featureGroups.get(feature.getGroup());
      if (fg!=null) fg.removeFeature(feature);
    }

    features.remove(name);
    refresh();
  }

  /** Removes all features from the plot.
      @see #removeFeature(String)
      @since NeXtMidas 2.7.4 for Bug 2041
   */
  public void removeAllFeatures () {
    features.clear();
    featureGroups.clear(); // added in 3.9.3
    refresh();
  }

  /** Load features from a file.
      @param filename The name of the file.
   */
  public void loadFeatures (String filename) {
    FileName fnFilename = new FileName(filename,FNCase.KeepCase);
    TextFile tf = new TextFile(fnFilename);
    Table t = new Table();
    t.fromTextFile(tf);
    loadFeatures(t);
  }

  /** Load features from a table.
      @param table The table of features.
   */
  public void loadFeatures (Table table) {
    Table.Iterator ti=table.iterator();
    for (int i=0; i<ti.size; i++) {
      Table ft = Convert.o2t(ti.values[i]);
      Feature f = new Feature(ft);
      addFeature(f);
    }
  }

  /** Save features to a file.
      @param filename The name of the file.
   */
  public void saveFeatures (String filename) {
    if (filename.indexOf('.')<0) filename += ".tbl";   // add .tbl if no extension given (Bug 2263)

    Table ft = new Table();
    for (int i=0; i<features.size(); i++) {
      Feature f = (Feature)features.get(i);
      ft.put (f.getName(), f.toTable());
    }
    FileName fnFilename = new FileName(filename,FNCase.KeepCase);
    TextFile tf = new TextFile(fnFilename);
    ft.toTextFile(tf);
    Shell.info("Saved FEATURES to: "+tf.getURL()); // since 3.7.0
  }

  /** Reset a given zoom level, clearing out any backing store images for the level.
      @param n The level to reset (-1=ResetAll).
   */
  public void reset (int n) {
    boolean unzooming = level<lastLevel;
    // null the Backing Store buffers */
    if (n<0) for (int i=0; i<images.length; i++) images[i]=null;
    else if (unzooming) for (int i=lastLevel; i>level; i--) images[i]=null;
    else images[n]=null;
    dbimage=null;
    // reset individual layers
    if (unzooming && n>=0 && images[n]!=null) {} // in backing store
    //else if (layers!=null) { for (int i=0; i<layers.size(); i++) ((Layer)layers.get(i)).reset(); }
  }

  /** Reset all Plot layers before refreshing */
  public void resetForRefresh() {
    if ( (options&O_BSTORE)==0 ) {
      for (int i=1; i<layers.size(); i++) ((Layer)layers.get(i)).reset();
    }
  }

  /** Draw a feature or a layer. This can be called with {@link #DO_CHECK} to do a very basic check
   *  to see if drawing is needed. This method should only be called by {@link MPlot} or
   *  {@link LayerDrawable}. When called by {@link LayerDrawable#draw(int)} the flags passed in
   *  will be set to {@link #DO_FORCE_DRAW} | {@link #DO_DRAW} since the backing-store checks have already been done.
   *  @param d        The item to draw.
   *  @param drawmode The draw mode (see {@link Drawable}).
   *  @param flags    This will be {@link #DO_CHECK}, or a mask value based on the set of draw flags
   *                  ({@link #DO_DRAW}, {@link #DO_BS}, {@link #DO_NBS}, {@link #DO_FORCE_DRAW}).
   *                  Always pass in {@link #DO_DRAW} when passing in {@link #DO_FORCE_DRAW}.
   *  @return true if the item is a draw candidate, false otherwise.
   */
  boolean doDraw (Drawable d, int drawmode, int flags) {
    if (d == null) return false; // since 3.1.1 to prevent occasional NPE for BUG 2500
    int en = d.getEnable();
    if ((en&EN_GLOBAL)==0) {
      if ((flags&DO_DRAW)!=0) d.clearNeedRefresh(); // clear refresh flags without drawing
      return false;
    }
    boolean doit = true;
    //boolean inbs = drawmode!=Drawable.FAST && (options&O_BSTORE)!=0 && (lay.enable&EN_BSTORE)!=0;
    boolean inbs = (options&O_BSTORE)!=0 && (en&EN_BSTORE)!=0; // true if feature is in backing store and PLOT has +BS
    if ((flags&DO_BS)!=0) doit = inbs;
    if ((flags&DO_NBS)!=0) doit = !inbs;
    if ((flags&DO_DRAW)!=0 && (doit || ((flags&DO_FORCE_DRAW)!=0))) {
      lastColor=null;
      lastLine=null;
      if (drawmode==Drawable.FULL) d.reset();
      d.draw(drawmode);
    }
    return doit;
  }

  /**
   * Resets the orx|y|z values (outer bounds)
   * - checks each layer's min and max axis value and expands outer bounds when
   *   a layer's range goes beyond the current min/max
   * - Note: Geodetic and XYZ views have their outer bounds initially set based
   *   in min/max axis values given in degrees, but updates are specified based on
   *   the display range of -1.2 to 1.2, which is also the 'full range' (see issue NXM-2940)
   */
  public void findRange () {
    int istart=1; Layer layer;
    if (view==V_GEODETIC) istart=0;
    for (int i=istart; i<layers.size(); i++) {
      layer = (Layer)layers.get(i);
      if (!doDraw(layer, 0, DO_CHECK)) continue;
      layer.rescale();
      if (i==istart) {
        orx1=layer.x1; orx2=layer.x2;
        ory1=layer.y1; ory2=layer.y2;
        orz1=layer.z1; orz2=layer.z2;
      } else {
        if (layer.x1<orx1) orx1=layer.x1;
        if (layer.x2>orx2) orx2=layer.x2;
        if (layer.y1<ory1) ory1=layer.y1;
        if (layer.y2>ory2) ory2=layer.y2;
        if (layer.z1<orz1) orz1=layer.z1;
        if (layer.z2>orz2) orz2=layer.z2;
      }
      layer.findFullRange();
      if (i==istart) {
        frx1=layer.fx1; frx2=layer.fx2;
        fry1=layer.fy1; fry2=layer.fy2;
        frz1=layer.fz1; frz2=layer.fz2;
      } else {
        if (layer.fx1<frx1) frx1=layer.fx1;
        if (layer.fx2>frx2) frx2=layer.fx2;
        if (layer.fy1<fry1) fry1=layer.fy1;
        if (layer.fy2>fry2) fry2=layer.fy2;
        if (layer.fz1<frz1) frz1=layer.fz1;
        if (layer.fz2>frz2) frz2=layer.fz2;
      }
      if ((options&O_ALL)!=0) {
        orx1=frx1; orx2=frx2;
        ory1=fry1; ory2=fry2;
        orz1=frz1; orz2=frz2;
      }
    }
    if (view==V_POLAR) {
      double r = Math.max(Math.abs(ory1),Math.abs(ory2));
      orx1=-r; orx2=r; ory1=-r; ory2=r;
    }
  }

  /**
   * Resets the  rx|y|z values (view bounds)
   * - the reset of the view bounds is generally based on 'outer limits'
   * - Geodetic and XYZ are exceptions and have their view and full range always set to -1.2 to 1.2
   * - 3DGraph - full range and view set to -2.5 to 2.5
   */
  public void useRange () {
    if (view==V_GEODETIC) {
      rx1=-1.2; rx2=1.2; ry1=-1.2; ry2=1.2;
      frx1=rx1; frx2=rx2; fry1=ry1; fry2=ry2;
    }
    else if (is3DView()) {
      double rangeMax = (view==V_3DGRAPH) ? 2.5 : 1.2; // Make the max val be 1.75 for V_3DGraph and 1.2 for V_XYZ
      rx1=ry1=rz1=-rangeMax;
      rx2=ry2=rz2=rangeMax;
      if (orz1<=0 && orz2>=0) { // center on zero
         double z=Math.max(orz2,-orz1); orz1=-z; orz2=z; }
      frx1=rx1; frx2=rx2; fry1=ry1; fry2=ry2; frz1=rz1; frz2=rz2;
    }
    else {
      // if values are equal, adjust value by the greater of 1 or Math.ulp - since 3.7.1 for NXM-2892
      rx1=orx1; rx2=orx2; if (rx1==rx2) { double xUlp=Math.ceil(Math.ulp(((float)rx1))); rx1-=xUlp; rx2+=xUlp; }
      ry1=ory1; ry2=ory2; if (ry1==ry2) { double yUlp=Math.ceil(Math.ulp(((float)ry1))); ry1-=yUlp; ry2+=yUlp; }
      rz1=orz1; rz2=orz2; if (rz1==rz2) { double zUlp=Math.ceil(Math.ulp(((float)rz1))); rz1-=zUlp; rz2+=zUlp; }

      if ((scale&S_RANGE)==S_RANGE) {
        if (ry2-ry1>maxRange) ry1 = ry2-maxRange;
        if (ry2-ry1<minRange) ry1 = ry2-minRange;
      }
      else if ((scale&S_RANGE)==S_MAXRANGE)  ry1 = ry2-maxRange;
      else if ((scale&S_RANGE)==S_MINRANGE)  ry2 = ry1+minRange;

      if ((scale&S_MAXZRANGE)!=0)      rz1 = rz2-maxRange;
      if ((scale&S_MINZRANGE)!=0)      rz2 = rz1+minRange;
    }

    // Adjust values to ensure all data is displayed (Bug 2478)
    rx1 -= axisBuffer;
    rx2 += axisBuffer;

    if (view==MPlot.V_LATLONCONTINUOUS) { // since 3.9.0 for NXM-3284
      // do not allow bounds to shift beyond valid minimum and range
      if (orx1 < MPlot.LONGITUDE_CONTINUOUS_MIN) {
        double invalidShift = MPlot.LONGITUDE_CONTINUOUS_MIN - rx1;
        rx1 = orx1 = MPlot.LONGITUDE_CONTINUOUS_MIN;
        rx2 = orx2 = Math.max(rx2 + invalidShift, MPlot.LONGITUDE_DEFAULT_MAX);
      }
      if ((orx2 - orx1) > MPlot.LONGITUDE_RANGE) {
        rx1 = orx1 = orx2 - MPlot.LONGITUDE_RANGE;  // probably more complex 3.9.0
      }
    }
    ry1 -= axisBuffer;
    ry2 += axisBuffer;

      // BUG 72: In case ZOOM occurs before plot initially drawn
    rstk[0]=rx1; rstk[1]=rx2;
    rstk[2]=ry1; rstk[3]=ry2;
    rstk[4]=rz1; rstk[5]=rz2; // Added in 3.1.0
    setup();
  }

  /** Apply any changes to the bounds that the user may have made. These include
      outer bounds settings (those made from the command line or by setting
      {@link #outerLimits} table) as well as viewport bounds settings (those made from
      calls to ZOOM or {@link #setXBounds(double, double)}, {@link #setYBounds(double, double)}, etc.
      @param type Denotes which changes to apply (outer or viewport or both(all)).
      @since NeXtMidas 2.7.3
   */
  public void applyUserBounds (String type) {
    /* JWB: This method is broken down like this (outer vs view) to correspond with calls
       to findRange(), which changes outer bounds values, and useRange(), which
       changes view bounds settings.     */
    type = type.toUpperCase();
    boolean changedView = false;  // denotes something has changed so we need to call setup() and refresh()
    boolean changedOuter = false;  // denotes something has changed so we need to call setup() and refresh()
    double tmp;  // temp value used to hold table values so they can be compared with existing before applying

    // get any user-set outer bounds settings
    // TODO: outerLimits for Geodetic and XYZ views should be specified in the range -1.2 to 1.2 (display limits),
    // but or.. values are initialized in degrees.  This can cause an inconsistency.  (see NXM-2940)
    if (type == null || type.equals("ALL") || type.equals("OUTER")) {
      if (outerLimits.containsKey("X1"))  {
        tmp = outerLimits.getD("X1");
        if (tmp != orx1) {
          orx1=tmp;
          changedOuter=true;
          if (view == MPlot.V_LATLONCONTINUOUS) { // since 3.9.0 for NXM-3284
            rstk[0] = llc_orx1 = orx1;
          }
        }
      }
      if (outerLimits.containsKey("X2"))  {
        tmp = outerLimits.getD("X2");
        if (tmp != orx2) {
          orx2=tmp;
          changedOuter=true;
          if (view == MPlot.V_LATLONCONTINUOUS) { // since 3.9.0 for NXM-3284
            rstk[1] = orx2;
            rstk[1] = llc_orx2 = orx2;
          }
        }
      }
      if (outerLimits.containsKey("Y1"))  {
        tmp = outerLimits.getD("Y1");
        if (tmp != ory1) { ory1=tmp; changedOuter=true; }
      }
      if (outerLimits.containsKey("Y2"))  {
        tmp = outerLimits.getD("Y2");
        if (tmp != ory2) { ory2=tmp; changedOuter=true; }
      }
      if (outerLimits.containsKey("Z1"))  {
        tmp = outerLimits.getD("Z1");
        if (tmp != orz1) { orz1=tmp; changedOuter=true; }
      }
      if (outerLimits.containsKey("Z2"))  {
        tmp = outerLimits.getD("Z2");
        if (tmp != orz2) { orz2=tmp; changedOuter=true; }
      }
    }
    // get any user-set viewport bounds settings
    if (type == null || type.equals("ALL") || type.startsWith("VIEW")){
      if (viewLimits.containsKey("X1"))  {
        tmp = viewLimits.getD("X1");
        if (tmp != rx1) { rx1=tmp; changedView=true; }
      }
      if (viewLimits.containsKey("X2"))  {
        tmp = viewLimits.getD("X2");
        if (tmp != rx2) { rx2=tmp; changedView=true; }
      }
      if (viewLimits.containsKey("Y1"))  {
        tmp = viewLimits.getD("Y1");
        if (tmp != ry1) { ry1=tmp; changedView=true; }
      }
      if (viewLimits.containsKey("Y2"))  {
        tmp = viewLimits.getD("Y2");
        if (tmp != ry2) { ry2=tmp; changedView=true; }
      }
      if (viewLimits.containsKey("Z1"))  {
        tmp = viewLimits.getD("Z1");
        if (tmp != rz1) { rz1=tmp; changedView=true; }
      }
      if (viewLimits.containsKey("Z2"))  {
        tmp = viewLimits.getD("Z2");
        if (tmp != rz2) { rz2=tmp; changedView=true; }
      }
    }

    if (changedView || changedOuter) {
      checkBounds(changedView,changedOuter);  // validate and then refresh  (since 3.1.0: Bug 2479)
      if ((viewLimits != null) && !viewLimits.isEmpty()) checkNeedTreatAsZoom(); // treat this as zoom if view limits within outer limits - since 3.9.3 - adjusted in 4.1.0 & 4.1.1
      setup();
      refresh();
    }
  }

  /** Check all the bounds settings to make sure the min and max are not equal.
      Note: does NOT call {@link #refresh()} or {@link #setup()}
      @since NeXtMidas 3.1.0
   */
  private void checkBounds (boolean warnView, boolean warnOuter) {
    // added for Bug 2479
    checkBounds("RX", warnView);
    checkBounds("RY", warnView);
    checkBounds("ORX", warnOuter);
    checkBounds("ORY", warnOuter);
  }


  /** Check the given bounds settings to make sure the min and max are not equal. If they ARE equal,
      then 1% (or 1e-12 if they are zero) of the value is 'padded' to each side.
      Note: does NOT call {@link #refresh()} or {@link #setup()}
      @since NeXtMidas 3.1.0
  */
  private void checkBounds (String dim, boolean warn) {
    double offset =.01;  // 1%. Also does Math.max(1e-12,..) to prevent mult by 0)
    if (dim.equals("RX")) {
      if (rx1==rx2) {
        if (warn) Shell.warning("View X Min and X Max both set to ["+rx1+"]. Adding 1% of X value to Min and Max.");
        rx1 -=  Math.max(1e-12,(rx1 * offset));
        rx2 +=  Math.max(1e-12,(rx2 * offset));
      }
    }
    if (dim.equals("RY")) {
      if (ry1==ry2) {
        if (warn) Shell.warning("View Y Min and Y Max both set to ["+ry1+"]. Adding 1% of Y value to Min and Max.");
        ry1 -= Math.max(1e-12, (ry1 * offset));
        ry2 += Math.max(1e-12, (ry2 * offset));
      }
    }
    if (dim.equals("ORX")) {
      if (orx1==orx2) {
        if (warn) Shell.warning("X Min and X Max both set to ["+orx1+"]. Adding 1% of X value to Min and Max.");
        orx1 -=  Math.max(1e-12, (orx1 * offset));
        orx2 +=  Math.max(1e-12, (orx2 * offset));
      }
    }
    if (dim.equals("ORY")) {
      if (ory1==ory2) {
        if (warn) Shell.warning("Y Min and Y Max both set to ["+ory1+"]. Adding 1% of Y value to Min and Max.");
        ory1 -=  Math.max(1e-12, (ory1 * offset));
        ory2 +=  Math.max(1e-12, (ory2 * offset));
      }
    }
  }

  /** Check to see if this limits setting should be treated as a zoom
   *  to the view limits with the outer limits being zoom level 0.<br>
   *  If no other zoom levels are present, this is needed to be able to zoom out.
   */
  private void checkNeedTreatAsZoom () {
    if ((level == 0) && viewBoundsLTOuterBounds()) {
      rstk[0] = orx1;
      rstk[1] = orx2;
      rstk[2] = ory1;
      rstk[3] = ory2;
      rstk[4] = orz1;
      rstk[5] = orz2;
      rstk[6] = view;
      level++;
      lastLevel = level;
    }
  }

  /** Are the view limits a smaller area than the outer limits
   *  @return true if smaller in at least one dimension and LT or equal in the others
   */
  private boolean viewBoundsLTOuterBounds () {
    double precision = 0.0001;
    double xDiff = Math.abs(orx2-orx1) - Math.abs(rx2-rx1);
    double yDiff = Math.abs(ory2-ory1) - Math.abs(ry2-ry1);
    double zDiff = Math.abs(orz2-orz1) - Math.abs(rz2-rz1);
    boolean xlimitsSame  = Tolerance.equals(xDiff, 0);
    boolean ylimitsSame  = Tolerance.equals(yDiff, 0);
    boolean zlimitsSame  = Tolerance.equals(zDiff, 0);
    boolean viewLimitsSmaller = (xlimitsSame || (xDiff > precision)) &&
        (ylimitsSame || (yDiff > precision));
    if ((view == V_GEODETIC) || is3DView()) {
      viewLimitsSmaller = viewLimitsSmaller & (zlimitsSame || (zDiff > precision));
    }
    return viewLimitsSmaller;
  }

  /** Used to prevent double booking of removed private MBox databox and ix1,iy1,ix21,iy21. */
  private boolean equalsDataBox (Rectangle r) {
    return (r!=null) && (r.width==ix21) && (r.height==iy21) && (r.x==ix1) && (r.y==iy1);
  }

  /** paints this plot
   *  @param g the Graphics object used for painting
   *  @deprecated since NeXtMidas 3.9.0
   */
  @Deprecated
  public synchronized void paint (Graphics g) {
    paintComponent(g);
  }

  private Container findMidasDisplayParent () {
    Container container = this.panel;
    while (container!=null && !(container instanceof MidasDisplay)) {
      container = container.getParent();
    }
    return container;
  }

  @Override
  @ProvisionalUseOnly("Treat this as protected and should only be called by subclass paintComponent methods")
  public synchronized void paintComponent (Graphics g) { // moved paint code here and pass g references - 3.9.0 for NXM-3213
    super.paintComponent(g);
    setGraphics(g);           // set current graphics context
    // Double Buffering drawing needs special treatment on Java 8+ with CentOS 7 - (3.9.0) and with REALTIME mode
    boolean isRealtime =  Theme.getOption(Theme.REALTIME);
    if(isRealtime && (dragging>0)) rubberbox(g.create(), 1);
    Rectangle clip = g.getClipBounds();
    if (oarbox.toRectangle().contains(clip)) { // since 2.5.3: fix for Bug 1259
      setAxisReadout(g,plotInfo);
      setExtendedReadout(g);
      readoutExtTime = Time.current(); // NXM-3628 3.9.4
      return;
    }
    boolean fresh = !equalsDataBox(clip) || (!useDB && dragging>0); // is this a full|data pass
    if (refreshing && !fresh) return;

    // JWB - If the plot is drawn by Swing AND is using Swing DoubleBuffering,
    // we need to draw the whole panel to avoid graphical glitches in 2d rasters.
    boolean fullSwing = SwingUtilities.isEventDispatchThread() &&
                        !(Theme.getOption(Theme.REALTIME)) &&
                        (useDB);
    int drawmode  = (fresh || fullSwing)? Drawable.FULL : refreshMode;
    boolean clear = (drawmode!=Drawable.DATAX);          // clear data area
    int flags     = DO_DRAW | (useBS? DO_NBS:0);
    // NOTE: may be a TODO, if Theme.REALTIME and default NOT set to MP.O_DBUFFER
    // then if (dragging>0) rubberbox(-1) to erase current box before drawing new one
    //if (dragging>0) rubberbox(-1);                       // undraw box
    if ((options&O_CROSS)!=0) drawCrossHairs(CH_UNDRAW); // undraw crosshairs  NOT in J. Czechowski's changes 3.9.0
    if (fresh) {          // fresh canvas
      g.setColor(theme.cbg);
      g.fillRect(0,0,pos.w,pos.h);
      drawAxis(g);
      if ((options&O_CONTRAST)==0) clear=false;
    }
    Shape origClip = g.getClip();
    if (needRefresh()) {
      g.setClip(ix1, iy1, ix21, iy21);                   // SET the clip to include the entire layer area
    } else {
      g.clipRect(ix1, iy1, ix21, iy21);                  // AND the clip regions (intersection)
    }
    Image image = images[level];
    if (useDB && dbimage!=null) image=dbimage;
    if (!neverUseDBImageBackup && useDBImageBackup && (image==null) && useDB) image = dbimageBackup; // 3.9.3
    if ((useBS||useDB) && image!=null && clip!=null) {   // apply backing store
      g.drawImage (image, clip.x,clip.y,clip.x+clip.width,clip.y+clip.height,
                          clip.x,clip.y,clip.x+clip.width,clip.y+clip.height, panel);
      clear=false;
    }

    if ((axis&G_GRIDFRONT) == 0 && (axis&G_GRID)!=0) drawAxisGrid(g.create()); // grid drawn in rear (back/underneath)
    if (!useDB) drawDataArea(g,drawmode,clear,flags);    // draw non-BS data
//    if (dragging>0) rubberbox(1);                        // draw box
    //if ((options&O_CROSS)!=0) drawCrossHairs(CH_DRAW); // draw crosshairs  // changed to line below for Bug 2327
    if ((options&O_CROSS)!=0) needsDrawCH = true;        // since 2.9.1: delay drawing to prevent artifacts on panel (Bug 2327)
    if (fresh && legend!=null && legend.getStatus()!=EXIT) {
      legend.update();
    }

    if (canvas != null) canvas.paintComponent(g);  // since 2.9.2: draw any items on the canvas (3.9.0 pass g)
    //if (!fresh && (axis&G_GRID)!=0) { JWB- Modded fdor Bugs 73, 1381, and 1435
    if  ( ((axis&G_GRIDFRONT) != 0)  && ((axis&G_GRID) !=0) ) {
      Graphics gtemp = g.create();
      drawAxisGrid(gtemp);  // draw axis grid in front
      gtemp.setPaintMode();
      gtemp.dispose();
    }
    if (dragging > 0) rubberbox(g.create(),1);

    g.setClip(origClip);
    // paint readout if this clip contains the onAxis readout region - since 3.9.3 for readout flickering or gone
    if (((readout&R_ONAXIS)!=0) && clip.contains(oarbox.toRectangle())  && !(oarbox.toRectangle().contains(clip))) {
      setAxisReadout(g,plotInfo);
      setExtendedReadout(g);
      readoutExtTime = Time.current();
    }
  }

  private void setExtendedReadout (Graphics g) {
    if (onAxisExtendedReadout()) {
      String rightAxisinfo = formRightAxisInfo();
      setAxisRightReadout(g,rightAxisinfo);
    }
  }

  private boolean onAxisExtendedReadout () {;
    return ((readoutExt&R_EXT_SHOW)!=0 && ((readout&R_ONAXIS)!=0) || (((readout&R_ONTITLE)==0) && ((readout&R_ONSTATUS)==0)));
  }

  private String formRightAxisInfo () {
    String rightAxisinfo = "";
    if ((readoutExt&R_EXT_FEATURE)!=0) {
      rightAxisinfo += "#MP_Features:"+numVisibleFeatures;
    }
    if (((readoutExt&R_EXT_POINT)!=0) && (layerDrawable != null)) {
      String spacer = "";
      if (!rightAxisinfo.equals("")) spacer+="  ";
      int visiblePoints = layerDrawable.getNumVisiblePoints();
      if (visiblePoints>=0) { // Do not add the points readout if the number of visible points < 0
        rightAxisinfo += spacer + "#Points:"+visiblePoints;
      }
    }
    if ((readoutExt&R_EXT_WMS_LEVEL)!=0  && (wmsLevel >= 0)) { // NXM-3726
      String spacer = "";
      if (!rightAxisinfo.equals("")) spacer+="  ";

      rightAxisinfo += spacer + "WMS_level:"+String.format("%1.3e",wmsLevel);
    }
    return rightAxisinfo;
  }

  /** Extended Readout is displaying WMS level */
  boolean isDisplayingWMSLevel () {
    return (readoutExt&R_EXT_WMS_LEVEL)!=0;
  }

  void setWMSLevel (double wmsLevel) {
    if ((wmsLevel==0) && !(this.wmsLevel==-1)) return;
    this.wmsLevel = wmsLevel;
  }

  /** Refreshes a drawable object. This method is most frequently used to draw the various layers
      onto the plot.  It is also a synchronous entry point for drawing feature data.
      @param drawmode The requested draw mode (see {@link Drawable}). This method may substitute
                  another draw mode if appropriate for the situation.
   */
  public void refresh (int drawmode) {
    if (!isPanelVisible()) {
      return; // This will prevent from tabs updating in the background - see test_plot_layer2d_ordered
    }

    if (needRefresh) drawmode=Drawable.FULL;
    // map views often have partially transparent objects NXM-3961
    boolean alwaysClearBecauseMap = MPlot.mapRefreshShouldClear && isMapView();
    boolean clear = (drawmode!=Drawable.DATAX) || alwaysClearBecauseMap;
    boolean fresh = ((drawmode!=Drawable.DATA) && clear);
    cancel = false;                                     // reset flag
    needRefresh = false;                                // refresh flag
    useBS = (options&O_BSTORE)!=0;
    useDB = (options&O_DBUFFER)!=0;
    Image image = images[level];
    int w = pos.w; // width for input to panel.createImage(..)  - cannot be <= 0
    int h = pos.h; // height for input to panel.createImage(..) - cannot be <= 0
    if (w<1) w = 1; // Since 2.7.4: prevent IllegalArgumentException (Bug 2079)
    if (h<1) h = 1; // Since 2.7.4: prevent IllegalArgumentException (Bug 2079)
    if (useBS && (image==null || !clear)) {             // Update the backing store (BS) image
      refreshing=true;
      // JC4: Next line keeps existing BS image if possible, Layer2D makes use of this "undocumented"
      //      functionality (see %TEST_PLOT_LAYER2D_DIRECTCOLOR). We should probably think about
      //      updating Layer2D since the image could be null'ed out for other reasons (zooming, etc).
      if (image==null) image = images[level] = panel.createImage(w,h); // <-- can return null
      if (image!=null) {
        Graphics gbs = image.getGraphics();
        gbs.setClip(ix1,iy1,ix21,iy21);
        drawDataArea(gbs,drawmode,clear,DO_BS|DO_DRAW);
      }
      refreshing=false;
    }
    if (useBS) clear=false;
    refreshMode = drawmode;
    if (useDB) {
      if (dbimage==null) dbimage = panel.createImage(w,h); // <-- can return null
      if (dbimage!=null) {                          // changed to dbimage for BUG 2524
        Graphics grt = dbimage.getGraphics();
        grt.setClip(ix1,iy1,ix21,iy21);
        if (useBS && image!=null) grt.drawImage(image,0,0,panel); // Draw BS
        int flags = DO_DRAW | (useBS? DO_NBS:0);
        drawDataArea(grt,drawmode,clear,flags);     // draw non-BS data
        dbimageBackup = dbimage;
      }
    }
    /** TODO: Bug 1399 - If <code>fresh = false</code> (which it is for this
              Bug's case), it enters the <code>IF</code> block below and calls
              <code>paint</code>. This causes the flicker mentioned in the DR.
              If you set <code>fresh = TRUE </code>, it enters the <code>ELSE
              </code> block and calls <code>repaint</code>. This fixes the
              flicker in Bug 1399 but causes horrible flicker in 2d plots with
              grid turned on.
     */
    if (!fresh && !useDB && Theme.getOption(Theme.REALTIME)) {
      // !fresh makes layer2d look good, plot_grid bad
      Graphics grt = panel.getGraphics();
      if (grt==null) return;  // added in 3.3.0 for Bug 2668
      grt.setClip(ix1,iy1,ix21,iy21);
      if (panel.isVisible() && panel.getParent().isVisible()) paint(grt);
    }
    else if (!fresh && !useDB && (panel instanceof JComponent)) { // all swing (PATCH FOR BUG 2091)
      if (EventQueue.isDispatchThread()) {
        ((JComponent)panel).paintImmediately(ix1,iy1,ix21,iy21);  // paintImmedately helps with timing issues in Layer2D
      }
      else {
        try {
          EventQueue.invokeAndWait(new Runnable() {  // swing painting done on EDT
            @Override
            public void run () {
              ((JComponent)panel).paintImmediately(ix1,iy1,ix21,iy21); // paintImmedately helps with timing issues in Layer2D
            }} );
        } catch (Exception e) {
          Shell.printStackTrace("Error in MPlot.refresh(..)", e);
        }
      }
    }
    else {
      // fresh makes plot_grid look good, layer2d bad
      if (fresh) panel.repaint();                 // kick off paint
      else       panel.repaint(ix1,iy1,ix21,iy21);
    }
  }

  /** Draw the "data" (layers and features) for the plot. */
  private synchronized void drawDataArea (Graphics g, int drawmode, boolean clear, int flags) {
    gc = g;
    Color currentColor = g.getColor();
    if (clear) {  // clear data area
      gc.setColor( ((options&O_CONTRAST)!=0)? Color.black:theme.cbg);
      gc.fillRect(ix1,iy1,ix21,iy21);
    }
    gc.setColor(lastColor);
    int l1 = (layerGrid.ndata>0)? 0:1; // include grid layer in plot ?
    layerGrid.clearNeedRefresh(); // clear refresh flag in LayerGrid
    if ((holdRefresh&HOLD_LAYER_REFRESHES)==0) {
      for (int i=l1; i<layers.size(); i++) {
        doDraw((Layer)layers.get(i), drawmode, flags);
      }
    }
    int tempNumVisibleFeatures = 0;
    for (int i=0; i<features.size();i++) {
      Feature thisFeature = (Feature)features.get(i);
      doDraw(thisFeature, drawmode, flags);
      if (thisFeature.isVisible()) tempNumVisibleFeatures++;
    }
    if (elasticFeatureSelected && (elasticFeature!=null)) { // PROTOTYPE - not officially supported - NXM-3702
      if (elasticFeature.isVisible()) {
        ResizeBox.drawSelectedBox(elasticFeature);
        if (!baseLayer.realtime) needRefresh = true;
      }
      else {
        elasticFeatureSelected = false;
        elasticHandle = ResizeBox.Handle.NONE;
      }
    }
    numVisibleFeatures = tempNumVisibleFeatures; // NXM-3512 keep tabs on number of visible features
    gc = null;
    g.setColor(currentColor);
  }

  /** synchronized overlay draw
   *  @param drawmode draw mode
   *  @param dc       Drawable
   */
  public synchronized void drawOverlay (int drawmode, Drawable dc) {
    dc.draw(drawmode);
  }

  /** Draw the grid lines on the Axis layer.
      @param g    the graphics context
      @param int  axis mask
      Note: This method was revised in NXM 2.7.0 to pass an int mask to
            {@link #drawAxis(int)} instead of altering the global axis mask.
   */
  private void drawAxisGrid (Graphics g) {
    int gridAxis=G_SHOW|G_GRID;
    drawAxis(g, gridAxis);  // only draw the Gridlines, not the axis data
  }

  /** Undraw, draw, or redraw cross hairs
      @param mode Whether to undraw, draw or redraw (both). <pre>
      UNDRAW :  -1
      DRAW   :   1
      REDRAW :   0 </pre> <br>
      NOTE: Since NeXtMidas 2.9.1, this method will not attempt to undraw crosshairs
      unless they are already drawn, and vice versa.
   */
  public void drawCrossHairs (int mode) {
    // List to hold all the gmenu popup objects to get them repainted after the corsshairs are put over the plot
    // Since NeXtMidas 3.9.0
    List<Component> gwidgets = new ArrayList<>();
    // Iterate the components that are on the same panel as this plot
    for (Component comp : panel.getParent().getComponents()) {
      if (comp instanceof MJPanel) {
        //If they are gwidgets and they intersect this this panel, they need to be repatined after the crosshairs
        if ((((MJPanel) comp).MW instanceof GWidget) && comp.getBounds().intersects(panel.getBounds())) {
          if (!comp.isShowing()) continue;
          gwidgets.add(comp);
        }
      }
    }

    // Need to return if the mouse is over a popup
    // This prevents crosshairs from being drawn when the menu is the focus
    if (popupCount > 0) {
      for (Component comp : gwidgets) {
        try {
          if (MouseInfo.getPointerInfo().getLocation().x >= comp.getLocationOnScreen().x
              && MouseInfo.getPointerInfo().getLocation().x <= comp.getLocationOnScreen().x + comp.getWidth()
              && MouseInfo.getPointerInfo().getLocation().y >= comp.getLocationOnScreen().y
              && MouseInfo.getPointerInfo().getLocation().y <= comp.getLocationOnScreen().y + comp.getHeight()) {
            return;
          }
        }
        catch (IllegalComponentStateException icse) {
          // We may be in the loop while one of the components gets closed. This raises an exception
          // Need to ignore the exception and continue to the next component
        }
      }
    }

    // Draw the crosshairs on top of the panel (this will draw over the popups, or anything that is on the panel)
    Graphics _g = panel.getGraphics();
    _g.setXORMode(crossHairsColor);
    // Undraws current crosshairs
    if (drawnCH && mode <= 0 && crossX > ix1 && crossX < ix2 && crossY > iy1 && crossY < iy2) {
      _g.drawLine(ix1, crossY, ix2 - 1, crossY);
      _g.drawLine(crossX, iy1, crossX, iy2 - 1);
      drawnCH = false; // Bug 2327
    }
    if (!drawnCH) {
      crossX = px;
      crossY = py;
    }
    // This draws the new crosshairs
    if (!drawnCH && mode >= 0 && crossX > ix1 && crossX < ix2 && crossY > iy1 && crossY < iy2) {
      _g.drawLine(ix1, crossY, ix2 - 1, crossY);
      _g.drawLine(crossX, iy1, crossX, iy2 - 1);
      drawnCH = true; // Bug 2327
      needsDrawCH = false; // Bug 2327
    }
    _g.setPaintMode();
    _g.dispose();

    // Now we can repaint any popups.
    for (Component comp : gwidgets) {
      comp.repaint();
    }

  }

  /** Set the color of the crosshairs. Converts String to Color using {@link MColor#getColor(Object)}
      then calls  {@link #setCrossHairsColor(Color)}
      @param col color of the crosshairs
      @since NeXtMidas 2.9.1
   */
  public void setCrossHairsColor (String col) {
    setCrossHairsColor(MColor.getColor(col));
  }

  /** Set the color of the crosshairs
      @param c color of the crosshairs
      @since NeXtMidas 2.9.1
   */
  public void setCrossHairsColor (Color c) {
    crossHairsColor = c;
  }

  /** Whether or not the crosshairs need to be drawn
   *  @return true if crosshairs need to be drawn */
  public boolean needCrossHairRefresh () { return needsDrawCH; }  // added for Bug 2327

  /** This will update the cursor by re-drawing the cross hairs if needed.
      @see #drawCrossHairs(int)
   */
  public void updateCursor () {
    if ((options & O_CROSS) != 0) drawCrossHairs(CH_REDRAW);
  }
  /** Sets the current cursor. Calls {@link #setCursor(String, boolean)} with true,
      so any cursor passed into this method becomes the default cursor, and users
      must call this method again with "default" to set the standard default back.
      @param cursor The name of the cursor (see {@link MWindow#cursorList} )
   */
  @Override
  public void setCursor (String cursor) {
    setCursor(cursor, true);
  }

  /** Sets the current cursor
      @param cursor The name of the cursor (see {@link MWindow#cursorList} )
      @param sticky Whether or not to use the new setting as the default
      @since NeXtMidas 2.9.0
   */
  public void setCursor (String cursor, boolean sticky) {
    if (sticky) userCursor = cursor;  // Bug 2249, 1230
    super.setCursor(cursor);
    //refresh(); // for NXM-3054, remove refresh added during work on NXM-2841
  }

  /** Get the cursor set by the user, or null if the user has not ever set it.
   *  @return The name of the cursor (see {@link MWindow#cursorList} )
   */
  public String getUserCursor () { return userCursor; }

  /** Changes the active color being used for plotting.
      @param color The new color to use.
   */
  public void setColor (Color color) {
    if (gc == null) return;
    gc.setColor (color);
    lastColor = color;
  }

  @Override
  public boolean isPanelVisible () {
    return super.isPanelVisible() && (panel.getHeight() >= MINIMUM_PANEL_HEIGHT)
                                  && (panel.getWidth()  >= MINIMUM_PANEL_WIDTH);
  }

  /** Determines if any part of the plot (layer or feature) is marked as needing a refresh. If the
      panel is not visible this will return false.
      @return <code>true</code> if a refresh is needed, <code>false</code> if
              a refresh is unlikely to be needed at this time.
  */
  public boolean needRefresh () {
    return isPanelVisible() && (needRefresh || needLayerRefresh() || needFeatureRefresh());
  }

  /** Determines if any layer in the plot is marked as needing a refresh.
      @return <code>true</code> if a refresh is needed, <code>false</code> if
              a refresh is unlikely to be needed at this time.
      @since NeXtMidas 2.1.2
   */
  public boolean needLayerRefresh () {
    if ((holdRefresh&HOLD_LAYER_REFRESHES) !=0) return false;
    return needRefresh(layers);
  }

  /** Determines if any feature in the plot is marked as needing a refresh.
      @return <code>true</code> if a refresh is needed, <code>false</code> if
              a refresh is unlikely to be needed at this time.
      @since NeXtMidas 2.1.2
   */
  public boolean needFeatureRefresh () {
    if ((holdRefresh&HOLD_FEATURE_REFRESHES) !=0) return false;
    return needRefresh(features);
  }

  /** Checks the set of layers or features to see if they need to be refreshed. */
  private boolean needRefresh (KeyVector kv) {
    boolean nr=needRefresh;
    for (int i=0; i<kv.size(); i++) {
      Drawable d = (Drawable)kv.get(i);
      if (d == null) continue;          // since 3.7.0 for Bug 2991 and Bug 2457
      if (!d.getNeedRefresh()) continue;
      if ((d.getEnable()&EN_BSTORE)!=0) reset(-1);
      nr = true;
    }
    return nr;
  }

  /** Adds a new Plot canvas with the current instance of {@link nxm.sys.libg.MPlot} */
  public void addCanvas () {
    if (canvas==null) canvas = new PlotCanvas(this);
  }

  /** Remove current Plot canvas */
  public void removeCanvas () {
    canvas = null;
  }

  /** Get current Plot canvas
      @return Plot canvas
   */
  public PlotCanvas getCanvas () {
    return canvas;
  }

  /**
   * @deprecated since NeXtMidas 3.9.0
   */
  @Deprecated
  public void clear () {
    clear(g);
  }

  /** This will clear the plot. */
  @Override
  public void clear (Graphics g) {
    // TODO: Why doesn't this call super.clear()?
    if (g != null) g.clearRect (0,0,pos.w,pos.h);
  }

  /** This will clear all data in the plot. This calls the clear() method in each layer and then
      does a FULL refresh. This method is intended for occasional use only (like when changing
      the characteristics of a filter such that displaying old data would be misleading).
      @since NeXtMidas 2.1.2
   */
  public void clearData () {
    for (int i=0; i<layers.size(); i++) ((Layer)layers.get(i)).clear();
    refresh(Drawable.FULL);
  }

  /** Set whether or not to always show delta from current mouse pos to last mouse click pos.
   *  @param val true if should always show delta
   *  @since NeXtMidas 2.9.0
   */
  public void setShowDeltas (boolean val) { showDeltas = val; }
  /** Get whether or not to always show delta from current mouse pos to last mouse click pos.
   *  @return if always show delta
   *  @since NeXtMidas 2.9.0
   */
  public boolean isShowDeltas () { return showDeltas; }
  /** Get whether or not to always show delta from current mouse pos to last mouse click pos.
   *  @return true if always show delta
   *  @since NeXtMidas 2.9.0
   */
  public boolean getShowDeltas () { return isShowDeltas(); }  // convenience

  /** Set the current X axis/readout offset
      @param xoff The real world offset value
      @since NeXtMidas 4.1.4
  */
  @ProvisionalUseOnly("minimally tested")
  public void setXOff (double xoff) {
    boolean redraw = (xoff != this.xOff);
    this.xOff = xoff;
    if (redraw) panel.repaint();
  }
  /** Set the current Y axis/readout offset
      @param yoff The real world offset value
      @since NeXtMidas 4.1.4
  */
  @ProvisionalUseOnly("not tested")
  public void setYOff (double yoff) {
    this.yOff = yoff;
  }

  /** Format and show the readout. <br>
      The following are valid readout mode (<code>mode</code>) settings:
      <table>
        <caption style="visibility:hidden">""</caption>
        <tr><td><code>mode ==  0</code></td><td>- Do not show deltas.</td></tr>
        <tr><td><code>mode == -9</code></td><td>- Null readout.</td></tr>
        <tr><td><code>mode &gt;   0</code></td><td>- Show deltas.</td></tr>
      </table>
      @param mode The readout mode.
      @return The properly-formatted readout.
      @see #readout
   */
  public String formatReadout (int mode) {
    String xUnits = "";
    String yUnits = "";

    if ((readout&R_UNITS)!=0) {
      xUnits = getXUnitsString();
      yUnits = getYUnitsString();
    }

    String plotName="Plot ";
    String layerInfo="";
    String _plotInfo="";  // create seperate plotInfo variable to avoid race conditions (since 3.3.0 - Bug 2662)
    Position mb=mt;
    if ((readout&R_SHOW)==0 || (mode==0 && !pactive)) { this.plotInfo=""; return ""; }
    if (mh instanceof IDable) plotName += ((IDable)mh).getID()+" ";
    boolean deltas = (mode!=0) && (mode!=9);
    if (deltas) getPosition(box.x,box.y,mb);
    else if (mm!=null) { mb=mm; deltas=true; }
    int dms  = ((readout&R_DMS)!=0)? -1: 0; // Use DMS for lat/lon?
    // BUG 910 Changes start
    //int rng  = (dRangeUnits == U_NAUTICAL_MILES)? -2: 0; // Use nmi for drange?
    int rng = 0;
    if      (dRangeUnits == U_NAUTICAL_MILES) rng = -2;  // -2 is used in formNumber to mean nmi
    else if (dRangeUnits == U_KILOMETERS)     rng = -6;  // -6 is used in formNumber to mean km
    else if (dRangeUnits == U_METERS)         rng = -7;  // -7 is used in formNumber to mean m
    // BUG 910 Changes end
    int xfmt = useDateString(xUnitsID,mp.x+xOff)? -3: xUnitsID; // Units for x and dx (values <0 are special)
    int yfmt = useDateString(yUnitsID,mp.y+yOff)? -3: yUnitsID; // Units for y and dx (values <0 are special)
    int dfmt = Units.NONE;                              // Units for dx/dy    (values <0 are special)
    setShowDeltas((readout&R_ALWAYSDELTAS)!=0); // set whether deltas remain active or toggle on and off - since 3.9.2 for NXM-3453
    if (isMapView()) {
      double normalizedLon = mp.lon; // Longitude readout within map should stay in -180 to 180 range NXM-3583
      if ((view == V_LATLONCONTINUOUS) && hasExtendedRange() && (mp.lon > LONGITUDE_DEFAULT_MAX)) {
        normalizedLon -= LONGITUDE_RANGE;
      }
      if ((readout&R_Y)!=0) _plotInfo += " Lat="+formNumber(mp.lat,0,dms);
      if ((readout&R_X)!=0) _plotInfo += " Lon="+formNumber(normalizedLon,0,dms);
      if ((readout&R_T)!=0) _plotInfo += " t="+formTime(time);
      if (deltas) {
        if ((readout&R_DY)!=0) _plotInfo += " dLat="+formNumber(mp.lat-mb.lat,0,dms);
        if ((readout&R_DX)!=0) _plotInfo += " dLon="+formNumber(mp.lon-mb.lon,0,dms);
        if ((readout&R_DR)!=0) {              // show dRange Units
          double r    = Transform.greatArc(mp,mb);
          int    rexp = getTicExp (r,r);
          _plotInfo   += " dRange="+formNumber(r,rexp,rng);
        }
        if ((readout&R_AZIMUTH)!=0) {       // show azimuth
          double azimuth = GeodeticUtil.computeCourse(mb, mp);
          int    azExp   = getTicExp(azimuth, azimuth);
          _plotInfo += " Azi="+formNumber(azimuth,azExp,-1);
        }
      }
    }
    else if ((readout&R_INDEX)!=0) {
      if ((readout&R_X)!=0) _plotInfo += " ix="+getXIndex(mp.x);
      if ((readout&R_Y)!=0) _plotInfo += " iy="+getYIndex(mp.y);
      if (deltas) {
        if ((readout&R_DX)!=0) _plotInfo += " idx="+(getXIndex(mp.x)-getXIndex(mb.x));
        if ((readout&R_DY)!=0) _plotInfo += " idy="+(getYIndex(mp.y)-getYIndex(mb.y));
      }
    }
    else if (view!=V_3DGRAPH) {
      int xexp = getTicExp (rx1+xOff,rx2+xOff);
      int yexp = getTicExp (ry1+yOff,ry2+yOff);
      String xName = "x";
      if (baseLayer instanceof Layer2D) { // NXM-4006
        TuneAdjust tuneAdjustments = ((Layer2D)baseLayer).getTuneAdjustments();
        if (tuneAdjustments!=null) {
          Double rf = tuneAdjustments.getRF(mp.y, mp.x);
          if (rf!=null) {
            _plotInfo += " rf="+formNumber(rf,xexp,xfmt,xMult) + " " + xUnits;
            xName = "if";
          }
        }
      }
      if ((readout&R_X)!=0) _plotInfo += " "+xName+"="+formNumber(mp.x+xOff,xexp,xfmt,xMult) + " " + xUnits;
      if ((readout&R_Y)!=0) _plotInfo += " y="+formNumber(mp.y+yOff,yexp,yfmt,yMult) + " " + yUnits;
      if ((readout&R_T)!=0) _plotInfo += " t="+formTime(mp.t);
      if ((readout&R_IX)!=0) _plotInfo += " ix="+getXIndex(mp.x);
      if ((readout&R_IY)!=0) _plotInfo += " iy="+getYIndex(mp.y);
      if (this.mode==M_RVI && (readout&R_DR)!=0) { // NXM-4065 include radius on RVI readouts (Jeff)
        double rp = Math.sqrt(mp.x*mp.x+mp.y*mp.y);
        double rb = Math.sqrt(mb.x*mb.x+mb.y*mb.y);
        _plotInfo += " r="+formNumber(rp,xexp,xfmt,xMult) + " " + xUnits;
        if (deltas) _plotInfo += " dr="+formNumber(rp-rb,xexp,xfmt,xMult) + " " + xUnits;
      }
      if (deltas) {
        xexp = getTicExp (mp.x-mb.x,mb.x-mp.x);
        yexp = getTicExp (mp.y-mb.y,mb.y-mp.y);
        if ((readout&R_DX)!=0) _plotInfo += " dx="+formNumber(mp.x-mb.x,xexp,xfmt,xMult) + " " + xUnits;
        if ((readout&R_DY)!=0) _plotInfo += " dy="+formNumber(mp.y-mb.y,yexp,yfmt,yMult) + " " + yUnits;
        if ((readout&R_DYDX)!=0) _plotInfo += " dy/dx="+formNumber((mp.y-mb.y)/(mp.x-mb.x),yexp,dfmt,yMult) + " " + yUnits;
        if ((readout&R_DT)!=0) _plotInfo += " dt="+formTime(mp.t-mb.t);
        if ((readout&R_DZ)!=0) _plotInfo += " dz="+ formNumber(mp.z-mb.z,0,dfmt);   // added in 2.9.0 for Bug 1383
      }
    }
    if ((readout&R_VIEW)!=0 && !deltas) _plotInfo += " View="+getView();
    // get optional Layer specific readout information
    for (int i=0; i<layers.size(); i++) {
      String tmpx = ((Layer)layers.get(i)).getReadOut(mp);
      if (tmpx!=null) layerInfo += " / "+tmpx;
    }
    if (_plotInfo.length() > 0) {
      _plotInfo = _plotInfo.substring(1);
      _plotInfo = "(" + _plotInfo + ")" + layerInfo;
    } else {
      _plotInfo = layerInfo;
    }
    String readoutString = plotName + _plotInfo;

    if (mode == -9) {
      _plotInfo = null;
      readoutString = null;
    }

    if ((readout&R_ONTITLE) !=0) setAltTitle(readoutString+"   "+formRightAxisInfo());
    if ((readout&R_ONSTATUS)!=0) setStatusBar(readoutString+"   "+formRightAxisInfo());
    if ((readout&R_ONAXIS)  !=0) refresh(oarbox);
    this.plotInfo = _plotInfo;  // set class variable for use when drawing
    return readoutString;
  }

  /** point readouts should update if stale
   *  @since NeXtMidas 3.9.4 for NXM-3628
   */
  public void updateReadoutExtIfStale () {
    if (onAxisExtendedReadout() && (Time.current()-readoutExtTime>0.1))
    refresh(oarbox);
  }

  /** This will form a time value. This will use the "standard" time format
    ({@link Time#STD}) by default when formatting the time value with {@link #precision}
    decimal places for the fractional seconds.
    @since nxm 3.3.3. The default can be overridden by setting the dateFormat.
    @param t The time (in seconds).
    @return The time formatted as a string.
    @see Time#STD
    @see Time#format(double,int,int)
    @see #formNumber(double,int,int)
   */
  private String formTime (double t) {
    if (t==0) return "0";
    return formTime(t, Time.STD, precision); // Switched for using new method in 3.3.3
  }

  private long getXIndex (double x) {
    Layer lay = getBaseLayer();
    if (lay==null) return 0;
    if (lay.isPaged) {
      double delta = lay.pf.getXDelta(); if (delta==0) delta=1;
      return (long)((x-lay.pf.getXStart())/delta+0.5);
    } else {
      double delta = lay.xdelta; if (delta==0) delta=1;
      return (long)((x-lay.xstart)/delta+0.5);
    }
  }

  private long getYIndex (double y) {
    Layer lay = getBaseLayer();
    if (lay==null) return 0;
    double delta = lay.ydelta; if (delta==0) delta=1;
    return (long)((y-lay.ystart)/delta+0.5);
  }

  /** This will form a numerical value. The format of the number will be based
      on its units (see {@link Units}).
      @param t     The numerical value.
      @param texp  The exponent to apply to t.
      @param units The units for the numerical value.
      @return string representation of numerical value formatted based on units
      @see Units
   */
  public String formNumber (double t, int texp, int units) {
    return formNumber(t,texp,units,0);
  }

  /** This will form a numerical value. The format of the number will be based
      on its units (see {@link Units}).
      @since nxm 3.3.3, default date format can be overridden by setting dateFormat
      @param t     The numerical value.
      @param texp  The exponent to apply to t.
      @param units The units (Special Values: -1=DMS, -2=m-&gt;nm, -3=DateTime, -4=HMS, -5=YMD).
      @param mult  Unit multiplier
      @return string representation of numerical value formatted based on units
      @see Units
   */
  public String formNumber (double t, int texp, int units, int mult) {
    if (units==-1) { return Convert.deg2dms(t,"00.00"); }
    if (units==-3) { if (Math.abs(t)>100) return formTime(t, Time.STD, precision); else units=1; } // 2011-11-14 passed in precision for BUG 2426
    if (units==-4) { if (Math.abs(t)>100) return formTime(t, Time.HMS, -1); else units=1; }
    if (units==-5) { if (Math.abs(t)>100) return formTime(t, Time.YMD, -1); else units=1; }
    if (units==-2) { t *= Constants.METERS2NM; texp=0; } // convert to Nautical Miles
    if (units==-6) { t /= 1000; texp=0; } //convert to kilometers

    String tmp;
    if ((readout&R_SCIENTIFIC)==R_SCIENTIFIC) {
        double tmul= Math.pow(10.0,(-texp));
        tmp = nf.format(t*tmul);
        if (texp-mult!=0) tmp += "E"+(texp-mult);
    } else {
        double tmul= Math.pow(10.0,(-mult));
        tmp = nf.format(t*tmul);
    }
    // NOTE: these are special cases to append the units ONLY for dRange
    if      (units ==  0) {}            // append nothing
    else if (units == -6) tmp += "km";  // Kilometers:     append 'km'  to end of range
    else if (units == -7) tmp += "m";   // Meters:         append 'm'   to end of range
    else if (units == -2) tmp += "nmi"; // Nautical Miles: append 'nmi' to end of range
    return tmp;
  }

  private String formTime (double t, int defaultFormat, int dp) {

    MFormat mformat = getDateFormat(true,defaultFormat,dp );
    boolean neg = t<0;
    if (neg) t = -t;

    String tmpstr;
    try {
      Time timeObj = new Time(t);
      StringBuffer sb = mformat.format (timeObj, new StringBuffer(), null);
      tmpstr =  sb.toString();
    }
    catch (Exception e) {
      // If some reason this crashes and burns, use default
      e.printStackTrace();
      Shell.warning("MPlot.formTime: error formtting time.  Using default." + e.getMessage());
      tmpstr = Time.format(t, defaultFormat, dp);
    }
    if (tmpstr.startsWith("00:00:00::")) tmpstr = tmpstr.substring(10);
    int ip=tmpstr.indexOf(".000");
    if (ip>0) tmpstr = tmpstr.substring(0,ip);
    if (neg) tmpstr = "-"+tmpstr;

    return tmpstr;
  }

  private void setAxisReadout (Graphics g, String plotInfo) {
    g.setColor(theme.cbg);
    g.fillRect(oarbox.x,oarbox.y,oarbox.w,oarbox.h);
    g.setColor(theme.cfg);
    if (plotInfo == null) return;
    g.drawString(plotInfo,tw,oarbox.y+oarbox.h-(ta/3));
  }

  private void setAxisRightReadout (Graphics g, String rightAxisInfo) {
    g.setColor(theme.cbg);
    g.setColor(theme.cfg);
    int plotInfoLen = plotInfo == null ? 0 : plotInfo.length();
    int readoutEnd = oarbox.x + plotInfoLen*tw;
    int xLoc = oarbox.w - tw * (rightAxisInfo.length());
    if (xLoc < readoutEnd) xLoc = readoutEnd +tw;
    g.drawString(rightAxisInfo,xLoc,oarbox.y+oarbox.h-(ta/3));
  }

  /** Draw the axis.This public method uses the global axis mask
   *  @deprecated in 3.9.0 to solve graphical timing issues
   */
  @Deprecated
  public void drawAxis () {
    drawAxis(g, axis);
  }

  /** Draw the axis.This public method uses the global axis mask
   *  @param g    The graphics component
   */
  public void drawAxis (Graphics g) {
    drawAxis(g, axis);
  }

  /** Draw the axis
      @param g    The graphics component
      @param axis The axis mask to draw. If called from the public method, uses the global axis mask.
      @return always returns 0 (TODO: maybe it should return 0 for not drawn, 1 for drawn or vice-versa)
   */
  private int drawAxis (Graphics g, int axis) {
    if (g==null) return 0;
    if ((axis&G_SHOW)==0) return 0;

    final int ticSize = 3; // tic size
    final int lblMarg = 5; // margin between label and plot

    int x,y,x1,x2,y1,y2,ip,texp,tsl,tl;
    int      tlast;           // Last tic text location (JC4: mostly replaced by lastXxx variables below)
    int      ixl = ix1 - 4;   // Left plot edge
    int      ixr = ix2 + 2;   // Right plot edge
    int      iyt = iy1 - 4;   // Top plot edge
    int      iyb = iy2 + 2;   // Bottom plot edge
    double   tic;             // Current tic value
    double   tic1;            // First tic value
    double   tic2;            // Last tic value
    double   dtic;            // Delta between tics
    double   dtmp;            // Temporary double value
    double   tmul;
    String   tmpstr;
    String   tmpstrp;
    boolean  rtol_btot_l;                                 // labels are right-to-left or bottom-to-top (decreasing x or y values, i.e. an inverted view for x axis and normal for y)
    boolean  rvi = (this.mode==M_RVI);
    int      hUnits   = (rvi)? getYUnits() : (xvy)? getYUnits() : getXUnits(); // Units for horizontal axis
    int      vUnits   = (rvi)? getYUnits() : (xvy)? getXUnits() : getYUnits(); // Units for vertical axis
    String   hDateTxt = "";                               // Date text to show on horizontal axis
    String   vDateTxt = "";                               // Date text to show on vertical axis
    Graphics _g       = g;
    Color    gridc    = new Color(0x505050); // TODO: Why isn't this checking the theme?
    _g.setColor(theme.cfg);
    if ((axis&L_AXIS)!=0) _g.drawLine(ixl, iyt, ixl, iyb);
    if ((axis&R_AXIS)!=0) _g.drawLine(ixr, iyt, ixr, iyb);
    if ((axis&T_AXIS)!=0) _g.drawLine(ixl, iyt, ixr, iyt);
    if ((axis&B_AXIS)!=0) _g.drawLine(ixl, iyb, ixr, iyb);
    if (view==0) return 0;

    // *** Draw Horizontal tics and labels/numbers on axis *****
    if ((axis&H_TICS)!=0) {                         // draw horizontal tics on axis
      if (xvy) {
        dtic = getTicDelta(ry1,ry2,ndy);
        tic1 = getTicStart(ry1,ry2,dtic);
        texp = getTicExp(ry1,ry2);
        tic2 = ry2;
        rtol_btot_l = myy<0;
      }
      else {
        dtic = getTicDelta(rx1,rx2,ndx);
        tic1 = getTicStart(rx1+xOff,rx2+xOff,dtic)-xOff;
        texp = getTicExp(rx1+xOff,rx2+xOff);
        tic2 = rx2;
        rtol_btot_l = mxx<0;
      }
      boolean  hDateStr = useDateString(hUnits,tic1);            // Show date string on horizontal axis
      int myexp;
      int lastLeft  = ixl; // Left label boundary
      int lastRight = ixr; // Right label boundary
      if ((readout&R_SCIENTIFIC)==R_SCIENTIFIC) {   // Use scientific notation on axes
        myexp = texp - xMult;
        tmul  = Math.pow(10.0,(-texp));
      }
      else {
        myexp = 0;
        tmul  = Math.pow(10.0,(-xMult));
      }

      dtmp = tmul*dtic;
      if (dtmp < 1.0 || Math.abs(dtmp - 2.5) < .1) tmpstr = "#0."; else tmpstr = "#0";
      while (dtmp < 1.0 || Math.abs(dtmp - 2.5) < .1) { tmpstr += "#"; dtmp *= 10.0; }
      DecimalFormat hticnf = new DecimalFormat(tmpstr);

      if (dtic != 0 && ((tic1 + dtic) != tic1)) {    // BUG 1913- very large or very small numbers will lose precision
        boolean isLatLonContinuousView = view == V_LATLONCONTINUOUS;
        for (tic=tic1; tic<=tic2; tic+=dtic) {
          x = (xvy)? getiy(tic) : getix(tic);                       // center pixel position for this tic
          if ((axis&T_TICS)!=0) _g.drawLine(x,iyt,x,iyt-ticSize);  // draw to x ticks from top of plot outline up
          if ((axis&B_TICS)!=0) _g.drawLine(x,iyb,x,iyb+ticSize);  // draw to x ticks from bottom of plot outline down
          if ((axis&G_GRID)!=0) { _g.setColor(gridc); _g.drawLine(x,iy1,x,iy2); _g.setColor(theme.cfg); }
          if ((axis&(T_NUMS|B_NUMS)) != 0) {
            if (hDateStr) {
              tmpstr = formNumber(tic+xOff,0,-4);
            }
            else if (isLatLonContinuousView) {
              // since 3.9.3 have tic labels match -180 to 180 range
              double displayTic = tic*tmul;
              //if ((orx2 > LONGITUDE_DEFAULT_MAX) && (displayTic > LONGITUDE_DEFAULT_MAX)) {displayTic -= LONGITUDE_RANGE;
              tmpstr = hticnf.format(displayTic);
              if ((orx2 > LONGITUDE_DEFAULT_MAX) && (displayTic > LONGITUDE_DEFAULT_MAX)) {
                tmpstr = hticnf.format(displayTic -= LONGITUDE_RANGE)+"("+tmpstr+")";
              }
            }
            else {
              tmpstr = hticnf.format((tic+xOff)*tmul);
            }
            tsl = tw*tmpstr.length();
            x1 = x - tsl/2;                                       // pixel location for start of label for this x tic
            if (x1     < lastLeft ) {                             // check if label would b< out of range on left
              boolean isLeftmostLabel = (!rtol_btot_l) ? (tic <= tic1) : (tic+dtic > tic2);
              if (isLeftmostLabel) x1 = lastLeft;                 // since 3.7.0 first htic, use left edge (NXM-2840)
              else continue;
            }
            if (x1+tsl > lastRight) {                             // check if label would be out of range on right
              boolean isRightmostLabel = (!rtol_btot_l) ? (tic+dtic > tic2) : (tic <= tic1);
              if (isRightmostLabel) {
                if (isLatLonContinuousView) {
                  // remove extended range value from tic label if there is not space for it
                  int startExtendedPortion = tmpstr.indexOf('(');
                  if (startExtendedPortion >= 0) {
                    tmpstr = tmpstr.substring(0, startExtendedPortion);
                    tsl = tw*tmpstr.length();
                  }
                }
                x1 = lastRight - tsl;                             // since 3.7.0 last htic, from right edge (NXM-2840)
                if (rtol_btot_l && (myexp!=0)) x1 = x1 - 4*tw;           // allow for space for exponent
              }
              else continue;
            }
            if (rtol_btot_l) lastRight = x1-tw/2;
            else      lastLeft  = x1+tw/2+tsl;
            if ((axis&T_NUMS)!=0) _g.drawString (tmpstr, x1, iyt-lblMarg);    // give margin between top plot outline and label line
            if ((axis&B_NUMS)!=0) _g.drawString (tmpstr, x1, iyb+ta+lblMarg); // give margin between bottom plot outline and top of label
            if (tic==tic1) {
              x2 = x1 + tsl + tw;
              String prefix = "";
              if (hDateStr) {
                hDateTxt = (tic > 86400)? " ["+formNumber(tic,0,-5)+"]": "";
              } else if (myexp!=0) {
                prefix = "E"+myexp;
                if (!rtol_btot_l) { lastLeft += tw*3; } // if exponent not on right, then 3 more character's width
              }
              if ((axis&T_NUMS)!=0) _g.drawString (prefix, x2, iyt-lblMarg);
              if ((axis&B_NUMS)!=0) _g.drawString (prefix, x2, iyb+ta+lblMarg);
            }
          }
        }
      }
    }
 // *** Draw Vertical tics and labels/numbers on axis *****
    if ((axis&V_TICS)!=0) {  // draw vertical tics on axis
      double  ticDeltaY;                            // y delta between tics (absolute value)
      if (xvy) {
        dtic = getTicDelta (rx1,rx2,ndx);
        tic1 = getTicStart (rx1,rx2,dtic);
        texp = getTicExp (rx1,rx2);
        tic2 = rx2;
        rtol_btot_l = mxx<0;
        ticDeltaY = Math.abs(getix(tic1)-getix(tic1+dtic));
      } else {
        dtic = getTicDelta (ry1,ry2,ndy);
        tic1 = getTicStart (ry1,ry2,dtic);
        texp = getTicExp (ry1,ry2);
        tic2 = ry2;
        rtol_btot_l = myy<0;
        ticDeltaY = Math.abs(getiy(tic1)-getiy(tic1+dtic));
      }
      boolean  vDateStr = useDateString(vUnits,tic1);            // Show date string on vertical axis
      int myexp;
      int lastTop = iyt; // Top label boundary
      int lastBtm = iyb; // Bottom label boundary
      tlast=10000;
      if ((readout&R_SCIENTIFIC)==R_SCIENTIFIC) {   // Use scientific notation on axes
        myexp = texp - yMult;
        tmul  = Math.pow(10.0,(-texp));
      } else {
        myexp = 0;
        tmul  = Math.pow(10.0,(-yMult));
      }

      dtmp = tmul*dtic;
      if (dtmp < 1.0 || Math.abs(dtmp - 2.5) < .1) tmpstr = "#0."; else tmpstr = "#0";
      while (dtmp < 1.0 || Math.abs(dtmp - 2.5) < .1) { tmpstr += "#"; dtmp *= 10.0; }
      DecimalFormat vticnf = new DecimalFormat(tmpstr);

      if (dtic != 0 && ((tic1 + dtic) != tic1)) {    // BUG 1913- very large or very small numbers will lose precision
        for (tic=tic1; tic<=tic2; tic+=dtic) {
          y = (xvy)? getix(tic) : getiy(tic);        // pixel location for start of label for first y tic
          if ((axis&L_TICS)!=0) _g.drawLine(ixl,y,ixl-ticSize,y); // draw to y ticks from left of plot to the left 3 pixels
          if ((axis&R_TICS)!=0) _g.drawLine(ixr,y,ixr+ticSize,y); // draw to y ticks from right of plot to the right 3 pixels
          if ((axis&G_GRID)!=0) { _g.setColor(gridc); _g.drawLine(ix1,y,ix2,y); _g.setColor(theme.cfg); }
          if ((axis&(L_NUMS|R_NUMS)) != 0) {
            // Figure out whether this is the topmost or bottom-most label based on whether labels are inverted (i.e. top-to-bottom)
            boolean isTopmostLabel   = (rtol_btot_l) ? (tic+dtic >= tic2) : (tic <= tic1);
            boolean isBtmmostLabel   = (rtol_btot_l) ? (tic <= tic1) : (tic+dtic >= tic2);
            if (vDateStr) { // JC4: This code block may be useful for future rotating of other labels.
              tmpstr = formNumber(tic,0,-4);
              tsl = tw*tmpstr.length();
              y1 = y - tsl/2;
              vDateTxt = (tic > 86400)? " ["+formNumber(tic,0,-5)+"]": "";
              if (y1     < lastTop) continue;  // Note: Since displaying units is not an issue, there is no need to make sure to print out the last label
              if (y1+tsl > lastBtm) continue;
              // set the last bottom/top point to make sure labels/values do not overlap
              if (rtol_btot_l) lastBtm = y1-tw/2;
              else      lastTop = y1+tw/2+tsl;
              if ((axis&L_NUMS)!=0) drawVerticalText(_g, tmpstr, ixl-4*tw, y1);
              if ((axis&R_NUMS)!=0) drawVerticalText(_g, tmpstr, ixr+3+tw, y1);
            } else {

              boolean threeCharFromLastLabel = Math.abs(y-tlast) < ta*3;
              y1=y2=y-2; tmpstr = vticnf.format(tic*tmul);
              ip=tmpstr.indexOf(".000000"); if (ip>0) tmpstr=tmpstr.substring(0,ip);

              if ((axis&O_ROTATEVALS)!=0) {  // rotate the labels on the Y axis  (Bug 2505)
                if (myexp!=0 && Double.parseDouble(tmpstr)!=0) tmpstr += "E"+myexp;  // add in the exponent
                tsl = tw*tmpstr.length();   // get the total length
                y1 = y - tsl/2;             // find the midpoint
                if (y1     < lastTop ) {                   // check if label would be out of range on top
                  if (isTopmostLabel && (lastTop + tsl < lastBtm)) y1 = lastTop;        // since 3.7.1 adjust label position for first vtic from top edge, if will not overwrite last label (NXM-2840)
                  else continue;
                }
                if (y1+tsl > lastBtm) {                    // check if label would be out of range on bottom
                  if (isBtmmostLabel && (lastBtm - tsl > lastTop)) y1 = lastBtm - tsl;  // since 3.7.1 adjust label position for last vtic from bottom edge, if will not overwrite last label (NXM-2840)
                    else continue;
                }
                // set the last bottom/top point to make sure labels/values do not overlap - since 3.7.1 added logic to check if inverted view (NXM-2840)
                if (rtol_btot_l) lastBtm = y1-tw/2;
                else      lastTop = y1+tw/2+tsl;
                if ((axis&L_NUMS)!=0) drawVerticalText(_g, tmpstr, ixl-3*tw, y1);  // draw vertically (y1 is top edge / end of label)
                if ((axis&R_NUMS)!=0) drawVerticalText(_g, tmpstr, ixr+3+tw, y1);  // draw vertically
              }
              else {
                // if too close to previous label skip, unless this is the topmost or bottom-most label - adjusted in 3.7.1 for NXM-2840
                if ((threeCharFromLastLabel) && !(isTopmostLabel || isBtmmostLabel)) continue;

                // Give top/bottom labels precedence over next to top or next to bottom, by skipping those if needed to make room
                // (based on whether labels are normal (bottom to top) or inverted (top to bottom) - since 3.7.1 for NXM-2840
                boolean isNextToTopLabel   = (rtol_btot_l) ? ((tic+2*dtic >= tic2)&&(!isTopmostLabel)) : ((tic-dtic   <= tic1)&&(!isTopmostLabel));
                boolean isNextToBtmLabel   = (rtol_btot_l) ? ((tic-dtic   <= tic1)&&(!isBtmmostLabel)) : ((tic+2*dtic >= tic2)&&(!isBtmmostLabel));
                if ((ticDeltaY<ta*3) && (isNextToTopLabel || isNextToBtmLabel)) continue; // skip next to top/bottom if necessary

                tl = tmpstr.length();
                if (tic==tic1 && tl>4 && (axis&O_AUTOROTATE)!=0) return drawAxis(g, axis|O_ROTATEVALS);  // auto adjust
                int spaceForTopTick = ((axis&H_TICS)==0) ? 0 : ticSize;
                if (((axis&T_NUMS)==0)&&((y1 - ta) < lastTop + ticSize)) {
                  y1 = y2 = lastTop - spaceForTopTick + ta ;
                }
                if (tl>4 && (ip=tmpstr.indexOf('.'))>0) {
                  y2=y+ta; tmpstrp = tmpstr.substring(ip,Math.min(ip+4,tl));
                  //if ((y2+th)> lastT
                  while (tmpstrp.length()<4) tmpstrp+='0';
                  if ((axis&L_NUMS)!=0) _g.drawString (tmpstrp, ixl-4*tw-2, y2);
                  if ((axis&R_NUMS)!=0) _g.drawString (tmpstrp, ixr+3, y2);
                  if (tl>ip+4) {
                    y2=y+ta+ta; tmpstrp = tmpstr.substring(ip+4,Math.min(ip+7,tl));
                    while (tmpstrp.length()<3) tmpstrp+='0';
                    if ((axis&L_NUMS)!=0) _g.drawString (tmpstrp, ixl-3*tw-2, y2);
                    if ((axis&R_NUMS)!=0) _g.drawString (tmpstrp, ixr+tw+3, y2);
                    tmpstr = tmpstr.substring(0,ip+4);
                  }
                  tmpstr = tmpstr.substring(0,ip);
                  tl = tmpstr.length();
                }
                if (tl>4)  { // for Bug 2643 - if text is too long, shrink it slightly and reduce the distance from the axis line
                  Font curFont = _g.getFont();
                  float newSize = curFont.getSize()-2.0f;
                  Font f = curFont.deriveFont(newSize);
                  _g.setFont(f);
                  if ((axis&L_NUMS)!=0) _g.drawString (tmpstr, ixl-4*tw-2, y1);
                  if ((axis&R_NUMS)!=0) _g.drawString (tmpstr, ixr+2, y1);
                  _g.setFont(curFont);  // set the font back to normal size
                }
                else if (tl<4) {
                  if ((axis&L_NUMS)!=0) _g.drawString (tmpstr, ixl-3*tw-2, y1);
                  if ((axis&R_NUMS)!=0) _g.drawString (tmpstr, ixr+tw+3, y1);
                }
                else {
                  if ((axis&L_NUMS)!=0) _g.drawString (tmpstr, ixl-4*tw-2, y1);
                  if ((axis&R_NUMS)!=0) _g.drawString (tmpstr, ixr+3, y1);
                }
                if (tic+dtic>tic2) {
                  String prefix = "";
                  if (myexp!=0) prefix = "E"+myexp;
                  if ((axis&L_NUMS)!=0) _g.drawString (prefix, ixl-4*tw, y2+th);
                  if ((axis&R_NUMS)!=0) _g.drawString (prefix, ixr+3+tw, y2+th);
                }
              tlast=y;
              }
            }
          }
        }
      }
    }
    isxy = isyx = -100;
    if ((scroll&1)>0 && (axis&H_SBAR)!=0) {
           if ((axis&B_NUMS)!=0) isxy=iyb;
      else if ((axis&T_NUMS)!=0) isxy=iyt;
      else if ((axis&B_AXIS)!=0) isxy=iyb;
      else if ((axis&T_AXIS)!=0) isxy=iyt;
      x1 = Math.max(ix1,isx-isxd);
      x2 = Math.min(ix2,isx+isxd);
      if (isxy>0) {
        isLowHScroll  = x2<=ix1; // top of H BAR falls below plot bounds
        isHighHScroll = x1>=ix2; // bottom of H BAR is above plot bounds
        int rectX     = (isHighHScroll) ? x2-7 : x1-1;
        int rectWidth = Math.max(x2-x1, 8);
        int lineX1    = Math.min(x1, x2-2);
        int lineX2    = Math.min(x2-2, x1+2);
        _g.drawRect(rectX, isxy-2, rectWidth, 4);
        _g.setColor(theme.cwfh);
        _g.drawLine(lineX1, isxy, lineX2, isxy);
        _g.setColor(theme.cfg);
      }
    }
    if ((scroll&2)>0 && (axis&V_SBAR)!=0) {
           if ((axis&R_NUMS)!=0) isyx=ixr;
      else if ((axis&L_NUMS)!=0) isyx=ixl;
      else if ((axis&R_AXIS)!=0) isyx=ixr;
      else if ((axis&L_AXIS)!=0) isyx=ixl;
      y1 = Math.max(iy1,isy-isyd);
      y2 = Math.min(iy2,isy+isyd);
      if (isyx>0) {
        isLowVScroll  = y2<=iy1; // top of H BAR falls below plot bounds
        isHighVScroll = y1>=iy2; // bottom of H BAR is above plot bounds
        int rectY     = (isHighVScroll) ? y2-7 : y1-1;
        int rectWidth = Math.max(y2-y1, 8);
        int lineY1    = Math.min(y1, y2-2);
        int lineY2    = Math.min(y2-2, y1+2);
        _g.drawRect(isyx-2, rectY, 4, rectWidth);
        _g.setColor(theme.cwfh);
        _g.drawLine(isyx, lineY1, isyx, lineY2);
        _g.setColor(theme.cfg);
      }
    }

    // add axis labels necessary and in 2D
    if ((axis&(T_LABS|B_LABS|L_LABS|R_LABS))!=0 && is2D) {
      // Get the units from the primary layer
      String hUnitsStr;
      String vUnitsStr;
      // use of custom Axis labels added in 3.3.1 for Bug 945
      if (xvy) {
        if (customXAxisLabel != null) vUnitsStr = customXAxisLabel;
        else                          vUnitsStr = DataFile.getUnitsName(getXUnits(), xnf.getCmult());
        if (customYAxisLabel != null) hUnitsStr = customYAxisLabel;
        else                          hUnitsStr = DataFile.getUnitsName(getYUnits(), ynf.getCmult());
      } else {
        if (customXAxisLabel != null) hUnitsStr = customXAxisLabel;
        else                          hUnitsStr = DataFile.getUnitsName(getXUnits(), xnf.getCmult());
        if (customYAxisLabel != null) vUnitsStr = customYAxisLabel;
        else                          vUnitsStr = DataFile.getUnitsName(getYUnits(), ynf.getCmult());
      }
      int top    = iyt - 4;
      int bottom = iyb + 4 + CHAR_HEIGHT;
      int left   = ixl - 6 - CHAR_HEIGHT;
      int right  = ixr + 4;

      if ((axis&T_NUMS)!=0) { top    -= (CHAR_HEIGHT + 2); }
      if ((axis&B_NUMS)!=0) { bottom += (CHAR_HEIGHT + 2); }
      if ((axis&L_NUMS)!=0) { left   -= (4 * tw); }
      if ((axis&R_NUMS)!=0) { right  += (4 * tw); }

      // this logic added in 3.3.2 to allow for adding a custom axis title where there is none
      boolean drawH = ((hUnits != Units.NONE) && (hUnits != Units.UNKNOWN) ) ||
                      ((!xvy && (customXAxisLabel!=null)) || (xvy && (customYAxisLabel!=null)));

      boolean drawV = ((vUnits != Units.NONE) && (vUnits != Units.UNKNOWN) ) ||
                      ((xvy && (customXAxisLabel!=null)) || (!xvy && (customYAxisLabel!=null)));

      if (drawH) {      // Draw Horizontal Title
        hUnitsStr += hDateTxt;
        int offset = ((ixr-ixl) - (hUnitsStr.length() * CHAR_WIDTH)) / 2;
        if ((axis&T_LABS)!=0) _g.drawString (hUnitsStr, ix1+offset, top   );
        if ((axis&B_LABS)!=0) _g.drawString (hUnitsStr, ixl+offset, bottom);
      }
      if (drawV) {         // Draw Vertical Title
        vUnitsStr += vDateTxt;
        int offset = ((iyt-iyb) + (vUnitsStr.length() * CHAR_WIDTH)) / 2;
        if ((axis&L_LABS)!=0) drawVerticalText(_g, vUnitsStr, left,  iyt-offset);
        if ((axis&R_LABS)!=0) drawVerticalText(_g, vUnitsStr, right, iyt-offset);
      }
    }
    return 0;  // end of method
  }

  /** Draws vertical text.
      @param g   The graphics (must be instanceof Graphics2D) to draw on.
      @param str The string to draw.
      @param x   The x coordinate.
      @param y   The y coordinate.
   */
  private static void drawVerticalText (Graphics g, String str ,int x, int y) {
    Graphics2D g2d = (Graphics2D)g;
    /* Apply the rotation */
    AffineTransform transform = AffineTransform.getRotateInstance(VERT_ROTATION);
    Font oldFont = g2d.getFont();
    Font newFont = oldFont.deriveFont(transform);

    g2d.setFont(newFont);

    /* Need to draw each character individually, using g2d.drawString(..)
       does not work. This is due to an issue with the g2d.drawString(..)
       method where it does not properly account for the text not being
       horizontal.
     */
    int offset = y + (str.length() * CHAR_WIDTH);
    for(int i = 0; i < str.length(); i++, offset-=CHAR_WIDTH) {
      g2d.drawChars(str.toCharArray(), i, 1, x + CHAR_HEIGHT, offset);
    }

    // Restore Settings
    g2d.setFont(oldFont);
  }

  /** Generate nice tic division
   *  @param dmin minimum
   *  @param dmax maximum
   *  @return nice tic division
   */
  public int getTicExp (double dmin, double dmax) {
    double absmax; int kengr;
    absmax = Math.max( Math.abs(dmin),Math.abs(dmax) );
    if (absmax == 0.0) return 0;
    kengr = (int)( 0.1447648 * Math.log(absmax/2) );
    if (absmax<1.0) kengr--;
    return kengr*3;
  }

  /** Generate nice tic division
   *  @param dmin minimum
   *  @param dmax maximum
   *  @param ndiv number of divisions
   *  @return nice tic division
   */
  public double getTicDelta (double dmin, double dmax, int ndiv) {
    double dtic,dran,df,ddf,sig,nsig;
    double LN2LOG10 = 0.43429448190325;
    if (dmax==dmin) return 1.0;     // degenerate case
    dran = Math.abs( dmax-dmin );
    df = dran / ndiv;
    if (df<1.0e-36) sig = -36;
    else sig = LN2LOG10 * Math.log(df);
    nsig = Math.floor(sig);
    sig = Math.pow(10.0,nsig);
    ddf = df / sig;
    if (sig==0) dtic = 1.0;
    else if (ddf<1.75) dtic = sig;
    else if (ddf<2.25) dtic = 2.0*sig;
    else if (ddf<3.50) dtic = 2.5*sig;
    else if (ddf<7.00) dtic = 5.0*sig;
    else dtic = 10.0*sig;
    // if (dmax < dmin) dtic = -dtic;
    return dtic;
  }

  /** Generate nice tic start
   *  @param dmin minimum
   *  @param dmax maximum
   *  @param dtic delta between tics
   *  @return nice tic starrt
   */
  public double getTicStart (double dmin, double dmax, double dtic) {
    double dpad,dtic1;
    if (dmax > dmin) {
      if (dmin >= 0.0) dpad = 0.995; else dpad = -0.005;
    } else {
      if (dmin >= 0.0) dpad = 0.005; else dpad = -0.995;
    }
    dtic = Math.abs(dtic);
    dtic1 = dtic * Math.floor( dmin/dtic + dpad);
    if (dmin<0) dtic1 += dtic;
    return dtic1;
  }

  /** Set plot high-contrast mode.
      @param mode The contrast mode (+1=On, -1=Off, 0=Toggle).
   */
  public void setContrast (int mode) {
    int oldoptions=options;
    if (mode==0) options &= ~O_CONTRAST;
    if (mode==1) options |= O_CONTRAST;
    if (mode==-1) options ^= O_CONTRAST;
    if (options!=oldoptions) refresh();
  }

  /** Get the pixel position along the X-axis corresponding to the given RWC.
      @param x The x-axis value in RWC.
      @return pixel position along the X-axis
   */
  public int getix (double x) {
    return (int)(x*mxx + mxb);
  }

  /** Get the pixel position along the Y-axis corresponding to the given RWC.
      @param y The y-axis value in RWC.
      @return pixel position along the Y-axis
   */
  public int getiy (double y) {
    if (view==V_MERCATOR) return viewMerc.ry2iy(y);
    return (int)(y*myy + myb);
  }

  /** Draw a circle on the surface of the earth with a radius measured in deg latitude. This method
      explicitly takes into account the fact that longitude values "narrow" near the poles (i.e.
      (1deg at Equator = N meters, 1deg at Pole = 0 meters). After computing the line that
      approximates the circle, {@link #drawLineLonLat(double[],int,int,int,Line)} is used
      to draw it correctly (taking into account that the circle may cross 180/-180 lon).
      @param lon    The longitude of the center point.
      @param lat    The latitude of the center point.
      @param radius The radius of the circle measured in deg latitude.
      @param div    Number of divisions (line segments) to make a circle (-1=UsePlotDefault).
      @param line   The line to draw with.
      @since NeXtMidas 2.5.0
   */
  public void drawCircleLonLat (double lon, double lat, double radius, int div, Line line) {
    drawCircleLonLat(lon,lat,radius,div,line,gc);
  }

  /** Draw a circle on the surface of the earth with a radius measured in deg latitude. This method
      explicitly takes into account the fact that longitude values "narrow" near the poles (i.e.
      (1deg at Equator = N meters, 1deg at Pole = 0 meters). After computing the line that
      approximates the circle, {@link #drawLineLonLat(double[],int,int,int,Line)} is used
      to draw it correctly (taking into account that the circle may cross 180/-180 lon).
      @param lon    The longitude of the center point.
      @param lat    The latitude of the center point.
      @param radius The radius of the circle measured in deg latitude.
      @param div    Number of divisions (line segments) to make a circle (-1=UsePlotDefault).
      @param line   The line to draw with.
      @param gc     The Graphics context to draw on
      @since NeXtMidas 2.9.3 (added graphics context arg)
   */
  public void drawCircleLonLat (double lon, double lat, double radius, int div, Line line, Graphics gc) {
    if (div == -1) { div = 90; } // JC4: We can optimize the default value in the future.

    int      len = (div+1)*2;
    double[] buf = new double[len]; // Points in the line { x1, y1, x2, y2, ... }
    double   ang = 0;               // Current angle to solve for.

    if ((lat >= 90) || (lat <= -90)) {
      // Handle the special case of being directly over the N/S pole.
      double deg = 360 / div; // Use deg rather than radians

      for (int i = 0; (i < buf.length) && !cancel; i+=2, ang+=deg) {
        buf[i+0] = ang;
        buf[i+1] = (lat==90)? (90 - radius): (-90 + radius);
      }
    } else {
      // For speed reasons, do all computation in radians and the convert
      // to deg just before adding the value to the buffer.
      double w      = 2 * Math.PI / div;
      double latRad = Math.toRadians(lat);
      double sinLat = Math.sin(latRad);
      double cosLat = Math.cos(latRad);
      double d      = Math.toRadians(radius);
      double cosD   = Math.cos(d);
      double sinD   = Math.sin(d);

      for (int i = 0; (i < len) && !cancel; i+=2, ang+=w) {
        // The computation or xRad and yRad is loosely based on equations posted on
        // http://viavirtualearth.com/Wiki/Draw+a+circle.ashx These equations take into
        // account the "narrowing" of the longitudinal distance (in meters) near the poles.
        double yRad = Math.asin(sinLat*cosD + cosLat*sinD*Math.cos(ang));
        double xRad = Math.atan2(Math.sin(ang)*sinD*cosLat, cosD-sinLat*Math.sin(yRad));

        buf[i+0] = Math.toDegrees(xRad) + lon;
        buf[i+1] = Math.toDegrees(yRad);
      }
    }
    buf[len-2] = buf[0];
    buf[len-1] = buf[1];

    drawLine(buf,0,2,buf.length/2,line,Pix.CLIP | Pix.KEEP,gc,true); // since 3.9.1 enable high zoom code);
  }

  /** Draw a line using a buffer of lon,lat values. Unlike the standard <code>drawLine(..)</code>
      methods this explicitly takes into account the fact that longitude values wrap around at
      180/-180 and will try to correctly handle this situation.
      @param buf   The buffer of values { lon, lat, ... }
      @param off   The offset into the buffer to start at.
      @param dim   The cardinality of the coordinates (use 2 for just lon, lat).
      @param npts  The number of points.
      @param line  The line to draw with.
      @since NeXtMidas 2.5.0
   */
  public void drawLineLonLat (double[] buf, int off, int dim, int npts, Line line) {
    drawLine(buf, off, dim, npts, line, Pix.CLIP | Pix.KEEP); // do not enable high zoom code (this is used by LayerKML)
  }

  /** Convert real world data to pixels and draw them on the plot; x values are computed based on
      fixed start and delta parameters.
      @param buf    the buffer
      @param off    the offset (this should usually be 0)
      @param dim    the dimension (usually 1 for an array of y values)
      @param npts   the number of points
      @param line   the line to draw with
      @param xstart the beginning x coordinate
      @param xdelta the change in position for the x coordinate
      @return The number of points to draw. Note that additional information is provided via
              <code>pix.off</code> and <code>pix.n</code> when {@link Pix#CLIP} is set in <code>pix.flags</code>
              (only {@link MPlot} should be using the {@link Pix#CLIP} flag).
   */
  public int drawLine (float[] buf, int off, int dim, int npts, Line line, double xstart, double xdelta) {
    while (npts>0 && buf!=null) {  // BUG 1975 - added 'buf!=null' to prevent NPE in View.rwc2pix(..)
      int n = viewer.rwc2pix(buf,off,dim,npts,pix,xstart,xdelta);  // number of points to plot
      int pt1 = (xvy)? pix.x[Math.max(0,n-1)] : pix.x[0];   // initial point pixel
      int pt2 = (xvy)? pix.x[0] : pix.x[Math.max(0,n-1)];   // end point pixel
      int d   = Math.max(1,Math.abs(pt2 - pt1));            // delta is range (positive number) between first/last pixel
      if (n>256 && n/d>maxcmp) n = -n;                      // if compressed, set n to neg (this si checked in drawPixels(..)
      if (xvy) drawPixels (pix.y,pix.x,n,line);             // if xvy, swap x and y pixel arrays
      else     drawPixels (pix.x,pix.y,n,line);
      npts -= pix.off; off += (dim*pix.off);
    }
    return off;
  }

  /** Convert real world data to pixels and draw them on the plot; x values are computed based on
      fixed start and delta parameters.
      @param buf    the buffer
      @param off    the offset (this should usually be 0)
      @param dim    the dimension (usually 1 for an array of y values)
      @param npts   the number of points
      @param line   the line to draw with
      @param xstart the beginning x coordinate
      @param xdelta the change in position for the x coordinate
      @return The number of points to draw. Note that additional information is provided via
              <code>pix.off</code> and <code>pix.n</code> when {@link Pix#CLIP} is set in <code>pix.flags</code>
              (only {@link MPlot} should be using the {@link Pix#CLIP} flag).
      @since NeXtMidas 3.2.2
   */
  public int drawLine (double[] buf, int off, int dim, int npts, Line line, double xstart, double xdelta) {
    while (npts>0 && buf!=null) {  // BUG 1975 - added 'buf!=null' to prevent NPE in View.rwc2pix(..)
      int n = viewer.rwc2pix(buf,off,dim,npts,pix,xstart,xdelta);  // number of points to plot
      int pt1 = (xvy)? pix.x[Math.max(0,n-1)] : pix.x[0];   // initial point pixel
      int pt2 = (xvy)? pix.x[0] : pix.x[Math.max(0,n-1)];   // end point pixel
      int d   = Math.max(1,Math.abs(pt2 - pt1));            // delta is range (positive number) between first/last pixel
      if (n>256 && n/d>maxcmp) n = -n;                      // if compressed, set n to neg (this si checked in drawPixels(..)
      if (xvy) drawPixels (pix.y,pix.x,n,line);             // if xvy, swap x and y pixel arrays
      else     drawPixels (pix.x,pix.y,n,line);
      npts -= pix.off; off += (dim*pix.off);
    }
    return off;
  }

  /** Sets the max compression threshold
      @param value new max compression
   */
  public void setMaxCompression (int value) { maxcmp = value; };

  /** Convert real world data to pixels and draw them on the plot. This is identical to the version
      that uses a double[] buffer, both versions are provided for performance reasons.
      @param buf  the buffer
      @param off  the offset (this should usually be 0)
      @param dim  the dimension, use 2 or 3 to indicate type of array (2=(x,y), 3=(x,y,z))
      @param npts the number of points
      @param line the line to draw with
      @return The number of points drawn. Do not rely on this return value, bounds checks and
              clipping make this value unreliable.
   */
  public int drawLine (float[] buf, int off, int dim, int npts, Line line) {
    return drawLine(buf, off, dim, npts, line, 0); // note: no high res fix for this version as it just uses float
  }

  /** Convert real world data to pixels and draw them on the plot. This is identical to the version
      that uses a float[] buffer, both versions are provided for performance reasons.
      @param buf  the buffer
      @param off  the offset (this should usually be 0)
      @param dim  the dimension, use 2 or 3 to indicate type of array (2=(x,y), 3=(x,y,z))
      @param npts the number of points
      @param line the line to draw with
      @return The number of points drawn. Do not rely on this return value, bounds checks and
              clipping make this value unreliable.
   */
  public int drawLine (double[] buf, int off, int dim, int npts, Line line) {
    return drawLine(buf, off, dim, npts, line, Pix.KEEP);
  }

  int drawLine (double[] buf, int off, int dim, int npts, Line line, boolean highZoomFix) {
    return drawLine(buf, off, dim, npts, line, Pix.KEEP, highZoomFix);
  }

  /** convert real world data to pixels */
  private int drawLine (float[] buf, int off, int dim, int npts, Line line, int pixFlags) {
    pix.flags = pixFlags;
    while (npts>0 && buf!=null) {   // BUG 1975 - added 'buf!=null' to prevent NPE in View.rwc2pix(..)
      int n = viewer.rwc2pix(buf,off,dim,npts,pix);
      if (xvy) drawPixels(pix.y, pix.x, n, line);
      else     drawPixels(pix.x, pix.y, n, line);
      npts -= pix.off; off += (dim*pix.off);

      if (pix.save) { // extra line segment due to clipping?
        if (xvy) drawPixels(pix.sy, pix.sx, 2, line);
        else     drawPixels(pix.sx, pix.sy, 2, line);
        pix.save = false;
      }
    }
    pix.flags = 0;
    return off;
  }

  /** convert real world data to pixels
   *  @since NeXtMidas 3.9.1 for NXM-3131 */
  int drawLineContour (double[] buf, int off, int dim, int npts, Line line) {
    pix.flags = Pix.KEEP;
    while (npts>0 && buf!=null) {   // BUG 1975 - added 'buf!=null' to prevent NPE in View.rwc2pix(..)
      int n = viewer.rwc2pix(buf,off,dim,npts,pix);
        if (xvy) drawPixels(pix.y, pix.x, n, line);
        else     drawPixels(pix.x, pix.y, n, line);
        npts -= pix.off; off += (dim*pix.off);
        if (pix.save) { // extra line segment due to clipping?
          if (xvy) drawPixels(pix.sy, pix.sx, pix.n, line); // Changed to use pix.n as that is the length of the save array since 3.9.2
          else     drawPixels(pix.sx, pix.sy, pix.n, line); // Changed to use pix.n as that is the length of the save array since 3.9.2
          pix.save = false;
        }
    }
    pix.flags = 0;
    return off;
  }

  /** convert real world data to pixels */
  private int drawLine (double[] buf, int off, int dim, int npts, Line line, int pixFlags) {
    return drawLine(buf,off,dim,npts,line,pixFlags,gc,false);
  }

  /** convert real world data to pixels */
  int drawLine (double[] buf, int off, int dim, int npts, Line line, int pixFlags, boolean highZoomFix) {
    return drawLine(buf,off,dim,npts,line,pixFlags,gc,highZoomFix);  // since 3.9.1 enable high zoom code
  }

  /** convert real world data to pixels
   *  @param buf      The real world coords
   *  @param off      The inital offset into the buffer
   *  @param dim      2 for x,y or 3 for x,y,x
   *  @param npts     number of points
   *  @param line     Line object (type of line)
   *  @param pixFlags pixel flags
   *  @param gc       graphics context
   *  @return offset
   */
  int drawLine (double[] buf, int off, int dim, int npts, Line line, int pixFlags, Graphics gc) {
    return drawLine(buf,off,dim,npts,line,pixFlags,gc,false);
  }

  /** convert real world data to pixels
   *  @param buf           The real world coords
   *  @param off           The initial offset into the buffer
   *  @param dim           2 for x,y or 3 for x,y,x
   *  @param npts          number of points
   *  @param line          Line object (type of line)
   *  @param pixFlags      pixel flags
   *  @param gc            graphics context
   *  @param highZoomFix   enable high zoom level fix - only applicable for certain data
   *  @return offset
   */
  int drawLine (double[] buf, int off, int dim, int npts, Line line, int pixFlags, Graphics gc, boolean highZoomFix) {
    pix.flags = pixFlags;
    boolean resetPix = false;
    boolean highZoomMXX = (view==V_GEODETIC) ? (mxx >= HIGH_ZOOM_GEO_MXX) : (mxx >= HIGH_ZOOM_MXX);
    boolean trimToVisibleLine = highZoomFix  && highZoomMXX; // TODO fix, as based on LatLon views
    DPix dpix  = null;
    boolean isContinuous = view==V_LATLONCONTINUOUS;
    if (trimToVisibleLine) {
      dpix = new DPix(npts,0); // NXM-3131
      dpix.flags = pixFlags; // NXM-3131
    }
    while (npts>0 && buf!=null) {      // BUG 1975 - added 'buf!=null' to prevent NPE in View.rwc2pix(..)
      int n, ns = 2;  // number of points in main and alt pixel buffers
      int dn = 0, dns = 2 ; // NXM-3131
      if ((view==V_LATLON || isContinuous || view==V_MERCATOR) && line.isTypeSet(Line.FILL)) {
        // special handling for filled polygons on maps that might cross the dateline (since 3.1.3 - Bug 2595)
        if (!trimToVisibleLine) {
          // use specialized fill based on the line type since 3.9.2
          n = viewer.rwc2pix(buf, off, dim, npts, pix, line.isTypeSet(Line.FILL));
        }
        else { //3/24 TODO 3.9.2 392: check why calling this method that does not seem to clip
          n  = viewer.rwc2dpixClip(buf, off, dim, npts, this, dpix);
          if (!isContinuous) ns = dpix.n;
        }
        resetPix = true;  // pix object gets modified in viewer, so it needs to be reset for later use
      } else {
        if (!trimToVisibleLine) {
          // use specialized fill based on the line type since 3.9.2 (the extra argument is ignored on 3D views)
          n = viewer.rwc2pix(buf,off,dim,npts,pix, line.isTypeSet(Line.FILL));
        }
        else {
          n = viewer.rwc2dpix(buf, off, dim, npts, dpix);
        }
      }
      if (!trimToVisibleLine) { // 3/24 just removed this ) || line.isTypeSet(Line.FILL)) {
        if (xvy) drawPixels(pix.y, pix.x, n, line,gc);
        else     drawPixels(pix.x, pix.y, n, line,gc);
        npts -= pix.off; off += (dim*pix.off);

        if (pix.save) { // extra line segment due to clipping?
          if (xvy) drawPixels(pix.sy, pix.sx,  pix.n, line,gc); // Changed to use pix.n as that is the length of the save array since 3.9.2
          else     drawPixels(pix.sx, pix.sy,  pix.n, line,gc); // Changed to use pix.n as that is the length of the save array since 3.9.2
          pix.save = false;
        }

      }
      else if (line.isTypeSet(Line.FILL)) { // cover fill case, including high precision zoom
        boolean isViewAreaInsidePolygon = false;
        Rectangle2D.Double viewBounds = new Rectangle2D.Double(ix1, iy1, ix21, iy21);
        GeometricUtil.LineSegmentGroup[] intersectingLineSegGrp = GeometricUtil.polygonRectangleIntersect(dpix.x,
            dpix.y, dpix.off, 0, viewBounds);
        // is the view box fully inside the polygon?
        if(intersectingLineSegGrp==null) {
          intersectingLineSegGrp = GeometricUtil.allOrNone(dpix.x, dpix.y, dpix.off, 0, viewBounds);
          if(intersectingLineSegGrp == null) return off;
          isViewAreaInsidePolygon = true;
        }
        if (intersectingLineSegGrp.length > 1 || isViewAreaInsidePolygon) {
          // With more than one line group we are not attempting a fill at this point
          // pseudo code on how multiple line groups can be handled is on the NXM-3131 issue
          for (int i = 0; i < intersectingLineSegGrp.length; i++) {
            if (xvy)
              drawPixels(intersectingLineSegGrp[i].ycoords, intersectingLineSegGrp[i].xcoords, intersectingLineSegGrp[i].ycoords.length, line, gc);
            else
              drawPixels(intersectingLineSegGrp[i].xcoords, intersectingLineSegGrp[i].ycoords, intersectingLineSegGrp[i].ycoords.length, line, gc);
          }

          if (dpix.save) { // extra line segment due to clipping?
            intersectingLineSegGrp = GeometricUtil.polygonRectangleIntersect(dpix.sx, dpix.sy, dpix.off,
                0, viewBounds);
            if (intersectingLineSegGrp==null) return off;
            for (int i = 0; i < intersectingLineSegGrp.length; i++) {
              if (xvy)
                drawPixels(intersectingLineSegGrp[i].ycoords, intersectingLineSegGrp[i].xcoords, intersectingLineSegGrp[i].ycoords.length, line, gc);
              else
                drawPixels(intersectingLineSegGrp[i].xcoords, intersectingLineSegGrp[i].ycoords, intersectingLineSegGrp[i].ycoords.length, line, gc);
              dpix.save = false;
            }
          }
        }
        else {
            int[][] lineSegmentGroup = GeometricUtil.connectTheDots(intersectingLineSegGrp[0].xcoords, intersectingLineSegGrp[0].ycoords,
                intersectingLineSegGrp[0].entry, intersectingLineSegGrp[0].exit, viewBounds, dpix.x, dpix.y, npts, 0);
            if (xvy)
              drawPixels(lineSegmentGroup[1], lineSegmentGroup[0], lineSegmentGroup[1].length, line, gc);
            else
              drawPixels(lineSegmentGroup[0], lineSegmentGroup[1], lineSegmentGroup[1].length, line, gc);
            if (dpix.save) { // extra line segment due to clipping?
              intersectingLineSegGrp = GeometricUtil.polygonRectangleIntersect(dpix.sx, dpix.sy, dpix.off,
                  0, viewBounds);
              if(intersectingLineSegGrp==null) return off;
              lineSegmentGroup = GeometricUtil.connectTheDots(intersectingLineSegGrp[0].xcoords, intersectingLineSegGrp[0].ycoords,
                  intersectingLineSegGrp[0].entry, intersectingLineSegGrp[0].exit, viewBounds, dpix.sx, dpix.sy, npts, 0);
              for (int i = 0; i < intersectingLineSegGrp.length; i++) {
                if (xvy)
                  drawPixels(lineSegmentGroup[1], lineSegmentGroup[0], lineSegmentGroup[1].length, line, gc);
                else
                  drawPixels(lineSegmentGroup[0], lineSegmentGroup[1], lineSegmentGroup[1].length, line, gc);
                dpix.save = false;
              }
            }
        }
        npts -= dpix.off; off += (dim*dpix.off);
      }
      else {
        Pix[] lineGrps = viewer.dpix2viewablePix(dpix, n, 0); // TODO handle pix.save for fill
        if (lineGrps != null ) {
          for (int g = 0; g < lineGrps.length; g++) {
            if (xvy) drawPixels(lineGrps[g].y, lineGrps[g].x, lineGrps[g].n, line,gc);
            else     drawPixels(lineGrps[g].x, lineGrps[g].y, lineGrps[g].n, line,gc);

            if (lineGrps[g].save) { // extra line segment due to clipping?
              if (xvy) drawPixels(lineGrps[g].sy, lineGrps[g].sx, dns, line,gc);
              else     drawPixels(lineGrps[g].sx, lineGrps[g].sy, dns, line,gc);
              dpix.save = false;
            }
          }
        }
        npts -= dpix.off; off += (dim*dpix.off);
      }
    }
    pix.flags = 0;
    if (resetPix) pix = new Pix(MAXPIX,pix.flags);
    return off;
  }

  /** draw a vertical line
   *  @param x x coordinate
   *  @return 1 if successful (0 if not)
   */
  public int drawVertical (double x) {
    int ix = (int)(x*mxx + mxb);
    if (ix<=ix1 || ix>=ix2 || gc==null) return 0;
    gc.drawLine(ix,iy1,ix,iy2-2);
    return 1;
  }

  /** draw a horizontal line
   *  @param y y coordinate
   *  @return 1 if successful (0 if not)
   */
  public int drawHorizontal (double y) {
    int iy = (int)(y*myy + myb);
    if (iy<=iy1 || iy>=iy2 || gc==null) return 0;
    gc.drawLine(ix1,iy,ix2-2,iy);
    return 1;
  }

  /** Draw an ellipse (scaled according to view). Since 2.3.0 several speed enhancements have been
      added for drawing ellipses when div=-1 (some files may have 50K+ ellipses):
      <pre>
        (1) Exclude ellipses not displayable (DONE IN THE LAYER).
        (2) If the ellipse is &lt;2 pixels across, just fill in a single pixel
            for the ellipse (this is common when zoomed out).
        (3) When zoomed out a ways lower the number of divisions used.
      </pre>
      @param lon       Longitude in degrees.
      @param lat       Latitude in degrees.
      @param alt       Altitude in meters.
      @param semiMajor Semi-major axis in meters.
      @param semiMinor Semi-minor axis in meters.
      @param tilt      Tilt of major axis in degrees.<b>NOTE: this rotating is done based on Java's method
                       starting at positive X, moving towards positive Y. To convert to 0 degrees = North,
                       do (90-tilt) or use {@link #drawEllipseOnMap(double, double, double, double, double, double, int, Line, Graphics)}</b>
      @param div       Number of divisions (line segments) to make an ellipse (-1=UsePlotDefault).
                       Should be a multiple of four.
      @param line      Line to draw with (color, line thickness, etc).
      @return Actual number of divisions used.
   */
  public int drawEllipse (double lon, double lat, double alt, double semiMajor,
                        double semiMinor, double tilt, int div, Line line) {
    return drawEllipse(lon,lat,alt,semiMajor,semiMinor,tilt,div,line,gc,false);
  }

  int drawEllipse (double lon, double lat, double alt, double semiMajor,
      double semiMinor, double tilt, int div, Line line, boolean highZoomFix) {
    return drawEllipse(lon,lat,alt,semiMajor,semiMinor,tilt,div,line,gc,highZoomFix);
  }

  /** Draw an ellipse (scaled according to view). Since 2.3.0 several speed enhancements have been
      added for drawing ellipses when div=-1 (some files may have 50K+ ellipses):
      <pre>
        (1) Exclude ellipses not displayable (DONE IN THE LAYER).
        (2) If the ellipse is &lt;2 pixels across, just fill in a single pixel
            for the ellipse (this is common when zoomed out).
        (3) When zoomed out a ways lower the number of divisions used.
      </pre>
      @param lon       Longitude in degrees.
      @param lat       Latitude in degrees.
      @param alt       Altitude in meters.
      @param semiMajor Semi-major axis in meters.
      @param semiMinor Semi-minor axis in meters.
      @param tilt      Tilt of major axis in degrees. <b>NOTE: this rotating is done based on Java's method
                       starting at positive X, moving towards positive Y. To convert to 0 degrees = North,
                       do (90-tilt) or use {@link #drawEllipseOnMap(double, double, double, double, double, double, int, Line, Graphics)}</b>
      @param div       Number of divisions (line segments) to make an ellipse (-1=UsePlotDefault).
                       Should be a multiple of four.
      @param line      Line to draw with (color, line thickness, etc).
      @param gc        The Graphics context to draw on
      @return Actual number of divisions used.
      @since NeXtMidas 2.9.3 (added gc param)
  */
  public int drawEllipse (double lon, double lat, double alt, double semiMajor,
                          double semiMinor, double tilt, int div, Line line, Graphics gc) {
    return drawEllipse(lon,lat,alt,semiMajor,semiMinor,tilt,div,line,gc,false);
  }

  int drawEllipse (double lon, double lat, double alt, double semiMajor,
        double semiMinor, double tilt, int div, Line line, Graphics gc, boolean highZoomFix) {
    int i,j;
    double r,a2i,b2i,theta,dtheta,ctheta,stheta;
    double maxdiv = ediv; // Max number of divisions.
    if (et==null) et = new Transform();
    et.setTOP(alt,lat,lon,tilt,0.0,0.0);
    a2i = 1.0/(semiMajor*semiMajor);
    b2i = 1.0/(semiMinor*semiMinor);

    if (div == -1) { // JC4 Added 05JUN06 - Need to speed up drawing of ellipses.

      // TODO - If we ever add double precision to MPoint.java, we should switch back to MPoint for this logic since 3.9.1
      // For now, we are using the DPix class as it supports double precision.
//      MPoint   center   = new MPoint(); // Center point (in pixels) OLD CODE pre 3.9.1 NXM-3131
//      MPoint   point    = new MPoint(); // Point on semiMajor axis (in pixels) OLD CODE pre 3.9.1 NXM-3131
      DPix centerD = new DPix(1, Pix.FULLKEEP); // since 3.9.1 // added FULLKEEP in 4.1.3 to get accurate delta
      DPix pointD = new DPix(1, Pix.FULLKEEP); // since 3.9.1
      double      delta;                   // Max delta (in pixels)
      double[] location = new double[] { Math.sqrt(1.0/a2i), 0.0, 0.0 }; // Point on semiMajor (RWC)


      Transform.top2ecr(location,1,1,et);
      Transform.car2geo(location,1,1);
      getPoint(lon,lat,alt,centerD);
//      getPoint(lon,lat,alt,center); // OLD CODE pre 3.9.1 NXM-3131
//      getPoint(location[2],location[1],location[0],point); //OLD CODE pre 3.9.1 NXM-3131
      getPoint(location[2],location[1],location[0],pointD); // since 3.9.1
//      delta  = Math.max(Math.abs(center.x-point.x), Math.abs(center.y-point.y)); // OLD CODE pre 3.9.1 NXM-3131
      delta  = Math.max(Math.abs(centerD.x[0]-pointD.x[0]), Math.abs(centerD.y[0]-pointD.y[0])); // since 3.9.1
      maxdiv = Math.max((delta/4), 1) * 8;

      if (delta < 2) { // Ellipse is less than 2 pixels across, just fill in a single pixel
        setColor(line.getColor());
        if (gc==null) return 0;
        if (view==MPlot.V_GEODETIC) {
          // now take into account visibility, so we do not draw something hidden behind earth
          centerD.flags = Pix.CHECK;
          getPoint(lon,lat,alt,centerD);
        }
        gc.drawLine((int)centerD.x[0],(int)centerD.y[0],(int)centerD.x[0],(int)centerD.y[0]); // since 3.9.1
//        gc.drawLine(center.x,center.y,center.x,center.y); // OLD CODE pre 3.9.1 NXM-3131
        return 1;
      }
    }

    if (div<0) div=(int) Math.min(maxdiv, ediv);
    int elen = (div+1)*3;
    if (edata==null || edata.length<elen) edata = new double[elen];
    dtheta = TWOPI/div;

    for (i=j=0; i<div; i++) {
      theta=i*dtheta; ctheta=Math.cos(theta); stheta=Math.sin(theta);
      r = Math.sqrt(1.0/(a2i*ctheta*ctheta+b2i*stheta*stheta));
      edata[j++] = r*ctheta;
      edata[j++] = r*stheta;
      edata[j++] = 0;
    }
    Transform.top2ecr (edata,1,div,et);
    Transform.car2geo (edata,1,div);
    Transform.all2lla (edata,div);
    for (i=0; i<3; i++) edata[j++]=edata[i];

    drawLine(edata, 0, 3, div+1, line, Pix.CLIP | Pix.KEEP,gc, highZoomFix); // since 3.9.1 enable high zoom code
    return div;
  }

  int drawEllipsoidSliceXYZ (Position pos, boolean isGeodeticData, double[] semis, ellipseAxis axisDiscretized,
          double[][] yprTMatrix, int div, Line line, Graphics gc, boolean highZoomFix) {

  int i,j;
  double r,a2i,b2i,theta,dtheta,ctheta,stheta;
  if (div<0) div = ediv;
  int elen = (div+1)*3;
  if (edata==null || edata.length<elen) edata = new double[elen];
  dtheta = TWOPI/div;

  if (edata==null || edata.length<elen) edata = new double[elen];
  dtheta = TWOPI/div;

  //
  switch (axisDiscretized) {
    case Z:
      a2i = 1.0/(semis[0]*semis[0]);
      b2i = 1.0/(semis[1]*semis[1]);
      break;
    case Y:
      a2i = 1.0/(semis[0]*semis[0]);
      b2i = 1.0/(semis[2]*semis[2]);
      break;
    default:
      a2i = 1.0/(semis[1]*semis[1]);
      b2i = 1.0/(semis[2]*semis[2]);
      break;
  }
  for (i=j=0; i<div; i++) {
    theta=i*dtheta; ctheta=Math.cos(theta); stheta=Math.sin(theta);
    r = Math.sqrt(1.0/(a2i*ctheta*ctheta+b2i*stheta*stheta));
    double _x, _y, _z;
    switch (axisDiscretized) {
      case Z:
        _x = r*ctheta;
        _y = r*stheta;
        _z = semis[2];
        break;
      case Y:
        _x = r*ctheta;
        _y = semis[1];
        _z = r*stheta;
        break;
      default:
        _x = semis[0];
        _y = r*ctheta;
        _z = r*stheta;
        break;
    }

    double xT = _x * yprTMatrix[0][0] + _y * yprTMatrix[0][1] + _z * yprTMatrix[0][2];
    double yT = _x * yprTMatrix[1][0] + _y * yprTMatrix[1][1] + _z * yprTMatrix[1][2];
    double zT = _x * yprTMatrix[2][0] + _y * yprTMatrix[2][1] + _z * yprTMatrix[2][2];
    edata[j++] = (isGeodeticData) ? xT : xT + pos.x;
    edata[j++] = (isGeodeticData) ? yT : yT + pos.y;
    edata[j++] = (isGeodeticData) ? zT : zT + pos.z;
  }

  // transform this from cartesian to lat, lon, alt if we are in a map view
  if (isGeodeticData) {
    et = new Transform();
    et.setTOP(pos.alt,pos.lat,pos.lon,0,0.0,0.0);
    Transform.top2ecr (edata,1,div,et);
    Transform.car2geo (edata,1,div);
    Transform.all2lla (edata,div);
  }

  for (i=0; i<3; i++) edata[j++]=edata[i];

  drawLine(edata, 0, 3, div+1, line, Pix.CLIP | Pix.KEEP,gc, highZoomFix); // since 3.9.1 enable high zoom code
  return div;
  }

  /** Same as {@link #drawEllipse(double, double, double, double, double, double, int, Line, Graphics)}, but adjusts
      tilt for 0==North
      @param lon       Longitude in degrees.
      @param lat       Latitude in degrees.
      @param alt       Altitude in meters.
      @param semiMajor Semi-major axis in meters.
      @param semiMinor Semi-minor axis in meters.
      @param tilt      Tilt of major axis in degrees.
      @param div       Number of divisions (line segments) to make an ellipse (-1=UsePlotDefault).
                       Should be a multiple of four.
      @param line      Line to draw with (color, line thickness, etc).
      @param gc        The Graphics context to draw on
      @return Actual number of divisions used.
      @since NeXtMidas 3.1.0
   */
  public int drawEllipseOnMap (double lon, double lat, double alt, double semiMajor,
                          double semiMinor, double tilt, int div, Line line, Graphics gc) {
    return drawEllipse(lon,lat,alt,semiMajor,semiMinor,90-tilt,div,line,gc);
  }

  /** Draw a conic (scaled according to view).
      This is the same as <code>drawCone(p,q,cut,div,angle,range,line,1)</code>.
      @param p     The position the cone is originating from (the viewer).
      @param q     The target location (the point the viewer is looking at).
      @param cut   The number of cuts (slices of the cone) to show (must be at least one).
      @param div   The number of divisions between <code>p</code> and <code>q</code> to draw the cuts on.
      @param angle The cone angle (in deg).
      @param range The range, set this to 0 to compute range from <code>p</code> and <code>q</code>.
      @param line  The line used to draw the cone and any cuts.
      @return <code>cut</code>.
   */
  public int drawCone (Position p, Position q, int cut, int div, double angle,
                       double range, Line line) {
    return drawCone(p,q,cut,div,angle,range,line,1);
  }

  int drawCone (Position p, Position q, int cut, int div, double angle,
      double range, Line line, boolean highZoomFix) {
    return drawCone(p,q,cut,div,angle,range,line,1,highZoomFix);
  }

  /** Draw a conic (scaled according to view).
      @param p     The position the cone is originating from (the viewer).
      @param q     The target location (the point the viewer is looking at).
      @param cut   The number of cuts (slices of the cone) to show (-1 or 1 or greater).
                   -1 means 1, but trim the cone to the earth.
      @param div   The number of divisions between <code>p</code> and <code>q</code> to draw the cuts on
                   (typically around 60).
      @param angle The cone angle (in deg).
      @param range The range, set this to 0 to compute range from <code>p</code> and <code>q</code>.
      @param line  The line used to draw the cone and any cuts.
      @param mode  The clipping mode when clipping at the Earth's surface. <pre>
                   mode =-1 - Draw both modes (mode=1 drawn normal, mode=0 drawn as dotted line).
                   mode = 1 - Cone extends to horizon.
                   mode = 0 - Clip cone at a plane perpendicular to q.</pre>
      @return <code>cut</code>.
   */
  public int drawCone (Position p, Position q, int cut, int div, double angle,
                       double range, Line line, int mode) {
    return drawCone(p, q, cut, div, angle, range, line, mode, false);
  }

  int drawCone (Position p, Position q, int cut, int div, double angle,
                       double range, Line line, int mode, boolean highZoomFix) {
    if (line==null) return cut;

    if (mode == -1) {
      drawCone(p,q,cut,div,angle,range,line,1,highZoomFix); // Draw it extending to the horizon
      mode = 0;                                 // Now continue but clipping at plane
      line = line.copy();
      line.setType(Line.POINT);
    }

    boolean trimToGeoSurface = false; // default is leave cone as full shape
    if (cut==-1) { // -1 means 1 cut and trimmed to geosurface
      trimToGeoSurface = true;
      cut = 1;
    }

    int ii=0, npts=div+2, nrec=npts*cut+1, elen=nrec*3;
    double x,y,z,r,cr,cz,theta,dtheta,azim,elev,roll,factor;
    double rclip = EARTH_EQUATORIAL_RADIUS+10000.0;
    double[] data = new double[3];
    if (ctp==null) ctp = new Transform();
    if (ctq==null) ctq = new Transform();
    if (cdata==null || cdata.length<elen) cdata = new double[elen];
    if (p.referenceFrame!=Position.GEODETIC) p.car2geo();
    ctp.setTOP(p.alt,p.lat,p.lon);
    if (q.referenceFrame!=Position.GEODETIC) q.car2geo();
    ctq.setTOP(q.alt,q.lat,q.lon);
    data[0]=data[1]=data[2]=0;
    Transform.top2top(data,1,1,ctq,ctp);
    x=data[0]; y=data[1]; z=data[2];
    r=Math.sqrt(x*x+y*y+z*z);
    azim=0; elev=0; roll=0;
    if (x!=0 || z!=0) elev = RAD2DEG*Math.atan2(-x,z);
    if (r!=0) roll = RAD2DEG*Math.asin(Math.min(1.0,Math.max(-1.0,-y/r)));
    ctp.setTOP(p.alt,p.lat,p.lon,azim,elev,roll);
    // range values other than 0 are not officially supported (sets r to range)
    if (range>0) r = range;
    dtheta = TWOPI/div;
    cr = r*Math.tan(DEG2RAD*angle);
    // negative range is unsupported (it does have the effect of reversing p and q)
    if (range<0) cz=0; else cz=r;
    cdata[ii++]=0; cdata[ii++]=0; cdata[ii++]=cz;
    for (int i=0; i<=div; i++) {
      theta = i*dtheta;
      cdata[ii++] = cr*Math.cos(theta);
      cdata[ii++] = cr*Math.sin(theta);
      cdata[ii++] = cz;
    }
    for (int j=1; j<cut; j++) {
      factor = (double)(cut-j)/(double)cut;
      if (range<0) cz=(1-factor)*r; else cz=r*factor;
      for (int i=0,jj=0; i<npts; i++) {
        cdata[ii++] = cdata[jj++] * factor;
        cdata[ii++] = cdata[jj++] * factor;
        cdata[ii++] = cz; jj++;
      }
    }
    if (range<0) cz=r; else cz=0;
    cdata[ii++]=0; cdata[ii++]=0; cdata[ii++]=cz;
    Transform.top2ecr(cdata,1,nrec,ctp);
    for (int i=0; i<3; i++) data[i]=cdata[elen-3+i];
    for (int j=0,i=0; j<cut; j++,i+=npts*3) {
      x=cdata[i+0]; y=cdata[i+1]; z=cdata[i+2]; r=Math.sqrt(x*x+y*y+z*z);
      if (r<rclip) {
        Transform.clipAtEarth(cdata,i,npts,data,mode,trimToGeoSurface,p,angle);
      }
    }
    Transform.car2geo(cdata,1,nrec);
    Transform.all2lla(cdata,nrec);
    if (trimToGeoSurface && Transform.getUseLegacyConeCode()) {
      for (int j=0,i=5; j<npts; j++,i+=3) {
        cdata[i] = 0; // zero out the altitudes
      }

    }
//DR #201 - To get footprints to draw correctly
//Pete Perrino mod as per X-Midas XGEOPLOT - 04NOV05 -- START
// Commenting out for fix to bug 871 (ConeFlagVert not extending from the
// globe in Geodetic view).
//    for (int j=0,i=0; j<cut; j++,i+=npts*3) {
//      x=cdata[i+0]; y=cdata[i+1]; z=cdata[i+2]; r=Math.sqrt(x*x + y*y + z*z);
//      if (r<rclip) {
//        for (int k=0; k < npts*3; k+=3) {
//          if (cdata[k+2] > 0.0) cdata[k+2] = 0.0; // Set alt to zero
//        }
//      }
//    }
//Pete Perrino mod -- END
    for (int j=0,i=3; j<cut; j++,i+=npts*3) {
      drawLine (cdata,i,3,div+1,line,Pix.CLIP | Pix.KEEP,highZoomFix); // since 3.9.1 enable high zoom fix
    }
    return cut;
  }

  /** Draw a shape based on pixel values. The type of shape drawn is based on the line settings;
      this includes {@link Line#FILL}, {@link Line#SOLID}, {@link Line#POINT}, {@link Line#DOT},
      {@link Line#BAR}, and {@link Line#SYMBOL} (note that these are mask values, so more than one
      can be done at any given time). <br><br>
      This method is usually called by <code>drawLine(..)</code>, <code>drawCone(..)</code>, etc.
      which take in Real-World-Coordinates and properly convert them to pixels.
      @param y  The x-axis values in pixels.
      @param x  The y-axis values in pixels.
      @param n    The number of pixels to draw (make sure pax.length&lt;=n and pay.length&lt;=n).
      @param line The line to use when drawing (nothing is drawn if this is null).
      @return Always returns n at this time.
   */
  public int drawPixels (int[] y, int[] x, int n, Line line) {
    return drawPixels(y,x,n,line,gc);
  }

  /** Draw a shape based on pixel values. The type of shape drawn is based on the line settings;
      this includes {@link Line#FILL}, {@link Line#SOLID}, {@link Line#POINT}, {@link Line#DOT},
      {@link Line#BAR}, and {@link Line#SYMBOL} (note that these are mask values, so more than one
      can be done at any given time). <br><br>
      This method is usually called by <code>drawLine(..)</code>, <code>drawCone(..)</code>, etc.
      which take in Real-World-Coordinates and properly convert them to pixels.
      @param pax  The x-axis values in pixels.
      @param pay  The y-axis values in pixels.
      @param n    The number of pixels to draw (make sure pax.length&lt;=n and pay.length&lt;=n).
      @param line The line to use when drawing (nothing is drawn if this is null).
      @param gc   The graphics context
      @return Always returns n at this time.
   */
  public int drawPixels (int[] pax, int[] pay, int n, Line line, Graphics gc) {
    if (line==null) return n;
    if (gc==null) return 0;

    if (line!=lastLine || line.getNeedRefresh()) {
      line.updateGraphics(gc);
      line.clearNeedRefresh();
      lastLine = line;
    }
    setColor(line.getColor());

    if (n<0) return drawCompressedPixels(pax,pay,-n,line);

    int type = line.getTypeInt();
    if (type == Line.SOLID) {  // 99% case for speed
      gc.drawPolyline (pax, pay, n);
      return n;
    }

    // JC4: Note that FILL is first so that the others will be drawn over top of it. This
    //      is necessary in order to get a filled shape (with alternate fill color, possibly
    //      transparent) that has a solid outline.
    // JC4: Calling fillPolygon with n==0 can cause a IllegalPathStateException (Bug 430)

    if ((type&Line.FILL)!=0 && n>0) {
      setColor(line.getFillColor());
      gc.fillPolygon (pax, pay, n);
      setColor(line.getColor());
    }
    if ((type&Line.SOLID)!=0) {
      gc.drawPolyline (pax, pay, n);
    }
    if ((type&Line.POINT)!=0) {
      for (int i=0; i<n; i++) gc.drawLine (pax[i], pay[i], pax[i], pay[i]);
    }
    if ((type&Line.DOT)!=0) {
      int r = line.getRadius(), d=r*2;
      for (int i=0; i<n; i++) gc.drawOval (pax[i]-r, pay[i]-r, d, d);
    }
    if ((type&Line.BAR)!=0) {
      for (int i=0; i<n; i++) gc.drawLine (pax[i], pay[i], pax[i], iy2);
    }
    if ((type&Line.SYMBOL)!=0) {
      Symbol sym = line.getSymbol();
      if (sym!=null) {
        for (int i=0; i<n; i++) sym.draw (pax[i], pay[i], gc);
      }
    }
    return n;
  }

  /** Draw a compressed image with pixels
      @param pax  The x-axis values in pixels
      @param pay  The y-axis values in pixels
      @param n    The number of pixels to draw
      @param line The line to use when drawing
      @return Always returns n at this time
   */
  public int drawCompressedPixels (int[] pax, int[] pay, int n, Line line) {
    if (gc==null) return 0;
    int x,y,y1,y2;
    int[] tmp;
    int type = line.getTypeInt();
    if ((type&Line.SOLID)!=0) {
      if (xvy) { tmp = pax; pax=pay; pay=tmp; }  // in view=xvy, swap pax/pay for algorithm below to work (Bug 2155)
      for (int i=0; i<n; ) {
        x = pax[i];
        y1 = y2 = pay[i];
        for (; i<n && pax[i]==x; i++) {
          y = pay[i];
               if (y<y1) y1=y;
          else if (y>y2) y2=y;
        }
        if (xvy) gc.drawLine(y1, x, y2, x);     // in view=xvy, draw from y position (Bug 2155)
        else     gc.drawLine (x, y1, x, y2);
      }
      if (xvy) { tmp = pax; pax=pay; pay=tmp; } // in view=xvy, swap pax/pay back (Bug 2155)
    }
    boolean pnt = (type&Line.POINT)!=0;
    boolean dot = (type&Line.DOT)!=0;
    boolean sym = (type&Line.SYMBOL)!=0;
    if (pnt||dot||sym) {
      int r = line.getRadius(), d=r*2;
      int _iy2 = this.iy2;  // 2010-02-12 NTN: fix for Bug 2079 for NegativeArraySizeException
      if (_iy2<0) return 0; // 2010-02-12 NTN: fix for Bug 2079 for NegativeArraySizeException
      boolean[] yb = new boolean[_iy2];
      for (int i=0; i<n; ) {
        x = pax[i];
        for (; i<n && pax[i]==x; i++) {
          y = pay[i];
          if (y>0 && y<_iy2) yb[y] = true;
        }
        for (y=iy1; y<_iy2; y++) if (yb[y]) {
          if (pnt) gc.drawLine (x, y, x, y);
          if (dot) gc.drawOval (x-r, y-r, d, d);
          if (sym) line.getSymbol().draw (x, y, gc);
          yb[y]=false;
        }
      }
    }
    return n;
  }

  /** Used for drawing images on maps
   *  @param img   image
   *  @param north north latitude
   *  @param south south latitude
   *  @param east  east longitude
   *  @param west  west longitude
   */
  public void drawImage (BufferedImage img, double north, double south, double east, double west) {

    // Make this value true to see detailed logging
    boolean debug = false;
    MPoint nwpx        = new MPoint();        // northWest pixel location
    MPoint sepx        = new MPoint();        // southEast pixel location
    double[] northWest = {west,north,0};      // northWest lat lon
    double[] southEast = {east,south,0};      // southEast lat lon
    int h              = img.getHeight();     // img height in pixels
    int w              = img.getWidth();      // img width in pixels
    double p = 0.00001; // precision
    // Left bounds is set based on mode. If in latloncontinuous, use the viewer.MP.orx1 value
    double leftBounds = (viewer.MP.view==MPlot.V_LATLONCONTINUOUS) ? viewer.MP.orx1 : MPlot.LONGITUDE_DEFAULT_MIN;
    double rightBounds = leftBounds + MPlot.LONGITUDE_RANGE;

    // does the image cross the international date line?
    boolean imageCrossBounds = (Math.abs(west - east) > LONGITUDE_RANGE/2);
    boolean crossOrigBounds = imageCrossBounds; // we need to know if it crossed bounds at the original map location (-180, 180)
    double unwrappedEast = (imageCrossBounds) ? east + MPlot.LONGITUDE_RANGE : east;

    if(debug) {
      System.out.println("crossDateLine ="+ imageCrossBounds);
      System.out.println("east="+east);
      System.out.println("west="+west);
      System.out.println("viewer.mp.rx1="+viewer.MP.rx1);
      System.out.println("viewer.mp.rx2="+viewer.MP.rx2);
      System.out.println("viewer.mp.orx1="+viewer.MP.orx1);
      System.out.println("viewer.mp.orx2="+viewer.MP.orx2);
    }
    boolean reversed = false; // This will decide whether to draw items backwards or not
    double reversedWidth = 0; // calculate this value if the image is reversed
    // Handle case of image that now crosses outer bounds
    double smallest = west; // set smallest equal to the left side of image
    double largest = unwrappedEast; // set largest equal to the right side of image
    // if running in latloncontinuous and has extended range need to do some checks
    // reversed mode
    if (west > unwrappedEast) {
      smallest = unwrappedEast; // smallest is now the right side
      largest = west; // largest is now the left side
      reversedWidth = Math.max(east, west) - Math.min(east, west); // find the width of the image
      reversed = true; // do reversed drawing
    }
    if(viewer.MP.view==V_LATLONCONTINUOUS && viewer.MP.hasExtendedRange()) {

      // check if it crosses on the right side
      boolean crossesRightBounds = (smallest < rightBounds && largest > rightBounds);
      // check if it crosses on the left side
      boolean crossesLeftBounds = (smallest < leftBounds &&  largest > leftBounds);
      if (imageCrossBounds && !(crossesRightBounds || crossesLeftBounds)) {
        east += MPlot.LONGITUDE_RANGE; // Add 360 to the east
        southEast[0]=east; // set the value in south east for drawing
        imageCrossBounds=false; // no longer cross bounds
      }
      else if (!imageCrossBounds) {
        if (crossesRightBounds || crossesLeftBounds) {
          // this means that is is crossing the adjusted bounds
          imageCrossBounds = true;
          if(reversed) {
            // Need to reverse the points here
            northWest[0]=east;
            southEast[0]=west;
          }
        }
      }
    } else if(imageCrossBounds && reversed) {
      // Need to reverse the points here
      northWest[0]=east;
      southEast[0]=west;
    }
    if (debug) {
      System.out.println("imageCrossBounds = " + imageCrossBounds);
      System.out.println("east after latloncheck=" + east);
    }
    boolean isViewGeo = (viewer instanceof ViewGeo);
    if (!isViewGeo && imageCrossBounds) {
      double max = rightBounds;
      double min = leftBounds;

      double[] newSouthEast   = {max - p,south,0}; // southEast pixel location for the 1st image
      double[] newNorthWest   = {min + p,north,0}; // northWest pixel location for the 2nd image
      // if the image crosses at the original bounds of -180/180, then set supplemental to 0 - otherwise set it to MPlot.LONGITUDE_RANGE
      double supplementalRange = crossOrigBounds ? MPlot.LONGITUDE_RANGE : 0;
      // Get the width using the supplementalRange
      double totalWidth = reversed ? reversedWidth : east + supplementalRange - west;
      // Figure out continuous adjust - if in extended range it is set to MPlot.LONGITUDE_RANGE, otherwise it is 0
      // Also, if the max - the right edge of the image is less than the total width, there is no need for adjusting.
      double continuousAdjust = ((max > MPlot.LONGITUDE_DEFAULT_MAX + p) && !((max - (reversed ? east : west)) <= totalWidth))? MPlot.LONGITUDE_RANGE : 0;
      if(debug) System.out.println("TotalWidth = "+totalWidth + " max:"+ max+" continuousAdjust:"+continuousAdjust);

      double   ratio;                                  // ratio of new Width to orig width (set below)
      int      newWidth;                               // width of the new image
      // 1st img: which is the left part of the orig image displayed on the right side of the screen
      // Get the partial width
      double partialWidth = newSouthEast[0] - continuousAdjust - (reversed? east : west);
      // Check if the partial width is a valid value (can't be larger than the width or negative)
      // If it is not valid, calculate it another way.
      if(partialWidth > totalWidth || partialWidth < 0) {
        partialWidth = totalWidth - (newSouthEast[0] - (reversed? east : west));
      }
      ratio = partialWidth / totalWidth;
      newWidth = (int) (w * ratio);     // how much of the orig image to draw

      if(debug) {
        System.out.println("newSouthEast[0]="+newSouthEast[0]);
        System.out.println("west = "+west);
        System.out.println("ratio1 = "+ratio);
        System.out.println("newWidth = "+newWidth);
      }

      // If reversed, we need to switch to newNorthWest
      viewer.rwc2pix(reversed ? newNorthWest : northWest, nwpx); // northWest pixel location
      // If reversed, we to to switch to newSouthEast
      viewer.rwc2pix(reversed ? southEast : newSouthEast, sepx); // southEast pixel location
      //Get the location of the image where the split will occur
      int splitLocationOfImage = reversed ? w-newWidth : newWidth;
      // Draw from 0 to the split location of the image on the right side.
      gc.drawImage(img, reversed ? sepx.x : nwpx.x, nwpx.y, reversed ? nwpx.x : sepx.x, sepx.y, 0, 0, splitLocationOfImage, h, this);
      // 2nd img: which is the right part of the orig image displayed on the left side of the screen
      if(debug) System.out.println("ratio2 = "+ratio);
      if(debug) System.out.println("newWidth = "+newWidth);
      // If reversed, we need to switch to northWest
      viewer.rwc2pix(reversed ? northWest : newNorthWest, nwpx); // northWest pixel location
      // If reversed, we need to switch to newSouthEast
      viewer.rwc2pix(reversed ? newSouthEast : southEast, sepx); // southEast pixel location
      // Draw from from the split location of the image to the width location of the image
      gc.drawImage(img, reversed ? sepx.x : nwpx.x, nwpx.y, reversed ? nwpx.x : sepx.x, sepx.y, splitLocationOfImage , 0, w, h, this);
    } else {
      viewer.rwc2pix(northWest, nwpx);       // northWest pixel location
      viewer.rwc2pix(southEast, sepx);       // southEast pixel location
      if (isViewGeo) forceKeepWhenOneSideVisible(nwpx, sepx, northWest, southEast);
      if(debug) System.out.println("nwpx.x="+nwpx.x+", nwpx.y="+nwpx.y+", sepx.x="+sepx.x+", sepx.y="+sepx.y+"h="+h+"w="+w);
      gc.drawImage(img, nwpx.x, nwpx.y, sepx.x, sepx.y, 0, 0, w, h, this);
    }
    if(debug) System.out.println("\n\n");

  }

  /** Redo the point that is not visible when the other one is visible */
  private void forceKeepWhenOneSideVisible (MPoint nwpx, MPoint sepx,
      double[] northWest, double[] southEast) {
    // handle case with one side of image visible and the other side not NXM-3992
    boolean visNW = (nwpx.x!=0) || (nwpx.y!=0);
    boolean visSE = (sepx.x!=0) || (sepx.y!=0);
    if (visNW && !visSE) {
      tpix.flags = Pix.FULLKEEP;
      viewer.rwc2pix(southEast, sepx);
      tpix.flags = Pix.CHECK;
    }
    else if (visSE && !visNW) {
      tpix.flags = Pix.FULLKEEP;
      viewer.rwc2pix(northWest, nwpx);
      tpix.flags = Pix.CHECK;
    }
  }

  /** Common draw for use by various Image layer formats.
      @param im       The image to draw.
      @param pixels   Pixels to draw (null for pure image data).
      @param mercator Is the image using a Mercator projection (used for scaling)?
                      (true=Mercator, false=LatLon/XY)
      @param xstart   X-start for the image (in RWC).
      @param xdelta   X-delta for the image (in RWC).
      @param nx       Number of pixels across in the X direction.
      @param ystart   Y-start for the image (in RWC).
      @param ydelta   Y-delta for the image (in RWC).
      @param ny       Number of pixels across in the Y direction.
   */
  public void drawImage (Image im, int[] pixels, boolean mercator,
                         double xstart, double xdelta, int nx,
                         double ystart, double ydelta, int ny) {
    if (gc==null) return;
    boolean outofrange;

    // find viewable region intersected with gridable region
    double gx1=xstart, gx2=xstart+(nx-1)*xdelta;
    double gy1=ystart, gy2=ystart+(ny-1)*ydelta;
    // If we are in latloncontinous mode, we need to use the logic in drawImage(BufferedImage).
    // Cast it and use the values to draw the image.
    if(view == V_LATLONCONTINUOUS) {
      drawImage(GraphicsUtil.toBufferedImage(im), gy1, gy2, gx2, gx1); // Corrected parameter order in 3.9.1
      return;
    }
    if (mercator) gy1 = ViewMercader.mer2lat(gy1);
    if (mercator) gy2 = ViewMercader.mer2lat(gy2);
    double[] gridBox = getXYGrid( (xdelta>0)?gx1:gx2, (xdelta>0)?gx2:gx1,
                              (ydelta>0)?gy1:gy2, (ydelta>0)?gy2:gy1);
    gx1=gridBox[0]; gx2=gridBox[1]; gy1=gridBox[2]; gy2=gridBox[3];
    if (mercator) gy1 = ViewMercader.lat2mer(gy1);
    if (mercator) gy2 = ViewMercader.lat2mer(gy2);

    int jx1=0, jx2=nx, jy1=0, jy2=ny;
    int _ix1, _ix2, _iy1, _iy2;
    if (xdelta>0) {
      outofrange = (xstart>gx2 || xstart+nx*xdelta<gx1);
      jx1 = Math.max(jx1,(int)((gx1-xstart)/xdelta+.49));
      jx2 = Math.min(jx2,(int)((gx2-xstart)/xdelta+1.51));
    } else {
      outofrange = (xstart<gx1 || xstart+nx*xdelta>gx2);
      jx1 = Math.max(jx1,(int)((gx2-xstart)/xdelta+.49));
      jx2 = Math.min(jx2,(int)((gx1-xstart)/xdelta+1.51));
    }
    if (outofrange || cancel) return;
    if (ydelta>0) {
      outofrange = (ystart>gy2 || ystart+ny*ydelta<gy1);
      jy1 = Math.max(jy1,(int)((gy1-ystart)/ydelta+.49));
      jy2 = Math.min(jy2,(int)((gy2-ystart)/ydelta+1.51));
    } else {
      outofrange = (ystart<gy1 || ystart+ny*ydelta>gy2);
      jy1 = Math.max(jy1,(int)((gy2-ystart)/ydelta+.49));
      jy2 = Math.min(jy2,(int)((gy1-ystart)/ydelta+1.51));
    }
    if (outofrange || cancel) return;
    if (xvy) {
      _iy1 = (int)(mxx*(xstart+(jx1-.5)*xdelta)+mxb);
      _iy2 = (int)(mxx*(xstart+(jx2-.5)*xdelta)+mxb);
      _ix1 = (int)(myy*(ystart+(jy1-.5)*ydelta)+myb);
      _ix2 = (int)(myy*(ystart+(jy2-.5)*ydelta)+myb);
      gc.drawImage(im, _ix1,_iy1,_ix2,_iy2, jx1,jy1,jx2,jy2, this);
    }
    else if (is2D && (view != V_MERCATOR) && !mercator) {
      // Quick version (Does not work for non-linear projections like Mercator)
      _ix1 = (int)(mxx*(xstart+(jx1-.5)*xdelta)+mxb);
      _ix2 = (int)(mxx*(xstart+(jx2-.5)*xdelta)+mxb);
      _iy1 = (int)(myy*(ystart+(jy1-.5)*ydelta)+myb);
      _iy2 = (int)(myy*(ystart+(jy2-.5)*ydelta)+myb);
      gc.drawImage(im, _ix1,_iy1,_ix2,_iy2, jx1,jy1,jx2,jy2, this);
    }
    else {
      Pix p0,p1;
      BufferedImage bi = null;
      boolean usePixels = (pixels!=null);
      boolean hasAlpha = !usePixels;         // Ues an image, assume Alpha
      if (!usePixels) bi = (BufferedImage)im;
      int i,k,jx,jy,jx21=(jx2-jx1);
      double factor = 1/mxx;
      if (view==V_GEODETIC) factor /= (Constants.DEG2RAD/2);
      if (is3DView()) factor /= view3D.getScale();
      factor /= xdelta;
      int jd = Math.max(1,(int)(factor+.3));
      int nx0=(jx2-jx1)/jd, nx1=nx0+1, nx3=nx1*3;
      int[] tx = new int[4], ty = new int[4];
      Pix pa = new Pix(nx1,Pix.CHECK);
      Pix pb = new Pix(nx1,Pix.CHECK);
      double[] dbuf = new double[nx3];
      for (jy=jy1-1,k=0; jy<jy2 && !cancel; jy+=jd,k=1-k) {
        double y = ystart + (jy+1)*ydelta;
        if (mercator) y = ViewMercader.mer2lat(y);
        if (jy<jy1) for (jx=jx1,i=0; i<nx3; jx+=jd) {
           dbuf[i++] = xstart + jx*xdelta; dbuf[i++] = y; dbuf[i++] = 0;
        }
        else for (/*jx=jx1,*/i=1; i<nx3; i+=3) dbuf[i]=y;
        if (k==0) { p0=pa; p1=pb; }
        else      { p0=pb; p1=pa; }
        viewer.rwc2pix(dbuf,0,3,nx1,p1);
        if (jy<jy1) continue;
        for (jx=jx1,i=0; i<nx0; i++,jx+=jd) {
          tx[0]=p0.x[i]; tx[1]=p0.x[i+1]; tx[2]=p1.x[i+1]; tx[3]=p1.x[i];
          ty[0]=p0.y[i]; ty[1]=p0.y[i+1]; ty[2]=p1.y[i+1]; ty[3]=p1.y[i];
          if (view==V_GEODETIC && (tx[0]==0||tx[1]==0||tx[2]==0||tx[3]==0) ) continue;
          int color = usePixels? pixels[jx+jy*nx] : bi.getRGB(jx,jy);
          gc.setColor(new Color(color, hasAlpha));
          gc.fillPolygon(tx,ty,4);
        }
      }
    }
  }

  @Override
  public boolean imageUpdate (Image img, int infoflags, int x, int y, int width, int height) {
    return true;
  }

  /** Returns the flag for drawing new data
      @return -1: do not draw new data
               0: refresh
               1: draw new data
   */
  public int dataMode () {
    if (level==0) return 1;
    else if ((options&O_BLOCK)!=0) return 0;
    else if ((options&O_STILL)!=0) return -1;
    else return 1;
  }

  /** Gets the position (relative to the actual data being plotted) from the
      current position on the plot.
      @return The position indicator (indicating the relative data position).
      @see #getPosition(int,int,Position)
   */
  public Position getPosition () {
    getPosition (px,py,mp);
    return mp;
  }

  /** Gets the position (relative to the actual data being plotted) from a point on the plot.
      @param px The x-position in the plot. NOT the RWC for the position.
      @param py The y-position in the plot. NOT the RWC for the position.
      @return The position indicator (indicating the relative data position).
      @see #getPosition(int,int,Position)
   */
  public Position getPosition (int px, int py) {
    Position p = new Position();
    getPosition (px,py,p);
    return p;
  }

  /** Gets the position (relative to the actual data being plotted) from a point on the plot.
      @param px The x-position in the plot. NOT the RWC for the position.
      @param py The y-position in the plot. NOT the RWC for the position.
      @param p  The position indicator to be set to indicate the relative data position.
      @see #getPosition(int,int)
   */
  public void getPosition (int px, int py, Position p) {
    double[] rwc = new double[3];
    MPoint _pix = new MPoint(px,py);
    if (view==V_MERCATOR) viewer.pix2rwc(_pix,rwc);
    else view2D.pix2rwc(_pix,rwc);
    p.x = rwc[0];
    p.y = rwc[1];
    p.z = 0;
    p.t = -1;
    Layer lay = getBaseLayer();
    if (lay != null) {
      // Since 3.3.2 the following have moved into Layer and Layer2D:
      //   p.z = ((Layer2D)lay).getZAtPos(p);
      //   p.t = lay.getTime(px,py);
      p = lay.getPositionMPlot(px,py,p);
    }
    if (p.t == -1) {
      p.t = (time>0)? time : p.x;
    }
    if (view==V_GEODETIC) {
      viewGeo.pix2rwc(_pix,rwc);
      p.lon = rwc[0];
      p.lat = rwc[1];
      p.alt = rwc[2];
    }
    else {
      p.lon = p.x;
      p.lat = p.y;
      p.alt = 0;
    }
    if (isMapView()) p.referenceFrame=Position.GEODETIC;
    else if (view==V_3DGRAPH) p.referenceFrame= Position.GRAPH3D;
    else p.referenceFrame=Position.NORMAL;
  }

  /** Gets the pixel position of a given point.
      @param p The point in RWC.
      @return The new pixel position.
      @since NeXtMidas 2.5.0
   */
  public MPoint getPoint (Position p) {
    MPoint _pix = new MPoint();
    getPoint(p,_pix);
    return _pix;
  }

  /** Gets the pixel position of a given point.
      @param p   The point in RWC.
      @param pix The destination for the pixel position.
      @return Return value is not applicable, and it maintained for backwards compatibility.
   */
  public int getPoint (Position p, MPoint pix) {
    if (p.referenceFrame==Position.GEODETIC) {
      return getPoint(p.getLon(), p.getLat(), p.getAlt(), pix);
    }
    else if (isMapView()) {
      p.car2geo();
      return getPoint(p.getLon(), p.getLat(), p.getAlt(), pix);
    }
    else {
      return getPoint(p.getX(), p.getY(), p.getZ(), pix);
    }
  }

  /** Gets the pixel position of a given point.
      Note: currently unused as of 3.9.1
      @param p   The point in RWC.
      @param pix The destination for the pixel position.
      @return Return value is not applicable, and it maintained for backwards compatibility.
      @since NextMidas 3.9.1
   */
  int getPoint (Position p, DPix pix) {
    if (p.referenceFrame==Position.GEODETIC || p.referenceFrame == Position.GRAPH3D) {
      return getPoint(p.getLon(), p.getLat(), p.getAlt(), pix);
    }
    else if (isMapView()) {
      p.car2geo();
      return getPoint(p.getLon(), p.getLat(), p.getAlt(), pix);
    }
    else {
      return getPoint(p.getX(), p.getY(), p.getZ(), pix);
    }
  }

  /** Gets the pixel position of a given point.
      @param x   real-world x.
      @param y   real-world y.
      @param z   real-world z.
      @param pix The destination for the pixel position.
      @return Return value is not applicable, and it maintained for backwards compatibility.
   */
  public int getPoint (double x, double y, double z, MPoint pix) {
    double[] rwc = { x, y, z };
    return viewer.rwc2pix(rwc,pix);
  }

  /** Gets the pixel position of a given point.
      @param x   real-world x.
      @param y   real-world y.
      @param z   real-world z.
      @param pix The destination for the double pixel position.
      @return Return value is not applicable, and it maintained for backwards compatibility.
  */
  int getPoint (double x, double y, double z, DPix pix) {
    double[] rwc = { x, y, z };
    return viewer.rwc2dpix(rwc,pix);
  }

  /** Gets the pixel position of a given point.
    @param x   real-world x.
    @param y   real-world y.
    @param z   real-world z.
    @param pix The destination for the pixel position.
    @param rangeAdjust Adjust the range if in new bounds of continuous lat lon plot.
    @return Return value is not applicable, and it maintained for backwards compatibility.
    @since NextMidas 3.9.1
  */
 int getPoint (double x, double y, double z, MPoint pix, boolean rangeAdjust) {
    double[] rwc = { x, y, z };
    if (view==V_LATLONCONTINUOUS) return viewer.rwc2pix(rwc,pix, rangeAdjust);
    else return viewer.rwc2pix(rwc,pix);
 }

 int getPoint (double x, double y, double z, DPix pix, boolean rangeAdjust) {
   double[] rwc = { x, y, z };
   return viewer.rwc2dpix(rwc,pix);
 }

  /** set the ellipse divisions
   *  @param value ellipse divisions */
  public void setEllipseDiv (int value) { ediv=value; refresh(); }

  /** Gets the plot view (see {@link #viewList}).
   *  @return plot view */
  public String getView () { return Parser.get(viewList,view); }

  /** Gets the plot mode (see {@link #modeList}).
   *  @return plot mode */
  public String getMode () { return Parser.get(modeList,mode); }

  /** Indicates if a map view ({@link #V_GEODETIC}, {@link #V_MERCATOR}, or {@link #V_LATLON}) is
      being used.
      @return true for any map view, false for all others.
      @since NeXtMidas 2.7.0
   */
  public boolean isMapView () {
    return (view == V_GEODETIC) || (view == V_MERCATOR) || (view == V_LATLON) || view==V_LATLONCONTINUOUS;
  }

  boolean isLatLonView () {
    // If geodetic needs to be supported, need to adjust the getZoomLevelAsDecimal method
    return (view == V_LATLON || view == V_LATLONCONTINUOUS);
  }

  boolean isStandardView () {
    return ((view == V_LATLON)   || (view == V_LATLONCONTINUOUS) ||
            (view == V_GEODETIC) || (view == V_MERCATOR) ||
            (view == V_3DGRAPH)  || (view == V_XYZ)      ||
            (view == V_POLAR)    || isStandardXYView());
  }

  boolean isStandardXYView () {
    return ((view == V_IXIY)     || (view == V_IYIX) ||
            (view == V_IXY)      || (view == V_IYX)  ||
            (view == V_XIY)      || (view == V_YIX)  ||
            (view == V_XY)       || (view == V_YX));
  }

  /** Gets the next link, which for {@link MPlot} is the base layer. */
  @Override
  public Object getNextLink () { return getBaseLayer(); }

  /** Gets the previous link, which for {@link MPlot} is the message handler (which
      is almost always PLOT).
   */
  @Override
  public Object getPrevLink () { return mh; }

  /** A generic method that will zoom and re-center a plot. To keep this as
      generic as possible this uses percentages (<code>0.00</code> to
      <code>1.00</code>) of the complete data range and internally converts
      the percentages into the applicable units. <br>
      <br>
      Example:
      <pre>
        To see the central one-third of the left half of the plot
        (see FIGURE 1) use:
           xCenter = 0.25 (horizontally center on 25% across)
           yCenter = 0.50 (vertically center on the midpoint)

           xZoom   = 0.50 (show 50% of the width)
           yZoom   = 0.33 (show 33% of the height)


           0% +---------+---------+---------+---------+
              |                                       |  [ ] Not Shown
              |                                       |
              |###################                    |  [#] Shown After Calling
          50% +###################                    +      centerAndZoom(..)
              |###################                    |
              |                                       |
              |                                       |
         100% +---------+---------+---------+---------+
              0%       25%       50%       75%       100%

                               FIGURE 1
      </pre>

      @param xCenter  The horizontal center of the image (percent).
      @param yCenter  The  vertical  center of the image (percent).
      @param xZoom    The horizontal scale/zoom factor (percent).
      @param yZoom    The  vertical  scale/zoom factor (percent).

      @since NeXtMidas 1.8.0
   */
  public void centerAndZoom (double xCenter, double yCenter,
                             double xZoom,   double yZoom) {
    double xMin = this.orx1;
    double xMax = this.orx2;
    double yMin = this.ory1;
    double yMax = this.ory2;

    double xCtr = (xCenter == 0)? xMin : (((xMax - xMin) * xCenter) + xMin);
    double yCtr = (yCenter == 0)? yMin : (((yMax - yMin) * yCenter) + yMin);

    double xDelta = (xMax - xMin) * xZoom / 2.0d;
    double yDelta = (yMax - yMin) * yZoom / 2.0d;

    zoom(new double[] { (xCtr-xDelta), (xCtr+xDelta),
                        (yCtr-yDelta), (yCtr+yDelta) }, 0);
    hardRefresh();
  }

  /** Get string representation of the X-units
      @return string representation of the X-units
   */
  public String getXUnitsString () {
    return xnf.getCmult() + xnf.getUnits();
  }

  /** Get string representation of the Y-units
      @return string representation of the Y-units
   */
  public String getYUnitsString () {
    return ynf.getCmult() + ynf.getUnits();
  }

  /** Gets the X-axis units for the plot.
      @return The X-axis units for the layer (see {@link Units}).
      @since NeXtMidas 1.9.3
   */
  public int getXUnits () {
    if (view==V_LATLON || view==V_LATLONCONTINUOUS || view==V_MERCATOR) return Units.LONGITUDE;
    return xUnitsID;
  }

  /** Gets the Y-axis units for the plot.
      @return The Y-axis units for the layer (see {@link Units}).
      @since NeXtMidas 1.9.3
   */
  public int getYUnits () {
    if (view==V_LATLON || view==V_LATLONCONTINUOUS || view==V_MERCATOR) return Units.LATITUDE;
    return yUnitsID;
  }

  /** Gets the Z-axis units for the plot.
      @return The Z-axis units for the layer (see {@link Units}).
      @since NeXtMidas 1.9.3
   */
  public int getZUnits () { return zUnitsID; }

  /** Set the exponent conversion settings for the x axis indicating an integer argument
   *  @param mult x multiplier in int form */
  public void setXMult (int mult) {
    setMult(mult, 'X');
  }
  /** Set the exponent conversion settings for the x axis indicating a String argument
   *  @param mult x multiplier in String form */
  public void setXMult (String mult) {
    setMult(mult, 'X');
  }

  /** Sets the exponent in the format class, and uses it for converting the exponent of the default units
   *  @param obj  Number or string representing the multiplier
   *  @param axis the axis (X or Y)
   */
  public void setMult (Object obj, char axis) {
    if (axis == 'X') {
      xnf = new Format(READOUT_FORMAT_DEFAULT+ " ?" +DataFile.getUnitsNameShort(getXUnits()));
      xnf.setExp(obj);
      xMult = xnf.getExp();
    }
    else if (axis == 'Y') {
      ynf = new Format(READOUT_FORMAT_DEFAULT+ " ?" +DataFile.getUnitsNameShort(getYUnits()));
      ynf.setExp(obj);
      yMult = ynf.getExp();
    }
    refresh();
  }

  /** Allows users to set the exponent conversion settings for the y axis indicating a String argument
      @param mult  exponent
   */
  public void setYMult (String mult) {
    setMult(mult, 'Y');
  }

  /** Allows users to set the exponent conversion settings for the y axis indicating an integer argument
      @param mult  exponent
   */
  public void setYMult (int mult) {
    setMult(mult, 'Y');
  }

  /** Sets the exponent in the format class, and uses it for converting the exponent of the default units
      @param mult  exponent
      @param axis  character representing the axis ('X', 'Y', or 'Z')
   */
  public void setMult (int mult, char axis) {
    if (axis == 'X') {
      xnf = new Format(READOUT_FORMAT_DEFAULT+ " ?" +DataFile.getUnitsNameShort(getXUnits()));
      xnf.setExp(mult);
      xMult = mult;
    }
    else if (axis == 'Y') {
      ynf = new Format(READOUT_FORMAT_DEFAULT+ " ?" +DataFile.getUnitsNameShort(getYUnits()));
      ynf.setExp(mult);
      yMult = mult;
    }
    refresh();
  }

  /** Sets the X-axis units for the plot.
      @param units The X-axis units for the layer (see {@link Units}).
                   Use -1 to set units based on the base layer for the plot.
      @since NeXtMidas 1.9.3
   */
  public void setXUnits (int units)  {
    setXUnits(units, true); // set x-axis units and refresh plot if units changed
  }

  /** Sets the X-axis units for the plot.
      @param units The X-axis units for the layer (see {@link Units}).
                   Use DEFAULT to set units based on the base layer for the plot.
      @since NeXtMidas 2.1.1
   */
  public void setXUnits (String units)  {
    setXUnits( units.equalsIgnoreCase("DEFAULT")? -1 : DataFile.getUnitsID(units), true ); // refresh if units change
  }

  /** Sets the X-axis units for the plot.
      @param units   The X-axis units for the layer (see {@link Units}).
                     Use -1 to set units based on the base layer for the plot.
      @param refresh true: refresh the plot if units are updates, false: no refresh
      @since NeXtMidas 3.6.0
   */
  private boolean setXUnits (int units, boolean refresh)  {
    if (units < 0) {
      units = (layers.size()>1)? getBaseLayer().getXUnits() :  Units.NONE;
    }
    boolean xUnitsChanged = (units != xUnitsID);
    if (xUnitsChanged) {
      xUnitsID = units;
      if (refresh) refresh();
    }
  return xUnitsChanged;
  }

  /** Sets the Y-axis units for the plot.
      @param units The Y-axis units for the layer (see {@link Units}).
                   Use -1 to set units based on the base layer for the plot.
      @since NeXtMidas 1.9.3
   */
  public void setYUnits (int units)  {
    setYUnits(units, true); // set y-axis units and refresh plot if units changed
  }

  /** Sets the Y-axis units for the plot.
      @param units The Y-axis units for the layer (see {@link Units}). Use DEFAULT to set units
                   based on the base layer for the plot.
      @since NeXtMidas 2.1.1
   */
  public void setYUnits (String units)  {
    setYUnits( units.equalsIgnoreCase("DEFAULT")? -1 : DataFile.getUnitsID(units), true );  // refresh if units change
  }

  /** Sets the Y-axis units for the plot.
      @param units   The Y-axis units for the layer (see {@link Units}).
                     Use -1 to set units based on the base layer for the plot.
      @param refresh true: refresh the plot if units are updates, false: no refresh
      @since NeXtMidas 3.6.0
   */
  private boolean setYUnits (int units, boolean refresh)  {
    if (units < 0) {
      units = (layers.size()>1)? getBaseLayer().getYUnits() :  Units.NONE;
    }
    boolean yUnitsChanged = (units != yUnitsID);
    if (yUnitsChanged) {
      yUnitsID = units;
      if (refresh) refresh();
    }
    return yUnitsChanged;
  }

  /** Sets the Z-axis units for the plot.
      @param units The Z-axis units for the layer (see {@link Units}). Use -1 to set units based on
                   the base layer for the plot.
      @since NeXtMidas 1.9.3
   */
  public void setZUnits (int units)  {
    setZUnits(units, true); // set z-axis units and refresh plot if units changed
  }

  /** Sets the Z-axis units for the plot.
      @param units The Z-axis units for the layer (see {@link Units}). Use DEFAULT to set units
                   based on the base layer for the plot.
      @since NeXtMidas 2.1.1
   */
  public void setZUnits (String units)  {
    setZUnits( units.equalsIgnoreCase("DEFAULT")? -1 : DataFile.getUnitsID(units), true );  // refresh if units change
  }

  /** Sets the Z-axis units for the plot.
      @param units   The Z-axis units for the layer (see {@link Units}).
                     Use -1 to set units based on the base layer for the plot.
      @param refresh true: refresh the plot if units are updates, false: no refresh
      @since NeXtMidas 3.6.0
   */
  private boolean setZUnits (int units, boolean refresh)  {
    if (units < 0) {
      units = (layers.size()>1)? getBaseLayer().getZUnits() :  Units.NONE;
    }
    boolean zUnitsChanged = (units != zUnitsID);
    if (zUnitsChanged) {
      zUnitsID = units;
      if (refresh) refresh();
    }
    return zUnitsChanged;
  }

  /**
     Sets the X, Y, and Z-axis units for the plot.
     @param xunits  The X-axis units for the layer (see {@link Units}).
                    Use DEFAULT to set units based on the base layer for the plot.
     @param yunits  The Y-axis units for the layer (see {@link Units}). Use DEFAULT to set units
                    based on the base layer for the plot.
     @param zunits  The Z-axis units for the layer (see {@link Units}). Use -1 to set units based on
                    the base layer for the plot.
     @param refresh true: refresh the plot if units are updates, false: no refresh
     @return true if units have changed
     @since 3.6.0 for Bugs 2894 and 2431
   */
  public boolean setXYZUnits (int xunits, int yunits, int zunits, boolean refresh)  {
    boolean xUnitsChanged = setXUnits(xunits, false); // set x-axis units, no refresh
    boolean yUnitsChanged = setYUnits(yunits, false); // set y-axis units, no refresh
    boolean zUnitsChanged = setZUnits(zunits, false); // set z-axis units, no refresh
    boolean unitsChanged  = (xUnitsChanged || yUnitsChanged || zUnitsChanged);

    // if refresh is true, refresh if there is a change in x, y, or x units
    if (refresh && unitsChanged) {
      refresh();
    }

    return unitsChanged;
  }

  /** Set the label displayed above the units on the X Axis<br><br>
      Note: This takes the place of the Units String being displayed but does not change the Units themeselves.
      @param label The text to set the label to. Use null or "" to revert to default behaviour (String form of Units)
      @since NeXtMidas 3.3.1
   */
  public void setXAxisLabel (String label) {
    setAxisLabels(label, customYAxisLabel);
  }

  /** Get the Label displayed above the Units in the X Axis
      @return the Label displayed above the Units in the X Axis
      @since NeXtMidas 3.3.1
   */
  public String getXAxisLabel () {
    if (customXAxisLabel != null) return customXAxisLabel;
    else return DataFile.getUnitsName(xUnitsID);
  }

  /** Set the label displayed above the units on the Y Axis<br><br>
      Note: This takes the place of the Units String being displayed but does not change the Units themeselves.
      @param label The text to set the label to. Use null or "" to revert to default behaviour (String form of Units)
      @since NeXtMidas 3.3.1
   */
  public void setYAxisLabel (String label) {
    setAxisLabels(customXAxisLabel, label);
  }

  /** Get the Label displayed above the Units in the Y Axis
      @return the Label displayed above the Units in the Y Axis
      @since NeXtMidas 3.3.1
   */
  public String getYAxisLabel () {
    if (customYAxisLabel != null) return customYAxisLabel;  // user has set custom label
    else return DataFile.getUnitsName(yUnitsID);
  }

  /** Set the label displayed above the units on the Y Axis<br><br>
      Note: This takes the place of the Units String being displayed but does not change the Units themeselves.
      @param label The text to set the label to. Use null or "" to revert to default behaviour (String form of Units)
      @since NeXtMidas 3.3.1
   */
   public void setZAxisLabel (String label) {
     setAxisLabels(customXAxisLabel, customYAxisLabel, label);
   }

  /** Get the Label displayed above the Units in the Y Axis
      @return the Label displayed above the Units in the Y Axis
      @since NeXtMidas 3.3.1
  */
  public String getZAxisLabel () {
    if (customZAxisLabel != null) return customZAxisLabel;  // user has set custom label
    else return DataFile.getUnitsName(zUnitsID);
  }

  /** Sets the X Axis and Y Axis custom units labels. If null or "" is passed in for either value, that label
      will be set to the default (i.e. remove any custom value).<br><br>
      Note: When setting both labels, users should use this method instead of calling {@link #setXAxisLabel(String)} and
      {@link #setYAxisLabel(String)} individually, as this method will only repaint the drawing surface a
      single time.
      @param xLabel The custom X label, or null or "" to use default (non-custom)
      @param yLabel The custom Y label, or null or "" to use default (non-custom)
      @since NeXtMidas 3.3.1
   */
  public void setAxisLabels (String xLabel, String yLabel) {
    if (xLabel!=null && xLabel.equals(customXAxisLabel)) { } // do nothing to x Label
    else if (xLabel==null || xLabel.isEmpty()) customXAxisLabel = null;  // reset X label to default, checked in drawAxis()
    else                                       customXAxisLabel = xLabel;

    if (yLabel!=null && yLabel.equals(customYAxisLabel)) { } // do nothing to Y label
    else if (yLabel==null || yLabel.isEmpty()) customYAxisLabel = null; // reset Y label to default, checked in drawAxis()
    else                                       customYAxisLabel = yLabel;

    panel.repaint();  // redraw the Axis. Faster then a refresh since SWING knows what is dirty and is thread-safe
  }

  private void setAxisLabels (String xLabel, String yLabel, String zLabel) {
    if (xLabel!=null && xLabel.equals(customXAxisLabel)) { } // do nothing to x Label
    else if (xLabel==null || xLabel.isEmpty()) customXAxisLabel = null;  // reset X label to default, checked in drawAxis()
    else                                       customXAxisLabel = xLabel;

    if (yLabel!=null && yLabel.equals(customYAxisLabel)) { } // do nothing to Y label
    else if (yLabel==null || yLabel.isEmpty()) customYAxisLabel = null; // reset Y label to default, checked in drawAxis()
    else                                       customYAxisLabel = yLabel;

    if (zLabel!=null && zLabel.equals(customZAxisLabel)) { } // do nothing to Y label
    else if (zLabel==null || zLabel.isEmpty()) customZAxisLabel = null; // reset Y label to default, checked in drawAxis()
    else                                       customZAxisLabel = zLabel;

    panel.repaint();  // redraw the Axis. Faster then a refresh since SWING knows what is dirty and is thread-safe
  }

  /** Autoscale length for exponential averaging.
      @return The autoscale length (in frames).
      @since NeXtMidas 1.9.4
   */
  public int getScaleAvg () { return scaleAvg; }

  /** Autoscale length for exponential averaging.
      @param frames The autoscale length (in frames).
      @since NeXtMidas 1.9.4
   */
  public void setScaleAvg (int frames) {
    scaleAvg = frames;
    // if using NOAVERAGE, create circular buffer needed for keeping track of smallest min
    // and largest max within the autoscale length - since 3.7.0 for bug 2901
    if ((scale & S_NOAVERAGE)!= 0) {
      if (scaleAvg > MAX_NOAVERAGE_AUTOL) {
        Shell.warning("MPlot.setScaleAverage: autoscale length capped to max for NOAVERAGE:" + MAX_NOAVERAGE_AUTOL+" user specified:"+scaleAvg);
        scaleAvg = MAX_NOAVERAGE_AUTOL;
      }
      Layer   lay;
      for (int n=1; n<layers.size(); n++) {
        lay = (Layer)layers.get(n);
        lay.setAutoscaleBuffersMaxActive(scaleAvg);
      }
    }
  }

  /** DRAGBOX zoom should maximize map and minimize margin areas,
   *  rather than centering on dragbox area
   *  @param maximizeMap true to maximize map area
   */
  public static void setMaxMapOnDragBox (boolean maximizeMap) {
    maxMapOnDragBox = maximizeMap;
  }

  /** Sets the plot drawing surface to be an image. This computes mxx, myy, mxb, and myb based on
      the current rx1, rx2, ry1, and ry2 and the image size. It will also set gc to be a graphics
      instance for the image.
      @param img The image
   */
  public void setDrawingSurface (BufferedImage img) {
    mxx  = img.getWidth()/(rx2-rx1);
    myy  = img.getHeight()/(ry2-ry1);
    mxb  = -(rx1*mxx);
    myb  = -(ry1*myy);
    double p = .00001;  // precision
    if (((rx1 < LONGITUDE_DEFAULT_MIN-p) || (rx2 > LONGITUDE_DEFAULT_MAX+p)) && (view == V_LATLONCONTINUOUS)) extendedRange =true;
    gc   = img.createGraphics();
  }

  /** Set whether to have paintComponent use the last valid
   *  double buffer image, rather than skip drawing data,
   *  when the dmimage and image[n] are null due to a pan or
   *  resize
   *
   *  @param useBackupIfNull false if should revert to pre-3.9.3
   *                         behavior of not drawing any data
   *  @since NeXtMidas 3.9.3
   */
  public void setUseDBImageBackup (boolean useBackupIfNull) {
    useDBImageBackup = useBackupIfNull;
  }

  /** Static global setting of whether to have paintComponent use the last valid
   *  double buffer image, rather than skip drawing data,
   *  when the dmimage and image[n] are null due to a pan or
   *  resize
   *
   *  @param neverUseBackupIfNull false if should revert to pre-3.9.3
   *                         behavior of not drawing any data
   *  @since NeXtMidas 3.9.3
   */
  public static void setNeverUseDBImageBackup (boolean neverUseBackupIfNull) {
    MPlot.neverUseDBImageBackup = neverUseBackupIfNull;
  }

  /** Should a date string be used for a given axis?
      @param units the units for the axis.
   */
  private boolean useDateString (int units, double t) {
    boolean uds = ((readout&R_DATESTRING)>0)
           && !isMapView() && (view!=V_POLAR) && (!is3DView())
           && ((units == Units.TIME) || (units == Units.TIMECODE)) && (Math.abs(t)>100.0);
    return uds;
  }

  /** This will force a full refresh of the plot. Unlike {@link #refresh} this
      will reinforce any existing sizing constraints for the plot and ensure
      that the axis is properly re-drawn.
      @see #resize()
      @see #refresh()
      @since NeXtMidas 1.7.5
   */
  public void hardRefresh () {
//    this.drawAxis(); // Removed in 3.9.0 This was calling drawAxis and extra time.
    this.resetForRefresh();
    this.resize();
    this.refresh(Drawable.FULL);
  }

  /** Drag and Drop. Only DROP is supported, as DRAG for plots are undefined.<br>
      <b>NOTE:</b> This code is user-provided and should be experimental and subject to change.
      @param allow true if allow drag and drop
      @param mq    the message queue
      @since NeXtMidas 2.8.0
   */
  public void setDragAndDrop (boolean allow, MessageQueue mq) {
    allowDragAndDrop = allow;
    if (allow && (stringDropTarget == null)) {
      stringDropTarget = new StringDropTarget (MW.panel, mq);
    }
  }

  /** Returns whether or not drag-and-drop is enabled
      @return whether or not drag-and-drop is enabled
   */
  public boolean isDragAndDropEnabled () { return allowDragAndDrop; }

  private class StringDropTarget implements DropTargetListener {
    private DropTarget dt   = null;
    private String    str   = null;
    private MessageQueue mq = null;

    public StringDropTarget(Component comp, MessageQueue mq) {
      this.mq = mq;
      dt = new DropTarget(comp, DnDConstants.ACTION_COPY_OR_MOVE, this);
    }

    @Override
    public void drop (DropTargetDropEvent dtde) {
      try {
        Transferable tr = dtde.getTransferable();
        if (tr.isDataFlavorSupported(DataFlavor.stringFlavor)) {
          dtde.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
          str = (String)tr.getTransferData(DataFlavor.stringFlavor);
          try {
            Table t;
            // as of 3.3.0, this now handles XML and Tables
            if (str.indexOf("<XML>")!=-1 && str.indexOf("<XML>")<10) {  // look for XML tag at the beginning
              Table xmlT = new Table();
              XmlFile.fromXML(str, xmlT); // Moved fromXML() to XmlFile from nxm.sys.prim.convert. since 3.9.2 TODO: see if we can use XmlFile.fastXmlToTable(..) instead to de-couple sys.libg from sys.prim package
              t = xmlT.getTable("XML");
            } else {
              t = new Table(str);
            }
            if (t!=null) mq.put("DROPDATA",0,t,"MAIN");
          } catch (Exception e) {
            Shell.printStackTrace("MPlot Exception in dropData", e);
          }
          dtde.getDropTargetContext().dropComplete(true);
        }
      } catch (Exception e) {
        Shell.printStackTrace(e);
      }
    }

    @Override
    public void dragEnter (DropTargetDragEvent dtde) {
      dtde.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
    }

    @Override public void dragExit         (DropTargetEvent     dte ) { }
    @Override public void dragOver         (DropTargetDragEvent dtde) { }
    @Override public void dropActionChanged(DropTargetDragEvent dtde) { }
  } // end inner class StringDropTarget

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

  /** Get the X Axis Start from the base layer.
      @return x start axis
      @deprecated since NeXtMidas 3.5.2, use {@link #getXStart()}
   */
  @Deprecated public double getXS () { return getXStart(); } // TODO: remove in 3.9+

  /** Set the axis start of MPlot and its main layers. This method calls {@link #setX1(double)}
      and then iterates over all of the layers calling their {@link Layer#setXAxis(double,double)}
      methods. When done it refreshes the plot.
      private since 3.5.2, was deprecated in 2.7.1, use {@link #setXStart(double)}
   */
  private void setXS (double value) { setXStart(value); } // TODO: remove in 3.7+

  /** Get the X Axis Start for the given layer.
      private since 3.5.2, was deprecated in 2.7.1, use {@link #getXStart(int)}
   */
  private double getXS (int layer) { return getXStart(layer); } // TODO: remove in 3.7+

  /** set the axis delta of MPlot and its main layers by delta change.
      private since 3.5.2, was deprecated in 2.7.1, use {@link #setXDelta(double)}
   */
  private void setXD (double size) { setXDelta(size); } // TODO: remove in 3.7+

  /** Get the X Axis Delta for the base layer.
      private since 3.5.2, was deprecated in 2.7.1, use {@link #getXDelta()}
   */
  private double getXD () { return getXDelta(); }

  /** Get the X Axis Delta for the given layer.
      private since 3.5.2, was deprecated in 2.7.1, use {@link #getXDelta(int)}
   */
  private double getXD (int layer) { return getXDelta(layer); } // TODO: remove in 3.7+

  /** Determines whether or not to use the mouse position
      @param useMousePosition whether or not to use the mouse position
   */
  public void setUseMousePosition (boolean useMousePosition) {
    this.useMousePosition = useMousePosition;
  }

  /** Get if we are on a continuous Lat Lon plot in the extended range
   *  area (extended range goes from 180 to 540)
   *  @return true if plot is in the extended range section of continuous lat lon
   */
  public boolean hasExtendedRange () {
    return extendedRange;
  }

  /** Set the current middle mouse button location
   *  @param currentMMBClickedLocation the current middle mouse button location
   *  @since NeXtMidas 3.9.3
   */
  public void setCurrentMMBClickedLocation (Point currentMMBClickedLocation) {
    this.currentMMBClickedLocation = currentMMBClickedLocation;
  }

  /** Checks if the current plot view is a 3D view
      @return whether or not the current plot view is a 3D view
   */
  public boolean is3DView () {
    return view == V_3DGRAPH || view == V_XYZ;
  }

  double getZoomLevelAsDecimal () {
    // If view == geodetic, need to figure out the right way to find the zoom level. The rx1 to rx2 values are -1.2 to 1.2

    double x1ToUse = rx1;
    double x2ToUse = rx2;

    if (rx1 < orx1) x1ToUse = orx1; // zoomed but rx1 is off screen
    else if (rx2 > orx2) x2ToUse = orx2;  // zoomed but rx2 is off screen
    double zoomedRange = Math.abs(x1ToUse - x2ToUse);
    double outerBounds = Math.abs(orx1 - orx2);
    return zoomedRange/outerBounds;
  }

  /** Sets if XCuts are allowed on piped Layer2D plots
   *  @param cutsOnPipedLayer2D whether or not to enable XCuts on piped Layer2D plots
   */
  @InternalUseOnly
  public void setCutsSwitchOnForPipedLayer2D (boolean cutsOnPipedLayer2D) {
    this.cutsOnPipedLayer2D = cutsOnPipedLayer2D;
  }

  /** Returns whether XCuts on piped Layer2D plots are allowed
   *  @return true if XCuts on piped Layer2D plots are allowed, otherwise false
   */
  boolean getCutsSwitchOnForPipedLayer2D () {
    return cutsOnPipedLayer2D;
  }

  private void nullArchivedFrames() {
    archivedFrames = null;
  }

  /** Set if map refreshes should clear old data to prevent
   *  issuse with objects becuming more opque during dragboxes
   *  @param mapRefreshShouldClear false to revert to old behavior
   */
  public static void setMapRefreshShouldClear (boolean mapRefreshShouldClear) {
    MPlot.mapRefreshShouldClear = mapRefreshShouldClear;
  }
}
