package nxm.sys.lib;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import nxm.sys.inc.Constants;
import nxm.sys.inc.DataTypes;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.Keyable;
import nxm.sys.inc.ListFile;
import nxm.sys.inc.MidasReference;
import nxm.sys.inc.PlotFile;
import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.inc.Tablizable;
import nxm.sys.inc.Units;
import nxm.sys.libm.Tolerance;

/** A class to define a position.
    @author Jeff Schoen / J. Czechowski IV
*/
public class Position extends Object implements Constants,Keyable,Cloneable,Tablizable {
  /** The Position/StateVector modes (see {@link StateVector} for details). */
  public static enum SVMode {
    /** Type 5000 state vector with applicable transform, data in ECR, ECI,
        TOP or TOPOCENT as specified in the QUADWORDS of the Type 5000 file
     */
    T5Data,
    /** Same as mode=Entries but with data in ECR, ECI, TOP or
        TOPOCENT as specified in the QUADWORDS of the Type 5000 file
     */
    T5Entries,
    /** ListFile or Table Entries with columns: TIME, POS, VEL, ACC
     */
    Entries,
    /** Ephemeral keywords read from a file */
    KeywordsEph,
    /** Vectored keywords read from a file */
    KeywordsVec,
    /** LON/LAT/ALT keywords read from a Platinum file */
    KeywordsLLA,
    /** Ephemeral keywords read from a Platinum file */
    KeywordsPlat
  };

  private static final Map<String, String> SV_KEY_MAP = Stream.of(new String[][] {
          { "ETIM", "TIME" },
          { "POSX", "X" },
          { "POSY", "Y" },
          { "POSZ", "Z" },
          { "VELX", "VX" },
          { "VELY", "VY" },
          { "VELZ", "VZ" },
          { "ACCX", "AX" },
          { "ACCY", "AY" },
          { "ACCZ", "AZ" } })
      .collect(Collectors.toMap(data -> data[0], data -> data[1]));

  /** Reference Frame: CARTESIAN (NORMAL) */
  public static final int NORMAL=0;
  /** Reference Frame: GEODETIC */
  public static final int GEODETIC=1;
  /** Reference Frame: 3D GRAPH */
  public static final int GRAPH3D=2;
  /** The current Reference Frame - do not modify directly, use public sets/gets. */
  @InternalUseOnly
  public int referenceFrame=0;
  /** CARTESIAN coordinates - do not modify directly, use public sets/gets. */
  @InternalUseOnly
  public double x,y,z;
  /** Range (see Transform) - do not modify directly, use public sets/gets. */
  @InternalUseOnly
  public double r;
  /** Time - do not modify directly, use public sets/gets. */
  @InternalUseOnly
  public double t;
  /** Geodetic coordinates - do not modify directly, use public sets/gets. */
  @InternalUseOnly
  public double lat,lon,alt;

  // Since NeXtMidas 3.3.0 there is the option of holding the data from an input
  // file in a data buffer (traditional method, uses Type 5000 file) or an array
  // of positional information. When 'data' is used, the entries will be tables
  // with entries: {TIME=time,PX=x,PY=y,PZ=z,...}
  protected BaseFile    inFile;        // File used for input (optional)
  protected Data        db;            // Data from DataFile for Type 5000 files (optional)
  protected int         iPos,iPosOff;  // Index into data buffer for POS
  protected List<Table> data;          // Position data (optional)
  protected Transform   transform;     // Applicable transform
  protected SVMode      svMode;        // StateVector mode
  protected Time        currentTime = new Time(); // Current time

  /** Use new efficient code contributed by Matt Smith */
  private static boolean useEfficientSVKeywordCode = true;

  /** Create a Position using the CARTESIAN (NORMAL) reference frame. */
  public Position () {
  }

  /** Create a Position in given reference frame
      @param referenceFrame reference frame for Position
   */
  public Position (int referenceFrame) {
    if (referenceFrame==GEODETIC) setGeo(0.0,0.0,0.0);
    this.referenceFrame = referenceFrame;
  }

  /** Create a Position given a position file.
   *  @param ref  The Midas reference
   *  @param name The file from which to initialize <i>(Prior to NeXtMidas 3.3.0 this
   *              only accepted a string. This was changed in NeXtMidas 3.3.0 to match
   *              the usage in {@link BaseFile} which permits {@link FileName}, etc.)</i>
   *  @deprecated Since NeXtMidas 3.5.4, use {@link Position#Position(MidasReference,Object)}
   */
  @Deprecated
  public Position (Object ref, Object name) {
    setFile(ref,name);
  }

  /** Create a Position given a position file.
   *  @param ref  The Midas reference
   *  @param name The file from which to initialize <i>(Prior to NeXtMidas 3.3.0 this
   *              only accepted a string. This was changed in NeXtMidas 3.3.0 to match
   *              the usage in {@link BaseFile} which permits {@link FileName}, etc.)</i>
   *  @since NeXtMidas 3.5.4
   */
  public Position (MidasReference ref, Object name) {
    setFile(ref,name);
  }

  /** Create a Position given a position file.
   *  @param file The file to use.
   *  @since NeXtMidas 3.5.4
   */
  @ProvisionalUseOnly("Added in NeXtMidas 3.5.4")
  public Position (BaseFile file) {
    setFile(file);
  }

  /** Creates a new Position with a copy of the data in this Position. Note that this copies only
      the data (x,y,z,alt,lat,lon, etc.), and does not copy any file references. This is typically
      used in cases where one wants to make a "snapshot in time" of a given position (often used
      with multi-threaded applications to allow separate threads to compute values at different times
      without interfering with each other).
      @return A new instance with the same values as this object.
   */
  public Position copy () {
    return copyTo(new Position());
  }

  /** Copies values to given object.
   *  @param p The destination.
   *  @return The destination (allows chaining).
   */
  protected Position copyTo (Position p) { // protected: used by subclasses, all others call copy()
    p.x = x;  p.lon = lon;
    p.y = y;  p.lat = lat;
    p.z = z;  p.alt = alt;
    p.r = r;
    p.t = t;
    p.referenceFrame = referenceFrame;
    p.transform      = transform;
    p.currentTime    = currentTime;
    return p;
  }

  @Override
  public Table toTable () {
    Table tbl = new Table();
    tbl.put("LON",         getLon());
    tbl.put("LAT",         getLat());
    tbl.put("ALT",         getAlt());
    tbl.put("X",           getX());
    tbl.put("Y",           getY());
    tbl.put("Z",           getZ());
    tbl.put("R",           getR());
    tbl.put("TIME",        getTime());        // double - could be from J1950 -or- time relative to start time
    tbl.put("CURRENTTIME", getCurrentTime()); // Time   - always from J1950 with highest precision
    return tbl;
  }

  /** <b>Internal use only:</b> Sets the transform used. This should not be used
      outside of internal library calls since any changes to the transform could
      have "unpredictable" effects on this instance.
      @param xform transform to use
      @since NeXtMidas 3.3.0
   */
  public void setTransform (Transform xform) {
    transform = xform;
  }

  /** <b>Internal use only:</b> Gets the transform used. This should not be used
      outside of internal library calls since any changes to the transform could
      have "unpredictable" effects on this instance.
      @return transform used
      @since NeXtMidas 3.3.0
   */
  public Transform getTransform () {
    return transform;
  }

  /** Set the alt, lat and lon. This is identical to <code>setGeo(all[0],all[1],all[2])</code>.
      @param all Geodetic position { alt, lat, lon }
      @since NeXtMidas 2.7.0
   */
  public void setGeoALL (double[] all) {
    setGeo(all[0],all[1],all[2]);
  }

  /** Set the alt, lat and lon. This is identical to <code>setGeo(lla[2],lla[1],lla[0])</code>.
      @param lla Geodetic position { lon, lat, alt }
      @since NeXtMidas 2.7.0
   */
  public void setGeoLLA (double[] lla) {
    setGeo(lla[2],lla[1],lla[0]);
  }

