Example #1
0
        /// <summary>
        /// Returns the radius of a given hexagon in km.
        /// </summary>
        /// <param name="h3Index">The index of the hexagon</param>
        /// <returns>The radius of the hexagon in km</returns>
        public static double _hexRadiusKm(H3Index h3Index)
        {
            // There is probably a cheaper way to determine the radius of a
            // hexagon, but this way is conceptually simple
            GeoCoord    h3Center   = h3Index.ToGeoCoord();
            GeoBoundary h3Boundary = h3Index.ToGeoBoundary();

            // TODO: double check this logic
            return(GeoCoord._geoDistKm(h3Center, h3Boundary.verts[0]));
        }
Example #2
0
        /// <summary>
        /// Get the center of a bounding box
        /// </summary>
        /// <returns>Center coordinate</returns>
        public GeoCoord bboxCenter()
        {
            var center = new GeoCoord();

            center.latitude = (north + south) / 2.0;

            // If the bbox crosses the antimeridian, shift east 360 degrees
            double newEast = bboxIsTransmeridian() ? east + M_2PI : east;

            center.longitude = ConstrainLongitude((newEast + west) / 2.0);

            return(center);
        }
Example #3
0
        /// <summary>
        /// Determines the center point in spherical coordinates of a cell given by 2D
        /// hex coordinates on a particular icosahedral face.
        /// </summary>
        /// <param name="v">The 2D hex coordinates of the cell.</param>
        /// <param name="face">The icosahedral face upon which the 2D hex coordinate system is centered.</param>
        /// <param name="res">The H3 resolution of the cell.</param>
        /// <param name="isSubstrate">Indicates whether or not this grid is actually a substrate
        /// grid relative to the specified resolution.</param>
        /// <returns>The spherical coordinates of the cell center point.</returns>
        public static GeoCoord FromHex2d(Vec2d v, int face, int res, bool isSubstrate)
        {
            var g = new GeoCoord();

            // calculate (r, theta) in hex2d
            double r = Vec2d._v2dMag(v);

            if (r < EPSILON)
            {
                return(FaceIJK.faceCenterGeo[face]);
            }

            double theta = Math.Atan2(v.y, v.x);

            // scale for current resolution length u
            for (int i = 0; i < res; i++)
            {
                r /= M_SQRT7;
            }

            // scale accordingly if this is a substrate grid
            if (isSubstrate)
            {
                r /= 3.0;
                if (H3Index.isResClassIII(res))
                {
                    r /= M_SQRT7;
                }
            }

            r *= RES0_U_GNOMONIC;

            // perform inverse gnomonic scaling of r
            r = Math.Atan(r);

            // adjust theta for Class III
            // if a substrate grid, then it's already been adjusted for Class III
            if (!isSubstrate && H3Index.isResClassIII(res))
            {
                theta = PositiveAngleRadians(theta + M_AP7_ROT_RADS);
            }

            // find theta as an azimuth
            theta = PositiveAngleRadians(FaceIJK.faceAxesAzRadsCII[face][0] - theta);

            // now find the point at (r,theta) from the face center
            g = GeoCoord._geoAzDistanceRads(FaceIJK.faceCenterGeo[face], theta, r);

            return(g);
        }
Example #4
0
        /*
         * Find the great circle distance in radians between two spherical coordinates.
         *
         * @param p1 The first spherical coordinates.
         * @param p2 The second spherical coordinates.
         * @return The great circle distance in radians between p1 and p2.
         */
        public static double _geoDistRads(GeoCoord p1, GeoCoord p2)
        {
            // use spherical triangle with p1 at A, p2 at B, and north pole at C
            double bigC = Math.Abs(p2.longitude - p1.longitude);

            if (bigC > M_PI)  // assume we want the complement
            {
                // note that in this case they can't both be negative
                double lon1 = p1.longitude;
                if (lon1 < 0.0)
                {
                    lon1 += 2.0 * M_PI;
                }

                double lon2 = p2.longitude;
                if (lon2 < 0.0)
                {
                    lon2 += 2.0 * M_PI;
                }

                bigC = Math.Abs(lon2 - lon1);
            }

            double b = M_PI_2 - p1.latitude;
            double a = M_PI_2 - p2.latitude;

            // use law of cosines to find c
            double cosc = Math.Cos(a) * Math.Cos(b) + Math.Sin(a) * Math.Sin(b) * Math.Cos(bigC);

            if (cosc > 1.0)
            {
                cosc = 1.0;
            }

            if (cosc < -1.0)
            {
                cosc = -1.0;
            }

            return(Math.Acos(cosc));
        }
