package nxm.sys.libg;

import java.awt.Color;
import java.awt.FontMetrics;
import java.util.Vector;

import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import nxm.sys.inc.Constants;
import nxm.sys.inc.PlotFile;
import nxm.sys.lib.Args;
import nxm.sys.lib.Convert;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Position;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.Transform;
import nxm.sys.lib.Vehicle;
import nxm.sys.lib.Vehicle.Location;
import nxm.sys.lib.Vehicle.PointingPos;

/** Layer used to plot a vehicle.

    <h2>Macro Usage</h2>
    <pre>
      <u>Add a new vehicle named MYVEH (long version):</u>
        ! Setup the vehicle see {@link Vehicle#toTable()} for a list of keys
        set t:vehicleTable {}
        set vehicleTable.vehpos {ALT=0,LAT=0,LON=0}
         :
        set vehicleTable.points {}
        set vehicleTable.points.a {}
        set vehicleTable.points.a.pos {ALT=0,LAT=15,LON=15}
         :
        set vehicleTable.points.b {}
        set vehicleTable.points.b.pos {ALT=0,LAT=30,LON=30}
         :
        ! Add the vehicle to the layer
        set reg.plot.layers.vehicle.setVehicle(MYVEH) vehicleTable

      <u>Add a new vehicle named MYVEH (short version):</u>
        ! Creates a new vehicle with default values
        set reg.plot.layers.vehicle.setVehicle(MYVEH) {}

      <u>Get a vehicle's info as a table:</u>
        res myvehicle reg.plot.layers.vehicle.getVehicleTable(XXX)
        res/all myvehicle

      <u>Update attributes for a vehicle:</u>
        set reg.plot.layers.vehicle.setVehicle(MYVEH) {SITEPOS={LAT=-15,LON=-15,ALT=0}}

        ! These next two lines are identical
        set reg.plot.layers.vehicle.setVehicle(MYVEH) {POINTS={A={POS={LAT=20,LON=20,ALT=0}}}}
        set reg.plot.layers.vehicle.setVehiclePoint(MYVEH,A) {POS={LAT=20,LON=20,ALT=0}}
    </pre>

    <h2>Important Notes</h2>
    <pre>
      (1) If CANGLE for a point is set to 0 (this is the default)
          no cone will be shown.
    </pre>

    @author  J. Czechowski IV
    @since   NeXtMidas 2.5.0
    @see     Vehicle
    @see     LayerGeo
 */
public class LayerVehicle extends Layer implements ChangeListener {

  /** The list of flag options ({@value}). */
  public static final String flagsList = "Markers,Horizon,FootPrint,ReadOut";

  /** Show target, site and state vector markers if enabled Default is on. (see {@link #flagsList}) */
  public static final int MARKERS   = 0x0001;
  /** Show horizon lines if enabled. Default is on. (see {@link #flagsList})  */
  public static final int HORIZON   = 0x0002;
  /** Show the footprint at the target if enabled. (see {@link #flagsList}) */
  public static final int FOOTPRINT = 0x0004;
  /** Display the readout if enabled. (see {@link #flagsList}) */
  public static final int READOUT   = 0x0008;
  /** Last flags defined (used by subclasses). */
  public static final int LAST_FLAG = READOUT;
  /** Default flags. */
  public static final int DEF_FLAGS = MARKERS|HORIZON|FOOTPRINT;

  /** FYI: zero-length arrays are immutable. For more info see Effective Java,
      Item 27: Return zero-length arrays, not null. */
  public static final Location[] EMPTY_LOCATION_ARRAY = new Location[0];

  protected final Table     vehicles         = new Table(Table.KV, Table.READONLY); // The set of all vehicles plotted.
  protected       int       coneMode         = 1;           // Mode for cone drawing (see MP.drawCone(..))
  protected       boolean   coneFill         = false;       // Should cones be filled?
  protected       Location  selected         = null;        // The selected location.
  protected       Color     selectColor      = Color.red;   // Color of selected location.
  protected       Color     deselectColor    = Color.blue;  // Color of non-selected location.
  protected       int       conic            = 1;           // The number of conics displayed when conics is enabled
  protected       int       cdiv             = 60;          // The conic division
  protected       double    cangle           = Double.NaN;  // The conic angle
  protected       double    crange           = 0;           // The conic range
  protected       int       flags            = DEF_FLAGS;   // Drawing flags.
  protected       int       fillAlpha        = 0x70;        // Default opacity