  /** Set the alt, lat and lon. The frame-of-reference is set to GEODETIC and the CARTESIAN
      coordinate class variables are set.
      @param alt Altitude
      @param lat Latitude
      @param lon Longitude
   */
  public void setGeo (double alt, double lat, double lon) {
    this.alt = alt;
    this.lat = lat;
    this.lon = lon;
    referenceFrame = GEODETIC;
    geo2car(); // Fill in the Cartesian coordinates
  }

  /** Get the altitude
      @return altitude
   */
  public double getAlt () { return alt; }
  /** Get the latitude
      @return latitude
   */
  public double getLat () { return lat; }
  /** Get the longitude
      @return longitude
   */
  public double getLon () { return lon; }

  /** Set the altitude. Unlike {@link #setGeo} this does not change
      the reference frame or update the Cartesian coordinates.
      @param val altitude
      @since NeXtMidas 2.7.0
   */
  public void setAlt (double val) { alt = val; }

  /** Set the latitude. Unlike {@link #setGeo} this does not change
      the reference frame or update the Cartesian coordinates.
      @param val latitude
      @since NeXtMidas 2.7.0
   */
  public void setLat (double val) { lat = val; }

  /** Set the longitude. Unlike {@link #setGeo} this does not change
      the reference frame or update the Cartesian coordinates.
      @param val longitude
      @since NeXtMidas 2.7.0
   */
  public void setLon (double val) { lon = val; }

  /** Get range.
      @return range
      @since NeXtMidas 2.7.0
   */
  public double getR () { return r; }

  /** Set range.
      @param val range
      @since NeXtMidas 2.7.0
   */
  public void setR (double val) { r = val; }

  /** Set CARTESIAN x,y,z. This is identical to <code>setCar(pos[0],pos[1],pos[2])</code>.
      @param pos position offset { x, y, z }
      @since NeXtMidas 2.7.0
   */
  public void setCar (double[] pos) {
    setCar(pos[0],pos[1],pos[2]);
  }

  /** Set CARTESIAN x,y,z.  The frame-of-reference is set to NORMAL and the GEODETIC
      coordinate class variables are set.
      @param x position offset in the x-direction
      @param y position offset in the y-direction
      @param z position offset in the z-direction
      @since NeXtMidas 2.7.0
   */
  public void setCar (double x, double y, double z) {
    this.x = x;
    this.y = y;
    this.z = z;
    referenceFrame = NORMAL;
    car2geo(); // Fill in the Geodetic coordinates
  }

  /** Set the position offset in the x-direction. Unlike {@link #setCar} this does not change
      the reference frame or update the geodetic coordinates.
      @param val position offset in the x-direction
      @since NeXtMidas 2.7.0
   */
  public void setX (double val) { x = val; }
  /** Set the position offset in the y-direction. Unlike {@link #setCar} this does not change
      the reference frame or update the geodetic coordinates.
      @param val position offset in the y-direction
      @since NeXtMidas 2.7.0
   */
  public void setY (double val) { y = val; }
  /** Set the position offset in the z-direction. Unlike {@link #setCar} this does not change
      the reference frame or update the geodetic coordinates.
      @param val position offset in the z-direction
      @since NeXtMidas 2.7.0
   */
  public void setZ (double val) { z = val; }

  /** Get the position offset in the x-direction.
      @return position offset in the x-direction
   */
  public double getX () { return x; }
  /** Get the position offset in the y-direction.
      @return position offset in the y-direction
   */
  public double getY () { return y; }
  /** Get the position offset in the z-direction.
      @return position offset in the z-direction
   */
  public double getZ () { return z; }
  /** Get the velocity (m/s) in the x-direction. Returns 0 if not a {@link StateVector}.
      @return velocity (m/s) in the x-direction
   */
  public double getVX () { return 0; }
  /** Get the velocity (m/s) in the y-direction. Returns 0 if not a {@link StateVector}.
      @return velocity (m/s) in the y-direction
   */
  public double getVY () { return 0; }
  /** Get the velocity in the z-direction. Returns 0 if not a {@link StateVector}.
      @return velocity in the z-direction
   */
  public double getVZ () { return 0; }
  /** Get the acceleration (m/s^2) in the x-direction. Returns 0 if not a {@link StateVector}.
      @return acceleration (m/s^2) in the x-direction
   */
  public double getAX () { return 0; }
  /** Get the acceleration (m/s^2) in the y-direction. Returns 0 if not a {@link StateVector}.
      @return acceleration (m/s^2) in the y-direction
   */
  public double getAY () { return 0; }
  /** Get the acceleration (m/s^2) in the z-direction. Returns 0 if not a {@link StateVector}.
      @return acceleration (m/s^2) in the z-direction
   */
  public double getAZ () { return 0; }
  /**
   Get the velocity (m/s) in the North-direction. Returns 0 if not a {@link StateVector}.
   @return velocity (m/s) in the North-direction
   @since NeXtMidas 2.9.2
   */
  public double getVN () { return 0; }
  /**
    Get the velocity (m/s) in the East-direction. Returns 0 if not a {@link StateVector}.
    @return velocity (m/s) in the East-direction
    @since NeXtMidas 2.9.2
   */
  public double getVE () { return 0; }
  /**
   Get the velocity (m/s) in the Up-direction. Returns 0 if not a {@link StateVector}.
   @return velocity (m/s) in the Up-direction
   @since NeXtMidas 2.9.2
  */
  public double getVU () { return 0; }
  /**
   Get the acceleration (m/s^2) in the North-direction. Returns 0 if not a {@link StateVector}.
   @return acceleration (m/s^2) in the North-direction
   @since NeXtMidas 2.9.2
   */
  public double getAN () { return 0; }
  /**
   Get the acceleration (m/s^2) in the East-direction. Returns 0 if not a {@link StateVector}.
   @return acceleration (m/s^2) in the East-direction
   @since NeXtMidas 2.9.2
   */
  public double getAE () { return 0; }
  /**
   Get the acceleration (m/s^2) in the Up-direction. Returns 0 if not a {@link StateVector}.
   @return acceleration (m/s^2) in the Up-direction
   @since NeXtMidas 2.9.2
   */
  public double getAU () { return 0; }


  /** Get the great circle route between this position and input position
    @param q Position to which to compute the great arc
    @return the great circle route between this position and input position
  */
  public double greatArc (Position q) { return Transform.greatArc(this,q); }

  /** Convert this position from GEO to CARTESIAN coordinates */
  public void geo2car () { Transform.geo2car(this); }
  /** @deprecated Since NeXtMidas 2.3.0 use geo2car */
  @Deprecated public void geo2ecr () { Shell.getSharedMidasContext().deprecate("geo2ecr: Since NeXtMidas 2.3.0 use the geo2car method"); geo2car(); }

  /** Convert this position from CARTESIAN to Geodetic coordinates */
  public void car2geo () { Transform.car2geo(this); }
  /** @deprecated Since NeXtMidas 2.3.0 use car2geo */
  @Deprecated public void ecr2geo () { Shell.getSharedMidasContext().deprecate("ecr2geo: Since NeXtMidas 2.3.0 use the car2geo method"); car2geo(); }

  /** Return a String according to reference frame
    @param refFrame Show x,y,y,t,lat,lon and alt if true
    @return String according to reference frame
  */
  public String toString (int refFrame) {
    if (refFrame==GEODETIC) {
      return "PosLonLatAlt("+lon+","+lat+","+alt+")";
    } else {
      return "PosXYZT("+x+","+y+","+z+","+t+")";
    }
  }
  /** Return a String according to the current reference frame */
  @Override
  public String toString () { return toString(this.referenceFrame); }

