package nxm.sys.libg;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Stack;

import nxm.sys.inc.Constants;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.ListFile;
import nxm.sys.inc.MessageHandler;
import nxm.sys.inc.MidasColor;
import nxm.sys.inc.PlotFile;
import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.inc.Units;
import nxm.sys.lib.Args;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.GeodeticUtil;
import nxm.sys.lib.Message;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Position;
import nxm.sys.lib.PredicateType;
import nxm.sys.lib.StateVector;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.Time;
import nxm.sys.lib.Transform;
import nxm.sys.lib.XmUtil;
import nxm.sys.prim.plot;

/**
 * A class to hold and plot type 5000 location data.
 * @author Beth Hassler / Jeff Schoen / Jay Hallam / J. Czechowski IV / Kevin Esterheld / Jeanette McCall
 *
 * A class for displaying Type 5000 or CSV files with record data containing
 * Points, Ellipses, and Cones. These points can also contain a NAME and NOTE.
 * Note: StateVector data (points with velocity and acceleration) are only supported
 * when using a DataFile, and not via CSV file or setting points directly.
 *
 * Points can be selected with a LMB or a Ctrl-LMB also brings up a popup describing
 * the point.  Though selection mode is defaulted to "GreatArc", the overall
 * selection mode will be IGNORED if the min/max altitude is something other than
 * zero. PIXEL based LMB selection is the only one that works with non-zero altitudes.
 *
 * Points are generally displayed in the order they arrive and the latest points
 * kept, but deleting any points will change this.  For efficiency, the deleted
 * points may be replaced with new points, causing arrival order to no longer
 * be maintained.  If keeping the latest points is critical, 'TimeOrder=True' can
 * be used to force the behavior of always keeping the latest buffer size number of
 * points.  A separate "TIME" field can be used to specify point times, if needed.
 *
 * To display points that occur only within a certain time window, the setTimeWindow
 * API can be used.
 *
 * To force the map LEGEND to display the the correct line color on one color ColorMaps
 * setAutoAdjustLineColor(true).  The default behavior is to show the line color
 * associated with LayerT5s default ColorMap
 *
 * The number of visible points in an individual LayerT5 or in a group of LayerT5s
 * that are part of a LayerDrawable can be displayed vie the plot extended readout
 * or access directly via the API getNumVisiblePoints.
 *
 * To group LayerT5s for point selection and counts, place them in a LayerDrawable group
 */
public class LayerT5 extends Layer {

  /** The list of selection modes. (Value: <code>{@value}</code>) */
  public static final String selectionModes = "Pixel,GreatArc";

  /** Field enable list. (Value: <code>{@value}</code>) */
  public static final String fieldList = "Symbols,Labels,Elps,Cones,Connect";

  /** The list of available flags. (Value: <code>{@value}</code>) */
  public static final String flagsList = "ConeFlagHoriz,ConeFlagVert,TimeScaleLabels,AutoScaleLabels,FillCones";

  /** Selection mode: Nearest using pixel coord. */
  public static final int NEAREST_BY_PIXELS = 1;

  /** Selection mode: Nearest using great arc. */
  public static final int NEAREST_BY_GREAT_ARC = 2;

  /** Field option: Show symbols. */
  public static final int SYMBOLS = 0x01;

  /** Field option: Show labels. */
  public static final int LABELS = 0x02;

  /** Field option: Show ellipses. */
  public static final int ELLIPSES = 0x04;

  /** Field option: Show cones. */
  public static final int CONES = 0x08;

  /** Field option: Connect points. */
  public static final int CONNECT = 0x10;

  /** Flag: Cones should extend to the horizon. */
  public static final int CONE_FLAG_HORIZ = 0x1;

  private PopUpHTML htmlPopup; // Since 3.9.3 to keep track of popup and close if necessary

  /** True if this is a ListFile - like CSV
   *  @since NeXtMidas 3.9.2 for NXM-3500
   */
  private boolean isListFile = false;

  /** This is to help with the graph range. On first pass, all the points need to be used to find the range
   *  Once first pass is complete, this value is set to false
   *  @since NeXtMidas 3.9.3
   */
  private boolean firstPassFindGraphRange = true;

  /** LayerT5 is typically geodetic data, but can be used for general X,Y,Z data */
  private boolean isGeodeticData = true;

  /** It is useful to ignore range on symbol layers that are over rasters */
  private boolean ignoreRange = false;

  /**
   * Flag: The flag value indicating that cones should be clipped at a plane vertical to their
   * intersection with the Earth's surface. If both {@link #CONE_FLAG_HORIZ} and
   * {@link #CONE_FLAG_VERT} are set then the vertical is shown as a dotted line.
   */
  public static final int CONE_FLAG_VERT = 0x2;

  /** Flag: Labels should be shown for time-scale plots. */
  public static final int SHOW_TIME_SCALE_LABELS = 0x4;

  /** Flag: Labels should be auto-scaled (hides labels until the user zooms in to avoid cluttering up the plot). */
  public static final int AUTO_SCALE_LABELS = 0x8;

  /** Flag: Cones and ellipses should be drawn filled rather than outlines. */
  public static final int FILL_CONES = 0x10;

  /** Matches ',' or "|" plus any whitespace before/after. */
  private static final String SPLIT_REGEX = "\\s*[,|]\\s*";

  /** Array of points to be displayed. */
  private Point3D[] points = null;


  /** show points as clusters when multiple points are in tight area */
  private boolean showClusters; // since 3.9.4 for NXM-2968
  /** constantly add points to region groups, so ready when time to show them */
  private boolean formRegionGroups;
  RegionGroup[][] regionGroups =
      new RegionGroup[MPlot.LONGITUDE_RANGE/RegionGroup.LON_GRID_SIZE][MPlot.LATITUDE_RANGE/RegionGroup.LAT_GRID_SIZE];
  private boolean doPosp, doVec, doCones, doEllipses, doPos;

  /** Number of spots used in the points array */
  int numSpotsUsedInPointsArray = 0;

  /** Points that are nulled in the points array. */
  private Stack<Integer> pointsNulled = new Stack<>();

  /** Start index of the points array (-1=Variable size). */
  private int firstPoint = -1;

  /** Maximum size of points array for grow to size case (-1=Not in effect)*/
  private int maxPointsSize = -1;

  /** The selection mode used by AutoSelect. */
  private int autoSelMode = NEAREST_BY_GREAT_ARC;

  /** The selection range used by AutoSelect. */
  private double autoSelRange = 500000.0;

  /** The AutoSelect enable flags. */
  private int autoSelEnable = 0x00;

  /** The enables fields, defaults to all fields on.
   * @deprecated Since NeXtMidas 3.7.0 - use {@link #getField()} and {@link #setField(String)} instead
   */
  @Deprecated
  public int field = -1;

  /** Index of selected point (-1 for none selected).
   * @deprecated Since NeXtMidas 3.7.0 - use {@link #getSelectedIndex()} and {@link #setSelectedIndex(int)} instead
   */
  @Deprecated
  public int select = -1;

  /**
   * Toggle Label Region (show text if {@code viewable < tlreg }).
   * @deprecated Since NeXtMidas 3.7.0 - use {@link #getTLReg()} and {@link #setTLReg(double)} instead
   */
  @Deprecated
  public double tlreg = 10.0;

  /** The keys to include when using getSelectedText(). */
  private String[] selectedKeys = null;

  /** The component byte offset of the name field. */
  private int bName = -1;

  /** The component byte offset of the position field. */
  private int bPos = -1;

  /** The component byte offset of the position pointer field. */
  private int bPosp = -1;

  /** The component byte offset of the cone field. */
  private int bCone = -1;

  /** The component byte offset of the ellipse field. */
  private int bElps = -1;

  /** The component byte offset of the flags field. */
  private int bFlag = -1;

  /** The component byte offset of the connect flag field. */
  private int bConn = -1;

  /** The component byte offset of the note field. */
  private int bNote = -1;

  /** The component byte offset of the heading field. */
  private int bHdg = -1;

  /** The component byte offset of the vector field. */
  int bVec = -1;

  /** The component byte offset of the time field. */
  int bTime = -1;

  /** The component byte offset of the metadata field. */
  int bMeta = -1; // 3.9.3 experimental

  /** The component byte offset of the color field, which allows a color to be directly specified. */
  int bColor = -1; // 3.9.4 experimental

  /** The component byte offset of the symbol field */
  int bSymbol = -1;

  /** The component byte offset of the ellipsoid field */
  private int bEllipsoid = -1;

  /** The component dimensions of the vector field. */
  private int dVec = -1; // since 3.9.3 for NXM-3515

  /** The component format for the name field. */
  private String fName = null;

  /** The component format for the note field. */
  private String fNote = null;

  /** The component format for the cone field. */
  private String fCone = null;

  /** The component format for the metadata field. */
  private String fMeta = null; // 3.9.3 experimental

  /** The component format for the color field. */
  private String fColor = null; // 3.9.4 experimental

  /** The component format for the symbol field. */
  private String fSymbol = null;

  /** Transform to use (based on DataFile quadwords). */
  private Transform transform = null;

  /** Using a time axis? */
  private boolean timeAxis = false;

  /** Font size (0=Auto). */
  private int fontsize = 0;

  /** Font to use. */
  private Font font = null;

  /** Pix to use for {@code RWC -> pixel conversions}. */
  Pix pix = null;

  /** The data file being plotted */
  private DataFile df = null;

  /** Number of conic sections. */
  private int conic = 1;

  /** Conic divisions. */
  private int cdiv = 60;

  /** Conic angle. */
  private double cangle = 1;

  /** Conic range. */
  private double crange = 0;

  /** Ellipsoid slices */
  private int eSlices = 3;

  /** Labels should be on the left? */
  private boolean leftLabels = false;

  /** Color of the selected point. */
  private Color selectColor = Color.RED;

  /** Any flags set. */
  private int flags = 0x09;

  /** Indicates if initialization has been completed. */
  private boolean initialized = false;

  /** Indicates if always convert to LonLatAlt. */
  private boolean toLLA = false;

  /** Only used inside draw methods: Zoomed in enough to show labels. */
  private boolean _labelZoom = false;

  /** Only used inside draw methods: show symbols. */
  private boolean _symbols = false;

  /** Only used inside draw methods: show labels. */
  private boolean _labels = false;

  /** Only used inside draw methods: connect points. */
  private boolean _connect = false;

  /** Only used inside draw methods: connect all points. */
  private boolean _connectAll = false;

  /** Only used inside draw methods: is it selected. */
  private boolean _selected = false;

  /** Only used inside draw methods: color to use. */
  private Color _color = null;

  /** Only used inside draw methods: show all points. */
  private boolean _showAll = false;

  /** Have minLon/maxLon/... been set yet? */
  private boolean minMaxSet = false;

  /** Minimum longitude. */
  private double minLon = 0.0;

  /** Maximum longitude. */
  private double maxLon = 0.0;

  /** Minimum latitude. */
  private double minLat = 0.0;

  /** Maximum latitude. */
  private double maxLat = 0.0;

  /** Minimum altitude. */
  private double minAlt = 0.0;

  /** Maximum altitude. */
  private double maxAlt = 0.0;

  /** Vector scale factor. */
  private double vscale = 0.0;

  /** whether or not to optimize drawing (warning: prevents some lines from displaying). */
  private boolean quickDraw = false;

  private Point3D timePoint = null;

  // The following are variables used for storing the format of each column in the csv file. Since 3.9.3
  private char ellipseMajorFormat,ellipseMinorFormat,ellipseTiltFormat,ellipsoidMajorFormat,ellipsoidMinorFormat,ellipsoidMedianFormat,
  ellipsoidRotationXFormat,ellipsoidRotationYFormat,ellipsoidRotationZFormat,xFormat,yFormat,zFormat,nFormat,pospXFormat,pospYFormat,pospZFormat,
  coneAFormat,coneBFormat,coneAngleFormat,flagNoteFormat,timeFormat,metaFormat,colorFormat,symbolFormat;

 // The following are variables used for storing the byte position of each column in the csv file. Since 3.9.3
  private int bEllipseMajor,bEllipseMinor,bEllipseTilt,bEllipsoidMajor,bEllipsoidMinor,bEllipsoidMedian,bEllipsoidRotationX,bEllipsoidRotationY,bEllipsoidRotationZ,
  bPosX,bPosY,bPosZ,bPospX,bPospY,bPospZ,bConeA,bConeB,bConeAngle,bFlagNote,bTimeLF,bMetaPos,bColorPos,bSymbolPos;

  // Arrays that will hold the flag formats and the byte positions in the buffer.
  private char[] flagFormats = buildFormatsArray();
  private int[] bFlags = buildFlagBytePosArray();

  // METADATA FIELDS - experimental in 3.9.4
  private String[] metadataFieldNames;
  private String[] fMetas;
  private int[]    bMetas;
  private char[]   metaFormats;
  private int[]    bMetasPos;
  // Filtering / highlighting of points matching filter support
  private Map<String,PredicateType> visibilityFilter;
  private Map<String,HighlightSetting> highlightSettingMap = new HashMap<>();
  /** Need to populate ArrayList of points matching visibility filter */
  volatile boolean needFilteredPoints = false;
  /** ArrayList containing one pass worth of points matching visibility filter */
  ArrayList<Point3D> filteredPoints;
  /** Sleep interval for checking if filtered points are ready */
  static final double SLEEP_INTERVAL = 0.01;
  // add this entry to the filter data table
  private boolean addNameFD, addPosFD, addPospFD, addElpsFD, addConeFD, addTimeFD, addNoteFD, addMetaFD;

  /** x Range - used when plotting points on 3D graph */
  private double xRange = 1.0;

  /** y Range - used when plotting points on 3D graph */
  private double yRange = 1.0;

  /** z Range - used when plotting points on 3D graph */
  private double zRange = 1.0;

  /** number of points visible - updated each draw */
  private int numVisiblePoints = 0;

  /** minimum cluster size */
  int minimumClusterSize = 5;

  private double tDelta = 1;

  /** Time at this transfer length worth of data */
  private double tlTime = 0;

  /** curently displaying only points within a specified time window */
  private boolean activeTimeWindow = false;

  // Used with point time window - 3.9.3 for NXM-3515
  /** Time Window Setting - Inactive */        public  static final int INACTIVE = -1;
  /** Time Window Setting - Window off */      public  static final int WINDOW_OFF   = 0;
  /** Time Window Setting - Window fixed */    public  static final int WINDOW_FIXED = 1;
  /** Time Window Setting - Window Relative */ public  static final int WINDOW_RELATIVE = 2;
  /** Time Window Setting - Window Relative to Newest Point */ public  static final int WINDOW_RELATIVE_POINT = WINDOW_RELATIVE;
  /** Time Window Setting - Window Relative to Clock*/         public  static final int WINDOW_RELATIVE_CLOCK = WINDOW_RELATIVE + 1;
  /** newest time stamp of any point received */
  private double  newestTimeReceived = 0;
  /** oldest time stamp visible */
  private double  oldestTimeVisible = INACTIVE;
  /** newest time stamp visible */
  private double  newestTimeVisible = INACTIVE;
  /** relativity mode for oldest time stamp visible */
  private double  relativeOldestTimeVisible = INACTIVE;
  /** relativity mode for newest time stamp visible */
  private double  relativeNewestTimeVisible = INACTIVE;
  /** relative window enabled */
  private boolean relativeTimeWindow = false;
  /** relativity mode */
  private int     relativeMode = WINDOW_RELATIVE_POINT; //default to using point time for relative time window
  /** Need to keep the time order of points received */
  private boolean needsTimeOrder = false;
  /** active Point indices in order oldest to newest
   *  this is currently just used to remove oldest points
   *  (i.e. only have the newest in the points array)
   *  this could be used in the future to traverse points in time order
   *  normally this is not used during the draw, as time can be
   *  part of the visibility check
   */
  private LinkedList<Integer> timeSortOfPtIndicies = null; // 3.9.3 NXM-3626
  /** Allow a user to set size of points buffer, even when reading from a fixed file - 3.9.3 */
  private static boolean filePointsSizeSettable = true; // 3.9.3 NXM-3552


  /** This is for use when a time set to 0 is needed. */
  private static final Time ZERO_TIME = new Time(0);

  private static final double PIXEL_MAX_RANGE = 625;

  /** Messages send upon selection of a point or deselection of all points */
  private boolean sendSelectMessages = false; // NXM-3582

  /** Messages send when number of highlighted points changes */
  private boolean sendHighlightTotalMessages = false;

  private MessageHandler mwMsgHandler = null; // since 3.9.3

  // Fields that allow older points (arrival time) to fade to fadeColor
  // which allows for better viewing of tracks
  /** Is track color fading enabled */
  private boolean colorFade = false;
  /** Number of points per gradual fade shift */
  private int colorFadeFactor = 5;
  /** default red component of fade color */
  private int fadeColorR = 50;
  /** default green component of fade color */
  private int fadeColorG = 50;
  /** default blue component of fade color */
  private int fadeColorB = 50;

  /** point X margin as decimal percent for XY views in XYZ mode */
  private static double xyPointXMargin = 0;
  /** point Y margin as decimal percent for XY views in XYZ mode */
  private static double xyPointYMargin = 0;

  /** point popup is active */
  private boolean activePopup; // keep track if there was an active popup when layer was hidden
  /** Table containing alternate labels for use in XYZ mode popups */
  Table popupLabels = new Table();

  /** keep LEGEND colormap and line color matching */
  private static boolean autoAdjustLineColor = false; // 3.9.3 NXM-3574 - allows line color to follow colormap, so LEGEND renders correctly

  /** Default constructor. */
  public LayerT5() {
    super();
    // Initialize all byte positions to -1
    bEllipseMajor=bEllipseMinor=bEllipseTilt=bEllipsoidMajor=bEllipsoidMinor=bEllipsoidMedian=bEllipsoidRotationX=bEllipsoidRotationY=bEllipsoidRotationZ=
        bPosX=bPosY=bPosZ=bPospX=bPospY=bPospZ=bConeA=bConeB=bConeAngle=bFlagNote=bTimeLF=bMetaPos=bColorPos=bSymbolPos=-1;
    // initialize all formats to ' '
    ellipseMajorFormat=ellipseMinorFormat=ellipseTiltFormat=ellipsoidMajorFormat=ellipsoidMinorFormat=ellipsoidMedianFormat=
        ellipsoidRotationXFormat=ellipsoidRotationYFormat=ellipsoidRotationZFormat=xFormat=yFormat=zFormat=nFormat=pospXFormat=pospYFormat=pospZFormat=
        coneAFormat=coneBFormat=coneAngleFormat=flagNoteFormat=timeFormat=metaFormat=colorFormat=symbolFormat=' ';
    name = "T5";
    enable &= ~MPlot.EN_BSTORE;
    line.setRadius(3);
    line.setSymbol("CROSS");
    line.getSymbol().setSize(4);
    line.setType(Line.SOLID | Line.SYMBOL);
    needRefresh = true; // JC4: Prevents race condition.
    pix = new Pix(1, 0);
    zaxis = true;
  }

  /**
   * Constructor with default size.
   * @param size The default size.
   */
  public LayerT5(int size) {
    this();
    setSizeWithForceOpt(size, false); // 3.9.3 NXM-3552
  }

  /**
   * Set the fields to be enabled.
   * @param value This is a mask value of the field elements.
   */
  public void setField(int value) {
    field = value;
    refresh();
  }

  /** Get Full set of Points as a List
   *  @return points as a List of Point3D
   */
  public List<Point3D> getPointsAsList () {
    if (points == null) {
      return new ArrayList<>();
    }
    else {
      if (numSpotsUsedInPointsArray==points.length && pointsNulled.isEmpty()) {
        // points array is full of points
        return Arrays.asList(points);
      }
      else {
        // array is not full or contains nulled out points
        List<Point3D> pointsAsList = new ArrayList<>();
        for (int i=0; i < numSpotsUsedInPointsArray; i++) {
          if (points[i] != null) pointsAsList.add(points[i]);
        }
        return pointsAsList;
      }
    }
  }

  /** Get Points as LatLon or LatLonAlt double[] array
   *
   *  @param dim 2 or 3 dimentional points
   *  @return double array with points
   */
  @InternalUseOnly
  public double[] getArrayPoints (int dim) {
    if (points == null) {
      return new double[0];
    }
    else {
      int maxcoords = numSpotsUsedInPointsArray*dim;
      int actcoords = maxcoords;
      double[] coordstemp = new double[maxcoords];
      for (int i=0,c=0; i < numSpotsUsedInPointsArray; i++,c+=dim) {
        if (points[i] != null) {
          coordstemp[c]   = points[i].posLon;
          coordstemp[c+1] = points[i].posLat;
          if (dim==3) coordstemp[c+2] = points[i].posAlt;
        }
        else {
          actcoords -= dim;
        }
      }
      if (maxcoords == actcoords) return coordstemp;
      return Arrays.copyOf(coordstemp, actcoords);
    }
  }

  /** Set the collection of points matching the current filter in motion
   *  Caution: If an unfulfilled request is in progress, this will just return the
   *  results of the already in progress collection of points matching the filter
   *  @return point matching the current filter
   */
  public List<Point3D> getPointsMatchingFilter () {
    // if there is no filter, all points match the request
    if ((visibilityFilter == null) && (activeTimeWindow == false)) {
      return getPointsAsList();
    }

    // check if a request is already in process
    // if so, we can just use those points
    // this does assume the visibility filter does not change in the middle of an unfilled request
    if (!needFilteredPoints) {
      needFilteredPoints = true;
      refresh();
    }

    int whileCount = 0;
    // wait until points are ready
    while (needFilteredPoints) {
      Time.sleep(SLEEP_INTERVAL);
      whileCount++;
    }
    if (false) System.out.println(name+ " # interations while loop "+whileCount);
    return filteredPoints;
  }

  /**
   * Set the fields to be enabled.
   * @param value This is a mask value of the field elements in {@link #fieldList}.
   */
  public void setField(String value) {
    setField(Parser.mask(fieldList, value, field));
  }

  /** Get the fields to be enabled.
   *  @return The fields enabled (see {@link #fieldList}).
   */
  public String getField() {
    return Parser.mask2s(fieldList, field);
  }

