package nxm.sys.libg;

import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import nxm.sys.inc.Constants;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MidasColor;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Data;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Position;
import nxm.sys.lib.PredicateType;
import nxm.sys.lib.Table;
import nxm.sys.lib.Time;

/** The Point3D class holds the functionality to create a Point/Ellipse/Cone
 *  This class was extracted out of LayerT5. It is default access for use
 *  only internally.
 *
 *  @see Region
 *  @see RegionGroup
 *  @see LayerT5
 *  @author Jeanette McCall / Kevin Esterheld
 *  @since NeXtMidas 3.9.3 - previously in LayerT5 as private internal class.
 */
public class Point3D {

  String name = ""; // The name of the point

  private String note = ""; // The notes for the point

  int indexInRegion;
  int indexInPointsObject;
  boolean posSet = false; // Has POS been set?
  int distFromCurrentPix = -1; // Used when selecting a point. It is available in case there are multiple LayerT5s that need to be checked for closest point since 3.9.3
  double posAlt = 0.0; // The POS position (alt,lat,lon)

  double posLat = 0.0; // The POS position (alt,lat,lon)

  double posLon = 0.0; // The POS position (alt,lat,lon)

  boolean pospSet = false; // Has POSP been set?

  double pospAlt = 0.0; // The POSP position (alt,lat,lon)

  double pospLat = 0.0; // The POSP position (alt,lat,lon)

  double pospLon = 0.0; // The POSP position (alt,lat,lon)

  boolean elpsSet = false; // Has ellipse been set?

  double elpsSemiMajor = 0.0; // The ellipse parameters (major,minor,tilt)

  double elpsSemiMinor = 0.0; // The ellipse parameters (major,minor,tilt)

  double elpsTilt = 0.0; // The ellipse parameters (major,minor,tilt)

  boolean ellipsoidSet = false; // Has ellipsoid been set?

  double ellipsoidSemiMajor = 0.0; // The ellipsoid semi-major axis

  double ellipsoidSemiMinor = 0.0; // The ellipsoid semi-minor axis

  double ellipsoidSemiMedian = 0.0; // The ellipsoid semi-median axis

  double ellipsoidRotationX = 0.0; // The ellipsoid x-axis rotation

  double ellipsoidRotationY = 0.0; // The ellipsoid y-axis rotation

  double ellipsoidRotationZ = 0.0; // The ellipsoid z-axis rotation

  boolean coneSet = false; // Has cone been set?

  double coneA = 0.0; // The cone parameters (radius,angle,angle)

  double coneB = 0.0; // The cone parameters (radius,angle,angle)

  double coneAngle = 0.0; // The cone parameters (radius,angle,angle)

  boolean conn = false; // The connect flags setting the drawing mode

  float hdg = 0.0f; // Symbol Heading

  byte flagNameLen = 0; // Flag 0: Name length in characters)

  byte flagNamePos = 0; // Flag 1: Name position (not supported)

  byte flagFgColor = 0; // Flag 2: foreground color (X-Midas color index number)

  byte flagBgColor = 0; // Flag 3: background color (not supported)

  byte flagCone = 0; // Flag 4: Cone/ellipse flag (0=hidden, -9=visible)

  byte flagInvert = 0; // Flag 5: Invert (not supported)

  byte flagSymCode = 0; // Flag 6: Symbol (X-Midas symbol ID number)

  byte flagSymSize = 0; // Flag 7: Symbol size

  int _ixPos = 0; // Only used by draw() - Pixel X for pos

  int _iyPos = 0; // Only used by draw() - Pixel Y for pos

  int _ixPosp = 0; // Only used by draw() - Pixel X for posp

  int _iyPosp = 0; // Only used by draw() - Pixel Y for posp

  private double[] coords = null; // Only used by prep(..)

  double minLon = 0.0; // Internal use only: min longitude used in hit(..)

  double maxLon = 0.0; // Internal use only: max longitude used in hit(..)

  double minLat = 0.0; // Internal use only: min latitude used in hit(..)

  double maxLat = 0.0; // Internal use only: max latitude used in hit(..)

  double minAlt = 0.0; // Internal use only: min altitude used in hit(..)

  double maxAlt = 0.0; // Internal use only: max altitude used in hit(..)

  double minX = 0.0; // Internal use only: min x

  double maxX = 0.0; // Internal use only: max x

  double minY = 0.0; // Internal use only: min y

  double maxY = 0.0; // Internal use only: max y

  Symbol symbol = null;   // Symbol to use instead of point
  String symbolName = ""; // Symbol type or path (in case of Icon)

  private double lonNormalization = 0.0; // amount of adjustment made when normalizing POS longitude
  private double latNormalization = 0.0; // amount of adjustment made when normalizing POS latitude

  double xRangeMax = 0.0; // Stores Max of abs(minLon) and maxLon - used when plotting points on 3D graph
  double yRangeMax = 0.0; // Stores Max of abs(minLat) and maxLon - used when plotting points on 3D graph
  double zRangeMax = 0.0; // Stores Max of abs(minAlt) and maxLon - used when plotting points on 3D graph

  private boolean hidden;

  /** X index into Regions Array */
  @InternalUseOnly
  public int xIndexInRegionsArray = -1; // EXPERIMENTAL

  /** Y index into Regions Array */
  @InternalUseOnly
  public int yIndexInRegionsArray = -1; // EXPERIMENTAL

  /** LayerT5 associated with this point */
  public LayerT5 layer;

