package nxm.sys.libg;

import java.awt.Color;
import java.awt.Font;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.logging.Level;

import javax.swing.JOptionPane;

import nxm.sys.inc.Chainable;
import nxm.sys.inc.DataTypes;
import nxm.sys.inc.Drawable;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MessageHandler;
import nxm.sys.inc.MidasReference;
import nxm.sys.inc.PlotFile;
import nxm.sys.inc.PlotFileNH;
import nxm.sys.inc.Units;
import nxm.sys.lib.Args;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.FileUtil;
import nxm.sys.lib.KeyObject;
import nxm.sys.lib.KeyVector;
import nxm.sys.lib.LogUtilities;
import nxm.sys.lib.Message;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Native;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Position;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Table;
import nxm.sys.lib.Time;
import nxm.sys.prim.plot;

/** A class to hold and plot a data layer.
    @author Jeff Schoen
 */
public class Layer extends Object implements MessageHandler,Chainable,Drawable,MidasReference {

  /** List of supported configuration options */
  public static final String configureList =
    "Rename,Delete,Enable,Scale,Line,Symbol,Surface,Feature,Color,Colors,ColorMap,Query,Effects";

  /** The color maps to show in any queries. This is identical to the one in MColor,
      except that it adds "Custom" to the end.
      @since NeXtMidas 3.3.2
   */
  private static final String colorMapList = MColor.colorMapList+",Custom";
  /** Allow a margin so 3D Graph plot */
  static double threeDGraphMarginFactor = 1.25; // seems to get all values, even the ones at the max range, to show NXM-3500
  // Effects variables below added for Bug 2196 in NeXtMidas 2.9.0 (and backported to 2.8.2)
  /** List of effects that can be applied to the Layer (usually to the ColorMap). Val= {@value #effectsList}*/
  public static final String effectsList = "InvertColors,ReverseColors";
  /** Effect: Invert the colormap. val= {@value #E_INVERT_COLORS} */
  public static final int E_INVERT_COLORS  = 0x01;
  /**  Effect: Reverse the colormap. Val= {@value #E_REVERSE_COLORS} */
  public static final int E_REVERSE_COLORS = 0x02;
  /**  Default effect. (0 = none) */
  public static final int DEF_EFFECTS = 0;  // none
  // Added to support labels
  private String label;
  /** Local usage flags
      @since NeXtMidas 3.3.2
   */
  private static final int LOCAL_XMIN = 0x000001, // Layer set useLocal value
                           LOCAL_XMAX = 0x000002, // Layer set useLocal value
                           MPLOT_XMIN = 0x000010, // MPlot set useLocal value
                           MPLOT_XMAX = 0x000020, // MPlot set useLocal value
                           MPLOT_X    = 0x0000C0, // Supports MPlot values? (2 bits set!)
                           LOCAL_YMIN = 0x000100, // Layer set useLocal value
                           LOCAL_YMAX = 0x000200, // Layer set useLocal value
                           MPLOT_YMIN = 0x001000, // MPlot set useLocal value
                           MPLOT_YMAX = 0x002000, // MPlot set useLocal value
                           MPLOT_Y    = 0x00C000, // Supports MPlot values? (2 bits set!)
                           LOCAL_ZMIN = 0x010000, // Layer set useLocal value
                           LOCAL_ZMAX = 0x020000, // Layer set useLocal value
                           MPLOT_ZMIN = 0x100000, // MPlot set useLocal value
                           MPLOT_ZMAX = 0x200000, // MPlot set useLocal value
                           MPLOT_Z    = 0xC00000; // Supports MPlot values? (2 bits set!)

  /** disable refresh rate smart thinning mode
      @since 3.5.1
   */
  @InternalUseOnly
  public static final int RR_DISABLED  =  0;
  /** enable refresh rate smart thinning mode using a reasonable default refresh rate
      @since 3.5.1
   */
  @InternalUseOnly
  public static final int RR_AUTO = -1;
  /** disable refresh rate smart thinning mode, but calculate incoming data frames per second (stats).
      @since 3.5.1
   */
  @InternalUseOnly
  public static final int RR_DISABLED_BUT_CALC_STATS = -2;
  /** default refresh rate for AUTO mode. still experimenting with good default, 24 or 30 matches most TV/film refresh rate (frame rate).
      @since 3.5.1
   */
  private static final int DEF_RR_AUTO_REFRESH_RATE = 30;
  /** default refresh rate smoothing factor. still experimenting with good default, maybe default to 1/refreshRate?. Range must be (0 - 1].
      @since 3.5.1
   */
  private static final float DEF_RR_SMOOTHING_FACTOR = 1.0f / 10;
  /** default refresh rate max elements skip multiplier. still experimenting with default value, between 2 and 100 seems to work OK for non-throttled input data rates.
      @since 3.5.1
   */
  private static final int DEF_RR_MAX_SKIP_MULTIPLIER = 2;

  /** Number of data elements being processed */
  @InternalUseOnly("since 3.3.3, will become protected in 3.7+")
  public    int           ndata;
  /** Frame size of the layer */
  @InternalUseOnly("since 3.3.3, will become protected in 3.7+ (instead use getFrame()")
  public    int           frame;
  /** Number of elements in a single transfer length */
  @InternalUseOnly("since 3.3.3, will become protected in 3.7+ (instead use getSize()")
  public    int           size;
  /** auto-scale flags (see MPlot.scaleList) */
  @InternalUseOnly("since 3.3.3, will become protected in 3.7+ (instead use getScale()")
  public    int           scaleable = 0xF;
  /** Enable flags (see MPlot.enableList) */
  @InternalUseOnly("since 3.3.3, will become protected in 3.7+ (instead use getEnable()")
  public    int           enable = MPlot.EN_DEFAULT;
  /** x-axis start value */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getXStart(), setXStart()")
  public    double        xstart;
  /** x-axis delta */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getXDelta(), setXDelta()")
  public    double        xdelta;
  /** x-axis units (see {@link Units}) */
  private   int           xunits;
  /** y-axis start value */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getYStart(), setYStart()")
  public    double        ystart;
  /** y-axis delta */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getYDelta(), setYDelta()")
  public    double        ydelta;
  /** y-axis units (see {@link Units}) */
  private   int           yunits;
  /** z-axis start value */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getZStart(), setZStart()")
  public    double        zstart;
  /** z-axis delta */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getZDelta(), setZDelta()")
  public    double        zdelta;
  /** z-axis units (see {@link Units}) */
  private   int           zunits;
  /** current time for animation (used by LayerGeo) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    double        time;
  /** Current seek position in the plot file of last data read */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    double        fseek;
  /** Minimum x-axis value (set by findRange()) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getX1(), setX1()")
  public    double        x1     = 0;
  /** Maximum x-axis value (set by findRange()) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getX2(), setX2()")
  public    double        x2     = 1;
  /** Minimum y-axis value (set by findRange()) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getY1(), setY1()")
  public    double        y1     = 0;
  /** Maximum y-axis value (set by findRange()) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getY2(), setY2()")
  public    double        y2     = 1;
  /** Minimum z-axis value (set by findRange()) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getZ1(), setZ1()")
  public    double        z1     = 0;
  /** Maximum z-axis value (set by findRange()) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getZ2(), setZ2()")
  public    double        z2     = 1;
  /** Delta   z-axis for contours */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getZDelta(), setZDelta()")
  public    double        zd     = 0.1;
  /** Minimum actual auto-scale value (set by findRange()) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    double        a1     = 0;
  /** Maximum actual auto-scale value (set by findRange()) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    double        a2     = 0;
  /** Minimum Full x-axis value (set by findFullRange()) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    double        fx1    = 0;
  /** Maximum Full x-axis value (set by findFullRange()) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    double        fx2    = 1;
  /** Minimum Full y-axis value (set by findFullRange()) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    double        fy1    = 0;
  /** Maximum Full y-axis value (set by findFullRange()) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    double        fy2    = 1;
  /** Minimum Full z-axis value (set by findFullRange()) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    double        fz1    = 0;
  /** Maximum Full z-axis value (set by findFullRange()) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    double        fz2    = 1;
  /** Is this a real-time plot layer */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    boolean       realtime;
  /** Is the layer paged (used by Layer2D, et.al.) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use isPaged()")
  public    boolean       isPaged;
  /** Indicates that the layer needs to be refreshed */
  @InternalUseOnly("since 3.3.3, will become protected in 3.7+ (instead use getNeedRefresh()")
  public    boolean       needRefresh;
  /** Has the layer been reset (set by reset()) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    boolean       isReset;
  /** Is the plot done? */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    boolean       isDone;
  /** True if data is represented on Z axis */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    boolean       zaxis;
  /** Is the layer is blank (i.e. don't need to draw) */
  protected boolean       isBlank;
  /** Indicates if this layer needs to be rescaled */
  private   boolean       needsRescale;
  /** Local usage flags (see LOCAL_*) */
  private   int           useLocal;
  /** Number of colors in the color map (usually colors.length-1)
      TODO: add getNumberColors()?
   */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    int           ncolors;
  /** The colors in the color map (colors[0] is special)
      TODO: add getColors()?
   */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    Color[]       colors;
  /** The color map ID (e.g. MColot.MULTI) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use setColorMap()")
  public    int           cmap;
  /** Type class of the plot file (7=images) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    int           typeClass;
  /** Default draw mode (overridden by some layers) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    int           defmode=Drawable.DATA;
  /** The surface type (usually used by Layer2D) */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use setSurface()")
  public    int           stype=Surface.FILL;
  /** The plot the layer is being drawn on */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+")
  public    MPlot         MP;
  /** The feature being used by this layer */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getFeature()")
  public    Feature       feature;
  /** Allows layers to have Features - trial version in 4.1.4 */
  KeyVector  layFeatures = new KeyVector(16);
  /** Allows layers to have FeatureGroups - trial version in 4.1.4 */
  FeatureGroupMap featureGroups;
  /** The file being plotted (can be null) */
  @InternalUseOnly("since 3.5.1")
  public    PlotFile      pf;
  /** pf cast to PlotFileNH (if applicable) */
  protected PlotFileNH    pfnh;
  /** The data buffer being used */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use setData()")
  public    Data          data;
  /** A comment about the layer
      @deprecated Since NeXtMidas 3.3.3, use {@link #getComment()}
      will become protected or private in 3.7+
   */
  @Deprecated
  public    String        comment;
  /** MidasReference (for results resolving and when opening files)
      since 3.5.2, changed from Object to MidasReference
   */
  protected MidasReference ref;
  /** Midas reference (since 3.5.2 - can be null) */
  private   Midas          M;
  /** The line used for drawing the layer */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getLine()")
  public    Line          line;
  /** The name of the layer */
  @InternalUseOnly("since 3.5.1, will become protected in 3.9+ (instead use getName(), setName()")
  public    String        name;
  /** LayerDrawable 'parent' name, if applicable
      @since NeXtMidas 3.5.0
   */
  private   String        groupName;
  /** is this Layer the Base Layer in a PLOT
      @since NeXtMidas 3.5.0
   */
  private   boolean       baseLayer = false;
  /** Fast float buffer (for direct IO reads) - package-private (used only in libg) */
            float[]       ffbuf;
  /** Legend entry for this layer */
  protected GLegend.Entry legendEntry;
  /** Mask of current effects on the Layer
      @deprecated Since NeXtMidas 3.3.3, use {@link #getEffects()},
      will become protected in 3.7+
   */
  @Deprecated
  public    int           effects = DEF_EFFECTS;
  /** Time-align Type 1000 plots off base layer
      @Since NeXtMidas 3.1.2 (Bug 2419)
   */
  private   boolean       timeAlign = false;
  /** n^th frame that is being autoscaled on (-1 is initial state)
      @since NeXtMidas 3.3.3 (Bug 1807)
   */
  private   int           nthAutoScaleFrame = -1;
  private   SlidingMinMax autoscaleMinMaxBuffer = null;

  /** debug options mask (zero for none)
      @since 3.5.1
   */
  protected int           debugMask;

  // === the below added since 3.5.1 for smart thinning based on desired refresh rate (BUG 2845) ===
  /** Desired screen refresh rate (RR) (e.g. frames per second (FPS) for Layer1D) to display for this layer. since 3.5.1 */
  private int              refreshRate = 0;
  /** incoming data rate exponential moving average smoothing factor. Must be in range of [0.0 - 1.0]. since 3.5.1 */
  private float            rrSmoothingFactor = DEF_RR_SMOOTHING_FACTOR;
  /** multiplier used to cap max elements to skip/drop based on num elements available in {@link #getElementsToSkipAfterRead(int)}. since 3.5.1 */
  private int              rrMaxSkipMultiplier = DEF_RR_MAX_SKIP_MULTIPLIER;
  /** incoming data screen rate (screens/sec) (e.g. incoming data FPS for Layer1D). since 3.5.1 */
  private transient float  rrInDataRate = 0;
  /** last number of screen available before last refresh rate statistics update. since 3.5.1 */
  private transient float  rrLastNumScreenAvail;
  /** number of elements read since last refresh rate statistics update. since 3.5.1 */
  private transient int    rrElemsReadSinceLastUpdate;
  /** time of last refresh rate statistics update. since 3.5.1 */
  private transient double rrLastUpdateTime;
  // === the above added since 3.5.1 for smart thinning based on desired refresh rate (BUG 2845) ===

  // Added in 3.9.3 for font size inside a circle.
  private double smallFactor  = 1;
  private double mediumFactor = .75;
  private double largeFactor  = .5;
  private Font numberFontSmall;
  private Font numberFontMedium;
  private Font numberFontLarge;

  /** Creates a new layer. */
  public Layer () {
    super();
    line        = new Line(this);
    isBlank     = true;
    legendEntry = new GLegend.Entry(this);
  }

  /** Creates a new layer with the given name.
      @since NeXtMidas 3.3.2
   */
  protected Layer (String name) {
    this();
    this.name = name;
  }