  /** Set the file input.
   *  @param pf The plot file to read.
   *  @param tab Layer table associated with the file
   *  @param MA <i>Not Used</i>
   *  @return <code>true</code> when the file has been opened, <code>false</code> if the file can not
   *          be opened (usually indicates the file is not a Type 5000 file).
   */
  @Override
  public synchronized boolean setFile(PlotFile pf, Table tab, Args MA) {
    setFile(pf);
    if (typeClass != 5 && !(pf instanceof ListFile)) // added limited CSV support in 3.9.3
      return false;

    // EXPERIMENTAL
    String pipeSeparatedMetaNames = (tab==null) ? null : tab.getS("METADATAFIELDS");
    metadataFieldNames = (pipeSeparatedMetaNames==null || pipeSeparatedMetaNames.isEmpty()) ?
        null : tab.getS("METADATAFIELDS").split("\\|");
    String filterDataFields = (tab==null) ? null : tab.getS("FILTERDATAFIELDS");
    setFilterDataEntries(filterDataFields);

    if (pf instanceof DataFile) {
      df = (DataFile) pf;
      DataFile df = (DataFile) pf;
      // since 3.9.3 for NXM-3515, allow point time to be set
      tDelta = df.getDelta();
      tlTime = df.getTimeAt();

      int iPos       = df.findRec("POS");
      int iPosp      = df.findRec("POSP");
      int iCone      = df.findRec("CONE");
      int iName      = df.findRec("NAME");
      int iNote      = df.findRec("NOTE");
      int iFlag      = df.findRec("FLAG");
      int iElps      = df.findRec("ELPS");
      int iConn      = df.findRec("CONN");
      int iHdg       = df.findRec("HDG");
      int iTime      = df.findRec("TIME");
      int iMeta      = df.findRec("METADATA"); // EXPERIMENTAL - not supported - since 3.9.3
      int iColor     = df.findRec("COLOR"); // EXPERIMENTAL - not supported - since 3.9.4
      int iSymbol    = df.findRec("SYMBOL");
      int iEllipsoid = df.findRec("ELLIPSOID");
      if (iEllipsoid == -1) iEllipsoid = df.findRec("ELPSOID");

      bPos   = (iPos   >= 0) ? df.getCompOffset(iPos)   : -1;
      bPosp  = (iPosp  >= 0) ? df.getCompOffset(iPosp)  : -1;
      bCone  = (iCone  >= 0) ? df.getCompOffset(iCone)  : -1;
      bName  = (iName  >= 0) ? df.getCompOffset(iName)  : -1;
      bNote  = (iNote  >= 0) ? df.getCompOffset(iNote)  : -1;
      bFlag  = (iFlag  >= 0) ? df.getCompOffset(iFlag)  : -1;
      bElps  = (iElps  >= 0) ? df.getCompOffset(iElps)  : -1;
      bConn  = (iConn  >= 0) ? df.getCompOffset(iConn)  : -1;
      bHdg   = (iHdg   >= 0) ? df.getCompOffset(iHdg)   : -1;
      bTime  = (iTime  >= 0) ? df.getCompOffset(iTime)  : -1;
      bMeta  = (iMeta  >= 0) ? df.getCompOffset(iMeta)  : -1;
      bColor = (iColor >= 0) ? df.getCompOffset(iColor) : -1;
      bSymbol = (iSymbol >= 0) ? df.getCompOffset(iSymbol) : -1;
      bEllipsoid = (iEllipsoid >= 0) ? df.getCompOffset(iEllipsoid) : -1;

      fCone  = df.getCompFormat(iCone);
      fName  = df.getCompFormat(iName);
      fNote  = df.getCompFormat(iNote);
      fMeta  = df.getCompFormat(iMeta);
      fColor = df.getCompFormat(iColor);
      fSymbol = df.getCompFormat(iSymbol);

      setDFMultiFieldMetadata(df);

      if (df.isPipe())
        firstPoint = 0; // Set to use a fixed number of points
    }
    else if (pf instanceof ListFile) {
      setupListFile((ListFile)pf, tab); // Extracted logic to own method for readability since 3.9.3
    }
    setSizeWithForceOpt((int) pf.getSize(),false);
    setData(pf.getDataBuffer(size));
    if (df != null) {
      transform = new Transform(df); //TODO df == null case
      toLLA = !df.getReferenceFrame().equals("NONE"); //TODO df == null case
    }
    timeAxis = (pf.getXUnits() == Units.TIME);
    quickDraw = (MA!=null)? MA.getState("/QUICKDRAW", quickDraw) : false;
    return true;
  }

  private void setFilterDataEntries (String fields) {
    if (fields==null || fields.isEmpty()) {
      fields = "META"; // default to metadata table
    }
    fields = fields.toUpperCase().trim();
    boolean addAll = "ALL".equals(fields);
    if (addAll) {
      addNameFD = addPosFD = addPospFD = addElpsFD= addConeFD = true;
      addTimeFD = addNoteFD = addMetaFD = true;
    }
    else {
      String[] fieldNames = (fields==null || fields.isEmpty()) ?
          null : fields.split("\\|");
      List<String> fieldList = Arrays.asList(fieldNames);
      addNameFD = fieldList.contains("NAME");
      addPosFD  = fieldList.contains("POS");
      addPospFD = fieldList.contains("POSP");
      addElpsFD = fieldList.contains("ELPS");
      addConeFD = fieldList.contains("CONE");
      addTimeFD = fieldList.contains("TIME");
      addNoteFD = fieldList.contains("NOTE");
      addMetaFD = fieldList.contains("META");
    }
  }

  private void setDFMultiFieldMetadata (DataFile df) {
    if (metadataFieldNames == null) return;
    bMetas = new int[metadataFieldNames.length];
    fMetas = new String[metadataFieldNames.length];
    int i = 0;
    for (String field : metadataFieldNames) {
      int index = df.findRec(field);
      bMetas[i] = (index >= 0) ? df.getCompOffset(index) : -1;
      fMetas[i] = df.getCompFormat(index);
      i++;
    }
  }

  /** Setup LayerT5 to parse the records from the file.
   *
   *  @param pf   The List File to be parsed.
   *  @param tab  Table that has the column information
   *  @since 3.9.3
   */
  private void setupListFile (ListFile lf, Table tab) {
    isListFile = true;
    if (tab==null) tab = new Table(); // Prevent NPE and use the default headers.
    String xRecName               = tab.getS("X", "X");
    String yRecName               = tab.getS("Y", "Y");
    String zRecName               = tab.getS("Z", "Z");
    String nRecName               = tab.getS("LABEL","NAME");
    String ellipseMajorName       = tab.getS("EMAJ","EMAJ");
    String ellipseMinorName       = tab.getS("EMIN","EMIN");
    String ellipseTiltName        = tab.getS("ETILT","ETILT");
    String ellipsoidMajorName     = tab.getS("ELLIPSOIDMAJ","ELLIPSOIDMAJ");
    String ellipsoidMinorName     = tab.getS("ELLIPSOIDMIN","ELLIPSOIDMIN");
    String ellipsoidMedianName    = tab.getS("ELLIPSOIDMED","ELLIPSOIDMED");
    String ellipsoidRotationXName = tab.getS("ELLIPSOIDROTX","ELLIPSOIDROTX");
    String ellipsoidRotationYName = tab.getS("ELLIPSOIDROTY","ELLIPSOIDROTY");
    String ellipsoidRotationZName = tab.getS("ELLIPSOIDROTZ","ELLIPSOIDROTZ");
    String[] flagRecNames   = new String[8];
    for (int i = 0; i < flagRecNames.length; i++) {
      String key = "F"+(i+1);
      flagRecNames[i] =tab.getS(key,key);
    }
    String coneAName        = tab.getS("CONEA", "CONEA");
    String coneBName        = tab.getS("CONEB", "CONEB");
    String coneAngleName    = tab.getS("CONEANGLE", "CONEANGLE");
    String pospXName        = tab.getS("POSPX", "POSPX");
    String pospYName        = tab.getS("POSPY", "POSPY");
    String pospZName        = tab.getS("POSPZ", "POSPZ");
    String flagNoteName     = tab.getS("NOTE","NOTE");
    String timeName         = tab.getS("TIMELF","TIME");
    String metaName         = tab.getS("METADATA","METADATA");
    String colorName        = tab.getS("COLOR","COLOR");
    String symbolName       = tab.getS("SYMBOL","SYMBOL");

    int bPosX,bPosY,bPosZ,bName,bEllipseMajor,bEllipseMinor,bEllipseTilt,bEllipsoidMajor,bEllipsoidMinor,bEllipsoidMedian,
    bEllipsoidRotationX,bEllipsoidRotationY,bEllipsoidRotationZ,bConeA,bConeB,bConeAngle,bPospX,bPospY,bPospZ,bFlagNote,bTimeLF,bMetaPos,bColorPos,bSymbolPos;
    bPosX=bPosY=bPosZ=bName=bEllipseMajor=bEllipseMinor=bEllipseTilt=bEllipsoidMajor=bEllipsoidMinor=bEllipsoidMedian=
        bEllipsoidRotationX=bEllipsoidRotationY=bEllipsoidRotationZ=bConeA=bConeB=bConeAngle=bPospX=bPospY=bPospZ=bFlagNote=bTimeLF=bMetaPos=bColorPos=bSymbolPos= 0;
    int[] bFlags = new int[8];

    // experimental support for multiple metadata fields
    List<String> metaNamesList;
    if (metadataFieldNames == null) {
      metaNamesList = null;
    }
    else {
      metaNamesList = Arrays.asList(metadataFieldNames);
      this.metaFormats = new char[metadataFieldNames.length];
      this.bMetasPos   = new int[metadataFieldNames.length];
      Arrays.fill(metaFormats,' ');
    }

    for (int i = 0; i < lf.getRecordDefCount(); i++) {
      Table  recDef = lf.getRecordDef(i);
      String fmt    = recDef.getS("FORMAT", "XA");

      if (xRecName.equals(recDef.getS("NAME"))) {
        this.xFormat = fmt.charAt(1);
        this.bPosX = recDef.getL("OFFSET", bPosX);
      }
      else if (yRecName.equals(recDef.getS("NAME"))) {
        this.yFormat = fmt.charAt(1);
        this.bPosY = recDef.getL("OFFSET", bPosY);
      }
      else if (zRecName.equals(recDef.getS("NAME"))) {
        this.zFormat = fmt.charAt(1);
        this.bPosZ = recDef.getL("OFFSET", bPosZ);
      }
      else if (nRecName.equals(recDef.getS("NAME"))) {
        this.nFormat = fmt.charAt(1);
        this.bName = recDef.getL("OFFSET",bName);
      }
      else if (ellipseMajorName.equals(recDef.getS("NAME"))) {
        this.ellipseMajorFormat = fmt.charAt(1);
        this.bEllipseMajor = recDef.getL("OFFSET",bEllipseMajor);
      }
      else if (ellipseMinorName.equals(recDef.getS("NAME"))) {
        this.ellipseMinorFormat = fmt.charAt(1);
        this.bEllipseMinor = recDef.getL("OFFSET",bEllipseMinor);
      }
      else if (ellipseTiltName.equals(recDef.getS("NAME"))) {
        this.ellipseTiltFormat = fmt.charAt(1);
        this.bEllipseTilt = recDef.getL("OFFSET",bEllipseTilt);
      }
      else if (ellipsoidMajorName.equals(recDef.getS("NAME"))) {
        this.ellipsoidMajorFormat = fmt.charAt(1);
        this.bEllipsoidMajor = recDef.getL("OFFSET",bEllipsoidMajor);
      }
      else if (ellipsoidMinorName.equals(recDef.getS("NAME"))) {
        this.ellipsoidMinorFormat = fmt.charAt(1);
        this.bEllipsoidMinor = recDef.getL("OFFSET",bEllipsoidMinor);
      }
      else if (ellipsoidMedianName.equals(recDef.getS("NAME"))) {
        this.ellipsoidMedianFormat = fmt.charAt(1);
        this.bEllipsoidMedian = recDef.getL("OFFSET",bEllipsoidMedian);
      }
      else if (ellipsoidRotationXName.equals(recDef.getS("NAME"))) {
        this.ellipsoidRotationXFormat = fmt.charAt(1);
        this.bEllipsoidRotationX = recDef.getL("OFFSET",bEllipsoidRotationX);
      }
      else if (ellipsoidRotationYName.equals(recDef.getS("NAME"))) {
        this.ellipsoidRotationYFormat = fmt.charAt(1);
        this.bEllipsoidRotationY = recDef.getL("OFFSET",bEllipsoidRotationY);
      }
      else if (ellipsoidRotationZName.equals(recDef.getS("NAME"))) {
        this.ellipsoidRotationZFormat = fmt.charAt(1);
        this.bEllipsoidRotationZ = recDef.getL("OFFSET",bEllipsoidRotationZ);
      }
      else if (coneAName.equals(recDef.getS("NAME"))) {
        this.coneAFormat = fmt.charAt(1);
        this.bConeA = recDef.getL("OFFSET",bConeA);
      }
      else if (coneBName.equals(recDef.getS("NAME"))) {
        this.coneBFormat= fmt.charAt(1);
        this.bConeB = recDef.getL("OFFSET",bConeB);
      }
      else if (coneAngleName.equals(recDef.getS("NAME"))) {
        this.coneAngleFormat = fmt.charAt(1);
        this.bConeAngle = recDef.getL("OFFSET",bConeAngle);
      }
      else if (pospXName.equals(recDef.getS("NAME"))) {
        this.pospXFormat = fmt.charAt(1);
        this.bPospX = recDef.getL("OFFSET",bPospX);
      }
      else if (pospYName.equals(recDef.getS("NAME"))) {
        this.pospYFormat = fmt.charAt(1);
        this.bPospY = recDef.getL("OFFSET",bPospY);
      }
      else if (pospZName.equals(recDef.getS("NAME"))) {
        this.pospZFormat = fmt.charAt(1);
        this.bPospZ = recDef.getL("OFFSET",bPospZ);
      }
      else if (timeName.equals(recDef.getS("NAME"))) {
        this.timeFormat = fmt.charAt(1);
        this.bTimeLF = recDef.getL("OFFSET",bTimeLF);
      }
      else if (metaName.equals(recDef.getS("NAME"))) { // 3.9.3 experimental
        this.metaFormat = fmt.charAt(1);
        this.bMetaPos = recDef.getL("OFFSET",bMetaPos);
      }
      else if (colorName.equals(recDef.getS("NAME"))) {
        this.colorFormat = fmt.charAt(1);
        this.bColorPos = recDef.getL("OFFSET",bColorPos);
      }
      else if (flagNoteName.equals(recDef.getS("NAME"))) {
        this.flagNoteFormat = fmt.charAt(1);
        this.bFlagNote = recDef.getL("OFFSET",bFlagNote);
      }
      else if (symbolName.equals(recDef.getS("NAME"))) {
        this.symbolFormat = fmt.charAt(1);
        this.bSymbolPos = recDef.getL("OFFSET",bSymbolPos);
      }
      else {
        // iterate the flag names and see if it is a match
        for(int j = 0; j < flagRecNames.length; j++) {
          if (flagRecNames[j].equals(recDef.getS("NAME"))) {
            this.flagFormats[j] = fmt.charAt(1);
            this.bFlags[j] = recDef.getL("OFFSET",bFlags[j]);
          }
        }
      }
      if ((metaNamesList != null) && metaNamesList.contains(recDef.getS("NAME"))) {
        // we have multiple fields contributing to the metadata
        int index = metaNamesList.indexOf(recDef.getS("NAME"));
        this.metaFormats[index] = fmt.charAt(1);
        this.bMetasPos[index] = recDef.getL("OFFSET",bMetasPos[index]);
      }

      if (xFormat == ' ') bPosX     += Data.getBPA(fmt);
      if (yFormat == ' ') bPosY     += Data.getBPA(fmt);
      if (zFormat == ' ') bPosZ     += Data.getBPA(fmt);
      if (nFormat == ' ') bName     += Data.getBPA(fmt);
      if (ellipseMajorFormat == ' ') bEllipseMajor += Data.getBPA(fmt);
      if (ellipseMinorFormat == ' ') bEllipseMinor += Data.getBPA(fmt);
      if (ellipseTiltFormat == ' ') bEllipseTilt += Data.getBPA(fmt);
      if (ellipsoidMajorFormat == ' ') bEllipsoidMajor += Data.getBPA(fmt);
      if (ellipsoidMinorFormat == ' ') bEllipsoidMinor += Data.getBPA(fmt);
      if (ellipsoidMedianFormat == ' ') bEllipsoidMedian += Data.getBPA(fmt);
      if (ellipsoidRotationXFormat == ' ') bEllipsoidRotationX += Data.getBPA(fmt);
      if (ellipsoidRotationYFormat == ' ') bEllipsoidRotationY += Data.getBPA(fmt);
      if (ellipsoidRotationZFormat == ' ') bEllipsoidRotationZ += Data.getBPA(fmt);
      if (pospXFormat == ' ') bPospX += Data.getBPA(fmt);
      if (pospYFormat == ' ') bPospY += Data.getBPA(fmt);
      if (pospZFormat == ' ') bPospZ += Data.getBPA(fmt);
      if (coneAFormat == ' ') bConeA += Data.getBPA(fmt);
      if (coneBFormat == ' ') bConeB += Data.getBPA(fmt);
      if (coneAngleFormat == ' ') bConeAngle += Data.getBPA(fmt);
      if (flagNoteFormat == ' ') bFlagNote += Data.getBPA(fmt);
      for (int j = 0;j<flagFormats.length;j++) {
        if (flagFormats[j] == ' ') bFlags[j] += Data.getBPA(fmt);
      }
      if (timeFormat  == ' ') bTimeLF   += Data.getBPA(fmt);
      if (colorFormat == ' ') bColorPos += Data.getBPA(fmt);
      if (symbolFormat == ' ') bSymbolPos += Data.getBPA(fmt);
      if (metaFormat  == ' ') bMetaPos  += Data.getBPA(fmt);
      if (metadataFieldNames != null) {
        for (int mf = 0; mf < metadataFieldNames.length; mf++) {
          if (metaFormats[mf] == ' ') bMetasPos[mf] += Data.getBPA(fmt);
        }
      }
    }

    // put warnings out if a point is missing data.
    if (xFormat == ' ') {
      warning("LayerPoint Can not find subrecord '"+xRecName+"' in "+this);
    }
    if (yFormat == ' ') {
      warning("LayerPoint Can not find subrecord '"+yRecName+"' in "+this);
    }

    setXAxis( 0, 1 ); //Initialize the x-axis start and delta
    setYAxis( 0, 1 ); //Initialize the y-axis start and delta
  }

  /** set vector name
   *  @param name vector name
   */
  public void setVName(String name) {
    int iVec = df.findRec(name); //TODO df == null case
    bVec = (iVec >= 0) ? df.getCompOffset(iVec) : -1; //TODO df == null case
    dVec = (iVec >= 0) ? Data.getSPA(df.getCompFormat(iVec).charAt(0)) : 1; //TODO df == null case
  }

  /** set vector scale value
   *  @param scale scale value
   */
  public void setVScale(double scale) {
    vscale = scale;
  }

  @Override
  public synchronized void setSize(int size) {
    //TODO check on this for CSV files
    setSizeWithForceOpt(size, filePointsSizeSettable); // 3.9.3 by default allow fixed Points size to be set on file data
  }

  /** setSize that allows a fixed size to be set, even when not dealing with pipe data
   *  Note: expect this to become private, but temporarily making public in case new
   *  behavior causes some breakage
   *
   *  @param size           size of points buffer
   *  @param forceFixedSize true if should force points buffer to a fixed size
   *  @since NeXtMidas 3.9.3 for NXM-3552
   */
  @InternalUseOnly
  public synchronized void setSizeWithForceOpt (int size, boolean forceFixedSize) {
    super.setSize(size);
    if (forceFixedSize && (firstPoint==-1)) firstPoint = 0; // add means of forcing setSize to work when not in pipe for testing NXM-3515
    if ((df == null) && !isListFile) {
      // This is for cases that we have no file and are just holding things in memory
      // e.g. xgeoplot  - Since 3.7.0 for Bug 2917 & Bug 2965
      initialized = true; // no init needed
      if (points == null) points = new Point3D[size];
      else if (points.length != size) points = Arrays.copyOf(points, size);
      // TODO add time points array stuff here
    }
    else if (!initialized) { // 3.9.4 - need to check as this hits here after setPoint is called
      // At this point initialized should be false, so that we go and initialize it.
      // There are two special cases: file size is 0 and file is a pipe; in both
      // of these cases the initialization skips out, so we force initialized = true
      // so the plot can start up, and we can later add data to them.
      initialized = (!isListFile && ((size == 0) || (df.isPipe())));
      points = null;
    }
    colors = MColor.getColorMap(MColor.T5, -1);
    line.setColor(colors[MColor.T5_INDEX_TRACE5000]);
  }

  /** Allows users to switch to earlier behavior that always expanded the points buffer to hold
   *  all a file's data, even when size was directly set.  Though size is primarily intended for pipe data,
   *  by the time the user specifically sets size it is now assumed they mean it.
   *
   *  @param isSettable set to false to revert to pre-NeXtMidas 3.9.3 behavior
   *  @since NeXtMidas 3.9.3 for NXM-3552
   */
  @ProvisionalUseOnly("Please let the NeXtMidas team know if you need original behavior")
  public void setFilePointsSizeSettable (String isSettable) {
    filePointsSizeSettable = isSettable.toLowerCase().startsWith("true");
  }

  /** Allows users to switch to earlier behavior that always expanded the points buffer to hold
   *  all a file's data, even when size was directly set.  Though size is primarily intended for pipe data,
   *  by the time the user specifically sets size it is now assumed they mean it.
   *
   *  @param isSettable set to false to revert to pre-NeXtMidas 3.9.3 behavior
   *  @since NeXtMidas 3.9.3 for NXM-3552
   */
  public void setFilePointsSizeSettable (boolean isSettable) {
    filePointsSizeSettable = isSettable;
  }

  /** Allow a grow to this maximum size for the points buffer
   *  @param maxPointsSize maximum size for points buffer to grow to
   */
  public void setMaxPointsSize (int maxPointsSize) {
    this.maxPointsSize = maxPointsSize;
  }

  @Override
  public synchronized void process() {
    super.process();
    if (time > 0 && !initialized) {
      initialized = true;
      matchTimePoint();
      return;
    }

    if (df == null && !isListFile) { // keep going if it is a list file since 3.9.3
      // This is for cases that we have no file and are just holding things in memory
      // e.g. xgeoplot - Since 3.7.0 for Bug 2917 & Bug 2965
      initialized = true;
      return;
    }

    // if one of the options is enabled that requires points be accessed in order
    // of arrival is set, we should not try to insert points in the positions
    // nulled due to points being deleted - since 3.9.3 for NXM-3515

    for (int i = 0; i < data.getSize(); i++) {
      int boff = i * data.bpe;
      // 2 different ways to get points based on file type since 3.9.3
      if (isListFile) addPoint(getPointListFile(boff));
      else {
        // default for point time is based on df header information
        // 3.9.3 TODO: may only want to set in some cases?
        addPoint(getPoint5000(boff, tlTime + (tDelta * i)));
      }
    }

    // since the data has already been read .. set for next TL
    // 3.9.3 TODO this does not take into account pipe restarts .. how likely with 5000?
    if (df !=null) {
      // 3.9.3 TODO: may only want to set in some cases?
      tlTime = df.getTimeAt();
    }

    initialized = true;
  }