  double time = 0;
  Color directColor = null;

  /** normally null, but holds highlight color temporarily during draw */
  Color highlightColor = null;

  /** visible highlighted point */
  boolean visibleHighlight = false;

  /** metadata associated with point - often a Table */
  Object metadata = null;

  /** The point's data that is available as filter criteria */
  Table  filterDataTable = new Table();

  /** The MPlot (plot) this point belongs to */
  private MPlot MP;

  private boolean isGeodeticData = true;
  boolean in2DViewRange = false;

  /** Creates a new instance.
   *  @param layer LayerT5 this point is part of
   */
  Point3D(LayerT5 layer) {
    coords = new double[6];
    if (layer!=null) setLayerFields(layer);
  }

  /** Creates a new instance.
   *  @param tbl   Table describing point
   *  @param layer layer LayerT5 this point is part of
   */
  Point3D (Table tbl, LayerT5 layer) {
    this(layer);
    this.fromTable(tbl);
    if (layer!=null) setLayerFields(layer);
  }

  private void setLayerFields (LayerT5 layer) {
    this.layer = layer;
    MP = layer.getMPlot();
    this.isGeodeticData = layer.isGeodeticData();

  }

  /** Prepare for use inside draw() -- only used by draw() method. */
  void prep (boolean doPosp) {
    int k = 0;
    if (doPosp) {
      k = MP.viewer.rwc2pix(coords, 0, 3, 2, layer.pix);
      _ixPosp = layer.pix.x[1];
      _iyPosp = layer.pix.y[1];
    }
    else {
      k = MP.viewer.rwc2pix(coords, 0, 3, 1, layer.pix);
    }
    hidden = (MP.view == MPlot.V_GEODETIC) && k < 1;
    _ixPos = layer.pix.x[0];
    _iyPos = layer.pix.y[0];
  }

  /** Done using in draw() -- only used by draw() method. */
  void done () {
    // may need to do something here in the future
  }

  /** Is this point visible -- only used by draw() method. */
  boolean hit (double lon1, double lon2, double lat1, double lat2) {
    if (coneSet)
      return true; // JC4: Not sure how to optimize this one

    // If the smallest latitude of this pt is beyond the max of the view area
    // or the largest latitude of this pt is under the min of the view area, it is not visible
    if ((lat2 < minLat) || (lat1 > maxLat)) return false; // Out of range (Lat)
    if ((lon2 < minLon) || (lon1 > maxLon)) {
      // If the smallest longitude of this pt is beyond the max of the view area
      // or the largest longitude of this pt is under the min of the view area,
      // it could still be visible if we are in LATLONCONTINUOUS
      if (MP.view != MPlot.V_LATLONCONTINUOUS || !MP.hasExtendedRange()) return false; // Out of range (Lon)
      // Need more sophisticated range check for continuous lat lon - since 3.9.0 for NXM-3284
      if ((lon2 < (minLon+MPlot.LONGITUDE_RANGE)) || (lon1 > (maxLon+MPlot.LONGITUDE_RANGE))) return false;
    }
    return true;
  }

  /** Updates the min/max values for the point. */
  private void updateMinMax () {
    minLon = posLon;
    maxLon = posLon;
    minLat = posLat;
    maxLat = posLat;
    minAlt = posAlt;
    maxAlt = posAlt;
    minX = minLon;
    maxX = maxLon;
    minY = minLat;
    minY = maxLat;
    if (pospSet) {
      minLon = Math.min(minLon, pospLon);
      maxLon = Math.max(maxLon, pospLon);
      minLat = Math.min(minLat, pospLat);
      maxLat = Math.max(maxLat, pospLat);
      minAlt = Math.min(minAlt, pospAlt);
      maxAlt = Math.max(maxAlt, pospAlt);
      minX = minLon;
      maxX = maxLon;
      minY = minLat;
      minY = maxLat;
    }
    if (elpsSet) {
      double rad = Math.max(elpsSemiMajor, elpsSemiMinor);
      double radLat = rad * Constants.METERS2DEG * 2.0; // JC4: Add factor of 2 since this is an est.
      double radLon = rad * Constants.METERS2DEG * 2.0; // JC4: Add factor of 2 since this is an est.
      minLon = Math.min(minLon, posLon - radLon);
      maxLon = Math.max(maxLon, posLon + radLon);
      minLat = Math.min(minLat, posLat - radLat);
      maxLat = Math.max(maxLat, posLat + radLat);
      minX = Math.min(minX, posLon - radLon);
      maxX = Math.min(maxX, posLon + radLon);
      minY = Math.min(minY, posLat - radLat);
      maxY = Math.min(maxY, posLat + radLat);
    }
    if (ellipsoidSet) {
      double rad = Math.max(Math.max(ellipsoidSemiMajor, ellipsoidSemiMinor),ellipsoidSemiMedian);
      double deg = rad * Constants.METERS2DEG;
      minLon = Math.min(minLon, posLon - deg);
      maxLon = Math.max(maxLon, posLon + deg);
      minLat = Math.min(minLat, posLat - deg);
      maxLat = Math.max(maxLat, posLat + deg);
      minAlt = Math.min(minAlt, posAlt - rad);
      maxAlt = Math.max(maxAlt, posAlt + rad);
      minX = Math.min(minX, posLon - rad);
      maxX = Math.min(maxX, posLon + rad);
      minY = Math.min(minY, posLat - rad);
      maxY = Math.min(maxY, posLat + rad);
    }
    // Save off the maximum x,y, & z distance from 0,0,0 - used by 3D Graph view
    xRangeMax = Math.max(maxX, Math.abs(minX));
    yRangeMax = Math.max(maxY, Math.abs(minY));
    zRangeMax = Math.max(maxAlt, Math.abs(minAlt));
    layer.updateRange(this);
  }

