package nxm.sys.lib;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MidasReference;
import nxm.sys.inc.Units;

/** A class to handle ESRI Shape files. This file matches the "ESRI Shapefile Technical Description"
    dated July 1998 and corresponding to files with the version number matching {@link #VERSION}.

    @author J. Czechowski major 3.9.0 update
*/
public class ShapeFile extends BaseFile implements ShapeFileModSwapInterface {

  /** ShapeFile Code Number (value={@value}) */
  public static final int FILECODE=9994;

  /** @deprecated Since NeXtMidas 2.3.1: Use {@link #FILECODE} */
  @Deprecated public static final int SHAPEFILE_ID=FILECODE;

  /** ShapeFile Version Number (value={@value}) */
  public static final int VERSION=1000;

  /** ShapeFile Shape Type: Null Shape (ID={@value}) */
  public static final int NULLSHAPE = 0;

  /** ShapeFile Shape Type: Point (ID={@value}) */
  public static final int POINT = 1;

  /** ShapeFile Shape Type: PolyLine (ID={@value}) */
  public static final int POLYLINE = 3;

  /** ShapeFile Shape Type: Polygon (ID={@value}) */
  public static final int POLYGON = 5;

  /** ShapeFile Shape Type: MultiPoint (ID={@value}) */
  public static final int MULTIPOINT = 8;

  /** ShapeFile Shape Type: PointZ (ID={@value}) */
  public static final int POINTZ = 11;

  /** ShapeFile Shape Type: PolyLineZ (ID={@value}) */
  public static final int POLYLINEZ = 13;

  /** ShapeFile Shape Type: MultiPointZ (ID={@value}) */
  public static final int MULTIPOINTZ = 18;

  /** ShapeFile Shape Type: PointM (ID={@value}) */
  public static final int POINTM = 21;

  /** ShapeFile Shape Type: PolyLineM (ID={@value}) */
  public static final int POLYLINEM = 23;

  /** ShapeFile Shape Type: PolygonM (ID={@value}) */
  public static final int POLYGONM = 25;

  /** ShapeFile Shape Type: MultiPointM (ID={@value}) */
  public static final int MULTIPOINTM = 28;

  /** ShapeFile Shape Type: MultiPatch (ID={@value}) */
  public static final int MULTIPATCH = 31;

  /** @deprecated Since NeXtMidas 2.3.1: Use {@link #POLYLINE} */
  @Deprecated public static final int ARC=POLYLINE;

  /** @deprecated Since NeXtMidas 2.3.1: Use {@link #POLYLINEM} */
  @Deprecated public static final int ARC_M=POLYLINEM;

  /** Internal file code for the file (see FILECODE) */
  private int fileCode;
  /** Length of the file (number of 16-bit words) including header */
  private int fileLength;
  /** Read offset into the file (starts at 50=LengthOfHeader) */
  private int fileOffset;
  /** ShapeFile Version Number (see VERSION) */
  private int version;
  /** Shape file type code */
  public int shapeType;
  /** XMin from bounding box in ShapeFile header */
  public double x1;
  /** YMin from bounding box in ShapeFile header */
  public double y1;
  /** XMax from bounding box in ShapeFile header */
  public double x2;
  /** YMax from bounding box in ShapeFile header */
  public double y2;
  /** Timecode set with setTimeCode(..), not in the ShapeFile */
  private double timecode;
  /** Accompanying DBF file */
  public DbfFile dbf;
  /** All of the shapes once read in */
  protected List<Shape> allShapes;

  protected byte[] ghb = new byte[1024];

  /** Creates a new instance. */
  public ShapeFile () { }

  /** Creates a new instance.
      @param ref reference
      @param filename the name of the file to use
      @deprecated since NeXtMidas 3.5.0 use {@link #ShapeFile(MidasReference, Object)}
   */
  @Deprecated
  public ShapeFile (Object ref, Object filename) {
    if (ref instanceof MidasReference) {
      init((MidasReference) ref, filename);
    } else {
      init(Convert.ref2Midas(ref), filename);
    }
  }

  /** Creates a new instance.
   *  @param ref      The midas reference to use (Ideally a Command for proper cmdline args picking)
   *  @param filename The name of the file to use.
   *  @since NeXtMidas 3.5.0
   */
  public ShapeFile (MidasReference ref, Object filename) {
    init(ref,filename);
  }