  Point3D getPoint5000 (int boff, double time) {
    Point3D point = new Point3D(this);
    if (bPos != -1) { // get the Position Vector
      int off = bPos + boff;
      double[] pos = new double[] { data.unpackD(off), data.unpackD(off + 8),
          data.unpackD(off + 16) };
      if (toLLA)
        transform.toLonLatAlt(pos, 1);
      point.setPos(pos[2], pos[1], pos[0]);
    }
    if (bVec != -1) { // get the Position Pointer Vector
      int off = bPos + boff;
      int voff = bVec + boff;
      double[] posp = new double[] { data.unpackD(off + 0), data.unpackD(off + 8),
          data.unpackD(off + 16) };
      for (int j = 3 - dVec; j < 3; j++, voff += 8)
        posp[j] += vscale * data.unpackD(voff);
      if (toLLA)
        transform.toLonLatAlt(posp, 1);
      point.setPosp(posp[2], posp[1], posp[0]);
    }
    if (bPosp != -1) { // get the Position Pointer Vector
      int off = bPosp + boff;
      double[] posp = new double[] { data.unpackD(off), data.unpackD(off + 8),
          data.unpackD(off + 16) };
      if (toLLA)
        transform.toLonLatAlt(posp, 1);
      point.setPosp(posp[2], posp[1], posp[0]);
    }
    if (bCone != -1) { // get the Conic Vector
      int off = bCone + boff;
      if (fCone.charAt(0) == 'V') {
        point.setCone(data.unpackD(off), data.unpackD(off + 8), data.unpackD(off + 16));
      }
      else { // For old files with single-element cone.
        // JC4 -- Changed from unpackD(off+16) in 2.4.1 to unpackD(off) in 2.5.0
        point.setCone(0.0, 0.0, data.unpackD(off));
      }
    }
    if (bElps != -1) { // get the Ellipse Vector
      int off = bElps + boff;
      point.setElps(data.unpackD(off), data.unpackD(off + 8), data.unpackD(off + 16));
    }
    if (bFlag != -1) { // get the flags
      int off = bFlag + boff;
      point.setFlagNameLen(data.unpackB(off + 0));
      point.setFlagNamePos(data.unpackB(off + 1));
      point.setFlagFgColor(data.unpackB(off + 2));
      point.setFlagBgColor(data.unpackB(off + 3));
      point.setFlagCone(data.unpackB(off + 4));
      point.setFlagInvert(data.unpackB(off + 5));
      point.setFlagSymCode(data.unpackB(off + 6));
      point.setFlagSymSize(data.unpackB(off + 7));
    }
    if (bConn != -1) { // get the connect flags
      int off = bConn + boff;
      point.setConn(data.unpackB(off));
    }
    if (bHdg != -1) { // get the heading value
      int off = bHdg + boff;
      point.setHdg(data.unpackF(off));
    }
    if (bName != -1) { // get the point name
      int off = bName + boff;
      int len = Data.getBPA(fName);
      point.setName(data.unpackS(off, len));
    }
    if (bNote != -1) { // get the point note
      int off = bNote + boff;
      int len = Data.getBPA(fNote);
      point.setNote(data.unpackS(off, len));
    }

    // 3.9.3 TODO: decide if this should be conditional
    if (bTime != -1) { // get the point note
      int off = bTime + boff;
      point.setTime(data.unpackD(off));
    }
    else {
      point.setTime(time);
    }

    if (bColor != -1) { // get a direct color specification for the point
      int off = bColor + boff;
      int len = Data.getBPA(fColor);
      String colorValue = data.unpackS(off, len);
      // next line is temporary until we fix our CSV to BLUE process
      colorValue = colorValue.replace("(", "").replace(")","");
      point.setDirectColor(colorValue);
    }

    // 3.9.3 - EXPERIMENTAL
    if (bMetas != null) {
      Table metaTable = new Table();
      for (int i=0; i < bMetas.length; i++) {
        if (bMetas[i] != -1) {
          int off = bMetas[i] + boff;
          Object value = unpackData(fMetas[i], data, off);
          metaTable.put(metadataFieldNames[i], value);
        }
      }
      point.setMetadata(metaTable);
    }
    else if (bMeta != -1) { // get the point metadata
      int off = bMeta + boff;
      int len = Data.getBPA(fMeta);
      point.setMetadata(data.unpackS(off, len));
    }

    if (bSymbol != -1) {
      int off = bSymbol + boff;
      int len = Data.getBPA(fSymbol);
      String symbolName = data.unpackS(off, len);
      if (!symbolName.isEmpty()) {
        point.setSymbol(symbolName);
      }
    }

    if (bEllipsoid != -1) { // get the Ellipsoid parameters
      int off = bEllipsoid + boff;
      double[] ellipsoid = new double[] { data.unpackD(off), data.unpackD(off + 8),
          data.unpackD(off + 16), data.unpackD(off + 24), data.unpackD(off + 32),
          data.unpackD(off + 40)};
      point.setEllipsoid(ellipsoid[0], ellipsoid[1], ellipsoid[2], ellipsoid[3], ellipsoid[4], ellipsoid[5]);
    }
    return point;
  }

  // since 3.9.4
  private Object unpackData (String fmt, Data data, int off) {
    int len;
    char type;
    if (fmt==null) fmt="";
    switch (fmt.length()) {
      case 1:
        len = Data.getBPA("X"+fmt);
        type = fmt.charAt(0);
        break;
      case 0:
        len = Data.getBPA("XA");
        type = 'A';
        break;
      default:
        len = Data.getBPA(fmt);
        type = fmt.charAt(1);
        break;
    }


    switch (type) {
      case Data.FLOAT:  return data.unpackF(off);
      case Data.DOUBLE: return data.unpackD(off);
      case Data.XLONG:  return data.unpackX(off);
      case Data.LONG:   return data.unpackL(off);
      case Data.INT:    return data.unpackI(off);
      case Data.BYTE:   return data.unpackB(off);
      case Data.NIBBLE: return data.getN(off);
      case Data.PACKED: return data.getP(off);
      case Data.ASCII:  return data.unpackS(off, len);
      case Data.ASCII256: return data.unpackS(off, len);
      default : return data.unpackS(off, len);
    }
  }

  /** Get point in a list file
   *
   *  @param boff The byte offset to where the point is in the data buffer.
   *  @return The point that is located at the offset.
   *  @since 3.9.3
   */
  Point3D getPointListFile (int boff) {
  // Get the points now!!!!
    Point3D point = new Point3D(this);
    double x = 0;
    double y = 0;
    double z = 0;

    // Pos work
    if (bPosX != -1) { // get the Position Vector
      int off = bPosX + boff;
      x = Convert.data2d(data, off, xFormat);
    }
    if (bPosY != -1) { // get the Position Pointer Vector
      int off = bPosY + boff;
      y = Convert.data2d(data, off, yFormat);
    }
    if (bPosZ != -1) { // get the Conic Vector
      int off = bPosZ + boff;
      z = Convert.data2d(data, off, zFormat);
    }
    point.setPos(z,y,x);

    if (bName != -1) { // get the point name
      int off = bName + boff;
      point.setName(data.unpackS(off,Data.getBPA("XA")));
    }
    // set the flags by building a byte array of the flags
    byte[] flags = new byte[8];
    for (int i = 0; i< bFlags.length; i++) {
      if (bFlags[i] != -1) {
        int off = bFlags[i] + boff;
        flags [i] = Convert.unpackB(data.buf,off);
      }
      else {
        flags[i] = 0;
      }
    }
    point.setFlags(flags);

    // Ellipse work
    Double ellipseMajor = null;
    Double ellipseMinor = null;
    Double ellipseTilt  = 0.0;

    if (bEllipseMajor != -1) { // get the point name
      int off = bEllipseMajor + boff;
      ellipseMajor = data.unpackD(off);
    }
    if (bEllipseMinor != -1) { // get the point name
      int off = bEllipseMinor + boff;
      ellipseMinor = data.unpackD(off);
    }
    if (bEllipseTilt != -1) { // get the point name
      int off = bEllipseTilt + boff;
      ellipseTilt = data.unpackD(off);
    }
    if (ellipseMajor != null && ellipseMinor != null) {
      point.setElps(ellipseMajor, ellipseMinor, ellipseTilt);
    }

    // Ellipsoid work
    Double ellipsoidMajor     = null;
    Double ellipsoidMinor     = null;
    Double ellipsoidMedian    = null;
    Double ellipsoidRotationX = 0.0;
    Double ellipsoidRotationY = 0.0;
    Double ellipsoidRotationZ = 0.0;

    if (bEllipsoidMajor != -1) {
      int off = bEllipsoidMajor + boff;
      ellipsoidMajor = data.unpackD(off);
    }
    if (bEllipsoidMinor != -1) {
      int off = bEllipsoidMinor + boff;
      ellipsoidMinor = data.unpackD(off);
    }
    if (bEllipsoidMedian != -1) {
      int off = bEllipsoidMedian + boff;
      ellipsoidMedian = data.unpackD(off);
    }
    if (bEllipsoidRotationX != -1) {
      int off = bEllipsoidRotationX + boff;
      ellipsoidRotationX = data.unpackD(off);
    }
    if (bEllipsoidRotationY != -1) {
      int off = bEllipsoidRotationY + boff;
      ellipsoidRotationY = data.unpackD(off);
    }
    if (bEllipsoidRotationZ != -1) {
      int off = bEllipsoidRotationZ+ boff;
      ellipsoidRotationZ = data.unpackD(off);
    }
    if (ellipsoidMajor != null && ellipsoidMinor != null && ellipsoidMedian != null) {
      point.setEllipsoid(ellipsoidMajor, ellipsoidMinor, ellipsoidMedian, ellipsoidRotationX, ellipsoidRotationY, ellipsoidRotationZ);
    }

    // Cone work
    Double coneA = null;
    Double coneB = null;
    Double coneAngle = 0d;
    if (bConeA != -1) { // get the point name
      int off = bConeA + boff;
      coneA = data.unpackD(off);
    }
    if (bConeB != -1) { // get the point name
      int off = bConeB + boff;
      coneB = data.unpackD(off);
    }
    if (bConeAngle != -1) { // get the point name
      int off = bConeAngle + boff;
      coneAngle = data.unpackD(off);
    }
    if (coneA != null && coneB != null) {
      point.setCone(coneA, coneB, coneAngle);
    }

    // posp work
    Double pospX = null;
    Double pospY = null;
    Double pospZ = 0d;
    if (bPospX != -1) { // get the point name
      int off = bPospX + boff;
      pospX = data.unpackD(off);
    }
    if (bPospY != -1) { // get the point name
      int off = bPospY + boff;
      pospY = data.unpackD(off);
    }
    if (bPospZ != -1) { // get the point name
      int off = bPospZ + boff;
      pospZ = data.unpackD(off);
    }
    if (pospX != null && pospY != null) {
      point.setPosp(pospZ, pospY, pospX);
    }

    if (bFlagNote != -1) { // get the point note
      int off = bFlagNote + boff;
      point.setNote(data.unpackS(off, Data.getBPA("XA")));
    }

    if (bTimeLF != -1) { // get the point time
      int off = bTimeLF + boff;
      point.setTime(data.unpackD(off));
    }
    if (bColorPos != -1) { // get the point time
      int off = bColorPos + boff;
      point.setDirectColor(data.unpackS(off, Data.getBPA("XA")));
    }
    if (bSymbolPos != -1) {
      int off = bSymbolPos + boff;
      String symbolName = data.unpackS(off, Data.getBPA("XA"));
      if (!symbolName.isEmpty()) {
        point.setSymbol(symbolName);
      }
    }

    if (bMetasPos != null) {
      Table metaTable = new Table();
      for (int i=0; i < bMetasPos.length; i++) {
        if (bMetasPos[i] != -1) {
          int off = bMetasPos[i] + boff;
          Object value = unpackData(""+metaFormats[i], data, off);
          point.setMetadata(metaTable);
          metaTable.put(metadataFieldNames[i], value);
        }
      }
      point.setMetadata(metaTable);
    }
    else if (bMetaPos != -1) {
      int off = bMetaPos + boff;
      point.setMetadata(data.unpackS(off, Data.getBPA("XA")));
    }

    return point;
  }

  /** Since 3.1.0: Method assigns a specific point if a time value has been given. */
  private void matchTimePoint() {
    Point3D point = new Point3D(this);
    boolean needFullRescale = false;

    // Retrieve point, Transformed and interpolated (if needed)
    double[] p = StateVector.getPosVelAccAtTime(df, time, data, toLLA);
    point.setPos(p[2], p[1], p[0]);

    // get current lat/lon values for optimization purposes
    if (timePoint != null) {
      // TODO NXM-3500 - may just need update range in both cases?
      if (MP.view!=MPlot.V_3DGRAPH) {
        minMaxSet = false;
      }
      else {
        needFullRescale = true;
      }
    }
    timePoint = point;
    if (needFullRescale) recalculateFullRange();
    else updateRange(point);
    addPoint(point);
  }

  /**
   * TBA
   * @param attr Table of attributes (Color, Diameter, ElevAngle)
   */
  public void setConeAttr(Table attr) {
    // This method was added in 3.3.3 to support the WorldWind Option Tree in
    // defining cones for LayerT5 space objects. See nxm.worldwind.prim.worldwind.java
    // for implementation details
  }

  /**
   * Set the reference time. This is the file-relative, for cases where the file starts at midnight
   * of a given day, this will be the number of seconds since midnight. To match X-Midas, it is
   * assumes that very large numbers (greater than 86400*100) are meant to be J1950 and thus this
   * method will pass-through to {@link #setTimeCode(double)}.
   * @param value Time in seconds.
   * @see #setTimeCode
   * @since NeXtMidas 3.1.1
   */
  @Override
  public void setTime(double value) {
    // this method added in 3.1.1 to allow for plotting a single position from the file without using LayerGeo.
    // for large numbers, assume they meant to set J1950 time
    if (df == null) {
      // if we do not have a DataFile (i.e. CSV or no file and setting points directly)
      // 1) always assumes J1950 times, since file (if there is one) does not have timecode
      // 2) skip any StateVector animation support
      // Note: Calling setTimeCode just results in circular logic and even
      // doSetTime requires a DataFile - NXM-3682
      super.setTime(value);
    }
    else if (value > (86400 * 100)) {
      if (data != null) {
        setTimeCode(value);
      }
      else {
        // this should not happen, but setTimeCode requires data!=null or you get circular logic
        warning("LayerT5.setTime(double) not intended for case where data is null - file:"+this.getFile());
        doSetTime(value);
      }
    }
    else {
      doSetTime(value);
    }
  }

  /**
   * Set the time via {@link Layer#setTime(double)}, then set {@link #points} to null and call
   * {@link #matchTimePoint()}
   * @param value Time as a double
   */
  private void doSetTime(double value) {
    super.setTime(value);
    points = null; // remove old points from array
    timeSortOfPtIndicies = null;
    if (initialized)
      matchTimePoint();
  }

  /**
   * Get the reference time. This is the file-relative, for cases where the file starts at midnight
   * of a given day, this will be the number of seconds since midnight.
   * @return Time in seconds.
   * @see #getTimeCode
   * @since NeXtMidas 3.1.1
   */
  @Override
  public double getTime() {
    return super.getTime();
  }

  /**
   * Sets the timecode in J1950 time. This computes the file-relative time and then calls {@link #setTime}.
   * @param value Time in seconds since midnight, 1 Jan 1950.
   * @return true if the timecode is within the range of the current state vector file, false if not.
   *         This can be used to identify cases where the file is no longer valid and must be changed.
   * @since NeXtMidas 3.1.1
   */
  public boolean setTimeCode(double value) {
    if ((df == null) || (data == null)) {
      setTime(value);
      return false;
    }

    double start = df.getTimeCode();
    double end = start + (df.getXDelta() * df.getSize());
    doSetTime(value - start);
    return ((value >= start) && (value <= end));
  }

  /**
   * Gets the timecode in J1950 time. This is computed based on the file-relative time from {@link #getTime}.
   * @return Time in seconds since midnight, 1 Jan 1950.
   * @since NeXtMidas 3.1.1
   */
  public double getTimeCode() {
    if ((df == null) || (data == null))
      return getTime();

    return getTime() + df.getTimeCode();
  }

  /** Set X point margin for non geodetic data in XY view
   *  @param xyPointXMargin margin as percent
   */
  public static void setXYPointXMargin (double xyPointXMargin) {
    LayerT5.xyPointXMargin = (xyPointXMargin < 1) ? xyPointXMargin : xyPointXMargin/100;
  }

  /** Set Y point margin for non geodetic data in XY view
   *  @param xyPointYMargin margin as percent
   */
  public static void setXYPointYMargin (double xyPointYMargin) {
    LayerT5.xyPointYMargin = (xyPointYMargin < 1) ? xyPointYMargin : xyPointYMargin/100;
  }

  private double getPointXMargin () {
    if (!isGeodeticData && MP.isStandardXYView()) return xyPointXMargin;
    else return 0;
  }

  private double getPointYMargin () {
    if (!isGeodeticData && MP.isStandardXYView()) return xyPointYMargin;
    else return 0;
  }

  private void rangeMatchPoint (Point3D point) {

    x1 = point.minLon * (1 - getPointXMargin());
    x2 = point.maxLon * (1 + getPointXMargin());
    y1 = point.minLat * (1 - getPointYMargin());
    y2 = point.maxLat * (1 + getPointYMargin());

    z1 = point.minAlt;
    z2 = point.maxAlt;
  }

  /** Updates the internal min/max values, see findRange(). */
  void updateRange(Point3D point) {
    double oldXRange = xRange;
    double oldYRange = yRange;
    double oldZRange = zRange;

    if (!minMaxSet) {
      minLon = point.minLon * (1 - getPointXMargin());
      maxLon = point.maxLon * (1 + getPointXMargin());
      minLat = point.minLat * (1 - getPointYMargin());
      maxLat = point.maxLat * (1 + getPointYMargin());
      minAlt = point.minAlt;
      maxAlt = point.maxAlt;
      minMaxSet = true;
    }
    else {
      minLon = Math.min(minLon, point.minLon * (1 - getPointXMargin()));
      maxLon = Math.max(maxLon, point.maxLon * (1 + getPointXMargin()));
      minLat = Math.min(minLat, point.minLat * (1 - getPointYMargin()));
      maxLat = Math.max(maxLat, point.maxLat * (1 + getPointYMargin()));
      minAlt = Math.min(minAlt, point.minAlt);
      maxAlt = Math.max(maxAlt, point.maxAlt);
    }

    // used when in 3D Graph mode
    xRange = Math.max(xRange, point.xRangeMax);
    yRange = Math.max(yRange, point.yRangeMax);
    zRange = Math.max(zRange, point.zRangeMax);

    if ((MP != null)  && !ignoreRange) {
      if (((oldXRange!=xRange) || (oldYRange!=yRange) || (oldZRange!=zRange)) & MP.view != MPlot.V_LATLONCONTINUOUS) {
        // Need to tell MPlot that things have changed. May want to check if this is needed. i.e. ranges have changed.
        // 3.9.3 NXM-3576 skipping this in LATLONCONTINUOUS as causes issues when in extended range and have multiple LayerT5s
        if (MP.view == MPlot.V_LATLON) { // 3.9.4 to address issue with invalid ellipses messing up range
          minLon = Math.max(minLon, MPlot.LONGITUDE_DEFAULT_MIN);
          maxLon = Math.min(maxLon, MPlot.LONGITUDE_DEFAULT_MAX);
          minLat = Math.max(minLat, MPlot.LATITUDE_DEFAULT_MIN);
          maxLat = Math.min(maxLat, MPlot.LATITUDE_DEFAULT_MAX);
        }
        MP.findRange();
      }
      else if (!MP.isMapView()) {
        MP.findRange();
      }
    }
  }

  @Override
  public void findRange () {
    // If this layer is not the baselayer and used for symbols/features
    // MPlot should ignore this range
    if (ignoreRange) {
      x1 = MP.rx1;
      x2= MP.rx2;
      y1 = MP.ry1;
      y2 =MP.ry2;
      return;
    }
    // JC4: When plotting a pipe this is called following every pipe read that has new dats.
    // To avoid recomputing for the large number of unchanged points, use updateRange(..)
    // to update min/max values as new points come in. The only "problem" with this is
    // that the range does not rescale as points drop off. My guess is that such is not
    // an issue since LayerT5 is usually overlaid on a map where the bounds of the map
    // dominate the plot.
    if (MP.view!=MPlot.V_3DGRAPH) {
      if (minMaxSet && MP.isMapView()) {
        x1 = minLon;
        x2 = maxLon;
        y1 = minLat;
        y2 = maxLat;
        z1 = minAlt;
        z2 = maxAlt;
        a1 = y1;
        a2 = y2;
      }
      else {
        x1 = Math.min(x1, minLon);
        x2 = Math.max(x2, maxLon);
        y1 = Math.min(y1, minLat);
        y2 = Math.max(y2, maxLat);
        z1 = Math.min(z1, minAlt);
        z2 = Math.max(z2, maxAlt);
        a1 = y1;
        a2 = y2;
      }
    }
    else {
      // support plotting points on a graph - since 3.9.3 form NXM-3500
      findGraphRange();
    }
  }

  /** Find range needed if plotting these points on 3 3D graph
   *  In this case, the negative axis and positive axis need to be equal length.
   *  This allows the graph to rotate properly
   */
  private void findGraphRange () {
    // NXM-3500 TODO - may want to only change x1 if xRange change is significant
    // enough that the margin factor is not sufficient

    // Need to do it at first pass because of the a1 and a2 values - those are used to setup the view.
    if (firstPassFindGraphRange || x2<xRange) { // x2 is off the graph
      x1 = -xRange * threeDGraphMarginFactor;
      x2 = xRange * threeDGraphMarginFactor;
    }
    if (firstPassFindGraphRange || y2<yRange) { // y2 is off the graph
      y1 = -yRange * threeDGraphMarginFactor;
      y2 = yRange * threeDGraphMarginFactor;
    }
    if (firstPassFindGraphRange || z2<zRange) { // z2 is off the graph
      z1 = -zRange * threeDGraphMarginFactor;
      z2 = zRange * threeDGraphMarginFactor;
      a1 = z1;
      a2 = z2;
    }
    firstPassFindGraphRange = false;

  }

  /** Sets the Color of the selected point
   *  @param value The color
   *  @see MColor#getColor(Object)
   *  @since NeXtMidas 3.5.3 for Bug 2888
   */
  public void setSelectColor (Object value) {
    if ((value instanceof String) && (value.toString().length()==0)) return;
    if ((value instanceof String) && ("NONE".equalsIgnoreCase((String)value))) setSelectColor(null);
    else {
      Color c = MColor.getColor(value);
      setSelectColor(c);
    }
  }

  /** Sets the Color of the selected point
   *  @param c the Color to use for selected points
   *  @since NeXtMidas 3.5.3 for Bug 2888
   */
  public void setSelectColor (Color c) {
    if(selectColor == null || c == null || !selectColor.equals(c)){
      selectColor = c;
      refresh();
    }
  }

  /** Gets the Color used for selected points
   *  @return selectColor Color used for selected points
   *  @since NeXtMidas 3.5.3 for Bug 2888
   */
  public Color getSelectColor(){
    return selectColor;
  }