  /** Determine necessary layer type from the table of layer flags and return instance. This looks
      for the <code>LAYERTYPE=</code> or (more commonly) the <code>LT=</code> flag that specifies
      the layer type. <code>LT=</code> should be the fully-qualified name of a layer (e.g.
      <code>LT=nxm.sys.libg.LayerWMS</code>; however, if the name begins with
      <code>nxm.sys.libg.Layer</code> such can be omitted (e.g. <code>LT=WMS</code> will be
      converted to <code>LT=nxm.sys.libg.LayerWMS</code> automatically).
      @param tab table of layer flags
      @return A new Layer (usually a subclass of Layer) or null if type could not be determined.
   */
  public static Layer getLayer (Table tab) {
    if (tab==null) return null;
    // Give the user the option to use longhand or shorthand to set the layer class name.
    String className = tab.getS("LAYERTYPE");
    if (className==null) className = tab.getS("LT");
    if (className==null) return null;
    if (className.indexOf('.')<0) className="nxm.sys.libg.Layer"+className;
    try {
      Class<?> c = Class.forName(className);
      Layer lay = (Layer)c.getConstructor().newInstance(); // Updated in 3.9.0 to use default constructor
      return lay;
    }
    catch (Exception e) {
      throw new MidasException("Err loading Layer class: "+className,e);
    }
  }