  @Override
  public String toString() {
    String str = "ShapeFile ";
    if (io!=null) str += io.getTypeString();
    str += " Resource";
    return str;
}

  @Override
  public void connect (int mode) { }

  @Override
  public Data getDataBuffer (int size) { return null; }

  /** Gets the internal file code for the file (see {@link #FILECODE}). To get the shape type
      use {@link #getShapeType()}.
   */
  @Override
  public int getType() { return fileCode; }

  /** Always returns 1 (scalar data). */
  @Override
  public int getMode() { return 1; }

  /** Currently does nothing and returns -1. */
  @Override
  public int read (Data data, int ndo) { return -1; }

  @Override
  public String listHeader () {
    String list =
      "ShapeFile   :  "+getURL()+"\n"
    + "Code        :  "+fileCode+"\n"
    + "Version     :  "+version+"\n"
    + "Length      :  "+fileLength+"\n"
    + "Size        :  "+getSize()+" bytes\n"
    + "Type        :  "+getShapeType()+"\n"
    + "X1,X2       :  ("+x1+","+x2+")\n"
    + "Y1,Y2       :  ("+y1+","+y2+")\n"
    + "DB File     :  "+((dbf == null)? null : dbf.getName())
                       +" (exists="+((dbf == null)? null : dbf.isOpen())+")\n"
    + "Comment     :  "+getComment()+"\n";
    return list;
  }

  /** Gets the shape type, same as {@link #getShapeType()}.
      @return shape type string*/
  public String getTypeString() {
    // Replaced this with getShapeType() to prevent confusion with getType() and
    // IOResource.getTypeString() which are NOT related to this method.
    return getShapeType();
  }

  /** Gets the shape type, returns "unknown" if not found.
      @return shape type*/
  public String getShapeType() {
    return getShapeType(shapeType,"unknown");
  }

  /** Maps a ShapeFile type code to its type string, returns null if not found.
   *  @param type ShapeFile type code
   *  @return ShapeFile type string
   */
  public static String getShapeType(int type) {
    return getShapeType(type,null);
  }

  /** Maps a ShapeFile type code to its type string, returns def if not found. */
  private static String getShapeType (int type, String def) {
    String str = def;
    switch (type) {
      case NULLSHAPE:   str = "NullShape";   break;
      case POINT:       str = "Point";       break;
      case POLYLINE:    str = "PolyLine";    break;
      case POLYGON:     str = "Polygon";     break;
      case MULTIPOINT:  str = "MultiPoint";  break;
      case POINTZ:      str = "PointZ";      break;
      case POLYLINEZ:   str = "PolyLineZ";   break;
      case MULTIPOINTZ: str = "MultiPointZ"; break;
      case POINTM:      str = "PointM";      break;
      case POLYLINEM:   str = "PolyLineM";   break;
      case POLYGONM:    str = "PolygonM";    break;
      case MULTIPOINTM: str = "MultiPointM"; break;
      case MULTIPATCH:  str = "MultiPatch";  break;
    }
    return str;
  }

  /** Always returns "SD". */
  public String getFormat() { return "SD"; }
  public double getStart()  { return 0; }
  public double getDelta()  { return 1; }
  public int    getUnits()  { return Units.NONE; }
  public double getXStart() { return 0; }
  public double getXDelta() { return 1; }
  public int    getXUnits() { return Units.NONE; }
  public int    getXFrame() { return 1; }
  public double getYStart() { return 0; }
  public double getYDelta() { return 1; }
  public int    getYUnits() { return Units.TIME; }
  public int    getYFrame() { return 1; }

