package nxm.sys.test;

import static nxm.sys.lib.StringUtil.isEmpty;
import static nxm.sys.lib.StringUtil.join;
import static nxm.sys.lib.StringUtil.split;
import static nxm.sys.libg.MWindow.LEFT_MOUSE_BUTTON;

import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.swing.JOptionPane;

import nxm.sys.inc.DataTypes;
import nxm.sys.inc.Drawable;
import nxm.sys.inc.Installation;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MessageHandler;
import nxm.sys.inc.PlotFile;
import nxm.sys.inc.Units;
import nxm.sys.lib.Args;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Command;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.DataFileHeader;
import nxm.sys.lib.FileName;
import nxm.sys.lib.KeyObject;
import nxm.sys.lib.KeyVector;
import nxm.sys.lib.Keywords;
import nxm.sys.lib.Message;
import nxm.sys.lib.Midas;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Position;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.Time;
import nxm.sys.libg.DragBox;
import nxm.sys.libg.ExportImage;
import nxm.sys.libg.Feature;
import nxm.sys.libg.GAlert;
import nxm.sys.libg.GDialog;
import nxm.sys.libg.GFiles;
import nxm.sys.libg.GMenu;
import nxm.sys.libg.GQuery;
import nxm.sys.libg.GValue;
import nxm.sys.libg.GWidget;
import nxm.sys.libg.Layer;
import nxm.sys.libg.LayerDrawable;
import nxm.sys.libg.LayerImage;
import nxm.sys.libg.LayerKML;
import nxm.sys.libg.MJPopup;
import nxm.sys.libg.MPlot;
import nxm.sys.libg.MWindow;
import nxm.sys.libg.Theme;
import nxm.sys.net.Rmif;
import nxm.sys.prim.plot;
import nxm.sys.prim.prescaler;
import nxm.sys.prim.rmif;

/**
  Displays Midas data in the form of line/raster/geodetic plots.

  @author Jeff Schoen
*/
public class plottester extends plot {
  protected     int     type;
  protected     int     controls       = ONCLICK;// type of mouse action that triggers a message back to the macro
  private       int     nice;
  private       int     ocm;                     // 0 for AUTO complex mode (ocm can be different than MP.mode for AUTO mode)
  private       int     oview;
  private       int     realtime       = 0;
  private       int     exit           = -1;
  private       int     openWarn       = 1;
  private       int     rescale        = 0;      // 1=layer requested to be rescaled,  2=???
  private       int     automask       = 2;
  private       Table   laytab;
  private       double  atime          = 0;      // Animate time in seconds
  private final double  atime1         = 0;      // Animate time minimum   (00:00:00)
  private final double  atime2         = 86400;  // Animate time maximum   (24:00:00)
  private final double  atinc          = 1;      // Animate time increment (00:00:01)
  private       boolean focus;
  private       boolean exiting;
  private       boolean unitsInit;
  private       rmif    rmif;
  private       Table   zoomTo         = null;
  private       int     pointerCount;
  private       double  pointerTime;
  private       Message pointerMsg;
  private       Table   mtag           = null;   // store MTAG results names. Since 3.1.1.
  private       boolean imageFlag      = false;  // determine if the view needs to be redrawn after plot (for Bug 467)
  private       int     compression    = 0;      // RMIF compression setting
  private       Table   setupTable     = null;   // since 3.9.1 for NXM-3078
  private       boolean needScaleTable;  // since 3.9.1 for NXM-3078

  /** Return exit flag */
  public static final int EXIT_RETURN  = 0x1;
  /** Menu exit flag */
  public static final int EXIT_MENU    = 0x2;
  /** Message exit flag */
  public static final int EXIT_MESSAGE = 0x4;
  /** Window exit flag */
  public static final int EXIT_WINDOW  = 0x8;
  /** All exit flag */
  public static final int EXIT_ALL     = 0xF;
  /** Auto exit flag */
  public static final int EXIT_AUTO    = 0x10;
  /** List of all exit flags */
  public static final String exitList  = "RETURN,MENU,MESSAGE,WINDOW,AUTO";

  /** Open Warn Option: File Not Found gives popup message. */
  public static final int OPENWARN_POPUP  = 0x1;
  /** Open Warn Option: File Not Found gives prints message.  */
  public static final int OPENWARN_PRINT  = 0x2;
  /** The list of Open Warn Message Options. Since 3.5.2 */
  public static final String openWarnList = "POPUP,PRINT";

  /** Pointer Message Option: Send mouse clicks. */
  public static final int ONCLICK  = 0x1;
  /** Pointer Message Option: Send mouse moves.  */
  public static final int ONMOTION = 0x2;
  /** Pointer Message Option: Send mouse drags.  */
  public static final int ONDRAG   = 0x4;
  /** The list of Pointer Message Options. */
  public static final String pointerMessageList = "Click,Motion,Drag";
  private final Map<String,MessageHandler> mimic                     = new LinkedHashMap<>(1); // see note in setMimic(..)
  private final boolean                    ENABLE_JEFF_TITLE_CHANGES = false; // NTN: backed out Bug 1420 from NXM 2.5.5, set to true to enable
  private       StealsMouseClick           stealNextClick            = null; // Pointer to the listener that will steal the next mouse click

  /** RMIF compression setting for PLOT. */
  public static String compressionList = "Off,Data,Plot";

  /** The plotter class. */
  public MPlot MP;
  /** file use for actual plot data - when prescaling, especially in a pipe, this may be a copy of the FILE= file */
  private String inFile;
  /** holds the custom scale values defined by either arguments passed or from prescaler
      @since NeXtMidas 3.9.1 */
  private Table scaleTable;
  /** This is the prescale inner class that is used to process everything prescale related
      @since NeXtMidas 3.9.1 */
  private final PrescaleSetup prescaleSetup = new PrescaleSetup();

  /** default constructor */
  public plottester () {}

  @Override
  public int open () {
    type = MA.getSelectionIndex("TYPE",PlotFile.typeList,type);
    String controlsStr = MA.getS("CNT","DEF");
    if (controlsStr.equals("DEF")) controlsStr = MA.getS("/XCNT","DEF");  // added check for /XCNT in 3.1.1
    setXCnt(controlsStr);
    if (MA.find("/MTAG")) {
      String str = MA.getCS("/MTAG", null);
      if(str != null && str.startsWith("(")) {
        setMTagXMidas(str);
      }
      else {
        setMTag(MA.getTable("/MTAG", null));
      }
    }
    realtime = MA.getL("/RT",0);
    nice = (M.pipeMode==Midas.POFF)? 0 : 1;
    nice = MA.getL("/NICE",nice);
    if (M.pipeMode==Midas.PINIT) exit=0; else exit=EXIT_ALL;
    exit = MA.getOptionMask("/EXIT",exitList,exit);
    openWarn = MA.getOptionMask("/OPENWARN",openWarnList,OPENWARN_POPUP|OPENWARN_PRINT); // Bug 2867 | NXM-4061
    focus = MA.getState("/FOCUS",(M.macro==null));
    MP = new MPlot("Plot",this);
    MP.setButtonAction(MA.getTable("BUTTONACTION")); // since 3.5.2 Bug 2868
    MW = MP;
    if (Theme.getOption(Theme.SWINGWIDGETS) && Theme.getOption(Theme.REALTIME))
      M.warning("Use of SWING-based widgets and menus in conjunction with " +
        "REALTIME drawing mode for PLOTs can cause widgets/menus to disappear");
    MP.open();
    MP.setAxisIndex(MA.getState("/INDEX", false));
    MP.setAxisIndex(MA.getState("/AXISINDEX", MP.isAxisIndex()));
    if (MA.find("/AXISBUFFER")) MP.setAxisBuffer(MA.getD("/AXISBUFFER"));  // Bug 2478, since 3.1.0
    String specifiedView = MA.getS("VIEW");
    if (specifiedView.length() > 0) { MP.setView(specifiedView); }
    oview = MP.view;
    String defCM = "";
    if (MA.getState("/DB")) defCM="10LOG";
    if (MA.getState("/DB2")) defCM="20LOG";
    MP.setMode(MA.getS("CM",defCM)); ocm = MP.mode;
    String specifiedAxis = MA.getS("AXIS");
    if (specifiedAxis.length() > 0) { MP.setAxis(specifiedAxis); }
    // if interactive mode then add ONAXIS to default
    if (ENABLE_JEFF_TITLE_CHANGES && M.macro==null) MP.setReadout("+ONAXIS|-ONTITLE"); // also remove ONTITLE from default
    else if (M.macro==null) MP.setReadout("+ONAXIS");
    String readoutString = MA.getS("READOUT", "");
    MP.setReadout(MA.getS("/READOUT", readoutString));
    if (MA.find("/DATEFORMAT")) MP.setDateFormat(MA.getS("/DATEFORMAT"));  // since 3.3.3
    if (MA.find("DRANGEUNITS")) MP.setDRangeUnits(MA.getS("DRANGEUNITS"));
    if (MA.find("/PRECISION")) MP.setPrecision(MA.getL("/PRECISION",-1));
    String opts = MA.getS("OPTIONS");
    MP.setOptions(opts);
    // the next two lines enable the MPlot Option DragZoom if the user has not set it
    if (opts != null && opts.contains("DRAGZOOM")) { } // do nothing, user has set DRAGZOOM
    else MP.setOptions("+DRAGZOOM");                      // Enabled by default
    if (MA.getState("/ALL")) MP.setOptions("+ALL|BS");
    if (MA.find("Y1")||MA.find("YMIN")||MA.find("Z1")||MA.find("ZMIN")) MP.scale &= ~MPlot.S_MIN;
    if (MA.find("Y2")||MA.find("YMAX")||MA.find("Z2")||MA.find("ZMAX")) MP.scale &= ~MPlot.S_MAX;
    MP.setScale(MA.getS("SCALE"));
    if (MA.find("RANGE")) MP.setRange(MA.getD("RANGE"));
    if (MA.find("MINRANGE")) MP.setMinRange(MA.getD("MINRANGE"));
    if (MA.find("MAXRANGE")) MP.setMaxRange(MA.getD("MAXRANGE"));
    laytab = MA.getTable("/LAYER",MA.getTable("LAYER"));
    automask = (M.pipeMode==Midas.POFF)? -1 : 2;
    automask = MA.getL("AUTOM",automask);
    setMimic(MA.getS("MIMIC"), true);
    MP.addTo(this);
    resize(1);
    setupTable = MA.getTable("SETUP");
    if (setupTable!=null) KeyObject.setKeys(this,setupTable,args,KeyObject.SOFT);
    if(MA.find("SCALETABLE")) scaleTable = MA.getTable("SCALETABLE");

    inFile = MA.getRaw("FILE");
    boolean waitingOnPrescale=false;
    // Prescale should not be run if we have already received scale values
    if (MA.find("/PRESCALE")) {
      waitingOnPrescale = prescaleSetup.open();
    }
    if(!waitingOnPrescale) {
      openFile(inFile);
      prescaleSetup.processScaleTable();
    }

    //moved setting scaleAvg here so after layer created so circular min/max can be made for layer - since 3.7.0 for bug 2901
    MP.setScaleAvg(MA.getL("AUTOL", MP.getScaleAvg())); // Need to set a default for the value. Since NeXtMidas 3.8.1
    MP.setMult(MA.getO("/XMULT",new Data(0),false), 'X');
    MP.setMult(MA.getO("/YMULT",new Data(0),false), 'Y');
    MP.setZoomAdjust(MA.getF("/ZOOMADJ", MP.getZoomAdjust()));
    MP.setZoomMult(MA.getF("/ZOOMMULT", MP.getZoomMult()));
    MP.setDblClickZoom(MA.getState("/DBLCLICKZOOM", MP.getDblClickZoom()));
    MP.setMouseWheelZoom(MA.getState("/MOUSEWHEELZOOM", MP.getMouseWheelZoom()));
    Table tmp = MR.getTable("ENV.KEYMAP");
    MP.setKeyMapTable( (tmp!=null)? tmp.getTable("PLOT"): null); // get the key mapping info from env settings
    if (MA.find("/LEGEND")) parseLegend();
    Table ftab = MA.getTable("FEATURES");
    if (ftab!=null) MP.loadFeatures(ftab);
    if (MA.find("ZOOMTO")) {  // Zoom when opening (Bug 2353)
      Table zoomto = MA.getTable("ZOOMTO");
      if (zoomto != null && zoomto.size()>0) zoomTo = zoomto;
    }
    if (MA.find("/DRAGDROP")) MP.setDragAndDrop(MA.getState("/DRAGDROP"), MQ);  // since 3.3.0 - Bug 2120
    if (!MA.find("MIMIC") && MA.find("/USEMOUSEPOSITION")) MP.setUseMousePosition(MA.getState("/USEMOUSEPOSITION")); // Since 3.8.1 to allow Google style zoom NXM-3261
    return NORMAL;
  }