  /** Gets the pixel position of pos. */
  Pix getPosPix (Pix p) {
    double[] pos = new double[] { posLon, posLat, posAlt };
    MP.viewer.rwc2pix(pos, 0, 3, 1, p);
    return p;
  }

  /** Gets the pixel position of posp. */
  private Pix getPospPix (Pix p) {
    double[] posp = new double[] { pospLon, pospLat, pospAlt };
    MP.viewer.rwc2pix(posp, 0, 3, 1, p);
    return p;
  }

  /** Sets the name of the point.
   *  @param name name of the point */
  public void setName (String name) {
    this.name = name;
  }

  /** Gets the name of the point.
   *  @return the name of the point */
  public String getName () {
    return name;
  }

  /** Sets the point's note.
   *  @param note the note associated with the point */
  public void setNote (String note) {
    this.note = note;
  }

  /** Gets the point's note.
   *  @return note the note associated with the point */
  public String getNote () {
    return note;
  }

  /** Gets the connection flag that indicates if this point is connected to the previous one. */
  boolean getConn () {
    return conn;
  }

  /** Sets the connection flag to indicate if this point is connected to the previous one. */
  void setConn (boolean conn) {
    this.conn = conn;
  }

  /** Sets the connection flag to indicate if this point is connected to the previous one. */
  void setConn (byte conn) {
    this.conn = (conn == 1);
  }

  /** Gets the heading of the symbol. */
  float getHdg () {
    return hdg;
  }

  /** Sets the heading of the symbol. */
  void setHdg (float hdg) {
    this.hdg = hdg;
  }

  /** Sets the connection flag to indicate if this point is connected to the previous one. */
  private void setConn (Object obj) {
    if (obj instanceof Data)
      obj = toArray(obj, new byte[1]);
    if (obj instanceof byte[])
      setConn(((byte[]) obj)[0] != 0);
    else if (obj != null)
      setConn(Convert.s2state(obj.toString(), conn));
  }

  /** Sets the direct color specification */
  void setDirectColor (Object obj) {
    directColor = MidasColor.getColor(obj);
  }

  /** Gets the direct color setting for the point. */
  Color getColor () {
    return directColor;
  }

  /** Sets the direct color specification */
  void setColor (Object obj) {
    directColor = MidasColor.getColor(obj);
  }

  /** Gets the direct color setting for the point. */
  Color getDirectColor () {
    return directColor;
  }

  /**
   * Sets the position of the point.
   * @param alt The altitude.
   * @param lat The latitude (in deg).
   * @param lon The longitude (in deg).
   */
  public void setPos (double alt, double lat, double lon) {
    if (isGeodeticData) {
      // wrap points for NXM-3510  3.9.3
      lonNormalization = 0;
      latNormalization = 0;
      while (lon > MPlot.LONGITUDE_DEFAULT_MAX) {lon -= MPlot.LONGITUDE_RANGE; lonNormalization -= MPlot.LONGITUDE_RANGE;}
      while (lon < MPlot.LONGITUDE_DEFAULT_MIN) {lon += MPlot.LONGITUDE_RANGE; lonNormalization += MPlot.LONGITUDE_RANGE;}
      while (lat > MPlot.LATITUDE_DEFAULT_MAX)  {lat -= MPlot.LATITUDE_RANGE;  latNormalization -= MPlot.LATITUDE_RANGE;}
      while (lat < MPlot.LATITUDE_DEFAULT_MIN)  {lat += MPlot.LATITUDE_RANGE;  latNormalization += MPlot.LATITUDE_RANGE;}
    }
    coords[0] = posLon = lon;
    coords[1] = posLat = lat;
    coords[2] = posAlt = alt;
    posSet = true;
    updateMinMax();
  }

  /** Internal use only */
  private void setPos (Object obj) {
    if (obj instanceof Table)
      setPos((Table) obj);
    if (obj instanceof Position)
      setPos((Position) obj);
    if (obj instanceof Data)
      obj = toArray(obj, new double[3]);
    if (obj instanceof double[])
      setPos((double[]) obj);
  }

  /**
   * Sets the position of the point with a table.
   * @param t Pointing position table <code>{LON=lon,LAT=lat,ALT=alt}</code>
   */
  public void setPos (Table t) {
    if (t.containsKey("LON") || !t.containsKey("X")) {
      setPos(t.getD("ALT", posAlt), t.getD("LAT",posLat), t.getD("LON",posLon)); // default to exiting values - since 3.7.0 Bug 2965
    }
    else {
      setPos(t.getD("Z", posAlt), t.getD("Y",posLat), t.getD("X",posLon));
    }
  }

  /**
   * Sets the position of the point with a Position.
   * @param p Position in {@link Position#GEODETIC} mode.
   */
  public void setPos (Position p) {
    setPos(p.getAlt(), p.getLat(), p.getLon());
  }

  /** Sets the position of the point with an array (lon,lat,alt).
   *  @param a The pointing position <code>{ lon, lat, alt }</code>
   */
  public void setPos (double[] a) {
    setPos(a[2], a[1], a[0]);
  }