  /** Determine necessary layer type from the table of layer flags, the plot file
      being used and the plot type. This function should only be called by PLOT.
      @param tab table of layer flags
      @param pf  The plot file (cannot be null).
      @param plotType plot type (e.g. line, raster, contour, mesh, point, dot)
      @return A new Layer or null if type could not be determined.
      @since NeXtMidas 3.3.2
      @since NeXtMidas 3.9.2 - changed to use the registry first. If methods are not in registry, then use the old way.
   */
  @InternalUseOnly
  @SuppressWarnings("deprecation")  // will be needed once getLayer method is officially deprecated
  public static Layer getLayer (Table tab, PlotFile pf, int plotType) {
    Layer lay = getLayer(tab);

    if (lay != null) return lay;

    if (pf instanceof DataFile) {
      // Special case for DataFile since it supports so many different data types
      // it needs extra parameters passed to it.
      return LayerUtil.getDataFileLayer((DataFile)pf,plotType);
    }
    LayerUtil.refreshRegistry(); // This is called because the registry may not be built yet. Lets do it before we try to get a function.
    if (LayerUtil.getFunction(pf.getClass()) == null) {
      LogUtilities.log(Level.WARNING, "Using the deprecated PlotFile.getLayer() method. Please add getLayer() to the registry going forward. For info nxm.sys.lib.LayerUtil.java");
      Method getLayerMethod;
      Object layer = null;
      try {
        getLayerMethod = PlotFile.class.getMethod("getLayer");
        layer = getLayerMethod.invoke(pf);
      } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
        throw new MidasException("Error accessing getLayer method for PlotFile: "+pf,e);
      }
      return (Layer) layer;
    }
    return LayerUtil.getFunction(pf.getClass()).apply(pf);
  }

  /** Initializes the layer. This function is used rather than initialization in
      the constructor since the layers are loaded automatically using reflection.
      <i>(The use of the empty constructor followed by a call to <code>init(..)</code>
      mirrors what is done for the subclasses of {@link BaseFile}.)</i>. This
      function should only be called by plot. Subclasses wishing to augment
      the initialization behavior should override the protected <code>init(..)</code>
      function which is called by {@link Layer#init}. <br>
      <br>
      Note that this function will adjust settings in PLOT using the reference
      passed in.
      @param plot       The instance of PLOT (cannot be null).
      @param layerName  The name for the layer.
      @param layerIndex The layer index.
      @param pf         The plot file (cannot be null).
      @param tab        The table passed to <code>setFile(..)</code>.
      @param oldLayer   The old layer this layer is replacing. (This is rarely
                        null, but may be a place-holder instance of {@link Layer}
                        rather than the applicable subclass.)
      @since NeXtMidas 3.3.2
   */
  @InternalUseOnly
  public final void init (plot plot, String layerName, int layerIndex,
                          PlotFile pf, Table tab, Layer oldLayer) {
    boolean rt = plot.isRealTimeMode() || pf.isStream();

    setMPlot(plot.getMPlot());
    setName(layerName);
    setRealTime(rt);
    setFile(pf, tab, plot.getMA());

    if (rt && MP.getOption(MPlot.O_BSTORE) && isEnabled(-1)) { // since 2.9.0 Warning issued as per Bug 1516
      M.warning("Backing Store not recommended for real-time plots due to Graphics performance. layer = " + this); // NXM-3678 was plot.getMidas() rather than M - 4.0.0 - check with test case
    }
    if (plot.isReverseColorsMode()) { // since 2.9.0 (backported to 2.8.2) for BUG 2196
      reverseColorMap();
    }

    init(plot, layerName, layerIndex, oldLayer);
  }

  /** Initializes the layer. This function is always called by {@link Layer#init}
      as part of the initialization. The exact order in which {@link Layer#init}
      calls this function is intentionally unspecified, but will always come
      AFTER calls to:
      <pre>
        setMPlot(..)
        setFile(..)
      </pre>
      Note that this function may adjust settings in PLOT using the reference
      passed in.
      @param p          The instance of PLOT.
      @param layerName  The name for the layer.
      @param layerIndex The layer index.
      @param oldLayer   The old layer this layer is replacing. (This is rarely
                        null, but may be a place-holder instance of {@link Layer}
                        rather than a "plottable" subclass.) Very few layers do
                        anything with this value though occasionally one will
                        use this to try and maintain some consistency following
                        a restart (e.g. see {@link Layer2D} for an example).
      @since NeXtMidas 3.3.2
   */
  protected void init (plot p, String layerName, int layerIndex, Layer oldLayer) {
    // nothing here, yet!
  }

  /** set the file input - if necessary - should be over-ridden.
      @param pf  The plot file (cannot be null).
      @param tab table of options
      @param MA  command line arguments
      @return <code>true</code> when the file has been opened, <code>false</code> if the file can not
             be opened (or indicates the file is not supported by this layer).
   */
  public boolean setFile (PlotFile pf, Table tab, Args MA) {
    setFile(pf);
    setSize((int)pf.getSize());
    setData(pf.getDataBuffer(size));
    return true;
  }

  /** Get the file being used
   *  @return the file being used (plotted by this layer). */
  public PlotFile getFile () {
    return pf;
  }

  /** Set the name of the Group (i.e. LayerDrawable name) that this Layer is a member of.
      @param name layer group name that this layer is a member of (null for none)
      @since NeXtmidas 3.5.0
   */
  public void setGroup (String name) {
    groupName = name;
  }

  /** Get the name of the Group (i.e. LayerDrawable name) that this Layer is a member of.
      @return layer group name that this layer is a member of (null for none)
      @since NeXtmidas 3.5.0
   */
  public String getGroup () {
    return groupName;
  }

  /** Set this Layer as the Plot's BaseLayer
      @since NeXtMidas 3.5.0
   */
  public void makeBaseLayer () {
    setBaseLayer(true);  // added for Bug 2792
  }

  /** Set this Layer as the Plot's BaseLayer
      @param base true if this should be treated as the base layer
      @since NeXtMidas 3.5.0
   */
  public void setBaseLayer (boolean base) {
    if (base!=baseLayer) {
      baseLayer = base;
      needRefresh = true;
      if (baseLayer && MP!=null) MP.setBaseLayer(this);
    }
  }

  /** Is this Layer the Base Layer for the Plot
      @return true if this should be treated as the base layer
      @since NeXtMidas 3.5.0
   */
  public boolean isBaseLayer () {
    return baseLayer;
  }

  /** Gets the default complex mode used by this layer. The default implementation in {@link Layer}
      returns {@link MPlot#M_RNI}, but other layers may override as appropriate.
      @return default complex mode for this layer
      @since NeXtMidas 3.3.2
   */
  public int getDefaultComplexMode () {
    return MPlot.M_RNI;
  }

  /** Gets the default view used by this layer. The default implementation in {@link Layer}
      returns {@link MPlot#V_YX}, but other layers may override as appropriate.
      @return default view for this layer
      @since NeXtMidas 3.3.2
   */
  public int getDefaultView () {
    return MPlot.V_YX;
  }

  /** Closes the layer. This should only be called from {@link MPlot#removeLayer}. Subclasses should
      override this method as needed, but should always include a call to <code>super.close()</code>.
      @since NeXtMidas 2.5.5
   */
  public void close () {
    if ((pf != null) && pf.isOpen()) pf.close();
  }

  /** Get the next available data from the input source.
      This should only be called by the plot primitive.
      @param mode always 0
      @return the drawing mode to use, see {@link Drawable}
   */
  public int getData (int mode) {
    if (pf==null || !pf.isOpen() || data==null || isDone) return Drawable.FULL;
    int stat = Drawable.FULL;
    int ndo = (int)pf.avail();    // in elements

    final int numElementsToSkipAfterRead = getElementsToSkipAfterRead(ndo); // since 3.5.1 for refresh rate smart thinning (BUG 2845)
    if (ndo>0) {         // check data range
      ndo = Math.min(ndo, data.size); // cap number of elements to read at data buffer size
      fseek = pf.seek(); // update for getTimeAt reference
      if (isPaged) {
        double s=pf.getStart(), d=pf.getDelta();
        if (typeClass==2) {
          if (d>0) ndo = Math.min(ndo,(int)((MP.ry2-s)/d-fseek+2));
          if (d<0) ndo = Math.min(ndo,(int)((MP.ry1-s)/d-fseek+2));
          setYStart(s+fseek*d);
        } else {
          if (d>0) ndo = Math.min(ndo,(int)((MP.rx2-s)/d-fseek+2));
          if (d<0) ndo = Math.min(ndo,(int)((MP.rx1-s)/d-fseek+2));
          setXStart(s+fseek*d);
        }
      }
    }
    if (ndo>0) {         // read and process new data
      if (ffbuf!=null) {
        data.buf = Native.uncastF(ffbuf);
        ndo = pf.read(data,ndo);
        ffbuf = Native.castF(data.buf);
      } else {
        ndo = pf.read(data,ndo);
      }
      if (ndo<=0) return Drawable.FULL;

      rrElemsReadSinceLastUpdate += ndo;    // since 3.5.1 for smart thinning based on desired refresh rate (BUG 2845)
      if (numElementsToSkipAfterRead > 0) { // since 3.5.1 for smart thinning based on desired refresh rate (BUG 2845)
        // FYI: skipping after read, allows us to move read pointer further ahead
        //      so that the pipe system can become more efficient when thinning out data
        if (pf instanceof DataFile) {
          ((DataFile)pf).skip(numElementsToSkipAfterRead); // skip(.) is more efficient than calling seek(.)
        } else {
          pf.seek(pf.seek() + numElementsToSkipAfterRead);
        }
      }

      process(ndo);
      if (isReset) stat = Drawable.DATA;
      else if (isPaged) stat = Drawable.DATAX;
      else stat = defmode;
    }
    else if (isPaged) {  // stop page reads
      pf.seek(pf.getSize());
      if (typeClass==2) setYStart(pf.getStart());
      else setXStart(pf.getStart());
      isDone=true;
      ndata = 0;
    }
    else if (!realtime) stat=Drawable.FULL;
    else if (pf.isStream() && !pf.isStreaming()) stat = Drawable.DONE;
    return stat;
  }

  /** Indicates if the layer is paged.
      @param dir The "directionality" of the plot file (use 2 for Type 2000 files
                 that use Y, 1 for all others that use X).
      @return true if this layer is page, false otherwise
   */
  public boolean isPaged (int dir) {
    if (dir==1) return isPaged && typeClass != 2;
    if (dir==2) return isPaged && typeClass == 2;
    return isPaged;
  }

  /** Reread the data for the layer, if possible, this will attempt to reopen the file.
      @return true if a reread was done, false otherwise (e.g. file is not open)
   */
  @SuppressWarnings("deprecation") // to for MPlot.options until it becomes package-private
  public boolean reread () {
    if (pf==null || !pf.isOpen() || realtime) return false;
    if (pf instanceof DataFile) {
      DataFile df = (DataFile)pf;
      pf = (PlotFile)df.reOpen();
    }
    if ((MP.options&MPlot.O_BSTORE)!=0 && (enable&MPlot.EN_BSTORE)!=0) MP.reset(-1);
    pf.seek(0.0);
    isDone=false;
    return true;
  }

  /** Reset the layer. This implies a full re-drawing of the entire layer (but
      not necessarily a re-read of the data, see {@link #reread()}) that
      is within the bounds of the plot. This method should only be called by
      {@link MPlot} when it needs to "reset" the layer (e.g. for a "FULL" draw
      such as in response to a ZOOM or when redrawing all of the layers in
      response to external events). (This method may also be used internally
      by the individual layers.) As such, expect this method to be called with
      relative frequency since the plot is usually operating in a dynamic
      environment where layers are changing, users are panning/zooming and
      the window manager is triggering refreshes (e.g. window resized, window
      moved to front, etc).<br>
      <br>
      This resets the internal Layer parameters to match the current parameters in
      {@link MPlot}. In the case of a plot that wraps around or shifts (e.g. {@link Layer2D}),
      this will take into account the RWC bounds the plot is currently showing. For other
      layers, this will typically reset the offset to 0.0 (since they typically have a
      plot window that "starts" at the start of the file). <br>
      <br>
      For the case of a paged file, this resets the the parameters so everything is
      reread and redrawn starting at the "start" of the plot. In the case of a pipe,
      the layer has some flexibility with respect to what is "redrawn," if it has
      some cached data available, it may choose to include it; otherwise it just
      starts over using the new data from the pipe.
      @return true if the layer has been reset
   */
  @Override
  public boolean reset () {
    if (isPaged && pf!=null && pf.isOpen()) {
      double s=pf.getStart(), d=pf.getDelta(), offset=0.0;
      // Recompute offset into file (computed based on the RWC bounds of the plot)
      if (typeClass==2) {
        if (d>0) offset = Math.floor((MP.ry1-s)/d);
        if (d<0) offset = Math.floor((MP.ry2-s)/d);
      } else {
        if (d>0) offset = Math.floor((MP.rx1-s)/d);
        if (d<0) offset = Math.floor((MP.rx2-s)/d);
      }
      offset = Math.max(0.0,Math.min(pf.getSize(),offset));
      if (fseek<0 || isDone || (offset!=pf.seek())) {
        pf.seek(offset);
        isReset=true;
        isDone=false;
      }
    }
    return isReset;
  }

  /** Clears all data in the layer. The default implementation in {@link Layer} does nothing since
      it holds no persistent information. Layers that hold persistent information will need to
      override this method and to clear that information.
      @return true If anything was cleared, false if there is nothing to clear.
      @since NeXtMidas 2.1.2
   */
  public boolean clear () {
    return false;
  }

  /** Set the Layer Type. This is really handled by {@link #getLayer(Table)}. This method is only
      here to prevent errors when the flags are set via {@link KeyObject}. Any value passed to this
      method will be ignored.
      @param value layer type (ignored)
   */
  public final void setLayerType (String value) {
    // this actually is handled in getLayer()
  }

  /** Gets the current layer type. This always returns the fully-qualified name of the layer, even
      if the layer type was initially set via an abbreviation in {@link #getLayer(Table)}.
      @return fully-qualified class name of this layer
      @since NeXtMidas 2.5.0
   */
  public final String getLayerType () {
    return getClass().getName();
  }

  /** Set the file being plotted by this layer.
      @param pf The plot file (cannot be null).
   */
  public void setFile (PlotFile pf) {
    this.pf   = pf;
    this.pfnh = FileUtil.asPlotFileNH(pf);  // <-- returns null if not an instance of PlotFileNH

    typeClass = pf.getType()/1000;
    fseek = -1;

    setXUnits(pf.getXUnits());
    setYUnits( (typeClass==2)? pf.getYUnits() : Units.NONE );
    setZUnits(Units.NONE);

    nthAutoScaleFrame = -1; // since 3.3.3 for Bug 1807 (-1 is initial state)
  }

  /** set MPlot for this Layer
   *  @param value MPlot
   *  @since NeXtMidas 3.5.4
   */
  @SuppressWarnings("deprecation") // deprecated Drawable.getMPlot to break in dependency on libg - 3.9.2 for JDK 11 modularity
  public void setMPlot (MPlot value) {
    MP = value;
    if (line!=null) line.setPlot(MP);
    if (feature!=null) feature.MP = MP;
  }

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

  /** Sets the data buffer being used by the layer.
      @param value data buffer for this layer to use
   */
  public void setData (Data value) { data = value; }

  /** Resync with MPlots mode/view settings */
  public void update () {
    if (ndata==0 && isPaged && data!=null) ndata = data.size;
    if (ndata>0) process();
  }

  @Override
  public void setEnable (int value) {
    if (enable != value) {
      enable = value;
      needRefresh=true; // special case, force update since we may be changing the ENABLE flag
      refresh();
    }
  }

  /** Toggles the layer's visibility. This is the same as toggling the GLOBAL enable flag. */
  public void toggle () {
    setEnable(enable^MPlot.EN_GLOBAL);
  }

  //////////////////////////////////////////////////////////////////////////////
  // The following get/set methods match those in Feature.java
  @Override public void    setEnable (String value) { setEnable(Parser.mask(MPlot.enableList,value,enable)); }
  @Override public int     getEnable ()             { return enable; }
  @Override public String  getEnableString ()       { return Parser.mask2s(MPlot.enableList, enable); }
  @Override public String  getName ()               { return name; }
  /** Get the line used for drawing the layer
   *  @return the line used for drawing the layer
   */
  @SuppressWarnings("deprecation") // deprecated Drawable.getLine to break in dependency on libg - 3.9.2 for JDK 11 modularity
            public Line    getLine ()               { return line; }
  /** Get the plot the layer is being drawn on
   *  @return the plot the layer is being drawn on
   */
  @SuppressWarnings("deprecation") // deprecated Drawable.getMPlot to break in dependency on libg - 3.9.2 for JDK 11 modularity
            public MPlot   getMPlot ()              { return MP; }         // since NeXtMidas 3.5.4
  @Override public Object  getNextLink ()           { return getLine(); }  // since NeXtMidas 3.5.4
  @Override public Object  getPrevLink ()           { return getMPlot(); } // since NeXtMidas 3.5.4
  //////////////////////////////////////////////////////////////////////////////

  /** Sets the auto-scale options flag. Outside of the Layers and {@link MPlot} the
      {@link #setScale(String)} method should be used in place of this one.
      @param value The auto-scale options.
   */
  public void setScale (int value) {
    scaleable = value;
  }

  /** Sets the auto-scale options flag.
      @param value The auto-scale options. The available flags are given in {@link MPlot#scaleList}.
      @since NeXtMidas 2.1.0
   */
  public final void setScale (String value) {
    setScale(Parser.mask(MPlot.scaleList,value,scaleable));
  }

  /** Get the auto-scale options
      @return {@link #scaleable}
      @since NeXtMidas 2.7.1
   */
  public int getScale () { return scaleable; }

  /** Sets the X-Axis Start value.
      @param value The start value.
      @since NeXtMidas 2.1.0
   */
  public void setXStart (double value) { xstart = value; }

  /** Gets the X-Axis start value
      @return {@link #xstart}
      @since NeXtMidas 2.7.1
   */
  public double getXStart () { return xstart; }

  /** Sets the X-Axis Delta value.
      @param value The delta value.
      @since NeXtMidas 2.1.0
   */
  public void setXDelta (double value) { xdelta = value; }

  /** Gets the X-Axis delta value
      @return {@link #xdelta}
      @since NeXtMidas 2.7.1
  */
  public double getXDelta () { return xdelta; }

  /** Sets the X-Axis Start and Delta values.
      @param start The start value.
      @param delta The delta value.
   */
  public void setXAxis (double start, double delta) {
    setAxis(start, delta, 'X');
  }

  /** Sets the Y-Axis Start value.
      @param value The start value.
      @since NeXtMidas 2.1.0
   */
  public void setYStart (double value) { ystart = value; }

  /** Get the Y Axis Start value
      @return {@link #ystart}
      @since NeXtMidas 2.7.1
   */
  public double getYStart () { return ystart;  }

  /** Sets the X-Axis Delta value.
      @param value The delta value.
      @since NeXtMidas 2.1.0
   */
  public void setYDelta (double value) { ydelta = value;  }

  /** Get the Y Axis delta value
      @return {@link #ydelta}
      @since NeXtMidas 2.7.1
   */
  public double getYDelta () { return ydelta; }

  /** Sets the Y-Axis Start and Delta values.
      @param start The start value.
      @param delta The delta value.
   */
  public void setYAxis (double start, double delta) {
    setAxis(start, delta, 'Y');
 }

  /** Sets the Y-Axis Start value.
      @param value The start value.
      @since NeXtMidas 2.1.0
   */
  public void setZStart (double value) { zstart = value;  }

  /** Get the Z Axis Start value
      @return {@link #zstart}
      @since NeXtMidas 2.7.1
   */
  public double getZStart () { return zstart; }

  /** Sets the Z-Axis Delta value.
      @param value The delta value.
      @since NeXtMidas 2.1.0
   */
  public void setZDelta (double value) { zdelta = value;  }

  /** Get the Z Axis delta value
      @return {@link #zdelta}
      @since NeXtMidas 2.7.1
   */
  public double getZDelta () { return zdelta; }

  /** Sets the Z-Axis Start and Delta values.
      @param start The start value.
      @param delta The delta value.
   */
  public void setZAxis (double start, double delta) {
    setAxis(start, delta, 'Z');
  }

  /** Sets the Axis values for the given Axis type. <br>
      The /INDEX switch and axis values precision is checked to ensure no loss of precision. <br>
      If /INDEX or the values fail the precision check, the values are set to /INDEX mode
      (START=0, DELTA=1). <br>
      @param start The Axis start value
      @param delta The Axis delta (change in values in the file)
      @param c The type of Axis being set (only valid values are 'X', 'Y', or 'Z')
   */
  private void setAxis (double start, double delta, char c) {
    boolean precision = true;  // whether the values maintain precision

    if (MP.isAxisIndex()) precision = false;   // INDEX switch has precedence
    else if ((delta==0) || (start == Double.MAX_VALUE)) { }    // Value is just a holding value
    else if ((start+delta) != start) precision = true;  // precision is fine
    else                             precision = false;

    // If no precision, set to INDEX values and notify user
    if (!precision)  {

      // create a popup to notify the user we are changing the axis values
      if (!MP.isAxisIndex()) {
        String text = "WARNING: "+c+" Axis START / DELTA values combination is    \n"+
                      "beyond precision: \n     START = "+start+
                      " \n     DELTA = " +delta+" \n"+
                      "Setting to INDEX mode: \n     START = 0 \n     DELTA = 1";
        MJPopup popup = MJPopup.createOk(MP.getPanel(), "Beyond Precision",
            JOptionPane.WARNING_MESSAGE,text );
        popup.display(0);

        warning(text);  // Also notify user via terminal
      }

      // Set the start and delta to /INDEX values
      start = 0;
      delta = 1;

      MP.setAxisIndex(true);    // Set /INDEX to true since we set the values that way
    }

    // Set the axis values
     switch (c) {
      case 'X' : xstart = start; xdelta = delta; break;
      case 'Y' : ystart = start; ydelta = delta; break;
      case 'Z' : zstart = start; zdelta = delta; break;
      default  : warning("Invalid axis type: " +c);
    }
  }

  /** Set frame size form this layer
   *  @param value desired frame size for this layer */
  public void setFrame (int value) { frame = value; }
  /** Get frame size for this layer
   *  @return this layer's frame size */
  public int  getFrame () { return frame; }

  /** Set number of elements in a single transfer length
   *  @param value desired number of elements in a single transfer length */
  public void setSize (int value) { size = value; }
  /** Get number of elements in a single transfer length
   *  @return Number of elements in a single transfer length */
  public int  getSize () { return size; }

  /** Set the current time for animation
   *  @param value set the current time for animation (e.g. used by LayerGeo) */
  public void   setTime (double value) { time = value; }
  /** Get the current time for animation
   *  @return the current time for animation (e.g. used by LayerGeo) */
  public double getTime () { return time; }

  /**
   * Initialize autoscaleMinMaxBuffer with new special purpose circular buffer
   * for keeping track of smallest min and largest max.  Note: this buffer is only
   * allocated when using NOAVERAGE (sliding window rather than exponential averaging)
   * @param autoscaleLength number of values (latest to oldest) to use in calculations
   *                        of the smallest min and largest max
   * @since NeXtMidas 3.7.0
   */
  synchronized void setAutoscaleBuffersMaxActive (int autoscaleLength) {
    if (autoscaleMinMaxBuffer == null) {
      autoscaleMinMaxBuffer = new SlidingMinMax(MPlot.MAX_NOAVERAGE_AUTOL,autoscaleLength);
    }
    else { // re-use current circular buffer (only need to adjust max active (i.e. new length)
      autoscaleMinMaxBuffer.setMaxActive(autoscaleLength);
    }
  }

  /** @param ref reference to use
   *  @deprecated since 3.5.2, use {@link #setRef(MidasReference)} */
  @Deprecated public void setRef (Object ref) { setRef(Convert.ref2Midas(ref)); } // TODO remove in 3.9+
  /** Get the Midas reference to use
   *  @param ref The Midas reference to use. */
  public void   setRef (MidasReference ref) {
    this.ref = ref;
    if (ref != null) {
      this.M = ref.getMidas();
    }
  }
  /** Get the Midas reference in use
   *  @return the Midas reference in use (null if none is set). */
  public MidasReference getRef () { return ref; } // since 3.5.2, returns MidasReference vs Object

  /** @since 3.5.2 to implement MidasReference interface */
  @Override
  public Midas getMidas() { return M; }

  /** Sets the real-time mode flag. Note that setting the plot to real-time (rt=true) causes the
      use of BackingStore (enable=+BSTORE) to be turned off.
      @param rt Indicates if the plot should be done in real-time (true) or if it is static (false).
   */
  public void setRealTime (boolean rt) {
    realtime = rt;
    if (rt) enable &= ~MPlot.EN_BSTORE;
  }

  /** Gets the readout for the plot at a given cursor position. The output format
      usually used by layers like {@link Layer2D}: "[layerName] (z=value +- delta-z)".
      @param mp The position on the plot.
      @return The readout string or null if the layer does not support readout.
              <i>(The default implementation in {@link Layer} typically returns null.</i>
   */
  public String getReadOut (Position mp) { return null; }

  /** Gets the positional information for the plot at a given cursor position.
      This is called by {@link MPlot#getPosition(int,int,Position)} to allow the
      base layer to override with additional information. The position passed in
      will have <code>p.x</code> and <code>p.y</code> initialized with the applicable
      RWC. Overriding methods should always call <code>super.getPosition(..)</code>
      as this allows Layer to provide additional information such as
      <code>p.t = getTime(px,py)</code>.
      @param px (IN)  The x-position (screen pixel) in the plot. NOT the RWC for the position.
      @param py (IN)  The y-position (screen pixel) in the plot. NOT the RWC for the position.
      @param p  (I/O) The position indicator to be set to indicate the relative
                data position (will never be null).
      @return Always returns <code>p</code>.
   */
  protected Position getPosition (int px, int py, Position p) {
    p.t = getTime(px,py);
    return p;
  }

  /** This permits a mechanism for MPlot to call getPosition(..) without making
      that function public.
   */
  @InternalUseOnly // Only used by mplot
  Position getPositionMPlot (int px, int py, Position p) {
    return getPosition(px, py, p);
  }

  /** Indicates if the layer needs to be refreshed. Subclasses should override this method if they
      have multiple {@link Line}s or if they have other fields to check. Subclasses should override
      this method as follows:
      <pre>
        public boolean getNeedRefresh () {
          return super.getNeedRefresh() || [other things to check];
        }
      </pre>
      If this class uses multiple line objects (other than just <code>Layer.line</code>) it should
      include a {@link Line#getNeedRefresh()} check for each line used, if any line has changed,
      this method should return true.
      @return {@inheritDoc}
      @since NeXtMidas 2.5.4
   */
  @Override
  public boolean getNeedRefresh () {
    return needRefresh || line.getNeedRefresh();
  }

  /** Called by {@link nxm.sys.libg.MPlot} to clear the refresh flag(s) without actually drawing
      the drawable. This should clear all of the flags checked in {@link #getNeedRefresh()}. This
      method is also called at the top of {@link nxm.sys.libg.Layer#draw(int)}. Any subclass that
      overrides {@link #getNeedRefresh()} should also override this method.
      @since NeXtMidas 2.5.4
   */
  @Override
  public void clearNeedRefresh () {
    needRefresh = false;
    line.clearNeedRefresh();
  }

  @Override
  public void refresh () {
    if ((enable&MPlot.EN_GLOBAL)!=0) needRefresh=true;
    if (isEnabled(-1) && MP!=null) MP.reset(-1);
  }

  /** Refresh the Layer. This method does NOT set {@link #needRefresh} to true.
      This method provides a mechanism for the Layer to call {@link MPlot#refresh(int)}
      with a user-defined drawmode (usually {@link Drawable#DATA}) to prevent the
      redrawing of the entire plot (including any backing store)
      @param drawmode The type of refresh. See {@link Drawable#DONE}, {@link Drawable#FULL},
      {@link Drawable#DATA}, {@link Drawable#DATAX}, {@link Drawable#FAST})
      @since NeXtMidas 2.9.3
   */
  public void refresh (int drawmode) {
    MP.refresh(drawmode);
  }

  /** process the plot data for this frame */
  public void process () {
    isBlank = false;
  }

  /** process the plot data for specified number of frame elements.
      @param elem number of data elements to process
   */
  public void process (int elem) {
    int origSize = data.size;
    if (elem<data.size) {
      data.setSize(elem);
    }
    process();
    if (elem!=origSize) data.setSize(origSize);
  }

  /** Find layer bounds. This function should be overridden by any subclasses as
      the basic implementation in {@link Layer} simply sets the range from 0 to 1.
      Note: The overriding method is responsible for setting all these fields:
      x1,x2 - Minimum and Maximum x-axis value
      y1,y2 - Minimum and Maximum y-axis value
      z1,z2 - Minimum and Maximum z-axis value
      a1,a2 - Minimum and Maximum actual auto-scale value (default 0,0)
   */
  public void findRange () {
    x1 = 0; x2 = 1;
    y1 = 0; y2 = 1;
    z1 = 0; z2 = 1;
    a1 = 0; a2 = 1;
  }

  /** Find full range for layer bounds. */
  public void findFullRange () {
    fx1 = x1; fx2 = x2;
    fy1 = y1; fy2 = y2;
    fz1 = z1; fz2 = z2;
  }

  /** Find full range for layer bounds.
      @param pf The plot file
   */
  public void findFullRange (PlotFile pf) {
    fx1 = x1; fx2 = x2;
    fy1 = y1; fy2 = y2;
    fz1 = z1; fz2 = z2;
    if (pf!=null && !realtime) {
      // Update the full range values based on x-start, file size, and x-delta
      // Note: This is an invalid calculation for LayerKML - see issue NXM-2938
      fx1 = pf.getXStart();
      fx2 = fx1 + (pf.getSize()-1)*pf.getXDelta();
      if (fx1>fx2) { double t=fx1; fx1=fx2; fx2=t; }
    }
  }

  /** Whether or not to align Type 1000 time-based plots off the base layer
      @return true if aligning type 1000 time-based plots off the base layer
      @since NeXtMidas 3.1.2
   */
  public boolean isTimeAlign () {
    return timeAlign;   // added for Bug 2419
  }

  /** Set whether or not to align Type 1000 time-based plots off the base layer
      @param val true to align time-based plots off the base layer (Type 1000 only)
      @since NeXtMidas 3.1.2
   */
  public void setTimeAlign (boolean val) {
    timeAlign = val;
  }

  /** Draws the layer. Subclasses should override this method to handle layer-specific drawing, note
      that in doing so they should always include a call to <code>super.draw(flag)</code>, for example:
      <pre>
        public void draw (int flag) {
          super.draw(flag);
          ...
        }
      </pre>
      When drawing, subclasses should make a copy of the line being used when drawing using
      {@link Line#copy()} this prevents threading issues if an external thread changes line
      parameters (e.g. color) during the draw.
   */
  @Override
  public void draw (int flag) {
    clearNeedRefresh();
    isReset = false;
  }

  /** Draws a segment of the data to the layer's {@link MPlot} reference. This
      is used by {@link Feature} when the {@link Feature#DATA} flag is set. The
      default implementation in {@link Layer} simply returns -1.
      @param lin    The line to draw with.
      @param start  The start value (along X axis) in RWC.
      @param end    The end   value (along X axis) in RWC.
      @return Number of points drawn or -1 if this layer does not support
              drawing of data.
      @since NeXtMidas 3.3.2
   */
  @InternalUseOnly
  public int drawDataSegment (Line lin, double start, double end) {
    return -1;
  }

  /** Launches the configuration menus */
  public void configure () {
    String menuItems = (layFeatures.size() > 0) ? configureList + ",Features" : configureList;
    new GMenu(MP,"Layer.cfg", menuItems, 0, 0, this);
  }

  @Override
  public int processMessage (Message msg) {
    // This function is extremely similar to the one in Feature (much is copy-paste)
    String text = Convert.o2s(msg.data);
    switch (msg.name) {
      case "LAYER.CFG":
        switch (text) {
          case "RENAME"   : new GPrompt(MP,"Name",    name,                         0,           this); break;
          case "QUERY:"   : new GQuery( MP,"Layer "+name, this,                     0,           this); break;
          case "SYMBOL"   : new GMenu(  MP,"Symbol",  Symbol.symbolList, 0,         0,           line); break;
          case "ENABLE"   : new GMenu(  MP,"Enable",  MPlot.enableList,  enable,    GMenu.TOGGLE,this); break;
          case "SCALE"    : new GMenu(  MP,"Scale",   MPlot.scaleList,   scaleable, GMenu.TOGGLE,this); break;
          case "SURFACE"  : new GMenu(  MP,"Surface", Surface.typeList,  stype,     GMenu.TOGGLE,this); break;
          case "COLORMAP" : new GMenu(  MP,"ColorMap",colorMapList,      cmap,      0,           this); break;
          case "EFFECTS"  : new GMenu(  MP,"Effects", effectsList,       effects,   GMenu.TOGGLE,this); break;
          case "DELETE"   : MP.removeLayer(name); break;
          case "LINE"     : line.configure(); break;
          case "FEATURE"  : getFeature().configure(); break; // updated in 3.1.1 to prevent Data features on non Layer1D plots (Bug 2549)
          case "FEATURES" :
            if (featureGroups.size() > 0) {
              String menuItems = String.join(",",featureGroups.keySet());
              new GMenu(MP,"FeatureGrp",menuItems,0,0,this);
            }
            else {
              new GMenu(MP,"LayFeature",layFeatures.getKeyList(),0,0,this);
            }
            break;
          case "COLOR" :
            String colorString = MColor.toString(getColor()); // Map color to a name if possible
            int len = (colorString != null)? Math.max(colorString.length(), 20) : 20;
            new GPrompt(MP,"Color",colorString, len, 0, this);
            break;
          case "COLORS" :
            int lgth = Math.max(Math.min(getColorMap().length(),500), 20);
            new GPrompt(MP,"Colors",getColorMap(), lgth, 0, this);
            break;
        }
        break;
      case "COLORMAP" :
        if (msg.data.toString().equalsIgnoreCase("CUSTOM")) {
          new GPrompt(MP,"Colors",getColorMap(), 0, this);
        } else {
          setColorMap(msg.info);
        }
        break;
      case "EFFECTS" :
        if (msg.data.toString().equals("REVERSECOLORS")) {
          reverseColorMap();
        } else if (msg.data.toString().equals("INVERTCOLORS")) {
          invertColorMap();
        }
        break;
      case "LAYFEATURE" :
        Feature f = (Feature)layFeatures.get(text);
        f.configure();
        break;
      case "FEATUREGRP":
        FeatureGroup group = featureGroups.get((String)msg.data);
        String menuitems = String.join(",",group.getFeatures().keySet());
        new GMenu(MP,"LayFeature",menuitems,0,0,this);
        break;
      case "COLOR"      : setColor(text); break;
      case "COLORS"     : setColors(text); break;
      case "ENABLE"     : setEnable(msg.info); break;
      case "SCALE"      : setScale(msg.info); break;
      case "SURFACE"    : setSurface(msg.info); break;
      case "NAME"       : setName(text); break;
      case "RENAME"     : setName(text); break;
      case "NEWNAME"    : setName(text); break;
      default           : return 0;
    }
    return 1;
  }

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

  /** Is the layer blank?
   *  @return true if layer is blank */
  public boolean isBlank () {
    return isBlank;
  }

  /** Indicates if the layer is visible.
      @return <code>true</code> if the layer is visible, <code>false</code> if not.
   */
  public boolean isVisible () {
    return ((enable&MPlot.EN_GLOBAL) != 0);
  }

  /** Sets the minimum X value. This is identical to {@link #setXMin}.
   *  @param r1 the minimum X value */
  public final void setX1 (double r1) { setXMin(r1); }
  /** Sets the maximum X value. This is identical to {@link #setXMax}.
   *  @param r2 the maximum X value */
  public final void setX2 (double r2) { setXMax(r2); }
  /** Sets the min/max X values. This is identical to {@link #setXBounds}.
      @param r1 first X value (at end of range)
      @param r2 second X value (other end of range)
   */
  public final void setX  (double r1, double r2) { setXBounds(r1,r2); }

  /** Sets the minimum Y value. This is identical to {@link #setYMin}.
   *  @param r1 the minimum Y value */
  public final void setY1 (double r1) { setYMin(r1); }
  /** Sets the maximum Y value. This is identical to {@link #setYMax}.
   *  @param r2 the maximum Y value */
  public final void setY2 (double r2) { setYMax(r2); }
  /** Sets the min/max Y values. This is identical to {@link #setYBounds}.
      @param r1 first Y value (at end of range)
      @param r2 second Y value (other end of range)
   */
  public final void setY  (double r1, double r2) { setYBounds(r1,r2); }

  /** Sets the minimum Z value. This is identical to {@link #setZMin}.
   *  @param r1 the minimum Z value */
  public final void setZ1 (double r1) { setZMin(r1); }
  /** Sets the maximum Z value. This is identical to {@link #setZMax}.
   *  @param r2 the maximum Z value */
  public final void setZ2 (double r2) { setZMax(r2); }
  /** Sets the min/max Z values. This is identical to {@link #setZBounds}.
      @param r1 first Z value (at end of range)
      @param r2 second Z value (other end of range)
   */
  public final void setZ  (double r1, double r2) { setZBounds(r1,r2); }

  /** Gets the minimum X value. This is identical to {@link #getXMin}.
   *  @return the minimum X value */
  public final double getX1 () { return getXMin(); }
  /** Gets the maximum X value. This is identical to {@link #getXMax}.
   *  @return the maximum X value */
  public final double getX2 () { return getXMax(); }
  /** Gets the minimum Y value. This is identical to {@link #getYMin}.
   *  @return the minimum Y value */
  public final double getY1 () { return getYMin(); }
  /** Gets the maximum Y value. This is identical to {@link #getYMax}.
   *  @return the maximum Y value */
  public final double getY2 () { return getYMax(); }
  /** Gets the minimum Z value. This is identical to {@link #getZMin}.
   *  @return the minimum Z value */
  public final double getZ1 () { return getZMin(); }
  /** Gets the maximum Z value. This is identical to {@link #getZMax}.
   *  @return the maximum Z value */
  public final double getZ2 () { return getZMax(); }

  /** Sets the minimum X value, adjusting the maximum X value so that the range remains the same.
      If setting both the minimum and maximum X values, this should be called *before*
      {@link #setXMax}; or better yet, use {@link #setXBounds} to set both at once.
      @param r1 minimum X value
   */
  public void setXMin (double r1) { setXBounds(r1, r1+x2-x1); }

  /** Sets the maximum X value without changing the minimum X value. If setting both the minimum
      and maximum X values, this should be called *after* {@link #setXMin}; or better yet, use
      {@link #setXBounds} to set both at once. Since NeXtMidas 3.3.2 this calls
      <code>setUseLocalXMax(true)</code> before calling {@link #setXBounds}.
      @param r2 maximum X value
   */
  public void setXMax (double r2) { setXBounds(x1, r2); }

  /** Sets the minimum and maximum X values. The range will automatically be derived from the
      difference of these two values. Note that the order of the inputs does *not* matter.
      @param r1 first X value (at end of range)
      @param r2 second X value (other end of range)
   */
  public void setXBounds (double r1, double r2) {
    x1 = Math.min(r1,r2);
    x2 = Math.max(r1,r2);
    needRefresh = true; // <-- Added in 3.3.2 (matches setXBounds())
  }

  /** Sets the minimum Y value, adjusting the maximum Y value so that the range remains the same.
      If setting both the minimum and maximum Y values, this should be called *before*
      {@link #setYMax}; or better yet, use {@link #setYBounds} to set both at once.
      @param r1 minimum Y value
   */
  public void setYMin (double r1) { setYBounds(r1, r1+y2-y1); }

  /** Sets the maximum Y value without changing the minimum Y value. If setting
      both the minimum and maximum Y values, this should be called *after*
      {@link #setYMin}; or better yet, use {@link #setYBounds} to set both at once.
      @param r2 maximum Y value
   */
  public void setYMax (double r2) { setYBounds(y1, r2); }

  /** Sets the minimum and maximum Y values. The range will automatically be
      derived from the difference of these two values. Note that the order of
      the inputs does *not* matter.
      @param r1 first Y value (at end of range)
      @param r2 second Y value (other end of range)
   */
  public void setYBounds (double r1, double r2) {
    y1 = Math.min(r1, r2);
    y2 = Math.max(r1, r2);
    needRefresh = true; // <-- Added in 3.3.2 (matches setXBounds())
  }

  /** Sets the minimum Z value, adjusting the maximum Z value so that the range
      remains the same. If setting both the minimum and maximum Z values, this
      should be called *before* {@link #setZMax}; or better yet, use
      {@link #setZBounds} to set both at once. Since NeXtMidas 3.3.2 this calls
      <code>setUseLocalZMin(true)</code> before calling {@link #setZBounds}.
      @param r1 minimum z
   */
  public void setZMin (double r1) { setUseLocalZMin(true); setZBounds(r1, r1+z2-z1); }

  /** Sets the maximum Z value without changing the minimum Z value. If setting
      both the minimum and maximum Z values, this should be called *after*
      {@link #setZMin}; or better yet, use {@link #setZBounds} to set both at once.
      Since NeXtMidas 3.3.2 this calls <code>setUseLocalZMax(true)</code> before
      calling {@link #setZBounds}.
      @param  r2 maximum z
   */
  public void setZMax (double r2) { setUseLocalZMax(true); setZBounds(z1, r2); }

  /** Sets the minimum and maximum Z values. The range will automatically be
      derived from the difference of these two values. Note that the order of
      the inputs does *not* matter. This does *not* call {@link #setUseLocalZMin}
      or {@link #setUseLocalZMax}.
      @param r1 first Z value (at end of range)
      @param r2 second Z value (other end of range)
   */
  public void setZBounds (double r1, double r2) {
    z1 = Math.min(r1,r2);
    z2 = Math.max(r1,r2);
    needRefresh = true; // <-- Added in 3.3.2 (see %TEST_PLOT_LAYER2D_ZVALS)
  }

  @Override public double getXMin () { return x1; }
  @Override public double getXMax () { return x2; }
  @Override public double getYMin () { return y1; }
  @Override public double getYMax () { return y2; }

  /** Gets the minimum Z values. If {@link #getUseLocalZMin} is true this will
      return the local (layer-specific) value; otherwise this will return the
      shared value in {@link MPlot}.
      @return The value in RWC.
   */
  @Override
  public double getZMin () { return (getUseLocalZMin())? z1 : MP.getZMin(); }

  /** Gets the maximum Z values. If {@link #getUseLocalZMax} is true this will
      return the local (layer-specific) value; otherwise this will return the
      shared value in {@link MPlot}.
      @return The value in RWC.
   */
  @Override
  public double getZMax () { return (getUseLocalZMax())? z2 : MP.getZMax(); }

  /** Should the local (layer-specific) minimum Z value be used? If false, the
      shared value in {@link MPlot} is assumed.
      @since NeXtMidas 3.3.2 (previously only in {@link Layer2D})
   */
  final boolean getUseLocalZMin () { return getUseLocalFlag(LOCAL_ZMIN); }

  /** Should the local (layer-specific) maximum Z value be used? If false, the
      shared value in {@link MPlot} is assumed.
      @since NeXtMidas 3.3.2 (previously only in {@link Layer2D})
   */
  final boolean getUseLocalZMax () { return getUseLocalFlag(LOCAL_ZMAX); }

  /** Should the local (layer-specific) minimum Z value be used? If false, the
      shared value in {@link MPlot} is assumed. Note that many layers do not
      support the use of non-local values and will ignore setting this to false.
      @since NeXtMidas 3.3.2 (previously only in {@link Layer2D})
   */
  final void setUseLocalZMin (boolean use) { setUseLocalFlag(LOCAL_ZMIN, use); }

  /** Should the local (layer-specific) maximum Z value be used? If false, the
      shared value in {@link MPlot} is assumed. Note that many layers do not
      support the use of non-local values and will ignore setting this to false.
      @since NeXtMidas 3.3.2 (previously only in {@link Layer2D})
   */
  final void setUseLocalZMax (boolean use) { setUseLocalFlag(LOCAL_ZMAX, use); }

  /** Sets MPlot override flag (only used by MPlot). (Since NeXtMidas 3.3.2) */
  final void setUseMPlotZMin (boolean use) { setUseMPlotFlag(MPLOT_ZMIN, use); }
  /** Sets MPlot override flag (only used by MPlot). (Since NeXtMidas 3.3.2) */
  final void setUseMPlotZMax (boolean use) { setUseMPlotFlag(MPLOT_ZMAX, use); }

  /** Gets the given Local flag. (Since NeXtMidas 3.3.2) */
  private boolean getUseLocalFlag (int flag) {
    if (MP == null) return true;        // prevents NPE when accessing MP

    return ((useLocal&(flag<<6))!=0)    // Trick: MPLOT_?    = LOCAL_?MIN<<6
        && ((useLocal&(flag<<4))==0)    // Trick: MPLOT_?MIN = LOCAL_?MIN<<4
        && ((useLocal&(flag   ))!=0);
  }

  /** Sets the given Local flag. (Since NeXtMidas 3.3.2) */
  private void setUseLocalFlag (int flag, boolean use) {
    // Clear the MPlot and Local flag
    useLocal &= ~(flag<<4 | flag);   // Trick: MPLOT_?MIN = LOCAL_?MIN<<4
    if (use) useLocal |=  flag;
  }

  /** Sets the given MPlot flag. (Since NeXtMidas 3.3.2) */
  private void setUseMPlotFlag (int flag, boolean use) {
    // Add override flag, but don't clear normal setting. This permits us to
    // clear any overrides without discarding the user's selection (though we
    // don't make use of this yet).
    useLocal &= ~(flag);
    if (use) useLocal |=  flag;
  }

  /** Turns on/off support for using non-local values for x, y, and/or z. This
      function is <code>setEnableNonLocal(..)</code> rather than <code>setEnableLocal(..)</code>
      since use of local values is the default and prior to NeXtMidas 3.3.2 was
      the only option (except for {@link Layer2D}.
      @param x Enable non-local x?
      @param y Enable non-local y?
      @param z Enable non-local z?
   */
  protected final void setEnableNonLocal (boolean x, boolean y, boolean z) {
    useLocal &= ~(MPLOT_X|MPLOT_Y|MPLOT_Z);

    if (x) useLocal |=  MPLOT_X;
    if (y) useLocal |=  MPLOT_Y;
    if (z) useLocal |=  MPLOT_Z;
  }

  /** Get the time at a given mouse position. <br>
      <br>
      The default implementation of this function (in {@link Layer}) simply returns
      the time associated with the entire layer. A number of layers such as
      {@link Layer1D} and {@link Layer2D} override this method to take the
      current mouse location and then derive which element in the file corresponds
      to it and the associated time.
      @param px x screen pixel location
      @param py y screen pixel location
      @return J1950 time at specified screen pixel location
   */
  public double getTime (int px, int py) {
    return time;
  }

  /** Gets the position at a given time.
      @param time The time to get the position for (in MJS).
      @return The position that corresponds with the given time (in pixels) or a value less than
              zero is such can not be computed for the given layer.
      @since NeXtMidas 1.9.3
   */
  public double getPosAtTime (double time) {
    return -1;
  }

  /** Gets the Y position at a given time.
      @param time The time to get the position for (in MJS).
      @return The Y offset that corresponds with the given time or a negative value
              if the position is out-of-range or cannot be computed for the given layer.
      @since NeXtMidas 2.7.1
      @deprecated Since NeXtMidas 3.3.2 -- This function was a temporary one while
                  the old {@link Layer2D#getPosAtTime(double)} was deprecated. That
                  old version was finally removed and replaced by the new version
                  in NeXtMidas 3.3.2 making this function redundant.
   */
  @Deprecated // TODO: Remove this function in NeXtMidas 3.7+
  public double getYPosAtTime (double time) {
    return -1;
  }

  /** Given x, return nearest y value
      @param x x value
      @return nearest y value
      @exception  ArithmeticException &nbsp;
   */
  public double getNearestYValue (double x)
    throws ArithmeticException {
    if (xdelta==0.0) throw new ArithmeticException();
    int i = (int)((x-xstart)/xdelta+0.5);
    if (i<0) i=0;
    if (i>=ndata) i=ndata-1;
    return getNearestYValue(i);
  }
  /** Currently returns 0.0
      TODO: needs to be implemented ??
      @param i not used
      @return 0.0
   */
  public double getNearestYValue (int i) {
    return 0.0;
  }

  /** Gets the current Feature being used by this Layer
      @return the current Feature being used by this layer
   */
  public Feature getFeature () {
    if (feature!=null) return feature;
    feature = new Feature(this);
    //feature.setType(Feature.DATA);  // changed to line below in 3.1.1 since most layers do not support it (Bug 2549)
    feature.setType(0);
    feature.setName("_LAYER_"+getName()+"_");  // added in 3.1.1
    feature.setEnable(0);
    return feature;
  }

  /** Set surface rendering mode. See {@link Surface#typeList} for available options.
   *  @param value surface rendering mode */
  public void setSurface (int value) { stype=value; refresh(); }

  /** Set surface rendering mode. See {@link Surface#typeList} for available options.
   *  @param value surface rendering mode as String */
  public void setSurface (String value) { setSurface( Parser.mask(Surface.typeList,value,stype) ); }

  /** Sets the color of the layer.
      @param value The name of the color.
   */
  public void setColor (String value) { line.setColor(value); }

  /** Sets the color of the layer based on indexing into the a pre-defined set of colors.
      This does NOT take in the RGB value of a color.
      @param value The index of the color.
   */
  public void setColor (int value) { line.setColor(value); }

  /** Sets the line color of the layer.
      @param value The color.
   */
  public void setColor (Color value) { line.setColor(value); }

  /** Gets the line color of the layer.  <br>
      Be careful, this may not be what you want if a color map is being used
      (i.e. there is no single color for the layer).
      @return current line color of this layer
  */
  public Color getColor () {
    return line.getColor();
  }

  /** Sets the color map colors being used in the color map.
      @param value The name of the color map or a list of colors.
      @see #setColorMap(String)
   */
  public void setColors (String value) {
    setColorMap(value);
  }

  /** Sets the color map being used. <br>
      This is the same as <code>setColorMap(value,-1)</code>.
      @param value The color map ID number.
   */
  public void setColorMap (int value) { setColorMap(value,-1); }

  /** Sets the color map being used.
      @param value   The color map ID number.
      @param ncolors The number of colors to use from the color map (-1 = use all colors).
   */
  public void setColorMap (int value, int ncolors) {
    if (value==-1) value = cmap;
    if (value<1 || value>MColor.MAXTYPE) return;
    if (value==cmap && ncolors==this.ncolors) return;
    cmap = value;
    setColorMap(MColor.getColorMap(value,ncolors));
  }

  /** Sets the actual color map. All of the other <code>setColor(..)</code> methods in {@link Layer}
      end up calling this method. Subclasses should override this if they need to update other
      fields (e.g. a backing store or image), but should remember to include a call to
      <code>super.setColorMap(colors)</code> at the top of the method.
      @param colors The colors that make up the color map.
   */
  public void setColorMap (Color[] colors) {
    if (effects != 0) colors = applyEffects(colors, effects);
    this.colors  = colors;
    this.ncolors = colors.length-1;
    refresh();
  }

  /** Sets the color map being used. <br>
      <br>
      Example usage:
      <pre>
        Use the "Ramp" color map defined in {@link MColor}:
          setColorMap("RAMP")

        Define a custom color map:
          setColorMap("RED,YELLOW,GREEN,BLUE")
            or
          setColorMap("(RED,YELLOW,GREEN,BLUE)")

      </pre>
      @param value The name of the color map or a list of colors.
      @since NeXtMidas 2.1.0
   */
  public void setColorMap (String value) {
    if (value.length()==0) return;
    setColorMap(MColor.getColorMap(value,-1));
  }

  /** Gets the color map being used. <br>
      Note that this intentionally omits color number zero from the list. This prevents the color
      map from changing if <code>setColorMap(..)</code> is later called with the string returned
      from this method.
      @return The list of colors that make up the color map being used.
      @since NeXtMidas 2.1.0
   */
  public String getColorMap () {
    if (colors == null) return "";
    StringBuilder str = new StringBuilder();
    for (int i = 1; i < colors.length; i++) {
      if (i != 1) { str.append(','); }
      str.append(MColor.toString(colors[i]));
    }
    return str.toString();
  }

  /** Get current effects setting on this layer. See {@link #effectsList}.
      @return current effects setting on this layer
      @since NeXtMidas 3.3.3
   */
  public String getEffects () {
    return Parser.mask2s(effectsList, effects);
  }

  /** Set the effects on the layer.
      @param arg The effects to set
      @return True if something was set, else false (if the effects were already that value)
      @see #effectsList
      @since NeXtMidas 2.9.0 (backported to 2.8.2)
   */
  public boolean setEffects (int arg) {
    if (arg == effects) return false;
    effects = arg;
    setColorMap(cmap);
    return true;
  }

  /** Set the effects on the layer.
      @param arg The effects to set
      @return True if something was set, else false (if the effects were already that value)
      @see #effectsList
      @since NeXtMidas 2.9.0 (backported to 2.8.2)
   */
  public boolean setEffects (String arg) {
    return setEffects(Parser.mask(effectsList, arg, effects));
  }

  /** Get whether or not the effect(s) given is set in {@link #effects}
      @param mask effects mask to check if it is set
      @return true of the effects for specified mask is set
      @since NeXtMidas 2.9.0 (backported to 2.8.2)
   */
  public boolean getEffect (int mask) {
    return (effects&mask)!=0;
  }

  /** Apply effects to an array of Colors
      @param colors The array of Colors to adjust
      @param effects Must be a subset of {@link #effectsList}
      @return The modified Color array
      @since NeXtMidas 2.9.0 (backported to 2.8.2)
   */
  private Color[] applyEffects (Color[] colors, int effects) {
    if ((effects&E_INVERT_COLORS) != 0 )  colors = MColor.invertColorsMap(colors);
    if ((effects&E_REVERSE_COLORS) != 0 ) colors = MColor.reverseColorMap(colors);
    return colors;
  }

  /** Reverses the current ColorMap. Doing this twice will revert colors back to normal.
      @since NeXtMidas 2.9.0 (backported to 2.8.2)
  */
  public void reverseColorMap () {
    if (colors==null) {
      warning("No color map in use, therefore cannot reverse it for "+this);
      return;
    }
    effects ^= E_REVERSE_COLORS; // Toggle
    setColorMap(cmap);
  }

  /** Reverse the current colorMap. This is a convenience method to allow for calling
      {@link #reverseColorMap()} on the plot command line using the {LAYER={ReverseColorMap=TRUE}} qualifier.
      @param val TRUE to reverse the colorMap, otherwise does nothing
      @since NeXtMidas 2.9.0 (backported to 2.8.2)
   */
  public void setReverseColorMap (boolean val) {
    if (val) reverseColorMap();   // FALSE does nothing
  }

  /** Invert the colors (to create photo negative effect). Doing this twice will
      revert colors back to normal.
      @since NeXtMidas 2.9.0
  */
  public void invertColorMap () {
    if (colors==null) {
      warning("No color map in use, therefore cannot invert it for "+this);
      return;
    }
    effects ^= E_INVERT_COLORS;  // Toggle
    setColorMap(cmap);
  }

  /** This is a convenience method to allow for calling {@link #invertColorMap()}
      on the plot command line using the {LAYER={InvertColorMap=TRUE}} qualifier.
      @param val TRUE to invert the colors, otherwise does nothing
      @since NeXtMidas 2.9.0
   */
  public void setInvertColorMap (boolean val) {
    if (val) invertColorMap(); // FALSE does nothing
  }

  // Not sure if this Method is supposed to do something else with Highlighting ??
  /** Currently calls {@link Feature#setType(int)} to set the type of Feature to be drawn
      @param value flag for the type of Feature to be drawn
   */
  public void setHiMode (int value) {
    if (value==1) value = Feature.DATA;
    else if (value==2) value = Feature.VLINE;
    else if (value==3) value = Feature.DATA|Feature.VLINE;
    getFeature().setType(value);
  }
  /** Sets the Highlight X value
      @param value the new Highlight X value
   */
  public void setHiX (double value) { getFeature().setX(value); }
  /** Sets the Highlight deltax value
      @param value the new Highlight deltax value
   */
  public void setHiXD (double value) { getFeature().setDX(value); }
  /** Sets the Highlight Color value
      @param value the new Highlight Color value
   */
  public void setHiColor (String value) { getFeature().getLine().setColor(value); }

  /** Get the Highlight X value
   *  @return the Highlight X value*/
  public double getHiX () { return getFeature().getX(); }

  /** Get the Highlight deltax value
   *  @return the Highlight deltax value*/
  public double getHiXD () { return getFeature().getDX(); }

  /** Gets x1i TRIM1
      @return x1i TRIM1
   */
  public int getx1i () {
    int i = (int)( (MP.rx1-xstart)/xdelta );
    return Math.max(1,Math.min(frame,i+1));
  }

  /** Gets x2i TRIM2
      @return x2i TRIM2*/
  public int getx2i () {
    int i = (int)( (MP.rx2-xstart)/xdelta );
    return Math.max(1,Math.min(frame,i+1));
  }

  /** @deprecated Since NeXtMidas 2.5.4 - this was for internal usage and may be
                 removed in a future release.
   */
  @Deprecated protected void prepDATAX (double x1, double x2, boolean delta, boolean marker) {
    if (MP.gc==null) return;
    int ic = 0;
    int ixc1 = MP.getix(x1);
    int ixc2 = MP.getix(x2)+1;
    if (delta) {
      MP.gc.clearRect(ixc1+1,MP.iy1,ixc2-ixc1+ic,MP.iy21);
    }
    if (marker && ixc2>MP.ix1 && ixc2<MP.ix2-1) {
      MP.gc.drawLine(ixc2+1,MP.iy1+3,ixc2+1,MP.iy2-4);
    }
  }

  /** calculate the scaling factor to use.
      @param data data buffer (to get it's scalars per atom (spa))
      @param plotMode current plot mode
      @return scaling factor to use
   */
  protected static float setScale (Data data, int plotMode) {
    return (float) getScaleFactor(data.spa, plotMode);
  }

  /** Gets the scaling factor associated with the plot data format and plot mode.
      @since NeXtMidas 3.3.2
   */
  static double getScaleFactor (int spa, int mode) { // should this be protected?
    double scale = 1;
    if (mode==MPlot.M_10LOG) scale=10.0/Math.log(10.0);
    if (mode==MPlot.M_20LOG) scale=20.0/Math.log(10.0);
    if (spa!=1) scale = scale/2;
    if (mode==MPlot.M_PHASE) scale=180.0/Math.PI;
    return scale;
  }

  @SuppressWarnings("fallthrough")
  protected float[] data2fa (Data data, int ndata, float[] fbuf, int offset, int mode) {
    int i=offset,i2=i+ndata,spa=data.spa;
    float scale = setScale(data, mode);
    if (ffbuf!=null) fbuf=ffbuf;
    else {
      if (fbuf==null || fbuf.length<i2*spa){
        try {
          fbuf = new float[i2*spa];
        } catch (OutOfMemoryError e) {  // for when user tries to allocate a buffer bigger than memory will hold (Bug 885)
          throw new MidasException("Not enough memory to allocate a (float) buffer of size "+(i2*spa*4)+" bytes. "
              +"Current free memory is "+Runtime.getRuntime().freeMemory()+" bytes.",e);
        }
      }
      Convert.bb2ja (data.buf,0,data.type, fbuf,i*spa,Data.FLOAT, ndata*spa);
    }
    if (data.spa==1) {
      switch (mode) {
      case MPlot.M_10LOG: case MPlot.M_20LOG:
        // Jeff changed lowest allowed DB 110 to -200 (i.e. 10.e-20) for NXM-2755
        for (;i<i2;i++) fbuf[i] = Math.min(DataTypes.FMAX, scale * (float)Math.log(Math.max(10.e-20,Math.abs((double)fbuf[i])))); // added Math.min in 3.7.0 for bug 2901
        break;
      default:
        break;
      }
    }
    else if (data.spa==2) {
      int j=i;
      switch (mode) {
      case MPlot.M_MAG:
        for (;i<i2;i++,j+=2) fbuf[i] = Math.min(DataTypes.FMAX,(float)Math.sqrt((double)(fbuf[j]*fbuf[j]+fbuf[j+1]*fbuf[j+1]))); // added Math.min in 3.7.0 for bug 2901
        break;
      case MPlot.M_PHASE:
        for (;i<i2;i++,j+=2) fbuf[i] = Math.min(DataTypes.FMAX,scale * (float)Math.atan2((double)fbuf[j+1],(double)fbuf[j]));   // added Math.min in 3.7.0 for bug 2901
        break;
      case MPlot.M_IMAG: case MPlot.M_VY:
        j+=1; // fallthrough
      case MPlot.M_REAL: case MPlot.M_VX:
        for (;i<i2;i++,j+=2) fbuf[i] = fbuf[j];
        break;
      case MPlot.M_10LOG:
      case MPlot.M_20LOG:
        for (;i<i2;i++,j+=2) {
          // added Math.min in 3.7.0 for bug 2901
          fbuf[i] = Math.min(DataTypes.FMAX,scale * (float)Math.log(Math.max(1.e-20,(double)(fbuf[j]*fbuf[j]+fbuf[j+1]*fbuf[j+1]))));
        }
        break;
      default:
      }
    }
    else if (data.spa==3) {
      int j=i;
      switch (mode) {
      case MPlot.M_MAG:
          for (;i<i2;i++,j+=3) fbuf[i] = Math.min(DataTypes.FMAX,(float)Math.sqrt((double)(fbuf[j]*fbuf[j]+fbuf[j+1]*fbuf[j+1]+fbuf[j+2]*fbuf[j+2]))); // added Math.min in 3.7.0 for bug 2901
        break;
      case MPlot.M_VZ:
          j+=1; // fallthrough
      case MPlot.M_VY: case MPlot.M_IMAG:
          j+=1; // fallthrough
      case MPlot.M_VX: case MPlot.M_REAL:
        for (;i<i2;i++,j+=3) fbuf[i] = fbuf[j];
        break;
      default:
      }
    }
    return fbuf;
  }

  /** Read specified amount of data (ndata) from Data and return it back as array of int
      @param data Data object to read data from
      @param ndata number of data elements
      @param buf buffer to put read data into, unless it does not have enough space.
      @param offset offset into buf that data is copied into
      @since NeXtMidas 2.5.0
   */
  protected int[] data2la (Data data, int ndata, int[] buf, int offset) {
    int i=offset,i2=i+ndata;
    if ( data.getFormat().equals("SL") ) {
      if (buf==null||buf.length<i2) buf = new int[i2];
      Convert.bb2ja(data.buf,0,data.type, buf,i,DataTypes.LONG, ndata);
    } else if (data.getFormat().equals("QB")) {
      if (buf==null||buf.length<i2) buf = new int[i2];
      // if data is not the same endian is OS, then swap the bytes
      if (!Convert.sameEndian(data.rep, Shell.rep)) Convert.swap4(data.buf, 0, ndata);

      Convert.bb2ja(data.buf, 0, DataTypes.LONG, buf, i, DataTypes.LONG, ndata);
    } else {
      warning("Layer.data2la: data format " + data.getFormat() + " NOT supported for this conversion.");
    }

    return buf;
  }

  /** Gets the X-axis units for the layer. This defaults to the X-Units field in a data file's
      header or {@link Units#NONE} if not applicable.
      @return integer constant of X-axis units
   */
  public int getXUnits () { return xunits; }

  /** Gets the Y-axis units for the layer. This defaults to the Y-Units field in a data file's
      header or {@link Units#NONE} if not applicable.
      @return integer constant of Y-axis units
   */
  public int getYUnits () { return yunits; }

  /** Gets the Z-axis units for the layer. This defaults to the Z-Units field in a data file's
      header or {@link Units#NONE} if not applicable.
      @return integer constant of Z-axis units
   */
  public int getZUnits () { return zunits; }

  /** Get the X-axis units for the layer
   *  @return the X-axis units for the layer. */
  public String getXUnitsName () { return DataFile.getUnitsVarName(getXUnits()); }

  /** Get the Y-axis units for the layer
   *  @return the Y-axis units for the layer. */
  public String getYUnitsName () { return DataFile.getUnitsVarName(getYUnits()); }

  /** Get the Z-axis units for the layer
   *  @return the Z-axis units for the layer. */
  public String getZUnitsName () { return DataFile.getUnitsVarName(getZUnits()); }

  /** Sets the X-axis units for the layer. This will not alter the header of the file being plotted.
      @param units The X-axis units for the layer (see {@link Units}).
   */
  public void setXUnits (String units) {
    setXUnits(DataFile.getUnitsID(units)); }

  /** Sets the Y-axis units for the layer. This will not alter the header of the file being plotted.
      @param units The Y-axis units for the layer (see {@link Units}).
   */
  public void setYUnits (String units) { setYUnits(DataFile.getUnitsID(units)); }

  /** Sets the Z-axis units for the layer. This will not alter the header of the file being plotted.
      @param units The Z-axis units for the layer (see {@link Units}).
   */
  public void setZUnits (String units) { setZUnits(DataFile.getUnitsID(units)); }

  /** Sets the X-axis units for the layer. This will not alter the header of the file being plotted.
      @param units The X-axis units for the layer (see {@link Units}).
   */
  public void setXUnits (int units) { xunits = units; }

  /** Sets the Y-axis units for the layer. This will not alter the header of the file being plotted.
      @param units The Y-axis units for the layer (see {@link Units}).
   */
  public void setYUnits (int units) { yunits = units; }

  /** Sets the Z-axis units for the layer. This will not alter the header of the file being plotted.
      @param units The Z-axis units for the layer (see {@link Units}).
   */
  public void setZUnits (int units) { zunits = units; }

  /** Gets the symbol used for drawing this layer.
      @return The symbol used in drawing this layer. This will return <code>null</code> when the
              symbols are turned off, although the line (see {@link #getLine()}) may still have a
              pointer to the symbol.
   */
  public Symbol getSymbol () {
    if (getLine().isSymbolOn()) {
      return getLine().getSymbol();
    } else {
      return null;
    }
  }

  /** Get the class name of the layer */
  String getClassName () {
    String className = getClass().getName();

    if (className.startsWith("nxm.sys.libg.")) {
      className = className.substring(13);
    }

    return className;
  }

  /** Gets a description of the layer. */
  @Override
  public String toString () {
    String className = getClass().getName();

    if (className.startsWith("nxm.sys.libg.")) {
      className = className.substring(13);
    }

    return className + ": " + getName();
  }

  /** Processes mouse movement messages. The default implementation of this does nothing with the
      messages, however some layers do (such as LayerT5 which uses it to support AutoSelect).
      @param msg  The message.
      @param type The message type ({@link nxm.sys.prim.plot#ONCLICK},
                                    {@link nxm.sys.prim.plot#ONDRAG},
                                    {@link nxm.sys.prim.plot#ONMOTION}).
      @since NeXtMidas 2.1.0
   */
  public void processMouseMessage (Message msg, int type) {
    /* Default implementation does nothing. */
  }

  /** Indicates if the layer supports a given view. By default this returns <code>true</code>
      for all views but some layers (most often image layers) will override this to return
      <code>false</code> for some views.
      @param view The view (see {@link MPlot#viewList}).
      @return <code>true</code> if the layer supports the given view, <code>false</code> if not.
      @since NeXtMidas 2.1.1
   */
  public boolean isViewSupported (int view) {
    return true;
  }

  /** Gets the comment for this layer.
      @return the comment for this layer
      @since NeXtMidas 3.3.3
   */
  public String getComment () {
    return comment;
  }

  /** Sets the comment for this layer.
      @param comment the comment for this layer
   */
  public void setComment (String comment) {
    this.comment = comment;
  }

  /** Rescales the layer, ONLY called by MPlot. */
  void rescale () {
    setNeedsRescale(false);
    findRange();
  }

  /** Indicates if this layer needs to be rescaled. Only the individual layer can set this value.
      @return true if this layers needs to be rescaled, otherwise false
   */
  public boolean getNeedsRescale () {
    return needsRescale;
  }

  /** Indicates if this layer needs to be rescaled. Only the individual layer can set this value. */
  protected void setNeedsRescale (boolean rescale) {
    needsRescale = rescale;
  }

  /** Gets the legend entry for the layer. Subclasses should not need to override this method.
      @return The top-level entry for the layer.
      @see #getLegendSubEntries()
      @since NeXtMidas 2.5.5
   */
  public GLegend.Entry getLegendEntry () {
    return legendEntry;
  }

  /** Gets the legend sub-entries for the layer. Subclasses should override this method if they have
      any sub-entries to add to the legend.
      @return The sub-entries for the layer. If there are no sub-entries, this method may return
              either null or an array with length 0.
      @see #getLegendEntry()
      @since NeXtMidas 2.5.5
   */
  public GLegend.Entry[] getLegendSubEntries () {
    return null;
  }

  /** Auto scaling helper method that should only be called inside {@link #findRange()}.
      This code was originally in Layer1D, Layer2D, and LayerXY's findRange().
      @since NeXtMidas 3.3.3
   */
  protected void autoScale4FindRange (double newMin, double newMax) {
    // begin since 3.5.2 for BUG 2857 - bound min/max to valid double ranges that PLOT understands and protect against NaN
    final double DELTA_FOR_BIG_NUM = 1e23; // since values at FMAX increments by ~1e22 not 1
    if      (newMin < DataTypes.FMIN) newMin = DataTypes.FMIN;
    else if (newMin > DataTypes.FMAX) newMin = DataTypes.FMAX - DELTA_FOR_BIG_NUM;
    else if (Double.isNaN(newMin)) newMin = a1;
    if      (newMin < DataTypes.FMIN) newMin = DataTypes.FMIN + DELTA_FOR_BIG_NUM;
    else if (newMax > DataTypes.FMAX) newMax = DataTypes.FMAX;
    else if (Double.isNaN(newMax)) newMax = a2;
    // end since 3.5.2 for BUG 2857
    if ((MP.scale&MPlot.S_EXPANDONLY)!= 0) { // no shrink autoscaling: since 3.4.2 for BUG 1807
      if (newMax > a2) a2 = newMax; // new max
      if (newMin < a1) a1 = newMin; // new min
    }
    else if ((MP.scale&MPlot.S_NOAVERAGE)!= 0 && MP.scaleAvg>0) { // find actual/true/full min/max (no exp avg) over n frames
      double[] minMax = autoscaleMinMaxBuffer.updateMinMax(newMin, newMax);
      a1 = minMax[SlidingMinMax.COLUMN_MIN];
      a2 = minMax[SlidingMinMax.COLUMN_MAX];
    }
    else if (MP.scaleAvg>0) { // exponential averaging (sliding window) min/max autoscaling for n frames
      double p = 1.0/MP.scaleAvg;
      if (a1==a2) p=1.0;
      a1 = a1*(1.0-p) + newMin*p;
      a2 = a2*(1.0-p) + newMax*p;
//    if (newMax > a2) a2 = newMax; // ensure max is visible (TODO: consider doing this in 3.5+)
//    if (newMin < a1) a1 = newMin; // ensure min is visible (TODO: consider doing this in 3.5+)
    }
  }

  /** get current debug options mask
      @return current debug mask (0 for none)
      @since 3.5.1
   */
  @InternalUseOnly
  public int getDebugMask () {
    return debugMask;
  }

  /** set debugging options mask (to print debug messages).
      @param newVal new debug options mask (0 for none) to use
      @since 3.5.1
   */
  @InternalUseOnly
  public void setDebugMask (int newVal) {
    debugMask = newVal;
  }

  /** Get the desired fresh rate (screens/sec) for this layer, such that data pipe
      coming at faster rate will be ignored (dropped).
      @return desired refresh rate (screens/frames per second) for this layer
      @since NeXtMidas 3.5.1 for BUG 2845
   */
  public int getRefreshRate () {
    return refreshRate;
  }

  /** Set the desired refresh rate (screen frames per second (FPS)) for this layer, such that
      incoming data coming at faster rate will be ignored/dropped via skipping (a.k.a. thinned).
      The layer does dynamic skipping of data (thinning), but may cap the maximum
      skipped/dropped elements using the value set by {@link #setMaxSkipMultiplier(int)}
      to prevent bursty input data to cause too much data to be skipped which in
      turn makes the plot not update in a timely manner.
      Note: some layers can disable enable this feature.
      @param newRefresRate desired data screens/frames per second (refresh rate) for this layer.<br>
             Set to {@value #RR_DISABLED} to disable (current default to match existing behavior).<br>
             Set to {@value #RR_AUTO} for auto mode (e.g. an appropriate refresh rate will be used, such as 30 fps).<br>
             Set to {@value #RR_DISABLED_BUT_CALC_STATS} to disable, but calculate incoming data fps.<br>
             All other negative values are reserved (invalid).
      @throws IllegalArgumentException when given an invalid value
      @since NeXtMidas 3.5.1 for BUG 2845
   */
  public void setRefreshRate (int newRefresRate) {
    if (newRefresRate < RR_DISABLED_BUT_CALC_STATS) throw new IllegalArgumentException("Invalid refresh rate! " + newRefresRate);
    if (newRefresRate == RR_AUTO) { // auto mode (use reasonable default)
      newRefresRate = DEF_RR_AUTO_REFRESH_RATE;
    } else if (newRefresRate == RR_DISABLED) {
      rrInDataRate = 0;             // reset input data rate
    }
    refreshRate = newRefresRate;
    if (debugMask != 0) System.out.println("setRefreshRate() refreshRate=" + refreshRate + " " + this);
  }

  /** Get the desired refresh rate smoothing factor
   *  @return current incoming data refresh rate smoothing factor for the exponential moving average calculation
   *  @since NeXtMidas 3.5.1 for BUG 2845
   */
  @InternalUseOnly
  public float getRefreshSmoothingFactor () {
    return rrSmoothingFactor;
  }

  /** Set desired refresh rate smoothing factor
   *  @param newVal new desired refresh rate smoothing factor (MUST in range of (0 - 1].
   *  @since NeXtMidas 3.5.1 for BUG 2845
   */
  @InternalUseOnly
  public void setRefreshSmoothingFactor (float newVal) {
    if (newVal > 1 ||  newVal <= 0) throw new IllegalArgumentException("Invalid refresh rate smoothing factor; must be in range of (0 - 1]! " + newVal);
    rrSmoothingFactor = newVal;
    if (debugMask != 0) System.out.println("setRefreshSmoothingFactor() smoothingFactor=" + rrSmoothingFactor + " " + this);
  }

  /** Get the maximum available elements skip/drop multiplier
   *  @return current maximum available elements skip/drop multiplier for refresh rate smart thinning
   *  @since NeXtMidas 3.5.1 for BUG 2845
   */
  @InternalUseOnly
  public int getMaxSkipMultiplier () {
    return rrMaxSkipMultiplier;
  }

  /** Set the maximum available elements skip/drop multiplier
   *  @param newVal new maximum available elements skip/drop multiplier for refresh rate smart thinning
   *  @since NeXtMidas 3.5.1 for BUG 2845
   */
  @InternalUseOnly
  public void setMaxSkipMultiplier (int newVal) {
    if (newVal < 1) throw new IllegalArgumentException("Invalid (refresh rate) max (avail elements) skip multiplier; must be >= 1! " + newVal);
    rrMaxSkipMultiplier = newVal;
  }

  /** Get current/calculated incoming input data rate (screen/frames) (in seconds).
      This is only available when the desired refresh rate is greater than zero or set to {@link #RR_DISABLED_BUT_CALC_STATS}.
      @return incoming input data rate (zero if none)
      @since NeXtMidas 3.5.1 for BUG 2845
   */
  @InternalUseOnly
  public float getInputDataRate () {
    return rrInDataRate;
  }

  /** Subclass should override this if the elements per screen (e.g. lines per screen) is NOT the same
      as {@link #getSize()}, or for a type 1000 file: {@link #getSize()} * {@link #getFrame()}.
      This is used by the refresh rate smart/dynamic thinning to attempt to skip/drop incoming data
      so that the screen refresh rate is not flickering too much and/or blocking the pipe.<br>
      For 1D (line), this is generally number of elements per frame (screen).<br>
      For 2D (raster), this is the number of (elements) lines per screen.<br>
      NOTE: This should never return a value less than 1.
      @return number of elements per screen (for refresh rate calculation), or 1 if none
      @since NeXtMidas 3.5.1 for BUG 2845
   */
  @InternalUseOnly
  public int getElementsPerScreen () {
    int lps = getSize();
    if (lps < 1) lps = 1; // prevent's divide by zero and bogus negative size (lps)
    if (typeClass == 1) { // convert type 1000 to elements per screen frame
      final int frameSize = getFrame();
      if (frameSize > 0) return frameSize * lps;
    }
    return lps;
  }

  /** Get number of elements to drop/skip (thin) when a desired refresh rate (screen/frames per second) is set.
      This also updates current incoming data rate statistics if enabled.
      Subclass that wish to disable (auto/dynamic) smart thinning based on refresh rate (when set via
      {@link #setRefreshRate(int)}) should override and return 0.
      @param elementsAvail number of element available to read
      @return number of elements to skip (e.g. drop/thin) after read
      @since NeXtMidas 3.5.1 for BUG 2845
   */
  @InternalUseOnly
  protected int getElementsToSkipAfterRead (final int elementsAvail) {
    int elementsToDrop = 0;
    // allow zero elemsAvail to update incoming data rate stats
    if ((elementsAvail >= 0) && (refreshRate > 0 || refreshRate == RR_DISABLED_BUT_CALC_STATS)) {
      final double curTime = Time.current();
      double elapsedTime = curTime - rrLastUpdateTime;
      if (elapsedTime > 0.01) {
        // don't update stats or calc elements to skip if elapsed time is very small to prevent bogus spikes in refresh rate
        final int elementsPerScreen = getElementsPerScreen();
        final float screensAvailable = (float) elementsAvail / elementsPerScreen; // cast to float, otherwise this could be zeros
        final float screensReadSinceLastUpdate = (float) rrElemsReadSinceLastUpdate / elementsPerScreen; // cast to float, otherwise it quickly
        final float newScreenAvailable = screensAvailable - (rrLastNumScreenAvail - screensReadSinceLastUpdate);
        if (newScreenAvailable > 0) {
          if ((debugMask & 0x1) != 0) System.out.println("new data avail, elapsedTime="+ elapsedTime + " newFrames="+ newScreenAvailable + " framesAvail="+screensAvailable + " lastNumFramesAvail="+rrLastNumScreenAvail);
          rrLastUpdateTime = curTime;
          rrElemsReadSinceLastUpdate = 0;
          rrLastNumScreenAvail = newScreenAvailable;
        } else { // no new data frames
          if ((debugMask & 0x2) != 0) System.out.println("   no new data, elapsedTime="+ elapsedTime + " newFrames="+ newScreenAvailable + " framesAvail="+screensAvailable + " lastNumFramesAvail="+rrLastNumScreenAvail + " elementsReadSinceUpdate="+rrElemsReadSinceLastUpdate);
        }

        float newInDataRate = rrLastNumScreenAvail / (float) elapsedTime; // using float since we don't need precision of double
        // calc exponential moving average of incoming data rate (screen frames per second (FPS))
        rrInDataRate = rrInDataRate*(1 - rrSmoothingFactor) + (newInDataRate*rrSmoothingFactor);
        if ((debugMask & 0x4) != 0) System.out.println("inDataRate="+ (int)rrInDataRate + " tmpDateRate="+ newInDataRate + " elapsedTime="+ elapsedTime + " lastNumFramesAvail="+ rrLastNumScreenAvail + " framesAvail=" + screensAvailable + " newFrames=" + newScreenAvailable + " elementsAvail="+elementsAvail);

        if ((screensAvailable > 0) && (refreshRate > 0)) { // calc # elements to drop if have incoming data and doing refresh rate smart thinning
          int screensToDrop = (int) (rrInDataRate - refreshRate);
          if (screensToDrop > 0) {
            final int desiredNumToDrop = elementsToDrop = screensToDrop * elementsPerScreen; // convert screen (frames or raster screens) to elements
            // NOTE: using a multiplier of the # elements avail to cap the max elements to skip/drop
            //       to protect against scenario of bursty input data rate causing this layer to
            //       skip too many elements, causing long wait (e.g. delay) for new data to be plotted.
            elementsToDrop = Math.min(elementsToDrop, elementsAvail * rrMaxSkipMultiplier);
            if ((debugMask & 0x8) != 0 || ((debugMask & 0x10) != 0 && (elementsToDrop < desiredNumToDrop)))
              System.out.println("skipping " + elementsToDrop + " vs " + desiredNumToDrop + " elements. inDataRate="+ (int)rrInDataRate + " screensToDrop="+ screensToDrop + " elemsAvail="+ elementsAvail);
          }
        }
      }
    }
    return elementsToDrop;
  }

  /** @param msg warning message to display to appropriate context (terminal, log, etc.)
      @since NeXtMidas 3.5.2 with protected access so sub-classes can use this utility method
   */
  protected void warning (CharSequence msg) {
    Midas midas = Convert.ref2Midas(M); // <-- this NeXtMidas 3.5.0 API never returns null
    midas.warning(msg);
  }

  @Override
  public boolean isAtPosition (Position pos, int dpx, int dpy) {
    // Default implementation ignores dpx and dyp and just does a simplistic min/max check

    // Check x-axis (if applicable)
    if ((dpx != -2)) {
      if (pos.getX() < getXMin()) return false;
      if (pos.getX() > getXMax()) return false;
    }

    // Check y-axis (if applicable)
    if ((dpy != -2)) {
      if (pos.getY() < getYMin()) return false;
      if (pos.getY() > getYMax()) return false;
    }

    // everything matches
    return true;
  }

  /**
   * Allows more thorough testing of the SlidingMinMax class
   * @param buffLength length/size of the buffer (maximum number of min/max combination values it can hold)
   * @param autoL number of values to take the smallest min / largest max over (last auto Length number of values
   * @param indexResetAutoL index where autoL should get reset to new value (make beyond indexReportMinMax if never)
   * @param newAutoL new auto length value that takes effect right before adding values for index indexResetAuto
   * @param minMaxValues array holding min (column 0) and max (column 1) values for test purposes
   * @param indexReportMinMax index in array to stop and report back current smallest min and current largest max
   * @return {smallest min, largest max} at indexReportMinMax
   */
  @InternalUseOnly("since created in 3.7.0")
  public static double[] testSlidingMinMax (int buffLength, int autoL, int indexResetAutoL, int newAutoL, double[][] minMaxValues, int indexReportMinMax) {
    //System.out.println("Layer.testSlidingMinMax buffLength:"+buffLength+" autoL:"+autoL+" indexResetAutoL:"+indexResetAutoL+" newAutoL:"+newAutoL+" indexReportMinMax:"+indexReportMinMax);
    SlidingMinMax forTestingMinMax = new SlidingMinMax(buffLength, autoL);
    double[] minMax = null;
    for (int i = 0; i <= indexReportMinMax; i++) {
      if (i == indexResetAutoL) {
        forTestingMinMax.setMaxActive(newAutoL);
      }
      minMax = forTestingMinMax.updateMinMax(minMaxValues[i][SlidingMinMax.COLUMN_MIN], minMaxValues[i][SlidingMinMax.COLUMN_MAX]);
    }
    return minMax;
  }

  /** Set the 3D Graph Margin Factor
   *  @param threeDGraphMarginFactor the 3D Graph Margin Factor
   */
  public static void setThreeDGraphMarginFactor (double threeDGraphMarginFactor) {
    Layer.threeDGraphMarginFactor = threeDGraphMarginFactor;
  }

  /** Set the label for the point
   *  @param label the label to set
   */
  public void setLabel (String label) {
    this.label = label;
  }

  /** This will set the font sizes based on the font factors.
   *  The font factors can be changed by the setters.
   *  @since NeXtMidas 3.9.4 for NXM-2968
   */
  void setClusterFontSizes () {
    this.numberFontSmall = new Font("Small Font",   Font.BOLD, (int) (MP.gc.getFont().getSize() * smallFactor));
    this.numberFontMedium = new Font("Medium Font", Font.BOLD, (int) (MP.gc.getFont().getSize() * mediumFactor));
    this.numberFontLarge = new Font("Large Font",   Font.BOLD, (int) (MP.gc.getFont().getSize() * largeFactor));
  }

  /** Set the factor to adjust the small font size.
   *  @param smallFactor the smallFactor to set
   */
  @InternalUseOnly
  public void setSmallFactor (double smallFactor) {
    this.smallFactor = smallFactor;
  }

  /** Set the factor to adjust the medium font size.
   *  @param mediumFactor the mediumFactor to set
   */
  @InternalUseOnly
  public void setMediumFactor (double mediumFactor) {
    this.mediumFactor = mediumFactor;
  }

  /** Set the factor to adjust the large font size.
  *   @param largeFactor the largeFactor to set
  */
  @InternalUseOnly
  public void setLargeFactor (double largeFactor) {
    this.largeFactor = largeFactor;
  }

  /** Get the small font size
   *  @return the numberFontSmall
   */
  @InternalUseOnly
  public Font getNumberFontSmall () {
    return numberFontSmall;
  }

  /** Get the medium font size
   *  @return the numberFontMedium
   */
  @InternalUseOnly
  public Font getNumberFontMedium () {
    return numberFontMedium;
  }

  /** Get the large font size
   *  @return the numberFontLarge
   */
  @InternalUseOnly
  public Font getNumberFontLarge () {
    return numberFontLarge;
  }
} // end class Layer