  /** Sets the time. This is a legacy function that was intended for use with
      a position/state-vector that represented a single point in time. The current
      implementation of this function is to set the time for a single point in
      time or set the *relative* time for a set of points where the value is
      relative to {@link #getStartTime()}. <br>
      <br>
      For uses that involve J1950 times, users are strongly encouraged to use
      {@link #setCurrentTime(Time)} since use of this function can result in a
      loss of precision. <br>
      <br>
      Since NeXtMidas 3.3.0 this function will check the input value and if given
      a value that is clearly not relative to {@link #getStartTime()} (i.e. a
      J1950 value greater than 1 year), the value will be passed directly to
      {@link #setCurrentTime(Time)} as a J1950 value.
      @param value The time in J1950 seconds for a single-pint position, --or--
                   the time in seconds relative to {@link #getStartTime()}.
  */
  public void setTime (double value) {
    if ((db == null) && (data == null)) {
      currentTime = new Time(value);
      t           = value;
    }
    else if (value <= 366*86400) {
      setCurrentTime(getStartTime().addSec(value));
    }
    else {
      setCurrentTime(new Time(value));
    }
  }

  /** Gets the time. This is a legacy function that was intended for use with
      a position/state-vector that represented a single point in time. The current
      implementation of this function is to get the time for a single point in
      time or set the *relative* time for a set of points where the value is
      relative to {@link #getStartTime()}. <br>
      <br>
      For uses that involve J1950 times, users are strongly encouraged to use
      {@link #getCurrentTime()} since use of this function can result in a
      loss of precision.
      @return The time in J1950 seconds for a single-point position, --or--
              the time in seconds relative to {@link #getStartTime()}.
      @since NeXtMidas 2.5.0
   */
  public double getTime () {
    return t;
  }

  /** <b>Work in progress may change within this beta series:</b>
      Sets the current time for the state vector.
      @param value Time in seconds since midnight, 1 Jan 1950.
      @return true if the time is within the range of {@link #getStartTime()} and
              {@link #getEndTime()}. This can be used to identify cases where the
              file is no longer valid and must be changed.
      @since NeXtMidas 3.3.0
   */
  public boolean setCurrentTime (Time value) {
    boolean ok;
    if ((db == null) && (data == null)) {
      currentTime = value;
      t           = value.getSec();
      ok          = false;
    }
    else {
      currentTime = value;
      t           = value.diff(getStartTime());
      ok          = (value.compareTo(getStartTime()) >= 0)
                 && (value.compareTo(getEndTime()  ) <= 0);
    }
    if (inFile!=null) process();
    return ok;
  }

  /** <b>Work in progress may change within this beta series:</b>
      Gets the current time for the state vector.
      @return Current time.
      @since NeXtMidas 3.3.0
   */
  public Time getCurrentTime () {
    return currentTime;
  }

  /** <b>Work in progress may change within this beta series:</b>
      Gets the start time for the state vector.
      @return In order of precedence:
              1. timecode of input file (via {@link DataFile#getTime()}) -or-
              2. time at index 0 of position data -or-
              3. {@link #getCurrentTime()} if not applicable.
      @since NeXtMidas 3.3.0
   */
  public Time getStartTime () {
         if (db   != null) return ((DataFile)inFile).getTime();
    else if (data != null) return getTimeAt(data, 0);
    else                   return getCurrentTime();
  }

  /** <b>Work in progress may change within this beta series:</b>
      Gets the end time for the state vector. The end time is explicitly defined
      here as the start time of the last entry (i.e. <code>times[N-1]</code>) and
      not the end time of the last entry (which can -- occasionally -- be computed
      as <code>times[N-1] + deltaT</code>). This definition is consistent with the
      way a single-entry {@link Position} or {@link StateVector} will return
      the same value from both {@link #getStartTime()} and {@link #getEndTime()}.
      @return End time or {@link #getCurrentTime()} if not applicable.
      @since NeXtMidas 3.3.0
   */
  public Time getEndTime () {
         if (db   != null) return ((DataFile)inFile).getTimeAt(db.getSize()-1, null);
    else if (data != null) return getTimeAt(data, -1);
    else                   return getCurrentTime();
  }

  /** Gets time from data list (i=-1 for last entry, etc). */
  static Time getTimeAt (List<Table> data, int i) {
    if (i < 0) i = data.size()+i;
    return new Time(Time.toTime(data.get(i).getO("TIME")));
  }

  /** Setup and transform a file. This is identical to:
   *  <code>setFile(ref, filename, 0, null, null)</code>. <br>
   *  <br>
   *  <i>Prior to NeXtMidas 3.3.0 it was possible to override this function, this
   *  was changed in NeXtMidas 3.3.0 to always go through the version that takes
   *  the 'flags' and 'mode' options.</i>
   *  @param ref      The Midas context reference.
   *  @param filename The file name.
   *  @return true if the file has been open, false otherwise.
   *  @deprecated Since NeXtMidas 3.5.4 Use {@link #setFile(MidasReference,Object)}
   */
  @Deprecated
  public final boolean setFile (Object ref, Object filename) {
    return (setFile(ref, filename, 0, null) != null);
  }

  /** Setup and transform a file. This is identical to:
   *  <code>setFile(ref, filename, 0, null, null)</code>.
   *  @param ref      The Midas context reference.
   *  @param filename The file name.
   *  @return true if the file has been open, false otherwise. <i>Note: Although
   *          this nominally returns false when a file can not be opened, in
   *          practice a {@link MidasException} will be thrown if the file can
   *          not be opened, making the a non-true return value only possible
   *          if the user specifically overrides the normal file-open behavior
   *          with a qualifier.</i>
   *  @since NeXtMidas 3.5.4
   */
  public final boolean setFile (MidasReference ref, Object filename) {
    return (setFile(ref, filename, 0, null) != null);
  }

  /** Setup and transform a file. This is identical to:
   *  <code>setFile(file, 0, null, null)</code>.
   *  @param file The file.
   *  @return true if the file has been open, false otherwise. <i>Note: Although
   *          this nominally returns false when a file can not be opened, in
   *          practice a {@link MidasException} will be thrown if the file can
   *          not be opened, making the a non-true return value only possible
   *          if the user specifically overrides the normal file-open behavior
   *          with a qualifier.</i>
   *  @since NeXtMidas 3.5.4
   */
  @ProvisionalUseOnly("Added in NeXtMidas 3.5.4")
  public final boolean setFile (BaseFile file) {
    return (setFile(file, 0, null) != null);
  }

  /** Setup and transform a file. <br>
      <br>
      <i>This method can not be overridden directly. Subclasses should override
      the applicable <code>setFile*(..)</code> methods (where '*' matches the
      applicable StateVector mode) as needed.</i>
      @param ref      The Midas context reference.
      @param filename The file name.
      @param flags    The file open flags (typically 0 or {@link BaseFile#OPTIONAL}).
                      <i>The {@link BaseFile#INPUT} flag is always added to this.</i>
      @param mode     The input mode (null = automatic). <i>This is nearly-always
                      set to null, except the case of forcing a read of keywords
                      from a Type 5000 file.</i>
      @return The mode actually used (null = file not read in). Failing to read a
              non-optional file yields an exception unless the {@link BaseFile#NOABORT}
              flag is set.
      @since NeXtMidas 3.3.0
      @deprecated since NeXtMidas 3.5.0 use {@link #setFile(MidasReference, Object, int, SVMode)}
   */
  @Deprecated
  public final SVMode setFile (Object ref, Object filename, int flags, SVMode mode) {
    if (ref instanceof MidasReference) {
      return setFile((MidasReference) ref, filename, flags, mode);
    } else {
      return setFile(Convert.ref2Midas(ref), filename, flags, mode);
    }
  }