  /** Gets the position.
   *  @return the point's position */
  public Position getPos () {
    return Position.fromGeo(posAlt, posLat, posLon);
  }

  /** Sets the pointing position of the point.
   *  @param alt The altitude.
   *  @param lat The latitude (in deg).
   *  @param lon The longitude (in deg).
   */
  public void setPosp (double alt, double lat, double lon) {
    if (isGeodeticData) {
      // wrap/adjust points the same amount as for POS for NXM-3510  3.9.3
      // assumes POS will be set before POSP
      lon += lonNormalization;
      lat += latNormalization;
    }
    coords[3] = pospLon = lon;
    coords[4] = pospLat = lat;
    coords[5] = pospAlt = alt;
    pospSet = (layer.bVec == -1);
    updateMinMax();
  }

  /** Internal use only */
  private void setPosp (Object obj) {
    if (obj instanceof Table)
      setPosp((Table) obj);
    if (obj instanceof Position)
      setPosp((Position) obj);
    if (obj instanceof double[])
      setPosp((double[]) obj);
  }

  /** Sets the pointing position of the point with a table.
   *  @param t Pointing position table <code>{LON=lon,LAT=lat,ALT=alt}</code>
   */
  public void setPosp (Table t) {
    if (t.containsKey("LON") || !t.containsKey("X")) {
      setPosp(t.getD("ALT", pospAlt), t.getD("LAT", pospLat), t.getD("LON", pospLon)); // default to exiting values - since 3.7.0 Bug 2965
    }
    else {
      setPosp(t.getD("Z", pospAlt), t.getD("Y",pospLat), t.getD("X",pospLon));
    }
  }

  /** Sets the pointing position of the point with a Position.
   *  @param p Position in {@link Position#GEODETIC} mode.
   */
  public void setPosp (Position p) {
    setPosp(p.getAlt(), p.getLat(), p.getLon());
  }

  /** Sets the pointing position of the point with an array (lon,lat,alt).
   *  @param a The pointing position <code>{ lon, lat, alt }</code>
   */
  public void setPosp (double[] a) {
    setPosp(a[2], a[1], a[0]);
  }

  /** Gets the pointing position.
   *  @return the point's pointing position.
   */
  public Position getPosp () {
    return Position.fromGeo(pospAlt, pospLat, pospLon);
  }


  /** Sets the flags associated with the point
   *  @param flags the flags associated with the point */
  void setFlags (Object flags) {
    if (flags instanceof Data)
      flags = toArray(flags, new byte[8]);
    if (flags instanceof byte[])
      setFlags((byte[]) flags);
    if (flags instanceof Table)
      setFlags((Table) flags, (byte) name.length());
  }

  /** Sets the flags associated with the point
   *  @param flags the byte[] of flags associated with the point */
  private void setFlags (byte[] flags) {
    flagNameLen = flags[0];
    flagNamePos = flags[1];
    flagFgColor = flags[2];
    flagBgColor = flags[3];
    flagCone = flags[4];
    flagInvert = flags[5];
    flagSymCode = flags[6];
    flagSymSize = flags[7];
  }

  /** Sets the name length flag.
   *  @param  name length flag */
  void setFlagNameLen (byte val) {
    flagNameLen = val;
  }

  /** Sets the name position flag.
   *  @param  name position flag */
  void setFlagNamePos (byte val) {
    flagNamePos = val;
  }

  /** Sets the foreground color flag. */
  void setFlagFgColor (byte val) {
    flagFgColor = val;
  }

  /** Sets the background color flag. */
  void setFlagBgColor (byte val) {
    flagBgColor = val;
  }

  /** Sets the cone/ellipse flag. */
  void setFlagCone (byte val) {
    flagCone = val;
  }

  /** Sets the invert flag. */
  void setFlagInvert (byte val) {
    flagInvert = val;
  }

  /** Sets the symbol code flag. */
  void setFlagSymCode (byte val) {
    flagSymCode = val;
  }

  /** Sets the symbol size flag. */
  void setFlagSymSize (byte val) {
    flagSymSize = val;
  }

  /** Gets the name length flag. */
  byte getFlagNameLen () {
    return flagNameLen;
  }

  /** Gets the name position flag. */
  byte getFlagNamePos () {
    return flagNamePos;
  }

  /** Gets the foreground color flag. */
  byte getFlagFgColor () {
    return flagFgColor;
  }

  /** Gets the background color flag. */
  byte getFlagBgColor () {
    return flagBgColor;
  }

  /** Gets the cone/ellipse flag. */
  byte getFlagCone () {
    return flagCone;
  }

  /** Gets the invert flag. */
  byte getFlagInvert () {
    return flagInvert;
  }

  /** Gets the symbol code flag. */
  byte getFlagSymCode () {
    return flagSymCode;
  }

  /** Gets the symbol size flag. */
  byte getFlagSymSize () {
    return flagSymSize;
  }

  /** Sets the flags via a table. */
  private void setFlags (Table t) {
    setFlags(t, flagNameLen);
  }

  /** Internal use only. */
  private void setFlags (Table tab, byte nameLen) {
    flagNameLen = tab.getB("NAMELEN", nameLen);
    flagNamePos = tab.getB("NAMEPOS", flagNamePos);
    flagFgColor = tab.getB("COLOR", flagFgColor);
    flagBgColor = tab.getB("BGCOLOR", flagBgColor);
    flagCone    = tab.getB("CONE", flagCone);
    flagInvert  = tab.getB("INVERT", flagInvert);
    flagSymCode = tab.getB("SYMBOLID", flagSymCode);
    flagSymSize = tab.getB("SYMBOLSIZE", flagSymSize);
  }