  /** Handle X-Midas style MTAG switch for XGEOPLOT.
   *  @param str the xgeoplot string of mtag values
   */
  private void setMTagXMidas (String str) {
    try {
      String tableStr = str.toUpperCase().replaceAll("[(]", "{").replaceAll("[)]", "}");
      tableStr = tableStr.replaceAll(":", "=");
      final Table tab = new Table(tableStr);
      setMTag(tab);
    } catch (Exception e) {
      M.printStackTrace("Unable to parse X-Midas style /MTAG string=" + str + ".", e);
    }
  }

  /** Set the plot bounds (and the table which keeps track of user settings) from command-line arguments */
  private void setBoundsFromArgs () {
    if (MA.find("X1"))   { MP.setUserLimits("X1",MA.getD("X1"),   true); MP.orx1 = MA.getD("X1"); }
    if (MA.find("XMIN")) { MP.setUserLimits("X1",MA.getD("XMIN"), true); MP.orx1 = MA.getD("XMIN"); }
    if (MA.find("X2"))   { MP.setUserLimits("X2",MA.getD("X2"),   true); MP.orx2 = MA.getD("X2"); }
    if (MA.find("XMAX")) { MP.setUserLimits("X2",MA.getD("XMAX"), true); MP.orx2 = MA.getD("XMAX"); }
    if (MA.find("Y1"))   { MP.setUserLimits("Y1",MA.getD("Y1"),   true); MP.ory1 = MA.getD("Y1"); }
    if (MA.find("YMIN")) { MP.setUserLimits("Y1",MA.getD("YMIN"), true); MP.ory1 = MA.getD("YMIN"); }
    if (MA.find("Y2"))   { MP.setUserLimits("Y2",MA.getD("Y2"),   true); MP.ory2 = MA.getD("Y2"); }
    if (MA.find("YMAX")) { MP.setUserLimits("Y2",MA.getD("YMAX"), true); MP.ory2 = MA.getD("YMAX"); }
    if (MA.find("Z1"))   { MP.setUserLimits("Z1",MA.getD("Z1"),   true); MP.orz1 = MA.getD("Z1"); }
    if (MA.find("ZMIN")) { MP.setUserLimits("Z1",MA.getD("ZMIN"), true); MP.orz1 = MA.getD("ZMIN"); }
    if (MA.find("Z2"))   { MP.setUserLimits("Z2",MA.getD("Z2"),   true); MP.orz2 = MA.getD("Z2"); }
    if (MA.find("ZMAX")) { MP.setUserLimits("Z2",MA.getD("ZMAX"), true); MP.orz2 = MA.getD("ZMAX"); }
  }

  private void parseLegend() {
    Table   table  = MA.getTable("/LEGEND",null,Args.NO_ERROR);
    GWidget legend = MP.createLegend();
    if (table != null) {
      KeyObject.setKeys(legend,table);
    } else { // Try /LEGEND or /LEGEND=BR
      String legStr = MA.getS("/LEGEND","TL");
      if (legStr == null || legStr.length() == 0) legStr="TL";
      MP.getLegend().setCorner(legStr);
    }
  }

  @Override
  public int process () {

    // If we are requiring a SETUP table be available for scaling, do no processing until we have it
    // This is used when a pre-scaler is used in a pipe - since 3.9.1 for NXM-3078
    if (needScaleTable) {
      if(!prescaleSetup.processPrescalar()) return NOOP;
    }

    if (MP.status==MWindow.INIT) {
      return NOOP;  // plot not up yet
    }
    if (exiting) return FINISH;
    if (focus) focus = !MP.getFocus();      // try to get focus ?
    // disconnect from pipes if nice and not shown
    if ((nice==2 && MP.status==MWindow.ICON) ||
        (nice==-2 && MP.status==MWindow.FRONT)) {
      nice = -nice;
      for (int n=1; n<MP.layers.size(); n++) {
        Layer lay = (Layer)MP.layers.get(n);
        if (lay.pf!=null) lay.pf.connect(nice);
      }
    }
    int stat=0, mode=MP.dataMode(); // check for new data ?
    if (mode!=0) stat = getData();  // get available data
    if (mode<0) { }                 // don't draw new data
    else if (stat>0) refresh(stat); // draw new data
    else if (stat==0 && MP.needRefresh()) { // layer induced refresh
      Time.sleep(0.1); // wait for others then refresh
      // if feature driven change, don't redo BS data
      refresh( MP.needLayerRefresh()? Drawable.FULL : Drawable.DATA );
    }
    if (zoomTo != null) { processMessage(new Message("ZOOM",0,zoomTo)); zoomTo=null; }  // initial Zoom done here to ensure original vals are set
    if (!ENABLE_JEFF_TITLE_CHANGES && pointerCount>0 && Time.current()-pointerTime>0.1) updatePointer(); // NTN: backed out Bug 1420 changes introduced in NXM 2.5.5
    if (MP.needCrossHairRefresh()) MP.updateCursor();  // Bug 2327
    if (stat<=0) return NOOP;     // nothing to do
    return NORMAL;
  }

  @Override
  public int close () {
    MP.features.clear();
    closeFile("*");
    MP.close();
    return NORMAL;
  }

  @Override
  public int processReady () { return 1; }

  /** Open the named files. This is identical to <code>openFile(names)</code>.
      @param names The pipe ('|') delimited names of the files to open.
      @see #openFile(String)
  */
  public void setFile (String names) {
    openFile (names);
  }

  /** Open the named files.
      @param names The pipe ('|') delimited names of the files to open.
   */
  public void openFile (String names) {
    openFile(names,null);
  }

  /** Open the named files.
      @param names The pipe ('|') delimited names of the files to open.
      @param newargs Table of parameters to MERGE with the current Args
      @since NeXtMidas 3.3.1 (added Args support)
   */
  public void openFile (String names, Table newargs) {
    MP.setAxisIndex(MA.getState("/INDEX", false));
    MP.setAxisIndex(MA.getState("/AXISINDEX", MP.isAxisIndex()));
    MP.viewLimits = new Table(); // clear out any values in the temp bounds table
    if (names == null) names = "";
    Layer lay; BaseFile bf;
    if (newargs!=null) updateArgs(newargs);  // since 3.3.1 (Bug 2710)
    Args.SplitArg sa = MA.getSplitArg(names,'|');
    HashSet<String> tags = new HashSet<>(); // Set of tags used on THIS call to openFile
    for (int i=0; i<sa.getFields(); i++) {
      String fname = sa.getField(i);
      if (fname.length()==0) continue;

      // BEGIN Bug 2460 Changes to handle tables passed in as results
      if (MR.containsKey(fname.toUpperCase())) {
        Object nameO = MR.get(fname.toUpperCase());
        if (nameO instanceof Table) {
          fname = nameO.toString();  // Result is a Table
        }
        else if (nameO instanceof String )  {
          if (((String)nameO).startsWith("{") || ((String)nameO).endsWith(".tbl")) {
            fname = (String) nameO;  // Result is a Table in String form
          }
        }
      }
      // END Bug 2460 Changes

      Table tab = null;
      if (fname.startsWith("{") || fname.endsWith(".tbl")) {  // table driven
        tab = Convert.o2t(fname,M);
        String fn = tab.getS("FILENAME");
        if (fn!=null) {    // file driven with table modifiers
          fname = fn;
          tab.remove("FILENAME");
        } else {
          lay = Layer.getLayer(tab);
          KeyObject.setKeys(lay,tab,args,0);
          if (MP.getLayers()!=null && MP.getLayers().findKey(lay.getName()) >= 0) {
            M.warning("PLOT: Multiple layers found with TAG="+lay.getName()+
                ". Use new TAG=<tag> qualifier in order to show both layers.");
          }
          MP.addLayer(lay);
          continue;
        }
      }
      bf = BaseFile.getInstanceFor(this,fname);
      if (!fname.startsWith("_") && !bf.find(-1)) {
        if ((openWarn & OPENWARN_POPUP) != 0) { // since 3.5.2 Bug 2867
          if (Theme.getOption(Theme.SWINGWIDGETS)) {
            MJPopup popup = MJPopup.createOk(MP.getPanel(), getID()+": NoFile",
                            JOptionPane.ERROR_MESSAGE, "File "+bf.getName()+" not found");
            popup.display(0);
          }
          else {
            new GAlert(MP,"NoFile","File "+bf.getName()+" not found","OK",1,0,this);
          }
        }
        if ((openWarn & OPENWARN_PRINT) != 0) { // since 3.5.2 Bug 2867
          M.warning("PLOT: File "+bf.getName()+" not found");
        }
      }
      else if (bf instanceof PlotFile) {
        bf.setFlags(BaseFile.ALWAYSREOPEN);
        lay = new Layer();
        lay.setFile((PlotFile)bf);
        if (tags.contains(lay.pf.getTag())) {
          // Issue warning about duplicate tags in THIS call to openFile, duplicates from other
          // calls are ignored since it is assumed that the user intended to override an old layer
          M.warning("PLOT: Multiple layers found with TAG="+lay.pf.getTag()+". Use new TAG=<tag> "
                   +"qualifier in order to show both layers.");
        }
        tags.add(lay.pf.getTag());

        // If the Layer is in a Group, don't call MP.addLayer - Since 3.5.0  (Bug 2792)
        // This seems not very efficient, but only happens when opening a Layer.
        Table layquals = Convert.o2t(bf.getQualifier("LAYER"));
        if (layquals!=null && (layquals.get("GROUP") != null)) {
          // do nothing
        }
        else {
          MP.addLayer(lay.pf.getTag(),lay);
        }
        openLayerFile(lay,lay.pf,tab);
      }
      else {
        M.warning("File "+bf.getName()+" is not a PlotFile");
      }
    }
  }

  /** Update the current Args with new ones passed in to openfile(). <br><br>
      NOTE: This does a merge/update, not a complete wipe and replace.
      <pre>
        Though the passed-in args are merged with the current args for use
        later in the Layer class (or plot restarts), the only parameters
        handled in this method are:
           /ALL
           /NICE
        The other arguments can be passed in before/after the OPENFILE (or
        added in a future version, if needed)
      </pre>
      @param newArgs The Args.
      @since NeXtMidas 3.3.1
   */
  private void updateArgs (Table newArgs) {
    if (newArgs==null) return;

    String[] keys = newArgs.getKeys();

    for (String key : keys) {
      key = key.toUpperCase();
      args.put(key, newArgs.get(key).toString());  // merge the new args into the args object

      // handle special cases for Args (switches) that are handled in open()
      if (key.equals("/ALL")) {
        MP.setOptions("+ALL|BS");
      } else if (key.equals("/NICE")) {
        nice = newArgs.getL("/NICE",nice);
      }
    }
  }