  /** Setup and transform a file. <br>
   *  <br>
   *  <i>This method can not be overridden directly. Subclasses should override
   *  the applicable <code>setFile*(..)</code> methods (where '*' matches the
   *  applicable StateVector mode) as needed.</i>
   *  @param ref      The Midas context reference.
   *  @param filename The file name.
   *  @param flags    The file open flags (typically 0 or {@link BaseFile#OPTIONAL}).
   *                  <i>The {@link BaseFile#INPUT} flag is always added to this.</i>
   *  @param mode     The input mode (null = automatic). <i>This is nearly-always
   *                  set to null, except the case of forcing a read of keywords
   *                  from a Type 5000 file.</i>
   *  @return The mode actually used (null = file not read in). Failing to read a
   *          non-optional file yields an exception unless the {@link BaseFile#NOABORT}
   *          flag is set.
   *  @since NeXtMidas 3.5.0
   */
  public final SVMode setFile (MidasReference ref, Object filename, int flags, SVMode mode) {
    return setFile(BaseFile.getInstanceFor(ref, filename), flags, mode);
  }

  /** Setup and transform a file. <br>
   *  <br>
   *  <i>This method can not be overridden directly. Subclasses should override
   *  the applicable <code>setFile*(..)</code> methods (where '*' matches the
   *  applicable StateVector mode) as needed.</i>
   *  @param file     The file.
   *  @param flags    The file open flags (typically 0 or {@link BaseFile#OPTIONAL}).
   *                  <i>The {@link BaseFile#INPUT} flag is always added to this.</i>
   *                  These flags are ignored if the file is already open.
   *  @param mode     The input mode (null = automatic). <i>This is nearly-always
   *                  set to null, except the case of forcing a read of keywords
   *                  from a Type 5000 file.</i>
   *  @return The mode actually used (null = file not read in). Failing to read a
   *          non-optional file yields an exception unless the {@link BaseFile#NOABORT}
   *          flag is set.
   *  @since NeXtMidas 3.5.4
   */
  @ProvisionalUseOnly("Added in NeXtMidas 3.5.4")
  public final SVMode setFile (BaseFile file, int flags, SVMode mode) {
    inFile = file;
    db     = null; // clear this out, will be null if unable to open file
    data   = null; // clear this out, will be null if unable to open file
    svMode = null; // clear this out, will be null if unable to open file

    boolean doOpen = !inFile.isOpen();
    if (doOpen) {
      if (!inFile.open(BaseFile.INPUT|flags)) return null;
    }

    if (inFile instanceof DataFile) {
      DataFile df = (DataFile)inFile;

      if (mode == null) {
             if (setFileT5Data(df)           ) svMode = SVMode.T5Data;
        else if (setFileT5Entries(df)        ) svMode = SVMode.T5Entries;
        else if (setFileEntries(df)          ) svMode = SVMode.Entries;
        else if (setFileKeywordsEph(df,false)) svMode = SVMode.KeywordsEph;
        else if (setFileKeywordsVec(df)      ) svMode = SVMode.KeywordsVec;
        else if (setFileKeywordsPlat(df)     ) svMode = SVMode.KeywordsPlat;
        else if (setFileKeywordsLLA(df)      ) svMode = SVMode.KeywordsLLA;
        else if (setFileKeywordsEph(df,true) ) svMode = SVMode.KeywordsEph;
      }
      else {
        switch (mode) {
          case T5Data:       if (setFileT5Data(df)          ) svMode = mode;  break;
          case T5Entries:    if (setFileT5Entries(df)       ) svMode = mode;  break;
          case Entries:      if (setFileEntries(df)         ) svMode = mode;  break;
          case KeywordsEph:  if (setFileKeywordsEph(df,true)) svMode = mode;  break;
          case KeywordsVec:  if (setFileKeywordsVec(df)     ) svMode = mode;  break;
          case KeywordsPlat: if (setFileKeywordsPlat(df)    ) svMode = mode;  break;
          case KeywordsLLA:  if (setFileKeywordsLLA(df)     ) svMode = mode;  break;
        }
      }
    }
    else if (inFile instanceof ListFile) {
      ListFile lf = (ListFile)inFile;
      if (mode == null) {
        if (setFileEntries(lf)) svMode = SVMode.Entries;
      }
      else {
        switch (mode) {
          case Entries: if (setFileEntries(lf)) svMode = mode;  break;
          default: break;
        }
      }
    }

    if (doOpen) {
      inFile.close();
    }

    if ((svMode == null) && ((inFile.getFlags() & BaseFile.NOABORT) == 0)) {
      String msg = "Could not find Position/StateVector data in "+inFile.getName();

      if (mode == null) throw new MidasException(msg);
      else              throw new MidasException(msg+" matching "+mode);
    }

    process();
    return svMode;
  }

  /** Read the file using T5Data mode, returns false if not read in.
   *  @param df The input file.
   *  @return true if successful, false otherwise.
   */
  protected boolean setFileT5Data (DataFile df) {
    // Prior to NeXtMidas 3.3.0 Position & StateVector ignored the TIME column
    // when it was given. This resulted in files which relied on the TIME column
    // (rather than xstart+N*xdelta) to be read incorrectly.
    if (df.getTypeCodeClass() != 5) return false; // not Type 5000
    if (df.findRec("TIME")    >= 0) return false; // has TIME column, use T5Entries
    iPos = df.findRec("POS");
    iPosOff = (iPos < 0)? -1 : df.getCompOffset(iPos);

    if ((iPos < 0) || (iPosOff < 0))         return false; // missing POS column
    if (!df.getRecFormat(iPos).equals("VD")) return false; // not VD, use T5Entries

    db = df.getDataBuffer((int)df.getSize());
    df.read(db);
    referenceFrame = GEODETIC;
    transform = new Transform(df);
    return true;
  }

  /** Read the file using T5Entries mode, returns false if not read in.
   *  @param df The input file.
   *  @return true if successful, false otherwise.
   */
  protected boolean setFileT5Entries (DataFile df) {
    if (!setFileEntries((ListFile)df)) return false;
    if (df.getTypeCodeClass() == 5) {
      referenceFrame = GEODETIC;
      transform = new Transform(df);
    }
    return true;
  }

  /** Read the file using Entries mode, returns false if not read in.
   *  @param lf The input file.
   *  @return true if successful, false otherwise.
   */
  protected boolean setFileEntries (ListFile lf) {
    Table recDefs = lf.getRecordDefs();

    if (!recDefs.containsKey("POS")) {         // no POS/VEL/ACC columns
      return false;
    }
    else if (recDefs.containsKey("TIME")) {    // TIME given in column
      data = new ArrayList<>();
      for (int i = 0; i < lf.getNumberOfRows(); i++) {
        data.add(normalizeRow(lf.getDataTable(i), null));
      }
    }
    else if (lf instanceof PlotFile) {         // time based on header
      data = new ArrayList<>();
      PlotFile pf = (PlotFile)lf;
      for (int i = 0; i < lf.getNumberOfRows(); i++) {
        data.add(normalizeRow(lf.getDataTable(i), pf.getTimeAt(i)));
      }
    }
    else {                                     // no time reference
      return false;
    }
    referenceFrame = GEODETIC;
    transform      = Transform.toTransform("ECR");
    return true;
  }

  /** Read the file using KeywordsEph mode, returns false if not read in.
   *  @param df    The input file.
   *  @param force If true use even if USE_SV_KEYWORDS is not set to 1.
   *  @return true if successful, false otherwise.
   */
  protected boolean setFileKeywordsEph (DataFile df, boolean force) {
    // the following is a mirror of what is in nxm.sys.lib.Position as of v3.8
    Keywords kw              = df.getKeywordsObject();
    Double   use_sv_keywords = kw.getD("USE_SV_KEYWORDS", null);
    boolean  noETIM1         = (kw.find("ETIM1") == null);

    if (use_sv_keywords == null) {
      if (!force || noETIM1) return false;
    }
    else if (use_sv_keywords != 1.0) {
      if (!force || noETIM1) return false;
      Shell.warning("Reading KeywordsEph from "+df.getName()+" even though " + "USE_SV_KEYWORDS="+use_sv_keywords+".");
    }
    else if (noETIM1) {
      Shell.warning(""+df.getName()+" has USE_SV_KEYWORDS="+use_sv_keywords+" " + "but ETIM1=null.");
      return false;
    }

    data = new ArrayList<>();
    if (useEfficientSVKeywordCode) populateDataFromKeywords(df, kw, data); // Matt Smith contribution
    else populateDataFromKeywordsLegacy(kw, data);

    transform = new Transform();
    transform.setECR();
    referenceFrame = NORMAL;
    return true;
  }