  /** Gets the flag as a data object. */
  private Data getFlagsData () {
    byte[] array = { flagNameLen, flagNamePos, flagFgColor, flagBgColor, flagCone, flagInvert,
        flagSymCode, flagSymSize };
    return new Data(array);
  }

  /** Converts a Data object to an array. */
  private Object toArray (Object obj, Object array) {
    Data data = (Data) obj;
    data.toArray(array);
    return array;
  }

  /** Internal use only. */
  private void setEllipsoid (Object obj) {
    if (obj instanceof Table) // Since 3.7.0 for bug 2965
      setEllipsoid((Table) obj);
    if (obj instanceof Data)
      obj = toArray(obj, new double[3]);
    if (obj instanceof double[])
      setEllipsoid((double[]) obj);
  }

  /** sets the ellipsoid via a Table
   *  @param t Table describing ellipsoid
   */
  private void setEllipsoid (Table t) {
    double semiMajor = (t.containsKey("SEMIMAJOR")) ? t.getD("SEMIMAJOR") : t.getD("AXIS1");
    double semiMinor = (t.containsKey("SEMIMINOR")) ? t.getD("SEMIMINOR") : t.getD("AXIS2");
    double semiMedian = (t.containsKey("SEMIMEDIAN")) ? t.getD("SEMIMEDIAN") : t.getD("AXIS3");
    double rotationX = (t.containsKey("ROTATION_X")) ? t.getD("ROTATION_X") : t.getD("ANGLE1");
    double rotationY = (t.containsKey("ROTATION_Y")) ? t.getD("ROTATION_Y") : t.getD("ANGLE2");
    double rotationZ = (t.containsKey("ROTATION_Z")) ? t.getD("ROTATION_Z") : t.getD("ANGLE3");
    setEllipsoid(semiMajor, semiMinor, semiMedian,rotationX,rotationY,rotationZ);
  }

  /** Sets the ellipsoid parameters
   *
   *  @param semiMajor  The semi-major axis (in meters).
   *  @param semiMinor  The semi-minor axis (in meters).
   *  @param semiMedian The semi-median axis (in meters).
   *  @param rotationX  rotation about X axis (East)
   *  @param rotationY  rotation about Y axis (North)
   *  @param rotationZ  rotation about Z axis (Up)
   */
  void setEllipsoid (double semiMajor, double semiMinor, double semiMedian,
      double rotationX, double rotationY, double rotationZ) {
    ellipsoidSemiMajor = semiMajor;
    ellipsoidSemiMinor = semiMinor;
    ellipsoidSemiMedian = semiMedian;
    ellipsoidRotationX = rotationX;
    ellipsoidRotationY = rotationY;
    ellipsoidRotationZ = rotationZ;
    ellipsoidSet = true;
    updateMinMax();
  }

  /** Gets the ellipsoid as a data object. */
  private Data getEllipsoidData () {
    double[] array = { ellipsoidSemiMajor, ellipsoidSemiMinor, ellipsoidSemiMedian,
                       ellipsoidRotationX, ellipsoidRotationY, ellipsoidRotationZ};
    return new Data(array);
  }

  /** Gets the ellipsoid axes as a data object. */
  private Data getEllipsoidAxes () {
    double[] array = { ellipsoidSemiMajor, ellipsoidSemiMinor, ellipsoidSemiMedian};
    return new Data(array);
  }

  /** Gets the ellipsoid rotation as a data object. */
  private Data getEllipsoidRotation () {
    double[] array = {ellipsoidRotationX, ellipsoidRotationY, ellipsoidRotationZ};
    return new Data(array);
  }

  /**
   * Sets the ellipse parameters with a table.
   * @param t ellipse parameters table <code>{SEMIMAJOR=elpsSemeiMajor,SEMIMINOR=elpsSemiMinor,TILT=elpsTilt}</code>
   * @since NeXtMidas 3.7.0 for Bug 2965
   */
  private void setElps (Table t) {
    setElps(t.getD("SEMIMAJOR", elpsSemiMajor), t.getD("SEMIMINOR", elpsSemiMinor), t.getD("TILT",elpsTilt)); // default to exiting values - since 3.7.0 Bug 2965
  }

  /**
   * Sets the ellipse parameters from an array (a,b,c).
   * @param semiMajor The semi-major axis (in meters).
   * @param semiMinor The semi-minor axis (in meters).
   * @param tilt The tilt (in deg).
   */
  public void setElps (double semiMajor, double semiMinor, double tilt) {
    elpsSemiMajor = semiMajor;
    elpsSemiMinor = semiMinor;
    elpsTilt = tilt;
    elpsSet = true;
    updateMinMax();
  }

  /**
   * Sets the ellipse parameters from an array (a,b,c).
   * @param a The ellipse parameters <code>{ semiMajor, semiMinor, tilt }</code>.
   */
  public void setElps (double[] a) {
    setElps(a[0], a[1], a[2]);
  }

  /** Internal use only. */
  private void setElps (Object obj) {
    if (obj instanceof Table) // Since 3.7.0 for bug 2965
      setElps((Table) obj);
    if (obj instanceof Data)
      obj = toArray(obj, new double[3]);
    if (obj instanceof double[])
      setElps((double[]) obj);
  }

