/// <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])); }
/// <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); }
/// <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); }
/* * 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)); }
/// <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);
/// <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); }
/// <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);
/// <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); }
/// <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); }
/// <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); }
/* * 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))); }
public double longitude; // longitude in radians public GeoCoord(GeoCoord g) { latitude = g.latitude; longitude = g.longitude; }
/* * 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);
/* * 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)); }
/* * 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); }