/// <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); }
/// <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> /// Convert an FaceIJK address to the corresponding H3Index. /// </summary> /// <param name="res">The cell resolution.</param> /// <returns>The encoded H3Index (or 0 on failure).</returns> public H3Index ToH3Index(int res) { // initialize the index var h = new H3Index(H3Index.H3_INIT); h.Mode = H3_HEXAGON_MODE; h.Resolution = res; // check for res 0/base cell if (res == 0) { if (coord.i > MAX_FACE_COORD || coord.j > MAX_FACE_COORD || coord.k > MAX_FACE_COORD) { return(H3Index.H3_INVALID_INDEX); // out of range input } // TODO: fix h.BaseCell = this.ToBaseCell(); return(h); } // we need to find the correct base cell FaceIJK for this H3 index; // start with the passed in face and resolution res ijk coordinates // in that face's coordinate system //var fijkBC = new FaceIJK(this); var fijkBC = new FaceIJK(this); // build the H3Index from finest res up // adjust r for the fact that the res 0 base cell offsets the indexing digits //CoordIJK* ijk = &fijkBC.coord; var ijk = fijkBC.coord; for (int r = res - 1; r >= 0; r--) { var lastIJK = new CoordIJK(ijk); CoordIJK lastCenter; if (H3Index.isResClassIII(r + 1)) { // rotate ccw ijk = ijk._upAp7(); lastCenter = ijk._downAp7(); } else { // rotate cw ijk = ijk._upAp7r(); lastCenter = ijk._downAp7r(); } var normalDiff = (lastIJK - lastCenter).Normalize(); h.SetIndexDigit(r + 1, normalDiff.UnitIJKToDigit()); } // fijkBC should now hold the IJK of the base cell in the // coordinate system of the current face if (fijkBC.coord.i > MAX_FACE_COORD || fijkBC.coord.j > MAX_FACE_COORD || fijkBC.coord.k > MAX_FACE_COORD) { return(H3Index.H3_INVALID_INDEX); // out of range input } // TODO: check, added this check for debugging since it leads to invalid negative array indexes //if (fijkBC.coord.i < 0 || fijkBC.coord.j < 0 || fijkBC.coord.k < 0) // return H3Index.H3_INVALID_INDEX; // out of range input // lookup the correct base cell var baseCell = fijkBC.ToBaseCell(); h.BaseCell = baseCell; // rotate if necessary to get canonical base cell orientation // for this base cell int numRots = _faceIjkToBaseCellCCWrot60(fijkBC); if (_isBaseCellPentagon(baseCell)) { // force rotation out of missing k-axes sub-sequence if ((int)h._h3LeadingNonZeroDigit() == (int)Direction.K_AXES_DIGIT) { // check for a cw/ccw offset face; default is ccw if (_baseCellIsCwOffset(baseCell, fijkBC.face)) { h = h._h3Rotate60cw(); } else { h = h._h3Rotate60ccw(); } } for (int i = 0; i < numRots; i++) { h = h._h3RotatePent60ccw(); } } else { for (int i = 0; i < numRots; i++) { h = h._h3Rotate60ccw(); } } return(h); }
/// <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); }