  private void populateDataFromKeywords(DataFile df, Keywords kw, List<Table> data) {
    // *** Matt Smith performance improvement starts here NXM-3988 ***
    // get all SV keywords sorted
    List<Map.Entry<String, Object>> kws = kw.getAll().stream()
                    .filter(entry -> isSVKeyword(entry.getKey()))
                    .sorted(SV_COMPARATOR)
                    .collect(Collectors.toList());
    // track current row/index
    Table row = null;
    int idx = 0;
    // loop the sorted keywords
    for (Map.Entry<String, Object> key : kws) {
      String name = key.getKey();
      try {
        // get the SV index 1..N
        int i = Integer.parseInt(name.substring(4));
        if (i == idx) {
          // current row
        }
        else if (i == idx + 1) {
          // start a new row
          row = emptyRow();
          data.add(row);
          ++idx;
        }
        else {
          // out of order or missing?
          _warn(df,"Encountered unexpected SV keyword "+name+", may be missing or out of order");
        }
        // add the value to the current row using the appropriate key
        String nkey = SV_KEY_MAP.get(name.substring(0, 4));
        if ("TIME".equals(nkey)) {
          row.put(nkey, Time.toTime(key.getValue()));
        }
        else {
          row.put(nkey, key.getValue());
        }
      }
      catch (NumberFormatException e) {
        _error(df,"Encountered unexpected SV keyword "+name+", maybe missing or out of order "+ e);
      }
    }
  }

  private void populateDataFromKeywordsLegacy (Keywords kw, List<Table> data) {
    for (int i = 1; kw.get("ETIM"+i) != null; i++) {
      Table row = new Table();
      data.add(row);
      row.put("TIME", Time.toTime(kw.get("ETIM"+i)));
      row.put("X",    kw.get("POSX"+i));
      row.put("Y",    kw.get("POSY"+i));
      row.put("Z",    kw.get("POSZ"+i));
      row.put("VX",   kw.get("VELX"+i));
      row.put("VY",   kw.get("VELY"+i));
      row.put("VZ",   kw.get("VELZ"+i));
      row.put("AX",   kw.get("ACCX"+i));
      row.put("AY",   kw.get("ACCY"+i));
      row.put("AZ",   kw.get("ACCZ"+i));
    }
  }

  /** Identify if this is an SV keyword or not
   *  @param name
   *  @return true if it is a State Vector Keyword
   */
  private boolean isSVKeyword (String name) {
    if (name != null && name.length() > 4) {
      return SV_KEY_MAP.keySet().contains(name.substring(0, 4));
    }
    return false;
  }

  /** Create a new row with all the keys we expect to have
   *  @return Table with empty row for each key we expect
   */
  private static final Table emptyRow () {
    Table row = new Table();
    SV_KEY_MAP.values().stream().forEach(k -> row.put(k, null));
    return row;
  }

  /** Comparator to order keywords according to SV index 1..N */
  private static final Comparator<Map.Entry<String, Object>> SV_COMPARATOR = new Comparator<Map.Entry<String, Object>>() {
    @Override
    public int compare(Entry<String, Object> a, Entry<String, Object> b) {
      if (a == b) return 0;
      if (a == null) return -1;
      if (b == null) return +1;
      return compareKeys(a.getKey(), b.getKey());
    }
    private int compareKeys(String a, String b) {
      if (a == b) return 0;
      if (a == null) return -1;
      if (b == null) return +1;
      try {
        Integer anum = Integer.valueOf(a.substring(4));
        Integer bnum = Integer.valueOf(b.substring(4));
        return anum.compareTo(bnum);
      } catch (NumberFormatException e) {
        return a.compareTo(b);
      }
    }
  };

  /** For an error in setFile*(..). */
  private static void _error (DataFile df, String msg) {
    if ((df.getFlags() & DataFile.NOABORT) == 0) {
      throw new MidasException(msg);
    }
    _warn(df, msg);
  }

  /** For a warning in setFile*(..). */
  private static void _warn (DataFile df, String msg) {
    if (df.M != null) df.M.warning(msg);
    else              Shell.warning(msg);
  }

  /** Checks units for setFile*(..). */
  private void _checkUnit (DataFile df, Keywords kw, String name, int expUnit) {
    _checkUnit(df, kw, name, expUnit, expUnit);
  }

  /** Checks units for setFile*(..). */
  private void _checkUnit (DataFile df, Keywords kw, String name, int expUnit, int altUnit) {
    int unit = kw.getUnitsFor(name, expUnit);
    if (unit == expUnit) return;
    if (unit == altUnit) return;

    _error(df, "Found "+name+".UNIT(S)="+unit+" ("+DataFile.getUnitsVarName(unit)+") "
             + "but expected "+expUnit+" ("+DataFile.getUnitsVarName(expUnit)+").");
  }

  /** Adds entries, only used by setFileKeywordsVec(..) and setFileKeywordsPlat(..). */
  private void _addDataEntries (String key, Keywords kw, String name, char type) {
    Object value = kw.get(name);
    if (value != null) {
      Data val = (Data)Convert.o2o(value, type, null);

      if (val.getSize() != data.size()) {
        throw new MidasException("Invalid value for "+name+" keyword, expected "
                                +data.size()+" entries but found "+val.getSize());
      }

      for (int i = 0; i < val.getSize(); i++) {
        data.get(i).put(key, val.getNumber(i));
      }
    }
  }

  /** Read the file using KeywordsVec mode, returns false if not read in.
   *  @param df The input file.
   *  @return true if successful, false otherwise.
   */
  protected boolean setFileKeywordsVec (DataFile df) {
    Keywords kw = df.getKeywordsObject();

    if (kw.find("ETIM_VEC") == null) return false;
    Data time = (Data)Convert.o2o(kw.get("ETIM_VEC"), 'D', null);

    data = new ArrayList<>();
    for (int i = 0; i < time.getSize(); i++) {
      Table row = new Table();
      row.put("TIME", Time.toTime(time.getNumber(i)));
      data.add(row);
    }
    _addDataEntries("X",  kw, "POSX_VEC", 'D');
    _addDataEntries("Y",  kw, "POSY_VEC", 'D');
    _addDataEntries("Z",  kw, "POSZ_VEC", 'D');
    _addDataEntries("VX", kw, "VELX_VEC", 'D');
    _addDataEntries("VY", kw, "VELY_VEC", 'D');
    _addDataEntries("VZ", kw, "VELZ_VEC", 'D');
    _addDataEntries("AX", kw, "ACCX_VEC", 'D');
    _addDataEntries("AY", kw, "ACCY_VEC", 'D');
    _addDataEntries("AZ", kw, "ACCZ_VEC", 'D');
    transform = new Transform();
    transform.setECR();
    referenceFrame = NORMAL;
    return true;
  }