  /** The list of cone modes. Available options:
      <pre>
        Vertical - Clip cone at a plane perpendicular line from pos to posp at posp.
        Horizon  - Cone extends to horizon.
        Both     - Draw both modes (Horizon is drawn normal, Vertical is drawn as dotted line).
      </pre>
   */
  public static final String coneModeList = "Both,Vertical,Horizon";

  /** Creates a new instance. */
  public LayerVehicle () {
    this("VEHICLE");
  }

  /** Constructors
   *  @param name layer name */
  protected LayerVehicle (String name) {
    super.name    = name;
    super.xstart  = -180;
    super.xdelta  =   15;
    super.frame   =   25;
    super.ystart  =  -90;
    super.ydelta  =   15;
    super.size    =   13;
    line.setColor(Color.PINK);  // Use a default that is different from most others.
    enable &= ~MPlot.EN_BSTORE;
  }

  @Override
  public boolean clear() {
    // Implementation added in 3.9.0
    // resets this layer. Permanent deletion of the vehicles
    if(vehicles.size()==0)return false;
    vehicles.clear(Table.FORCE);
    refresh();
    return true;
  }

  /** Gets the flags.
   *  @return the flags */
  public String getFlags () {
    return Parser.mask2s(flagsList, getFlagsMask());
  }

  /** Sets the flags.
   *  @param value the flags */
  public void setFlags (String value) {
    setFlagsMask(Parser.mask(flagsList, value, flags));
  }

  /** Gets the flags as an int mask (see {@link #getFlags}.
   *  @return the flags as an int value */
  public int getFlagsMask () {
    return flags;
  }

  /** Sets the flags as an int mask (see {@link #setFlags}.
   *  @param value the flags as an int value
   */
  public void setFlagsMask (int value) {
    if (value != flags) {
      flags = value;
      refresh();
    }
  }

  /** This does nothing. */
  public boolean setFile (PlotFile pf, Table tab, Args MA) {
    return true;
  }

  public void stateChanged(ChangeEvent e) {
    Object src = e.getSource();
    if (vehicles.containsValue(src)) {
      refresh();
    } else {
      System.err.println("LayerVehicle: Got change event from unknown source, event="+e);
    }
  }

  /** Gets the vehicle used by this layer, if one does not already exist, create a new one.
   *  @param name vehicle name
   *  @return the vehicle used by this layer
   */
  public Vehicle useVehicle(String name) {
    Vehicle veh = getVehicle(name);
    if (veh == null) {
      veh = new Vehicle(ref);
      addVehicle(name,veh);
    }
    return veh;
  }

  /** Get the reference time.
      @return The time.
   */
  public double getTime () {
    return time;
  }

  /** Set the reference time.
      @param value The time.
   */
  public void setTime (double value) {
    String names[] = getVehicleNames();
    time = value;
    for (int i = 0; i < names.length; i++) {
      getVehicle(names[i]).setTime(time);
    }
  }

  /** Gets a read-only table containing each of the vehicles.
   *  @return a read-only table containing each of the vehicles */
  public Table getVehicles() {
    return vehicles;
  }

  /** Gets the names used by all of the vehicles for this layer.
   *  @return the names used by all of the vehicles for this layer */
  public String[] getVehicleNames() {
    return vehicles.getKeys();
  }

  /** Sets a vehicle in this layer. If a vehicle with the given name already exists, remove it first
      otherwise just add the new vehicle. Using <code>setVehicle(name,null)</code> will remove an
      existing vehicle. This method is designed for use from a primitive, see
      {@link #setVehicle(String,Table)} for a version intended for use from a macro.
      @param name    The name of the vehicle.
      @param vehicle The vehicle.
   */
  public void setVehicle(String name, Vehicle vehicle) {
    Vehicle veh = getVehicle(name);
    if (veh != vehicle) {
      if (veh != null) {
        vehicles.remove(name, Table.FORCE);
        veh.removeChangeListener(this);
      }
      if (vehicle != null) {
        vehicle.setTime(getTime());
        vehicle.addChangeListener(this);
        vehicles.put(name, vehicle, Table.FORCE);
      }
      vehiclesUpdated();
      refresh();
    }
  }