  /** Returns the TimeCode set with {@link #setTimeCode(double)}, not in the file. */
  public double getTimeAt(double offset) { return timecode; }

// Deprecating these methods because they do nothing except send a warning since NeXtMidas 3.9.0
  /** Sends a warning message stating that this method is not implemented
      @param value value
   */
  @Deprecated
  public void setFormat (String value) { Shell.warning("ShapeFile: setFormat(v) not implemented"); }
  /** Sends a warning message stating that this method is not implemented
      @param value value
   */
  @Deprecated
  public void setXStart (double value) { Shell.warning("ShapeFile: setXStart(v) not implemented"); }
  /** Sends a warning message stating that this method is not implemented
      @param value value
   */
  @Deprecated
  public void setXDelta (double value) { Shell.warning("ShapeFile: setXDelta(v) not implemented"); }
  /** Sends a warning message stating that this method is not implemented
      @param value value
   */
  @Deprecated
  public void setXUnits (int value)    { Shell.warning("ShapeFile: setXUnits(v) not implemented"); }
  /** Sends a warning message stating that this method is not implemented
      @param value value
   */
  @Deprecated
  public void setXFrame (int value)    { Shell.warning("ShapeFile: setXFrame(v) not implemented"); }
  /** Sends a warning message stating that this method is not implemented
      @param value value
   */
  @Deprecated
  public void setYStart (double value) { Shell.warning("ShapeFile: setYStart(v) not implemented"); }
  /** Sends a warning message stating that this method is not implemented
      @param value value
   */
  @Deprecated
  public void setYDelta (double value) { Shell.warning("ShapeFile: setYDelta(v) not implemented"); }
  /** Sends a warning message stating that this method is not implemented
      @param value value
   */
  @Deprecated
  public void setYUnits (int value)    { Shell.warning("ShapeFile: setYUnits(v) not implemented"); }
  /** Sends a warning message stating that this method is not implemented
      @param value value
   */
  @Deprecated
  public void setYFrame (int value)    { Shell.warning("ShapeFile: setYFrame(v) not implemented"); }

  /** Sets the time code based on a given value
      @param value new time code
   */
  public void setTimeCode (double value) { timecode=value; }

  public boolean open() {
    if (!super.open()) return false;

    // ==== INIT ===================================================================================
    FileName    shpName = getFileName();
    String      dbfName = shpName.getPath() + shpName.getRoot() + ".dbf";
    byte[]      hb      = new byte[1024];
    List<Shape> all     = new ArrayList<>(1024);

    // ==== READ THE HEADER ========================================================================
    read (hb,0,100); // Read header
    Convert.swap4(hb,0,1);  // Special case: FileCode   is bigendian, swap to match rest of header
    Convert.swap4(hb,24,1); // Special case: FileLength is bigendian, swap to match rest of header
    fileCode   = Convert.unpackL(hb, 0,EEEI);
    fileLength = Convert.unpackL(hb,24,EEEI);
    version    = Convert.unpackL(hb,28,EEEI);
    shapeType  = Convert.unpackL(hb,32,EEEI);
    x1 = Convert.unpackD(hb,36,EEEI);
    y1 = Convert.unpackD(hb,44,EEEI);
    x2 = Convert.unpackD(hb,52,EEEI);
    y2 = Convert.unpackD(hb,60,EEEI);
    fileOffset = 50; // Header = 50 x 16-bit words

    // ==== READ THE SHAPES ========================================================================
    for (int i = 0; fileOffset < fileLength; i++) {
      Shape shape = new Shape(this, i);
      fileOffset += shape.getLength();
      all.add(shape);
    }
    allShapes = Collections.unmodifiableList(all);

    // ==== OPEN DBF FILE ==========================================================================
    dbf = new DbfFile(M, dbfName);
    dbf.open(INPUT|OPTIONAL);

    // ==== SANITY CHECKING ========================================================================
    if (version != VERSION) {
      Shell.getSharedMidasContext().warning("ShapeFile: Shape file "+getName()+" has Version="+version
                                    + " but only Version="+VERSION+" is supported by ShapeFile.");
    }
    if (fileCode != FILECODE) {
      Shell.getSharedMidasContext().warning("ShapeFile: Shape file "+getName()+" has FileCode="+fileCode
                                    + " but only FileCode="+FILECODE+" is supported by ShapeFile.");
    }

    return true;
  }

  @Override
  public void close() {
    super.close();
    if (dbf != null)
      dbf.close(); // close underlying file, BUG 2393
  }

  /** Gets the array of shapes in the file. <br>
   *  <br>
   *  Warning: Prior to NeXtMidas 3.9.0, this method would produce garbage results if called at
   *  any point other than immediately after calling {@link #open()}. It is strongly advised that
   *  users use {@link #getAllShapes()} since doing so will ensure that a compile-time error are
   *  present if there is an attempt to the version of this method as it exists in older versions.
   *  (The other method is also faster.)
   *  @return The shapes from the file.
   */
  public Shape[] getShapes () {
    List<Shape> all = getAllShapes();
    return all.toArray(new Shape[all.size()]);
  }