  /** Opens the file for a layer, initializes the layer and adds it to MP.
      In 3.1.0 synchronized was added to avoid collisions with {@link #getData()}
      for Bug 2457, BUT it caused GUI to lockup under SWING only GRAPHICS mode
      (Bug 2537), so it has been removed in 3.1.1.
      @param oldLayer The old layer. This will typically be a temporary one created
                      by openFile(..) but may sit around in the MP.layers file for
                      some time in the case of an unopened pipe with /NICE.
       @param pf      The plot file (not opened yet).
       @param flaytab The file's layer table (?)
   */
  private void openLayerFile (Layer oldLayer, PlotFile pf, Table flaytab) {
// System.out.println("PLOT.openLayerFile lay="+oldlay+" pf="+pf+" ["+getID()+"]");
    MP.viewLimits = new Table();  // clear out any existing view bounds settings

    String layerName  = oldLayer.name;
    int    layerIndex = MP.layers.find(layerName); // ==1 indicates first data layer

    // ==== OPEN FILE FOR LAYER ================================================
    if (nice>0 && !pf.find(-1)) return;
    if (!pf.open(BaseFile.OPTIONAL)) return;

    // ==== GET QUALIFIER TABLE ================================================
    if (flaytab==null) {
      flaytab = Convert.o2t(pf.getQualifier("LAYER"),M);
    }
    if (flaytab==null && pf instanceof DataFile) {
      DataFile df = (DataFile)pf;
      flaytab = Convert.o2t(df.keywords.get("PLOTLAYERTABLE"),M);
    }
    if (flaytab==null) flaytab=laytab;
    else if (laytab!=null) flaytab.setPrevious(laytab);

    // ==== GET LAYER INSTANCE =================================================
    // Since NeXtMidas 3.3.2 all of the DataFile-specific checks have moved into
    // DataFile.getLayer(..) so we only have a single call here.
    Layer lay = Layer.getLayer(flaytab, pf, type);
    if (lay == null) {
      M.warning("File type not supported by plotter: "+pf);
      pf.close();
      return;
    }

    // ==== INIT LAYER INSTANCE ================================================
    // Since NeXtMidas 3.3.2 all of the layer-specific initialization has moved
    // into Layer.init(..) or the protected init(..) function in each layer.
    // This makes the layers more like instances of BaseFile where we construct
    // them using the default constructor and then call init(..) to do all of the
    // initialization.
    lay.init(this, layerName, layerIndex, pf, flaytab, oldLayer);

    if (laytab  != null  ) KeyObject.setKeys(lay,laytab,args,0);
    if (flaytab != laytab) KeyObject.setKeys(lay,flaytab,args,0);
    MP.addLayer(lay);

    // ==== SET DEFAULTS IF GIVEN BASE LAYER ===================================
    // Set CM= and VIEW= to their default settings unless the user provided
    // them on the command line, in which case ocm and/or oview will be non-zero.
    // Also set units unless already initialized.
    if (lay.isBaseLayer()) {  // check directly if layer is base layer - since 3.6.0 for bug 2894
      adjustPlotNewBaselayer(lay);
    }

    // ==== DONE (RESCALE AS REQUIRED) =========================================
    if (MP.getZoomLevel()==0) rescale = 1;
  }

  /**
   * Sets the layer that functions as the base layer
   * @param layName name of the layer that is to be the base layer
   * @since 3.6.0 for Bug 2894
   */
  public void setBaseLayer(String layName){
    Layer lay = (Layer)MP.layers.get(layName);
    if (lay != null) {
      setBaseLayer(lay);
    }
    else {
      M.error("Layer "+layName + " does not exist - not updating Base Layer");
    }
  }

  /**
   * Sets the layer that functions as the base layer
   * @param lay layer that is to be the base layer
   * @since 3.6.0 for Bug 2894
   */
  public void setBaseLayer(Layer lay){
    if (!lay.isBaseLayer()) {
      Layer previousBaseLayer = MP.getBaseLayer();
      if (previousBaseLayer != null) previousBaseLayer.setBaseLayer(false);
      MP.setBaseLayer(lay);
      adjustPlotNewBaselayer(lay);
    }
  }

  /**
   * Adjusts the plot values based on the new base layer
   * @param lay layer that is the base layer
   * @since 3.6.0 for Bug 2894 - move this block of code into method for reuse
   */
  private void adjustPlotNewBaselayer(Layer lay) {
    if (ocm==0) {
      MP.setMode(determineComplexMode(lay), false);
    }

    if (MP.getZoomLevel() == 0) {
      MP.rescale(true); // make sure new file bounds are used (BUG 2151)
    }

    if (oview==0) {
      MP.setView(lay.getDefaultView());
    }

    if (!unitsInit) {
      MP.setXYZUnits(lay.getXUnits(),lay.getYUnits(),lay.getZUnits(),true); // use new 4 parameter method with refresh=true - Since 3.6.0 for Bugs 2894 & 2431
      unitsInit = true;
    }
  }

  /** Close the named file.
      @param match The name of the file to close or <code>"*"</code> to close all files.
  */
  public void closeFile (String match) {
    MP.viewLimits = new Table(); // clear out any values in the temp bounds table
    for (int n=MP.layers.size()-1; n>0; n--) {
      String layerName = MP.layers.getKey(n);
      Layer  lay       = (Layer)MP.layers.get(n);

      if (match.equals("*") || layerName.equalsIgnoreCase(match)) {
        MP.removeLayer(layerName);
        if (lay.pf!=null) lay.pf.close();
      }
      else if (lay instanceof LayerDrawable)  ((LayerDrawable)lay).removeDrawable(match);
    }

    if (state==CLOSE || state==ABORT) return;
    MP.reset(-1);
    // Zoom all the way back out if you closed the last file
    if (MP.layers.size()==1) {
      MP.unzoom(0); //TBD -- make this an option
    }
    refresh();
  }

  /** Removes a named feature from the plot.
      @param match The name of the feature to remove or <code>"*"</code> to remove all features.
  */
  public void removeFeature (String match) {
    KeyVector features = MP.getFeatures();   // Names of all the features.
    boolean   all      = match.equals("*");  // Remove all if match is "*".
    for (int i=features.size()-1; i>=0; i--) {
      String featureName = features.getKey(i);
      if (all || featureName.equalsIgnoreCase(match)) MP.removeFeature(featureName);
    }

    // check for the Feature in a LayerDrawable
    for (int n=MP.layers.size()-1; n>0; n--) {
      Layer  lay  = (Layer)MP.layers.get(n);
      if (lay instanceof LayerDrawable)  ((LayerDrawable)lay).removeDrawable(match);
    }
  }

  /** synchronized was added 3.1.0 for Bug 2457 to avoid collisions in {@link #openLayerFile(Layer, PlotFile, Table)}.
      BUT it caused GUI to lockup under SWING only GRAPHICS mode (Bug 2537), so it has been removed in 3.1.1.
    */
  private int getData () {
    int status=0;
    boolean autoscale;
    autoscale = (rescale==0) && ((MP.scale&MPlot.S_AUTO)!=0) && MP.getScaleAvg()>0;
    for (int n=1; n<MP.layers.size(); n++) {
      Layer lay = (Layer)MP.layers.get(n);
      if (lay == null) continue;          // since 3.7.0 for Bug 2991 and Bug 2457
      PlotFile pf = lay.pf;
      // Layerkml does not need the plot file to be open but it does need to be defined since 3.9.1 (is there a case where the plot file is not defined?)
      if (((pf!=null) && (pf.isOpen() || lay instanceof LayerKML)) || lay instanceof LayerDrawable ) {
        int stat = lay.getData(0);
        if (stat>0) {
          status = stat;
          if (autoscale && !lay.isPaged) lay.findRange();
        }
        else if (stat==Drawable.DONE) pf.close();
        if (exit==EXIT_AUTO && stat<=0) exiting = true;
      }
      else if ((pf!=null) && (nice>0)) {
        openLayerFile(lay,pf,null);
      }
      else if (autoscale && lay.getNeedsRescale()) {
        rescale = 1;
        status  = Drawable.DATA;
      }
    }
    if (status<=0 && !imageFlag) { }
    else if (rescale>0) {   // static plotter rescale
      MP.findRange();
      setBoundsFromArgs();
      MP.applyUserBounds("OUTER");  // BUG 1520: User changes over-ride data defaults
      if (MA.find("XU")) MP.setXUnits(MA.getS("XU"));
      if (MA.find("YU")) MP.setYUnits(MA.getS("YU"));
      if (MA.find("ZU")) MP.setZUnits(MA.getS("ZU"));
      MP.useRange();
      MP.applyUserBounds("VIEW");  // BUG 1520: User changes over-ride data defaults
      rescale=2;
    }
    else if (autoscale && status!=Drawable.FAST && MP.getZoomLevel()==0) {
      if (MP.autoScale(automask,false)) rescale=2; // realtime plotter autoscale
    }
    if (imageFlag) {
      MP.setView(MP.view, false, true);
      imageFlag = false;
    }
    return status;
  }

  /** Refresh the plot. This is the same as <code>refresh(Drawable.FULL)</code>.
      @see #refresh(int)
  */
  private void refresh () {
    refresh(Drawable.FULL);
  }

  /** Refresh the plot. This is the same as <code>refresh(Drawable.FULL)</code>.
      @param flag The flag indicating what needs to be refreshed.
      @see Drawable#FULL
      @see Drawable#DATA
      @see Drawable#DATAX
      @see Drawable#FAST
  */
  private void refresh (int flag) {
    if (state==STOP||state==CLOSE||state==ABORT||exiting) return;
    if (flag!=Drawable.DATAX) MQ.remove("REFRESH");
    if (rescale>0) flag=Drawable.FULL;
    if (MP.status==MWindow.FRONT) MP.refresh(flag);
    if (rescale==2) rescale=0;
  }

  /** This will resize the plot.
      @param mode <i>This parameter is currently ignored.</i>
  */
  private void resize (int mode) {
    MP.resize();
  }

  @Override
  public boolean isRealTimeMode () {
    return realtime > 0;
  }

  @Override
  public boolean isReverseColorsMode () {
    return MA.getState("/REVERSECOLORS",false);
  }

  /** Sets the image flag. This is currently used as a work-around for an issue
      with {@link LayerImage} (see Bug 467) and should not be used elsewhere.
      @param imgFlag  image flag
      @since NeXtMidas 3.3.2
   */
  @InternalUseOnly
  public void setImageFlag (boolean imgFlag) {
    this.imageFlag = imgFlag;
  }

  /** Whether to allow drag and drop.
      @param allow true to allow drag-n-drop.
      @since NeXtMidas 2.8.0
   */
  public void setDragAndDrop (boolean allow) {
    MP.setDragAndDrop(allow, MQ);
  }

  /** Set the mouse action that triggers a message. Used in conjunction with the /MTAG switch
      to update results based on mouse movements.
      @param mode {@link #ONCLICK},{@link #ONDRAG},{@link #ONMOTION} or 0 (none)
      @since NeXtMidas 3.1.1
   */
  public void setXCnt (int mode) {
    // This method added in 3.1.1 for Bug 2576 to support XMPY syntax
    if (mode==controls) return; // nothing to do
    controls = mode;
  }

  /** Set the mouse action that triggers a message. Used in conjunction with the /MTAG switch
      to update results based on mouse movements.
      @param mode "ONCLICK", "ONDRAG", "ONMOTION", "DEF", or "NONE"
      @since NeXtMidas 3.1.1
   */
  public void setXCnt (String mode) {
    setXCnt(Parser.mask(pointerMessageList, mode, controls));
  }

  /** Set the Dimension and result name for mouse actions. When used in conjunction with CNT= or /XCNT,
      this table shows what values to put in what results. <br><br>
      For example, "plot/xcnt=click/mtag={x="cfreq"} sinewave" will, when the mouse is left-clicked,
      put the value of the X coordinate below the mouse into the results table in the result "cfreq".
      @param tbl  Valid keys are "X","Y","Z", and "X+Y"
      @since NeXtMidas 3.1.1
   */
  public void setMTag (Table tbl) {
    mtag = tbl;
  }

  /** Get the Table containing the Dimension and result name for mouse actions, or null or "{}" if not set
      @return  Table containing the Dimension and result name for mouse actions
      @since NeXtMidas 3.1.1
   */
  public Table getMTag () {
    return mtag;
  }

  @Override
  public int processMessage (Message msg) {
    return processMessage(msg, false);
  }