Example #5
0
 /// <summary>
 /// Whether the bounding box contains a given point
 /// </summary>
 /// <param name="point">Point to test</param>
 /// <returns>Whether the point is contained</returns>
 public bool bboxContains(GeoCoord point) =>
 point.latitude >= south && point.latitude <= north && bboxIsTransmeridian()
 // transmeridian case
         ? (point.longitude >= west || point.longitude <= east)
 // standard case
         : (point.longitude >= west && point.longitude <= east);
Example #6
0
        /// <summary>
        /// Generates the cell boundary in spherical coordinates for a cell given by a
        /// FaceIJK address at a specified resolution.
        /// </summary>
        /// <param name="res">The H3 resolution of the cell.</param>
        /// <param name="isPentagon">Whether or not the cell is a pentagon.</param>
        /// <returns>The spherical coordinates of the cell boundary.</returns>
        public GeoBoundary ToGeoBoundary(int res, bool isPentagon)
        {
            if (isPentagon)
            {
                return(PentagonToGeoBoundary(res));
            }

            var g = new GeoBoundary();

            // the vertexes of an origin-centered cell in a Class II resolution on a
            // substrate grid with aperture sequence 33r. The aperture 3 gets us the
            // vertices, and the 3r gets us back to Class II.
            // vertices listed ccw from the i-axes
            var vertsCII = new CoordIJK[NUM_HEX_VERTS]
            {
                new CoordIJK(2, 1, 0),  // 0
                new CoordIJK(1, 2, 0),  // 1
                new CoordIJK(0, 2, 1),  // 2
                new CoordIJK(0, 1, 2),  // 3
                new CoordIJK(1, 0, 2),  // 4
                new CoordIJK(2, 0, 1)   // 5
            };

            // the vertexes of an origin-centered cell in a Class III resolution on a
            // substrate grid with aperture sequence 33r7r. The aperture 3 gets us the
            // vertices, and the 3r7r gets us to Class II.
            // vertices listed ccw from the i-axes
            var vertsCIII = new CoordIJK[NUM_HEX_VERTS]
            {
                new CoordIJK(5, 4, 0),  // 0
                new CoordIJK(1, 5, 0),  // 1
                new CoordIJK(0, 5, 4),  // 2
                new CoordIJK(0, 1, 5),  // 3
                new CoordIJK(4, 0, 5),  // 4
                new CoordIJK(5, 0, 1)   // 5
            };

            // get the correct set of substrate vertices for this resolution
            var verts = H3Index.isResClassIII(res) ? vertsCIII : vertsCII;

            // adjust the center point to be in an aperture 33r substrate grid
            // these should be composed for speed
            FaceIJK centerIJK = this;

            centerIJK.coord = centerIJK.coord._downAp3()._downAp3r();

            // if res is Class III we need to add a cw aperture 7 to get to
            // icosahedral Class II
            int adjRes = res;

            if (H3Index.isResClassIII(res))
            {
                centerIJK.coord = centerIJK.coord._downAp7r();
                adjRes++;
            }

            // The center point is now in the same substrate grid as the origin
            // cell vertices. Add the center point substate coordinates
            // to each vertex to translate the vertices to that cell.
            var fijkVerts = new FaceIJK[NUM_HEX_VERTS];

            for (int v = 0; v < NUM_HEX_VERTS; v++)
            {
                fijkVerts[v].face  = centerIJK.face;
                fijkVerts[v].coord = (centerIJK.coord + verts[v]).Normalize();
            }

            // convert each vertex to lat/lon
            // adjust the face of each vertex as appropriate and introduce
            // edge-crossing vertices as needed
            g.numVerts = 0;
            int lastFace    = -1;
            int lastOverage = 0;  // 0: none; 1: edge; 2: overage

            for (int vert = 0; vert < NUM_HEX_VERTS + 1; vert++)
            {
                int v = vert % NUM_HEX_VERTS;

                FaceIJK fijk = fijkVerts[v];

                int overage = AdjustOverageClassII(adjRes, false, true);

                /*
                 * Check for edge-crossing. Each face of the underlying icosahedron is a
                 * different projection plane. So if an edge of the hexagon crosses an
                 * icosahedron edge, an additional vertex must be introduced at that
                 * intersection point. Then each half of the cell edge can be projected
                 * to geographic coordinates using the appropriate icosahedron face
                 * projection. Note that Class II cell edges have vertices on the face
                 * edge, with no edge line intersections.
                 */
                if (H3Index.isResClassIII(res) && vert > 0 && fijk.face != lastFace && lastOverage != 1)
                {
                    // find hex2d of the two vertexes on original face
                    int lastV   = (v + 5) % NUM_HEX_VERTS;
                    var orig2d0 = fijkVerts[lastV].coord.ToHex2d();

                    var orig2d1 = fijkVerts[v].coord.ToHex2d();

                    // find the appropriate icosa face edge vertexes
                    int maxDim = maxDimByCIIres[adjRes];
                    var v0     = new Vec2d(3.0 * maxDim, 0.0);
                    var v1     = new Vec2d(-1.5 * maxDim, 3.0 * M_SQRT3_2 * maxDim);
                    var v2     = new Vec2d(-1.5 * maxDim, -3.0 * M_SQRT3_2 * maxDim);

                    int   face2 = (lastFace == centerIJK.face) ? fijk.face : lastFace;
                    Vec2d edge0;
                    Vec2d edge1;
                    switch (adjacentFaceDir[centerIJK.face][face2])
                    {
                    case IJ:
                        edge0 = v0;
                        edge1 = v1;
                        break;

                    case JK:
                        edge0 = v1;
                        edge1 = v2;
                        break;

                    case KI:
                    default:
                        //assert(adjacentFaceDir[centerIJK.face][face2] == KI);
                        edge0 = v2;
                        edge1 = v0;
                        break;
                    }

                    // find the intersection and add the lat/lon point to the result
                    var inter = new Vec2d(0, 0);
                    Vec2d._v2dIntersect(orig2d0, orig2d1, edge0, edge1, ref inter);

                    /*
                     * If a point of intersection occurs at a hexagon vertex, then each
                     * adjacent hexagon edge will lie completely on a single icosahedron
                     * face, and no additional vertex is required.
                     */
                    bool isIntersectionAtVertex = Vec2d._v2dEquals(orig2d0, inter) || Vec2d._v2dEquals(orig2d1, inter);
                    if (!isIntersectionAtVertex)
                    {
                        g.verts[g.numVerts] = GeoCoord.FromHex2d(inter, centerIJK.face, adjRes, true);
                        g.numVerts++;
                    }
                }

                // convert vertex to lat/lon and add to the result
                // vert == NUM_HEX_VERTS is only used to test for possible intersection
                // on last edge
                if (vert < NUM_HEX_VERTS)
                {
                    var vec = fijk.coord.ToHex2d();
                    g.verts[g.numVerts] = GeoCoord.FromHex2d(vec, fijk.face, adjRes, true);
                    g.numVerts++;
                }

                lastFace    = fijk.face;
                lastOverage = overage;
            }

            return(g);
        }