/**
 * Special purpose circular buffer that keep track of the smallest minimum and
 * largest maximum over a range (maxActive) of values. The active range up to the
 * length of the buffer and can be updated at any time.  Using the current algorithm
 * a full recalculation is only needed when the smallest min or largest max go
 * out of range (too old).<br>
 * <br>
 * DESIGN Decision: synchronization is not done here because resizing AUTOL (maxActive)
 * is rarely done, a safe copy of forceRescale is made at the beginning of updateMinMax
 * and the field reset right away, and even in the worst case the indices (and therefore values)
 * of the smallest min and largest max will recover within autolength values.<br>
 *
 * @author Jeanette McCall
 * @since NeXtMidas 3.7.0 for bug 2901
 */
class SlidingMinMax {
  /** In circular minMaxBuffer that holds min/max pairs the min values are stored in
   * column 0 and the max values in column 1*/
  static final int COLUMN_MIN = 0, COLUMN_MAX = 1;

  /** Number of valid min/max pairs in the buffer: 0 when empty to buffLength as soon as the buffer is first filled*/
  private int numValid = 0;
  /** Location of newest min/max pair */
  private int currentHead = -1;
  /** Number of min/max pairs to be evaluated (starting with newest and progressing back in time)
   *  to determine the smallest min and the largest max (generally the same as AUTOL auto scale length,
   *  but capped at buffLength)
   */
  private int maxActive = 0;
  /** Is recalculation being forced because of new longer autoscaleLength */
  private boolean forceRecalculation = false;
  /** Length of the circular buffer holding min/max pairs */
  private int buffLength = 0;
  /** Index of smallest in range minimum value */
  private int indexSmallestMin = 0;
  /** Index of largest in range maximum value */
  private int indexLargestMax = 0;
  /** Smallest Minimum value that is in range */
  private double currentSmallestMin = 0;
  /** Largest Maximum value that is in range */
  private double currentLargestMax = 0;
  /** Circular Buffer holding min/max pairs */
  private double[][] minMaxBuffer;