  /** Process a message. In order to prevent race conditions, if this method is
      not called from the plot thread, then it will likely get re-queued. Exceptions
      are when direct==true and the PAINT, POINTER, and DRAG messages (for performance)
      @param msg    The message.
      @param direct If true, do not re-queue most messages
      @return The result (see {@link #processMessage(Message)}).
      @since NeXtMidas 3.1.1
   */
  public int processMessage (Message msg, boolean direct) {
    String text;
    Table  quals = Convert.o2t(msg.quals);
    // translate hot-key equivalents
    switch (msg.name) {  // convert to switch statement in 3.8.1
      case "KEYPRESS":
        text = (String)msg.data;
        if ("M".equalsIgnoreCase(text)) {
          msg.name = "MENU";
        } else if ("A".equalsIgnoreCase(text)) {
          MP.setReadout("~INDEX");
          return NOOP;
        // Code below moved to Mplot keyPressed
        //} else if (text.equalsIgnoreCase("X") || text.equalsIgnoreCase("Y")) {
        //  doXYCut (text,MP.px,MP.py, null);
        } else if ("Enter".equalsIgnoreCase(text)) {
          if ((exit&EXIT_RETURN)!=0) MP.cancel=exiting=true;
          return NOOP;
        }
        break;
      case "KEYRELEASE":
        return NOOP;
      case "DRAG":
     // fire off those events which can run, even when the pipe is paused
        if ((M != null) && (M.pipeMode == Midas.PPAUSE)) {
          MP.refresh();
          sendMouseMessage(msg, ONDRAG);
        }
        break;
    }

    // This check added in 3.1.1 to reduce race conditions caused by Threading collision (BUG 2537)
    if (!direct && !thisIsMe()) {
      switch (msg.name) {
        case "PAINT":    // FALLTHROUGH
        case "POINTER":  // FALLTHROUGH
        case "DRAG":     // FALLTHROUGH
        case "PAN":      // FALLTHROUGH
        case "PANOBS":   // FALLTHROUGH
        case "PANGEO":   // (not on official list of message names)
          // do nothing
          break;
        default:
          if(MQ!=null)MQ.put(msg);
          return NOOP;
      }
    }
    if ("PAINT".equals(msg.name)) {
      //MP.paint((Graphics)msg.data);
      MP.refresh(); // Since 3.9.0 to solve graphical timing issues (NXM-3213)
    }
    else if ("KEYPRESS".equals(msg.name)) {
      // do nothing
    }
    else if ("RESIZE".equals(msg.name)) {
      setCursorAndRefresh(null, false); // resets the cursor
      resize(0);
      if (M.pipeMode == Midas.PPAUSE) MP.cancel=false;   // ensure repaints when pipe is paused (Bug 1545)
    }
    else if (MP!=null && MP.processMessage(msg, getMessageHandler(), this)==1) {
      // it was processed by the MPlot class
      // the configuration menus are processed there
    }
    else if (super.processMessage(msg)!=NOOP) {
      // it was processed in the Command class
    }
    else {
      int flags;
      Table tbl;
      switch (msg.name) { // Restructured to use switch/case in 3.7.2 (NXM-3034)
        case "POINTER":   // FALLTHROUGH
        case "DRAG":
          if (msg.info==9) return 0; // mouse enter - only update on motion
          if (ENABLE_JEFF_TITLE_CHANGES) {
            msg.data = MP.getPosition();
            MP.formatReadout(msg.info);
            MP.updateCursor();
            if (msg.name.equals("DRAG")) {
              setCursorAndRefresh("Crosshair", true);
              sendMouseMessage(msg, ONDRAG);
            }
            else {
              setCursorAndRefresh(null, false); // resets the cursor
              msg.name="MARK";
              if (MP.px>MP.ix1 && MP.px<MP.ix2
               && MP.py>MP.iy1 && MP.py<MP.iy2) sendMouseMessage(msg, ONMOTION);
            }
          }
          else { // NTN: backed out Bug 1420 changes introduced in NXM 2.5.5
            if (++pointerCount == 1) pointerTime=Time.current();
            pointerMsg = msg; // for time decimated processing
          }
          break;
        case "FOCUS":  // FALLTHROUGH
        case "MOVED":
          // do nothing yet
          break;
        case "HOVER":
          msg.data = MP.getPosition(MP.px,MP.py);
          sendMouseMessage(msg, ONCLICK);
          break;
        case "BUTTON":
          if ((msg.info%100)!=LEFT_MOUSE_BUTTON) {
            // do nothing as further "BUTTON" processing will be handled in MPlot.java
          }
          else {
            // if (msg.info%100)==LEFT_MOUSE_BUTTON, process as MARK message
            processMarkMessage(msg);
          }
          break;
        case "MARK_MESSAGE":
          processMarkMessage(msg);
          break;
        case "REFRESH":
          MP.resetForRefresh();
          refresh();
          break;
        case "HOLDREFRESH":
          MP.setHoldRefresh((String) msg.data);
          break;
        case "RESIZE":
          setCursorAndRefresh(null, false); // resets the cursor
          resize(0);
          break;
        case "DRAGBOX":  // (pre 3.7.2 was startsWith DRAGBOX check)
          setCursorAndRefresh(null, true); // refresh-true to force refresh of dragbox
          sendMessage(msg);
          MP.panel.repaint(); // call this so that it clears the dragbox that was drawn since 3.9.0
          break;
        case "DBLCLICK":
          MP.setDblClickZoom(Convert.o2z(msg.data));
          break;
        case "DBLCLICKZOOM":   // FALLTHROUGH
        case "DBLCLICKUNZOOM":
          sendMessage(msg);
          break;
        case "ZOOMIN":  // FALLTHROUGH (pre 3.7.2 was startsWith ZOOMIN check)
        case "ZOOMOUT": //             (pre 3.7.2 was startsWith ZOOMOUT check)
          handlePercentZoomMessage(msg);
          break;
        case "ZOOM":  //FALLTHROUGH (pre 3.7.2 was startsWith ZOOM check)
        case "ZOOMX": //FALLTHROUGH
        case "ZOOMY":
          flags = 0;
          setCursorAndRefresh(null, true); // refresh-true to force refresh of zoombox
          if (msg.name.indexOf('X')>0) flags |= 1;
          if (msg.name.indexOf('Y')>0) flags |= 2;
          if ((quals != null) && !isEmpty(quals.getS("MIMIC_ORIGIN"))) flags=1;
          if (MP.zoom(msg.data,flags)) { refresh(); sendMessage(msg); }
          break;
        case "UNZOOM": // (pre 3.7.2 was startsWith UNZOOM check)
          if ( MP.unzoom()) { refresh(); msg.info = MP.getZoomLevel(); }
          else msg.info = -1;              // BUG 2238
          DragBox db = MP.getZoomBounds();
          addTimeValuesFromPlot(db);
          msg.data = db;
          sendMessage(msg);                // BUG 2235: Always return message to macro
          break;
        case "PANXY":
          String cursor = (msg.info==-9)? "NERESIZE" : "MOVE";
          flags  = 3;
          setCursorAndRefresh(cursor, false);  // since NeXtMidas 3.7.2
          if ((quals != null) && !isEmpty(quals.getS("MIMIC_ORIGIN"))) flags=1;
          if (MP.panxy(msg.data,flags)) { refresh(Drawable.FAST); sendMessage(msg); }
          break;
        case "PANX":
          if (MP.panxy(msg.data,1)) {
            setCursorAndRefresh("MOVE", true); // refresh is mandatory; since 3.7.2
            sendMessage(msg);
          }
          else {
            setCursorAndRefresh("MOVE", false); // refresh is as needed; since 3.7.2
          }
          break;
        case "PANY":
          flags = 2;
          if ((quals != null) && !isEmpty(quals.getS("MIMIC_ORIGIN"))) flags=1;
          if (MP.panxy(msg.data,flags)) {
            setCursorAndRefresh("MOVE", true); // refresh is mandatory; since 3.7.2
            sendMessage(msg);
          }
          else {
            setCursorAndRefresh("MOVE", false); // refresh is as needed; since 3.7.2
          }
          break;
        case "PANTW":
          if (MP.pantw(msg.data,msg.info)) {
            setCursorAndRefresh("MOVE", true); // refresh is mandatory; since 3.7.2
            sendMessage(msg);
          }
          else {
            setCursorAndRefresh("MOVE", false); // refresh is as needed; since 3.7.2
          }
          break;
        case "PANOBS":   // FALLTHROUGH
        case "PANGEO":
          if (MP.panobs(msg.data)) {
            // In NeXtMidas 3.7.0 this did a set-cursor with optional refresh
            // followed by a MP.refresh(Drawable.FAST)
            // The MP.refresh(Drawable.FAST) is needed so LayerWMS renders
            // during PANing. The refreshes available through
            // setCursorAndRefresh do not give the responsiveness needed, so
            // we are not requesting or relying on its refreshes
            setCursorAndRefresh("HAND", false);
            MP.refresh(Drawable.FAST);
            sendMessage(msg);
          }
          else {
            setCursorAndRefresh("HAND", false); // refresh is as needed
          }
          break;
        case "MENU":
          new GMenu(MP,"Plot","Files,Layers,Features,View,Mode,Scale,Axis,"
              + "Readout,Legend,Options,Query,Export,More,Exit",0,0,this);
          break;
        case "PLOT":
          switch ((String)msg.data) {
            case "FILES":
              int i = MP.layers.size();
              new GMenu(MP,"File","AddNew,Reread,"+MP.layers.getKeyList(1,i),0,0,this);
              break;

            case "QUERY":
              new GQuery(MP,"Query Plot",this,0,this);
              break;

            case "FEATURES":
              String menuitems;
              if (MP.features.size()>0) { menuitems = "Load,Save,AddNew,DeleteAll,"+MP.features.getKeyList(); } // 2010-01-11 NTN: for Bug 2041
              else                      { menuitems = "Load,Save,AddNew"; }
              new GMenu(MP,"Feature",menuitems,0,0,this);
              break;

            case "MORE":
              new GMenu(MP,"More","About,Window,Controls,Animate,Theme,Precision,Type,~Dots,Locate,GoToLocation",0,0,this);
              break;

            case "TYPE": // <-- TODO: doesn't look like this is used (see ~47 lines below instead)
              new GMenu(MP,"Type",PlotFile.typeList,0,0,this);
              break;

            case "EXIT":
              if ((exit&EXIT_MENU)!=0) MP.cancel=exiting=true;
              break;

            case "EXPORT":   // Export the plot to a file
              String[] types  = ExportImage.getFileTypes(true);
              String   ext    = ExportImage.getSuggestedType();
              String   filter = "*."+StringUtil.join(types,"|");

              if (types.length == 0) {
                new GDialog(MP,"Can not export plot: Java can not find an appropriate export library.", 0, 0, 0,null);
              } else {
                flags = Parser.mask(GFiles.flagsList,"QUAL|SINGLEAUX|HIDEOPTS",0); // flags changed in 3.3.2 for Bug 2729
                new GFiles(MP, "ExportImage", "myplot."+ext, filter, null, M, 0, 0, flags, this);
              }
              break;

            default:
              MP.configure((String)msg.data);
              break;
          }
          break;
        case "EXPORTIMAGE":
          // This code block revamped in 3.1.0 for Bug 2475
          FileName fname = FileName.toFileName(msg.data);
          String ext = fname.getExt();
          if ((ext==null) || (ext.length()==0)) {
            ext = ExportImage.getSuggestedType();
            fname.setExt(ext);
          }
          M.info("Exporting image "+fname);
          try {
            MP.exportImage().writeToFile(fname.toLocalFileName(),ext);
          } catch (Exception e) {
            M.printStackTrace("Unable to export image TYPE="+fname.getExt()+
                " FILENAME="+fname+".", e);
          }
          break;
        case "NOFILE":
          // M.warning((String)msg.data);
          break;
        case "MORE":
          switch ((String)msg.data) {
            case "WINDOW":
              new GMenu(MP,"Window","Toggle,Push,Pop,Panel",0,0,this);
              break;

            case "CONTROLS":
              new GMenu(MP,"Controls",pointerMessageList,controls,
                  GMenu.TOGGLE,this);
              break;

            case "ANIMATE":
              new GValue(MP,"Time",atime,atime1,atime2,atinc,'T',0,this);
              break;

            case "THEME":
              new GMenu(MP,"Theme",MWindow.themeList,0,0,this);
              break;

            case "PRECISION":
              new GValue(MP,"Precision",MP.getPrecision(),-1,20,1,'L',0,this);
              break;

            case "TYPE":
              new GMenu(MP,"Type",PlotFile.typeList,type,0,this);
              break;

            case "~DOTS":
              for (int n=1; n<MP.layers.size(); n++) {
                Object layerOrFeature = MP.layers.get(n);
                if (layerOrFeature instanceof Feature) ((Feature)MP.layers.get(n)).getLine().setType("~DOT");
                else if (layerOrFeature instanceof Layer) ((Layer)MP.layers.get(n)).getLine().setType("~DOT");
              }
              break;

            case "LOCATE":
              Position pos = MP.getPosition();
              Feature feature = new Feature();
              feature.getLine().setColor("RED");
              feature.setType("TEXT|OSYMBOL");
              feature.setText("YouAreHere");
              feature.setX(pos.x);
              feature.setY(pos.y);
              MP.addFeature("YOUAREHERE", feature);
              feature.dialog();
              break;

            case "GOTOLOCATION":
              goToLocationDialog();
              break;

            case "ABOUT": // since 3.5.2 to show About dialog
              String messageText = "NeXtMidas PLOT " + Installation.NM_VERSION;
              if (Theme.getOption(Theme.SWINGWIDGETS)) {
                MJPopup popup = MJPopup.createOk(MP.getPanel(), "About PLOT",
                    JOptionPane.PLAIN_MESSAGE, messageText);
                popup.display(0);
              }
              else {
                new GAlert(MP, "ABOUT:PLOT", messageText, "OK", 1, 0, this);
              }
              break;

            default:
              M.warning("Unexpected message "+msg+" data="+msg.data);
              break;
          }
          break;
        case "ABOUT:PLOT":   // since 3.5.2
         // ignore OK button press from About PLOT GAlert
          break;
        case "GOTOLOCATION":
          goToLocationHandler ((Table)msg.data);
          break;
        case "FILE":
        case "LAYER":
          text = (String)msg.data;
          switch (text) {
            case "ADDNEW":
              flags = Parser.mask(GFiles.flagsList,"PATH|EXT",0);
              new GFiles(MP,"OpenFile",null,"*.tmp|prm|shp|jpg|png",null,M,0,
                  0,flags,this);
              break;
            case "REREAD":
              for (int n=1; n<MP.layers.size(); n++) {
                ((Layer)MP.layers.get(n)).reread();
              }
              break;
            default:
              ((Layer)MP.layers.get((String)msg.data)).configure();
              break;
          }
          break;
        case "FEATURE":
          text = (String)msg.data;
          switch (text) {
            case "LOAD":
              // line below changed to include extension for Bug 2263
              new GFiles(MP,"LoadFeaturesFromFile",null,"*.tbl",null,M,0,0,
                  Parser.mask(GFiles.flagsList,"EXT",0),this);
              break;
            case "SAVE":
              new GFiles(MP,"SaveFeaturesToFile",null,"*.tbl",null,M,0,0,0,this);
              break;
            case "ADDNEW":
              {
                Position p = MP.getPosition(MP.px,MP.py);
                Feature f = new Feature();
                f.setX(p.x);
                f.setY(p.y);
                MP.addFeature(getAutoFeatureName("F"),f);
                f.configure();
                break;
              }
            case "DELETEALL": // 2010-01-11 NTN: for Bug 2041
              new GMenu(MP,"Delete All Features?","Yes,No",0,0,this);
              break;
            default:
              {
                Feature f = (Feature)MP.features.get(text);
                f.configure();
                break;
              }
          }
          break;
        case "LOADFEATURESFROMFILE":
          MP.loadFeatures ((String)msg.data);
          break;
        case "SAVEFEATURESTOFILE":
          MP.saveFeatures ((String)msg.data);
          break;
        case "DELETE ALL FEATURES?":
          if ("YES".equals(msg.data)) MP.removeAllFeatures(); // 2010-01-11 NTN: for Bug 2041
          break;
        case "LIMITS": // add in 3.9.0 to allow easy saving of user limits (NXM-3284)
          text = (String)msg.data;
          switch (text) {
            case "LOAD":
              // line below changed to include extension for Bug 2263
              new GFiles(MP,"LoadLimitsFromFile",null,"*.tbl",null,M,0,0,
                  Parser.mask(GFiles.flagsList,"EXT",0),this);
              break;
            case "SAVE":
              new GFiles(MP,"SaveLimitsToFile",null,"*.tbl",null,M,0,0,0,this);
              break;
          }
          break;
        case "LOADLIMITSFROMFILE":
          MP.loadLimits ((String)msg.data);
          break;
        case "SAVELIMITSTOFILE":
          MP.saveLimits ((String)msg.data);
          break;
        case "CONTROLS":
          controls = msg.info;
          break;
        case "SHOW":
          MP.display(msg.info);
          break;
        case "SHOWN":
          if (MP.status!=MWindow.INIT) MP.status=msg.info;
          break;
        case "WINDOW":
          text = (String)msg.data;
          if (text.equals("CLOSING") && (exit&EXIT_WINDOW)!=0) {
            MP.cancel=exiting=true;
          }
          else if (text.equals("TOGGLE")) MP.pop(-1);
          else if (text.equals("PUSH")) MP.pop(0);
          else if (text.equals("POP")) MP.pop(1);
          break;
        case "OPENFILE":
          openFileWithLimits(msg);
          break;
        case "CLOSEFILE":
          closeFile((String)msg.data);
          break;
        case "TIME":
          atime = Convert.o2d(msg.data);
          MP.setTime (atime);
          break;
        case "TYPE":
          setType((String)msg.data);
          refresh();
          break;
        case "PRECISION":
          MP.setPrecision(Convert.o2l(msg.data));
          refresh();
          break;
        case "THEME":
          MP.setTheme((String)msg.data);
          refresh();
          break;
        case "REPARENT":
          MP.reParent(msg.data);
          refresh();
          break;
        case "EXIT":
          if ((exit&EXIT_MESSAGE)!=0) return FINISH;
          break;
        case "XCUT":
          if (!thisIsMe()) { // NXM-1968 - prevent EDT from spawning command
            MQ.put(msg);
            return NOOP;
          }
          if (msg.data == null) msg.data = new Table("IX="+MP.px+",IY="+MP.py);
          tbl = (Table)msg.data;
          doXYCut("X", tbl.getL("IX", MP.px), tbl.getL("IY", MP.py),
              (plottester)tbl.getO("WIN"));
          break;
        case "YCUT":
          if (!thisIsMe()) { // NXM-1968 - prevent EDT from spawning command
            MQ.put(msg);
            return NOOP;
          }
          if (msg.data == null) msg.data = new Table("IX="+MP.px+",IY="+MP.py);
          tbl = (Table)msg.data;
          doXYCut("Y", tbl.getL("IX", MP.px), tbl.getL("IY", MP.py),
              (plottester)tbl.getO("WIN"));
          break;
        case "DROPDATA":
          sendMessage(msg);  // just send back to Macro for handling
          break;
        default:
          M.warning("Unhandled message name="+msg.name+" in PLOT from "+msg.from);
          break;
      }
    }
    return NORMAL;
  }