Example #7
0
 /// <summary>
 /// Determines the center point in spherical coordinates of a cell given by
 /// a FaceIJK address at a specified resolution.
 /// </summary>
 /// <param name="res">The H3 resolution of the cell.</param>
 /// <returns>The spherical coordinates of the cell center point.</returns>
 public GeoCoord ToGeoCoord(int res) => GeoCoord.FromHex2d(coord.ToHex2d(), face, res, false);
Example #8
0
        /// <summary>
        /// Generates the cell boundary in spherical coordinates for a pentagonal cell
        /// given by a FaceIJK address at a specified resolution.
        /// </summary>
        /// <param name="res">The H3 resolution of the cell.</param>
        /// <returns>The spherical coordinates of the cell boundary.</returns>
        public GeoBoundary PentagonToGeoBoundary(int res)
        {
            var g = new GeoBoundary();

            // the vertexes of an origin-centered pentagon in a Class II resolution on a
            // substrate grid with aperture sequence 33r. The aperture 3 gets us the
            // vertices, and the 3r gets us back to Class II.
            // vertices listed ccw from the i-axes
            var vertsCII = new CoordIJK[NUM_PENT_VERTS]
            {
                new CoordIJK(2, 1, 0),  // 0
                new CoordIJK(1, 2, 0),  // 1
                new CoordIJK(0, 2, 1),  // 2
                new CoordIJK(0, 1, 2),  // 3
                new CoordIJK(1, 0, 2),  // 4
            };

            // the vertexes of an origin-centered pentagon in a Class III resolution on
            // a substrate grid with aperture sequence 33r7r. The aperture 3 gets us the
            // vertices, and the 3r7r gets us to Class II. vertices listed ccw from the
            // i-axes
            var vertsCIII = new CoordIJK[NUM_PENT_VERTS]
            {
                new CoordIJK(5, 4, 0),  // 0
                new CoordIJK(1, 5, 0),  // 1
                new CoordIJK(0, 5, 4),  // 2
                new CoordIJK(0, 1, 5),  // 3
                new CoordIJK(4, 0, 5),  // 4
            };

            // get the correct set of substrate vertices for this resolution
            CoordIJK[] verts = H3Index.isResClassIII(res) ? vertsCIII : vertsCII;

            // adjust the center point to be in an aperture 33r substrate grid
            // these should be composed for speed
            FaceIJK centerIJK = this;

            centerIJK.coord = centerIJK.coord._downAp3()._downAp3r();

            // if res is Class III we need to add a cw aperture 7 to get to
            // icosahedral Class II
            int adjRes = res;

            if (H3Index.isResClassIII(res))
            {
                centerIJK.coord = centerIJK.coord._downAp7r();
                adjRes++;
            }

            // The center point is now in the same substrate grid as the origin
            // cell vertices. Add the center point substate coordinates
            // to each vertex to translate the vertices to that cell.
            FaceIJK[] fijkVerts = new FaceIJK[NUM_PENT_VERTS];
            for (int v = 0; v < NUM_PENT_VERTS; v++)
            {
                fijkVerts[v].face  = centerIJK.face;
                fijkVerts[v].coord = (centerIJK.coord + verts[v]).Normalize();
            }

            // convert each vertex to lat/lon
            // adjust the face of each vertex as appropriate and introduce
            // edge-crossing vertices as needed
            g.numVerts = 0;
            var lastFijk = new FaceIJK(0, new CoordIJK(0, 0, 0));

            for (int vert = 0; vert < NUM_PENT_VERTS + 1; vert++)
            {
                int v = vert % NUM_PENT_VERTS;

                FaceIJK fijk = fijkVerts[v];

                var pentLeading4 = false;
                int overage      = AdjustOverageClassII(adjRes, pentLeading4, true);
                if (overage == 2)  // in a different triangle
                {
                    while (true)
                    {
                        overage = AdjustOverageClassII(adjRes, pentLeading4, true);
                        if (overage != 2)  // not in a different triangle
                        {
                            break;
                        }
                    }
                }

                // all Class III pentagon edges cross icosa edges
                // note that Class II pentagons have vertices on the edge,
                // not edge intersections
                if (H3Index.isResClassIII(res) && vert > 0)
                {
                    // find hex2d of the two vertexes on the last face

                    FaceIJK tmpFijk = fijk;

                    var orig2d0 = lastFijk.coord.ToHex2d();

                    int currentToLastDir = adjacentFaceDir[tmpFijk.face][lastFijk.face];

                    var fijkOrient = faceNeighbors[tmpFijk.face][currentToLastDir];

                    tmpFijk.face = fijkOrient.face;
                    CoordIJK ijk = tmpFijk.coord;

                    // rotate and translate for adjacent face
                    for (int i = 0; i < fijkOrient.ccwRot60; i++)
                    {
                        ijk = ijk._ijkRotate60ccw();
                    }

                    CoordIJK transVec = fijkOrient.translate;
                    transVec *= unitScaleByCIIres[adjRes] * 3;
                    ijk       = (ijk + transVec).Normalize();

                    var orig2d1 = ijk.ToHex2d();

                    // find the appropriate icosa face edge vertexes
                    int maxDim = maxDimByCIIres[adjRes];
                    var v0     = new Vec2d(3.0 * maxDim, 0.0);
                    var v1     = new Vec2d(-1.5 * maxDim, 3.0 * M_SQRT3_2 * maxDim);
                    var v2     = new Vec2d(-1.5 * maxDim, -3.0 * M_SQRT3_2 * maxDim);

                    Vec2d edge0;
                    Vec2d edge1;
                    switch (adjacentFaceDir[tmpFijk.face][fijk.face])
                    {
                    case IJ:
                        edge0 = v0;
                        edge1 = v1;
                        break;

                    case JK:
                        edge0 = v1;
                        edge1 = v2;
                        break;

                    case KI:
                    default:
                        //assert(adjacentFaceDir[tmpFijk.face][fijk.face] == KI);
                        edge0 = v2;
                        edge1 = v0;
                        break;
                    }

                    // find the intersection and add the lat/lon point to the result
                    var inter = new Vec2d(0, 0);
                    Vec2d._v2dIntersect(orig2d0, orig2d1, edge0, edge1, ref inter);
                    g.verts[g.numVerts] = GeoCoord.FromHex2d(inter, tmpFijk.face, adjRes, true);
                    g.numVerts++;
                }

                // convert vertex to lat/lon and add to the result
                // vert == NUM_PENT_VERTS is only used to test for possible intersection
                // on last edge
                if (vert < NUM_PENT_VERTS)
                {
                    var vec = fijk.coord.ToHex2d();
                    g.verts[g.numVerts] = GeoCoord.FromHex2d(vec, fijk.face, adjRes, true);
                    g.numVerts++;
                }

                lastFijk = fijk;
            }

            return(g);
        }