  @Override
  public synchronized void draw(int flag) {
    if (!initialized || (size == 0 && (pf == null))) { // add check for size - Since 3.7.0 for Bug 2917 & Bug 2965
      return; // JC4: Prevents race condition
    }

    if (MP.gc == null) {
      return;
    }
    if (showClusters) setClusterFontSizes();

    // if (needRefresh && !df.isPipe()) process(); // JC4: This may read in data after end of the pipe
    super.draw(flag); // resets needRefresh
    Line lin = line.copy();

    setDrawingConfig(lin);

    if (!_showAll) {
      // Are all points visible (this is common when zoomed out)?
      _showAll = (minLat >= MP.ry1) && (maxLat <= MP.ry2) && (minLon >= MP.rx1) && (maxLon <= MP.rx2);
    }

    // Set the font and color.
    Font f = null;
    if (fontsize > 0 && _symbols) {
      f = MP.gc.getFont();
      MP.gc.setFont(font);
    }
    MP.gc.setColor(lin.getColor());

    // When forming pixels, keep those that are not visible, as we may be drawing lines between points
    // Since 3.9.1 for NXM-3131
    pix.flags = pix.flags | Pix.KEEP;
    // Draw everything.
    double zoom = MP.getZoomLevelAsDecimal();
    boolean drawClusters = showClusters && zoom > RegionGroup.MAX_ZOOM_PERCENT && !_connect && MP.isLatLonView();
    if (drawClusters) {
      int zoomLevel = RegionGroup.getZoomLevel(zoom);
      if (zoomLevel<RegionGroup.MAX_ZOOM_LEVEL) drawCluster(lin, zoomLevel);
      else drawClusters = false;
    }
    if (!drawClusters) {
      Point3D point = null;
      Point3D last = null;
      int tempPointCount = 0; // since 3.9.3 for NXM-3512 - keep track of # of points drawn this pass
      if (relativeTimeWindow) {
        double mostRecentPtTime = (relativeMode == WINDOW_RELATIVE_POINT) ?
            getLatestPointTime() : Time.current(); // need to update time window each draw, because we are in relative mode
        if (relativeOldestTimeVisible != INACTIVE) oldestTimeVisible = mostRecentPtTime - relativeOldestTimeVisible;
        if (relativeNewestTimeVisible != INACTIVE) newestTimeVisible = mostRecentPtTime - relativeNewestTimeVisible;
      }
      // Since 4.1.2, add support for highlight and/or visibility filtering of track data
      // The points matching the filter, can also be requested and are initially stored in a temporary
      // collection and only placed in the filteredPoints field when complete to avoid timing / thread issues
      Map<String,PredicateType> _visibilityFilter = visibilityFilter;
      boolean visibilityMetaAction = (_visibilityFilter != null);
      Map<String,HighlightSetting> _highlightSettingMap = highlightSettingMap;
      boolean highlightMetaAction = (!_highlightSettingMap.isEmpty());
      boolean _needFilteredPoints = needFilteredPoints;
      ArrayList<Point3D> _filteredPoints = (_needFilteredPoints) ? new ArrayList<>() : filteredPoints;

      // First Pass Covers All But Highlighted Points
      int iStart = (hasWrapped()) ? firstPoint : 0;
      int i = iStart;
      for (int j = 0; j < numSpotsUsedInPointsArray; j++) {
        point = getVisiblePoint(i, doPosp || doVec, doCones, doEllipses);
        if (point!=null) {
          // all available points are considered to be in the time window,
          // unless we have a limited time window set - time window new to 3.9.3
          boolean inTimeWindow = (activeTimeWindow) ? isInTimeWindow(point) : true;
          boolean passVisibilityFilter = (_visibilityFilter !=null) ?
              passVisibilityFilter(point, _visibilityFilter) : true;
          if (inTimeWindow && (!visibilityMetaAction || passVisibilityFilter)) {
            // Do tighter check on visibility for point count than for drawing
            // Check if cone is set by using the center point (rather than considering all visible)
            if (point.coneSet) {
              if (isConeCenterVisible(point)) tempPointCount++;
            }
            // Do check without taking into account connected points for other types
            else if (isPointVisible(i)) tempPointCount++;

            point.highlightColor = (highlightMetaAction) ?
                assignHighlightColor(_highlightSettingMap, point) : null;
            // highlighted points will be drawn later
            if (point.highlightColor == null) {
              point.visibleHighlight = false;
              drawPoint(lin, point, last, i);
            }
            else point.visibleHighlight = true; // highlighted

            if (_needFilteredPoints && passVisibilityFilter) _filteredPoints.add(point);
          }
        }
        else {
          if (points[i] !=null) {
            points[i].highlightColor = (highlightMetaAction) ?
                assignHighlightColor(_highlightSettingMap, points[i]) : null;
            points[i].visibleHighlight = false; // point is not visible, so any highlight not visible
          }
        }

        last = (point != null && point.isHidden()) ? null : point;
        i = (i + 1) % numSpotsUsedInPointsArray;
      }

      // Now draw any highlighted points over non-highlighted points
      if (highlightMetaAction) {
        last = null;
        i = iStart;
        for (int j = 0; j < numSpotsUsedInPointsArray; j++) {
          // if this point has a highlight color, it was considered visible and matched a highlight filter
          point = points[i];
          point.prep(doPosp || doVec);
          if ((point != null ) && (point.visibleHighlight)) {
            drawPoint(lin, point, last, i);
            last = (point.isHidden()) ? null : point;
          }
          else {
            last = null; // not drawing this point, so should not try to connect to it
          }
          i = (i + 1) % numSpotsUsedInPointsArray;
        }
        updateHighlightCountStatus(_highlightSettingMap);
      }

      numVisiblePoints = tempPointCount;
      if (_needFilteredPoints) {
        filteredPoints = _filteredPoints;
        needFilteredPoints = false;
      }
    }

    // Restore the font and color
    if (f != null)
      MP.gc.setFont(f);
    MP.gc.setColor(colors[0]);
  }

  /** Checks a point against all the HighlightSetting filters.
   *  and returns the highlight color if applicable
   */
  private Color assignHighlightColor (Map<String,HighlightSetting> _highlightSettingMap, Point3D point) {
    for (String key : _highlightSettingMap.keySet()) {
      HighlightSetting highlightSetting = _highlightSettingMap.get(key);
      Map<String,PredicateType> highlightFilter = highlightSetting.getHighlightFilter();
      boolean isHighlighted = passHighlightFilter(point, highlightFilter);
      if (isHighlighted) {
        if (point.hit(MP.rx1, MP.rx2, MP.ry1, MP.ry2)) {
          highlightSetting.incrementRunningVisibleTotal();
        }
        highlightSetting.incrementRunningTotal();
        return highlightSetting.getHighlightColor();
      }
    }
    return null;
  }

  /** Put a new HighlightSetting in the HighlightSetting Map
   *
   *  @param name  The key/name associated with the new HighlightSetting
   *  @param mapPT The PredicateType Map filter for this HighlightSetting
   *  @param color The Color for this HighlightSetting as a String
   *  @since NeXtMidas 4.1.3 for NXM-3947
   */
  @InternalUseOnly
  public void putHighlightSetting (String name, Map<String,PredicateType> mapPT, String color) {
    HighlightSetting highlightSetting = new HighlightSetting(mapPT,color);
    highlightSettingMap.put(name, highlightSetting);
    refresh();
  }

  /** Put a new HighlightSetting in the HighlightSetting Map
   *
   *  NOTE: This method should not be used since track fading
   *        for HighlightSetting is no longer supported
   *
   *  @param name      The key/name associated with the new HighlightSetting
   *  @param mapPT     The PredicateType Map filter for this HighlightSetting
   *  @param color     The Color for this HighlightSetting as a String
   *  @param trackFade Enables or disables track color fading for this HighlightSetting
   *  @see #putHighlightSetting(String, Map, String)
   *  @since NeXtMidas 4.1.2 for NXM-3947
   */
  @InternalUseOnly
  public void putHighlightSetting (String name, Map<String,PredicateType> mapPT, String color, boolean trackFade) {
    putHighlightSetting(name,mapPT,color);
  }

  /** Put a new HighlightSetting in the HighlightSetting Map
   *
   *  @param name  The key/name associated with the new HighlightSetting
   *  @param mapPT The PredicateType Map filter for this HighlightSetting
   *  @param color The Color for this HighlightSetting as a Color Object
   *  @since NeXtMidas 4.1.3 for NXM-3947
   */
  @InternalUseOnly
  public void putHighlightSetting (String name, Map<String,PredicateType> mapPT, Color color) {
    HighlightSetting highlightSetting = new HighlightSetting(mapPT,color);
    highlightSettingMap.put(name, highlightSetting);
    refresh();
  }

  /** Put a new HighlightSetting in the HighlightSetting Map
   *
   *  NOTE: This method should not be used since track fading
   *        for HighlightSetting is no longer supported
   *
   *  @param name      The key/name associated with the new HighlightSetting
   *  @param mapPT     The PredicateType Map filter for this HighlightSetting
   *  @param color     The Color for this HighlightSetting as a Color Object
   *  @param trackFade Enables or disables track color fading for this HighlightSetting
   *  @see #putHighlightSetting(String, Map, Color)
   *  @since NeXtMidas 4.1.2 for NXM-3947
   */
  @InternalUseOnly
  public void putHighlightSetting (String name, Map<String,PredicateType> mapPT, Color color, boolean trackFade) {
    putHighlightSetting(name,mapPT,color);
  }

  /** Removes a single HighlightSetting from the HighlightSetting Map
   *  @param name The key/name associated with the HighlightSetting to be removed
   *  @since NeXtMidas 4.1.2 for NXM-3947
   */
  @InternalUseOnly
  public void removeHighlightSetting (String name) {
    if (!highlightSettingMap.isEmpty()) {
      highlightSettingMap.remove(name);
      refresh();
    }
  }

  /** Clears all HighlightSettings from the HighlightSetting Map
   *  @since NeXtMidas 4.1.2 for NXM-3947
   */
  @InternalUseOnly
  public void clearHighlightSettings () {
    if (!highlightSettingMap.isEmpty()) {
      highlightSettingMap.clear();
      refresh();
    }
  }

  /** Gets a single HighlightSetting from the HighlightSettingMap
   *  @param name The key/name associated with the HighlightSetting
   *  @return The HighlightSetting associated with the key/name provided
   *  @since NeXtMidas 4.1.2 for NXM-3947
   */
  @InternalUseOnly
  public HighlightSetting getHighlightSetting (String name) {
    return highlightSettingMap.get(name);
  }

  /** Gets the HighlightSetting Map
   *  @return The HighlightSetting Map
   *  @since NeXtMidas 4.1.2 for NXM-3947
   */
  @InternalUseOnly
  public Map<String,HighlightSetting> getHighlightSettingsMap () {
    return highlightSettingMap;
  }

  private void updateHighlightCountStatus (Map<String,HighlightSetting> _highlightSettingMap) {
    for (String key : _highlightSettingMap.keySet()) {
      HighlightSetting highlightSetting = _highlightSettingMap.get(key);
      int currentTotal = highlightSetting.getRunningTotal();
      int previousTotal = highlightSetting.getNumHighlighted();
      int currentVisibleTotal = highlightSetting.getRunningVisibleTotal();
      int previousVisibleTotal = highlightSetting.getNumVisibleHighlighted();
      highlightSetting.setNumHighlighted(currentTotal);
      highlightSetting.clearRunningTotal();
      highlightSetting.setNumVisibleHighlighted(currentVisibleTotal);
      highlightSetting.clearRunningVisibleTotal();
      boolean totalUpdated = currentTotal != previousTotal;
      boolean visibleTotalUpdated = currentVisibleTotal != previousVisibleTotal;
      if (sendHighlightTotalMessages && (totalUpdated || visibleTotalUpdated) ) {
        if (mwMsgHandler == null) mwMsgHandler = ((plot) MP.mh).getMessageHandler();
        if (mwMsgHandler != null) {
          if (totalUpdated) mwMsgHandler.processMessage(new Message("HIGHLIGHT_TOTAL",currentTotal,key,mwMsgHandler,MP.mh));
          if (visibleTotalUpdated) mwMsgHandler.processMessage(new Message("HIGHLIGHT_VISIBLE_TOTAL",currentVisibleTotal,key,mwMsgHandler,MP.mh));
        }
      }
    }
  }

  private Color getFadedColor (Color color, int indexPt) {
    int grayFactor = (computeNumPtsBehind(indexPt)) / colorFadeFactor;
    int red = Math.max(color.getRed() - grayFactor, fadeColorR);
    int green = Math.max(color.getGreen() - grayFactor, fadeColorG);
    int blue = Math.max(color.getBlue() - grayFactor, fadeColorB);
    return new Color(red,green,blue);
  }

  private int computeNumPtsBehind (int indexPt) {
    int numPtsBehind;
    boolean wrappingPoints = (firstPoint != -1) && (numSpotsUsedInPointsArray == points.length)
        && (points.length >= maxPointsSize);
    if (!wrappingPoints) {
      numPtsBehind = numSpotsUsedInPointsArray - indexPt - 1;
    }
    else {
      if (indexPt < firstPoint) {
        numPtsBehind = firstPoint - indexPt - 1;
      }
      else {
        numPtsBehind = firstPoint + (numSpotsUsedInPointsArray - indexPt);
      }
    }
    return numPtsBehind;
  }
  /** Checks if point time falls within the visibility time window
   *  (default is no time restrictions on points)
   *
   *  @param point the point to check
   *  @return true if the point falls within the time window
   *  @since NeXtMidas 3.9.3 for NXM-3515
   */
  private boolean isInTimeWindow (Point3D point) {
    if ((oldestTimeVisible != INACTIVE) && (point.time < oldestTimeVisible)) {
      return false; // too old of a point
    }
    if ((newestTimeVisible != INACTIVE) && (point.time > newestTimeVisible)) {
      return false; // too recent of a point
    }
    return true;
  }

  private boolean passVisibilityFilter (Point3D point, Map<String,PredicateType> _visibilityFilter) {
    for (String key : _visibilityFilter.keySet()) {
      boolean passTest = PredicateType.passesFilter(_visibilityFilter, key, point.filterDataTable.get(key));
      if (!passTest) return false;
    }
    return true;
  }

  private boolean passHighlightFilter (Point3D point, Map<String,PredicateType> _highlightFilter) {
    for (String key : _highlightFilter.keySet()) {
      boolean passTest = PredicateType.passesFilter(_highlightFilter, key, point.filterDataTable.get(key));
      if (!passTest) return false;
    }
    return true;
  }

  private boolean isConeCenterVisible (Point3D point) {
    double x = point.getPosLon();
    double y = point.getPosLat();
    if (x < MP.rx1) return false;
    if (x > MP.rx2) return false;
    if (y < MP.ry1) return false;
    if (y > MP.ry2) return false;
    return true;
  }

  /** Setup LayerT5 for drawing - will figure out what needs to be drawn by what is enabled/disabled
   *
   *  @param lin The line object for graphical settings
   */
  void setDrawingConfig (Line lin) {
    boolean coneFile = false;
    boolean elpsFile = false;
    doPosp = doVec = doCones = doEllipses = doPos = false;

    if (isListFile) {
      coneFile = (conic != 0) && (bPosX != -1) && (bPospX != -1);
      doPosp = ((bPospX != -1) && (bPospY != -1));
      elpsFile = (bEllipseMajor != -1) && (bEllipseMinor != -1) && (bPosX != -1);
      doPos = (bPosX != -1) && (bPosY != -1);
    }
    else {
      coneFile = (conic != 0) && (bPos != -1) && (bPosp != -1);
      elpsFile = (bElps != -1) && (bPos != -1);
      doVec = (bVec != -1);
      doPosp = (bPosp != -1);
      doPos = (bPos != -1) && (bVec == -1);
    }

    doCones = showField(CONES) && coneFile;
    doEllipses = showField(ELLIPSES) && elpsFile;
    double vreg;
    if (MP.view == MPlot.V_GEODETIC || MP.view == MPlot.V_POLAR) {
      vreg = 50.0 * (Math.min(MP.rx2 - MP.rx1, MP.ry2 - MP.ry1));
    }
    else {
      vreg = Math.min(MP.rx2 - MP.rx1, MP.ry2 - MP.ry1);
    }

    // Determine what needs to be drawn
    _labels = showField(LABELS) && (bName != -1) && ((enable & MPlot.EN_LABELS) != 0);
    _labelZoom = (vreg < tlreg);
    _symbols = showField(SYMBOLS) && lin.isTypeSet(Line.SYMBOL);
    _connect = showField(CONNECT) && (bConn != -1);
    _connectAll = timeAxis && ((flags & SHOW_TIME_SCALE_LABELS) == 0);

    // not sure how to optimize these, default to it being shown
    _showAll = (!MP.is2D) || _connect || _connectAll || doCones;
  }

  void drawPoint (Line lin, Point3D point, Point3D last, int index) {

    int saveType = lin.getTypeInt();
    Color saveColor = MP.gc.getColor();

    if (point != null && !point.isHidden()) {
      point.prep(doPosp);
      _selected = (select == point.indexInPointsObject);
      _color = saveColor;
      if (bFlag != -1 || bFlags[2] != -1) {
        _color = colors[Math.min(point.flagFgColor, colors.length - 1)]; // subtract one from colors.length
                                                                         // because it is zero based
      }
      else if (point.flagFgColor > 0) { // (BUG 2823) allow control for files without FLAG component
        _color = colors[Math.min(point.flagFgColor, colors.length - 1)];
      }
      else if (point.directColor !=null) {
        _color = point.directColor;
      }

      // If highlighing point, then fade is not in effect
      if (point.highlightColor != null) {
        _color = point.highlightColor;
      }
      else if (colorFade && !showClusters) { // 3.9.4, with update in 4.1.1
        _color = getFadedColor(_color, index);
      }

      if (_selected && (selectColor != null))
        _color = selectColor;


      // Specialty Points (Ellipsoids or Symbols/Icons take precedence over other objects
      boolean ptEllipsoid = point.ellipsoidSet;
      boolean ptSymbol    = point.symbol != null;


      // If not enough movement to doVec, then just return
      if (!(ptEllipsoid || ptSymbol) && doVec && last != null
          && Math.abs(point._ixPos - last._ixPos) < 5
          && Math.abs(point._iyPos - last._iyPos) < 5)
        return;

      // Adjust colors for draw
      MP.gc.setColor(_color);
      lin.setColor(_color);
      lin.setType(Line.SOLID);

      if (doPos)        drawPosSymbolLabel(lin, point, last);
      // nothing extra to do for doPosp

      if (!ptSymbol && ptEllipsoid) drawEllipsoid(lin, point);

      if (!ptSymbol && !ptEllipsoid) {
        if (doVec)      drawVec(lin, point);
        if (doCones)    drawCone(lin, point);
        if (doEllipses) drawEllipse(lin, point);
      }
    }
    if (last != null)
      last.done();
    last = (point != null && point.isHidden()) ? null : point;

    MP.gc.setColor(saveColor);
    lin.setColor(saveColor);
    lin.setType(saveType);


    if (last != null)
      last.done();

  }

  private void drawCluster (Line lin, int zoomLevel) {
    List<LayerT5> layerT5s = new ArrayList<>();
    layerT5s.add(this);
    RegionGroup.drawGrouping(lin, zoomLevel, layerT5s, MP);
  }


  /** Draws the labels. -- only used by draw(). */
  private void drawPosSymbolLabel(Line lin, Point3D point, Point3D last) {
    if (MP.gc == null)
      return;
    int ix = point._ixPos;
    int iy = point._iyPos;
    boolean pointIsSymbol = point.symbol != null;
    Symbol symbol = (pointIsSymbol) ? point.symbol : lin.getSymbol();
    float prevSymAng = lin.getSymbolHeading();
    String name = point.name;
    boolean sym = true;
    boolean conn = (_connect && point.conn) || _connectAll;
    boolean noAuto = (flags & AUTO_SCALE_LABELS) == 0;
    boolean lbl = (noAuto || _selected || _labelZoom);

    if (bFlag != -1 || bFlags[0] != -1) {
      // Set the defaults for files with flags
      lbl = true;
      sym = true;

      if (!pointIsSymbol && _symbols) {
        String symbolName = XmUtil.getXmSymbolName(point.flagSymCode);
        int symbolSize = point.flagSymSize;

        if (symbolName == null) {
          warn("Invalid symbol ID (" + point.flagSymCode + ") for plot (" + name + ") using default.");
        } else if ((symbolSize <= 0) || (symbolName.equals("NULL"))) {
          sym = false; // Turn the symbol off
        } else {
          symbol = Symbol.get(symbolName, point.flagSymSize);
        }
      }

      if (_labels) {
        byte nameLength = point.flagNameLen;

        if (nameLength == 0) {
          lbl = false;
        }
        else if (name.length() <= nameLength) {
          // Do Nothing - Name is Fine
        }
        else if ((nameLength > 0) && (nameLength <= name.length())) {
          name = name.substring(0, nameLength);
        }
        else {
          warn("Invalid name length (" + nameLength + ") for plot (" + name + ").");
        }
      }
    }

    if (conn && (last != null)) {
      if (!MP.is2D || Math.abs(point.posLon - last.posLon) < 180) {

        // Since 3.9.1 - send the points into the preexisting latlon continuous code
        // TODO - change all scenarios to use this code
        if (MP.view==MPlot.V_LATLONCONTINUOUS) {
          double[] linePoints;
          int dim;
          if (MP.is2D) {
            linePoints = new double[] {last.minLon,  last.minLat,  point.minLon,  point.minLat};
            dim = 2;
          }
          else {
            linePoints = new double[] {last.minLon,  last.minLat, last.posAlt,  point.minLon,  point.minLat, point.posAlt};
            dim = 3;
          }
          MP.drawLine(linePoints, 0, dim, 2, line, Pix.KEEP, MP.gc, true);
        }
        else {
          int lastx,lasty,thisx,thisy;
          // Handle high zoom scenario avoiding pixel Interger.MAX_VALUE saturation - 3.9.1 for NXM-3131
          if (MP.mxx >  MPlot.HIGH_ZOOM_MXX) {
            DPix dPix1 = new DPix(1,Pix.KEEP);
            DPix dPix2 = new DPix(1,Pix.KEEP);
            double[] lastPoint;
            double[] thisPoint;
            int dim;
            if (MP.is2D) {
              lastPoint = new double[]{last.posLon,last.posLat};
              thisPoint = new double[]{point.posLon,point.posLat};
              dim = 2;
            }
            else {
              lastPoint = new double[]{last.posLon,last.posLat,last.posAlt};
              thisPoint = new double[]{point.posLon,point.posLat,point.posAlt};
              dim = 3;
            }

            MP.viewer.rwc2dpix(lastPoint, 0, dim, 1, dPix1);
            MP.viewer.rwc2dpix(thisPoint, 0, dim, 1, dPix2);
            IntersectingLine visibleLine = GeometricUtil.adjustToVisibleLine(new Line2D.Double(dPix1.x[0], dPix1.y[0], dPix2.x[0], dPix2.y[0]),
                new Point2D.Double(MP.ix1, MP.iy1), new Point2D.Double(MP.ix2, MP.iy2));
            if (visibleLine == null) {
              // just use original unadjusted values
              lastx = last._ixPos;
              lasty = last._iyPos;
              thisx = ix;
              thisy = iy;
            }
            else {
              java.awt.geom.Line2D.Double line = visibleLine.getLine();
              lastx = (int)line.getX1();
              lasty = (int)line.getY1();
              thisx = (int)line.getX2();
              thisy = (int)line.getY2();
            }
          }
          else {
            lastx = last._ixPos;
            lasty = last._iyPos;
            thisx = ix;
            thisy = iy;
          }
          MP.gc.drawLine(lastx, lasty, thisx, thisy);
        }
      }
    }

    if (noAuto || (bFlag != -1 || bFlags[0] != -1) || (ix >= MP.ix1 && ix <= MP.ix2 && iy >= MP.iy1 && iy <= MP.iy2)) {
      if (_symbols && sym && (symbol != null)) {
        if (pointIsSymbol) lin.setSymbolHeading(point.symbol.getHeading());
        else if (bHdg != -1) lin.setSymbolHeading(point.hdg);
        symbol.draw(ix, iy, MP.gc);
      }
      if (_labels && lbl) {
        if (_selected) {
          name = getSelectedText();
        }
        // Determine the x-offset of the label; left vs right of point
        int xoff = getLabelxoffset(lin.getRadius(), name);
        if (name.indexOf('\n') < 0) {
          MP.gc.drawString(name, ix + xoff, iy + lin.getRadius());
        }
        else {
          String[] lines = name.split("\n");
          int lineHeight = MP.gc.getFontMetrics().getHeight();
          for (int lineNum = 0; lineNum < lines.length; lineNum++) {
            int delta = lineHeight * lineNum;
            MP.gc.drawString(lines[lineNum], ix + xoff, iy + lin.getRadius() + delta);
          }
        }
      }
    }
    lin.setSymbolHeading(prevSymAng); // restore previous angle
  }