  /**
   * Process message as mark message, whether a MARK_MESSAGE or
   * originally a LEFT_MOUSE_BUTTON BUTTON Message
   * @param msg message
   */
  private void processMarkMessage (Message msg) {
    msg.name = "MARK";
    msg.data = MP.getPosition(MP.px,MP.py);
    if (stealNextClick != null) {  // Since 2.1.0
      StealsMouseClick smc = stealNextClick;
      stealNextClick = null; // Set this null first to prevent any possible click-loops.
      smc.mouseClicked(msg);
    } else {
      sendMouseMessage(msg, ONCLICK);
    }
  }

  /**
   * Process the OVERLAY or the OPENFILE message.
   * @param  msg  Message
   * @since NeXtMidas 3.7.1
   */
  protected void openFileWithLimits (Message msg) {
    if (msg.info == -1) {
      if (msg.name.equals("OVERLAY")) {
        M.warning("Plot.processOMessage: -1 cannot be msg.info value for opening overlays.");
      }
      else { // -1 means close previous layers in OPENFILE message
        closeFile("*");
      }
    }
    Table  quals = Convert.o2t(msg.quals);
    imageFlag = false;   // ensure flag is set to false before opening file
    openFile(Convert.o2s(msg.data),quals);
    if (quals!=null) {
      Table olimits = quals.getTable("OUTERLIMITS");
      if (olimits != null) {
        String[] keys = olimits.getKeys();
        for (String key : keys) {
          MP.setOuterLimits(key, olimits.getD(key));
        }
      }
      Table vlimits = quals.getTable("VIEWLIMITS");
      if (vlimits != null) {
        String[] keys = vlimits.getKeys();
        for (String key : keys) {
          MP.setViewLimits(key, vlimits.getD(key));
        }
      }
    }
  }

  // NTN: backed out Bug 1420 changes introduced in NXM 2.5.5
  private void updatePointer () {
    if (pointerMsg==null) return;
    pointerCount=0;
    Message msg = pointerMsg;  // in case event thread reassigns pointerMsg
    msg.data = MP.getPosition();
    MP.formatReadout(msg.info);
    MP.updateCursor();
    if (msg.name.equals("DRAG")) {
      setCursorAndRefresh("Crosshair", true); // force refresh to update drag; since NeXtMidas 3.7.2
      sendMouseMessage(msg, ONDRAG);
    }
    else {
      setCursorAndRefresh(null, false); // reset, refresh is optional; since NeXtMidas 3.7.2
      msg.name="MARK";
      if (MP.px>MP.ix1 && MP.px<MP.ix2
       && MP.py>MP.iy1 && MP.py<MP.iy2) sendMouseMessage(msg, ONMOTION);
    }
  }

  /** Handle the ZOOMIN and ZOOMOUT messages
      @since NeXtMidas 3.3.2
   */
  private void handlePercentZoomMessage (Message msg) {
    float percent = 0.5f; // default is 50 percent
    boolean setInQuals = false;
    Table t = null;

    // first check QUALs table for PERCENT=f
    if (msg.quals != null) {
      t = Convert.o2t(msg.quals);
      if (t.containsKey("PERCENT")) {
        percent = t.getF("PERCENT",percent);
        setInQuals = true;
      }
    }
    // if not set in QUALs, convert data portion to float
    if (!setInQuals) {
      percent = Convert.o2f(msg.data,this);
      if (t == null) t = new Table();
      t.put("PERCENT", percent);
      msg.quals = t;  // set the QUALS to have the percent
    }

    if (percent > 0.0f || percent==-1) {        // -1 == zoom all the way out
      if (msg.name.equals("ZOOMIN")) MP.zoomIn(percent);
      else                           MP.zoomOut(percent);
    } else {
      if (msg.name.equals("ZOOMIN")) MP.zoomIn();
      else                           MP.zoomOut();
    }

    // replace the data portion with the new Bounds and send back to the macro
    DragBox db = MP.getZoomBounds();
    addTimeValuesFromPlot(db);
    msg.data = db;
    sendMessage(msg);
  }

  /** Update a DragBox to have the TMIN/TMAX and T1/T2  */
  private void addTimeValuesFromPlot (DragBox db) {
    // this logic pulled from processMessage in 3.3.2 for Bug 2722
    double tmin = Math.min(MP.getTMin(), MP.getTMax());   // adjust order of tmin/tmax if necessary
    double tmax = Math.max(MP.getTMin(), MP.getTMax());   // adjust order of tmin/tmax if necessary
    db.setTMin(tmin);
    db.setT1(MP.getT1());
    db.setTMax(tmax);
    db.setT2(MP.getT2());
  }