Example #9
0
        /// <summary>
        /// Encodes a coordinate on the sphere to the corresponding icosahedral face and
        /// containing 2D hex coordinates relative to that face center.
        /// </summary>
        /// <param name="res">The desired H3 resolution for the encoding.</param>
        /// <param name="face">The icosahedral face containing the spherical coordinates.</param>
        /// <returns>The 2D hex coordinates of the cell containing the point.</returns>
        public Vec2d ToHex2d(int res, int face)
        {
            var v = new Vec2d();

            var v3d = this.ToVec3d();

            // determine the icosahedron face
            face = 0;
            double sqd = Vec3d._pointSquareDist(FaceIJK.faceCenterPoint[0], v3d);

            for (int f = 1; f < NUM_ICOSA_FACES; f++)
            {
                double sqdT = Vec3d._pointSquareDist(FaceIJK.faceCenterPoint[f], v3d);
                if (sqdT < sqd)
                {
                    face = f;
                    sqd  = sqdT;
                }
            }

            // cos(r) = 1 - 2 * sin^2(r/2) = 1 - 2 * (sqd / 4) = 1 - sqd/2
            double r = Math.Acos(1 - sqd / 2);

            if (r < EPSILON)
            {
                v.x = v.y = 0.0;
                return(v);
            }

            // now have face and r, now find CCW theta from CII i-axis
            double theta = PositiveAngleRadians(FaceIJK.faceAxesAzRadsCII[face][0] - PositiveAngleRadians(GeoCoord._geoAzimuthRads(FaceIJK.faceCenterGeo[face], this)));

            // adjust theta for Class III (odd resolutions)
            if (H3Index.isResClassIII(res))
            {
                theta = PositiveAngleRadians(theta - M_AP7_ROT_RADS);
            }

            // perform gnomonic scaling of r
            r = Math.Tan(r);

            // scale for current resolution length u
            r /= RES0_U_GNOMONIC;
            for (int i = 0; i < res; i++)
            {
                r *= M_SQRT7;
            }

            // we now have (r, theta) in hex2d with theta ccw from x-axes

            // convert to local x,y
            v.x = r * Math.Cos(theta);
            v.y = r * Math.Sin(theta);

            return(v);
        }