  /** Sets a the parameters for a vehicle in this layer. If tbl is null, do nothing. Otherwise
      set the given values in the vehicle adding a new vehicle if necessary.
      <pre>
        NOTE: The behavior of this method differs from {@link #setVehicle(String,Vehicle)}.
              This method is effectively the same as getVehicle(name).setValues(tbl), and is
              designed to UPDATE fields in an EXISTING vehicle rather than setting an
              entirely new vehicle object.
      </pre>
      @param name  The name of the vehicle.
      @param tbl   Set of parameters to set.
   */
  public final void setVehicle(String name, Table tbl) {
    if (tbl != null) { useVehicle(name).setValues(tbl); }
  }

  /** Sets a the parameters for a point. If tbl is null, do nothing. Otherwise set the given values
      in the vehicle adding a new vehicle if necessary.
      <pre>
        NOTE: The behavior of this method is similar to {@link #setVehicle(String,Table)}
              in that is was designed to be easy to use from a macro.
      </pre>
      @param name  The name of the vehicle.
      @param point The name of the point.
      @param tbl   Set of parameters to set.
   */
  public final void setVehiclePoint(String name, String point, Table tbl) {
    if (tbl != null) { useVehicle(name).usePointingPos(point).setValues(tbl); }
  }

  /** Gets the info for a point as a table.
      @param name  The name of the vehicle.
      @param point The name of the point.
      @return the info for a point as a table
   */
  public final Table getVehiclePoint(String name, String point) {
    Vehicle       veh = getVehicle(name);
    Vehicle.PointingPos pnt = (veh==null)? null: veh.getPointingPos(point);

    return (pnt==null)? null: pnt.toTable();
  }

  /** Gets the vehicle used by this layer.
   *  @param name entry to get
   *  @return the vehicle used by this layer
   */
  public Vehicle getVehicle(String name) {
    return (Vehicle)vehicles.get(name);
  }

  /** Gets the vehicle parameters in table form. This is the identical to
      <code>getVehicle(name).toTable()</code>, except that it returns null
      if the named vehicle does not exist.
      @param name the name of the vehicle
      @return the vehicle parameters in Table form
   */
  public final Table getVehicleTable(String name) {
    Vehicle veh = getVehicle(name);
    return (veh==null)? null: veh.toTable();
  }

  /** Sets the vehicle parameters using a table. This is identical to
      {@link #setVehicle(String,Table)}, but is here to match {@link #getVehicleTable(String)}.
      @param name he name of the vehicle
      @param tbl table containing vehicle parameters
   */
  public final void setVehicleTable(String name, Table tbl) {
    setVehicle(name,tbl);
  }

  /** Adds a new vehicle to this layer.
      This is identical to <code>setVehicle(name,vehicle)</code>.
      @param name    name of vehicle to add
      @param vehicle the Vehicle to add
   */
  public final void addVehicle(String name, Vehicle vehicle) {
    setVehicle(name,vehicle);
  }

  /** Removes a vehicle from this layer.
      This is identical to <code>setVehicle(name,(Vehicle)null)</code>.
      @param name name of vehicle to remove
   */
  public final void removeVehicle(String name) {
    setVehicle(name,(Vehicle)null);
  }

  /** Removes all vehicles from this layer. */
  public void removeAllVehicles () {
    if (!vehicles.isEmpty()) {
      vehicles.clear(Table.FORCE);
      vehiclesUpdated();
      refresh();
    }
  }

  /** Called when the list of vehicles is changed. */
  protected void vehiclesUpdated () {
    // do nothing, allow subclasses to override.
  }

  /** Sets the cone mode to use, see {@link #coneModeList}.
   *  @param mode the cone mode to use */
  public void setConeMode(String mode) {
    int val = Parser.find(coneModeList,mode,coneMode,-2,Parser.E_WARNING);
    if (val != coneMode) {
      coneMode = val;
      refresh();
    }
  }

  /** Gets the cone mode in use, see {@link #coneModeList}.
   *  @return the cone mode in use */
  public String getConeMode() {
    return Parser.get(coneModeList,coneMode,-2);
  }