  /** Update the results identified in the Mouse Tag table. This is called by {@link #sendMouseMessage(Message, int)}.
      @since NeXtMidas 3.1.1
   */
  private void updateMtagResults () {
    if (mtag!=null) {
      String[] keys = mtag.getKeys();
      Position p = MP.getPosition(MP.px,MP.py);
      for (String key : keys) {
        final String resLabel = mtag.getS(key);
        if (key.equalsIgnoreCase("X"))   MR.put(resLabel, MP.isMapView()? p.lon : p.x);
        if (key.equalsIgnoreCase("Y"))   MR.put(resLabel, MP.isMapView()? p.lat : p.y);
        if (key.equalsIgnoreCase("Z"))   MR.put(resLabel, MP.isMapView()? p.alt : p.z);
        if (key.equalsIgnoreCase("X+Y")) MR.put(resLabel, p.x+p.y); // this just denotes some sort of movement
      }
    }
  }

  @Override
  protected void sendMessage (Message msg) {
    // send to the mimic plotter(s) and rmif
    if (msg.name.startsWith("ZOOM") ||
        msg.name.startsWith("UNZOOM") ||
        msg.name.startsWith("PAN") ) {
      Table quals = Convert.o2t(msg.getQuals());
      if (!mimic.isEmpty() && ((quals == null) || isEmpty(quals.getS("MIMIC_ORIGIN")))) {
        if (quals == null) {
          msg.setQuals(quals = new Table());
        }
        Table quals2 = quals.copy();
        quals2.put("MIMIC_ORIGIN", id); // Show which plot is being copied

        for (Map.Entry<String,MessageHandler> e : mimic.entrySet()) {
          if (e.getValue() == null) {
            e.setValue(M.registry.getMessageHandler(e.getKey(), -1));
          }
          Message mmsg = new Message(msg.name,msg.info,msg.data,null,this,quals2);
          e.getValue().processMessage(mmsg);
        }
      }

      if (rmif!=null) {
        modifyRmifProperty();
      }
    }

    // send to the macro
    super.sendMessage(msg);
  }

  /** Create an XCut or a YCut from first layer in this PLOT.
      @param key   'X' for XCut, 'Y' for YCut
      @param ix    The X location on the plot to get the cut
      @param iy    The Y location on the plot to get the cut
      @param plotprim Existing PLOT to open the "cut" in, or null to open a new PLOT window
   */
  public void doXYCut (String key, int ix, int iy, plottester plotprim) {
    // 2011-03-04: This method was extensively re-written in 2.9.1 to support YCuts (Bug 1831)

    if (ix < MP.ix1 || ix > MP.ix2 || iy < MP.iy1 || iy > MP.iy2) {
      M.warning("Mouse position out of bounds. Unable to create "+key+"Cut.");
      return; // only do if within data bounds
    }
    char     cutType = key.toUpperCase().charAt(0);
    double   x       = MP.getrx(ix,iy);             // convert the pixel loc to data loc
    double   y       = MP.getry(ix,iy);             // convert the pixel loc to data loc
    boolean  xcut    = (cutType=='X');
    boolean  ycut    = (cutType=='Y');
    Layer    lay     = getLayer();
    DataFile tmpFile;
    Table    quals;   // file qualifiers, if any
    String   qualString = "";
    String   outFile;
    String   plotCmd;
    boolean  zoom = false;  // whether or not the original plot is zoomed and this needs to be propagated
    double   boundsMin = xcut? MP.getXMin() : MP.getYMin();      // plot zoom min bounds, only used if zooming
    double   boundsMax = xcut? MP.getXMax() : MP.getYMax();      // plot zoom max bounds, only used if zooming

    try {
      tmpFile = (DataFile)lay.getFile();
      quals = tmpFile.getQualifiers();   // get any qualifiers on the file
      if (quals != null) qualString = quals.toString();  // qualifiers in String form
      if (tmpFile.isPipe()) {
        M.warning(cutType+"Cut not supported for piped files. File:["+tmpFile.getTag()+"]");
        return;
      }
    } catch (Exception e) {
      M.warning("Missing or invalid file type for "+cutType+"Cut. File must be a DataFile (Midas Blue File)");
      return;
    }

    Table userBounds = MP.viewLimits;
    if ((xcut && (userBounds.containsKey("X1") || userBounds.containsKey("X2"))) ||
        (ycut && (userBounds.containsKey("Y1") || userBounds.containsKey("Y2")))) {
      zoom = true;
    }

    String cutPlotID = (getID().isEmpty())? cutType + "CUT_" + tmpFile.getTag()
                                          : cutType + "CUT_" + getID();

    if (xcut) {
      double startIndex = tmpFile.getIndex(y, DataFile.ISTART, DataFile.ADDR_ABSCISSA);
      outFile = tmpFile.filename+"("+startIndex+":"+(startIndex+1)+")"+qualString;
      // since 3.7.1 for NXM-2898 added parameters. (removed view and corrected readout in 3.7.3 for NXM-3158)
      String readoutMaskStr = Parser.mask2s(MPlot.readoutList, (MP.getReadout() | MPlot.R_ONAXIS));
      plotCmd = "PLOT/NICE=0/ID="+cutPlotID+"/BG " +outFile + " CM=" + MP.getMode() + " READOUT=" + readoutMaskStr;   // NICE=0 prevents getData race condition
    }
    else if (ycut) {
      //double adjX = tmpFile.getIndex(Double.toString(x), DataFile.JSTART, DataFile.ADDR_ABSCISSA); // changed to line below with Bug 2361 fix
      double fstart = tmpFile.getIndex(x, DataFile.JSTART, DataFile.ADDR_ABSCISSA);

     // need to map y coordinate start/delta/units to X coordinate (via XS=, XD=, XU= qualifiers and set YUnits to NONE
      outFile = "ram:ycut_{type=1000,XStart="+lay.getYStart()+",XDelta="+lay.getYDelta()+",XUnits="+MP.getYUnits()+",YUnits=NONE}";
      String thinCmd = "thin "+tmpFile.filename+qualString+" "+outFile+" fstart="+fstart+" fend="+(fstart+1);
      Shell.runCommand(M, thinCmd);  // do a thin to get the required output to plot

      plotCmd = "PLOT/NICE=0/ID="+cutPlotID+"/BG/READOUT=\"+OnAxis\" "+outFile ; // Y vals kept constant, NICE=0 prevents getData race condition
    }
    else {
      M.warning("Invalid CUT type of ["+key+"]Cut. Cut must be 'X' or 'Y'");
      return;
    }

    // Get existing cut plot, if any
    Command cmd = M.registry.getCommand(cutPlotID, 1);
    if (plotprim==null && cmd!=null && cmd instanceof plottester) plotprim = (plottester)cmd;

    // Display the cut
    if (plotprim != null)  {   // existing or given window
      plotprim.closeFile("*");
      plotprim.openFile(outFile);
      if (zoom) plotprim.zoomTo = new Table("{X1="+boundsMin+",X2="+boundsMax+"}");
    }
    else {
      plotCmd += " Y1="+MP.orz1+" Y2="+MP.orz2;
      if (zoom) plotCmd += " ZOOMTO={X1="+boundsMin+",X2="+boundsMax+"}";  // apply any existing zoom
      Shell.runCommand(M, plotCmd);  // open in new window
    }

  }

  private void goToLocationDialog () {
    final int TOGGLE_MASK = GMenu.TOGGLE|GMenu.TOFF|GMenu.NCOL|GWidget.NOMSG;
    double xDef = 0.0, xMin = DataTypes.DMIN, xMax = DataTypes.DMAX; String xName = "X";
    double yDef = 0.0, yMin = DataTypes.DMIN, yMax = DataTypes.DMAX; String yName = "Y";
    double rDef = 1.0, rMin = 0.0,            rMax = DataTypes.DMAX;

    boolean yFirst = false;
    if (MP.getView().equals("LatLon") || MP.getView().equals("Mercator") || MP.getView().equals("Geodetic")) {
      rDef =   15.0;
      xMin = -180.0; xMax = 180.0; xName = "LON";
      yMin =  -90.0; yMax =  90.0; yName = "LAT";
      yFirst = true;
    }
    GDialog gd = new GDialog(null, "GoToLocation", 200, 75, "Submit,Cancel,Apply", "Apply", 0, this);
    gd.addTo(MP);
    GValue xValue = new GValue(null, "XVAL", xDef, xMin, xMax, 1.0, GWidget.NOMSG, this);
    GValue yValue = new GValue(null, "YVAL", yDef, yMin, yMax, 1.0, GWidget.NOMSG, this);
    xValue.setTitle(xName);
    yValue.setTitle(yName);
    if (yFirst) { gd.addWidget(yValue); gd.addWidget(xValue); } // Put LAT above lon for this case
    else        { gd.addWidget(xValue); gd.addWidget(yValue); } // Normal X over Y
    gd.addWidget(new GValue(null, "Radius", rDef, rMin, rMax, 0.1, GWidget.NOMSG, this));
    gd.addWidget(new GMenu(null, "Flags",  "MarkLocation,Zoom", 0x1, TOGGLE_MASK, this));
    gd.resize(1);
  }

  private void goToLocationHandler (Table tbl) {
    double  xPos  = getDVal(tbl, "XVAL");
    double  yPos  = getDVal(tbl, "YVAL");
    double  rad   = getDVal(tbl, "RADIUS");
/* prior to 3.7.0 TODO delete in 3.9+ (prepend another '/' to this line to enable previous code)
    String  flags = "|" + Convert.o2s(tbl.get("FLAGS")) + "|";
    boolean mark  = (flags.indexOf("|MarkLocation|") >= 0);
    boolean zoom  = (flags.indexOf("|Zoom|"        ) >= 0);
/*/
    // adjusted in 3.7.0 for flags to be case-insensitive (Bug 2984)
    String  flags = tbl.getS("FLAGS");
    boolean mark  = Parser.find(flags, "MarkLocation",-1) >= 0;
    boolean zoom  = Parser.find(flags, "Zoom",        -1) >= 0;
//*/
    Feature feature = (Feature)MP.getFeatures().get("GOTO_LOCATION");
    if (mark) {
      if (feature == null) {
        feature = new Feature();
        feature.getLine().setColor("RED");
        feature.setType("HLINE|VLINE");
        MP.addFeature("GOTO_LOCATION", feature);
      }
      feature.setEnable(feature.getEnable()|MPlot.EN_GLOBAL);
      feature.setX(xPos);
      feature.setY(yPos);
    } else {
      if (feature != null) {
        MP.removeFeature("GOTO_LOCATION");
      }
    }
    if (zoom) {
      if (rad > 0.0) {
        if (MP.view == MPlot.V_GEODETIC) { MP.setView(MPlot.V_LATLON); } // Can not zoom in geodetic
        if (MP.zoom(new double[] { xPos-rad, xPos+rad, yPos-rad, yPos+rad }, 0)) { MP.refresh(); }
      } else {
        M.warning("PLOT: Got GOTOLOCATION message with invalid radius, RADIUS="+rad);
      }
    }
  }

  /** Gets the IDs of the plot(s) will mimic this one.
   *  @return The IDs of the plots will mimic this one.
   *  @since NeXtMidas 3.5.4
   */
  public String getMimic () {
    return join(mimic.keySet().toArray(), "|");
  }

