/// <summary> /// Internal: Create a vertex graph from a set of hexagons. It is the /// responsibility of the caller to call destroyVertexGraph on the populated /// graph, otherwise the memory in the graph nodes will not be freed. /// </summary> /// /// <param name="h3Set">Set of hexagons</param> /// <param name="numHexes">Number of hexagons in the set</param> /// <param name="graph">Output graph</param> /// <!-- Based off 3.1.1 --> public static void h3SetToVertexGraph(ref List <H3Index> h3Set, int numHexes, ref VertexGraph graph) { GeoBoundary vertices = new GeoBoundary(); GeoCoord fromVertex = new GeoCoord(); GeoCoord toVertex = new GeoCoord(); VertexGraph.VertexNode edge; if (numHexes < 1) { // We still need to init the graph, or calls to destroyVertexGraph will // fail graph = new VertexGraph(0, 0); return; } int res = H3Index.H3_GET_RESOLUTION(h3Set[0]); const int minBuckets = 6; // TODO: Better way to calculate/guess? int numBuckets = numHexes > minBuckets ? numHexes : minBuckets; graph = new VertexGraph(numBuckets, res); // Iterate through every hexagon for (int i = 0; i < numHexes; i++) { H3Index.h3ToGeoBoundary(h3Set[i], ref vertices); // iterate through every edge for (int j = 0; j < vertices.numVerts; j++) { fromVertex = new GeoCoord(vertices.verts[j].lat, vertices.verts[j].lon); //fromVtx = vertices.verts[j]; int idx = (j + 1) % vertices.numVerts; toVertex = new GeoCoord(vertices.verts[idx].lat, vertices.verts[idx].lon); //toVtx = vertices.verts[(j + 1) % vertices.numVerts]; // If we've seen this edge already, it will be reversed edge = VertexGraph.findNodeForEdge(ref graph, toVertex, fromVertex); if (edge != null) { // If we've seen it, drop it. No edge is shared by more than 2 // hexagons, so we'll never see it again. VertexGraph.removeVertexNode(ref graph, ref edge); } else { // Add a new node for this edge VertexGraph.addVertexNode(ref graph, fromVertex, toVertex); } } } }
/// <summary> /// Returns whether or not the provided H3Indexes are neighbors. /// </summary> /// <param name="origin">The origin H3 index</param> /// <param name="destination">The destination H3 index</param> /// <returns>1 if the indexes are neighbors, 0 otherwise</returns> /// <!-- Based off 3.1.1 --> public static int h3IndexesAreNeighbors(H3Index origin, H3Index destination) { // Make sure they're hexagon indexes if (H3Index.H3_GET_MODE(ref origin) != Constants.H3_HEXAGON_MODE || H3Index.H3_GET_MODE(ref destination) != Constants.H3_HEXAGON_MODE) { return(0); } // Hexagons cannot be neighbors with themselves if (origin == destination) { return(0); } // Only hexagons in the same resolution can be neighbors if (H3Index.H3_GET_RESOLUTION(origin) != H3Index.H3_GET_RESOLUTION(destination)) { return(0); } // H3 Indexes that share the same parent are very likely to be neighbors // Child 0 is neighbor with all of its parent's 'offspring', the other // children are neighbors with 3 of the 7 children. So a simple comparison // of origin and destination parents and then a lookup table of the children // is a super-cheap way to possibly determine they are neighbors. int parentRes = H3Index.H3_GET_RESOLUTION(origin) - 1; if (parentRes > 0 && (H3Index.h3ToParent(origin, parentRes) == H3Index.h3ToParent(destination, parentRes))) { Direction originResDigit = H3Index.H3_GET_INDEX_DIGIT(origin, parentRes + 1); Direction destinationResDigit = H3Index.H3_GET_INDEX_DIGIT(destination, parentRes + 1); if (originResDigit == Direction.CENTER_DIGIT || destinationResDigit == Direction.CENTER_DIGIT) { return(1); } // These sets are the relevant neighbors in the clockwise // and counter-clockwise Direction[] neighborSetClockwise = { Direction.CENTER_DIGIT, Direction.JK_AXES_DIGIT, Direction.IJ_AXES_DIGIT, Direction.J_AXES_DIGIT, Direction.IK_AXES_DIGIT, Direction.K_AXES_DIGIT, Direction.I_AXES_DIGIT }; Direction[] neighborSetCounterclockwise = { Direction.CENTER_DIGIT, Direction.IK_AXES_DIGIT, Direction.JK_AXES_DIGIT, Direction.K_AXES_DIGIT, Direction.IJ_AXES_DIGIT, Direction.I_AXES_DIGIT, Direction.J_AXES_DIGIT }; if ( neighborSetClockwise[(int)originResDigit] == destinationResDigit || neighborSetCounterclockwise[(int)originResDigit] == destinationResDigit) { return(1); } } // Otherwise, we have to determine the neighbor relationship the "hard" way. var neighborRing = new ulong[7].Select(cell => new H3Index(cell)).ToList(); Algos.kRing(origin, 1, ref neighborRing); for (int i = 0; i < 7; i++) { if (neighborRing[i] == destination) { return(1); } } // Made it here, they definitely aren't neighbors return(0); }
/// <summary> /// Produces an index for ijk+ coordinates anchored by an origin. /// /// The coordinate space used by this function may have deleted /// regions or warping due to pentagonal distortion. /// /// Failure may occur if the coordinates are too far away from the origin /// or if the index is on the other side of a pentagon. /// </summary> /// <param name="origin">An anchoring index for the ijk+ coordinate system.</param> /// <param name="ijk">IJK+ Coordinates to find the index of</param> /// <param name="out_h3">The index will be placed here on success</param> /// <returns>0 on success, or another value on failure</returns> /// <!-- Based off 3.2.0 --> internal static int localIjkToH3(H3Index origin, CoordIJK ijk, ref H3Index out_h3) { int res = H3Index.H3_GET_RESOLUTION(origin); int originBaseCell = H3Index.H3_GET_BASE_CELL(origin); int originOnPent = BaseCells._isBaseCellPentagon(originBaseCell) ? 1 : 0; // This logic is very similar to faceIjkToH3 // initialize the index out_h3 = H3Index.H3_INIT; H3Index.H3_SET_MODE(ref out_h3, Constants.H3_HEXAGON_MODE); H3Index.H3_SET_RESOLUTION(ref out_h3, res); Direction dir; // check for res 0/base cell if (res == 0) { if (ijk.i > 1 || ijk.j > 1 || ijk.k > 1) { // out of range input return(1); } dir = CoordIJK._unitIjkToDigit(ref ijk); int newBaseCell = BaseCells._getBaseCellNeighbor(originBaseCell, dir); if (newBaseCell == BaseCells.INVALID_BASE_CELL) { // Moving in an invalid direction off a pentagon. return(1); } H3Index.H3_SET_BASE_CELL(ref out_h3, newBaseCell); return(0); } // we need to find the correct base cell offset (if any) for this H3 index; // start with the passed in base cell and resolution res ijk coordinates // in that base cell's coordinate system CoordIJK ijkCopy = new CoordIJK(ijk.i, ijk.j, ijk.k); // build the H3Index from finest res up // adjust r for the fact that the res 0 base cell offsets the indexing // digits for (int r = res - 1; r >= 0; r--) { CoordIJK lastIJK = ijkCopy; CoordIJK lastCenter; if (H3Index.isResClassIII(r + 1)) { // rotate ccw CoordIJK._upAp7(ref ijkCopy); lastCenter = ijkCopy; CoordIJK._downAp7(ref lastCenter); } else { // rotate cw CoordIJK._upAp7r(ref ijkCopy); lastCenter = ijkCopy; CoordIJK._downAp7r(ref lastCenter); } CoordIJK diff = new CoordIJK(); CoordIJK._ijkSub(ref lastIJK, ref lastCenter, ref diff); CoordIJK._ijkNormalize(ref diff); H3Index.H3_SET_INDEX_DIGIT(ref out_h3, r + 1, (ulong)CoordIJK._unitIjkToDigit(ref diff)); } // ijkCopy should now hold the IJK of the base cell in the // coordinate system of the current base cell if (ijkCopy.i > 1 || ijkCopy.j > 1 || ijkCopy.k > 1) { // out of range input return(2); } // lookup the correct base cell dir = CoordIJK._unitIjkToDigit(ref ijkCopy); int baseCell = BaseCells._getBaseCellNeighbor(originBaseCell, dir); // If baseCell is invalid, it must be because the origin base cell is a // pentagon, and because pentagon base cells do not border each other, // baseCell must not be a pentagon. int indexOnPent = (baseCell == BaseCells.INVALID_BASE_CELL ? 0 : BaseCells._isBaseCellPentagon(baseCell) ? 1 : 0); if (dir != (int)Direction.CENTER_DIGIT) { // If the index is in a warped direction, we need to unwarp the base // cell direction. There may be further need to rotate the index digits. int pentagonRotations = 0; if (originOnPent != 0) { Direction originLeadingDigit = H3Index._h3LeadingNonZeroDigit(origin); pentagonRotations = PENTAGON_ROTATIONS_REVERSE[(int)originLeadingDigit, (int)dir]; for (int i = 0; i < pentagonRotations; i++) { dir = CoordIJK._rotate60ccw(dir); } // The pentagon rotations are being chosen so that dir is not the // deleted direction. If it still happens, it means we're moving // into a deleted subsequence, so there is no index here. if (dir == Direction.K_AXES_DIGIT) { return(3); } baseCell = BaseCells._getBaseCellNeighbor(originBaseCell, dir); // indexOnPent does not need to be checked again since no pentagon // base cells border each other. if (baseCell == BaseCells.INVALID_BASE_CELL) { throw new Exception("assert(baseCell != BaseCells.INVALID_BASE_CELL);"); } if (BaseCells._isBaseCellPolarPentagon(baseCell)) { throw new Exception("assert(!BaseCells._isBaseCellPentagon(baseCell));"); } } // Now we can determine the relation between the origin and target base // cell. int baseCellRotations = BaseCells.baseCellNeighbor60CCWRots[originBaseCell, (int)dir]; if (baseCellRotations < 0) { throw new Exception("assert(baseCellRotations >= 0);"); } // Adjust for pentagon warping within the base cell. The base cell // should be in the right location, so now we need to rotate the index // back. We might not need to check for errors since we would just be // double mapping. if (indexOnPent != 0) { Direction revDir = BaseCells._getBaseCellDirection(baseCell, originBaseCell); if (revDir == Direction.INVALID_DIGIT) { throw new Exception("assert(revDir != Direction.INVALID_DIGIT);"); } // Adjust for the different coordinate space in the two base cells. // This is done first because we need to do the pentagon rotations // based on the leading digit in the pentagon's coordinate system. for (int i = 0; i < baseCellRotations; i++) { out_h3 = H3Index._h3Rotate60ccw(ref out_h3); } Direction indexLeadingDigit = H3Index._h3LeadingNonZeroDigit(out_h3); if (BaseCells._isBaseCellPolarPentagon(baseCell)) { pentagonRotations = PENTAGON_ROTATIONS_REVERSE_POLAR[(int)revDir, (int)indexLeadingDigit]; } else { pentagonRotations = PENTAGON_ROTATIONS_REVERSE_NONPOLAR[(int)revDir, (int)indexLeadingDigit]; } if (pentagonRotations < 0) { throw new Exception("assert(pentagonRotations >= 0);"); } for (int i = 0; i < pentagonRotations; i++) { out_h3 = H3Index._h3RotatePent60ccw(ref out_h3); } } else { if (pentagonRotations < 0) { throw new Exception("assert(pentagonRotations >= 0);"); } for (int i = 0; i < pentagonRotations; i++) { out_h3 = H3Index._h3Rotate60ccw(ref out_h3); } // Adjust for the different coordinate space in the two base cells. for (int i = 0; i < baseCellRotations; i++) { out_h3 = H3Index._h3Rotate60ccw(ref out_h3); } } } else if (originOnPent != 0 && indexOnPent != 0) { int originLeadingDigit = (int)H3Index._h3LeadingNonZeroDigit(origin); int indexLeadingDigit = (int)H3Index._h3LeadingNonZeroDigit(out_h3); int withinPentagonRotations = PENTAGON_ROTATIONS_REVERSE[originLeadingDigit, indexLeadingDigit]; if (withinPentagonRotations < 0) { throw new Exception("assert(withinPentagonRotations >= 0);"); } for (int i = 0; i < withinPentagonRotations; i++) { out_h3 = H3Index._h3Rotate60ccw(ref out_h3); } } if (indexOnPent != 0) { // TODO: There are cases in h3ToLocalIjk which are failed but not // accounted for here - instead just fail if the recovered index is // invalid. if (H3Index._h3LeadingNonZeroDigit(out_h3) == Direction.K_AXES_DIGIT) { return(4); } } H3Index.H3_SET_BASE_CELL(ref out_h3, baseCell); return(0); }
/// <summary> /// Produces ijk+ coordinates for an index anchored by an origin. /// /// The coordinate space used by this function may have deleted /// regions or warping due to pentagonal distortion. /// /// Coordinates are only comparable if they come from the same /// origin index. /// /// Failure may occur if the index is too far away from the origin /// or if the index is on the other side of a pentagon. /// </summary> /// <param name="origin">An anchoring index for the ijk+ coordinate system</param> /// <param name="h3">Index to find the coordinates of</param> /// <param name="out_coord">ijk+ coordinates of the index will be placed here on success</param> /// <returns>0 on success, or another value on failure.</returns> /// <!-- Based off 3.2.0 --> static int h3ToLocalIjk(H3Index origin, H3Index h3, ref CoordIJK out_coord) { int res = H3Index.H3_GET_RESOLUTION(origin); if (res != H3Index.H3_GET_RESOLUTION(h3)) { return(1); } int originBaseCell = H3Index.H3_GET_BASE_CELL(origin); int baseCell = H3Index.H3_GET_BASE_CELL(h3); // Direction from origin base cell to index base cell Direction dir = 0; Direction revDir = 0; if (originBaseCell != baseCell) { dir = BaseCells._getBaseCellDirection(originBaseCell, baseCell); if (dir == Direction.INVALID_DIGIT) { // Base cells are not neighbors, can't unfold. return(2); } revDir = BaseCells._getBaseCellDirection(baseCell, originBaseCell); if (revDir == Direction.INVALID_DIGIT) { throw new Exception("assert(revDir != INVALID_DIGIT)"); } } int originOnPent = (BaseCells._isBaseCellPentagon(originBaseCell) ? 1 : 0); int indexOnPent = (BaseCells._isBaseCellPentagon(baseCell) ? 1 : 0); FaceIJK indexFijk = new FaceIJK(); if (dir != Direction.CENTER_DIGIT) { // Rotate index into the orientation of the origin base cell. // cw because we are undoing the rotation into that base cell. int baseCellRotations = BaseCells.baseCellNeighbor60CCWRots[originBaseCell, (int)dir]; if (indexOnPent != 0) { for (int i = 0; i < baseCellRotations; i++) { h3 = H3Index._h3RotatePent60cw(h3); revDir = CoordIJK._rotate60cw(revDir); if (revDir == Direction.K_AXES_DIGIT) { revDir = CoordIJK._rotate60cw(revDir); } } } else { for (int i = 0; i < baseCellRotations; i++) { h3 = H3Index._h3Rotate60cw(ref h3); revDir = CoordIJK._rotate60cw(revDir); } } } // Face is unused. This produces coordinates in base cell coordinate space. H3Index._h3ToFaceIjkWithInitializedFijk(h3, ref indexFijk); if (dir != Direction.CENTER_DIGIT) { if (baseCell == originBaseCell) { throw new Exception("assert(baseCell != originBaseCell);"); } if ((originOnPent != 0) && (indexOnPent != 0)) { throw new Exception("assert(!(originOnPent && indexOnPent));"); } int pentagonRotations = 0; int directionRotations = 0; if (originOnPent != 0) { int originLeadingDigit = (int)H3Index._h3LeadingNonZeroDigit(origin); if ((H3Index.isResClassIII(res) && FAILED_DIRECTIONS_III[originLeadingDigit, (int)dir]) || (!H3Index.isResClassIII(res) && FAILED_DIRECTIONS_II[originLeadingDigit, (int)dir])) { // TODO this part of the pentagon might not be unfolded // correctly. return(3); } directionRotations = PENTAGON_ROTATIONS[originLeadingDigit, (int)dir]; pentagonRotations = directionRotations; } else if (indexOnPent != 0) { int indexLeadingDigit = (int)H3Index._h3LeadingNonZeroDigit(h3); if ((H3Index.isResClassIII(res) && FAILED_DIRECTIONS_III[indexLeadingDigit, (int)revDir]) || (!H3Index.isResClassIII(res) && FAILED_DIRECTIONS_II[indexLeadingDigit, (int)revDir])) { // TODO this part of the pentagon might not be unfolded // correctly. return(4); } pentagonRotations = PENTAGON_ROTATIONS[(int)revDir, indexLeadingDigit]; } if (pentagonRotations < 0) { throw new Exception("assert(pentagonRotations >= 0);"); } if (directionRotations < 0) { throw new Exception("assert(directionRotations >= 0);"); } for (int i = 0; i < pentagonRotations; i++) { CoordIJK._ijkRotate60cw(ref indexFijk.coord); } CoordIJK offset = new CoordIJK(); CoordIJK._neighbor(ref offset, dir); // Scale offset based on resolution for (int r = res - 1; r >= 0; r--) { if (H3Index.isResClassIII(r + 1)) { // rotate ccw CoordIJK._downAp7(ref offset); } else { // rotate cw CoordIJK._downAp7r(ref offset); } } for (int i = 0; i < directionRotations; i++) { CoordIJK._ijkRotate60cw(ref offset); } // Perform necessary translation CoordIJK._ijkAdd(indexFijk.coord, offset, ref indexFijk.coord); CoordIJK._ijkNormalize(ref indexFijk.coord); } else if (originOnPent != 0 && indexOnPent != 0) { // If the origin and index are on pentagon, and we checked that the base // cells are the same or neighboring, then they must be the same base // cell. if (baseCell != originBaseCell) { throw new Exception("assert(baseCell == originBaseCell);"); } int originLeadingDigit = (int)H3Index._h3LeadingNonZeroDigit(origin); int indexLeadingDigit = (int)H3Index._h3LeadingNonZeroDigit(h3); if (FAILED_DIRECTIONS_III[originLeadingDigit, indexLeadingDigit] || FAILED_DIRECTIONS_II[originLeadingDigit, indexLeadingDigit]) { // TODO this part of the pentagon might not be unfolded // correctly. return(5); } int withinPentagonRotations = PENTAGON_ROTATIONS[originLeadingDigit, indexLeadingDigit]; for (int i = 0; i < withinPentagonRotations; i++) { CoordIJK._ijkRotate60cw(ref indexFijk.coord); } } out_coord = indexFijk.coord; return(0); }
/// <summary> /// Returns the hexagon index neighboring the origin, in the direction dir. /// /// Implementation note: The only reachable case where this returns 0 is if the /// origin is a pentagon and the translation is in the k direction. Thus, /// 0 can only be returned if origin is a pentagon. /// </summary> /// <param name="origin">Origin index</param> /// <param name="dir">Direction to move in</param> /// <param name="rotations"> /// Number of ccw rotations to perform to reorient the translation vector. /// Will be modified to the new number of rotations to perform (such as /// when crossing a face edge.) /// </param> /// <returns>H3Index of the specified neighbor or 0 if deleted k-subsequence distortion is encountered.</returns> /// <!-- Based off 3.2.0 --> internal static ulong h3NeighborRotations(H3Index origin, Direction dir, ref int rotations) { H3Index out_hex = origin; for (int i = 0; i < rotations; i++) { dir = CoordIJK._rotate60ccw(dir); } int newRotations = 0; int oldBaseCell = H3Index.H3_GET_BASE_CELL(out_hex); Direction oldLeadingDigit = H3Index._h3LeadingNonZeroDigit(out_hex); // Adjust the indexing digits and, if needed, the base cell. int r = H3Index.H3_GET_RESOLUTION(out_hex) - 1; while (true) { if (r == -1) { H3Index.H3_SET_BASE_CELL(ref out_hex, BaseCells.baseCellNeighbors[oldBaseCell, (int)dir]); newRotations = BaseCells.baseCellNeighbor60CCWRots[oldBaseCell, (int)dir]; if (H3Index.H3_GET_BASE_CELL(out_hex) == BaseCells.INVALID_BASE_CELL) { // Adjust for the deleted k vertex at the base cell level. // This edge actually borders a different neighbor. H3Index.H3_SET_BASE_CELL(ref out_hex, BaseCells.baseCellNeighbors[oldBaseCell, (int)Direction.IK_AXES_DIGIT]); newRotations = BaseCells.baseCellNeighbor60CCWRots[oldBaseCell, (int)Direction.IK_AXES_DIGIT]; // perform the adjustment for the k-subsequence we're skipping // over. out_hex = H3Index._h3Rotate60ccw(ref out_hex); rotations++; } break; } Direction oldDigit = H3Index.H3_GET_INDEX_DIGIT(out_hex, r + 1); Direction nextDir; if (H3Index.isResClassIII(r + 1)) { H3Index.H3_SET_INDEX_DIGIT(ref out_hex, r + 1, (ulong)NEW_DIGIT_II[(int)oldDigit, (int)dir]); nextDir = NEW_ADJUSTMENT_II[(int)oldDigit, (int)dir]; } else { H3Index.H3_SET_INDEX_DIGIT(ref out_hex, r + 1, (ulong)NEW_DIGIT_III[(int)oldDigit, (int)dir]); nextDir = NEW_ADJUSTMENT_III[(int)oldDigit, (int)dir]; } if (nextDir != Direction.CENTER_DIGIT) { dir = nextDir; r--; } else { // No more adjustment to perform break; } } int newBaseCell = H3Index.H3_GET_BASE_CELL(out_hex); if (BaseCells._isBaseCellPentagon(newBaseCell)) { int alreadyAdjustedKSubsequence = 0; // force rotation out of missing k-axes sub-sequence if (H3Index._h3LeadingNonZeroDigit(out_hex) == Direction.K_AXES_DIGIT) { if (oldBaseCell != newBaseCell) { // in this case, we traversed into the deleted // k subsequence of a pentagon base cell. // We need to rotate out of that case depending // on how we got here. // check for a cw/ccw offset face; default is ccw if (BaseCells._baseCellIsCwOffset( newBaseCell, BaseCells.baseCellData[oldBaseCell].homeFijk.face)) { out_hex = H3Index._h3Rotate60cw(ref out_hex); } else { out_hex = H3Index._h3Rotate60ccw(ref out_hex); // LCOV_EXCL_LINE } // See cwOffsetPent in testKRing.c for why this is // unreachable. alreadyAdjustedKSubsequence = 1; } else { // In this case, we traversed into the deleted // k subsequence from within the same pentagon // base cell. if (oldLeadingDigit == Direction.CENTER_DIGIT) { // Undefined: the k direction is deleted from here return(H3Index.H3_INVALID_INDEX); } switch (oldLeadingDigit) { case Direction.JK_AXES_DIGIT: // Rotate out of the deleted k subsequence // We also need an additional change to the direction we're // moving in out_hex = H3Index._h3Rotate60ccw(ref out_hex); rotations++; break; case Direction.IK_AXES_DIGIT: // Rotate out of the deleted k subsequence // We also need an additional change to the direction we're // moving in out_hex = H3Index._h3Rotate60cw(ref out_hex); rotations += 5; break; default: // Should never occur return(H3Index.H3_INVALID_INDEX); // LCOV_EXCL_LINE } } } for (int i = 0; i < newRotations; i++) { out_hex = H3Index._h3RotatePent60ccw(ref out_hex); } // Account for differing orientation of the base cells (this edge // might not follow properties of some other edges.) if (oldBaseCell != newBaseCell) { if (BaseCells._isBaseCellPolarPentagon(newBaseCell)) { // 'polar' base cells behave differently because they have all // i neighbors. if (oldBaseCell != 118 && oldBaseCell != 8 && H3Index._h3LeadingNonZeroDigit(out_hex) != Direction.JK_AXES_DIGIT) { rotations++; } } else if (H3Index._h3LeadingNonZeroDigit(out_hex) == Direction.IK_AXES_DIGIT && alreadyAdjustedKSubsequence == 0) { // account for distortion introduced to the 5 neighbor by the // deleted k subsequence. rotations++; } } } else { for (int i = 0; i < newRotations; i++) { out_hex = H3Index._h3Rotate60ccw(ref out_hex); } } rotations = (rotations + newRotations) % 6; return(out_hex); }