  /**
   * Sets the cone parameters
   * @param a (saved, but not used at this time)
   * @param b (saved, but not used at this time)
   * @param angle Cone angle.
   */
  public void setCone (double a, double b, double angle) {
    coneA = a;
    coneB = b;
    coneAngle = angle;
    coneSet = true;
    updateMinMax();
  }

  /** Gets the elps as a data object. */
  private Data getElpsData () {
    double[] array = { elpsSemiMajor, elpsSemiMinor, elpsTilt };
    return new Data(array);
  }

  /**
   * Sets the cone parameters from an array (a,b,c).
   * @param a The cone parameters <code>{ a, b, angle }</code>.
   */
  public void setCone (double[] a) {
    setCone(a[0], a[1], a[2]);
  }

  /** Gets the time associated with the point
   *  @return time associated with the point as a double
   */
  public double getTime () {
    return time;
  }

  /** Set Time is seconds
   *  @param time double containing time in seconds (since 1950)
   *  @since NeXtMidas 3.9.3 for NXM-3515
   */
  void setTime (double time) {
//    if (time < Time.SECONDS_PER_DAY) time += Time.currentTime().getYiS();  TODO - Do we need this ...Time might take care of the csecond current day
    this.time = time;
  }

  /** Set Time is seconds
   *  @param time double containing time in seconds (since 1950)
   *  @since NeXtMidas 3.9.3 for NXM-3515
   */
  void setTime (Object obj) {
     if (obj instanceof Data)
      setTime(((Data) obj).toD());
    if (obj instanceof double[])
      setTime((double[]) obj);
  }

  Object getMetadata () {
    return metadata;
  }

  /** If the metadata parameter passed in represents a valid Table, then
   *  set the metadata field to the converted Table. Otherwise, just
   *  set the metadata field to the original metadata parameter.
   *  @param metadata The metadata Object parameter
   *  @since NeXtMidas 3.9.3, updated in 4.1.2
   */
  void setMetadata (Object metadata) {
    if (metadata!=null && metadata instanceof String) {
      String tableStr = ((String)metadata).trim();
      if (tableStr.startsWith("{") && tableStr.endsWith("}")) {
        int mode = Table.getDefaultModeInt();
        int flags = Table.getDefaultFlagsInt() + Table.CHECK_MALFORMED;
        try {
          Table metaTable = new Table(mode,flags);
          metaTable.fromBytes(tableStr.getBytes(),0, tableStr.length());
          if (metaTable != null) metadata=metaTable;
        }
        catch (MidasException e){
          // Table is malformed, let metadata field just be a String
        }
      }
    }
    this.metadata = metadata;
  }

  /** Sets the symbol object based on the name of the Symbol or Icon provided.
   *  Sets the symbol orientation based on where it is relative to POSP.
   *    heading = 90 - toDegrees(atan2(latDelta,lonDelta))
   *    NOTE: The angle calculated from atan2 is with respect to EAST, the 90 degree adjustment
   *          makes it so that the final calculated heading is with respect to NORTH since this
   *          is what NeXtMidas uses.
   *  Sets the symbol size based on its distance from POSP.
   *    size = sqrt((latDelta)^2+(lonDelta)^2)
   *  @param symName the name of the Symbol or Icon to use
   */
  void setSymbol (String symName) {
    symbolName = symName;
    symbol = Symbol.getSymbol(symbolName);
    if (pospSet) {
      double latDelta = pospLat - posLat;
      double lonDelta = pospLon - posLon;
      double radians = Math.atan2(latDelta, lonDelta);
      double angle = Math.toDegrees(radians);
      float heading = (float)(90 - angle);
      symbol.setHeading(heading);
      double hypSquare = latDelta*latDelta + lonDelta*lonDelta;
      symbol.setSize((int)(Math.sqrt(hypSquare)*2));
    }
  }

  /** Set up the table used for filtering points.
   *  This needs to be CALLED AFTER the point is
   *  fully initialized.
   *  VD fields have each component added as a
   *  separate entry (ex: POS_LAT)
   *
   *  @param addName add NAME if present
   *  @param addPos  add POS if present
   *  @param addPosp add POSP if present
   *  @param addElps add ELPS if present
   *  @param addCone add CONE if present
   *  @param addTime add TIME if present
   *  @param addNote add NOTE if present
   *  @param addMeta add META if present
   */
  void setFilterDataTable (boolean addName, boolean addPos, boolean addPosp,
      boolean addElps, boolean addCone, boolean addTime, boolean addNote, boolean addMeta) {
    filterDataTable.clear();
    if (addName && (name!=null) && !name.isEmpty()) filterDataTable.put("NAME", getName());
    if (isGeodeticData) {
      if (addPos && posSet) {
        Table posTable = new Table();
        posTable.put("LAT", posLat);
        posTable.put("LON", posLon);
        posTable.put("ALT", posAlt);
        filterDataTable.put("POS",posTable);
      }
      if (addPosp && pospSet) {
        Table pospTable = new Table();
        pospTable.put("LAT", pospLat);
        pospTable.put("LON", pospLon);
        pospTable.put("ALT", pospAlt);
        filterDataTable.put("POSP",pospTable);
      }
    }
    else {
      if (addPos && posSet) {
        Table posTable = new Table();
        posTable.put("X", posLon);
        posTable.put("Y", posLat);
        posTable.put("Z", posAlt);
        filterDataTable.put("POS",posTable);
      }
      if (addPosp && pospSet) {
        Table pospTable = new Table();
        pospTable.put("X", pospLon);
        pospTable.put("Y", pospLat);
        pospTable.put("Z", pospAlt);
        filterDataTable.put("POSP",pospTable);
      }
    }
    if (addElps && elpsSet) {
      Table elpsTable = new Table();
      elpsTable.put("MAJOR",elpsSemiMajor);
      elpsTable.put("MINOR",elpsSemiMinor);
      elpsTable.put("TILT",elpsTilt);
      filterDataTable.put("ELPS",elpsTable);
    }
    if (addCone && coneSet) {
      Table coneTable = new Table();
      coneTable.put("A",coneA);
      coneTable.put("B",coneB);
      coneTable.put("ANGLE",coneAngle);
      filterDataTable.put("CONE",coneTable);
    }
    if (addTime && time > 0) {
      filterDataTable.put("TIME",time);
    }
    if (addNote && (note != null) && !note.isEmpty()) { // since 3.9.3
      filterDataTable.put("NOTE", note);
    }
    if (addMeta && (metadata !=null)) { // experimental 3.9.3
      if (!(metadata instanceof Table)) filterDataTable.put("METADATA", metadata);
      else {
        filterDataTable.addIfNotPresent((Table)metadata);
      }
    }
  }