  /** Read the file using KeywordsPlat mode, returns false if not read in.
   *  @param df The input file.
   *  @return true if successful, false otherwise.
   */
  protected boolean setFileKeywordsPlat (DataFile df) {
    Keywords kw     = df.getKeywordsObject();
    String   prefix = df.getQualifiers().getS("SVPREFIX", "EPH");

    if (kw.find(prefix+".TIME") == null) return false;

    int    unit      = kw.getUnitsFor(prefix+".TIME", Units.COUNTS);
    Time   timeEpoch = Time.toTime(kw.get("TIME_EPOCH"), Time.ISO8601);
    double timeDelta = Convert.o2d(kw.get("TIME_DELTA"), 0.0);

    if (timeEpoch == null) return false; // not a valid platinum file
    if (timeDelta == 0   ) return false; // not a valid platinum file

    data = new ArrayList<>();

    _checkUnit(df, kw, prefix+".TIME",      Units.COUNTS,     Units.TIME_S);
    _checkUnit(df, kw, prefix+"_POS.X",     Units.DISTANCE_M);
    _checkUnit(df, kw, prefix+"_POS.Y",     Units.DISTANCE_M);
    _checkUnit(df, kw, prefix+"_POS.Z",     Units.DISTANCE_M);
    _checkUnit(df, kw, prefix+"_VEL.X",     Units.SPEED_M_S);
    _checkUnit(df, kw, prefix+"_VEL.Y",     Units.SPEED_M_S);
    _checkUnit(df, kw, prefix+"_VEL.Z",     Units.SPEED_M_S);
    _checkUnit(df, kw, prefix+"_ACC.X",     Units.ACCELERATION_M_S2);
    _checkUnit(df, kw, prefix+"_ACC.Y",     Units.ACCELERATION_M_S2);
    _checkUnit(df, kw, prefix+"_ACC.Z",     Units.ACCELERATION_M_S2);

    // if ((unit != Units.COUNTS) && (unit == Units.TIME_S)) {
    //   This clearly violates Vol. 1 and Vol. 2 of the Platinum ICD. Note
    //   that if the NOABORT flag is set we will just presume that COUNTS is
    //   the proper units. A few buggy systems are known to set the units to
    //   garbage values (i.e. the unit code -- which is not strictly required
    //   under Vol. 2 -- is just bogus, but the associated time-values are
    //   in COUNTS per Vol. 2). By only issuing a warning in _checkUnits(..) if
    //   the NOABORT flag is set we are able to support this buggy case by just
    //   assuming that anything that is not COUNTS or TIME_S is a bogus unit
    //   setting and that the data is actually using COUNTS.
    // }
    if (unit == Units.TIME_S) {
      // Not strictly legal under Vol. 2 of the Platinum ICD, but Vol. 1 does
      // not prohibit it. Since we know what to do, just assume the Vol. 1 use.
      Data time = (Data)Convert.o2o(kw.get(prefix+".TIME" ), 'D', null);
      for (int i = 0; i < time.getSize(); i++) {
        Time  ts  = Time.toTime(time.getNumber(i)).addTime(timeEpoch);
        Table row = new Table();
        row.put("TIME", ts);
        data.add(row);
      }
    }
    else {
      Data time = (Data)Convert.o2o(kw.get(prefix+".TIME" ), (char)DataTypes.XLONG, null);
      for (int i = 0; i < time.getSize(); i++) {
        Time  ts  = new Time(time.getNumber(i).longValue(), timeEpoch, timeDelta);
        Table row = new Table();
        row.put("TIME", ts);
        data.add(row);
      }
    }
    _addDataEntries("X",  kw, prefix+"_POS.X", (char)DataTypes.DOUBLE);
    _addDataEntries("Y",  kw, prefix+"_POS.Y", (char)DataTypes.DOUBLE);
    _addDataEntries("Z",  kw, prefix+"_POS.Z", (char)DataTypes.DOUBLE);
    _addDataEntries("VX", kw, prefix+"_VEL.X", (char)DataTypes.DOUBLE);
    _addDataEntries("VY", kw, prefix+"_VEL.Y", (char)DataTypes.DOUBLE);
    _addDataEntries("VZ", kw, prefix+"_VEL.Z", (char)DataTypes.DOUBLE);
    _addDataEntries("AX", kw, prefix+"_ACC.X", (char)DataTypes.DOUBLE);
    _addDataEntries("AY", kw, prefix+"_ACC.Y", (char)DataTypes.DOUBLE);
    _addDataEntries("AZ", kw, prefix+"_ACC.Z", (char)DataTypes.DOUBLE);

    transform = new Transform();
    transform.setECR();
    referenceFrame = NORMAL;
    return true;
  }

  /** Read the file using KeywordsLLA mode, returns false if not read in.
   *  @param df The input file.
   *  @return true if successful, false otherwise.
   */
  protected boolean setFileKeywordsLLA (DataFile df) {
    Keywords kw     = df.getKeywordsObject();
    String   prefix = df.getQualifiers().getS("SVPREFIX", null);

    if (prefix == null) return false;
    if (kw.find(prefix+".LON") == null) return false;
    t = df.getTime().getSec();
    setGeo(Convert.o2d(kw.get(prefix+".ALT")),
           Convert.o2d(kw.get(prefix+".LAT")),
           Convert.o2d(kw.get(prefix+".LON")));

    // The use of LATITUDE_DEG, LONGITUDE_DEG, and ALTITUDE_M are not permitted
    // under Vol. 1 or Vol. 2 of the Platinum ICD, but since they are more common
    // than ANGLE_DEG or ALTITUDE_M under BLUE 1.1 (and have the same meaning),
    // we permit them as an alternative (with a warning).
    _checkUnit(df, kw, prefix+".ALT", Units.DISTANCE_M, Units.ALTITUDE_M);
    _checkUnit(df, kw, prefix+".LAT", Units.ANGLE_DEG,  Units.LATITUDE_DEG);
    _checkUnit(df, kw, prefix+".LON", Units.ANGLE_DEG,  Units.LONGITUDE_DEG);
    return true;
  }

  /** Internal use only: Normalizes a row of data. */
  private Table normalizeRow (Tablizable t, Double time) {
    Table tbl = t.toTable();
    Table row = new Table();
    Time  tc  = Time.toTime(tbl.get("TIME", time));
    row.put("TIME", tc);
    normalizeEntry(row, tbl, "POS", "X",  "Y",  "Z" );
    normalizeEntry(row, tbl, "VEL", "VX", "VY", "VZ");
    normalizeEntry(row, tbl, "ACC", "AX", "AY", "AZ");
    if (tbl.containsKey("LON")) {
      if (!tbl.containsKey("POS")) {
        double[] p = { tbl.getD("LON"), tbl.getD("LAT"), tbl.getD("ALT") };
        transform.fromLonLatAlt(p, 1);
        row.put("X", p[0]);
        row.put("Y", p[1]);
        row.put("Z", p[2]);
      }
      row.put("LON", tbl.getD("LON"));
      row.put("LAT", tbl.getD("LAT"));
      row.put("ALT", tbl.getD("ALT"));
    }
    return row;
  }

  /** Internal use only: Normalizes an entry in a row for normalizeRow(..). */
  private void normalizeEntry (Table row, Table tbl, String name, String x, String y, String z) {
    Table table = tbl.getTable(name, true);

    if (table != null) {
      row.put(x, table.getD("X"));
      row.put(y, table.getD("Y"));
      row.put(z, table.getD("Z"));
    }
    else {
      double[] da = Convert.o2da(tbl.getO(name));
      if (da != null) {
        row.put(x, da[0]);
        row.put(y, da[1]);
        row.put(z, da[2]);
      }
    }
  }

  /** Process the file set in setFile */
  public void process () {
    double[] d    = new double[3];
    int      mode = NORMAL;

    // NOTE: For historic reasons the version of process() in Position does not
    //       interpolate between points, it simply takes the value of the prior
    //       point. The version in StateVector does interpolate between points.
    if (db != null) {
      DataFile df = (DataFile)inFile;
      double offset = (t-df.getXStart())/df.getXDelta();
      int off = Math.max(0,Math.min(db.size-1,(int)offset));
      int kp = off*db.bpe + iPosOff;
      x=db.unpackD(kp+0); y=db.unpackD(kp+8); z=db.unpackD(kp+16);
      d[0]=x; d[1]=y; d[2]=z;
    }
    else if (data != null) {
      int off = 0;
      while ((off+1 < data.size()) && (data.get(off+1).getD("TIME") <= t)) {
        off++;
      }
      d[0] = data.get(off).getD("X");
      d[1] = data.get(off).getD("Y");
      d[2] = data.get(off).getD("Z");
    }
    if (mode != GEODETIC && mode != GRAPH3D) {
      transform.toLonLatAlt(d,1);
    }
    lon=d[0]; lat=d[1]; alt=d[2];
  }