  /**
   * Constructor
   * @param buffLength length of buffer (number of min/max value pairs it can hold)
   * @param maxActive  number of values looked at in calculation of smallest min or
   *                   greatest max (capped at buffLength)
   */
  SlidingMinMax (int buffLength, int maxActive){
    this.buffLength = buffLength;
    minMaxBuffer = new double[buffLength][2];
    this.maxActive = (maxActive < buffLength) ? maxActive: buffLength;
  }

  /**
   * Add new min and max value at head of buffer and update values of smallest minimum
   * and largest maximum within the active range (based on autoscale length).  To
   * generally avoid a full recalculation the following logic is used for smallest min:
   * <pre>
   * 1) if new Min smaller than previous Min, save both it and index
   * 2) if new Min not as small as smallest min
   *    a) if index of smallest min in range - keep current value
   *    b) if index out of range (too old or AUTOL change makes out of range),
   *       loop through all in range values to recalculate
   *
   *  Similar logic for largest max
   *
   * <b>See note in class description why this is not synchronized</b>
   * </pre>
   * @param  newMin new min value
   * @param  newMax new max value
   * @return {smallest min, largest max}
   */
  double[] updateMinMax (double newMin, double newMax) {
    boolean needForceRecalculation = forceRecalculation; // make local copy to avoid threading issues
    if (forceRecalculation) forceRecalculation = false; // reset flag field right away

    currentHead = (currentHead + 1) % buffLength;
    minMaxBuffer[currentHead][COLUMN_MIN] = newMin;
    minMaxBuffer[currentHead][COLUMN_MAX] = newMax;
    if (numValid < buffLength) numValid++;

    if (needForceRecalculation) {
      recalculateSmallestMinAndIndex();
      recalculateLargestMaxAndIndex();
    }
    else {
      // Update smallest min, if first value or new smallest min
      if (numValid == 1 || newMin < currentSmallestMin) {
        indexSmallestMin = currentHead;
        currentSmallestMin = newMin;
      }
      else {
        // check if current smallest min is still within active range
        // it is not in range if we just replaced it or it is older than maxActive
        boolean smallestMinActive = (indexSmallestMin != currentHead) &&
            (((currentHead + buffLength - indexSmallestMin) % buffLength) < maxActive);
        if (!smallestMinActive) {
          // need to recalculate smallest min
          recalculateSmallestMinAndIndex();
        }
      }

      // Update largest max, if first value or new smallest min
      if (numValid == 1 || newMax > currentLargestMax) {
        indexLargestMax = currentHead;
        currentLargestMax = newMax;
      }
      else {
        // check if current largest max is still within active range
        // it is not in range if we just replaced it or it is older than maxActive
        boolean largestMaxActive = (indexLargestMax != currentHead) &&
            (((currentHead + buffLength - indexLargestMax) % buffLength) < maxActive);
        if (!largestMaxActive) {
          // need to recalculate largest max
          recalculateLargestMaxAndIndex();
        }
      }
    }
    double[] minMax = {currentSmallestMin,currentLargestMax};
    return minMax;
  }