  /** Get the point information in the Table form used by point fitlering
   *  (ex: POS.LAT with a double, rather than POS as a Position)
   *  @return point information in Table with nested key format
   */
  public Table getFilterDataTable () {
    return filterDataTable;
  }

  /** Internal use only. */
  private void setCone (Object obj) {
    if (obj instanceof Data)
      obj = toArray(obj, new double[3]);
    if (obj instanceof double[])
      setCone((double[]) obj);
  }

  private Data getConnData () {
    return (conn) ? new Data(1) : new Data(0);
  }

  /** Gets the cone as a data object.
   *  @return cone as data object*/
  private Data getConeData () {
    double[] array = { coneA, coneB, coneAngle };
    return new Data(array);
  }

  /** Returns whether this point is hidden by another object.
   *  @return whether this point is hidden by another object */
  public boolean isHidden () {
    return hidden;
  }

  /** Gets the points values based as a table.
   *  This version is used for point replication and display.
   *  @return point values as a table
   */
  public Table toTable() {

    if (!isGeodeticData) return toXYZTable();

    Table table = new Table();

    if ((name!=null) && !name.isEmpty()) table.put("NAME", getName());
    if ((note != null) && !note.isEmpty()) table.put("NOTE", note);
    table.put("CONN", getConnData());
    table.put("POS", getPos());
    if (pospSet) table.put("POSP", getPosp());
    if (elpsSet) table.put("ELPS", getElpsData());
    if (ellipsoidSet) table.put("ELLIPSOID", getEllipsoidData());
    table.put("CONE", getConeData());
    table.put("FLAG", getFlagsData());
    table.put("TIME",Time.toString(time,Time.STD,6));
    if (symbol!=null) table.put("SYMBOL",symbolName);
    if ((metadata !=null)) table.put("METADATA", metadata);
    if ((layer != null) && (layer.name != null)) { // since 3.9.3
      table.put("LAYER", layer.name);
    }

    return table;
  }

  /** Forms a Table describing an XYZ (non-geodetic) point, but only active attributes are included
   *
   *  @return Table describing this point
   *  @since NeXtMidas 4.1.2
   */
  Table toXYZTable () {
    Table table = new Table();
    if ((name!=null) && !name.isEmpty()) table.put("NAME", getName());
    if ((note != null) && !note.isEmpty()) table.put("NOTE", note);
    Table posTbl = new Table("{X="+posLon+",Y="+posLat+",Z="+posAlt+"}");
    table.put("POS",posTbl);
    if (pospSet) {
      Table pospTbl = new Table("{X="+pospLon+",Y="+pospLat+",Z="+pospAlt+"}");
      table.put("POSP",pospTbl);
    }
    if (elpsSet) table.put("ELPS", getElpsData());
    if (ellipsoidSet) table.put("ELLIPSOID", getEllipsoidData());
    if (coneSet) table.put("CONE", getConeData());
    if (time > 0) table.put("TIME",Time.toString(time,Time.STD,6));
    if (symbol!=null) table.put("SYMBOL",symbolName);
    if ((metadata !=null)) table.put("METADATA", metadata);
    return table;
  }