  /** Gets the array of shapes in the file.
   *  @return The shapes from the file as a read-only list. Returns null if the file is not open.
   *  @since NeXtMidas 3.9.0
   */
  public List<Shape> getAllShapes () {
    return allShapes;
  }

  /** Gets all polygon shapes that contain a given point.
   *  @param alt The altitude (currently ignored).
   *  @param lat The latitude.
   *  @param lon The longitude.
   *  @return The matching shapes (can be empty, but won't be null).
   *  @since NeXtMidas 3.9.0
   */
  public List<Shape> getShapesContaining (double alt, double lat, double lon) {
    return getShapesContaining(Position.fromGeo(alt, lat, lon));
  }

  /** Gets all polygon shapes that contain a given point.
   *  @param p The position (altitude is ignored).
   *  @return The matching shapes (can be empty, but won't be null).
   *  @since NeXtMidas 3.9.0
   */
  public List<Shape> getShapesContaining (Position p) {
    return getAllShapes().stream().filter((s) -> s.contains(p)).collect(Collectors.toList());
  }

  /** Gets all shapes near a given point. For a polygon, this will return true if the point is
   *  inside or near the edge of the polygon; the "or near the edge" is particularly useful when
   *  the polygon is small relative the the overall scale.
   *  @param alt The altitude (currently ignored).
   *  @param lat    The latitude.
   *  @param lon    The longitude.
   *  @param radius The *approximate* "nearness" radius in meters.
   *  @return The matching shapes (can be empty, but won't be null).
   *  @since NeXtMidas 3.9.0
   */
  public List<Shape> getShapesNear (double alt, double lat, double lon, double radius) {
    return getShapesNear(Position.fromGeo(alt, lat, lon), radius);
  }

  /** Gets all shapes near a given point. For a polygon, this will return true if the point is
   *  inside or near the edge of the polygon; the "or near the edge" is particularly useful when
   *  the polygon is small relative the the overall scale.
   *  @param p      The position (altitude is ignored).
   *  @param radius The *approximate* "nearness" radius in meters.
   *  @return The matching shapes (can be empty, but won't be null).
   *  @since NeXtMidas 3.9.0
   */
  public List<Shape> getShapesNear (Position p, double radius) {
    return getAllShapes().stream().filter((s) -> s.isNear(p, radius)).collect(Collectors.toList());
  }

  /** Represents a shape in a ShapeFile. */
  public static class Shape {
    private       ShapeFile      sf;    // <-- make 'final' once deprecated methods removed
    private final int            index;
    /**  The default color for the shape */
    private       Color          color;
    private final List<double[]> lineStrings;
    /** Shape first x component  */ @Deprecated public double x1;
    /** Shape second x component */ @Deprecated public double x2;
    /** Shape first y component  */ @Deprecated public double y1;
    /** Shape second y component */ @Deprecated public double y2;
    /** Shape record number       */ @Deprecated public int      recordNumber;
    /** Shape content length      */ @Deprecated public int      contentLength;
    /** Shape type                */ @Deprecated public int      shapeType;
    /** Shape number of parts     */ @Deprecated public int      numParts;
    /** Shape number of points    */ @Deprecated public int      numPoints;
    /** Shape array of parts      */ @Deprecated public int[]    parts;
    /** Shape array of points     */ @Deprecated public double[] points;
    /** Shape maximum part length */ @Deprecated public int      maxPartLength;
    /** Shape label               */ @Deprecated public String   label;           // <-- Remove this (unused)

    /** Creates a new instance.
     *  @param sf    The file to read from.
     *  @param index The index number.
     */
    @SuppressWarnings("OverridableMethodCallInConstructor")
    protected Shape (ShapeFile sf, int index) {
      this.sf          = Objects.requireNonNull(sf);
      this.index       = index;
      this.lineStrings = new ArrayList<>(128);
      readFromFile();
    }

    /** Gets the length.
     *  @return The length of the record in 16-bit words.
     */
    @InternalUseOnly("This method should be marked 'protected' as it only used when reading from "
                    +"a shape file")
    public int getLength() {
      return (4 + contentLength);
    }

    /** Reads the shape from the file
        @param sf ShapeFile
        @return length of the record in 16-bit words
     */
    @Deprecated
    public int readFrom (ShapeFile sf) {
      this.sf = sf;
      readFromFile();
      return getLength();
    }