  /** Indicates if the cones should be filled.
   *  @param fill true if the cones should be filled */
  public void setConeFill(boolean fill) {
    if (fill != coneFill) {
      coneFill = fill;
      refresh();
    }
  }

  /** Indicates if the cones should be filled.
   *  @return true if the cones should be filled
   */
  public boolean getConeFill() {
    return coneFill;
  }

  /** Sets the range appropriate for a map of the world. */
  @Override
  public void findRange() {
    x1 = -180.0; x2 = 180.0;
    y1 =  -90.0; y2 =  90.0;
    z1 =    0.0; z2 =   0.0;
    // Set Minimum and Maximum actual auto-scale value (since 3.7.0 for NXM-2984)
    a1 = y1;
    a2 = y2;
  }

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

  /** Checks to see if a given flag is set. */
  protected boolean is(int val) {
    return ((flags & val) != 0);
  }

  /** Is vehicle visible? */
  private boolean isVisible (Vehicle loc) {
    Object vis = loc.getAttribute("VISIBLE");
    return (vis == null) || StringUtil.isTrue(vis.toString());
  }

  /** Is location visible? */
  private boolean isVisible (Location loc) {
    Object vis = loc.getAttribute("VISIBLE");
    return (vis == null) || StringUtil.isTrue(vis.toString());
  }

  /** Is vehicle marked (i.e. labeled)? */
  private boolean isMarked (Vehicle veh) {
    Object marked = veh.getAttribute("MARKER");
    return ((marked == null) && is(MARKERS)) || StringUtil.isTrue(marked.toString());
  }

  /** Sets the cone fill transparency based on percentage
      @param percent  Value between 1 and 100
      @since NeXtMidas 3.3.3
   */
  public void setOpaqueness (int percent) { // Added for Bug 2757
    int val = MColor.pct2alpha(percent);
    setAlpha(val);
  }

  /** Get the cone fill transparency based on percentage (0-100)
      @return percent  Value between 1 and 100
      @since NeXtMidas 3.3.4
   */
  public int getOpaqueness () {
    return MColor.alpha2pct(fillAlpha);
  }

  /** Sets the cone fill transparency based on alpha component (0-255)
      @param alpha  Value between 1 and 255
      @since NeXtMidas 3.3.4
  */
  public void setAlpha (int alpha) {
    if (alpha != fillAlpha) {
      fillAlpha = alpha;
      refresh();
    }
  }

  /** Get the cone fill transparency based on alpha component (0-255)
      @return percent  Value between 1 and 255
      @since NeXtMidas 3.3.4
   */
  public int getAlpha() {
    return fillAlpha;
  }

  public void draw (int flag) {
    draw(flag, true);
  }

  protected void draw (int flag, boolean doVehicles) {
    super.draw(flag);
    if (doVehicles) {
      String[] names = getVehicleNames();
      for (int v = 0; v < names.length; v++) {
        Vehicle veh = getVehicle(names[v]);
        if (isVisible(veh)) drawVehicle(veh);
      }
    }
  }

  protected void drawVehicle (Vehicle vehicle) {
    drawVehicle(vehicle, null, cangle, crange, Double.NaN);
  }