Example #10
0
        /// <summary>
        /// Computes the point on the sphere a specified azimuth and distance from another point.
        /// </summary>
        /// <param name="p1">The first spherical coordinates.</param>
        /// <param name="az">The desired azimuth from p1.</param>
        /// <param name="distance">The desired distance from p1, must be non-negative.</param>
        /// <returns>The spherical coordinates at the desired azimuth and distance from p1.</returns>
        public static GeoCoord _geoAzDistanceRads(GeoCoord p1, double az, double distance)
        {
            if (distance < EPSILON)
            {
                return(new GeoCoord(p1));
            }

            var p2 = new GeoCoord();

            double sinlat, sinlon, coslon;

            az = PositiveAngleRadians(az);

            // check for due north/south azimuth
            if (az < EPSILON || Math.Abs(az - M_PI) < EPSILON)
            {
                if (az < EPSILON)  // due north
                {
                    p2.latitude = p1.latitude + distance;
                }
                else  // due south
                {
                    p2.latitude = p1.latitude - distance;
                }

                if (Math.Abs(p2.latitude - M_PI_2) < EPSILON)  // north pole
                {
                    p2.latitude  = M_PI_2;
                    p2.longitude = 0.0;
                }
                else if (Math.Abs(p2.latitude + M_PI_2) < EPSILON)  // south pole
                {
                    p2.latitude  = -M_PI_2;
                    p2.longitude = 0.0;
                }
                else
                {
                    p2.longitude = ConstrainLongitude(p1.longitude);
                }
            }
            else  // not due north or south
            {
                sinlat = Math.Sin(p1.latitude) * Math.Cos(distance) +
                         Math.Cos(p1.latitude) * Math.Sin(distance) * Math.Cos(az);

                if (sinlat > 1.0)
                {
                    sinlat = 1.0;
                }
                if (sinlat < -1.0)
                {
                    sinlat = -1.0;
                }
                p2.latitude = Math.Asin(sinlat);

                if (Math.Abs(p2.latitude - M_PI_2) < EPSILON)  // north pole
                {
                    p2.latitude  = M_PI_2;
                    p2.longitude = 0.0;
                }
                else if (Math.Abs(p2.latitude + M_PI_2) < EPSILON)  // south pole
                {
                    p2.latitude  = -M_PI_2;
                    p2.longitude = 0.0;
                }
                else
                {
                    sinlon = Math.Sin(az) * Math.Sin(distance) / Math.Cos(p2.latitude);
                    coslon = (Math.Cos(distance) - Math.Sin(p1.latitude) * Math.Sin(p2.latitude)) / Math.Cos(p1.latitude) / Math.Cos(p2.latitude);
                    if (sinlon > 1.0)
                    {
                        sinlon = 1.0;
                    }
                    if (sinlon < -1.0)
                    {
                        sinlon = -1.0;
                    }
                    if (coslon > 1.0)
                    {
                        sinlon = 1.0;
                    }
                    if (coslon < -1.0)
                    {
                        sinlon = -1.0;
                    }
                    p2.longitude = ConstrainLongitude(p1.longitude + Math.Atan2(sinlon, coslon));
                }
            }

            return(p2);
        }