  /**
   * Recalculate the smallest min and its index working backwards circularly through
   * the buffer starting at the current head and ending at the last index within range
   */
  private void recalculateSmallestMinAndIndex() {
    //System.out.println("SlidingMinMax.recalculateSmallestMinandIndex currentHead:"+currentHead);
    currentSmallestMin = minMaxBuffer[currentHead][COLUMN_MIN];
    indexSmallestMin = currentHead;
    // working backwards circularly in the array, get the first out of range index
    int firstIndexToSkip = ((currentHead + buffLength - Math.min(maxActive, numValid)) % buffLength);
    // back up 1 circularly from the current head
    int index = (currentHead-1+buffLength)%buffLength;
    while (index != firstIndexToSkip){
      if (minMaxBuffer[index][COLUMN_MIN] < currentSmallestMin) {
        currentSmallestMin = minMaxBuffer[index][COLUMN_MIN];
        indexSmallestMin = index;
      }
      // back up 1 circularly from the current index
      index = ((index - 1 + buffLength)%buffLength);
    }
  }

  /**
   * Recalculate the largest max and its index working backwards circularly through
   * the buffer starting at the current head and ending at the last index within range
   */
  private void recalculateLargestMaxAndIndex() {
    //System.out.println("SlidingMinMax.recalculateLargestMaxandIndex currentHead:"+currentHead);
    currentLargestMax = minMaxBuffer[currentHead][COLUMN_MAX];
    indexLargestMax = currentHead;
    // working backwards circularly in the array, get the first out of range index
    int firstIndexToSkip = ((currentHead + buffLength - Math.min(maxActive, numValid)) % buffLength);
    int index = (currentHead-1+buffLength)%buffLength;
    // back up 1 circularly from the current head
    while (index != firstIndexToSkip){
      if (minMaxBuffer[index][COLUMN_MAX] > currentLargestMax) {
        currentLargestMax = minMaxBuffer[index][COLUMN_MAX];
        indexLargestMax = index;
      }
      // back up 1 circularly from the current index
      index = ((index - 1 + buffLength)%buffLength);
    }
  }