  protected void drawVehicle (Vehicle vehicle, Position pospOR, double cangleOR, double crangeOR, double radiusOR) {
    Location loc  = vehicle.getLocation();
    Location site = vehicle.getSite();
    Position pos  = loc.getPos();
    String[] keys = vehicle.getPointingPosNames();

    if (is(FOOTPRINT)) {
      for (int i = 0; i < keys.length; i++) {
        PointingPos point  = vehicle.getPointingPos(keys[i]);
        if (!isVisible(point)) continue;

        Position   posp      = (pospOR   != null    )? pospOR   : point.getPos();
        double     angle     = (cangleOR == cangleOR)? cangleOR : point.getConeAngle();
        double     range     = (crangeOR == crangeOR)? crangeOR : 0; // Always 0 for now
        double     radius    = (radiusOR == radiusOR)? radiusOR : point.getRadius();
        double[][] footprint = point.getFootprint();

        if ((angle > 0) || (radius > 0) || (footprint != null)) {
          Line tline = line.copy();
          setLineColor(tline,"COLOR",vehicle,point);
          if (coneFill) {
            tline.setType(tline.getTypeInt()|Line.FILL);
            //tline.setFillColor(MColor.getColor(tline.getColor(), 0x70));
            tline.setFillColor(MColor.getColor(tline.getColor(), fillAlpha));
          }

          if (footprint != null) { // show complex footprint rather then radius or cone
            for (int fp = 0; fp < footprint.length; fp++) {
              MP.drawLineLonLat(footprint[fp], 0, 3, footprint[fp].length/3, tline);
            }
          }
          else if (radius > 0) { // show effective radius rather than cone
            MP.drawEllipse(posp.getLon(), posp.getLat(), 0, radius, radius, 0.0, -1, tline, true); // since 3.9.1 for NXM-3131 high precision
          }
          else {
            MP.drawCone(pos,posp,conic,cdiv,angle,range,tline,coneMode, true); // enable high precision zoom 3.9.1
          }
          drawLabel(vehicle, point, tline, posp);
        }
      }
    }

    Line lin = line.copy();
    setLineColor(lin,"HORIZONCOLOR",vehicle,null);
    if (isMarked(vehicle)) {
      drawLabel(vehicle, loc,  lin, null);
      drawLabel(vehicle, site, lin, null);
    }
    if (is(HORIZON)) {
      double radius = loc.getRadius();

      if (radius > 0) { // show effective radius rather than horizon
        MP.drawEllipse(pos.getLon(), pos.getLat(), 0, radius, radius, 0.0, -1, lin, true); // enable high precision zoom 3.9.1
      }
      else {
        drawHorizon(pos,lin);
      }
    }
  }

  private void drawLabel (Vehicle vehicle, Location point, Line line, Position posOR) {
    if (!isVisible(vehicle) || !isVisible(point)) return;

    String   label = point.getLabel();
    Position pos   = (posOR != null)? posOR : point.getPos();
    Line     tline = line.copy();
    if (label == null) label = "";

    setLineColor(tline,"COLOR",vehicle,point);
    setLineSymbol(tline,vehicle,point);

    tline.setSymbolHeading((float)pos.getHeading());

    drawLabel(pos, label, tline);
  }

  protected void drawLabel (Position pos, String label, Line line) {
    if (MP.gc==null) return;
    MPoint pix = MP.getPoint(pos);
    if (pix.x < MP.ix1 || pix.x > MP.ix2) {
      return;
    }
    if (pix.y < MP.iy1 || pix.y > MP.iy2) {
      return;
    }

    int radius = 0;
    MP.gc.setColor(line.getColor());

    if (line.getSymbol() != null) {
      radius = line.getSymbol().getSize() / 2;
      line.getSymbol().draw(pix.x, pix.y, MP.gc);
    }
    if (label != null) {
      // Determine the x-offset of the label; left vs right of point
      int xoff = getLabelxoffset(radius, false, name);
      if (label.indexOf('\n') < 0) {
        MP.gc.drawString(label, pix.x + xoff, pix.y + line.getRadius());
      } else {
        String[] lines = label.split("\n");
        int lineHeight = MP.gc.getFontMetrics().getHeight();
        for (int lineNum = 0; lineNum < lines.length; lineNum++) {
          int delta = lineHeight * lineNum;
          MP.gc.drawString(lines[lineNum], pix.x + xoff, pix.y + line.getRadius() + delta);
        }
      }
    }
  }

  private int getLabelxoffset (int radius, boolean left, String label) {
    if (MP.gc==null) return -1;
    int xoff = 2 + radius;
    if (left) {
      FontMetrics fm = MP.gc.getFontMetrics();
      xoff = -1 * (fm.bytesWidth(label.getBytes(), 0, label.length()) + xoff);
    }
    return xoff;
  }

  protected void drawPoint (Line lin, MPoint pix, String label) {
    if (MP.gc==null) return;
    if (pix.x < MP.ix1 || pix.x > MP.ix2) {
      return;
    }
    if (pix.y < MP.iy1 || pix.y > MP.iy2) {
      return;
    }
    int r = lin.getRadius();
    MP.gc.drawLine(pix.x - r, pix.y - r, pix.x + r, pix.y + r);
    MP.gc.drawLine(pix.x - r, pix.y + r, pix.x + r, pix.y - r);
    MP.gc.drawString(label, pix.x + r, pix.y + r);
  }