  /** Draws the ellipses. -- only used by draw(). */
  private void drawEllipse(Line lin, Point3D point) {
    if ((bFlag == -1) || (point.flagCone != 0)) {
      // Estimate if the ellipse needs to be drawn. If the symbol is drawn and the ellipse is
      // smaller than the symbol, there is no need to draw it.
      double rad = Math.max(point.elpsSemiMajor, point.elpsSemiMinor);
      double dy = rad * Constants.METERS2DEG * MP.myy;
      double dx = rad * Constants.METERS2DEG * MP.mxx;
      int r = lin.getSymbol().getSize();

      if (!_symbols || (dx > r) || (dy > r)) {
        if ((flags & FILL_CONES) != 0) {
          lin.setType(Line.FILL);
          lin.setColor(new Color(0x70FFFFFF & _color.getRGB(), true));
          MP.drawEllipse(point.posLon, point.posLat, point.posAlt, point.elpsSemiMajor,
              point.elpsSemiMinor, (90.0 - point.elpsTilt), -1, lin, true); // 3.9.1 enable high zoom code
          lin.setType(Line.SOLID);
        }
        lin.setColor(_color);
        MP.drawEllipse(point.posLon, point.posLat, point.posAlt, point.elpsSemiMajor,
            point.elpsSemiMinor, (90.0 - point.elpsTilt), -1, lin, true);
      }
    }
  }

  /** Draws the ellipsoids as a wire cage -- only used by draw().
   *  1) creates a 0,0,0 centered 'wire cage' of ellispoid slices about each axis based on semis (in meters)
   *  2) creates and applies a rotation matrix based on rotation angles provided in degrees (to each slice)
   *  3) translates to ellipsoid to be centered on the POS position (its cartesian X,Y,Z equivalent) (each slice)
   *  4) if the data is geodetic (lat, lon, alt), transpose each slice to lla
   *  5) draw each ellipse slice
   */
  private void drawEllipsoid(Line lin, Point3D point) {

    lin.setColor(_color);

    // ellipsoid semis will be in meters and we form the basic wire cage in
    // cartesian coordinates about 0,0,0 and will rotate and position from there
    double[][][] wireCage = GeodeticUtil.computeXYZEllipsoidCrossSections(
        point.ellipsoidSemiMajor, point.ellipsoidSemiMinor, point.ellipsoidSemiMedian, eSlices);
    double[][] xCage = wireCage[0];
    double[][] yCage = wireCage[1];
    double[][] zCage = wireCage[2];

    // Get the transform matrix for rotating the ellipsoid
    double[][] zyxRotationTMatrix = GeodeticUtil.getZYXRotationTMatrix(Math.toRadians(point.ellipsoidRotationZ),
        Math.toRadians(point.ellipsoidRotationY), Math.toRadians(point.ellipsoidRotationX));

    // Get the point position (POS) in cartesian coordinates
    Position pos = new Position();
    if (isGeodeticData) {
      pos.setGeo(point.posAlt, point.posLat, point.posLon);
    }
    else {
      pos.setCar(point.posLon, point.posLat, point.posAlt);
    }

    // Draw the wire cage representing the ellipsoid one slice at a time
    // draw each x cross section
    for (int cs = 0; cs < xCage.length; cs++) {
      double[] semis = xCage[cs];
      MP.drawEllipsoidSliceXYZ(pos, isGeodeticData,  semis, MPlot.ellipseAxis.X, zyxRotationTMatrix, -1, lin, MP.gc, true);
    }
    // draw each y cross section
    for (int cs = 0; cs < yCage.length; cs++) {
      double[] semis = yCage[cs];
      MP.drawEllipsoidSliceXYZ(pos, isGeodeticData, semis, MPlot.ellipseAxis.Y, zyxRotationTMatrix, -1, lin, MP.gc, true);
    }
    // draw each y cross section
    for (int cs = 0; cs < zCage.length; cs++) {
      double[] semis = zCage[cs];
      MP.drawEllipsoidSliceXYZ(pos, isGeodeticData, semis, MPlot.ellipseAxis.Z, zyxRotationTMatrix, -1, lin, MP.gc, true);
    }

  }

  /** Draws the cones -- only used by draw(). */
  private void drawCone(Line lin, Point3D point) {
    // Determine which cone format(s) to draw
    boolean drawHoriz = (flags & CONE_FLAG_HORIZ) != 0;
    boolean drawVert = (flags & CONE_FLAG_VERT) != 0;

    // Set the line type to use.
    int hLineType = Line.SOLID;
    int vLineType = (drawHoriz && drawVert) ? Line.POINT : Line.SOLID;

    // Init the positions.
    Position pPos = (bFlag != -1 || bFlags[0] != -1) ? point.getPosp() : point.getPos();
    Position qPos = (bFlag == -1 && bFlags[0] == -1) ? point.getPosp() : point.getPos();

    if (bCone != -1 || bConeA != -1)
      cangle = point.coneAngle;

    // DRAW CONE
    if ((bFlag == -1 || bFlags[0] == -1) || (point.flagCone != 0)) {
      if ((flags & FILL_CONES) != 0) {
        lin.setType(Line.FILL);
        lin.setColor(new Color(0x70FFFFFF & _color.getRGB(), true));
        if (drawVert) {
          MP.drawCone(pPos, qPos, conic, cdiv, cangle, crange, lin, 0, true); // 3.9.1 enable high zoom fix
        }
        if (drawHoriz) {
          MP.drawCone(pPos, qPos, conic, cdiv, cangle, crange, lin, 1, true);
        }
        lin.setColor(_color);
      }
      if (drawVert) {
        lin.setType(vLineType);
        MP.drawCone(pPos, qPos, conic, cdiv, cangle, crange, lin, 0, true);
      }
      if (drawHoriz) {
        lin.setType(hLineType);
        if (!Transform.getCompareConeCode()) {
          MP.drawCone(pPos, qPos, conic, cdiv, cangle, crange, lin, 1, true);
        }
        else {
          // this is only temporary and used for comparing cone footprint implementations
          lin.setColor(7); // red - original
          boolean origValueLecacy =Transform.getUseLegacyConeCode();
          boolean origValueSpherical =Transform.getUseSphericalConeCode();
          Transform.setUseLegacyConeCode(true);
          if (Transform.isOnCompareList(Transform.CONE_LEGACY)) MP.drawCone(pPos, qPos, conic, cdiv, cangle, crange, lin, 1, true);
          lin.setColor(3); // goldenrod - Andrew's
          Transform.setUseSphericalConeCode(true);
          if (Transform.isOnCompareList(Transform.CONE_SPHERE)) MP.drawCone(pPos, qPos, conic, cdiv, cangle, crange, lin, 1, true);
          lin.setColor(4); // purple Andrew's oblate - default
          Transform.setUseSphericalConeCode(false);
          if (Transform.isOnCompareList(Transform.CONE_DEFAULT)) MP.drawCone(pPos, qPos, conic, cdiv, cangle, crange, lin, 1, true);
          lin.setColor(_color);
          Transform.setUseSphericalConeCode(origValueSpherical);
          Transform.setUseLegacyConeCode(origValueLecacy);
        }
      }
      lin.setType(Line.SOLID);
    }
  }

  /** Draws a vector -- only used by draw(). */
  private void drawVec (Line lin, Point3D point) {
    MP.gc.drawLine(point._ixPos, point._iyPos, point._ixPosp, point._iyPosp);
  }

  /* Determine the offset necessary to place LABEL to left or right of point */
  private int getLabelxoffset (int radius, String label) {
    if (MP.gc == null)
      return -1;
    int xoff = 2 + radius;
    if (getLeft()) {
      FontMetrics fm = MP.gc.getFontMetrics();
      xoff = -1 * (fm.bytesWidth(label.getBytes(), 0, label.length()) + xoff);
    }
    return xoff;
  }

  /** Gets the number of cuts (slices of the cone) to show for a file with cones.
   *  @return the number of cuts */
  public int getConic () {
    return conic;
  }

  /** Gets the number of divisions between POS and POSP to draw the cuts on (typically around 60).
   *  @return the number of divisions between POS and POSP to draw the cuts on */
  public int getConeDiv () {
    return cdiv;
  }

  /** Gets the cone angle (in deg). This value is typically superseded by the CONE field in the file.
   *  @return the cone angle in degrees */
  public double getConeAngle () {
    return cangle;
  }

  /** Gets the cone range (0=Compute range based on POS, POSP).
   *  @return the cone range */
  public double getConeRange () {
    return crange;
  }

  /** Sets the number of cuts (slices of the cone) to show for a file with cones.
   *  @param value The number of cuts (must be at least one). A value of 1 will display the cone only
   *               where it hits the surface of the Earth.
   */
  public void setConic (int value) {
    if (conic != value) {
      conic = value;
      refresh();
    }
  }

  /** Sets the number of divisions between POS and POSP to draw the cuts on (typically around 60).
   *  @param value the number of divisions between POS and POSP to draw the cuts on */
  public void setConeDiv (int value) {
    if (cdiv != value) {
      cdiv = value;
      refresh();
    }
  }

  /** Gets the cone angle (in deg). This value is typically superseded by the CONE field in the file.
   *  @param value the cone angle in degrees */
  public void setConeAngle (double value) {
    if (cangle != value) {
      cangle = value;
      refresh();
    }
  }

  /** Sets the cone range (0=Compute range based on POS, POSP).
   *  @param value the cone range */
  public void setConeRange (double value) {
    if (crange != value) {
      crange = value;
      refresh();
    }
  }

  /** Set the number of slices down each axis
   *  @param slicesEachAxis odd number 1 or greater (will round up to next odd number if invalid value)
   */
  public void setEllipsoidSlices (int slicesEachAxis) {
    int slices = 1;
    if (slicesEachAxis > 1) {
      int halfNumber = slicesEachAxis/2;
      slices = halfNumber*2 + 1;
    }
    eSlices = slices;
  }

  /** Gets the Toggle Label Region. This controls the point, when zooming in, at which text labels
   *  are shown (show label if {@code viewable < tlreg }). This value is ignored if the file has a
   *  FLAG field that specifies if a label is shown.
   *  @return the Toggle Label Region
   */
  public double getTLReg() {
    return tlreg;
  }

  /** Sets the Toggle Label Region, see {@link #getTLReg()} for details.
   *  @param value Toggle Label Region (show text if viewable is less than tlreg) */
  public void setTLReg(double value) {
    if (tlreg != value) {
      tlreg = value;
      refresh();
    }
  }

  /** Get the Labels left placement
   *  @return true if labels should be on the left */
  public boolean getLeft() {
    return leftLabels;
  }

  /** Set if label should be on the left
   *  @param value 1 if true */
  public void setLeft(int value) {
    if (value == 1) {
      leftLabels = true;
    }
    else {
      leftLabels = false;
    }
    refresh();
  }

  /** Get the font size
   *  @return the font size */
  public int getFontSize() {
    return fontsize;
  }

  /** Set the font size
   *  @param value the font size */
  public void setFontSize(int value) {
    fontsize = value;
    if (fontsize > 0)
      font = MP.newFont(MP.font, "", -1, fontsize);
  }

  // ////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods to set/get the selected point
  // ////////////////////////////////////////////////////////////////////////////////////////////////
  /** Selects the nearest point.
   *  @param pos position as a Position Object
   *  @deprecated Since NeXtMidas 2.1.0: Use <code>selectNearest(pos,NEAREST_BY_PIXELS)</code>.
   */
  @Deprecated
  public void select(Position pos) {
    // deprecated("select(Position) method is deprecated use selectNearest(Position,\"Pixel\").");
    double[] pp = new double[3];
    pp[0] = pos.x;
    pp[1] = pos.y;
    pp[2] = pos.z;
    select(pp);
  }

  /** Selects the nearest point.
   *  @param pos the position as an array of doubles
   *  @deprecated Since NeXtMidas 2.1.0: Use <code>selectNearest(pos,NEAREST_BY_PIXELS)</code>.
   */
  @Deprecated
  public void select(double[] pos) {
    setSelectedIndex(getNearestByPixels(pos, 25 * 25));
  }

  /** Selects the given point.
   *  @param offset index of the point
   *  @deprecated Since NeXtMidas 2.1.0: Use <code>setSelectedIndex(offset)</code>.
   */
  @Deprecated
  public void select(int offset) {
    // deprecated("select(index) method is deprecated use setSelectedIndex(index).");
    setSelectedIndex(offset);
  }

  /** Gets the index of the selected point.
   *  @return index of the selected point
   *  @deprecated Since NeXtMidas 2.1.0: Use <code>getSelectedIndex()</code>.
   */
  @Deprecated
  public int select() {
    // deprecated("select() method is deprecated use getSelectedIndex().");
    return getSelectedIndex();
  }

  /** Selects the nearest point (POS) to the given position. This method is intended for use from
   *  inside a macro.
   *  @param mode The selection mode (either "Pixel" or "GreatArc").
   *  @param position The given position (usually the result of the user selecting a point on the plot).
   *  @return The table of data for the selected point or <code>null</code> if none is selected (see
   *          {@link #getSelected()} for details about the contents of the table).
   */
  public Table selectNearest(String mode, Position position) {
    setSelectedIndex(getNearest(mode, position));

    return getSelected();
  }

  /**
   * Selects the nearest point (POS) to the given position. This method is intended for use from
   * inside a macro.
   * @param mode The selection mode (either "Pixel" or "GreatArc").
   * @param position The given position (usually the result of the user selecting a point on the plot).
   * @param range The maximum distance (or -1.0 for unlimited).
   * @return The table of data for the selected point or <code>null</code> if none is selected (see
   *         {@link #getSelected()} for details about the contents of the table).
   */
  public Table selectNearest(String mode, Position position, double range) {
    setSelectedIndex(getNearest(mode, position, range));

    return getSelected();
  }

  /** Selects the nearest point (POS) to the given position. This method simply calls
   *  {@link #selectNearest(int,Position,double)} with a default range value. The defaults are:
   *
   *  <pre>{@code
   *         mode={@link #NEAREST_BY_PIXELS}    --> range=625.0  [nxm200 default]
   *         mode={@link #NEAREST_BY_GREAT_ARC} --> range=-1.0   [unlimited range]
   *  }</pre>
   *  @param mode     The index in the file (0..n-1) of the point to select or -1 to select none.
   *  @param position position
   *  @return the nearest point to the given position
   *  @throws IllegalArgumentException - if the mode is invalid
   */
  public Table selectNearest(int mode, Position position) throws IllegalArgumentException {
    setSelectedIndex(getNearest(mode, position));

    return getSelected();
  }

  /**
   * Selects the nearest point (POS) to the given position. No point will be selected if none is
   * within the given range (use <code>range=-1.0</code> for unlimited range), this can be used to
   * prevent selecting points that are far away from where the user selected on the plot.<br>
   * <br>
   * The following selection modes are available:
   *
   * <pre>
   *         {@link #NEAREST_BY_PIXELS}
   *           The nearest point is determined based on its pixel position in the
   *           plot (i.e. the point that is nearest "visually" will be selected).
   *           The range is set in pixels and should be an integer value. If the
   *           given position's reference frame is set to GEODETIC it will use
   *           the positions lon,lat,alt values to define the position; otherwise
   *           it will use the x,y,z values; no attempt will be made to perform
   *           a conversion of the coordinate system.
   *
   *         {@link #NEAREST_BY_GREAT_ARC}
   *           The nearest point is determined based on the great arc distance
   *           between points. This is the most accurate for using geodetic or
   *           Lon-Lat-Alt plots (which is the majority of the Type 5000 files
   *           plotted). The range is set in real-world coordinates. If the
   *           given position's reference frame is not set to GEODETIC, it will
   *           be converted to GEODETIC coordinates before computing the nearest
   *           point.
   * </pre>
   * @param mode The selection mode (either {@link #NEAREST_BY_PIXELS} or {@link #NEAREST_BY_GREAT_ARC}).
   * @param position The given position (usually the result of the user selecting a point on the plot).
   * @param range The maximum distance (or -1.0 for unlimited).
   * @return The table of data for the selected point or <code>null</code> if none is selected (see
   *         {@link #getSelected()} for details about the contents of the table).
   * @throws IllegalArgumentException If the mode is invalid.
   */
  public Table selectNearest(int mode, Position position, double range)
      throws IllegalArgumentException {
    setSelectedIndex(getNearest(mode, position, range));

    return getSelected();
  }

  /** Sets the selected point based on the index value of the point.
   *  @param index The index in the file (0..n-1) of the point to select or -1 to select none.
   */
  public void setSelectedIndex (int index) {
    if (select != index) {
      Table previousSelectionInfo =  getSelected(); // save off previously selected
      select = index;
      if ((select == -1) && activePopup) closeCurrentPopup (); // since 3.9.3 make sure popup closed // 3.9.4 update
      refresh();
      if (sendSelectMessages) { // send message with information about selected point 3.9.3 NXM-3582
        if (mwMsgHandler == null) mwMsgHandler = ((plot) MP.mh).getMessageHandler();
        if (mwMsgHandler != null) {
          // Send DeSelect message for previous selected point, if we have one
          if (previousSelectionInfo!=null) mwMsgHandler.processMessage(new Message("DESELECT",0,previousSelectionInfo,mwMsgHandler,MP.mh));
          if (select != -1) {
            // If a new point is selected, send out a select message for it
            mwMsgHandler.processMessage(new Message("SELECT",0,getSelected(),mwMsgHandler,MP.mh));
          }
        }
      }
    }
  }

  /** Gets the index value of the selected point (0..n-1) or -1 if none is selected.
   *  @return the index value of the selected point
   */
  public int getSelectedIndex() {
    return (select < 0) ? -1 : select;
  }

  /** Gets the data for the selected point as a table. The table will include the following fields:
   *
   *  <pre>
   *          INDEX - The index of the point.                    (---ALWAYS---)
   *          NAME  - The name of the point.                     (If Available)
   *          POS   - The POS position.                          (If Available)
   *          POSP  - The POSP position.                         (If Available)
   *          ELPS  - The ellipse parameters.                    (If Available)
   *          CONE  - The cone parameters.                       (If Available)
   *          FLAG  - The set of flags setting the drawing mode. (If Available)
   *  </pre>
   *  @return The table of data for the selected point or <code>null</code> if none is selected.
   */
  public Table getSelected() {
    return (select < 0) ? null : getPoint(select);
  }

  // ////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods to manage the text displayed for the selected point.
  // ////////////////////////////////////////////////////////////////////////////////////////////////

  /** Sets the keys to include when using {@link #getSelectedText()}. This should be a
   *  comma-separated list of field names (in the order to be shown). <br>
   *  <br>
   *  Examples:
   *
   *  <pre>
   *         setSelectionKeys("NAME,POS")
   *         setSelectionKeys("NAME,POS,POSP,CONE")
   *  </pre>
   *  @param keys The keys to show use <code>null</code> to show only the <code>NAME</code> field
   *              without the <code>"NAME="</code> prefix.
   */
  public void setSelectionKeys(String keys) {
    if (keys == null) {
      selectedKeys = null;
    }
    else {
      selectedKeys = keys.trim().toUpperCase().split(SPLIT_REGEX);
    }
  }

  /** Gets the text of the selected entries.
   *  @return The selected text or "" if none are selected. */
  public String getSelectedText() {
    return getSelectedText(selectedKeys);
  }

  /** Gets the text of the selected entries.
   *  @param keys The keys to show (often <code>{"NAME"}</code> or <code>{"NAME","POS"}</code>) use
   *              <code>null</code> to show only the <code>NAME</code> field without the
   *             <code>"NAME="</code> prefix.
   *  @return The selected text or "" if none are selected.
   */
  private String getSelectedText(String[] keys) {
    final Table table = getSelected();
    final StringBuffer str = new StringBuffer();

    if (table != null) {
      if (keys == null) {
        str.append(table.get("NAME"));
      }
      else {
        for (int i = 0; i < keys.length; i++) {
          Object value = table.get(keys[i]);

          if (value != null) {
            if (value instanceof Position) { /* JC4: Make positions easy-to-read. */
              Position pos = (Position) value;

              if (pos.referenceFrame == Position.GEODETIC) {
                value = Convert.deg2dmsUnicode(pos.getLon(), "EW") + " "
                    + Convert.deg2dmsUnicode(pos.getLat(), "NS") + " " + ((float) pos.getAlt()) + "m";
              }
            }

            if (str.length() > 0) {
              str.append("\n");
            }
            str.append(StringUtil.padRight(keys[i], 4));
            str.append(" = ");
            str.append(value);
          }
        }
      }
    }

    return str.toString();
  }

  // ////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods that get the nearest point.
  // ////////////////////////////////////////////////////////////////////////////////////////////////
  /**
   * Selects the nearest point (POS) to the given position. <br>
   * <br>
   * This method simply calls {@link #selectNearest(int,Position,double)} with a default range
   * value. The defaults are:
   *
   * <pre>{@code
   *         mode="Pixel"    --> range=625.0  [nxm200 default]
   *         mode="GreatArc" --> range=-1.0   [unlimited range]
   * }</pre>
   * @param mode The selection mode (either {@link #NEAREST_BY_PIXELS} or {@link #NEAREST_BY_GREAT_ARC}).
   * @param position The given position (usually the result of the user selecting a point on the plot).
   * @return The table of data for the selected point or <code>null</code> if none is selected (see
   *         {@link #getSelected()} for details about the contents of the table).
   * @throws IllegalArgumentException If the mode is invalid.
   */
  public int getNearest(String mode, Position position) throws IllegalArgumentException {
    return getNearest(Parser.find(selectionModes, mode, -1), position);
  }

  /** Select the nearest position to a point of reference
   *  This will return an array of int with the first index being the distance in pixels from the reference point.
   *  The second index is the index of the point in the LayerT5 object.
   *
   *  @param point The pixel point selected as the point of reference
   *  @return an int array of [distance from position, index of point in the LayerT5 object]
   *  @since NeXtMidas 3.9.3
   */
  int[] getNearestPosition(Point point) {
    int[] pos;
    int _select = getNearestPixelIndex(point.x, point.y, PIXEL_MAX_RANGE);
    if (_select < 0) return null;
    pos = new int[2];
    Point3D pnt = getPointAt(_select);
    pnt.prep(false);
    pos[0] = pnt.distFromCurrentPix;
    pos[1] = _select; // need absolute index - 3.9.3 to be safe
    return pos;
  }

  /**
   * Selects the nearest point (POS) to the given position. This method is intended for use from
   * inside a macro.
   * @param mode The selection mode (either "Pixel" or "GreatArc").
   * @param position The given position (usually the result of the user selecting a point on the plot).
   * @param range The maximum distance (or -1.0 for unlimited).
   * @return The table of data for the selected point or <code>null</code> if none is selected (see
   *         {@link #getSelected()} for details about the contents of the table).
   * @see #selectNearest(int,Position)
   */
  public int getNearest(String mode, Position position, double range) {
    return getNearest(Parser.find(selectionModes, mode, -1), position, range);
  }

  /**
   * Selects the nearest point (POS) to the given position. <br>
   * <br>
   * This method simply calls {@link #selectNearest(int,Position,double)} with a default range
   * value. The defaults are:
   *
   * <pre>{@code
   *         mode={@link #NEAREST_BY_PIXELS}    --> range=625.0  [nxm200 default]
   *         mode={@link #NEAREST_BY_GREAT_ARC} --> range=-1.0   [unlimited range]
   * }</pre>
   * @param mode The selection mode (either {@link #NEAREST_BY_PIXELS} or {@link #NEAREST_BY_GREAT_ARC}).
   * @param position The given position (usually the result of the user selecting a point on the plot).
   * @return The table of data for the selected point or <code>null</code> if none is selected (see
   *         {@link #getSelected()} for details about the contents of the table).
   * @throws IllegalArgumentException If the mode is invalid.
   */
  public int getNearest(int mode, Position position) throws IllegalArgumentException {
    switch (mode) {
      case NEAREST_BY_PIXELS:
        return getNearest(mode, position, PIXEL_MAX_RANGE);
      case NEAREST_BY_GREAT_ARC:
        return getNearest(mode, position, -1.0);
      default:
        return getNearest(mode, position, -1.0);
    }
  }