  /**
   * Update the active range (based on autoscale length) and if needed the smallest
   * min and largest max.  To generally avoid a full recalculation the following
   * logic is used for smallest min:
   * <pre>
   * 1) if new active range is larger than the previous active range, force a
   *    recalculation of smallest min and largest max the next time a min/max
   *    pair is added (next call to updateMinMax)
   * 2) if the new active range is smaller than the previous active range, the
   *    smallest min or largest max only need updating if their indexes now fall
   *    out of range, but this will be handled when the next min/max pair is added
   *    (next call to updateMinMax)
   * 3) no updates if the new active range is the same as the old one (either
   *    because the autoscale length equals the old or both were capped to the
   *    buffer length)
   *
   * <b>See note in class description why this is not synchronized</b>
   * </pre>
   * @param  autoscaleLength new autoscale length (active range for smallest min and largest max)
   * @return maxActive original parameter or capped value if it was greater than the buffer length
   */
  int setMaxActive (int autoscaleLength) {
    int newMaxActive = (autoscaleLength < buffLength) ? autoscaleLength: buffLength;
    if (newMaxActive != maxActive) {
      int oldMaxActive = maxActive;
      maxActive = newMaxActive;
      // Only need to force recalculations from scratch if to active range is longer than
      // is was previously. If is is a shorter range, then any out of date indices will
      // be detected the next time updateMinMax is called.
      if (maxActive > oldMaxActive) {
        // Note: this structure of making the setting of the flag directly dependent on
        // maxActive having been set is done to ensure the flag is set AFTER the value
        // is valid. (i.e. do not allow JMM to reorder the instructions)
        forceRecalculation = true;
      }
    }
    return maxActive;
  }

  /**
   * Get the number of values (latest to oldest) check for smallest min and largest max
   * @return minimum of autoscale length and buffer length
   */
  int getMaxActive () {
    return maxActive;
  }

  /**
   * Get the number of valid values in the buffer (less than the buffer length
   * only when the buffer has not get been filled)
   * @return number valid values in buffer
   */
  int getNumValid() {
    return numValid;
  }
} // end class SlidingMinMax