Example #11
0
 /*
  * Determines the azimuth to p2 from p1 in radians.
  *
  * @param p1 The first spherical coordinates.
  * @param p2 The second spherical coordinates.
  * @return The azimuth in radians from p1 to p2.
  */
 public static double _geoAzimuthRads(GeoCoord p1, GeoCoord p2)
 {
     return(Math.Atan2(Math.Cos(p2.latitude) * Math.Sin(p2.longitude - p1.longitude),
                       Math.Cos(p1.latitude) * Math.Sin(p2.latitude) - Math.Sin(p1.latitude) * Math.Cos(p2.latitude) * Math.Cos(p2.longitude - p1.longitude)));
 }
Example #12
0
        public double longitude; // longitude in radians

        public GeoCoord(GeoCoord g)
        {
            latitude  = g.latitude;
            longitude = g.longitude;
        }
Example #13
0
 /*
  * Find the great circle distance in kilometers between two spherical
  * coordinates.
  *
  * @param p1 The first spherical coordinates.
  * @param p2 The second spherical coordinates.
  * @return The distance in kilometers between p1 and p2.
  */
 public static double _geoDistKm(GeoCoord p1, GeoCoord p2) => EARTH_RADIUS_KM *_geoDistRads(p1, p2);
Example #14
0
 /*
  * Determines if the components of two spherical coordinates are within our
  * standard epsilon distance of each other.
  *
  * @param p1 The first spherical coordinates.
  * @param p2 The second spherical coordinates.
  * @return Whether or not the two coordinates are within the epsilon distance
  *         of each other.
  */
 public static bool AlmostEqual(GeoCoord p1, GeoCoord p2)
 {
     return(AlmostEqualThreshold(p1, p2, EPSILON_RAD));
 }
Example #15
0
 /*
  * Determines if the components of two spherical coordinates are within some
  * threshold distance of each other.
  *
  * @param p1 The first spherical coordinates.
  * @param p2 The second spherical coordinates.
  * @param threshold The threshold distance.
  * @return Whether or not the two coordinates are within the threshold distance
  *         of each other.
  */
 public static bool AlmostEqualThreshold(GeoCoord p1, GeoCoord p2, double threshold)
 {
     return(Math.Abs(p1.latitude - p2.latitude) < threshold &&
            Math.Abs(p1.longitude - p2.longitude) < threshold);
 }