  /** Sets the points values based on a table. */
  void fromTable (Table tbl) {
    if (tbl.containsKey("NAME")) setName(tbl.getS("NAME", name));
    if (tbl.containsKey("NOTE")) setNote(tbl.getS("NOTE", note));

    if (tbl.containsKey("CONN")) setConn(tbl.getKey("CONN"));
    setPos(tbl.getKey("POS"));
    if (tbl.containsKey("POSP")) setPosp(tbl.getKey("POSP"));
    if (tbl.containsKey("FLAG")) setFlags(tbl.getKey("FLAG"));
    if (tbl.containsKey("ELLIPSOID")) setEllipsoid(tbl.getKey("ELLIPSOID"));
    else if (tbl.containsKey("ELPSOID")) setEllipsoid(tbl.getKey("ELPSOID"));
    if (tbl.containsKey("ELPS")) setElps(tbl.getKey("ELPS"));
    if (tbl.containsKey("CONE")) setCone(tbl.getKey("CONE"));
    if (tbl.containsKey("TIME")) setTime(tbl.getKey("TIME"));
    if (!tbl.containsKey("METADATA")) {
      String pipeSeparatedMetaNames = tbl.getS("METADATAFIELDS");
      if (pipeSeparatedMetaNames != null) {
        String[] metadataFieldNames = (pipeSeparatedMetaNames==null || pipeSeparatedMetaNames.isEmpty()) ?
            null : tbl.getS("METADATAFIELDS").split("\\|");
        layer.setMetadataFieldNames(metadataFieldNames);
        metadata = new Table();
        for (String field : metadataFieldNames) {
          ((Table) metadata).put(field, tbl.getKey(field));
        }
      }
    }
    else {
      metadata = tbl.getKey("METADATA");
    }
    if (tbl.containsKey("COLOR")) {
      Object colorValue = tbl.get("COLOR");
      directColor = MidasColor.getColor(colorValue);
    }
    if (tbl.containsKey("SYMBOL")) {
      symbolName = tbl.getS("SYMBOL");
      symbol = Symbol.getSymbol(symbolName);
      if (tbl.containsKey("POSP")) {
        double latDelta = pospLat - posLat;
        double lonDelta = pospLon - posLon;
        double radians = Math.atan2(latDelta, lonDelta);
        double angle = Math.toDegrees(radians);
        float heading = (float)(90 - angle);
        symbol.setHeading(heading);
        double hypSquare = latDelta*latDelta + lonDelta*lonDelta;
        symbol.setSize((int)(Math.sqrt(hypSquare)*2));
      }
    }
    updateMinMax();
  }

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

  /** Forms a Table describing the point, but only active attributes are included
   *  (for use in popup describing point)
   *
   *  @return Table describing this point
   *  @since NeXtMidas 3.9.3
   */
  Table toPopupTable () {
    Table table = new Table();
    if ((name!=null) && !name.isEmpty()) table.put("NAME", getName());
    if (isGeodeticData) {
      table.put("Pos Lat", posLat);
      table.put("Pos Lon", posLon);
      table.put("Pos Alt", posAlt);
      if (pospSet) {
        table.put("Posp Lat", pospLat);
        table.put("Posp Lon", pospLon);
        table.put("Posp Alt", pospAlt);
      }
    }
    else {
      String xlabel = (layer.popupLabels.containsKey("POS.X")) ? layer.popupLabels.getS("POS.X") : "Pos X";
      String ylabel = (layer.popupLabels.containsKey("POS.Y")) ? layer.popupLabels.getS("POS.Y") : "Pos Y";
      String zlabel = (layer.popupLabels.containsKey("POS.Z")) ? layer.popupLabels.getS("POS.Z") : "Pos Z";
      table.put(xlabel, posLon);
      table.put(ylabel, posLat);
      if (!zlabel.isEmpty()) table.put(zlabel, posAlt);
      if (pospSet) {
        xlabel = (layer.popupLabels.containsKey("POSP.X")) ? layer.popupLabels.getS("POSP.X") : "Posp X";
        ylabel = (layer.popupLabels.containsKey("POSP.Y")) ? layer.popupLabels.getS("POSP.Y") : "Posp Y";
        zlabel = (layer.popupLabels.containsKey("POSP.Z")) ? layer.popupLabels.getS("POSP.Z") : "Posp Z";
        if (!xlabel.isEmpty()) table.put(xlabel, pospLon);
        if (!ylabel.isEmpty()) table.put(ylabel, pospLat);
        if (!zlabel.isEmpty()) table.put(zlabel, pospAlt);
      }
    }
    if (elpsSet) {
      table.put("Ellipse Major",elpsSemiMajor);
      table.put("Ellipse Minor",elpsSemiMinor);
      table.put("Ellipse Tilt",elpsTilt);
    }
    if (ellipsoidSet) {
      table.put("Ellipsoid Axes ENU",getEllipsoidAxes());
      table.put("Ellipsoid Rotation ENU",getEllipsoidRotation());
    }
    if (coneSet) {
      table.put("Cone A",coneA);
      table.put("Cone B",coneB);
      table.put("Cone Angle",coneAngle);
    }
    if (time > 0) { //ToDO decide if want this
      table.put("Time",Time.toString(time,Time.STD,6));
    }
    if ((note != null) && !note.isEmpty()) { // since 3.9.3
      table.put("Note", note);
    }
    if ((metadata !=null)) { // experimental 3.9.3
      table.put("Metadata", metadata);
    }
    if ((layer !=null) && (layer.name !=null)) { // since 3.9.3
      table.put("Layer", layer.getName());
    }
    return table;
  }

  /** Get Latitude component of the point's POS
   *  @return the latitude component of the point's POS
   */
  public double getPosLat () {
    return posLat;
  }

  /** Get Longitude component of the point's POS
   *  @return the longitude component of the point's POS
   */
  public double getPosLon () {
    return posLon;
  }

  /** Get a List of points that pass the filter tests, given a List of Points and a Filter Map
   *  @param points     points to be checked
   *  @param metaFilter Map with Key and Predicate Test
   *  @return only the points that pass the filter tests
   */
  public static List<Point3D> getPointsMatchingFilter (List<Point3D> points, Map<String,PredicateType> metaFilter) {
    List<Point3D> matchingPoints = new ArrayList<>();
    for (Point3D point : points) {
      if (passFilterTests(point, metaFilter)) matchingPoints.add(point);
    }
    return matchingPoints;
  }

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