  /** Make a clone of this position
      @return clone of this position
   */
  public Position cloneOf () {
    Position pos=null;
    try { pos = (Position)this.clone(); }
    catch (CloneNotSupportedException e) {}
    return pos;
  }

  private static String[] keys = {"X","Y","Z","R","T","LAT","LON","ALT","FRAME"};
  @Override
  public String[] getKeys () { return keys; }

  @Override
  public Object setKey (String name, Object value) {
         if (name.equals("X"))     setX(Convert.o2d(value));
    else if (name.equals("Y"))     setY(Convert.o2d(value));
    else if (name.equals("Z"))     setZ(Convert.o2d(value));
    else if (name.equals("R"))     setR(Convert.o2d(value));
    else if (name.equals("T"))     setTime(Convert.o2d(value));
    else if (name.equals("LAT"))   setLat(Convert.o2d(value));
    else if (name.equals("LON"))   setLon(Convert.o2d(value));
    else if (name.equals("ALT"))   setAlt(Convert.o2d(value));
    else if (name.equals("FRAME")) referenceFrame = Convert.o2l(value);
    else return null;
    return value;
  }

  @Override
  public Object getKey (String name) {
         if (name.equals("X"))     return Convert.d2o(getX());
    else if (name.equals("Y"))     return Convert.d2o(getY());
    else if (name.equals("Z"))     return Convert.d2o(getZ());
    else if (name.equals("R"))     return Convert.d2o(getR());
    else if (name.equals("T"))     return Convert.d2o(getTime());
    else if (name.equals("LAT"))   return Convert.d2o(getLat());
    else if (name.equals("LON"))   return Convert.d2o(getLon());
    else if (name.equals("ALT"))   return Convert.d2o(getAlt());
    else if (name.equals("FRAME")) return Convert.l2o(referenceFrame);
    return null;
  }

  /** Creates a new position based on the given geodetic coordinates.
      @param tbl A table with <code>{ALT=alt,LAT=lat,LON=lon}</code>.
      @return new position based on the given geodetic coordinates
      @since NeXtMidas 2.5.0
  */
  public static Position fromGeo (Table tbl) {
    double alt = tbl.getD("ALT",0.0);
    double lat = tbl.getD("LAT",0.0);
    double lon = tbl.getD("LON",0.0);

    return Position.fromGeo(alt,lat,lon);
  }

  /** Creates a new position based on the given geodetic coordinates.
      @param alt Altitude
      @param lat Latitude
      @param lon Longitude
      @return new position based on the given geodetic coordinates
      @since NeXtMidas 2.1.0
  */
  public static Position fromGeo (double alt, double lat, double lon) {
    Position pos = new Position();
    pos.setGeo(alt,lat,lon);
    return pos;
  }

  /** Returns the position in <code>(lon,lat,alt)</code> format.
    @return The position in <code>(lon,lat,alt)</code> format.
    @since NeXtMidas 2.1.0
  */
  public double[] toLonLatAlt () {
    return new double[] { getLon(), getLat(), getAlt() };
  }

  /** Returns the position in <code>(alt,lat,lon)</code> format.
    @return The position in <code>(alt,lat,lon)</code> format.
    @since NeXtMidas 2.1.0
    @see #toLonLatAlt()
  */
  public double[] toAltLatLon () {
    return new double[] { getAlt(), getLat(), getLon() };
  }

  /** Creates a new position based on the given geodetic coordinates in
      <code>(lon,lat,alt)</code> format.
      @param pos The position in <code>(lon,lat,alt)</code> format.
      @return new position based on the given geodetic coordinates
      @since NeXtMidas 2.1.0
  */
  public static Position fromLonLatAlt (double[] pos) {
    return fromGeo(pos[2], pos[1], pos[0]);
  }

  /** Creates a new position based on the given geodetic coordinates in
      <code>(alt,lat,lon)</code> format.
    @param pos The position in <code>(alt,lat,lon)</code> format.
    @return new position based on the given geodetic coordinates
    @since NeXtMidas 2.1.0
  */
  public static Position fromAltLatLon (double[] pos) {
    return fromGeo(pos[0], pos[1], pos[2]);
  }

  /** Computes the degree heading for the {@link StateVector}. For a basic {@link Position} or a stationary
      {@link StateVector} this will return <code>0.0</code>.
      @return The heading in degrees (0=North,90=East,etc).
   */
  public double getHeading () {
    // For stationary positions, always return 0.0. Doing the full computations with stationary
    // values could result in random results due to small rounding errors.
    if ((getVX() == 0) && (getVY() == 0) && (getVZ() == 0)) return 0.0;

    // Convert to geodetic first
    Position p = new Position();
    p.setCar(getX()+getVX(), getY()+getVY(), getZ()+getVZ());
    Transform.car2geo(p);
    return computeHeading(getLon(), getLat(), p.getLon(), p.getLat(), 0.0);
  }

  /**
    Computes a precise heading for this {@link Position} instance knowing only a single
    geodetic point; to extrapolate the second point, this method uses
    the {@link StateVector} Velocity-East and Velocity-North values at t=1 (seconds)
    to infer Point 2.
    This calculation is considered the "great arc circle bearing";
    it represents the initial bearing of an object between two points (P1 and P2)
    considering unevenly spaced latitude/longitude degree lines and the curvature of the
    Earth's surface.

    Note #1 that this method should be called after <code>StateVector.setHeadingSpeedGeodetic</code>.
    Note #2 that this method calls {@link #computeHeadingPrecise}.
    For basic or stationary {@link StateVector}, this method will return <code>0.0</code>.
    Reference bug 2219 for more information.
    @return The heading
    @since NeXtMidas 2.9.2
   */
  public double getHeadingGeodetic () {
    if (referenceFrame == GEODETIC) {
      if ((getVU() == 0) && (getVE() == 0) && (getVN() == 0)) return 0.0;
    }
    else {
      if ((getVX() == 0) && (getVY() == 0) && (getVZ() == 0)) return 0.0;
      Transform.car2geo(this);
    }

    return computeHeadingPrecise(
            getLon(),
            getLat(),
            getLon() + getVE(), // add converted Velocity East
            getLat() + getVN(), // add converted Velocity North
            0.0);
  }

  /**
    Computes a precise heading for the {@link StateVector} knowing both {@link Position} P
    and {@link Position} Q geodetic points.
    This calculation is considered the "great arc circle bearing";
    it represents the initial bearing of an object between two points (P and Q)
    considering unevenly spaced latitude/longitude degree lines and the curvature of the
    Earth's surface.

    Note that this method calls {@link #computeHeadingPrecise}.
    For basic or stationary {@link Position}, this method will return <code>0.0</code>.
    Reference bug 2219 for more information.
    @param p The first {@link Position} object
    @param q The second {@link Position} object
    @return The initial bearing at P in order to travel to Q
    @since NeXtMidas 2.9.2
   */
  public static double getGreatArcInitialBearing (Position p, Position q) {
    if (p.getLat() == q.getLat() && p.getLon() == q.getLon()) {
      return 0.0;
    }

    return computeHeadingPrecise(
            p.getLon(),
            p.getLat(),
            q.getLon(),
            q.getLat(),
            0.0);
  }

  /** Gets the speed for the {@link StateVector}. For a basic {@link Position} or a stationary
      {@link StateVector} this will return <code>0.0</code>. At this time, this computation assumes
      evenly spaced degrees of latitude and longitude (see {@link #METERS2DEG}).
      @return The speed in m/s.
   */
  public double getSpeed () {
    // For stationary positions, always return 0.0. Doing the full computations with stationary
    // values could result in random results due to small rounding errors.
    if ((getVX() == 0) && (getVY() == 0) && (getVZ() == 0)) return 0.0;

    Position p = new Position();
    p.setCar(getX()+getVX(), getY()+getVY(), getZ()+getVZ());
    Transform.car2geo(p);

    double dLon = p.getLon() - getLon(); // deg/s along line of longitude
    double dLat = p.getLat() - getLat(); // deg/s along line of latitude
    double mLon = dLon * DEG2METERS;     // m/s along line of longitude
    double mLat = dLat * DEG2METERS;     // m/s along line of latitude
    double mAlt = p.getAlt() - getAlt(); // m/s veritcal

    return Math.sqrt(mLon*mLon + mLat*mLat + mAlt*mAlt);
  }