  /**
   * Selects the nearest point (POS) to the given position. No point will be selected if none is
   * within the given range (use <code>range=-1.0</code> for unlimited range), this can be used to
   * prevent selecting points that are far away from where the user selected on the plot.<br>
   * <br>
   * The following selection modes are available:
   *
   * <pre>
   *         {@link #NEAREST_BY_PIXELS}
   *           The nearest point is determined based on its pixel position in the
   *           plot (i.e. the point that is nearest "visually" will be selected).
   *           The range is set in pixels and should be an integer value. If the
   *           given position's reference frame is set to GEODETIC it will use
   *           the positions lon,lat,alt values to define the position; otherwise
   *           it will use the x,y,z values; no attempt will be made to perform
   *           a conversion of the coordinate system.
   *
   *         {@link #NEAREST_BY_GREAT_ARC}
   *           The nearest point is determined based on the great arc distance
   *           between points. This is the most accurate for using geodetic or
   *           Lon-Lat-Alt plots (which is the majority of the Type 5000 files
   *           plotted). The range is set in real-world coordinates. If the
   *           given position's reference frame is not set to GEODETIC, it will
   *           be converted to GEODETIC coordinates before computing the nearest
   *           point.
   * </pre>
   * @param mode The selection mode (either {@link #NEAREST_BY_PIXELS} or {@link #NEAREST_BY_GREAT_ARC}).
   * @param position The given position (usually the result of the user selecting a point on the plot).
   * @param range The maximum distance (or -1.0 for unlimited).
   * @return The table of data for the selected point or <code>null</code> if none is selected (see
   *         {@link #getSelected()} for details about the contents of the table).
   * @throws IllegalArgumentException If the mode is invalid.
   */
  public int getNearest(int mode, Position position, double range) throws IllegalArgumentException {
    switch (mode) {
      case NEAREST_BY_PIXELS:
        return getNearestByPixels(position, range);
      case NEAREST_BY_GREAT_ARC:
        return getNearestByGreatArc(position, range);
      default:
        throw new IllegalArgumentException("LayerT5: Unknown selection mode (" + mode + ").");
    }
  }

  /**
   * Selects the nearest point based on a pixel distance. Prior to NeXtMidas 2.1.0 the select method
   * used this same code but made the assumption that <code>pos.x</code>, <code>pos.y</code>, and
   * <code>pos.z</code> were the valid position coordinates (ignoring the reference frame set in the position).
   * @param pos The position.
   * @param range The maximum range.
   * @return The index of the nearest point or (-1 if none found within the given range).
   */
  private int getNearestByPixels(Position pos, double range) {
    double[] pp = new double[3];

    if (pos.referenceFrame == Position.GEODETIC) {
      pp[0] = pos.x;
      pp[1] = pos.y;
      pp[2] = pos.z;
    }
    else {
      pp[0] = pos.x;
      pp[1] = pos.y;
      pp[2] = pos.z;
    }

    return getNearestByPixels(pp, range);
  }

  /**
   * Selects the nearest point based on a pixel distance. Prior to NeXtMidas 2.1.0 the select method
   * used this code to select the nearest point except that the range was hard-coded to be 25*25.
   * @param pos The position.
   * @param range The maximum range.
   * @return The index of the nearest point or (-1 if none found within the given range).
   */
  private int getNearestByPixels(double[] pos, double range) {
    if (bPos != -1 || bPosX != -1) {
      MPoint ppos = new MPoint();
      MP.viewer.rwc2pix(pos, ppos);
      return getNearestPixelIndex(ppos.x, ppos.y, range);
    }

    return -1;
  }

  private int getNearestPixelIndex (int x, int y, double range) {
    if (points == null) return -1;
    int dist;
    int _select = -1;
    int mdist = (int) Math.ceil(range); // maximum pixel radius

    int i = (hasWrapped()) ? firstPoint : 0;
    for (int j = 0; j < numSpotsUsedInPointsArray; j++) {
      Point3D point = points[i];

      if ((point != null) && !point.isHidden() && ((visibilityFilter==null) || passVisibilityFilter(point, visibilityFilter))) { // fix for wrong point being selected in geodetic view since 3.9.3
        point.getPosPix(pix);

        int ix = pix.x[0];
        int iy = pix.y[0];
        if (ix >= MP.ix1 && ix <= MP.ix2 && iy >= MP.iy1 && iy <= MP.iy2) {
          int dx = ix - x, dy = iy - y;
          dist = dx * dx + dy * dy;
          if ((dist <= mdist)) {
            // if distance is the same, we still not need keep the selection being the
            // earlier point, if the earlier point is highlighted and this later one is not
            if ((dist < mdist) || (_select == -1) || (point.highlightColor != null) ||
                (points[_select].highlightColor == null)) {
              _select = i;
              mdist = dist;
              point.distFromCurrentPix = mdist;
            }
          }
        }
      }
      i = (i + 1) % numSpotsUsedInPointsArray;
    }
    return _select;
  }

  /**
   * Selects the nearest point (POS) in the layer to the given position.
   * @param pos The location to search from (must be in {@link Position#GEODETIC} coordinates).
   * @param range The maximum distance (or -1.0 for unlimited).
   * @return The index of the nearest point or (-1 if none found within the given range).
   * @since NeXtMidas 2.1.0
   */
  private int getNearestByGreatArc(Position pos, double range) {
    int _select = -1;

    if (bPos == -1 && bPosX == -1) {
      warn("Can not select by great arc, no POS component found.");
    }
    else {
      if (pos.referenceFrame == Position.NORMAL) {
        pos.car2geo();
      }

      Position point = new Position(Position.GEODETIC);
      double lastDist = (range < 0) ? Double.MAX_VALUE : range;

      int i = (hasWrapped()) ? firstPoint : 0;
      for (int j = 0; j < numSpotsUsedInPointsArray; j++) {
        Point3D pnt = points[i];
        if ((pnt != null) && !pnt.isHidden() && ((visibilityFilter==null) || passVisibilityFilter(pnt, visibilityFilter))) {
          point.lon = pnt.posLon;
          point.lat = pnt.posLat;
          point.alt = pnt.posAlt;

          point.referenceFrame = Position.GEODETIC;
          double dist = Transform.greatArc(pos, point);
          pos.car2geo(); // Reset pos
          if (dist <= lastDist) {
            _select = i;
            lastDist = dist;
          }
        }
        i = (i + 1) % numSpotsUsedInPointsArray;
      }
    }
    return _select;
  }

  private boolean hasWrapped() {
    return (firstPoint != -1) && (points != null) && (numSpotsUsedInPointsArray == points.length) &&
        (points.length >= maxPointsSize);
  }

  private Point3D getPointAt(int index) {
    return points[index];
  }

  int getNumVisiblePoints () {
    if (showClusters) return 0; // Do not do point counts if drawing region circles. The counts are in the region circle - EXPERIMENTAL
    return numVisiblePoints;
  }

  /** Gets the point at a given index, returns null if the point is not visible on the plot. */
  private Point3D getVisiblePoint(int index, boolean posp, boolean cones, boolean ellipses) {
    // JC4: Added next line to prevent errors when called when no points defined
    if ((points == null) || (points.length == 0)){
      return null;
    }

    Point3D point = points[index];
    if (point == null) {
      return null;
    }
    if (quickDraw && MP.is2D) {
      if (!point.hit(MP.rx1, MP.rx2, MP.ry1, MP.ry2)) {
        point.in2DViewRange = false;
        return null;
      }
      point.in2DViewRange = true;
    }
    else {
      if (!isPointVisible(index)) {
        point.in2DViewRange = false;
        if (!isPointVisible(index + 1) && !isPointVisible(index - 1) && !isLineVisibleToFromPoint(index)) { // isPointVisible and isLineVisible added for Bug 2488
          return null;
        }
      }
      else {
        // cones are currently always considered visible according to isPointVisible
        // we also only care if this is directly in the view range and not just connected to a point that is
        // 3D views are unsupported, as currently all points are counted as visible in that case
        // only count cones if the center point is in the range
        point.in2DViewRange = true && MP.is2D && (!point.coneSet || isConeCenterVisible(point));
      }
    }
    // pulled the prep in here and return non-null so that we can someday
    // we can do partial draws from hidden to non-hidden points
    point.prep(posp);
    return point;
  }

  /** test whether a given point is visible
   *  @param index Index of the point (absolute index into points array)
   *  @return True if the point is visible
   *  @since NeXtMidas 3.1.0
   */
  private boolean isPointVisible(int index) {
    if (index < 0 || index > (points.length - 1))
      return false;
    Point3D point = points[index];
    if (point == null)
      return false;
    if (MP.is2D && !point.hit(MP.rx1, MP.rx2, MP.ry1, MP.ry2))
      return false;
    return true;
  }

  /**
   * Test whether a line drawn to or from a given point is visible. If either line would be visible,
   * then returns true
   * @param index Index of the current point
   * @return True if the line needs to be drawn
   * @since NeXtMidas 3.1.0
   */
  private boolean isLineVisibleToFromPoint(int index) {
    boolean visNext = false;
    boolean visLast = false;
    if (index < 0)
      return false;
    Point3D point = points[index];
    if (point == null)
      return false;
    Point3D next = null;
    if ((index + 1) < points.length)
      next = points[index + 1];
    if (next != null) {
      visNext = true;
      if (((point.maxLon < MP.rx1) && (next.maxLon < MP.rx1))
          || ((point.minLon > MP.rx2) && (next.minLon > MP.rx2))
          || ((point.maxLat < MP.ry1) && (next.maxLat < MP.ry1))
          || ((point.minLat > MP.ry2) && (next.minLat > MP.ry2))) {
        visNext = false;
      }
    }
    Point3D last = null;
    if ((index - 1) >= 0)
      last = points[index - 1];
    if (last != null) {
      visLast = true;
      if (((point.maxLon < MP.rx1) && (last.maxLon < MP.rx1))
          || ((point.minLon > MP.rx2) && (last.minLon > MP.rx2))
          || ((point.maxLat < MP.ry1) && (last.maxLat < MP.ry1))
          || ((point.minLat > MP.ry2) && (last.minLat > MP.ry2))) {
        visLast = false;
      }
    }
    return visNext || visLast;
  }

  /** @deprecated Since NeXtMidas 2.5.0: use {@link #getPoint(int)} */
  @Deprecated
  protected final String getPointName(int index) {
    Table tbl = getPoint(index);
    return (tbl == null) ? null : tbl.getS("NAME");
  }

  /** @deprecated Since NeXtMidas 2.5.0: use {@link #getPoint(int)} */
  @Deprecated
  protected final String getPointNote(int index) {
    Table tbl = getPoint(index);
    return (tbl == null) ? null : tbl.getS("NOTE");
  }

  /** @deprecated Since NeXtMidas 2.5.0: use {@link #getPoint(int)} */
  @Deprecated
  protected final float[] getPointHeading(int index) {
    Table tbl = getPoint(index);
    return (tbl == null) ? null : (float[]) tbl.getKey("HDG");
  }

  /** @deprecated Since NeXtMidas 2.5.0: use {@link #getPoint(int)} */
  @Deprecated
  protected final double[] getPointPos(int index) {
    Table tbl = getPoint(index);
    return (tbl == null) ? null : (double[]) tbl.getKey("POS");
  }

  /** @deprecated Since NeXtMidas 2.5.0: use {@link #getPoint(int)} */
  @Deprecated
  protected final double[] getAllPointPos() {
    double[] allPos = new double[3 * numSpotsUsedInPointsArray];
    for (int i = 0, j = 0; j < allPos.length; i++, j += 3) {
      System.arraycopy(getPointPos(i), 0, allPos, j, 3);

    }
    return allPos;
  }

  /** @deprecated Since NeXtMidas 2.5.0: use {@link #getPoint(int)} */
  @Deprecated
  protected final double[] getAllPointPosp() {
    double[] allPos = new double[3 * numSpotsUsedInPointsArray];
    for (int i = 0, j = 0; j < allPos.length; i++, j += 3) {
      System.arraycopy(getPointPosp(i), 0, allPos, j, 3);

    }
    return allPos;
  }

  /** @deprecated Since NeXtMidas 2.5.0: use {@link #getPoint(int)} */
  @Deprecated
  protected final double[] getPointPosp(int index) {
    Table tbl = getPoint(index);
    return (tbl == null) ? null : (double[]) tbl.getKey("POSP");
  }

  /** @deprecated Since NeXtMidas 2.5.0: use {@link #getPoint(int)} */
  @Deprecated
  protected final double[] getPointElps(int index) {
    Table tbl = getPoint(index);
    return (tbl == null) ? null : (double[]) tbl.getKey("ELPS");
  }

  /** @deprecated Since NeXtMidas 2.5.0: use {@link #getPoint(int)} */
  @Deprecated
  protected final double[] getPointCone(int index) {
    Table tbl = getPoint(index);
    return (tbl == null) ? null : (double[]) tbl.getKey("CONE");
  }

  /** @deprecated Since NeXtMidas 2.5.0: use {@link #getPoint(int)} */
  @Deprecated
  protected final byte[] getPointFlag(int index) {
    Table tbl = getPoint(index);
    return (tbl == null) ? null : (byte[]) tbl.getKey("FLAG");
  }

  /** @deprecated Since NeXtMidas 2.5.0: use {@link #getPoint(int)} */
  @Deprecated
  protected final byte getPointFlag(int index, int id) {
    byte[] flags = getPointFlag(index);
    return (flags == null) ? 0 : flags[id];
  }

  /** @deprecated Since NeXtMidas 2.5.0: use {@link #getPoint(int)} */
  @Deprecated
  protected final byte[] getPointConn(int index) {
    Table tbl = getPoint(index);
    return (tbl == null) ? null : (byte[]) tbl.getKey("CONN");
  }

  /** @deprecated Since NeXtMidas 2.5.0: use {@link #getPoint(int)}
   *  @return number of spots used in the points array
   */
  @Deprecated
  protected int getPointCount() {
    return numSpotsUsedInPointsArray;
  }

  /** Count the points in a given range
   *  <br><b>Note:</b> The coordinates provided will be arranged (x1 &gt; x2 will be swapped or y1 &gt; y2 will be swapped)
   *  @param x1 first x value (-180 to 180)
   *  @param x2 second x value (-180 to 180)
   *  @param y1 first y value (-90 to 90)
   *  @param y2 second y value (-90 to 90)
   *  @return the point count in the given range
   *  @since NeXtMidas 3.9.3
   */
  public int countPointsInRange (double x1, double x2, double y1, double y2) {
    if (y1==y2) return 0;
    if (x1==x2) return 0;
    double xmin = Math.min(x1, x2);
    double xmax = Math.max(x1, x2);
    double ymin = Math.min(y1, y2);
    double ymax = Math.max(y1, y2);
    int indexX1 = RegionGroup.getXIndex(xmin);
    int indexX2 = RegionGroup.getXIndex(xmax);
    int indexY1 = RegionGroup.getYIndex(ymin);
    int indexY2 = RegionGroup.getYIndex(ymax);
    double startXRWC = getRwcFromIndex(indexX1,0, true);
    double endXRWC = getRwcFromIndex(indexX2,0, false);
    double startYRWC = getRwcFromIndex(indexY1,1, true);
    double endYRWC = getRwcFromIndex(indexY2,1, false);
    int pntCount = 0;
    if (xmin >= startXRWC && xmax <= endXRWC && ymin >= startYRWC && ymax <= endYRWC) {
      for (int i = indexX1;i<=indexX2;i++) {
        for (int j = indexY1; j<=indexY2; j++) {
          if (regionGroups[i][j]==null) continue;
          RegionGroup cg = regionGroups[i][j];
          if (cg.minX >= xmin && cg.maxX <= xmax && cg.minY >= ymin && cg.maxY <= ymax) {
            pntCount += regionGroups[i][j].getNumPtsInRegionGroup(); // just add all points, fully contained in range
          }
          else pntCount += cg.countPointsInRange(xmin, xmax, ymin, ymax);
        }
      }
    }
    return pntCount;
  }

  double getRwcFromIndex (int val, int xOrY, boolean start) {
    int addToComplete = 0;
    if (xOrY==0) {
      if (!start) addToComplete = RegionGroup.LON_GRID_SIZE ;
      // do x
      return val * RegionGroup.LON_GRID_SIZE - MPlot.LONGITUDE_RANGE/2 + addToComplete;
    }
    if (!start) addToComplete = RegionGroup.LAT_GRID_SIZE;
    return (val * RegionGroup.LAT_GRID_SIZE - MPlot.LATITUDE_RANGE/2) + addToComplete;
  }

  /** get Point information as a Table
   *  @param index index of point
   *  @return table containing the points information
   */
  public Table getPoint(int index) {
    Table tbl = null;

    if ((index >= 0) && (index < numSpotsUsedInPointsArray)) {
      if (points[index] != null) {
        tbl = points[index].toTable();
      }
    }
    return tbl;
  }

  /** Set/replace an existing point at specified index -or-
   *  Add a new point, if index is negative or greater than current number of points.
   *  The input table can have the following fields:
   *
   *  <pre>
   *         NAME=  - A String (any other object is ok, .toString() is used)
   *         NOTE=  - A String (any other object is ok, .toString() is used)
   *         CONN=  - A byte[1] or a Data (CONN)
   *         POS=   - A Position, an Data (LON,LAT,ALT), or Table {ALT=,LAT=,LON=}
   *         POSP=  - A Position, an Data (LON,LAT,ALT), or Table {ALT=,LAT=,LON=}
   *         ELPS=  - A double[3] or Data (semiMajor,semiMinor,tilt)
   *         CONE=  - A double[3] or Data (radius,angle,angle)
   *         FLAG=  - A byte[8] or Data (NAMELEN,NAMEPOS,COLOR,BGCOLOR,CONE,
   *                  INVERT,SYMBOLID,SYMBOLSIZE) or Table {NAMELEN=,NAMEPOS=,
   *                  COLOR=,BGCOLOR=,CONE=,INVERT=,SYMBOLID=,SYMBOLSIZE=}
   *  </pre>
   *  @param index The index of the point (-1 to add new point to the end).
   *  @param table The point.
   */
  public void setPoint(int index, Table table) {
    if (numSpotsUsedInPointsArray == 0) {
      // initialize component fields if this is the first point for this LayerT5 - since 3.7.0 bug 2965
      updatePointsComponents(table);
    }
    else {
      // validate point components match existing points (merging pt:false) - since 3.7.0 for bug 2965
      validatePointComponents(table,false);
    }
    Point3D point = new Point3D(table,this);

    if (index < 0) {
      index = numSpotsUsedInPointsArray;
    }

    if (index >= numSpotsUsedInPointsArray) {
      while (index + 1 < numSpotsUsedInPointsArray) {
        addPoint(null, true);
      }
      addPoint(point, true);
      setSizeWithForceOpt(points.length, false);
    }
    else {
      points[index] = point;
    }
    // handle the need to update range if not a map view
    if ((MP!=null) && !MP.isMapView() && (MP.view!=MPlot.V_3DGRAPH) && (numSpotsUsedInPointsArray==1)) {
      rangeMatchPoint(point);
      minMaxSet = false;
      updateRange(point);
      minMaxSet = false;
      MP.useRange();
    }
    else if ((MP!=null) && !MP.isMapView()) {
      updateRange(point);
      MP.useRange();
    }
    // process(); // commented out in 3.3.2 since it is unnecessary and grows the points array with dupes
    refresh();
  }

  /** Merge new point properties with an existing point. The input table can have the following fields:
      <pre>
       NAME=  - A String (any other object is ok, .toString() is used)
       NOTE=  - A String (any other object is ok, .toString() is used)
       CONN=  - A byte[1] or a Data (CONN)
       POS=   - A Position, an Data (LON,LAT,ALT), or Table {ALT=,LAT=,LON=}
       POSP=  - A Position, an Data (LON,LAT,ALT), or Table {ALT=,LAT=,LON=}
       ELPS=  - A double[3] or Data (semiMajor,semiMinor,tilt)
       CONE=  - A double[3] or Data (radius,angle,angle)
       FLAG=  - A byte[8] or Data (NAMELEN,NAMEPOS,COLOR,BGCOLOR,CONE,
                  INVERT,SYMBOLID,SYMBOLSIZE) or Table {NAMELEN=,NAMEPOS=,
                  COLOR=,BGCOLOR=,CONE=,INVERT=,SYMBOLID=,SYMBOLSIZE=}
      </pre>
      @param index index of point to merge point properties
      @param table input table of point properties to merge with existing point, see above for expected key/values
      @since 3.5.1 for BUG 2823  (major update/fix 3.7.0 for BUG 2965)
   */
  public void mergePoint (int index, Table table) {
    // only merging with existing point if index in range of current points - since 3.7.0 bug 2965
    boolean mergingWithExistingPt = index < numSpotsUsedInPointsArray;

    if (numSpotsUsedInPointsArray == 0) {
      // initialize component fields if this is the first point for this LayerT5 - since 3.7.0 bug 2965
      updatePointsComponents(table);
    }
    else {
      // validate point components match existing points - since 3.7.0 for bug 2965
      if (numSpotsUsedInPointsArray > 0) validatePointComponents(table,mergingWithExistingPt);
    }

    if ((debugMask & 0x1) != 0) System.out.println("******** LayerT5.mergePoint********: index=" + index + ", #pts=" + numSpotsUsedInPointsArray);
    if (index < 0)
      index = numSpotsUsedInPointsArray;

    if (!mergingWithExistingPt) {
      Point3D point = new Point3D(table,this);
      while (index + 1 < numSpotsUsedInPointsArray) {
        addPoint(null, true);
      }
      addPoint(point, true);
      setSizeWithForceOpt(points.length, false);
    }
    else { // simplified, since updates to other methods use existing values as defaults
      if (points[index] == null) {
        points[index] = new Point3D(table,this);
      }
      else {
        points[index].fromTable(table);
      }
    }

    refresh();
  }

  /** Merge all points in new table with properties in the existing existing points.
      The input table can have the following fields:
      <pre>
        NAME=  - A String (any other object is ok, .toString() is used)
        NOTE=  - A String (any other object is ok, .toString() is used)
        CONN=  - A byte[1] or a Data (CONN)
        POS=   - A Position, an Data (LON,LAT,ALT), or Table {ALT=,LAT=,LON=}
        POSP=  - A Position, an Data (LON,LAT,ALT), or Table {ALT=,LAT=,LON=}
        ELPS=  - A double[3] or Data (semiMajor,semiMinor,tilt)
        CONE=  - A double[3] or Data (radius,angle,angle)
        FLAG=  - A byte[8] or Data (NAMELEN,NAMEPOS,COLOR,BGCOLOR,CONE,
                 INVERT,SYMBOLID,SYMBOLSIZE) or Table {NAMELEN=,NAMEPOS=,
                 COLOR=,BGCOLOR=,CONE=,INVERT=,SYMBOLID=,SYMBOLSIZE=}
      </pre>
      @param table input table of point properties to merge with existing point, see above for expected key/values
      @since 3.5.1 (major update/fix 3.7.0 for BUG 2965)
   */
  public void mergeAllPoints (Table table) {

    // validate point components match existing points (merging pt:true) - since 3.7.0 for bug 2965
    if (numSpotsUsedInPointsArray > 0) validatePointComponents(table,true);

    if ((debugMask & 0x1) != 0) System.out.println("******LayerT5.mergeAllPoints********: #pts=" + numSpotsUsedInPointsArray);

    for (int index = 0; index < numSpotsUsedInPointsArray; index++) {
      if (points[index] == null) {
        points[index] = new Point3D(table,this);
      }
      else {
        points[index].fromTable(table);
      }
    }

    refresh();
  }