    /** Reads the shape from the file starting at the current offset. */
    protected void readFromFile () {
      byte[] hb = new byte[1024];

      sf.read(hb,0,12);
      Convert.swap4(hb,0,2);
      recordNumber  = Convert.unpackL(hb,0,EEEI);
      contentLength = Convert.unpackL(hb,4,EEEI);
      shapeType     = Convert.unpackL(hb,8,EEEI);

      switch (shapeType) {
        case POINT:
          sf.read(hb,0,16);
          x1            = Convert.unpackD(hb,0,EEEI);
          y1            = Convert.unpackD(hb,8,EEEI);
          x2            = x1;
          y2            = y1;
          maxPartLength = 1;
          break;
        case POLYLINE:
          /* FALLTHROUGH */
        case POLYGON:
          sf.read (hb,0,40);
          x1            = Convert.unpackD(hb, 0,EEEI);
          y1            = Convert.unpackD(hb, 8,EEEI);
          x2            = Convert.unpackD(hb,16,EEEI);
          y2            = Convert.unpackD(hb,24,EEEI);
          numParts      = Convert.unpackL(hb,32,EEEI);
          numPoints     = Convert.unpackL(hb,36,EEEI);
          maxPartLength = numPoints;
          parts         = new int[numParts];
          points        = new double[numPoints*2];

          int bufLen = Math.max(numParts*4, numPoints*16);
          if (bufLen > hb.length) hb = new byte[bufLen];

          sf.read(hb,0,numParts*4);
          for (int i = 0; i < numParts; i++) {
            parts[i] = Convert.unpackL(hb, i*4, EEEI);
          }

          sf.read(hb,0,numPoints*16);
          for (int i = 0; i < numPoints*2; i++) {
            points[i] = Convert.unpackD(hb, i*8, EEEI);
          }

          if (numParts > 0) {
            maxPartLength = numPoints-parts[numParts-1];
          }

          int n = 0;
          for (int i = 0; i < numParts-1; i++) {
            int nPoints = parts[i+1]-parts[i];
            if (nPoints > maxPartLength) maxPartLength = nPoints;

            lineStrings.add(Arrays.copyOfRange(points, n, n+nPoints*2));
            n += nPoints*2;
          }
          lineStrings.add(Arrays.copyOfRange(points, n, points.length));
          break;
        case MULTIPOINT:
          break;
        case POLYLINEM:
          break;
        case NULLSHAPE:
          /* FALLTHROUGH */
        default:
          int nullread = ((contentLength+4)*2) - 12;
          sf.read(hb,0,nullread);
      }
    }

    @Override
    public String toString () {
      return "Shape: index="+index+" recordNumber="+recordNumber+" type="+getShapeType(shapeType)
            +" numParts="+numParts+" lineStringCount="+lineStrings.size()+" dataTable="+getDataTable();
    }
    /** Sets the color of the shape.
        @param color The new color (null for default).
        @since NeXtMidas 2.1.0
     */
    public void setColor(Color color) { this.color = color; }

    /** Gets the color of the shape.
        @return The color (null for default).
        @since NeXtMidas 2.1.0
     */
    public Color getColor() { return this.color; }

    /**
     * Gets the DBF content that is associated with this shape.
     * @return The DBF content matching {@code dbf.getDataTable(index)} where {@code index} is the applicable index of
     *         this shape. Returns null if not available.
     * @since NeXtMidas 3.9.0
     */
    public Table getDataTable () {
      if (sf.dbf.isOpen() && (index >= 0)) {
        return sf.dbf.getDataTable(index);
      }
      return null;
    }

    /** Determines if a given point is contained within a polygon. If the shape is not a polygon,
     *  this will simply return false.
     *  @param p The position (altitude is ignored).
     *  @return true if the lat/lon of the position falls inside of this polygon.
     *  @since NeXtMidas 3.9.0
     */
    public boolean contains (Position p) {
      switch (shapeType) {
        default:      return false;
        case POLYGON: return lineStrings.stream().anyMatch((ls) -> GeodeticUtil.insidePolygon(p, 2, ls));
      }
    }