  private void drawHorizon(Position pos, Line line) {
    MP.drawCircleLonLat(pos.lon, pos.lat, computeHorizonDistance(pos.alt), -1, line);
  }

  /** Compute the distance to the horizon of the Earth as seen from a given altitude. This is based
      on the following computation:
      <pre>
        Let P a point external to a circle (with center point O) and T
        be a point on the circle such that the line PT is tangent to
        the circle. By the definition of a tangent, PT and OT are
        perpendicular, therefore PTO is a right triangle and we can
        solve for the angle w that lies between PT and PO.

            sin(w) = r/d
            w = asin(r/d)

        The other interior angle of the triangle (90-w) is therefore
        the radius of the horizon line as measured in deg lat or lon.
      </pre>
      @param alt The altitude in meters.
      @return The horizon distance as measured in deg latitude.
   */
  public double computeHorizonDistance(double alt) {
    double r = Constants.EARTH_EQUATORIAL_RADIUS;
    double d = alt+r;
    double w = Math.asin(r/d); // returns angle in radians

    return 90.0 - Math.toDegrees(w);
  }

  /** Gets all of the locations from all vehicles. */
  protected final Location[] getAllLocations () {
    return doGetAllLocations().toArray(EMPTY_LOCATION_ARRAY);
  }

  /** Gets all of the locations from all vehicles. */
  protected Vector<Location> doGetAllLocations () {
    Vector<Location> locations = new Vector<>(vehicles.size() * 8);

    String[] names = getVehicleNames();
    for (int i = 0; i < names.length; i++) {
      Vehicle veh = getVehicle(names[i]);
      locations.add(veh.getLocation());
      locations.add(veh.getSite());

      String[] pposNames = veh.getPointingPosNames();
      for (int j = 0; j < pposNames.length; j++) {
        locations.add(veh.getPointingPos(pposNames[j]));
      }
    }
    return locations;
  }

  public void refresh () {
    checkSelection();
    super.refresh();
  }

  /** Check to see that the selection is still valid. */
  private void checkSelection () {
    if ((selected != null) && !doGetAllLocations().contains(selected)) {
      selected = null; // Current selection no longer exists.
    }
  }

  /** Gets the current selected point or <code>null</code> if none is selected.
   *  @return the current selected point */
  public Location getSelected () {
    return selected;
  }

  /** Sets the current selected point or <code>null</code> if none is selected.
   *  @param location the pint to select */
  public void setSelected (Location location) {
    if (selected == location) {
      // do nothing
    } else if ((location == null) || doGetAllLocations().contains(location)) {
      selected = location;
      refresh();
    } else {
      throw new MidasException(getName()+": Invalid location setSelected("+location+"), location "
                             +"does not match any of the vehicles in this layer.");
    }
  }

  /** Clears the current selection. This is the same as <code>setSelected(null)</code>. */
  public final void clearSelected () {
    setSelected(null);
  }

  /** This is the same as <code>selectNearest2D(position,-1)</code>.
   *  @param position the given position
   *  @return the nearest point to the given position or null if none is in range
   */
  public final Location selectNearest2D (Position position)    { return selectNearest2D(position,-1); }
  /** This is the same as <code>selectNearest3D(position,-1)</code>.
   *  @param position the given position
   *  @return the nearest point to the given position or null if none is in range
   */
  public final Location selectNearest3D (Position position)    { return selectNearest3D(position,-1); }
  /** This is the same as <code>selectNearestPoint(position,-1)</code>.
   *  @param position the given position
   *  @return the nearest point to the given position or null if none is in range
   */
  public final Location selectNearestPoint (Position position) { return selectNearestPoint(position,-1); }