  /** Sets the IDs of the plot(s) to mimic this one. Unlike MIMIC= on the command line, which will
   *  lazy-load the registry entries (allowing a PLOT command in the macro to point to the next
   *  PLOT down) this will load the registry entry right away. <br>
   *  <br>
   *  Note: Prior to NeXtMidas 3.5.4 the mimic capability could only be accessed via this method and
   *  only permitted a strict 1:1 mapping between two plots.
   *  @param regIDs The registry IDs of the plots to mimic this one. This accepts the
   *                add/remove syntax (e.g. "-PLOT1|+PLOT2").
   */
  public void setMimic (String regIDs) {
    // Note: In NeXtMidas 3.5.4 this method changed from a single entry ('private plot mimic') that
    //       had to be loaded immediately to a set of entries that could be lazy-loaded allowing
    //       MIMIC= to be added to the command line and processed even if any of the mimic plotters
    //       are not defined yet. Along with making this a set of entries, the messages now use a
    //       MIMIC_ORIGIN= entry in the message qualifiers as the token to prevent loops (A->B->A).
    //       This change allows greater flexibility in the way the plots are connected.
    //
    // Note: The mimic Map is a map such that we have an easy way to hold the registry IDs of the
    //       entries we need to lazy-load. Also, it holds MessageHandler instances rather than
    //       plot instances to allow automated testing (e.g. sending message to macro), though
    //       this is clearly not the typical use-case.
    setMimic(regIDs, false);
  }

  /** Sets the IDs of the plot(s) to mimic this one.
   *  @param regIDs The IDs of the plots to mimic this one. This accepts the
   *                add/remove syntax (e.g. "-PLOT1|+PLOT2").
   *  @param inOpen Enable the special handling that applies to any calls from open(..)?
   *  @since NeXtMidas 3.5.4
   */
  @InternalUseOnly
  private void setMimic (String regIDs, boolean inOpen) {
    if ((regIDs == null) || regIDs.isEmpty()) {
      // Special case - nothing to do but clear the list and return
      mimic.clear();
      return;
    }

    if (inOpen || (!regIDs.startsWith("+") && !regIDs.startsWith("-"))) {
      mimic.clear();
    }
    int wait = (inOpen)? 0 : -1; // lazy-load (0) if in open, otherwise error (-1) if not found

    for (String regID : split(regIDs)) {
      if (regID.startsWith("-")) { mimic.remove(regID.substring(1)); continue; }
      if (regID.startsWith("+")) { regID = regID.substring(1); }
      mimic.put(regID, M.registry.getMessageHandler(regID, wait));
    }
  }

  /** Set ID of RMIF feeding these plot layers to minimize link BW.
   *  @param regID The registry ID of the RMIF instance.
   */
  public void setRmif (String regID) {
    // TODO: Add getRimf(..) and allow RMIF= to be set on command-line. This would have been done
    //       along with MIMIC= (NeXtMidas 3.5.4), had there been a DR and test-cases for it.
    rmif = M.registry.get(regID, rmif.class, 0);
    if (compression==0) compression=3;
  }

  /** send current channel modification message */
  @InternalUseOnly // since NeXtMidas 3.5.4 -- TODO: this should be private
  public void modifyRmifProperty () {
    if (rmif==null) return;
    for (int i=1; i<MP.layers.size(); i++) {
      Layer lay = (Layer) MP.layers.get(i);
      Rmif.Channel c = (Rmif.Channel)rmif.getChans().get(lay.name);
      if (c==null) continue;
      String modify = "{PROPERTY="+c.rname+",COMP="+(compression-1)+
        ",PLOTWIDTH="+MP.ix21+",TRIM1="+lay.getx1i()+",TRIM2="+lay.getx2i()+"}";
      MQ.put ("MODIFY",0,modify,c.r);
    }
  }

  /** set the default RMIF compression mode / settings for channels feeding plots.
      @param rmifCmp RMIF compression mode (see {@link #compressionList}).
      @since NeXtMidas 2.9.0
   */
  public void setRMIFCompression (String rmifCmp) {
    int newCompression = Parser.find(compressionList, rmifCmp, compression, 0, Parser.E_ERROR); // since 3.3.1: errors if given invalid value
    if (newCompression == compression) return; // break out on no change
    modifyRmifProperty();
  }

  /** Get the RMIF Compression mode for PLOT. See {@link #compressionList}.
      @return The RMIF Compression mode for PLOT
      @since NeXtMidas 2.9.0
   */
  public String getRMIFCompression () {
    return Parser.get(compressionList, compression);
  }

  /** set the default compression mode for channels feeding plots.
      private since NeXtMidas 3.7.0, it was deprecated in 2.9.0. Use {@link #setRMIFCompression(String)}
   */
  private void setCompression (String cmp) { // private since 3.7.0 - TODO remove in 3.9+
    M.deprecate("This method will be removed in a future release. Use reg.plot.rmifCompression");  // Bug 2276
    setRMIFCompression(cmp);
  }

  /** Set plot type to one of {@link PlotFile#typeList} (i.e. {@value PlotFile#typeList}) or "NONE" to revert to default.
      @param stype plot type to display (see {@link PlotFile#typeList})
      @since NeXtMidas 2.9.0
   */
  public void setPlotType (String stype) {
    int newType;
    // This method added in 2.9.0 due to the confusion between the Plot type and the Line type (see Bug 2276)
    if (stype.equalsIgnoreCase("NONE")) {
      newType = 0;   // handle NONE case (for setting to default)
    } else {
      newType = Parser.find(PlotFile.typeList, stype, type, 0, Parser.E_ERROR); // since 3.3.1: errors if given invalid value
    }
    if (newType == type) return; // no change
    type = newType;
    StringBuilder list = new StringBuilder(32);
    for (int i=1; i<MP.layers.size(); i++) {
      Layer l = (Layer)MP.layers.get(i);
      if (l==null || l.pf==null) continue;
      if (list.length() != 0) list.append("|");
      list.append(l.pf.getName());       //Bug 605 - changed from getUrl to getName
    }
    closeFile("*");
    MP.setView(0);
    openFile(list.toString());
  }

  /** Get plot type (one of {@value PlotFile#typeList}) as an int.
      @return  plot type as int
      @since NeXtMidas 3.3.2
   */
  @InternalUseOnly
  public int getPlotTypeID () {
    return type;
  }

  /** Get plot type (one of {@value PlotFile#typeList}).
      @return plot type as String
      @since NeXtMidas 2.9.0
   */
  public String getPlotType () {
    return Parser.get(PlotFile.typeList, type);
  }

  /** Set plot type to one of LINE|RASTER|POINT|...
      @param stype  plot type
   */
  public void setType (String stype) {
    setPlotType(stype);
  }

  /** Get plot type (one of {@value PlotFile#typeList}).
      @return  plot type as String
   */
  public String getType () {
    return getPlotType();
  }

  /**
   * Get Base Layer
   * @return Plot Base Layer
   */
  public Layer getLayer () {
    return MP.getBaseLayer();
  }

  /** Create a unique Feature name when none is supplied.
      @param prefix prefix for feature name
      @return unique feature named with specified prefix for this plot instance
      @since NeXtMidas 3.5.1
   */
  @InternalUseOnly
  public synchronized String getAutoFeatureName (String prefix) {
    KeyVector existingFeatures = MP.features;
    int featureNum = existingFeatures.size();
    int keyExists;

    do {
      keyExists = existingFeatures.findKey(prefix + ++featureNum);
    } while (keyExists >= 0);
    return prefix + featureNum;
  }

  /** Sets the X-axis exponent in the format class, and uses it for converting
      the exponent of the default units. See {@link MPlot#setMult(int, char)}.
      @param mult  exponent
   */
  public void setXMult (int mult) { MP.setMult(mult, 'X'); }

  /** Sets the Y-axis exponent in the format class, and uses it for converting
      the exponent of the default units. See {@link MPlot#setMult(int, char)}.
      @param mult  exponent
   */
  public void setYMult (int mult) { MP.setMult(mult, 'Y'); }

  /** Sets the X-axis exponent in the format class, and uses it for converting
      the exponent of the default units. See {@link MPlot#setMult(Object, char)}.
      @param mult  exponent
   */
  public void setXMult (String mult) { MP.setMult(mult, 'X'); }

  /** Sets the Y-axis exponent in the format class, and uses it for converting
      the exponent of the default units. See {@link MPlot#setMult(Object, char)}.
      @param mult  exponent
   */
  public void setYMult (String mult) { MP.setMult(mult, 'Y'); }

  /** Shows the plot's legend.
      @param corner The corner the legend should be in (defined in GLegend). Should be one of:
                    <pre>
                      TL or TOPLEFT
                      TR or TOPRIGHT
                      BL or BOTTOMLEFT
                      BR or BOTTOMRIGHT
                    </pre>
  */
  public void showLegend (String corner) {
    MP.createLegend();
    MP.getLegend().setCorner(corner);
  }

  /** Hides the plot's legend.  */
  public void hideLegend () {
    MP.getLegend().close();
  }

  /** Attempts to steal the next mouse click. When the user next clicks the mouse (LMB click) the
      message will be passed to the {@link StealsMouseClick} rather than being processed like a
      normal mouse click. <br>
      <br>
      Once the mouse is clicked the message will be sent to the listener and then the plot reverts
      back to normal mouse-click processing mode. If the plot exits before the user clicks or if
      another class asks to steal the next mouse click the listener will never be called. <br>
      <br>
      If this method is called multiple times before the user clicks, only the last listener will
      be remembered (the others will just be discarded).

      @param smc The class to pass the stolen mouse click message back to.
      @since 2.1.0
   */
  public void stealNextMouseClick (StealsMouseClick smc) {
    stealNextClick = smc;
  }

  /** Sends a message to all the layers. This is used for passing mouse messages to the layers
      so they can handle autoselect.
      @param msg  The message to send.
      @param type The message type ({@link #ONCLICK}, {@link #ONDRAG}, {@link #ONMOTION}).
      @since NeXtMidas 2.1.0
   */
  protected void sendMouseMessage (Message msg, int type) {
    /* Send to macro is type is set in controls. */
    if ((controls & type) != 0) {
      updateMtagResults();  // update results based on mouse clicks (since 3.1.1 - Bug 2576)
      sendMessage(msg);
    }

    /* Also pass it to all layers for those doing autoselect. */
    KeyVector layers = MP.getLayers();
    for (int i = 0; i < layers.size(); i++) {
      ((Layer)layers.get(i)).processMouseMessage(msg, type);
    }
  }

  /** Path to follow for internal (not user-driven) cursor changes
   *  @param cursor  The name of the cursor (see {@link MWindow#cursorList}
   *  @param refresh Force a refresh even if the cursor doesn't require changing (if the cursor
   *                 changes a refresh will be done anyhow).
   *  @since NeXtMidas 3.7.2 set cursor and refresh
   */
  private void setCursorAndRefresh (String cursor, boolean refresh) {
    if (cursor == null) cursor = MP.getUserCursor();  // Bug 2249, 1230
    if (cursor == null) cursor = "Default";
    if (MP.isCursor(cursor)) {  // since NeXtMidas 3.7.2
      // No cursor change needed, but need to refresh if refresh=true
      if (refresh) MP.refresh(Drawable.DATAX);  // since NeXtMidas 3.7.2
    }
    else {
      MP.setCursor(cursor,false); // <-- this calls refresh()
    }
  }

  /** Gets a double value from a table, if it gets a widget, convert it to a double.
      Only called by processMessage.
   */
  private double getDVal (Table tbl, String name) {
    Object val = tbl.get(name);

    if (val instanceof GWidget) { val = ((GWidget)val).getValue(); }

    return Convert.o2d(val);
  }

  /**
   * Get the CM= argument for complex mode for displaying data (i.e. plot mode).
   * NOTE: This MIGHT NOT match the current plot mode (since empty string means AUTO mode).
   * If current plot mode is desired, use {@link MPlot#getMode()}.
   * @return empty string if CM= arg is not set (i.e. it is in AUTO mode)
   * @since NeXtMidas 3.4.2
   */
  public String getComplexMode() {
    return Parser.get(MPlot.modeList, ocm);
  }

  /**
   * Set the outer/initial (same effect as the CM= argument) and current plot's complex mode (i.e. plot mode).
   * This is need when all layers are removed, then some added back for PLOT to continue using the
   * specified complex mode for displaying data.
   * @param mode new plot mode (10LOG, 20LOG, etc., empty string means AUTO mode)
   * @since NeXtMidas 3.4.2
   */
  public void setComplexMode (String mode) { // for BUG 2808
    if ("".equals(mode)) { // AUTO mode complex selected
      int complexMode = 0;
      Layer lay = MP.getBaseLayer();
      if (lay != null) {
        complexMode = determineComplexMode(lay); // get complex from base layer
      }
      MP.setMode(complexMode);
      ocm = 0; // 0 for AUTO complex mode (ocm can be different than MP.mode for AUTO mode)
    } else {
      MP.setMode(mode);
      ocm = MP.mode;
    }
  }