    /** Determines if a given point is near the shape. For a polygon, this will return true if
     *  the point is inside or near the edge of the polygon; the "or near the edge" is particularly
     *  useful when the polygon is small relative the the overall scale.
     *  @param p      The position (altitude is ignored).
     *  @param radius The *approximate* "nearness" radius in meters.
     *  @return true if the lat/lon of the position falls near this shape.
     *  @since NeXtMidas 3.9.0
     */
    public boolean isNear (Position p, double radius) {
      switch (shapeType) {
        default:       return false;
        case POINT:    return (GeodeticUtil.computeDistanceMeters(p, Position.fromGeo(0, y1, x1)) <= radius);
        case POLYLINE: return lineStrings.stream().anyMatch((ls) -> isNear(p, radius, ls));
        case POLYGON:  return contains(p) || lineStrings.stream().anyMatch((ls) -> isNear(p, radius, ls));
      }
    }

    /** Determines if a given point is near the shape. For a polygon, this will return true if
     *  the point is inside or near the edge of the polygon; the "or near the edge" is particularly
     *  useful when the polygon is small relative the the overall scale.
     *  @param p      The position (altitude is ignored).
     *  @param radius The *approximate* "nearness" radius in meters.
     *  @param ls     The points of the line string.
     *  @return true if the lat/lon of the position falls near this shape.
     *  @since NeXtMidas 3.9.0
     */
    private boolean isNear (Position p, double radius, double[] ls) {
      if (contains(p)) return true;
      double   lon0;
      double   lat0;
      int      i;

      switch (shapeType) {
        default:
          throw new AssertionError("Unexpected shape: "+shapeType);

        case POLYLINE:
          lon0 = ls[0];
          lat0 = ls[1];
          i    = 1;
          break;

        case POLYGON:
          // Start at segment between last point and first point to close the polygon
          lon0 = ls[ls.length-2];
          lat0 = ls[ls.length-1];
          i    = 0;
          break;
      }

      // See if it is near the line. This treats it as a simple geometry problem
      // and (at least partially) discounts the curvature of the earth which is
      // ok since we only promise an "approximate" answer.
      //           p
      //          /:\
      //         / : \
      //        /  :  \
      //      d/   :r  \       r = d0 * tan(a)
      //      /    :    \      a = angle between hdg(p0,p1) and hdg(p0,p)
      //     / a   :     \
      //    /=============\
      //   p0    LINE      p1
      //
      // The angle a has to be less than 90 or it compares the distance p is from p0 or p1, whichever is closer
      //
      //         p
      //          *
      //          * *
      //           *  *                 r = greater distance between p and either p1 or p2
      //         d  *   *               If either of the following are greater than 90, we do not use tangent, we use the shorter of the distances (p0 to p) and (p1 to p)
      //             *    *             alphaP0 = angle between hdg(p0,p1) and hdg(p0,p)
      //              *a>90 *           alphaP1 = angle between hdg(p1,p0) and hdg(p1,p)
      //               *=====*
      //             p0 LINE p1
      Position p0 = Position.fromGeo(0, lat0, lon0);

      for (; i < ls.length; i+=2) {
        double   lon1  = ls[i];
        double   lat1  = ls[i+1];
        Position p1    = Position.fromGeo(0, lat1, lon1);

        double   hdgP0P1 = Position.computeHeadingPrecise(p0, p1, 0.0); // Heading from p0 to p1
        double   hdgP0P  = Position.computeHeadingPrecise(p0, p,  0.0); // heading from p0 to p
        double   alphaP0 = hdgP0P1 - hdgP0P;                            // angle p0

        double   hdgP1P  = Position.computeHeadingPrecise(p1, p, 0.0);  // Heading from p1 to p
        double   hdgP1P0 = Position.computeHeadingPrecise(p1, p0, 0.0); // Heading from p1 to p0
        double   alphaP1 = hdgP1P0  - hdgP1P;                           //.angle p1

        double r = 0;

        if (Position.equals(p0, p1, 0.000001)) {
          // If positions are both the same, calculate distance from the position
          r = GeodeticUtil.computeDistanceMeters(p, p0);
        }
        else if((Math.max(Math.abs(alphaP0), Math.abs(alphaP1))<90) && !Position.equals(p0, p1, 0.000001)) {
          // If less than 90 degrees, find the point on the line.
          r     = Math.abs(GeodeticUtil.computeDistanceMeters(p, p0) * Math.tan(Math.toRadians(alphaP0)));
        }
        else {
          // One of the 2 points is closest
          r = Math.min(GeodeticUtil.computeDistanceMeters(p, p0), GeodeticUtil.computeDistanceMeters(p, p1));
        }
        if (r <= radius) return true;
      }
      return false;
    }

  } // end class Shape
} // end class ShapeFile