  /** Update component fields to allow for point components not specified in the file
   *  (Points added via {@link #setPoint(int, Table)}, {@link #mergePoint(int, Table)},
   *  or {@link #mergeAllPoints(Table)} may have additional point components)
   *  Point components contained only in memory will have their "byte position"
   *  set to -2 (-1: component not exist, >0 = byte position of component in file)
   *  @param table table containing Point components
   *  @since NeXtMidas 3.7.0 for Bugs 2917 and 2965
   */
  private void updatePointsComponents (Table table) {
    if (bName == -1  && table.containsKey("NAME")) {bName = -2;}
    if (bPos  == -1  && table.containsKey("POS" )) {bPos  = -2;}
    if (bPosp == -1  && table.containsKey("POSP")) {bPosp = -2;} // position pointer
    if (bFlag == -1  && table.containsKey("FLAG")) {bFlag = -2;}
    if (bNote == -1  && table.containsKey("NOTE")) {bNote = -2;}
    if (bElps == -1  && table.containsKey("ELPS")) {bElps = -2;}
    if (bConn == -1  && table.containsKey("CONN")) {bConn = -2;} // connect flag
    if (bHdg  == -1  && table.containsKey("HDG" )) {bHdg  = -2;} // heading
  }

  /** Validate Point Components
   *  @param table table containing either info describing new point or point update info
   *  @param isMerge true:information is being merged with an existing point, false:completely new point
   *  @throws MidasException if this update would result in non-homogeneous points
   *  @since NextMidas 3.7.0 for Bugs 2917 and 2965
   */
  private void validatePointComponents (Table table, boolean isMerge) {

    if (table == null) { return; } // null points are allowed

    StringBuilder additionalKeys = new StringBuilder();
    StringBuilder missingKeys = new StringBuilder();
    boolean ptHasName = table.containsKey("NAME");
    boolean ptHasPos  = table.containsKey("POS" );
    boolean ptHasPosp = table.containsKey("POSP" );
    boolean ptHasFlag = table.containsKey("FLAG" );
    boolean ptHasNote = table.containsKey("NOTE" );
    boolean ptHasElps = table.containsKey("ELPS" );
    boolean ptHasConn = table.containsKey("CONN" );
    boolean ptHasHdg  = table.containsKey("HDG" );
    //boolean ptHasTime = table.containsKey("TIME");
    //boolean ptHasColor = table.containsKey("COLOR");

    // New/Updated point can not contain more Type 5000 Components than the existing points
    if (bName == -1 && ptHasName) { additionalKeys.append(" \"NAME\""); }
    if (bPos  == -1 && ptHasPos ) { additionalKeys.append(" \"POS\"");  }
    if (bPosp == -1 && ptHasPosp) { additionalKeys.append(" \"POSP\""); }
    if (bFlag == -1 && ptHasFlag) { additionalKeys.append(" \"FLAG\""); }
    if (bNote == -1 && ptHasNote) { additionalKeys.append(" \"NOTE\""); }
    if (bElps == -1 && ptHasElps) { additionalKeys.append(" \"ELPS\""); }
    if (bConn == -1 && ptHasConn) { additionalKeys.append(" \"CONN\""); }
    if (bHdg  == -1 && ptHasHdg ) { additionalKeys.append(" \"HDG\"");  }
    //if (bTime == -1 && ptHasTime ) { additionalKeys.append(" \"TIME\"");  }
    //if (bColor == -1 && ptHasColor ) { additionalKeys.append(" \"COLOR\"");  }

    // If this point information is NOT being merged, it needs to contain all the
    // Type 5000 components contained in the existing points (e.g. check for missing Components)
    if (!isMerge) {
      if (bName != -1 && !ptHasName) { missingKeys.append(" \"NAME\""); }
      if (bPos  != -1 && !ptHasPos ) { missingKeys.append(" \"POS\"");  }
      if (bPosp != -1 && !ptHasPosp) { missingKeys.append(" \"POSP\""); }
      if (bFlag != -1 && !ptHasFlag) { missingKeys.append(" \"FLAG\""); }
      if (bNote != -1 && !ptHasNote) { missingKeys.append(" \"NOTE\""); }
      if (bElps != -1 && !ptHasElps) { missingKeys.append(" \"ELPS\""); }
      if (bConn != -1 && !ptHasConn) { missingKeys.append(" \"CONN\""); }
      if (bHdg  != -1 && !ptHasHdg ) { missingKeys.append(" \"HDG\"");  }
    }

    boolean invalidExtraComponents = additionalKeys.length() != 0;
    boolean missingComponents      = missingKeys.length()    != 0;
    if (invalidExtraComponents ||  missingComponents) {
      StringBuilder sb = new StringBuilder();
      sb.append("LayerT5: Invalid new/updated Point with table: ").append(table);
      sb.append("\n New/updated Point MUST match existing Point Components so that they are homogeneous.");

      if (invalidExtraComponents) {
        sb.append("\n - new/updated Point contains these invalid additional Type 5000 Components: ").append(additionalKeys);
      }
      if (missingComponents) {
        sb.append("\n - new/replaced Point is missing these Type 5000 Components: ").append(missingKeys);
      }
      throw new IllegalArgumentException(sb.toString());
    }

  }

  /** Removes a point.
      @param index The index of the point.
   */
  public synchronized void deletePoint (int index) {
    if (points[index]==null) return; // point already gone or not in range
    if ((index < 0) && (index >= numSpotsUsedInPointsArray)) return; // point not in range
    removePointFromRegion(points[index]);
    if (select == index) setSelectedIndex(-1); // turn off point NXM-3566
    points[index] = null;
    pointsNulled.push(index);
    // Need to recalculate the full range if the point was on the edge of 3D graph (i.e. pnt.y == yRange)
    if ((MP!=null) && (MP.view==MPlot.V_3DGRAPH) && isPointMaxRange(points[index])) {
      recalculateFullRange();
    }
    refresh();

  }

  @Override
  public synchronized boolean clear () {
    // Throw everything out and let garbage collector handle the trash.
    if ((numSpotsUsedInPointsArray == 0) || (pointsNulled.size() == points.length)) return false; // return false if there is nothing to clear
    setSelectedIndex(-1); // turn off selected point before clearing NXM-3566
    points = new Point3D[size];
    pointsNulled = new Stack<>();
    if (needsTimeOrder) timeSortOfPtIndicies = new LinkedList<>();
    numSpotsUsedInPointsArray=0;
    firstPoint = Math.min(0, firstPoint); // NXM-3629 3.9.4
    regionGroups = new RegionGroup[MPlot.LONGITUDE_RANGE/RegionGroup.LON_GRID_SIZE][MPlot.LATITUDE_RANGE/RegionGroup.LAT_GRID_SIZE];
    if ((MP!=null) && MP.view==MPlot.V_3DGRAPH) recalculateFullRange();
    //refresh(); // this does not seem to be needed and does not match other layers NXM-3571
    return true;
  }

  /** Removes a range of points.
      @param indexFirst index of the first point.
      @param indexLast  index of last point to remove (if -1 means last point)
      @since NeXtMidas 3.7.0
   */
  @InternalUseOnly
  public synchronized void deletePoints (int indexFirst, int indexLast) {
    boolean updateRange = false;
    // if it is deletePoints(0,-1) we want to wipe it out. Call the clear method that wipes it out and return.
    if ((indexFirst == 0) && (indexLast == -1)) {
      clear();
    }
    else {
      if(indexFirst>numSpotsUsedInPointsArray) { // Can't delete what does not exists, exit out
        warning("LayerT5.deletePoints indexFirst="+indexFirst + " larger than the number of points, skipping the deletetion of points.");
        return;
      }
      if (indexLast == -1) {
        indexLast = numSpotsUsedInPointsArray - 1;
      }
      if (indexLast < indexFirst) {
        warning("LayerT5.deletePoints indexLast:" + indexLast + " < indexFirst:" + indexFirst);
        return;
      }
      else {
        if(indexFirst<0) {
          warning("LayerT5.deletePoints indexFirst=" + indexFirst + " < 0 setting value to 0");
          indexFirst = 0;
        }

        if(indexLast >= numSpotsUsedInPointsArray) {
          warning("LayerT5.deletePoints indexLast=" + indexLast +
              " beyond point range, setting value to index of last point:" + (numSpotsUsedInPointsArray - 1));
          indexLast = numSpotsUsedInPointsArray - 1;
        }
      }

      for (int index = indexFirst; index <= indexLast; index++) {
        if(points[index]!=null) {
          if (select == index) setSelectedIndex(-1); // turn off selected point NXM-3566
          updateRange = ((MP!=null) && MP.view==MPlot.V_3DGRAPH) && (updateRange || isPointMaxRange(points[index])); // since 3.9.3 for NXM-3500
          if (formRegionGroups) removePointFromRegion(points[index]);
          points[index] = null;
          pointsNulled.push(index);
        }
      }
    }
//    process(); Removed as this will process data that is already processed. Redundant.
    if (updateRange) recalculateFullRange();
    refresh();

  }

  /**  Make copy of existing point at specified index
   *  @param indexOrig    index of original
   *  @param indexForCopy index to contain copy (-1 - append to end)
   *  @since NeXtMidas 3.7.0 for bug 2917
   */
  @InternalUseOnly
  public void copyPoint (int indexOrig,int indexForCopy) {
    if (indexOrig < 0 || indexOrig >= numSpotsUsedInPointsArray) {
      throw new MidasException("LayerT5.copyPoint invalid index of original:"+
                                indexOrig+" valid range 0 - "+(numSpotsUsedInPointsArray-1));
    }
    if (indexForCopy == -1) {
      // append copy to end
      addPoint(new Point3D(points[indexOrig].toTable(),this), true);
    }
    else if (indexForCopy >= 0 && indexForCopy < numSpotsUsedInPointsArray){
      // overwrite point at indexForCopy
      if (points[indexOrig] != null) {
        points[indexForCopy] = new Point3D(points[indexOrig].toTable(),this);
      }
      else {
        points[indexForCopy] = null;
      }
    }
    else {
      // indexForCopy is beyond end so fill with null points and then append
      if (indexForCopy >= numSpotsUsedInPointsArray) {
        while (indexForCopy + 1 < numSpotsUsedInPointsArray) {
          addPoint(null, true);
        }
        addPoint(new Point3D(points[indexOrig].toTable(),this), true);
        setSizeWithForceOpt(points.length, false);
      }
    }
  }

  /** Get percent of points matching filter
   *  @return percent of points matching filter
   */
  @InternalUseOnly
  public int getPercentMatchingFilter () {
    if ((visibilityFilter != null) && (filteredPoints != null)) {
      return 100 * filteredPoints.size() / numSpotsUsedInPointsArray;
    }
    return 100;
  }

  /** Adds a point to the array of points, resizing as needed, returns insertion index. */
  private int addPoint (Point3D pnt) {
    return addPoint(pnt, false);
  }

  /** Adds a point to the array of points, resizing as needed, returns insertion index. */
  private synchronized int addPoint (Point3D pnt, boolean addToEnd) {
    pnt.setFilterDataTable(addNameFD, addPosFD, addPospFD, addElpsFD, addConeFD, addTimeFD, addNoteFD, addMetaFD);
    if (pnt.time > newestTimeReceived) newestTimeReceived = pnt.time; // since 3.9.3 keep track of latest time of any point ever received NXM-3515
    int index = -1;
    boolean pointsIntialized = !((points == null) || (points.length == 0));
    boolean pointsFull = pointsIntialized && (numSpotsUsedInPointsArray == points.length);
    if (!pointsIntialized) {
      if (size < 1) {
        warning("LayerT5: Got size < 1");
        size = 1;
      }
      points = new Point3D[size];
      numSpotsUsedInPointsArray = 1;
      index = 0;
      firstPoint = (firstPoint == -1) ? -1 : 0;
      pnt.indexInPointsObject = index;
      points[index] = pnt;
      if (needsTimeOrder) {
        timeSortOfPtIndicies = new LinkedList<>();
        timeSortOfPtIndicies.add(index);
      }
      if (formRegionGroups) addPointToRegion(pnt);

      return index;
    }
    else if ((firstPoint == -1) || (pointsFull && (points.length < maxPointsSize))) { // variable size or grow to size
      if (!addToEnd && (pointsNulled.size() > 0)) { // Reuse one of the existing slots that is null
        index = pointsNulled.pop();
      }
      else {
        if (numSpotsUsedInPointsArray + 1 >= points.length) { // Grow the array
          int doubleSize = points.length * 2;
          int newLength = (firstPoint == -1) ? doubleSize : Math.min(maxPointsSize, doubleSize);
          Point3D[] array = new Point3D[newLength];
          System.arraycopy(points, 0, array, 0, points.length);
          points = array;
        }
        index = numSpotsUsedInPointsArray;
        numSpotsUsedInPointsArray++; // increment the number of spots used
      }
    }
    else if (pointsFull) { // fixed size array and we are at the last spot.
      if(pointsNulled.size()>0) { // use any nulled points before wrapping.
        index = pointsNulled.pop();
      } else {
        if (!needsTimeOrder) {
          removePointFromRegion(points[firstPoint]);
          index = firstPoint;
          if (select == index) setSelectedIndex(-1); // NXM-3566
          // firstPoint will be a pointer to which index in the array to replace.
          firstPoint = (firstPoint + 1) % points.length; // wrap around if there are no null points and start dropping the oldest points.
        }
        else {
          if (pnt.time > points[timeSortOfPtIndicies.peekFirst()].time) {
            index = removeOldestPoint();
            if (select == index) setSelectedIndex(-1); // NXM-3566
            pointsNulled.remove(Integer.valueOf(index));
          }
          else {
            return INACTIVE;
          }
        }
      }
    }
    else { // fixed size, still has room
      index = numSpotsUsedInPointsArray;
      numSpotsUsedInPointsArray++; // increment the number of spots used
    }
    pnt.indexInPointsObject = index;
    points[index] = pnt;
    if (needsTimeOrder) insertPointIdxInTimeArray(index);
    updateRange(pnt);
    if (formRegionGroups) addPointToRegion(pnt); // EXPERIMENTAL - do not use
    return index;
  }

  private int removeOldestPoint () {
    int idxRemoved = timeSortOfPtIndicies.removeFirst();
    deletePoint(idxRemoved);
    return idxRemoved;
  }

  /** This is only called if the point is within the time range it is being kept.
   *  If needed, a point was removed to make space for this point
   *  @param index point index to keep track of in time sort list
   */
  private void insertPointIdxInTimeArray (int index) {
    double pntTime = points[index].time;
    if (timeSortOfPtIndicies.size() > 1) {
      double timeFirst = points[timeSortOfPtIndicies.peekFirst().intValue()].time;
      double timeLast = points[timeSortOfPtIndicies.peekLast().intValue()].time;
      // if before first, addFirst()
      if (pntTime <= timeFirst) { // 3.9.4 both need '=' check too
        timeSortOfPtIndicies.addFirst(index);
      }
      // if after last, addEnd
      else if (pntTime >= timeLast) {
        timeSortOfPtIndicies.addLast(index);
      }
      // otherwise, find insertion index and add
      else {
        int insertionIndex = findInsertionIndex (index, timeFirst, timeLast);
        timeSortOfPtIndicies.add(insertionIndex, index);
      }
    }
    else if (timeSortOfPtIndicies.size() == 1) {
      // we only have 1 point before this one
      double timeFirst = points[timeSortOfPtIndicies.peekFirst().intValue()].time;
      if (pntTime < timeFirst) {
        timeSortOfPtIndicies.addFirst(index);
      }
      else {
        timeSortOfPtIndicies.addLast(index);
      }
    }
    else {
      // so we just removed any that were there ... unusual
      timeSortOfPtIndicies.addFirst(index);
    }
  }

  /** Find Insertion Index
   *
   *  @param pointIdx   location of point in points array
   *  @param timeOldest oldest time as double
   *  @param timeNewest newest time as double
   *  @return insertion index
   */
  @InternalUseOnly
  public int findInsertionIndex (int pointIdx, double timeOldest, double timeNewest) {
    double pntTime = points[pointIdx].time;
    if ((pntTime-timeOldest) > (pntTime-timeNewest)) {
      // if closer to end, decendingIterator
      // iterate until find time too large keeping track of index
      Iterator<Integer> iterDown = timeSortOfPtIndicies.descendingIterator();
      while (iterDown.hasNext()) {
        int thisIndex = iterDown.next();
        double thisTime = points[thisIndex].time;
        if (pntTime > thisTime) {
          // insertion point has been found
          return thisIndex; //todo make sure this is right
        }
      }
    }
    else {
      // if closer to beginning, ascending iterator
      ListIterator<Integer> iterUp = timeSortOfPtIndicies.listIterator();
      while (iterUp.hasNext()) {
        int thisIndex = iterUp.next();
        double thisTime = points[thisIndex].time;
        if (pntTime < thisTime) {
          // insertion point has been found
          return thisIndex; //todo make sure this is right
        }
      }
    }
    return -1;
  }

  // ////////////////////////////////////////////////////////////////////////////////////////////////
  // Misc Methods
  // ////////////////////////////////////////////////////////////////////////////////////////////////

  /** Sets the flags for plotting and refreshes the plot.
   *  @param value The new plotting flags (see {@link #flagsList}). */
  public void setFlags(int value) {
    if (flags != value) {
      flags = value;
      refresh();
    }
  }

  /** Sets the flags for plotting and refreshes the plot.
   *  @param value The new plotting flags (see {@link #flagsList}). */
  public void setFlags(String value) {
    setFlags(Parser.mask(flagsList, value, flags));
  }

  /** Gets the flags for plotting (see {@link #flagsList}).
   *  @return the flags for plotting */
  public String getFlags() {
    return Parser.mask2s(flagsList, flags);
  }

  /** Outputs a warning message.
   *  @param msg The message. */
  private void warn(String msg) {
    warning("LayerT5: " + msg);
  }

  /** Indicates if a given field should be shown.
   *  @param flag The flag for the field (e.g. {@link #LABELS}).
   *  @return true if shown, flase if not.
   */
  private boolean showField(int flag) {
    return (field & flag) != 0;
  }

  /** issuer configuration menus */
  @Override
  public void configure() {
    new GMenu(MP, "Layer.cfg", Layer.configureList + ",AutoSelect", 0, 0, this);
  }

  @Override
  public int processMessage(Message msg) {
    if (msg.name.equals("LAYER.CFG") && ((String) msg.data).equals("AUTOSELECT")) {
      new GMenu(MP, "AutoSelect.Options", "Enable,SelectionMode,SelectionRange", 0, 0, this);
    }
    else if (msg.name.equals("AUTOSELECT.OPTIONS")) {
      String text = (String) msg.data;
      if (text.equals("ENABLE")) {
        new GMenu(MP, "AutoSelect.Enable", plot.pointerMessageList, autoSelEnable, GMenu.TOGGLE,
            this);
      }
      else if (text.equals("SELECTIONMODE")) {
        new GMenu(MP, "AutoSelect.SelectionMode", selectionModes, autoSelMode, 0, this);
      }
      else if (text.equals("SELECTIONRANGE")) {
        new GPrompt(MP, "AutoSelect.SelectionRange", Double.toString(autoSelRange), 0, this);
      }
    }
    else if (msg.name.equals("AUTOSELECT.ENABLE")) {
      autoSelEnable = msg.info;
    }
    else if (msg.name.equals("AUTOSELECT.SELECTIONMODE")) {
      autoSelMode = msg.info;
    }
    else if (msg.name.equals("AUTOSELECT.SELECTIONRANGE")) {
      autoSelRange = Convert.o2d(msg.data);
    }
    else {
      return super.processMessage(msg);
    }
    return 1;
  }

  /** Sets the AutoSelect enable flags.
   *  @param enable The flags (see {@link plot#pointerMessageList}).
   *  @since NeXtMidas 2.1.0
   */
  public void setAutoSelectEnable(String enable) {
    autoSelEnable = Parser.mask(plot.pointerMessageList, enable, autoSelEnable);
  }

  /** Gets the AutoSelect enable flags (see {@link plot#pointerMessageList}).
   *  @return the AutoSelect enable flags
   *  @since NeXtMidas 2.1.0
   */
  public String getAutoSelectEnable() {
    return Parser.mask2s(plot.pointerMessageList, autoSelEnable);
  }

  /** Sets the AutoSelect mode.
   *  @param mode The mode (see {@link #selectionModes}).
   *  @since NeXtMidas 2.1.0
   */
  public void setAutoSelectMode(String mode) {
    autoSelMode = Parser.find(selectionModes, mode, autoSelMode, 0, Parser.E_WARNING);
  }

  /** Gets the AutoSelect mode (see {@link #selectionModes}).
   *  @return the AutoSelect mode
   *  @since NeXtMidas 2.1.0
   */
  public String getAutoSelectMode() {
    return Parser.get(selectionModes, autoSelMode);
  }

  /** Sets the AutoSelect range.
   *  @param range The range (units are determined based on AutoSelect mode).
   *  @since NeXtMidas 2.1.0
   */
  public void setAutoSelectRange(double range) {
    autoSelRange = range;
  }

  /** Gets the AutoSelect range (units are determined based on AutoSelect mode).
   *  @return the AutoSelect range
   *  @since NeXtMidas 2.1.0
   */
  public double getAutoSelectRange() {
    return autoSelRange;
  }

  /** Set if data is typical alt, lat, lon or XYZ
   *  @param dataType "XYZ" for non-map usage of LayerT5
   */
  public void setDataType (String dataType) {
    if (dataType.toLowerCase().startsWith("xyz")) {
      isGeodeticData = false;
    }
    else {
      isGeodeticData = true;
    }
  }

  /** Set Alternate Popup Labels for XYZ mode popups
   *  @param strTbl String representation of Table */
  @InternalUseOnly
  public void setPopupLabels (String strTbl) {
    popupLabels = ((strTbl==null) || strTbl.isEmpty()) ? new Table() : new Table(strTbl);
  }

  /** Processes mouse movement messages. If the message type matches the autoselect mode then a
   *  selection is made as a result of the message.
   *  @param msg {@inheritDoc}
   *  @param type {@inheritDoc}
   *  @since NeXtMidas 2.1.0
   */
  @Override
  public void processMouseMessage(Message msg, int type) {
    if (!isVisible()) return;
    if ((autoSelEnable & type) != 0) {
      // LMB pt selection only works with GreatArc if all pt altitudes are 0
      if (autoSelMode == NEAREST_BY_PIXELS || (maxAlt != 0) || (minAlt != 0)) {
        if (!(msg.quals instanceof Table)) return; // Need to have the mouse event to select by pixel.
        Table quals = (Table) msg.quals;
        if (!(quals.get("ORIG_MSG_DATA") instanceof MouseEvent)) return; // Need to have the mouse event to select by pixel.
        MouseEvent me = (MouseEvent) quals.get("ORIG_MSG_DATA");
        Point point = me.getPoint();
        setSelectedIndex(getNearestPixelIndex(point.x, point.y, PIXEL_MAX_RANGE));
      }
      else {
        Position pos = (Position) msg.getData();
        selectNearest(autoSelMode, pos, autoSelRange);
      }

      if (GraphicsUtil.isCtrlLMB(msg)) { // Since 3.9.3 add a popup for the selected if necessary
        drawPopup(getSelectedIndex());
      }
    }
  }

  /** Draw popup for the point at the given index
   *
   *  @param index The index of point
   *  @since NeXtMidas 3.9.3
   */
  void drawPopup (int index) {
    if (index <0) return;
    if (activePopup) closeCurrentPopup (); // 3.9.4
    htmlPopup = new PopUpHTML(points[index].toPopupTable(), MP);
    activePopup = true;
  }

  /** Close the current popup
  *   @since 3.9.3
  */
  void closeCurrentPopup () {
    if (htmlPopup != null) htmlPopup.close();
    activePopup = false;
  }

