h3_algos.c (630 lines of code) (raw):
/*
* Copyright 2016-2021 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @file algos.c
* @brief Hexagon grid algorithms
*/
#include "h3_algos.h"
#include <assert.h>
#include <float.h>
#include <math.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include "h3_alloc.h"
#include "h3_baseCells.h"
#include "h3_bbox.h"
#include "h3_faceijk.h"
#include "h3_h3Assert.h"
#include "h3_h3Index.h"
#include "h3_h3api.h"
#include "h3_latLng.h"
#include "h3_linkedGeo.h"
#include "h3_polygon.h"
#include "h3_vertexGraph.h"
/*
* Return codes from gridDiskUnsafe and related functions.
*/
#define MAX_ONE_RING_SIZE 7
#define POLYGON_TO_CELLS_BUFFER 12
/**
* Directions used for traversing a hexagonal ring counterclockwise around
* {1, 0, 0}
*
* <pre>
* _
* _/ \\_
* / \\5/ \\
* \\0/ \\4/
* / \\_/ \\
* \\1/ \\3/
* \\2/
* </pre>
*/
static const Direction DIRECTIONS[6] = {J_AXES_DIGIT, JK_AXES_DIGIT,
K_AXES_DIGIT, IK_AXES_DIGIT,
I_AXES_DIGIT, IJ_AXES_DIGIT};
/**
* Direction used for traversing to the next outward hexagonal ring.
*/
static const Direction NEXT_RING_DIRECTION = I_AXES_DIGIT;
/**
* New digit when traversing along class II grids.
*
* Current digit -> direction -> new digit.
*/
static const Direction NEW_DIGIT_II[7][7] = {
{CENTER_DIGIT, K_AXES_DIGIT, J_AXES_DIGIT, JK_AXES_DIGIT, I_AXES_DIGIT,
IK_AXES_DIGIT, IJ_AXES_DIGIT},
{K_AXES_DIGIT, I_AXES_DIGIT, JK_AXES_DIGIT, IJ_AXES_DIGIT, IK_AXES_DIGIT,
J_AXES_DIGIT, CENTER_DIGIT},
{J_AXES_DIGIT, JK_AXES_DIGIT, K_AXES_DIGIT, I_AXES_DIGIT, IJ_AXES_DIGIT,
CENTER_DIGIT, IK_AXES_DIGIT},
{JK_AXES_DIGIT, IJ_AXES_DIGIT, I_AXES_DIGIT, IK_AXES_DIGIT, CENTER_DIGIT,
K_AXES_DIGIT, J_AXES_DIGIT},
{I_AXES_DIGIT, IK_AXES_DIGIT, IJ_AXES_DIGIT, CENTER_DIGIT, J_AXES_DIGIT,
JK_AXES_DIGIT, K_AXES_DIGIT},
{IK_AXES_DIGIT, J_AXES_DIGIT, CENTER_DIGIT, K_AXES_DIGIT, JK_AXES_DIGIT,
IJ_AXES_DIGIT, I_AXES_DIGIT},
{IJ_AXES_DIGIT, CENTER_DIGIT, IK_AXES_DIGIT, J_AXES_DIGIT, K_AXES_DIGIT,
I_AXES_DIGIT, JK_AXES_DIGIT}};
/**
* New traversal direction when traversing along class II grids.
*
* Current digit -> direction -> new ap7 move (at coarser level).
*/
static const Direction NEW_ADJUSTMENT_II[7][7] = {
{CENTER_DIGIT, CENTER_DIGIT, CENTER_DIGIT, CENTER_DIGIT, CENTER_DIGIT,
CENTER_DIGIT, CENTER_DIGIT},
{CENTER_DIGIT, K_AXES_DIGIT, CENTER_DIGIT, K_AXES_DIGIT, CENTER_DIGIT,
IK_AXES_DIGIT, CENTER_DIGIT},
{CENTER_DIGIT, CENTER_DIGIT, J_AXES_DIGIT, JK_AXES_DIGIT, CENTER_DIGIT,
CENTER_DIGIT, J_AXES_DIGIT},
{CENTER_DIGIT, K_AXES_DIGIT, JK_AXES_DIGIT, JK_AXES_DIGIT, CENTER_DIGIT,
CENTER_DIGIT, CENTER_DIGIT},
{CENTER_DIGIT, CENTER_DIGIT, CENTER_DIGIT, CENTER_DIGIT, I_AXES_DIGIT,
I_AXES_DIGIT, IJ_AXES_DIGIT},
{CENTER_DIGIT, IK_AXES_DIGIT, CENTER_DIGIT, CENTER_DIGIT, I_AXES_DIGIT,
IK_AXES_DIGIT, CENTER_DIGIT},
{CENTER_DIGIT, CENTER_DIGIT, J_AXES_DIGIT, CENTER_DIGIT, IJ_AXES_DIGIT,
CENTER_DIGIT, IJ_AXES_DIGIT}};
/**
* New traversal direction when traversing along class III grids.
*
* Current digit -> direction -> new ap7 move (at coarser level).
*/
static const Direction NEW_DIGIT_III[7][7] = {
{CENTER_DIGIT, K_AXES_DIGIT, J_AXES_DIGIT, JK_AXES_DIGIT, I_AXES_DIGIT,
IK_AXES_DIGIT, IJ_AXES_DIGIT},
{K_AXES_DIGIT, J_AXES_DIGIT, JK_AXES_DIGIT, I_AXES_DIGIT, IK_AXES_DIGIT,
IJ_AXES_DIGIT, CENTER_DIGIT},
{J_AXES_DIGIT, JK_AXES_DIGIT, I_AXES_DIGIT, IK_AXES_DIGIT, IJ_AXES_DIGIT,
CENTER_DIGIT, K_AXES_DIGIT},
{JK_AXES_DIGIT, I_AXES_DIGIT, IK_AXES_DIGIT, IJ_AXES_DIGIT, CENTER_DIGIT,
K_AXES_DIGIT, J_AXES_DIGIT},
{I_AXES_DIGIT, IK_AXES_DIGIT, IJ_AXES_DIGIT, CENTER_DIGIT, K_AXES_DIGIT,
J_AXES_DIGIT, JK_AXES_DIGIT},
{IK_AXES_DIGIT, IJ_AXES_DIGIT, CENTER_DIGIT, K_AXES_DIGIT, J_AXES_DIGIT,
JK_AXES_DIGIT, I_AXES_DIGIT},
{IJ_AXES_DIGIT, CENTER_DIGIT, K_AXES_DIGIT, J_AXES_DIGIT, JK_AXES_DIGIT,
I_AXES_DIGIT, IK_AXES_DIGIT}};
/**
* New traversal direction when traversing along class III grids.
*
* Current digit -> direction -> new ap7 move (at coarser level).
*/
static const Direction NEW_ADJUSTMENT_III[7][7] = {
{CENTER_DIGIT, CENTER_DIGIT, CENTER_DIGIT, CENTER_DIGIT, CENTER_DIGIT,
CENTER_DIGIT, CENTER_DIGIT},
{CENTER_DIGIT, K_AXES_DIGIT, CENTER_DIGIT, JK_AXES_DIGIT, CENTER_DIGIT,
K_AXES_DIGIT, CENTER_DIGIT},
{CENTER_DIGIT, CENTER_DIGIT, J_AXES_DIGIT, J_AXES_DIGIT, CENTER_DIGIT,
CENTER_DIGIT, IJ_AXES_DIGIT},
{CENTER_DIGIT, JK_AXES_DIGIT, J_AXES_DIGIT, JK_AXES_DIGIT, CENTER_DIGIT,
CENTER_DIGIT, CENTER_DIGIT},
{CENTER_DIGIT, CENTER_DIGIT, CENTER_DIGIT, CENTER_DIGIT, I_AXES_DIGIT,
IK_AXES_DIGIT, I_AXES_DIGIT},
{CENTER_DIGIT, K_AXES_DIGIT, CENTER_DIGIT, CENTER_DIGIT, IK_AXES_DIGIT,
IK_AXES_DIGIT, CENTER_DIGIT},
{CENTER_DIGIT, CENTER_DIGIT, IJ_AXES_DIGIT, CENTER_DIGIT, I_AXES_DIGIT,
CENTER_DIGIT, IJ_AXES_DIGIT}};
/**
* k value which will encompass all cells at resolution 15.
* This is the largest possible k in the H3 grid system.
*/
static const int K_ALL_CELLS_AT_RES_15 = 13780510;
/**
* Maximum number of cells that result from the gridDisk algorithm with the
* given k. Formula source and proof: https://oeis.org/A003215
*
* @param k k value, k >= 0.
* @param out size in indexes
*/
H3Error H3_EXPORT(maxGridDiskSize)(int k, int64_t *out) {
if (k < 0) {
return E_DOMAIN;
}
if (k >= K_ALL_CELLS_AT_RES_15) {
// If a k value of this value or above is provided, this function will
// estimate more cells than exist in the H3 grid at the finest
// resolution. This is a problem since the function does signed integer
// arithmetic on `k`, which could overflow. To prevent that, instead
// substitute the maximum number of cells in the grid, as it should not
// be possible for the gridDisk functions to exceed that. Note this is
// not resolution specific. So, when resolution < 15, this function may
// still estimate a size larger than the number of cells in the grid.
return H3_EXPORT(getNumCells)(MAX_H3_RES, out);
}
*out = 3 * (int64_t)k * ((int64_t)k + 1) + 1;
return E_SUCCESS;
}
/**
* Produce cells within grid distance k of the origin cell.
*
* k-ring 0 is defined as the origin cell, k-ring 1 is defined as k-ring 0 and
* all neighboring cells, and so on.
*
* Output is placed in the provided array in no particular order. Elements of
* the output array may be left zero, as can happen when crossing a pentagon.
*
* @param origin origin cell
* @param k k >= 0
* @param out zero-filled array which must be of size maxGridDiskSize(k)
*/
H3Error H3_EXPORT(gridDisk)(H3Index origin, int k, H3Index *out) {
return H3_EXPORT(gridDiskDistances)(origin, k, out, NULL);
}
/**
* Produce cells and their distances from the given origin cell, up to
* distance k.
*
* k-ring 0 is defined as the origin cell, k-ring 1 is defined as k-ring 0 and
* all neighboring cells, and so on.
*
* Output is placed in the provided array in no particular order. Elements of
* the output array may be left zero, as can happen when crossing a pentagon.
*
* @param origin origin cell
* @param k k >= 0
* @param out zero-filled array which must be of size
* maxGridDiskSize(k)
* @param distances NULL or a zero-filled array which must be of size
* maxGridDiskSize(k)
*/
H3Error H3_EXPORT(gridDiskDistances)(H3Index origin, int k, H3Index *out,
int *distances) {
// Optimistically try the faster gridDiskUnsafe algorithm first
const H3Error failed =
H3_EXPORT(gridDiskDistancesUnsafe)(origin, k, out, distances);
if (failed) {
int64_t maxIdx;
H3Error err = H3_EXPORT(maxGridDiskSize)(k, &maxIdx);
if (err) {
return err;
}
// Fast algo failed, fall back to slower, correct algo
// and also wipe out array because contents untrustworthy
memset(out, 0, maxIdx * sizeof(H3Index));
if (distances == NULL) {
distances = H3_MEMORY(calloc)(maxIdx, sizeof(int));
if (!distances) {
return E_MEMORY_ALLOC;
}
H3Error result = _gridDiskDistancesInternal(origin, k, out,
distances, maxIdx, 0);
H3_MEMORY(free)(distances);
return result;
} else {
memset(distances, 0, maxIdx * sizeof(int));
return _gridDiskDistancesInternal(origin, k, out, distances, maxIdx,
0);
}
} else {
return E_SUCCESS;
}
}
/**
* Internal algorithm for the safe but slow version of gridDiskDistances
*
* Adds the origin cell to the output set (treating it as a hash set)
* and recurses to its neighbors, if needed.
*
* @param origin Origin cell
* @param k Maximum distance to move from the origin
* @param out Array treated as a hash set, elements being either
* H3Index or 0.
* @param distances Scratch area, with elements paralleling the out array.
* Elements indicate ijk distance from the origin cell to
* the output cell
* @param maxIdx Size of out and scratch arrays (must be
* maxGridDiskSize(k))
* @param curK Current distance from the origin
*/
H3Error _gridDiskDistancesInternal(H3Index origin, int k, H3Index *out,
int *distances, int64_t maxIdx, int curK) {
// Put origin in the output array. out is used as a hash set.
int64_t off = origin % maxIdx;
while (out[off] != 0 && out[off] != origin) {
off = (off + 1) % maxIdx;
}
// We either got a free slot in the hash set or hit a duplicate
// We might need to process the duplicate anyways because we got
// here on a longer path before.
if (out[off] == origin && distances[off] <= curK) return E_SUCCESS;
out[off] = origin;
distances[off] = curK;
// Base case: reached an index k away from the origin.
if (curK >= k) return E_SUCCESS;
// Recurse to all neighbors in no particular order.
for (int i = 0; i < 6; i++) {
int rotations = 0;
H3Index nextNeighbor;
H3Error neighborResult = h3NeighborRotations(origin, DIRECTIONS[i],
&rotations, &nextNeighbor);
if (neighborResult != E_PENTAGON) {
// E_PENTAGON is an expected case when trying to traverse off of
// pentagons.
if (neighborResult != E_SUCCESS) {
return neighborResult;
}
neighborResult = _gridDiskDistancesInternal(
nextNeighbor, k, out, distances, maxIdx, curK + 1);
if (neighborResult) {
return neighborResult;
}
}
}
return E_SUCCESS;
}
/**
* Safe but slow version of gridDiskDistances (also called by it when needed).
*
* Adds the origin cell to the output set (treating it as a hash set)
* and recurses to its neighbors, if needed.
*
* @param origin Origin cell
* @param k Maximum distance to move from the origin
* @param out Array treated as a hash set, elements being either
* H3Index or 0.
* @param distances Scratch area, with elements paralleling the out array.
* Elements indicate ijk distance from the origin cell to
* the output cell
*/
H3Error H3_EXPORT(gridDiskDistancesSafe)(H3Index origin, int k, H3Index *out,
int *distances) {
int64_t maxIdx;
H3Error err = H3_EXPORT(maxGridDiskSize)(k, &maxIdx);
if (err) {
return err;
}
return _gridDiskDistancesInternal(origin, k, out, distances, maxIdx, 0);
}
/**
* 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.
*
* @param origin Origin index
* @param dir Direction to move in
* @param 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 out H3Index of the specified neighbor if succesful
* @return E_SUCCESS on success
*/
H3Error h3NeighborRotations(H3Index origin, Direction dir, int *rotations,
H3Index *out) {
H3Index current = origin;
if (dir < CENTER_DIGIT || dir >= INVALID_DIGIT) {
return E_FAILED;
}
// Ensure that rotations is modulo'd by 6 before any possible addition,
// to protect against signed integer overflow.
*rotations = *rotations % 6;
for (int i = 0; i < *rotations; i++) {
dir = _rotate60ccw(dir);
}
int newRotations = 0;
int oldBaseCell = H3_GET_BASE_CELL(current);
if (NEVER(oldBaseCell < 0) || oldBaseCell >= NUM_BASE_CELLS) {
// Base cells less than zero can not be represented in an index
return E_CELL_INVALID;
}
Direction oldLeadingDigit = _h3LeadingNonZeroDigit(current);
// Adjust the indexing digits and, if needed, the base cell.
int r = H3_GET_RESOLUTION(current) - 1;
while (true) {
if (r == -1) {
H3_SET_BASE_CELL(current, baseCellNeighbors[oldBaseCell][dir]);
newRotations = baseCellNeighbor60CCWRots[oldBaseCell][dir];
if (H3_GET_BASE_CELL(current) == INVALID_BASE_CELL) {
// Adjust for the deleted k vertex at the base cell level.
// This edge actually borders a different neighbor.
H3_SET_BASE_CELL(current,
baseCellNeighbors[oldBaseCell][IK_AXES_DIGIT]);
newRotations =
baseCellNeighbor60CCWRots[oldBaseCell][IK_AXES_DIGIT];
// perform the adjustment for the k-subsequence we're skipping
// over.
current = _h3Rotate60ccw(current);
*rotations = *rotations + 1;
}
break;
} else {
Direction oldDigit = H3_GET_INDEX_DIGIT(current, r + 1);
Direction nextDir;
if (oldDigit == INVALID_DIGIT) {
// Only possible on invalid input
return E_CELL_INVALID;
} else if (isResolutionClassIII(r + 1)) {
H3_SET_INDEX_DIGIT(current, r + 1, NEW_DIGIT_II[oldDigit][dir]);
nextDir = NEW_ADJUSTMENT_II[oldDigit][dir];
} else {
H3_SET_INDEX_DIGIT(current, r + 1,
NEW_DIGIT_III[oldDigit][dir]);
nextDir = NEW_ADJUSTMENT_III[oldDigit][dir];
}
if (nextDir != CENTER_DIGIT) {
dir = nextDir;
r--;
} else {
// No more adjustment to perform
break;
}
}
}
int newBaseCell = H3_GET_BASE_CELL(current);
if (_isBaseCellPentagon(newBaseCell)) {
int alreadyAdjustedKSubsequence = 0;
// force rotation out of missing k-axes sub-sequence
if (_h3LeadingNonZeroDigit(current) == 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 (ALWAYS(_baseCellIsCwOffset(
newBaseCell,
baseCellData[oldBaseCell].homeFijk.face))) {
current = _h3Rotate60cw(current);
} else {
// See cwOffsetPent in testGridDisk.c for why this is
// unreachable.
current = _h3Rotate60ccw(current);
}
alreadyAdjustedKSubsequence = 1;
} else {
// In this case, we traversed into the deleted
// k subsequence from within the same pentagon
// base cell.
if (oldLeadingDigit == CENTER_DIGIT) {
// Undefined: the k direction is deleted from here
return E_PENTAGON;
} else if (oldLeadingDigit == JK_AXES_DIGIT) {
// Rotate out of the deleted k subsequence
// We also need an additional change to the direction we're
// moving in
current = _h3Rotate60ccw(current);
*rotations = *rotations + 1;
} else if (oldLeadingDigit == IK_AXES_DIGIT) {
// Rotate out of the deleted k subsequence
// We also need an additional change to the direction we're
// moving in
current = _h3Rotate60cw(current);
*rotations = *rotations + 5;
} else {
// TODO: Should never occur, but is reachable by fuzzer
return E_FAILED;
}
}
}
for (int i = 0; i < newRotations; i++)
current = _h3RotatePent60ccw(current);
// Account for differing orientation of the base cells (this edge
// might not follow properties of some other edges.)
if (oldBaseCell != newBaseCell) {
if (_isBaseCellPolarPentagon(newBaseCell)) {
// 'polar' base cells behave differently because they have all
// i neighbors.
if (oldBaseCell != 118 && oldBaseCell != 8 &&
_h3LeadingNonZeroDigit(current) != JK_AXES_DIGIT) {
*rotations = *rotations + 1;
}
} else if (_h3LeadingNonZeroDigit(current) == IK_AXES_DIGIT &&
!alreadyAdjustedKSubsequence) {
// account for distortion introduced to the 5 neighbor by the
// deleted k subsequence.
*rotations = *rotations + 1;
}
}
} else {
for (int i = 0; i < newRotations; i++)
current = _h3Rotate60ccw(current);
}
*rotations = (*rotations + newRotations) % 6;
*out = current;
return E_SUCCESS;
}
/**
* Get the direction from the origin to a given neighbor. This is effectively
* the reverse operation for h3NeighborRotations. Returns INVALID_DIGIT if the
* cells are not neighbors.
*
* TODO: This is currently a brute-force algorithm, but as it's O(6) that's
* probably acceptable.
*/
Direction directionForNeighbor(H3Index origin, H3Index destination) {
bool isPent = H3_EXPORT(isPentagon)(origin);
// Checks each neighbor, in order, to determine which direction the
// destination neighbor is located. Skips CENTER_DIGIT since that
// would be the origin; skips deleted K direction for pentagons.
for (Direction direction = isPent ? J_AXES_DIGIT : K_AXES_DIGIT;
direction < NUM_DIGITS; direction++) {
H3Index neighbor;
int rotations = 0;
H3Error neighborError =
h3NeighborRotations(origin, direction, &rotations, &neighbor);
if (!neighborError && neighbor == destination) {
return direction;
}
}
return INVALID_DIGIT;
}
/**
* gridDiskUnsafe produces indexes within k distance of the origin index.
* Output behavior is undefined when one of the indexes returned by this
* function is a pentagon or is in the pentagon distortion area.
*
* k-ring 0 is defined as the origin index, k-ring 1 is defined as k-ring 0 and
* all neighboring indexes, and so on.
*
* Output is placed in the provided array in order of increasing distance from
* the origin.
*
* @param origin Origin location.
* @param k k >= 0
* @param out Array which must be of size maxGridDiskSize(k).
* @return 0 if no pentagon or pentagonal distortion area was encountered.
*/
H3Error H3_EXPORT(gridDiskUnsafe)(H3Index origin, int k, H3Index *out) {
return H3_EXPORT(gridDiskDistancesUnsafe)(origin, k, out, NULL);
}
/**
* gridDiskDistancesUnsafe produces indexes within k distance of the origin
* index. Output behavior is undefined when one of the indexes returned by this
* function is a pentagon or is in the pentagon distortion area.
*
* k-ring 0 is defined as the origin index, k-ring 1 is defined as k-ring 0 and
* all neighboring indexes, and so on.
*
* Output is placed in the provided array in order of increasing distance from
* the origin. The distances in hexagons is placed in the distances array at
* the same offset.
*
* @param origin Origin location.
* @param k k >= 0
* @param out Array which must be of size maxGridDiskSize(k).
* @param distances Null or array which must be of size maxGridDiskSize(k).
* @return 0 if no pentagon or pentagonal distortion area was encountered.
*/
H3Error H3_EXPORT(gridDiskDistancesUnsafe)(H3Index origin, int k, H3Index *out,
int *distances) {
// Return codes:
// 1 Pentagon was encountered
// 2 Pentagon distortion (deleted k subsequence) was encountered
// Pentagon being encountered is not itself a problem; really the deleted
// k-subsequence is the problem, but for compatibility reasons we fail on
// the pentagon.
if (k < 0) {
return E_DOMAIN;
}
// k must be >= 0, so origin is always needed
int idx = 0;
out[idx] = origin;
if (distances) {
distances[idx] = 0;
}
idx++;
if (H3_EXPORT(isPentagon)(origin)) {
// Pentagon was encountered; bail out as user doesn't want this.
return E_PENTAGON;
}
// 0 < ring <= k, current ring
int ring = 1;
// 0 <= direction < 6, current side of the ring
int direction = 0;
// 0 <= i < ring, current position on the side of the ring
int i = 0;
// Number of 60 degree ccw rotations to perform on the direction (based on
// which faces have been crossed.)
int rotations = 0;
while (ring <= k) {
if (direction == 0 && i == 0) {
// Not putting in the output set as it will be done later, at
// the end of this ring.
H3Error neighborResult = h3NeighborRotations(
origin, NEXT_RING_DIRECTION, &rotations, &origin);
if (neighborResult) {
// Should not be possible because `origin` would have to be a
// pentagon
// TODO: Reachable via fuzzer
return neighborResult;
}
if (H3_EXPORT(isPentagon)(origin)) {
// Pentagon was encountered; bail out as user doesn't want this.
return E_PENTAGON;
}
}
H3Error neighborResult = h3NeighborRotations(
origin, DIRECTIONS[direction], &rotations, &origin);
if (neighborResult) {
return neighborResult;
}
out[idx] = origin;
if (distances) {
distances[idx] = ring;
}
idx++;
i++;
// Check if end of this side of the k-ring
if (i == ring) {
i = 0;
direction++;
// Check if end of this ring.
if (direction == 6) {
direction = 0;
ring++;
}
}
if (H3_EXPORT(isPentagon)(origin)) {
// Pentagon was encountered; bail out as user doesn't want this.
return E_PENTAGON;
}
}
return E_SUCCESS;
}
/**
* gridDisksUnsafe takes an array of input hex IDs and a max k-ring and returns
* an array of hexagon IDs sorted first by the original hex IDs and then by the
* k-ring (0 to max), with no guaranteed sorting within each k-ring group.
*
* @param h3Set A pointer to an array of H3Indexes
* @param length The total number of H3Indexes in h3Set
* @param k The number of rings to generate
* @param out A pointer to the output memory to dump the new set of H3Indexes to
* The memory block should be equal to maxGridDiskSize(k) * length
* @return 0 if no pentagon is encountered. Cannot trust output otherwise
*/
H3Error H3_EXPORT(gridDisksUnsafe)(H3Index *h3Set, int length, int k,
H3Index *out) {
H3Index *segment;
int64_t segmentSize;
H3Error err = H3_EXPORT(maxGridDiskSize)(k, &segmentSize);
if (err) {
return err;
}
for (int i = 0; i < length; i++) {
// Determine the appropriate segment of the output array to operate on
segment = out + i * segmentSize;
H3Error failed = H3_EXPORT(gridDiskUnsafe)(h3Set[i], k, segment);
if (failed) return failed;
}
return E_SUCCESS;
}
/**
* Returns the "hollow" ring of hexagons at exactly grid distance k from
* the origin hexagon. In particular, k=0 returns just the origin hexagon.
*
* A nonzero failure code may be returned in some cases, for example,
* if a pentagon is encountered.
* Failure cases may be fixed in future versions.
*
* @param origin Origin location.
* @param k k >= 0
* @param out Array which must be of size 6 * k (or 1 if k == 0)
* @return 0 if successful; nonzero otherwise.
*/
H3Error H3_EXPORT(gridRingUnsafe)(H3Index origin, int k, H3Index *out) {
// Short-circuit on 'identity' ring
if (k == 0) {
out[0] = origin;
return E_SUCCESS;
}
int idx = 0;
// Number of 60 degree ccw rotations to perform on the direction (based on
// which faces have been crossed.)
int rotations = 0;
// Scratch structure for checking for pentagons
if (H3_EXPORT(isPentagon)(origin)) {
// Pentagon was encountered; bail out as user doesn't want this.
return E_PENTAGON;
}
for (int ring = 0; ring < k; ring++) {
H3Error neighborResult = h3NeighborRotations(
origin, NEXT_RING_DIRECTION, &rotations, &origin);
if (neighborResult) {
// Should not be possible because `origin` would have to be a
// pentagon
// TODO: Reachable via fuzzer
return neighborResult;
}
if (H3_EXPORT(isPentagon)(origin)) {
return E_PENTAGON;
}
}
H3Index lastIndex = origin;
out[idx] = origin;
idx++;
for (int direction = 0; direction < 6; direction++) {
for (int pos = 0; pos < k; pos++) {
H3Error neighborResult = h3NeighborRotations(
origin, DIRECTIONS[direction], &rotations, &origin);
if (neighborResult) {
// Should not be possible because `origin` would have to be a
// pentagon
// TODO: Reachable via fuzzer
return neighborResult;
}
// Skip the very last index, it was already added. We do
// however need to traverse to it because of the pentagonal
// distortion check, below.
if (pos != k - 1 || direction != 5) {
out[idx] = origin;
idx++;
if (H3_EXPORT(isPentagon)(origin)) {
return E_PENTAGON;
}
}
}
}
// Check that this matches the expected lastIndex, if it doesn't,
// it indicates pentagonal distortion occurred and we should report
// failure.
if (lastIndex != origin) {
return E_PENTAGON;
} else {
return E_SUCCESS;
}
}
/**
* maxPolygonToCellsSize returns the number of cells to allocate space for
* when performing a polygonToCells on the given GeoJSON-like data structure.
*
* The size is the maximum of either the number of points in the geoloop or the
* number of cells in the bounding box of the geoloop.
*
* @param geoPolygon A GeoJSON-like data structure indicating the poly to fill
* @param res Hexagon resolution (0-15)
* @param out number of cells to allocate for
* @return 0 (E_SUCCESS) on success.
*/
H3Error H3_EXPORT(maxPolygonToCellsSize)(const GeoPolygon *geoPolygon, int res,
uint32_t flags, int64_t *out) {
H3Error flagErr = validatePolygonFlags(flags);
if (flagErr) {
return flagErr;
}
// Get the bounding box for the GeoJSON-like struct
BBox bbox;
const GeoLoop geoloop = geoPolygon->geoloop;
bboxFromGeoLoop(&geoloop, &bbox);
int64_t numHexagons;
H3Error estimateErr = bboxHexEstimate(&bbox, res, &numHexagons);
if (estimateErr) {
return estimateErr;
}
// This algorithm assumes that the number of vertices is usually less than
// the number of hexagons, but when it's wrong, this will keep it from
// failing
int totalVerts = geoloop.numVerts;
for (int i = 0; i < geoPolygon->numHoles; i++) {
totalVerts += geoPolygon->holes[i].numVerts;
}
if (numHexagons < totalVerts) numHexagons = totalVerts;
// When the polygon is very small, near an icosahedron edge and is an odd
// resolution, the line tracing needs an extra buffer than the estimator
// function provides (but beefing that up to cover causes most situations to
// overallocate memory)
numHexagons += POLYGON_TO_CELLS_BUFFER;
*out = numHexagons;
return E_SUCCESS;
}
/**
* _getEdgeHexagons takes a given geoloop ring (either the main geoloop or
* one of the holes) and traces it with hexagons and updates the search and
* found memory blocks. This is used for determining the initial hexagon set
* for the polygonToCells algorithm to execute on.
*
* @param geoloop The geoloop (or hole) to be traced
* @param numHexagons The maximum number of hexagons possible for the geoloop
* (also the bounds of the search and found arrays)
* @param res The hexagon resolution (0-15)
* @param numSearchHexes The number of hexagons found so far to be searched
* @param search The block of memory containing the hexagons to search from
* @param found The block of memory containing the hexagons found from the
* search
*
* @return An error code if the hash function cannot insert a found hexagon
* into the found array.
*/
H3Error _getEdgeHexagons(const GeoLoop *geoloop, int64_t numHexagons, int res,
int64_t *numSearchHexes, H3Index *search,
H3Index *found) {
for (int i = 0; i < geoloop->numVerts; i++) {
LatLng origin = geoloop->verts[i];
LatLng destination = i == geoloop->numVerts - 1 ? geoloop->verts[0]
: geoloop->verts[i + 1];
int64_t numHexesEstimate;
H3Error estimateErr =
lineHexEstimate(&origin, &destination, res, &numHexesEstimate);
if (estimateErr) {
return estimateErr;
}
for (int64_t j = 0; j < numHexesEstimate; j++) {
LatLng interpolate;
double invNumHexesEst = 1.0 / numHexesEstimate;
interpolate.lat =
(origin.lat * (numHexesEstimate - j) * invNumHexesEst) +
(destination.lat * j * invNumHexesEst);
interpolate.lng =
(origin.lng * (numHexesEstimate - j) * invNumHexesEst) +
(destination.lng * j * invNumHexesEst);
H3Index pointHex;
H3Error e = H3_EXPORT(latLngToCell)(&interpolate, res, &pointHex);
if (e) {
return e;
}
// A simple hash to store the hexagon, or move to another place if
// needed
int64_t loc = (int64_t)(pointHex % numHexagons);
int64_t loopCount = 0;
while (found[loc] != 0) {
// If this conditional is reached, the `found` memory block is
// too small for the given polygon. This should not happen.
// TODO: Reachable via fuzzer
if (loopCount > numHexagons) return E_FAILED;
if (found[loc] == pointHex)
break; // At least two points of the geoloop index to the
// same cell
loc = (loc + 1) % numHexagons;
loopCount++;
}
if (found[loc] == pointHex)
continue; // Skip this hex, already exists in the found hash
// Otherwise, set it in the found hash for now
found[loc] = pointHex;
search[*numSearchHexes] = pointHex;
(*numSearchHexes)++;
}
}
return E_SUCCESS;
}
/**
* polygonToCells takes a given GeoJSON-like data structure and preallocated,
* zeroed memory, and fills it with the hexagons that are contained by
* the GeoJSON-like data structure.
*
* This implementation traces the GeoJSON geoloop(s) in cartesian space with
* hexagons, tests them and their neighbors to be contained by the geoloop(s),
* and then any newly found hexagons are used to test again until no new
* hexagons are found.
*
* @param geoPolygon The geoloop and holes defining the relevant area
* @param res The Hexagon resolution (0-15)
* @param out The slab of zeroed memory to write to. Assumed to be big enough.
*/
H3Error H3_EXPORT(polygonToCells)(const GeoPolygon *geoPolygon, int res,
uint32_t flags, H3Index *out) {
H3Error flagErr = validatePolygonFlags(flags);
if (flagErr) {
return flagErr;
}
// One of the goals of the polygonToCells algorithm is that two adjacent
// polygons with zero overlap have zero overlapping hexagons. That the
// hexagons are uniquely assigned. There are a few approaches to take here,
// such as deciding based on which polygon has the greatest overlapping area
// of the hexagon, or the most number of contained points on the hexagon
// (using the center point as a tiebreaker).
//
// But if the polygons are convex, both of these more complex algorithms can
// be reduced down to checking whether or not the center of the hexagon is
// contained in the polygon, and so this is the approach that this
// polygonToCells algorithm will follow, as it's simpler, faster, and the
// error for concave polygons is still minimal (only affecting concave
// shapes on the order of magnitude of the hexagon size or smaller, not
// impacting larger concave shapes)
//
// This first part is identical to the maxPolygonToCellsSize above.
// Get the bounding boxes for the polygon and any holes
BBox *bboxes = H3_MEMORY(malloc)((geoPolygon->numHoles + 1) * sizeof(BBox));
if (!bboxes) {
return E_MEMORY_ALLOC;
}
bboxesFromGeoPolygon(geoPolygon, bboxes);
// Get the estimated number of hexagons and allocate some temporary memory
// for the hexagons
int64_t numHexagons;
H3Error numHexagonsError =
H3_EXPORT(maxPolygonToCellsSize)(geoPolygon, res, flags, &numHexagons);
if (numHexagonsError) {
H3_MEMORY(free)(bboxes);
return numHexagonsError;
}
H3Index *search = H3_MEMORY(calloc)(numHexagons, sizeof(H3Index));
if (!search) {
H3_MEMORY(free)(bboxes);
return E_MEMORY_ALLOC;
}
H3Index *found = H3_MEMORY(calloc)(numHexagons, sizeof(H3Index));
if (!found) {
H3_MEMORY(free)(bboxes);
H3_MEMORY(free)(search);
return E_MEMORY_ALLOC;
}
// Some metadata for tracking the state of the search and found memory
// blocks
int64_t numSearchHexes = 0;
int64_t numFoundHexes = 0;
// 1. Trace the hexagons along the polygon defining the outer geoloop and
// add them to the search hash. The hexagon containing the geoloop point
// may or may not be contained by the geoloop (as the hexagon's center
// point may be outside of the boundary.)
const GeoLoop geoloop = geoPolygon->geoloop;
H3Error edgeHexError = _getEdgeHexagons(&geoloop, numHexagons, res,
&numSearchHexes, search, found);
// If this branch is reached, we have exceeded the maximum number of
// hexagons possible and need to clean up the allocated memory.
// TODO: Reachable via fuzzer
if (edgeHexError) {
H3_MEMORY(free)(search);
H3_MEMORY(free)(found);
H3_MEMORY(free)(bboxes);
return edgeHexError;
}
// 2. Iterate over all holes, trace the polygons defining the holes with
// hexagons and add to only the search hash. We're going to temporarily use
// the `found` hash to use for dedupe purposes and then re-zero it once
// we're done here, otherwise we'd have to scan the whole set on each insert
// to make sure there's no duplicates, which is very inefficient.
for (int i = 0; i < geoPolygon->numHoles; i++) {
GeoLoop *hole = &(geoPolygon->holes[i]);
edgeHexError = _getEdgeHexagons(hole, numHexagons, res, &numSearchHexes,
search, found);
// If this branch is reached, we have exceeded the maximum number of
// hexagons possible and need to clean up the allocated memory.
// TODO: Reachable via fuzzer
if (edgeHexError) {
H3_MEMORY(free)(search);
H3_MEMORY(free)(found);
H3_MEMORY(free)(bboxes);
return edgeHexError;
}
}
// 3. Re-zero the found hash so it can be used in the main loop below
for (int64_t i = 0; i < numHexagons; i++) found[i] = H3_NULL;
// 4. Begin main loop. While the search hash is not empty do the following
while (numSearchHexes > 0) {
// Iterate through all hexagons in the current search hash, then loop
// through all neighbors and test Point-in-Poly, if point-in-poly
// succeeds, add to out and found hashes if not already there.
int64_t currentSearchNum = 0;
int64_t i = 0;
while (currentSearchNum < numSearchHexes) {
H3Index ring[MAX_ONE_RING_SIZE] = {0};
H3Index searchHex = search[i];
H3_EXPORT(gridDisk)(searchHex, 1, ring);
for (int j = 0; j < MAX_ONE_RING_SIZE; j++) {
if (ring[j] == H3_NULL) {
continue; // Skip if this was a pentagon and only had 5
// neighbors
}
H3Index hex = ring[j];
// A simple hash to store the hexagon, or move to another place
// if needed. This MUST be done before the point-in-poly check
// since that's far more expensive
int64_t loc = (int64_t)(hex % numHexagons);
int64_t loopCount = 0;
while (out[loc] != 0) {
// If this branch is reached, we have exceeded the maximum
// number of hexagons possible and need to clean up the
// allocated memory.
// TODO: Reachable via fuzzer
if (loopCount > numHexagons) {
H3_MEMORY(free)(search);
H3_MEMORY(free)(found);
H3_MEMORY(free)(bboxes);
return E_FAILED;
}
if (out[loc] == hex) break; // Skip duplicates found
loc = (loc + 1) % numHexagons;
loopCount++;
}
if (out[loc] == hex) {
continue; // Skip this hex, already exists in the out hash
}
// Check if the hexagon is in the polygon or not
LatLng hexCenter;
H3_EXPORT(cellToLatLng)(hex, &hexCenter);
// If not, skip
if (!pointInsidePolygon(geoPolygon, bboxes, &hexCenter)) {
continue;
}
// Otherwise set it in the output array
out[loc] = hex;
// Set the hexagon in the found hash
found[numFoundHexes] = hex;
numFoundHexes++;
}
currentSearchNum++;
i++;
}
// Swap the search and found pointers, copy the found hex count to the
// search hex count, and zero everything related to the found memory.
H3Index *temp = search;
search = found;
found = temp;
for (int64_t j = 0; j < numSearchHexes; j++) found[j] = 0;
numSearchHexes = numFoundHexes;
numFoundHexes = 0;
// Repeat until no new hexagons are found
}
// The out memory structure should be complete, end it here
H3_MEMORY(free)(bboxes);
H3_MEMORY(free)(search);
H3_MEMORY(free)(found);
return E_SUCCESS;
}
/**
* 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.
* @private
* @param h3Set Set of hexagons
* @param numHexes Number of hexagons in the set
* @param graph Output graph
*/
H3Error h3SetToVertexGraph(const H3Index *h3Set, const int numHexes,
VertexGraph *graph) {
CellBoundary vertices;
LatLng *fromVtx;
LatLng *toVtx;
VertexNode *edge;
if (numHexes < 1) {
// We still need to init the graph, or calls to destroyVertexGraph will
// fail
initVertexGraph(graph, 0, 0);
return E_SUCCESS;
}
int res = H3_GET_RESOLUTION(h3Set[0]);
const int minBuckets = 6;
// TODO: Better way to calculate/guess?
int numBuckets = numHexes > minBuckets ? numHexes : minBuckets;
initVertexGraph(graph, numBuckets, res);
// Iterate through every hexagon
for (int i = 0; i < numHexes; i++) {
H3Error boundaryErr = H3_EXPORT(cellToBoundary)(h3Set[i], &vertices);
if (boundaryErr) {
// Destroy vertex graph as caller will not know to do so.
destroyVertexGraph(graph);
return boundaryErr;
}
// iterate through every edge
for (int j = 0; j < vertices.numVerts; j++) {
fromVtx = &vertices.verts[j];
toVtx = &vertices.verts[(j + 1) % vertices.numVerts];
// If we've seen this edge already, it will be reversed
edge = findNodeForEdge(graph, toVtx, fromVtx);
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.
removeVertexNode(graph, edge);
} else {
// Add a new node for this edge
addVertexNode(graph, fromVtx, toVtx);
}
}
}
return E_SUCCESS;
}
/**
* Internal: Create a LinkedGeoPolygon from a vertex graph. It is the
* responsibility of the caller to call destroyLinkedMultiPolygon on the
* populated linked geo structure, or the memory for that structure will not be
* freed.
* @private
* @param graph Input graph
* @param out Output polygon
*/
void _vertexGraphToLinkedGeo(VertexGraph *graph, LinkedGeoPolygon *out) {
*out = (LinkedGeoPolygon){0};
LinkedGeoLoop *loop;
VertexNode *edge;
LatLng nextVtx;
// Find the next unused entry point
while ((edge = firstVertexNode(graph)) != NULL) {
loop = addNewLinkedLoop(out);
// Walk the graph to get the outline
do {
addLinkedCoord(loop, &edge->from);
nextVtx = edge->to;
// Remove frees the node, so we can't use edge after this
removeVertexNode(graph, edge);
edge = findNodeForVertex(graph, &nextVtx);
} while (edge);
}
}
/**
* Create a LinkedGeoPolygon describing the outline(s) of a set of hexagons.
* Polygon outlines will follow GeoJSON MultiPolygon order: Each polygon will
* have one outer loop, which is first in the list, followed by any holes.
*
* It is the responsibility of the caller to call destroyLinkedMultiPolygon on
* the populated linked geo structure, or the memory for that structure will not
* be freed.
*
* It is expected that all hexagons in the set have the same resolution and
* that the set contains no duplicates. Behavior is undefined if duplicates
* or multiple resolutions are present, and the algorithm may produce
* unexpected or invalid output.
*
* @param h3Set Set of hexagons
* @param numHexes Number of hexagons in set
* @param out Output polygon
*/
H3Error H3_EXPORT(cellsToLinkedMultiPolygon)(const H3Index *h3Set,
const int numHexes,
LinkedGeoPolygon *out) {
VertexGraph graph;
H3Error err = h3SetToVertexGraph(h3Set, numHexes, &graph);
if (err) {
return err;
}
_vertexGraphToLinkedGeo(&graph, out);
destroyVertexGraph(&graph);
H3Error normalizeResult = normalizeMultiPolygon(out);
if (normalizeResult) {
H3_EXPORT(destroyLinkedMultiPolygon)(out);
}
return normalizeResult;
}