  /** This is the same as <code>getNearest2D(position,-1)</code>.
   *  @param position the given position
   *  @return the nearest point to the given position or null if none is in range
   */
  public final Location getNearest2D (Position position)    { return getNearest2D(position,-1); }
  /** This is the same as <code>getNearest3D(position,-1)</code>.
   *  @param position the given position
   *  @return the nearest point to the given position or null if none is in range
   */
  public final Location getNearest3D (Position position)    { return getNearest3D(position,-1); }
  /** This is the same as <code>getNearestPoint(position,-1)</code>.
   *  @param position the given position
   *  @return the nearest point to the given position or null if none is in range
   */
  public final Location getNearestPoint (Position position) { return getNearestPoint(position,-1); }

  /** Selects the nearest point to the given position. The nearest point is determined based on the
      great arc distance between points. Unlike {@link #getNearest3D(Position,double)} this
      explicitly <u>ignores</u> altitude during computation.
      @param position The given position (usually the result of the user clicking on the plot).
      @param range    The range (in RWC) or -1 for unlimited.
      @return The nearest point or null if none is in range.
   */
  public final Location selectNearest2D (Position position, double range) {
    setSelected(getNearest3D(position,range));
    return getSelected();
  }

  /** Selects the nearest point to the given position. The nearest point is determined based on the
      great arc distance between points. Unlike {@link #getNearest2D(Position,double)} this
      explicitly <u>includes</u> altitude during computation.
      @param position The given position (usually the result of the user clicking on the plot).
      @param range    The range (in RWC) or -1 for unlimited.
      @return The nearest point or null if none is in range.
   */
  public final Location selectNearest3D (Position position, double range) {
    setSelected(getNearest3D(position,range));
    return getSelected();
  }

  /** Selects the nearest point to the given position. The nearest point is determined based on the
      pixel distance between viewable points on the plot.
      @param position The given position (usually the result of the user clicking on the plot).
      @param range    The range (in pixels) or -1 for default.
      @return The nearest point or null if none is in range (and visible).
   */
  public final Location selectNearestPoint (Position position, int range) {
    setSelected(getNearestPoint(position,range));
    return getSelected();
  }

  /** Gets the nearest point to the given position. The nearest point is determined based on the
      great arc distance between points. Unlike {@link #getNearest3D(Position,double)} this
      explicitly <u>ignores</u> altitude during computation.
      @param position The given position (usually the result of the user clicking on the plot).
      @param range    The range (in RWC) or -1 for unlimited.
      @return The nearest point or null if none is in range.
   */
  public final Location getNearest2D (Position position, double range) {
    return getNearest(position,range,true);
  }

  /** Gets the nearest point to the given position. The nearest point is determined based on the
      great arc distance between points. Unlike {@link #getNearest2D(Position,double)} this
      explicitly <u>includes</u> altitude during computation.
      @param position The given position (usually the result of the user clicking on the plot).
      @param range    The range (in RWC) or -1 for unlimited.
      @return The nearest point or null if none is in range.
   */
  public final Location getNearest3D (Position position, double range) {
    return getNearest(position,range,false);
  }

  /** Gets the nearest point to the given position. The nearest point is determined based on the
      pixel distance between viewable points on the plot.
      @param position The given position (usually the result of the user clicking on the plot).
      @param range    The range (in pixels) or -1 for default.
      @return The nearest point or null if none is in range (and visible).
   */
  public Location getNearestPoint (Position position, int range) {
    if (position.referenceFrame == Position.NORMAL) { position.car2geo(); }

    Location[] locations = getAllLocations();
    Location   nearest   = null;
    int        lastDist  = (range < 0)? 625: range*range; // Use range^2 to avoid SQRT in the loop
    MPoint     pos       = MP.getPoint(position);
    int        pos_x     = pos.x; // eliminate extra dereferences of .x
    int        pos_y     = pos.y; // eliminate extra dereferences of .y

    for (int i = 0; i < locations.length; i++) {
      MPoint loc   = MP.getPoint(locations[i].getPos());
      int    loc_x = loc.x;
      int    loc_y = loc.y;

      if (MP.isInRange(loc_x, loc_y)) {
        int dx = pos_x - loc_x;
        int dy = pos_y - loc_y;
        int dist = dx*dx + dy*dy;
        if (dist < lastDist) {
          nearest = locations[i];
          lastDist = dist;
        }
      }
    }
    return nearest;
  }