  /** Go through all the points to recalculate full range for 3D graph */
  private void recalculateFullRange() {

    xRange = yRange = zRange = 0;
    for (int i = 0; i < points.length; i++) {
      Point3D pnt = points[i];
      if (pnt==null) continue;

      xRange = pnt.xRangeMax > xRange ? pnt.xRangeMax : xRange;
      yRange = pnt.yRangeMax > yRange ? pnt.yRangeMax : yRange;
      zRange = pnt.zRangeMax > zRange ? pnt.zRangeMax : zRange;
    }
  }

  /** Is this point one that affects maximum range ?
  *
  *  @param pnt the point
  *  @return true if is is a point that affects maximum range
  */
  private boolean isPointMaxRange (Point3D pnt) {
    if (pnt==null) return false;
    return (Math.abs(pnt.xRangeMax)==xRange) || (Math.abs(pnt.yRangeMax)==yRange || Math.abs(pnt.zRangeMax)==zRange);
  }


  @Override
  public void findFullRange () { findFullRange(pf); }

  @Override
  public void findFullRange (PlotFile pf) {
    fx1 = x1; fx2 = x2;
    fy1 = y1; fy2 = y2;
    fz1 = z1; fz2 = z2;
  }


  /** Utility function to see the size of the pointsNulled stack.
   *  @return the pointsNulled size.
   */
  @InternalUseOnly("Used by LayerT5Test")
  public int getPointsNulledSize () {
    return pointsNulled.size();
  }

  /** Utility function to see how many points are available.
   *  @return the number of points.
   */
  @InternalUseOnly("Used by LayerT5Test")
  public int getPointsSize () {
    return numSpotsUsedInPointsArray;
  }

  /** Gets length of points buffer
   *  Note: this is for testing purposes only
   *  @return length of current points buffer
   */
  @InternalUseOnly
  public int getPointsLength () {
    return points.length;
  }

  /** Utility function to see the next integer in the stack.
   *  @return the pointsNulled size.
   */
  @InternalUseOnly("Used by LayerT5Test")
  public int peekAtNextNulledIndex () {
    return pointsNulled.peek();
  }

  /** get a new LayerT5
   *  @return Layer that is LayerT5
   */
  public static Layer getLayer () {
    return new LayerT5();
  }

  /** ONLY here for MAO operation
   *  @param x not used
   */
  public void setX (String x) {
    // Do not set here because we need it before we get here
  }

  /** ONLY here for MAO operation
   *  @param y not used
   */
  public void setY (String y) {
    // Do not set here because we need it before we get here
  }

  /** ONLY here for MAO operation
   *  @param z not used
   */
  public void setZ (String z) {
    // Do not set here because we need it before we get here
  }

  /** ONLY here for MAO operation
   *  @param time not used
   */
  public void setTimeLF (String time) {
    // Do not set here because we need it before we get here - 3.9.3 for NXM-3515
  }

  /** ONLY here for MAO operation
   *  @param metadata not used
   */
  @InternalUseOnly
  public void setMetaPos (String metadata) {
    // Do not set here because we need it before we get here - 3.9.3 for NXM-3515
  }

  /** ONLY here for MAO operation
   *  @param metadataFields not used
   */
  @InternalUseOnly
  public void setMetadataFields (String metadataFields) {
    // Do not set here because we need it before we get here - 3.9.4
  }

  /** set metadata field names
   *  @param metadataFieldNames String array containing metatdata field names
   */
  public void setMetadataFieldNames (String[] metadataFieldNames) {
    this.metadataFieldNames = metadataFieldNames;
  }

  /** ONLY here for MAO operation
   *  @param filterDataFields not used
   */
  @InternalUseOnly
  public void setFilterDataFields (String filterDataFields) {
    // Do not set here because we need it before we get here - 4.1.2
  }

  /** enable sending of messages when points are selected or deselected
   *  @param sendSelectMessages true if messages should be sent
   */
  public void setSelectMessages (String sendSelectMessages) {
    setSelectMessages(sendSelectMessages.toLowerCase().startsWith("t"));
  }

  /** Send SELECT and DESELECT messages
   *  @param sendSelectMessages true if SELECT and DESELECT messages should be sent
   *  @since NeXtMidas 3.9.3
   */
  public void setSelectMessages (boolean sendSelectMessages) {
    this.sendSelectMessages = sendSelectMessages;
  }
  /** Send Highlight Total messages
   *  @param sendHighlightTotalMessages true if Highlight Total messages should be sent
   */
  public void setHighlightTotalMessages (boolean sendHighlightTotalMessages) {
    this.sendHighlightTotalMessages = sendSelectMessages;
  }

  /** enable sending of messages when Highlight Totals change
   *  @param sendHighlightTotalMessages true if messages should be sent
   */
  public void setHighlightTotalMessages (String sendHighlightTotalMessages) {
    setHighlightTotalMessages(sendHighlightTotalMessages.toLowerCase().startsWith("t"));
  }

  // Need to override this so hiding of LayerT5 causes any popup to
  // temporarily go away and a deselect message to be sent for any
  // selected points (if there is a message handler).
  // The popup should return and select message resent upon showing the layer,
  // if the point is still selected (not overridden or deselected)
  @Override
  public void setEnable (int value) {
    boolean layerHidden = (value&MPlot.EN_GLOBAL)==0;
    if ((enable&MPlot.EN_GLOBAL)==0 != layerHidden) {
      if (mwMsgHandler == null) mwMsgHandler = ((plot) MP.mh).getMessageHandler();
      if (select != -1) {
        if (layerHidden) {
          if (activePopup && (htmlPopup != null)) {
            closeCurrentPopup ();
            activePopup = true;
          }
          if (mwMsgHandler != null) mwMsgHandler.processMessage(new Message("DESELECT",0,getSelected(),mwMsgHandler,MP.mh));
        }
        else {
          if (activePopup) drawPopup(getSelectedIndex());
          if (mwMsgHandler != null) mwMsgHandler.processMessage(new Message("SELECT",0,getSelected(),mwMsgHandler,MP.mh));
        }
      }
      clearVisibleHighlightTotals ();
    }
    super.setEnable(value);
  }

  private void clearVisibleHighlightTotals () {
    for (String key : highlightSettingMap.keySet()) {
      HighlightSetting highlightSetting = highlightSettingMap.get(key);
      int visibleTotal = highlightSetting.getNumVisibleHighlighted();
      highlightSetting.setNumVisibleHighlighted(0);
      highlightSetting.clearRunningVisibleTotal();
      boolean visibleTotalUpdated = visibleTotal != 0;
      if (sendHighlightTotalMessages && visibleTotalUpdated) {
        if (mwMsgHandler == null) mwMsgHandler = ((plot) MP.mh).getMessageHandler();
        if (mwMsgHandler != null) {
          if (visibleTotalUpdated) mwMsgHandler.processMessage(new Message("HIGHLIGHT_VISIBLE_TOTAL",0,key,mwMsgHandler,MP.mh));
        }
      }
    }
  }

  private char[] buildFormatsArray () {
    char[] arr = new char[8];
    Arrays.fill(arr, ' ');
    return arr;
  }

  private int[] buildFlagBytePosArray () {
    int[] arr = new int[8];
    Arrays.fill(arr, -1);
    return arr;
  }

  boolean isGeodeticData () {
    return isGeodeticData;
  }

  public void clearNeedRefresh () {
    super.clearNeedRefresh();
    numVisiblePoints = 0;
  }

  /** Need to be able to access from LayerDrawable
   *  @return the autoSelEnable
   *  @since 3.9.3
   */
  int getAutoSelEnable () {
    return autoSelEnable;
  }

  /**
   *  {@inheritDoc}
   *  @since 3.9.3
   */
  @Override
  public void close() {
    setSelectedIndex(-1); // 3.9.4
    super.close();
  }

  // OVERRIDE of Layer methods to allow Line color to auto adjust with ColorMap change
  // this DOES NOT handle the case where the MMB line menu is used to update line color
  // 3.9.3 for NXM-3574

  @Override
  public void setColor (String value) {
    super.setColor(value);
    if (autoAdjustLineColor && (colors != null) && (colors.length > 1)) colors[colors.length-1] = line.getColor();

  }

  @Override
  public void setColor (int value) {
    super.setColor(value);
    if (autoAdjustLineColor && (colors != null) && (colors.length > 1)) colors[colors.length-1] = line.getColor();
  }

  @Override
  public void setColor (Color value) {
    super.setColor(value);
    if (autoAdjustLineColor && (colors != null) && (colors.length > 1)) colors[colors.length-1] = line.getColor();
  }

  /** Enable / Disable the auto adjustment of line color based on the colormap
   *  @param matchColormap true if line color should be last color in colormap
   *  @since NeXtMidas 3.9.3
   */
  public static void setAutoAdjustLineColor (boolean matchColormap) {
    autoAdjustLineColor = matchColormap;
  }

  /** Get value of  auto adjustment of line color based on the colormap
   *  @return true if auto adjustment is on
   *  @since NeXtMidas 3.9.4
   */
  public static boolean getAutoAdjustLineColor () {
    return autoAdjustLineColor;
  }

  @Override
  public void setColors (String value) {
    setColorMap(value);
  }

  @Override
  public void setColorMap (String value) {
    super.setColorMap(value);
    if (autoAdjustLineColor && (colors != null) && (colors.length > 1)) line.setColor(colors[colors.length - 1]);
  }

  @Override
  public void setColorMap (int value) { setColorMap(value,-1); }

  @Override
  public void setColorMap (int value, int ncolors) {
    super.setColorMap(value, ncolors);
    if (autoAdjustLineColor && (colors != null) && (colors.length > 1)) line.setColor(colors[colors.length - 1]);
    }

  @Override
  public void setColorMap (Color[] colors) {
    super.setColorMap(colors);
    if (autoAdjustLineColor && (colors != null) && (colors.length > 1)) line.setColor(colors[colors.length - 1]);
  }


  /// MAP FADE TIME methods below this point

  /** Set time window from newest point received back to the time specified
   *  @param timeOldest oldest point time that should be visible
   *  @param mode       the time window mode
   *  @since NeXtMidas 3.9.4 for NXM-3515
   */
  public void setTimeWindow (double timeOldest, int mode) {
    setTimeWindow (timeOldest, INACTIVE, mode);
    this.oldestTimeVisible = (timeOldest < 0) ? INACTIVE : timeOldest;
  }

  /** Sets a time window for point visibility
   *
   *  @param timeOldest oldest point time that should be visible
   *  @param timeNewest newest point time that should be visible
   *  @param mode       the time window mode
   *  @since NeXtMidas 3.9.4 for NXM-3515
   */
  public void setTimeWindow (double timeOldest, double timeNewest, int mode) {
    if (mode == WINDOW_OFF) turnOffTimeWindow();
    else if ((mode & WINDOW_RELATIVE) != 0) setRelativeTimeWindow(timeOldest, timeNewest, mode);
    else setFixedTimeWindow(timeOldest, timeNewest);
  }

  /** turn off point time visibility window */
  public void turnOffTimeWindow () {
    activeTimeWindow = relativeTimeWindow = false;
    this.oldestTimeVisible = this.newestTimeVisible = INACTIVE;
    this.relativeOldestTimeVisible = this.oldestTimeVisible = INACTIVE;
    if ((df == null) || !df.isPipe()) refresh();
  }

  private void setFixedTimeWindow (double timeOldest, double timeNewest) {
    this.oldestTimeVisible = (timeOldest < 0) ? INACTIVE : timeOldest;
    this.newestTimeVisible = (timeNewest < 0) ? INACTIVE : Math.max(timeNewest, timeOldest);
    this.relativeOldestTimeVisible = this.relativeNewestTimeVisible = INACTIVE;
    activeTimeWindow = (oldestTimeVisible != INACTIVE) || (newestTimeVisible != INACTIVE);
    relativeTimeWindow = false;
    if ((df == null) || !df.isPipe()) refresh();
  }

  /** Sets a time window that is relative to the current newest point time
   *
   *   @param relativeTimeOldest seconds back from newest for oldest point visible
   *   @param relativeTimeNewest seconds back from newest for newest point visible
   *   @since NeXtMidas 3.9.4 for NXM-3515
   */
  private void setRelativeTimeWindow (double relativeTimeOldest, double relativeTimeNewest, int mode) {
    this.relativeOldestTimeVisible = (relativeTimeOldest < 0) ? INACTIVE : relativeTimeOldest;
    this.relativeNewestTimeVisible = (relativeTimeNewest < 0) ? INACTIVE : Math.max(relativeTimeNewest, relativeTimeOldest);
    if (this.relativeOldestTimeVisible == INACTIVE) this.oldestTimeVisible = INACTIVE;
    if (this.relativeNewestTimeVisible == INACTIVE) this.newestTimeVisible = INACTIVE;
    activeTimeWindow = (relativeOldestTimeVisible != INACTIVE) || (relativeNewestTimeVisible != INACTIVE);
    relativeTimeWindow = true;
    relativeMode = mode;
    if ((df == null) || !df.isPipe()) refresh();
  }

  /** Time ordering needs to be set for ensuring the proper points are kept when
   * <pre>
   *  There is a limit on the buffer size and points are arriving out of order
   *  OR
   *  Points are being deleted
   *  </pre>
   *  @param inOrder true if a record of point time order is required
   *  @since NeXtMidas 3.9.4 for NXM-3626
   */
  public void setNeedsTimeOrder (boolean inOrder) {
    needsTimeOrder = inOrder;
  }

  /** Time ordering needs to be set for ensuring the proper points are kept when
   * <pre>
   *  There is a limit on the buffer size and points are arriving out of order
   *  OR
   *  Points are being deleted
   *  </pre>
   *  @param inOrder 'true' if a record of point time order is required
   *  @since NeXtMidas 3.9.4
   */
  public void setNeedsTimeOrder(String inOrder) {
    setNeedsTimeOrder(!(inOrder.toLowerCase().startsWith("f")));
  }

  /** Time ordering needs to be set for ensuring the proper points are kept when
   * <pre>
   *  There is a limit on the buffer size and points are arriving out of order
   *  OR
   *  Points are being deleted
   *  </pre>
   *  @param inOrder 'true' if a record of point time order is required
   *  @since NeXtMidas 3.9.4
   */
  public void setTimeOrder (String inOrder) {
    setNeedsTimeOrder(inOrder);
  }

  /** Get the latest of all the point times<br>
   *  Note: This may not be the time or the latest point received
   *  or directly associated with position in the points[] buffer
   *
   *  @return latest of all the point times
   *  @since NeXtMidas 3.9.4
   */
  public double getLatestPointTime () {
    if ((timeSortOfPtIndicies != null)  && !timeSortOfPtIndicies.isEmpty()) {
      return points[timeSortOfPtIndicies.peekLast()].time;
    }
    else {
      return newestTimeReceived;
    }
  }

  /** enable fading of older points to the fade color
   *  @param doColorFade true to turn on fading
   */
  public void setColorFade (boolean doColorFade) {
    colorFade = doColorFade;
  }

  /** set how fast the fade occurs to the fade color
   *  @param fadeFactor Number of points per gradual fade shift
   */
  public void setColorFadeFactor (int fadeFactor) {
    colorFadeFactor = fadeFactor;
  }

  /** set color to fade to
   *  @param fadeColor Color for older points
   */
  public void setFadeColor (Color fadeColor) {
    fadeColorR = fadeColor.getRed();
    fadeColorG = fadeColor.getGreen();
    fadeColorB = fadeColor.getBlue();
  }

  /** set color to fade to
   *  @param fadeColor Object defining color for older points
   */
  public void setFadeColor (Object fadeColor) {
    setFadeColor(MidasColor.getColor(fadeColor, new Color(fadeColorR,fadeColorG,fadeColorB)));
  }

  /** Set the metadata based visibility filter that controls which point are displayed
   *  @param visibilityFilter the key= Predicate and if the Predicate is a number
   */
  public void setVisibilityFilter (Map<String,PredicateType> visibilityFilter) {
    this.visibilityFilter = visibilityFilter;
    refresh();
  }

  /** remove the visibility filter by setting it to null */
  public void nullVisibilityFilter () {
    setVisibilityFilter(null);
  }

  /** Ignore the data range. This is not the base layer and MPlot should
   *  use its existing range
   *  @param ignoreRange true to ignore range
   */
  public void setIgnoreRange (boolean ignoreRange) {
    this.ignoreRange = ignoreRange;
  }


  // ------ Methods related to forming and rendering clusters of points --------

  /** Add point to Region Group within the overall RegionGroups structure
   *  Region Groups are structured with nested Region Groups until the
   *  finest level is reached
   *  @param pnt point to add to appropriate region group based on lat/lon
   *  @since NeXtMidas 3.9.4 for NXM-3542
   */
  private void addPointToRegion(Point3D pnt) {
    if (pnt==null) return; // Can this happen?
    int indexX = RegionGroup.getXIndex(pnt.getPosLon());
    int indexY = RegionGroup.getYIndex(pnt.getPosLat());
    if (regionGroups[indexX][indexY]==null) regionGroups[indexX][indexY]= new RegionGroup(indexX,indexY,this);
    pnt.indexInRegion = regionGroups[indexX][indexY].addPoint(pnt);
    pnt.xIndexInRegionsArray = indexX;
    pnt.yIndexInRegionsArray = indexY;
  }

  private void removePointFromRegion (Point3D pnt) {
    if (pnt==null) return; // There could be a chance this is null
    //System.out.println("index:"+pnt.xIndexInRegionsArray+","+pnt.yIndexInRegionsArray);
    if (pnt.xIndexInRegionsArray == -1 || pnt.yIndexInRegionsArray == -1) return; // point not in region
    regionGroups[pnt.xIndexInRegionsArray][pnt.yIndexInRegionsArray].removePoint(pnt);
  }

  /** Are we showing Region Circles
   *  @return true if we are showing region circles
   *  @since NeXtMidas 3.9.4
   */
  public boolean isShowClusters () {
    return showClusters;
  }

  /** Set whether of not to show points as region circles, when they are tightly grouped
   *  @param showClusters render tightly grouped points as region circles
   *  @since NeXtMidas 3.9.4 for NXM-2968
   */
  public void setShowClusters (boolean showClusters) {
    // if previously was not grouping points by region and now am, need to calculate regionss
    // means looping though all points
    if (showClusters) {
      // we have not been assigning points to regionss in the past, need to
      // calculate regions from scratch
      if (!this.formRegionGroups) setFormRegions(true);
    }
    this.showClusters = showClusters;
    refresh();
  }

  /** Continuously add any points to regions.
   *  After initial add, only need to update based on added and deleted points
   *  @param formRegion continuously add any points to regions
   */
  public void setFormRegions (boolean formRegion) {
    if (this.formRegionGroups != formRegion) {
      this.formRegionGroups = formRegion;
      if (formRegion) {
        if (points==null) return;
        // trigger region generation for all points
        for (Point3D thisPoint : points) {
          addPointToRegion(thisPoint);
        }
      }
      else {
        // will need to start from scratch when reenable
        regionGroups = new RegionGroup[MPlot.LONGITUDE_RANGE/RegionGroup.LON_GRID_SIZE][MPlot.LATITUDE_RANGE/RegionGroup.LAT_GRID_SIZE];
      }
    }
  }

  /** Value to determine the minimum size of a cluster
   *  Anything below this will draw all points within the region.
   *  @param minimumClusterSize the minimum value of number of points before cluster is drawn.
   */
  public void setMinimumClusterSize (int minimumClusterSize) {
    this.minimumClusterSize  = minimumClusterSize;
  }

  /** Get the Region Groups
   *  @return the regionGroups
   */
  RegionGroup[][] getRegionGroups () {
    return regionGroups;
  }

  /** A class to hold a highlight filter associated with a specific
   *  Color and track fade option. It gives users the ability to apply
   *  multiple highlight filters in a single LayerT5.
   *
   *  @author Zachary Matthews
   *  @since NeXtMidas 4.1.2 for NXM-3947
   */
  private static class HighlightSetting {
    private Map<String,PredicateType> highlightFilter;
    private Color highlightColor;
    /** number of points draw as highlighted because of this filter (as of last draw) */
    private int numHighlighted = 0;
    private int numVisibleHighlighted = 0;
    private int runningTotalHighlighted = 0;
    private int runningVisibleTotalHighlighted = 0;

    /** HighlightSetting constructor
     *  @param highlightFilter The PredicateType Map filter for this HighlightSetting
     *  @param highlightColor  The Color for this HighlightSetting as a String
     *  @since NeXtMidas 4.1.3 for NXM-3947
     */
    private HighlightSetting (Map<String,PredicateType> highlightFilter, String highlightColor) {
      this(highlightFilter,MidasColor.getColor(highlightColor));
    }

    /** HighlightSetting constructor
     *
     *  NOTE: This constructor should not be used since track fading
     *        for HighlightSetting is no longer supported
     *
     *  @param highlightFilter The PredicateType Map filter for this HighlightSetting
     *  @param highlightColor  The Color for this HighlightSetting as a String
     *  @param colorFade       Enable or disable color fading with this HighlightSetting
     *  @see #HighlightSetting(Map, String)
     */
    private HighlightSetting (Map<String,PredicateType> highlightFilter, String highlightColor, boolean colorFade) {
      this(highlightFilter,MidasColor.getColor(highlightColor));
    }

    /** HighlightSetting constructor
     *  @param highlightFilter The PredicateType Map filter for this HighlightSetting
     *  @param highlightColor  The Color for this HighlightSetting as a Color Object
     *  @since NeXtMidas 4.1.3 for NXM-3947
     */
    private HighlightSetting (Map<String,PredicateType> highlightFilter, Color highlightColor) {
      this.highlightFilter = highlightFilter;
      this.highlightColor = highlightColor;
    }

    /** HighlightSetting constructor
     *
     *  NOTE: This constructor should not be used since track fading
     *        for HighlightSetting is no longer supported
     *
     *  @param highlightFilter The PredicateType Map filter for this HighlightSetting
     *  @param highlightColor  The Color for this HighlightSetting as a Color Object
     *  @see #HighlightSetting(Map, Color)
     */
    private HighlightSetting (Map<String,PredicateType> highlightFilter, Color highlightColor, boolean colorFade) {
      this(highlightFilter,highlightColor);
    }

    /** Gets the PredicateType Map filter for this HighlightSetting
     *  @return The PredicateType Map filter for this HighlightSetting
     *  @since NeXtMidas 4.1.2 for NXM-3947
     */
    private Map<String,PredicateType> getHighlightFilter () {
      return highlightFilter;
    }

    /** Gets the Color for this HighlightSetting
     *  @return The Color for this HighlightSetting as a Color Object
     *  @since NeXtMidas 4.1.2 for NXM-3947
     */
    private Color getHighlightColor () {
      return highlightColor;
    }

    // Totals not checking visibility

    private void incrementRunningTotal () {
      runningTotalHighlighted++;
    }

    private void clearRunningTotal () {
      runningTotalHighlighted = 0;
    }

    private int getRunningTotal () {
      return runningTotalHighlighted;
    }

    private void setNumHighlighted (int numHighlighted) {
      this.numHighlighted = numHighlighted;
    }

    private int getNumHighlighted () {
      return numHighlighted;
    }

    // Totals of those visible

    private void incrementRunningVisibleTotal () {
      runningVisibleTotalHighlighted++;
    }

    private void clearRunningVisibleTotal () {
      runningVisibleTotalHighlighted = 0;
    }

    private int getRunningVisibleTotal () {
      return runningVisibleTotalHighlighted;
    }

    private void setNumVisibleHighlighted (int numVisibleHighlighted) {
      this.numVisibleHighlighted = numVisibleHighlighted;
    }

    private int getNumVisibleHighlighted () {
      return numVisibleHighlighted;
    }
  }
}