  /**
   * This method was extracted from {@link #openLayerFile(Layer, PlotFile, Table)} in 3.4.2 for BUG 2808.
   * @param layer Layer that is plotted (generally first layer) cannot be null
   * @return complex mode that should be used
   */
  private int determineComplexMode(Layer layer) {
      PlotFile pf = layer.getFile();
      if      (pf.getMode()   == 1         ) return MPlot.M_REAL;
      else if (pf.getXUnits() == Units.FREQ) return MPlot.M_MAG;
      else                                   return layer.getDefaultComplexMode();
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Nested classes
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Indicates a class that is able to steal a click from the plot.
      @since 2.1.0
   */
  public static interface StealsMouseClick {
    /** Called when the click is stolen.
        @param msg The click message.
     */
    public void mouseClicked(Message msg);
  }

  /** Note: this is used via MAO reflection by KeyObject.setKeys
   *  to initialize the scaleTable
   *  @param scaleTable the scaleTable to set
   */
  public void setScaleTable (Table scaleTable) {
    this.scaleTable = scaleTable;
  }

  /** A Container for functionality needed to support /PRESCALE, which allows prescaling
      based on either all the data or a random sampling THROUGHOUT the file. This can be
      used with zero-padded PreD data or other cases where auto-scaling proves inadequate.
      <pre>
      /PRESCALE=INTERNAL   [DEFAULT no other valid option is specified] ONLY SUPPORTS single
                           1000/2000 input file. /PRESCALE will force an internal prescale
                           even with SCALETABLE or KW present.
      /PRESCALE=EXT[ERNAL] Needs table assigned to SCALETABLE to match the one populated by
                           PRESCALER. Outside of a pipe, the lack of a SCALETABLE will cause
                           this switch to be ignored. Inside a pipe, the process will hang until
                           the SCALETABLE is available.
      /PRESCALE=KEY[WORD]  Looks for external header keyword specified by /SCALEKW or default
               =KW         keyword VOLATILE_KW_PLOT_SCALE to keyword VOLATILE_KW_PLOT_SCALE to
                           determine scale values for the plot. ONLY SUPPORTS single 1000/2000 input
                           file. If an "outcopy" is generated, then the scale keyword will be placed
                           in the output file. If the keyword is not in the input file, this will
                           switch to internal prescaling.
      </pre>
      @since NeXtMidas 3.9.1 for NXM-3078
   */
  private class PrescaleSetup {

    private static final String DEFAULT_OUTCOPY = "plot_internal_prescale_temp_out.tmp{AUX=RAM}";
    /** prescale based on external entity setting SCALETABLE */
    private boolean externalPrescale;
    /** file to read data from after internal prescale - critical to have this in a pipe */
    private String inFileAfterPrescale;
    /** external header keyword holding scale values */
    private String prescalerKeyword = "";
    /** BLUE file type - external prescale can be used for all types, but other prescale modes only accept 1000 or 2000 */
    private int prescalerTypeCode;
    /** used with /PRESCALE=KW to check for scale KW in external header keywords */
    private Keywords extHeaderKeywords;
    /** Even in a pipe, we may need to wait to open the output file when doing internal prescaling in /PRESCALE=KW mode */
    private boolean needWaitToOpenOutInFile;
    /** holds precaler primitive instance */
    private prescaler plotPrescaler = null;

    /** Process the scaleTable by setting MP.scale if necessary and setting the layer min and max values if necessary
     *  @since 3.9.1
     */
    private void processScaleTable () {
      if(scaleTable!=null) {
        if (scaleTable.containsKey("Y1")||scaleTable.containsKey("YMIN")||scaleTable.containsKey("Z1")||scaleTable.containsKey("ZMIN")) MP.scale &= ~MPlot.S_MIN;
        if (scaleTable.containsKey("Y2")||scaleTable.containsKey("YMAX")||scaleTable.containsKey("Z2")||scaleTable.containsKey("ZMAX")) MP.scale &= ~MPlot.S_MAX;
        setLayerMinMaxScale();
      }
    }

    /** Will iterate the layers that MPlot hold. Then apply the scale table values to that layer.
     *  @since NeXtMidas 3.9.1
     */
    private void setLayerMinMaxScale () {
      if(scaleTable == null || scaleTable.isEmpty()) return;
      needScaleTable=false;
      KeyObject.setKeys(MP,scaleTable,args,KeyObject.SOFT);
      KeyObject.setKeys(MP.getBaseLayer(),scaleTable,args,KeyObject.SOFT);
    }

    /** Internal prescaling launches the prescaler primitive in the background during
     *  the open() stage, via forming the command string and then using Shell to
     *  execute the command.
     */
    private void runPrescale () {
      String prescalarOut = MA.getRaw("OUTCOPY");
      if ((prescalarOut == null) && M.isPiped()) prescalarOut = DEFAULT_OUTCOPY;
      String prescalerCmd = "prescaler IN="+inFile;
      if (prescalarOut!=null) {
        inFileAfterPrescale = prescalarOut;
        prescalerCmd += " OUT="+prescalarOut;
        needWaitToOpenOutInFile = true; // have separate out/in file
      }
      else {
        inFileAfterPrescale = inFile;
      }

      int percent = MA.getL("/RANDOM",prescaler.DEFAULT_RANDOM_PERCENT,prescaler.WHOLE_FILE);
      prescalerCmd += " /RANDOM="+percent;
      if(!prescalerKeyword.equals("")) prescalerCmd += " "+ prescalerKeyword;
      plotPrescaler = (prescaler)Shell.execBackgroundCommand(M,prescalerCmd);
      scaleTable = plotPrescaler.table;
      if(M.isPiped() && !needWaitToOpenOutInFile) openFile(inFileAfterPrescale);
    }

    /** Handles the loading of the scale table either from a Result value or
     *  directly from the prescaler primitive field
     *  @return true if loaded
     */
    private boolean tryLoadScaleTable() {
      boolean loaded = false;
      // scaleTable will automatically be pointing to prescale.table unless we are running as external process
      if (externalPrescale) scaleTable = MA.getTable("SCALETABLE");
      if ((scaleTable!=null) && !(scaleTable.isEmpty())) {
        loaded = true;
      }
      return loaded;
    }

    /** Handles all the PRESCALE related functionality that need to occur during plot.open()<br>
     *  <pre>
     *  1) checking the prescale mode
     *  2) grabbing scale information that is already available
     *     a) from SCALETABLE if /PRESCALE=EXT outside pipe
     *     b) from external header keyword if /PRESCALE=KW
     *  3) determining if the prescaler primitive needs to be run for internal prescaling, or
     *     if the scenario dictates falling back to other scaling means
     *  4) Setting fields that determine if the plot.process() needs to handle the opening of
     *     the file used for suppling the data and/or waiting on the SCALETABLE to be set
     *  </pre>
     *  @return true if plot.process() needs to wait on a scale table to be set
     */
    private boolean open() {
      String prescaleType =  MA.getS("/PRESCALE").toLowerCase();
      needScaleTable = true;
      boolean runPrescaleCommand = true;
      boolean preferKeyword = prescaleType.equals("kw")|| prescaleType.startsWith("key");
      boolean prescalerExternal = prescaleType.startsWith("ext");
      boolean waitingOnPrescale = false;
      if (prescalerExternal) {
        waitingOnPrescale = true;
        externalPrescale = true;
        runPrescaleCommand=false;
        inFileAfterPrescale = inFile;

        if(M.isPiped()) {
          openFile(inFileAfterPrescale);
          processScaleTable();
        }
        else if (scaleTable != null && !scaleTable.isEmpty()) {
          // If we are not in a pipe and we don't already have a scale table, we messed up.
          waitingOnPrescale = false;
          externalPrescale = false;
        }
        else {
          // Don't need to try and get the scale table if we already have it
          waitingOnPrescale = false;
          needScaleTable = false;
        }
      }
      else {
        if(!preferKeyword && !(prescaleType.startsWith("int") || prescaleType.equals(""))) {
          M.warning("Invalid prescale option " + prescaleType+ ", defaulting the prescaler command to be run internally");
        }
        if(inFile.contains("|")) {
          // We DO NOT support multiple input files with prescaling, unless prescaling is external
          waitingOnPrescale=false;
          runPrescaleCommand = false;
          M.warning("Internal prescaler does not support multiple input files");
        }
        else {
          // only have one input file listed
          BaseFile bf = BaseFile.getInstanceFor(plottester.this,inFile);
          if (bf instanceof DataFile) {
            DataFile df = new DataFile(M, inFile);
            DataFileHeader dfHeader = new DataFileHeader(df);
            if(dfHeader.isAbleToParseHeader()) { // this is set if we were successful, if not we run prescaler
              prescalerTypeCode = dfHeader.getType() / 1000;
              extHeaderKeywords = dfHeader.getExtendedHeaderKeywords();
            }
            if ((prescalerTypeCode != 1) &&  (prescalerTypeCode != 2)) {
              // we do only support prescaling 1000/2000 files, unless prescaling is external
              waitingOnPrescale=false;
              runPrescaleCommand = false;
              needScaleTable = false;
              M.warning("Interal prescaler only works with type 1000 and 2000 files");
            } else {
              if(!preferKeyword && (scaleTable!=null && !scaleTable.isEmpty())) {
                M.info("Running internal prescaler even though SCALETABLE was provided");
              }
              // okay, this is a single 1000/2000 file, so internal and keyword prescaling are supported
              scaleTable = new Table();
              if (preferKeyword) {
                if(scaleTableFromKeywordFromFile()) {
                  runPrescaleCommand = false;
                }
                else {
                  needWaitToOpenOutInFile = true; // have separate out/in file
                }
              }
            }
            df.close();
          }
          else {
            // not even a DataFile
            waitingOnPrescale=false;
            runPrescaleCommand = false;
            needScaleTable = false;
            M.warning("Interal prescaler only works with type 1000/2000 file defined");
          }
          if(runPrescaleCommand) {
            waitingOnPrescale = true;
            runPrescale();
          }
        }
      }
      return waitingOnPrescale;
    }

    /** This is used by the process() portion to set plot scaling based on the SCALETABLE
     *  once it becomes available.
     *
     *   @return true if SCALETABLE was found and processed (i.e. process can now start processing data),
     *           false if the SCALETABLE is still not available and so no processing of data will happen
     *           this process() cycle
     */
    private boolean processPrescalar () {
      if (tryLoadScaleTable()) {
        if(needWaitToOpenOutInFile || !M.isPiped()) openFile(inFileAfterPrescale);
        processScaleTable(); // need to do after the file is open. if we do it before, then the layer specific scale values are  not used
        return true;
      }
      else {
        return false;
      }
    }

    /** Function that creates the setupTable from the keyword in the data file. If unsuccessful
     *  it will create the keyword switch=value for {@link #runPrescale(String)}
     *  @param df The file to retrieve the setupTable
     *  @return false if the setupTable was not created from the keyword
     */
    private boolean scaleTableFromKeywordFromFile () {
      String keywordSwitchValue = MA.getS(prescaler.KEYWORD_SWITCH);
      if(keywordSwitchValue.equals("")) keywordSwitchValue = prescaler.KEYWORD_DEFAULT;
      prescalerKeyword = prescaler.KEYWORD_SWITCH  +"="+keywordSwitchValue;
      Object tempScale = extHeaderKeywords.get(keywordSwitchValue);
      if(tempScale != null) {
        scaleTable = new Table(tempScale.toString());
        return true;
      }
      return false;
    }
  }

  public MPlot getMPlot () {
    return MP;
  }

  public Args getMA () {
    return MA;
  }

  public Midas getMidasFromPlot () {
    return getMidas();
  }
}