  /** Gets the nearest point to the given position. The nearest point is determined based on the
      great arc distance between points. Unlike {@link #getNearest3D(Position, double)} this
      explicitly <u>ignores</u> altitude during computation.
      @param position The given position (usually the result of the user clicking on the plot).
      @param range    The range (in RWC) or -1 for unlimited.
      @return The nearest point or null if none is in range.
   */
  protected Location getNearest (Position position, double range, boolean twoD) {
    if (position.referenceFrame == Position.NORMAL) { position.car2geo(); }

    Location[] locations = getAllLocations();
    Location   nearest   = null;
    double     lastDist  = (range < 0)? Double.MAX_VALUE: range;
    Position   pos       = newPos(position,twoD);

    for (int i = 0; i < locations.length; i++) {
      Position loc  = newPos(locations[i].getPos(),twoD);
      double   dist = Transform.greatArc(position, loc);
      pos.car2geo(); // Reset pos

      if (dist <= lastDist) {
        nearest = locations[i];
        lastDist = dist;
      }
    }
    return nearest;
  }

  /** Creates a new position in 2D or 3D. */
  private Position newPos (Position pos, boolean twoD) {
    if (twoD) {
      return Position.fromGeo(0.0,pos.lat,pos.lon);
    } else {
      return Position.fromGeo(pos.alt,pos.lat,pos.lon);
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Rendering attribute access methods.
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Gets an attribute from a point using default values. */
  private Object getAttr(String name, Vehicle veh, Location point, Object def) {
    Object val = (point == null)? null: point.getAttributes().getO(name);

    if (val == null) { val = veh.getAttributes().getO(name); }
    if (val == null) { val = def; }

    return val;
  }

  /** Sets the line color based on attributes from a point using default values. */
  private void setLineColor(Line line, String name, Vehicle veh, Location point) {
    Location sel = getSelected();

    if (sel == null) {
      Object val = getAttr(name,veh,point,null);

      if (val == null) { val = getAttr("COLOR",veh,null,null); } // Use vehicle default
      if (val != null) { line.setColor(val); }
    }
    else if (sel == point) {
      line.setColor(selectColor);
    }
    else {
      line.setColor(deselectColor);
    }
  }

  /** Sets the line symbol based on attributes from a point using default values. */
  private void setLineSymbol(Line line, Vehicle veh, Location point) {
    Object symbol = getAttr("SYMBOL",        veh,point,null);
    Object size   = getAttr("SYMBOLSIZE",    veh,point,null);
    Object fill   = getAttr("SYMBOLFILL",    veh,point,null);
    Object hdg    = getAttr("SYMBOLHEADING", veh,point,null);

    if (symbol != null) { line.setSymbol(symbol);   }
    if (size   != null) { line.setSymbolSize(Convert.o2l(size)); }
    if (fill   != null) { line.setSymbolFill(Convert.o2z(fill)); }
    if (hdg    != null) { line.setSymbolHeading(Convert.o2f(hdg)); }
  }

  /** Get the number of conic circles to see from vehicle position to pointing position
   *  @return the number of conic circles to see from vehicle position to pointing position */
  public int getConic () {
    return conic;
  }

  /** Set the number of conic circles to see from vehicle position to pointing position
   *  @param value the number of conic circles to see from vehicle position to pointing position */
  public void setConic (int value) {
    conic = value;
    refresh();
  }

  /** Get the number of conic divisions to see from vehicle position to pointing position
   *  @return the number of conic divisions to see from vehicle position to pointing position */
  public int getConeDiv () {
    return cdiv;
  }

  /** Set the number of conic divisions to see from vehicle position to pointing position
   *  @param value the number of conic divisions to see from vehicle position to pointing position */
  public void setConeDiv (int value) {
    cdiv = value;
    refresh();
  }

  /** Get the cone angle
   *  @return the cone angle range */
  public double getConeAngle () {
    return cangle;
  }

  /** Set the cone angle
   *  @param value the cone angle range */
  public void setConeAngle (double value) {
    cangle = value;
    refresh();
  }

  /** Get the cone angle range
   *  @return the cone angle range */
  public double getConeRange () {
    return crange;
  }

  /** Set the cone angle range
   *  @param value the cone angle range */
  public void setConeRange (double value) {
    crange = value;
    refresh();
  }
}