  /**
   Returns a more precise ground speed for the {@link StateVector}.
   @return The round speed in meters per second (m/s)
   @since NeXtMidas 2.9.2
   */
  public double getGroundSpeed () {
    // Return getSpeed with mode = 0
    return getSpeed(0);
  }

  /**
    Returns the precise horizontal speed for the {@link StateVector}.
    @return The horizontal speed in meters per second (m/s)
    @since NeXtMidas 2.9.2
   */
  public double getHorizontalSpeed () {
    // Return getSpeed with mode = 1
    return getSpeed(1);
  }

  /**
    Overloaded method to return a precise geodetic or horizontal frame of reference
    velocity in meters per second (m/s) for this {@link Position}.
    Set which frame of reference to use via the <code>mode</code> parameter.
    @param mode - 0: Ground Speed, 1: Horizontal Speed
    @return  The speed in meters per second (m/s)
    @since NeXtMidas 2.9.2
   */
  private double getSpeed (int mode) {
    // For stationary positions, always return 0.0. Doing the full computations with stationary
    // values could result in random results due to small rounding errors.

    if (referenceFrame == GEODETIC) {
      if ((getVU() == 0) && (getVE() == 0) && (getVN() == 0)) return 0.0;
    }
    else {
      if ((getVX() == 0) && (getVY() == 0) && (getVZ() == 0)) return 0.0;
        Transform.car2geo(this);
    }
    double ratio = (mode == 0) ? (Constants.EARTH_EQUATORIAL_RADIUS) / (Constants.EARTH_EQUATORIAL_RADIUS + getAlt()) : 1;
    return ratio * Math.sqrt(Math.pow(getVE(),2) + Math.pow(getVN(),2));
  }

  /** Computes a heading from two geodetic positions.
      @param pos1 First position.
      @param pos2 Second position.
      @param def  Default value to use in cases where the first and second position are identical.
      @return The heading in degrees (0=North,90=East,etc).
   */
  public static double computeHeading (Position pos1, Position pos2, double def) {
    return computeHeading(pos1.getLon(), pos1.getLat(), pos2.getLon(), pos2.getLat(), def);
  }

  /** Computes a heading from two geodetic positions.
      @param lon1 First position longitude.
      @param lat1 First position latitude.
      @param lon2 Second position longitude.
      @param lat2 Second position latitude.
      @param def  Default value to use in cases where the first and second position are identical.
      @return The heading in degrees (0=North,90=East,etc).
   */
  public static double computeHeading (double lon1, double lat1, double lon2, double lat2, double def) {
    if ((lat1 == lat2) && (lon1 == lon2)) return def; // Prevents NaN in computation
    //  NORTH
    //    |
    //    |     + p2
    //    |    /
    //    | t /                  (lon2-lon1)   dlon
    //    |  /          tan(t) = ----------- = ----
    //    | /                    (lat2-lat1)   dlat
    //    |/
    //    +--------------------------- EAST
    //   p1
    double dlon  = lon2 - lon1;
    double dlat  = lat2 - lat1;
    double rad   = Math.atan(dlon/dlat);
    double deg   = Math.toDegrees(rad);

    // Corrections for those not in the NE Quad
         if ((dlat < 0) && (dlon < 0)) deg = 180+deg;  // SW Quad
    else if (dlat < 0)                 deg = 180+deg;  // SE Quad
    else if (dlon < 0)                 deg = 360+deg;  // NW Quad

    return deg;
  }

  /** Computes a precise Great Arc Circle heading between two geodetic positions.
      See {#computeHeadingPrecise}.
      @param pos1 First position.
      @param pos2 Second position.
      @param def  Default value to use in cases where the first and second position are identical.
      @return The heading in degrees (0=North,90=East,etc).
      @since NeXtMidas 2.9.2
   */
  public static double computeHeadingPrecise (Position pos1, Position pos2, double def) {
    return computeHeadingPrecise(pos1.getLon(), pos1.getLat(), pos2.getLon(), pos2.getLat(), def);
}

  /**
    Computes a heading from two geodetic positions in degrees using precise computations
    that takes into account the unequal values of latitude/longitude lines near the poles.
    This method will convert the degree values to radians before computation.
    The Haversine Formula and Law of Spherical Cosines (below) is used to perform this calculation.
    <p>
    See the Aviation Formulary website for details on the formulas used to perform this calculation.
    <br>
    <a href="http://williams.best.vwh.net/avform.htm#Crs">Aviation Formulary</a>
    </p>
    <p>Reference bug 2219 for more information.</p>

    @param lon1 First position longitude.
    @param lat1 First position latitude.
    @param lon2 Second position longitude.
    @param lat2 Second position latitude.
    @param def  Default value to use in cases where the first and second position are identical.
    @return The heading in degrees (0=North,90=East,etc).
    @since NeXtMidas 2.9.2
   */
  public static double computeHeadingPrecise (double lon1, double lat1, double lon2, double lat2, double def) {
    if ((lat1 == lat2) && (lon1 == lon2)) return def; // Prevents NaN in computation

//                    / sin(lat2) - sin(lat1) * cos(d) \
//     Bearing = acos | ------------------------------- |
//                    \ sin(d) * cos(lat1)             /, where <code>d</code>
//      is <code>distance</code> between p1 and p2

    // latitude and longitude delta (not used?)
//  double dlon  = lon2 - lon1;
//  double dlat  = lat2 - lat1;
//  if (Math.abs(dlon) > 180) dlon -= Math.signum(dlon)*360;
//  double rad   = Math.atan(dlon/dlat);

    // Convert latitude and longitude values to radians in order to perform
    // "Great Arc Circle" calculation, using the Haversine Formula
    double lat1rad = lat1 * (Math.PI / 180);
    double lat2rad = lat2 * (Math.PI / 180);
    double lon1rad = lon1 * (Math.PI / 180);
    double lon2rad = lon2 * (Math.PI / 180);

    double y = Math.sin(lon2rad - lon1rad) * Math.cos(lat2rad);
    double x = Math.cos(lat1rad) * Math.sin(lat2rad)
             - Math.sin(lat1rad) * Math.cos(lat2rad) * Math.cos(lon2rad - lon1rad);

    double deg = Math.atan2(y, x);

    // Convert to degrees
    deg = Math.toDegrees(deg);

    // Take the modulus of the degree value to account for < 0 > 360 values
    deg = (deg + 360) % 360;

    // Return initial bearing in degrees
    return deg;
  }

  /** Tests if two positions are the same, within a tolerance
   *
   *  @param p0   first position
   *  @param p1   second position
   *  @param tolr tolerance
   *  @return     true if the two positions are the same
   *  @since NeXtMidas 3.9.4
   */
  public static boolean equals (Position p0, Position p1, double tolr) {
    return Tolerance.equalsWithTolerance(p0.alt, p1.alt, tolr) &&
           Tolerance.equalsWithTolerance(p0.lat, p1.lat, tolr) &&
           Tolerance.equalsWithTolerance(p0.lon, p1.lon, tolr);
  }

  /** Use contributed code that load StateVector keywords more efficiently
   *  @param useEfficientSVKeywordCode true to use more efficient code
   */
  @InternalUseOnly
  public static void setUseEfficientSVKeywordCode (boolean useEfficientSVKeywordCode) {
    Position.useEfficientSVKeywordCode = useEfficientSVKeywordCode;
  }

  /** Get whether to use contributed code that load StateVector keywords more efficiently
   *  @return true to use more efficient code
   */
  @InternalUseOnly
  public static boolean getUseEfficientSVKeywordCode () {
    return useEfficientSVKeywordCode;
  }
}
