diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e5d68a53..4388a7835 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,15 @@ The public API of this library consists of the functions declared in file [h3api.h](./src/h3lib/include/h3api.h). ## [Unreleased] -### Fixed -- Added #include to benchmark.h ### Added +- `experimentalH3ToLocalIj` function for getting local coordinates for an index. (#102) +- `experimentalLocalIjToH3` function for getting an index from local coordinates. (#102) - Benchmarks for the kRing method for k's of size 10, 20, 30, and 40. +### Changed +- Internal `h3ToIjk` function renamed to `h3ToLocalIjk`. (#102) +- `h3ToIjk` filter application replaced with `h3ToLocalIj`. (#102) +### Fixed +- Added `#include ` to `benchmark.h` (#142) ## [3.1.1] - 2018-08-29 ### Fixed diff --git a/CMakeLists.txt b/CMakeLists.txt index df9b872f5..18233cc82 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,7 @@ set(LIB_SOURCE_FILES src/h3lib/include/vec2d.h src/h3lib/include/vec3d.h src/h3lib/include/linkedGeo.h + src/h3lib/include/localij.h src/h3lib/include/baseCells.h src/h3lib/include/faceijk.h src/h3lib/include/vertexGraph.h @@ -108,6 +109,7 @@ set(LIB_SOURCE_FILES src/h3lib/lib/vec2d.c src/h3lib/lib/vec3d.c src/h3lib/lib/linkedGeo.c + src/h3lib/lib/localij.c src/h3lib/lib/geoCoord.c src/h3lib/lib/h3UniEdge.c src/h3lib/lib/mathExtensions.c @@ -130,7 +132,7 @@ set(EXAMPLE_SOURCE_FILES examples/edge.c) set(OTHER_SOURCE_FILES src/apps/filters/h3ToGeo.c - src/apps/filters/h3ToIjk.c + src/apps/filters/h3ToLocalIj.c src/apps/filters/h3ToComponents.c src/apps/filters/geoToH3.c src/apps/filters/h3ToGeoBoundary.c @@ -162,7 +164,9 @@ set(OTHER_SOURCE_FILES src/apps/testapps/mkRandGeo.c src/apps/testapps/testH3Api.c src/apps/testapps/testH3SetToLinkedGeo.c - src/apps/testapps/testH3ToIjk.c + src/apps/testapps/testH3ToLocalIj.c + src/apps/testapps/testH3Distance.c + src/apps/testapps/testCoordIj.c src/apps/miscapps/h3ToGeoBoundaryHier.c src/apps/miscapps/h3ToGeoHier.c src/apps/miscapps/generateBaseCellNeighbors.c @@ -288,7 +292,7 @@ endmacro() add_h3_executable(geoToH3 src/apps/filters/geoToH3.c ${APP_SOURCE_FILES}) add_h3_executable(h3ToComponents src/apps/filters/h3ToComponents.c ${APP_SOURCE_FILES}) add_h3_executable(h3ToGeo src/apps/filters/h3ToGeo.c ${APP_SOURCE_FILES}) -add_h3_executable(h3ToIjk src/apps/filters/h3ToIjk.c ${APP_SOURCE_FILES}) +add_h3_executable(h3ToLocalIj src/apps/filters/h3ToLocalIj.c ${APP_SOURCE_FILES}) add_h3_executable(h3ToGeoBoundary src/apps/filters/h3ToGeoBoundary.c ${APP_SOURCE_FILES}) add_h3_executable(hexRange src/apps/filters/hexRange.c ${APP_SOURCE_FILES}) add_h3_executable(kRing src/apps/filters/kRing.c ${APP_SOURCE_FILES}) @@ -460,7 +464,9 @@ if(BUILD_TESTING) add_h3_test(testPolygon src/apps/testapps/testPolygon.c) add_h3_test(testVec2d src/apps/testapps/testVec2d.c) add_h3_test(testVec3d src/apps/testapps/testVec3d.c) - add_h3_test(testH3ToIjk src/apps/testapps/testH3ToIjk.c) + add_h3_test(testH3ToLocalIj src/apps/testapps/testH3ToLocalIj.c) + add_h3_test(testH3Distance src/apps/testapps/testH3Distance.c) + add_h3_test(testCoordIj src/apps/testapps/testCoordIj.c) add_h3_test_with_arg(testH3NeighborRotations src/apps/testapps/testH3NeighborRotations.c 0) add_h3_test_with_arg(testH3NeighborRotations src/apps/testapps/testH3NeighborRotations.c 1) diff --git a/src/apps/filters/h3ToIjk.c b/src/apps/filters/h3ToLocalIj.c similarity index 78% rename from src/apps/filters/h3ToIjk.c rename to src/apps/filters/h3ToLocalIj.c index b03a5a956..52dae15ec 100644 --- a/src/apps/filters/h3ToIjk.c +++ b/src/apps/filters/h3ToLocalIj.c @@ -14,18 +14,21 @@ * limitations under the License. */ /** @file - * @brief stdin/stdout filter that converts from H3 indexes to relative IJK + * @brief stdin/stdout filter that converts from H3 indexes to local IJ * coordinates. This is experimental. * - * usage: `h3ToIjk [origin]` + * usage: `h3ToLocalIj [origin]` * * The program reads H3 indexes from stdin and outputs the corresponding - * IJK coordinates to stdout, until EOF is encountered. The H3 indexes - * should be in integer form. `-1 -1 -1` is printed if the IJK coordinates + * IJ coordinates to stdout, until EOF is encountered. The H3 indexes + * should be in integer form. `NA` is printed if the IJ coordinates * could not be obtained. * - * `origin` indicates the origin (or anchoring) index for the IJK coordinate + * `origin` indicates the origin (or anchoring) index for the IJ coordinate * space. + * + * This program has the same limitations as the `experimentalH3ToLocalIj` + * function. */ #include @@ -37,11 +40,11 @@ #include "utility.h" void doCell(H3Index h, H3Index origin) { - CoordIJK ijk; - if (h3ToIjk(origin, h, &ijk)) { - printf("-1 -1 -1\n"); + CoordIJ ij; + if (H3_EXPORT(experimentalH3ToLocalIj)(origin, h, &ij)) { + printf("NA\n"); } else { - printf("%d %d %d\n", ijk.i, ijk.j, ijk.k); + printf("%d %d\n", ij.i, ij.j); } } diff --git a/src/apps/testapps/testCoordIj.c b/src/apps/testapps/testCoordIj.c new file mode 100644 index 000000000..87d08e58b --- /dev/null +++ b/src/apps/testapps/testCoordIj.c @@ -0,0 +1,65 @@ +/* + * Copyright 2018 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 + * @brief tests IJ grid functions and IJK distance functions. + * + * usage: `testCoordIj` + */ + +#include +#include +#include +#include "algos.h" +#include "baseCells.h" +#include "constants.h" +#include "h3Index.h" +#include "h3api.h" +#include "localij.h" +#include "stackAlloc.h" +#include "test.h" +#include "utility.h" + +SUITE(coordIj) { + TEST(ijkToIj_zero) { + CoordIJK ijk = {0}; + CoordIJ ij = {0}; + + ijkToIj(&ijk, &ij); + t_assert(ij.i == 0, "ij.i zero"); + t_assert(ij.j == 0, "ij.j zero"); + + ijToIjk(&ij, &ijk); + t_assert(ijk.i == 0, "ijk.i zero"); + t_assert(ijk.j == 0, "ijk.j zero"); + t_assert(ijk.k == 0, "ijk.k zero"); + } + + TEST(ijkToIj_roundtrip) { + for (Direction dir = CENTER_DIGIT; dir < NUM_DIGITS; dir++) { + CoordIJK ijk = {0}; + _neighbor(&ijk, dir); + + CoordIJ ij = {0}; + ijkToIj(&ijk, &ij); + + CoordIJK recovered = {0}; + ijToIjk(&ij, &recovered); + + t_assert(_ijkMatches(&ijk, &recovered), + "got same ijk coordinates back"); + } + } +} diff --git a/src/apps/testapps/testH3ToIjk.c b/src/apps/testapps/testH3Distance.c similarity index 63% rename from src/apps/testapps/testH3ToIjk.c rename to src/apps/testapps/testH3Distance.c index 934174d1b..27803cb85 100644 --- a/src/apps/testapps/testH3ToIjk.c +++ b/src/apps/testapps/testH3Distance.c @@ -14,9 +14,9 @@ * limitations under the License. */ /** @file - * @brief tests H3 index to IJK+ grid functions. + * @brief tests H3 distance function. * - * usage: `testH3ToIjk` + * usage: `testH3Distance` */ #include @@ -27,6 +27,7 @@ #include "constants.h" #include "h3Index.h" #include "h3api.h" +#include "localij.h" #include "stackAlloc.h" #include "test.h" #include "utility.h" @@ -34,59 +35,12 @@ static const int MAX_DISTANCES[] = {1, 2, 5, 12, 19, 26}; static void h3Distance_identity_assertions(H3Index h3) { - int r = H3_GET_RESOLUTION(h3); - t_assert(H3_EXPORT(h3Distance)(h3, h3) == 0, "distance to self is 0"); - - CoordIJK ijk; - t_assert(h3ToIjk(h3, h3, &ijk) == 0, "failed to get ijk"); - if (r == 0) { - t_assert(_ijkMatches(&ijk, &UNIT_VECS[0]) == 1, "not at 0,0,0 (res 0)"); - } else if (r == 1) { - t_assert(_ijkMatches(&ijk, &UNIT_VECS[H3_GET_INDEX_DIGIT(h3, 1)]) == 1, - "not at expected coordinates (res 1)"); - } else if (r == 2) { - CoordIJK expected = UNIT_VECS[H3_GET_INDEX_DIGIT(h3, 1)]; - _downAp7r(&expected); - _neighbor(&expected, H3_GET_INDEX_DIGIT(h3, 2)); - t_assert(_ijkMatches(&ijk, &expected) == 1, - "not at expected coordinates (res 2)"); - } else { - t_assert(0, "wrong resolution"); - } -} - -static void h3Distance_neighbors_assertions(H3Index h3) { - CoordIJK origin = {0}; - t_assert(h3ToIjk(h3, h3, &origin) == 0, "got ijk for origin"); - - for (int d = 1; d < 7; d++) { - if (d == 1 && H3_EXPORT(h3IsPentagon)(h3)) { - continue; - } - - int rotations = 0; - H3Index offset = h3NeighborRotations(h3, d, &rotations); - - CoordIJK ijk = {0}; - t_assert(h3ToIjk(h3, offset, &ijk) == 0, "got ijk for destination"); - CoordIJK invertedIjk = {0}; - _neighbor(&invertedIjk, d); - for (int i = 0; i < 3; i++) { - _ijkRotate60ccw(&invertedIjk); - } - _ijkAdd(&invertedIjk, &ijk, &ijk); - _ijkNormalize(&ijk); - - t_assert(_ijkMatches(&ijk, &origin), "back to origin"); - } } static void h3Distance_kRing_assertions(H3Index h3) { int r = H3_GET_RESOLUTION(h3); - if (r > 5) { - t_assert(false, "wrong res"); - } + t_assert(r <= 5, "resolution supported by test function (kRing)"); int maxK = MAX_DISTANCES[r]; int sz = H3_EXPORT(maxKringSize)(maxK); @@ -109,7 +63,21 @@ static void h3Distance_kRing_assertions(H3Index h3) { } } -SUITE(h3ToIjk) { +SUITE(h3Distance) { + // Some indexes that represent base cells. Base cells + // are hexagons except for `pent1`. + H3Index bc1 = H3_INIT; + setH3Index(&bc1, 0, 15, 0); + + H3Index bc2 = H3_INIT; + setH3Index(&bc2, 0, 8, 0); + + H3Index bc3 = H3_INIT; + setH3Index(&bc3, 0, 31, 0); + + H3Index pent1 = H3_INIT; + setH3Index(&pent1, 0, 4, 0); + TEST(testIndexDistance) { H3Index bc = 0; setH3Index(&bc, 1, 17, 0); @@ -148,63 +116,22 @@ SUITE(h3ToIjk) { "distance in res 2 across pentagon (reversed)"); } - TEST(ijkDistance) { - CoordIJK z = {0, 0, 0}; - CoordIJK i = {1, 0, 0}; - CoordIJK ik = {1, 0, 1}; - CoordIJK ij = {1, 1, 0}; - CoordIJK j2 = {0, 2, 0}; - - t_assert(ijkDistance(&z, &z) == 0, "identity distance 0,0,0"); - t_assert(ijkDistance(&i, &i) == 0, "identity distance 1,0,0"); - t_assert(ijkDistance(&ik, &ik) == 0, "identity distance 1,0,1"); - t_assert(ijkDistance(&ij, &ij) == 0, "identity distance 1,1,0"); - t_assert(ijkDistance(&j2, &j2) == 0, "identity distance 0,2,0"); - - t_assert(ijkDistance(&z, &i) == 1, "0,0,0 to 1,0,0"); - t_assert(ijkDistance(&z, &j2) == 2, "0,0,0 to 0,2,0"); - t_assert(ijkDistance(&z, &ik) == 1, "0,0,0 to 1,0,1"); - t_assert(ijkDistance(&i, &ik) == 1, "1,0,0 to 1,0,1"); - t_assert(ijkDistance(&ik, &j2) == 3, "1,0,1 to 0,2,0"); - t_assert(ijkDistance(&ij, &ik) == 2, "1,0,1 to 1,1,0"); - } - TEST(h3Distance_identity) { iterateAllIndexesAtRes(0, h3Distance_identity_assertions); iterateAllIndexesAtRes(1, h3Distance_identity_assertions); iterateAllIndexesAtRes(2, h3Distance_identity_assertions); } - TEST(h3Distance_neighbors) { - iterateAllIndexesAtRes(0, h3Distance_neighbors_assertions); - iterateAllIndexesAtRes(1, h3Distance_neighbors_assertions); - iterateAllIndexesAtRes(2, h3Distance_neighbors_assertions); - } - TEST(h3Distance_kRing) { iterateAllIndexesAtRes(0, h3Distance_kRing_assertions); iterateAllIndexesAtRes(1, h3Distance_kRing_assertions); iterateAllIndexesAtRes(2, h3Distance_kRing_assertions); // Don't iterate all of res 3, to save time iterateAllIndexesAtResPartial(3, h3Distance_kRing_assertions, 27); - // These would take too long, even at partial execution - // iterateAllIndexesAtResPartial(4, h3Distance_kRing_assertions, 20); - // iterateAllIndexesAtResPartial(5, h3Distance_kRing_assertions, 20); + // Further resolutions aren't tested to save time. } TEST(h3DistanceBaseCells) { - H3Index bc1 = H3_INIT; - setH3Index(&bc1, 0, 15, 0); - - H3Index bc2 = H3_INIT; - setH3Index(&bc2, 0, 8, 0); - - H3Index bc3 = H3_INIT; - setH3Index(&bc3, 0, 31, 0); - - H3Index pent1 = H3_INIT; - setH3Index(&pent1, 0, 4, 0); - t_assert(H3_EXPORT(h3Distance)(bc1, pent1) == 1, "distance to neighbor is 1 (15, 4)"); t_assert(H3_EXPORT(h3Distance)(bc1, bc2) == 1, @@ -213,23 +140,49 @@ SUITE(h3ToIjk) { "distance to neighbor is 1 (15, 31)"); t_assert(H3_EXPORT(h3Distance)(pent1, bc3) == -1, "distance to neighbor is invalid"); + } + + TEST(ijkDistance) { + CoordIJK z = {0, 0, 0}; + CoordIJK i = {1, 0, 0}; + CoordIJK ik = {1, 0, 1}; + CoordIJK ij = {1, 1, 0}; + CoordIJK j2 = {0, 2, 0}; + + t_assert(ijkDistance(&z, &z) == 0, "identity distance 0,0,0"); + t_assert(ijkDistance(&i, &i) == 0, "identity distance 1,0,0"); + t_assert(ijkDistance(&ik, &ik) == 0, "identity distance 1,0,1"); + t_assert(ijkDistance(&ij, &ij) == 0, "identity distance 1,1,0"); + t_assert(ijkDistance(&j2, &j2) == 0, "identity distance 0,2,0"); + + t_assert(ijkDistance(&z, &i) == 1, "0,0,0 to 1,0,0"); + t_assert(ijkDistance(&z, &j2) == 2, "0,0,0 to 0,2,0"); + t_assert(ijkDistance(&z, &ik) == 1, "0,0,0 to 1,0,1"); + t_assert(ijkDistance(&i, &ik) == 1, "1,0,0 to 1,0,1"); + t_assert(ijkDistance(&ik, &j2) == 3, "1,0,1 to 0,2,0"); + t_assert(ijkDistance(&ij, &ik) == 2, "1,0,1 to 1,1,0"); + } - CoordIJK ijk; - t_assert(h3ToIjk(pent1, bc1, &ijk) == 0, "failed to get ijk (4, 15)"); - t_assert(_ijkMatches(&ijk, &UNIT_VECS[2]) == 1, "not at 0,1,0"); + TEST(h3DistanceResolutionMismatch) { + t_assert( + H3_EXPORT(h3Distance)(0x832830fffffffffL, 0x822837fffffffffL) == -1, + "cannot compare at different resolutions"); } - TEST(h3DistanceFailed) { - H3Index h3 = 0x832830fffffffffL; - H3Index edge = - H3_EXPORT(getH3UnidirectionalEdge(h3, 0x832834fffffffffL)); - H3Index h3res2 = 0x822837fffffffffL; - - t_assert(H3_EXPORT(h3Distance)(edge, h3) == -1, - "edges cannot be origins"); - t_assert(H3_EXPORT(h3Distance)(h3, edge) == -1, - "edges cannot be destinations"); - t_assert(H3_EXPORT(h3Distance)(h3, h3res2) == -1, - "cannot compare at different resolutions"); + TEST(h3DistanceEdge) { + H3Index origin = 0x832830fffffffffL; + H3Index dest = 0x832834fffffffffL; + H3Index edge = H3_EXPORT(getH3UnidirectionalEdge(origin, dest)); + + t_assert(0 != edge, "test edge is valid"); + t_assert(H3_EXPORT(h3Distance)(edge, origin) == 0, + "edge has zero distance to origin"); + t_assert(H3_EXPORT(h3Distance)(origin, edge) == 0, + "origin has zero distance to edge"); + + t_assert(H3_EXPORT(h3Distance)(edge, dest) == 1, + "edge has distance to destination"); + t_assert(H3_EXPORT(h3Distance)(edge, dest) == 1, + "destination has distance to edge"); } } diff --git a/src/apps/testapps/testH3ToLocalIj.c b/src/apps/testapps/testH3ToLocalIj.c new file mode 100644 index 000000000..e1526bba6 --- /dev/null +++ b/src/apps/testapps/testH3ToLocalIj.c @@ -0,0 +1,349 @@ +/* + * Copyright 2018 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 + * @brief tests H3 index to local IJ and IJK+ grid functions. + * + * usage: `testH3ToLocalIj` + */ + +#include +#include +#include +#include +#include "algos.h" +#include "baseCells.h" +#include "constants.h" +#include "h3Index.h" +#include "h3api.h" +#include "localij.h" +#include "stackAlloc.h" +#include "test.h" +#include "utility.h" + +static const int MAX_DISTANCES[] = {1, 2, 5, 12, 19, 26}; + +// The same traversal constants from algos.c (for hexRange) here reused as local +// IJ vectors. +static const CoordIJ DIRECTIONS[6] = {{0, 1}, {-1, 0}, {-1, -1}, + {0, -1}, {1, 0}, {1, 1}}; + +static const CoordIJ NEXT_RING_DIRECTION = {1, 0}; + +/** + * Test that the local coordinates for an index map to itself. + */ +void localIjToH3_identity_assertions(H3Index h3) { + CoordIJ ij; + t_assert(H3_EXPORT(experimentalH3ToLocalIj)(h3, h3, &ij) == 0, + "able to setup localIjToH3 test"); + + H3Index retrieved; + t_assert(H3_EXPORT(experimentalLocalIjToH3)(h3, &ij, &retrieved) == 0, + "got an index back from localIjTOh3"); + t_assert(h3 == retrieved, "round trip through local IJ space works"); +} + +/** + * Test that coordinates for an index match some simple rules about index + * digits, when using the index as its own origin. That is, that the IJ + * coordinates are in the coordinate space of the origin's base cell. + */ +void h3ToLocalIj_coordinates_assertions(H3Index h3) { + int r = H3_GET_RESOLUTION(h3); + + CoordIJ ij; + t_assert(H3_EXPORT(experimentalH3ToLocalIj)(h3, h3, &ij) == 0, + "get ij for origin"); + CoordIJK ijk; + ijToIjk(&ij, &ijk); + if (r == 0) { + t_assert(_ijkMatches(&ijk, &UNIT_VECS[0]) == 1, "res 0 cell at 0,0,0"); + } else if (r == 1) { + t_assert(_ijkMatches(&ijk, &UNIT_VECS[H3_GET_INDEX_DIGIT(h3, 1)]) == 1, + "res 1 cell at expected coordinates"); + } else if (r == 2) { + CoordIJK expected = UNIT_VECS[H3_GET_INDEX_DIGIT(h3, 1)]; + _downAp7r(&expected); + _neighbor(&expected, H3_GET_INDEX_DIGIT(h3, 2)); + t_assert(_ijkMatches(&ijk, &expected) == 1, + "res 2 cell at expected coordinates"); + } else { + t_assert(0, "resolution supported by test function (coordinates)"); + } +} + +/** + * Test the the immediate neighbors of an index are at the expected locations in + * the local IJ coordinate space. + */ +void h3ToLocalIj_neighbors_assertions(H3Index h3) { + CoordIJ origin = {0}; + t_assert(H3_EXPORT(experimentalH3ToLocalIj)(h3, h3, &origin) == 0, + "got ij for origin"); + CoordIJK originIjk; + ijToIjk(&origin, &originIjk); + + for (Direction d = K_AXES_DIGIT; d < INVALID_DIGIT; d++) { + if (d == K_AXES_DIGIT && H3_EXPORT(h3IsPentagon)(h3)) { + continue; + } + + int rotations = 0; + H3Index offset = h3NeighborRotations(h3, d, &rotations); + + CoordIJ ij = {0}; + t_assert(H3_EXPORT(experimentalH3ToLocalIj)(h3, offset, &ij) == 0, + "got ij for destination"); + CoordIJK ijk; + ijToIjk(&ij, &ijk); + CoordIJK invertedIjk = {0}; + _neighbor(&invertedIjk, d); + for (int i = 0; i < 3; i++) { + _ijkRotate60ccw(&invertedIjk); + } + _ijkAdd(&invertedIjk, &ijk, &ijk); + _ijkNormalize(&ijk); + + t_assert(_ijkMatches(&ijk, &originIjk), "back to origin"); + } +} + +/** + * Test that the neighbors (k-ring), if they can be found in the local IJ + * coordinate space, can be converetd back to indexes. + */ +void localIjToH3_kRing_assertions(H3Index h3) { + int r = H3_GET_RESOLUTION(h3); + t_assert(r <= 5, "resolution supported by test function (kRing)"); + int maxK = MAX_DISTANCES[r]; + + int sz = H3_EXPORT(maxKringSize)(maxK); + STACK_ARRAY_CALLOC(H3Index, neighbors, sz); + STACK_ARRAY_CALLOC(int, distances, sz); + + H3_EXPORT(kRingDistances)(h3, maxK, neighbors, distances); + + for (int i = 0; i < sz; i++) { + if (neighbors[i] == 0) { + continue; + } + + CoordIJ ij; + // Don't consider indexes which we can't unfold in the first place + if (H3_EXPORT(experimentalH3ToLocalIj)(h3, neighbors[i], &ij) == 0) { + H3Index retrieved; + t_assert( + H3_EXPORT(experimentalLocalIjToH3)(h3, &ij, &retrieved) == 0, + "retrieved index for unfolded coordinates"); + t_assert(retrieved == neighbors[i], + "round trip neighboring index matches expected"); + } + } +} + +void localIjToH3_traverse_assertions(H3Index h3) { + int r = H3_GET_RESOLUTION(h3); + t_assert(r <= 5, "resolution supported by test function (traverse)"); + int k = MAX_DISTANCES[r]; + + CoordIJ ij; + t_assert(H3_EXPORT(experimentalH3ToLocalIj)(h3, h3, &ij) == 0, + "Got origin coordinates"); + + // This logic is from hexRangeDistances. + // 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; + + while (ring <= k) { + if (direction == 0 && i == 0) { + ij.i += NEXT_RING_DIRECTION.i; + ij.j += NEXT_RING_DIRECTION.j; + } + + ij.i += DIRECTIONS[direction].i; + ij.j += DIRECTIONS[direction].j; + + H3Index testH3; + + int failed = H3_EXPORT(experimentalLocalIjToH3)(h3, &ij, &testH3); + if (!failed) { + t_assert(H3_EXPORT(h3IsValid)(testH3), + "test coordinates result in valid index"); + if (_isBaseCellPentagon(H3_EXPORT(h3GetBaseCell)(testH3))) { + // h3IsValid doesn't test for indexes representing part of the + // deleted subsequence. + t_assert( + _h3LeadingNonZeroDigit(testH3) != K_AXES_DIGIT, + "index is in a valid subsequence on a pentagon base cell."); + } + + CoordIJ expectedIj; + int reverseFailed = + H3_EXPORT(experimentalH3ToLocalIj)(h3, testH3, &expectedIj); + // If it doesn't give a coordinate for this origin,index pair that's + // OK. + if (!reverseFailed) { + if (expectedIj.i != ij.i || expectedIj.j != ij.j) { + // Multiple coordinates for the same index can happen due to + // pentagon distortion. In that case, the other coordinates + // should also belong to the same index. + H3Index testTestH3; + t_assert(H3_EXPORT(experimentalLocalIjToH3)( + h3, &expectedIj, &testTestH3) == 0, + "converted coordinates again"); + t_assert(testH3 == testTestH3, + "index has normalizable coordinates in " + "local IJ"); + } + } + } + + 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++; + } + } + } +} + +SUITE(h3ToLocalIj) { + // Some indexes that represent base cells. Base cells + // are hexagons except for `pent1`. + H3Index bc1 = H3_INIT; + setH3Index(&bc1, 0, 15, 0); + + H3Index bc2 = H3_INIT; + setH3Index(&bc2, 0, 8, 0); + + H3Index bc3 = H3_INIT; + setH3Index(&bc3, 0, 31, 0); + + H3Index pent1 = H3_INIT; + setH3Index(&pent1, 0, 4, 0); + + TEST(localIjToH3_identity) { + iterateAllIndexesAtRes(0, localIjToH3_identity_assertions); + iterateAllIndexesAtRes(1, localIjToH3_identity_assertions); + iterateAllIndexesAtRes(2, localIjToH3_identity_assertions); + } + + TEST(h3ToLocalIj_coordinates) { + iterateAllIndexesAtRes(0, h3ToLocalIj_coordinates_assertions); + iterateAllIndexesAtRes(1, h3ToLocalIj_coordinates_assertions); + iterateAllIndexesAtRes(2, h3ToLocalIj_coordinates_assertions); + } + + TEST(h3ToLocalIj_neighbors) { + iterateAllIndexesAtRes(0, h3ToLocalIj_neighbors_assertions); + iterateAllIndexesAtRes(1, h3ToLocalIj_neighbors_assertions); + iterateAllIndexesAtRes(2, h3ToLocalIj_neighbors_assertions); + } + + TEST(localIjToH3_kRing) { + iterateAllIndexesAtRes(0, localIjToH3_kRing_assertions); + iterateAllIndexesAtRes(1, localIjToH3_kRing_assertions); + iterateAllIndexesAtRes(2, localIjToH3_kRing_assertions); + // Don't iterate all of res 3, to save time + iterateAllIndexesAtResPartial(3, localIjToH3_kRing_assertions, 27); + // Further resolutions aren't tested to save time. + } + + TEST(localIjToH3_traverse) { + iterateAllIndexesAtRes(0, localIjToH3_traverse_assertions); + iterateAllIndexesAtRes(1, localIjToH3_traverse_assertions); + iterateAllIndexesAtRes(2, localIjToH3_traverse_assertions); + // Don't iterate all of res 3, to save time + iterateAllIndexesAtResPartial(3, localIjToH3_traverse_assertions, 27); + // Further resolutions aren't tested to save time. + } + + TEST(ijkBaseCells) { + CoordIJK ijk; + t_assert(h3ToLocalIjk(pent1, bc1, &ijk) == 0, + "got ijk for base cells 4 and 15"); + t_assert(_ijkMatches(&ijk, &UNIT_VECS[2]) == 1, + "neighboring base cell at 0,1,0"); + } + + TEST(ijBaseCells) { + CoordIJ ij = {.i = 0, .j = 0}; + H3Index origin = 0x8029fffffffffff; + H3Index retrieved; + t_assert( + H3_EXPORT(experimentalLocalIjToH3)(origin, &ij, &retrieved) == 0, + "got origin back"); + t_assert(retrieved == 0x8029fffffffffff, "origin matches self"); + ij.i = 1; + t_assert( + H3_EXPORT(experimentalLocalIjToH3)(origin, &ij, &retrieved) == 0, + "got offset index"); + t_assert(retrieved == 0x8051fffffffffff, + "modified index matches expected"); + ij.i = 2; + t_assert( + H3_EXPORT(experimentalLocalIjToH3)(origin, &ij, &retrieved) != 0, + "out of range base cell"); + } + + TEST(ijOutOfRange) { + const int numCoords = 5; + const CoordIJ coords[] = {{0, 0}, {1, 0}, {2, 0}, {3, 0}, {4, 0}}; + const H3Index expected[] = {0x81283ffffffffff, 0x81293ffffffffff, + 0x8150bffffffffff, 0x8151bffffffffff, + H3_INVALID_INDEX}; + + for (int i = 0; i < numCoords; i++) { + H3Index result; + const int err = H3_EXPORT(experimentalLocalIjToH3)( + expected[0], &coords[i], &result); + if (expected[i] == H3_INVALID_INDEX) { + t_assert(err != 0, "coordinates out of range"); + } else { + t_assert(err == 0, "coordinates in range"); + t_assert(result == expected[i], "result matches expectation"); + } + } + } + + TEST(experimentalH3ToLocalIjFailed) { + CoordIJ ij; + + t_assert(H3_EXPORT(experimentalH3ToLocalIj)(bc1, bc1, &ij) == 0, + "found IJ (1)"); + t_assert(ij.i == 0 && ij.j == 0, "ij correct (1)"); + t_assert(H3_EXPORT(experimentalH3ToLocalIj)(bc1, pent1, &ij) == 0, + "found IJ (2)"); + t_assert(ij.i == 1 && ij.j == 0, "ij correct (2)"); + t_assert(H3_EXPORT(experimentalH3ToLocalIj)(bc1, bc2, &ij) == 0, + "found IJ (3)"); + t_assert(ij.i == 0 && ij.j == -1, "ij correct (3)"); + t_assert(H3_EXPORT(experimentalH3ToLocalIj)(bc1, bc3, &ij) == 0, + "found IJ (4)"); + t_assert(ij.i == -1 && ij.j == 0, "ij correct (4)"); + t_assert(H3_EXPORT(experimentalH3ToLocalIj)(pent1, bc3, &ij) != 0, + "found IJ (5)"); + } +} diff --git a/src/h3lib/include/baseCells.h b/src/h3lib/include/baseCells.h index 97031184b..eadf3cf27 100644 --- a/src/h3lib/include/baseCells.h +++ b/src/h3lib/include/baseCells.h @@ -47,6 +47,7 @@ extern const BaseCellData baseCellData[NUM_BASE_CELLS]; // Internal functions int _isBaseCellPentagon(int baseCell); +bool _isBaseCellPolarPentagon(int baseCell); int _faceIjkToBaseCell(const FaceIJK* h); int _faceIjkToBaseCellCCWrot60(const FaceIJK* h); void _baseCellToFaceIjk(int baseCell, FaceIJK* h); diff --git a/src/h3lib/include/coordijk.h b/src/h3lib/include/coordijk.h index b25c57fff..565f8d74c 100644 --- a/src/h3lib/include/coordijk.h +++ b/src/h3lib/include/coordijk.h @@ -31,10 +31,13 @@ #define COORDIJK_H #include "geoCoord.h" +#include "h3api.h" #include "vec2d.h" /** @struct CoordIJK * @brief IJK hexagon coordinates + * + * Each axis is spaced 120 degrees apart. */ typedef struct { int i; ///< i component @@ -102,5 +105,7 @@ void _ijkRotate60cw(CoordIJK* ijk); Direction _rotate60ccw(Direction digit); Direction _rotate60cw(Direction digit); int ijkDistance(const CoordIJK* a, const CoordIJK* b); +void ijkToIj(const CoordIJK* ijk, CoordIJ* ij); +void ijToIjk(const CoordIJ* ij, CoordIJK* ijk); #endif diff --git a/src/h3lib/include/h3Index.h b/src/h3lib/include/h3Index.h index bc1593aac..9b7fc913e 100644 --- a/src/h3lib/include/h3Index.h +++ b/src/h3lib/include/h3Index.h @@ -152,11 +152,12 @@ int isResClassIII(int res); // Internal functions +int _h3ToFaceIjkWithInitializedFijk(H3Index h, FaceIJK* fijk); H3Index _faceIjkToH3(const FaceIJK* fijk, int res); Direction _h3LeadingNonZeroDigit(H3Index h); H3Index _h3RotatePent60ccw(H3Index h); +H3Index _h3RotatePent60cw(H3Index h); H3Index _h3Rotate60ccw(H3Index h); H3Index _h3Rotate60cw(H3Index h); -int h3ToIjk(H3Index origin, H3Index h3, CoordIJK* out); #endif diff --git a/src/h3lib/include/h3api.h b/src/h3lib/include/h3api.h index 7a847fb00..cbc859ec5 100644 --- a/src/h3lib/include/h3api.h +++ b/src/h3lib/include/h3api.h @@ -129,6 +129,16 @@ struct LinkedGeoPolygon { LinkedGeoPolygon *next; }; +/** @struct CoordIJ + * @brief IJ hexagon coordinates + * + * Each axis is spaced 120 degrees apart. + */ +typedef struct { + int i; ///< i component + int j; ///< j component +} CoordIJ; + /** @defgroup geoToH3 geoToH3 * Functions for geoToH3 * @{ @@ -449,6 +459,24 @@ void H3_EXPORT(getH3UnidirectionalEdgeBoundary)(H3Index edge, GeoBoundary *gb); int H3_EXPORT(h3Distance)(H3Index origin, H3Index h3); /** @} */ +/** @defgroup experimentalH3ToLocalIj experimentalH3ToLocalIj + * Functions for experimentalH3ToLocalIj + * @{ + */ +/** @brief Returns two dimensional coordinates for the given index */ +int H3_EXPORT(experimentalH3ToLocalIj)(H3Index origin, H3Index h3, + CoordIJ *out); +/** @} */ + +/** @defgroup experimentalLocalIjToH3 experimentalLocalIjToH3 + * Functions for experimentalLocalIjToH3 + * @{ + */ +/** @brief Returns index for the given two dimensional coordinates */ +int H3_EXPORT(experimentalLocalIjToH3)(H3Index origin, const CoordIJ *ij, + H3Index *out); +/** @} */ + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/h3lib/include/localij.h b/src/h3lib/include/localij.h new file mode 100644 index 000000000..ed0cda593 --- /dev/null +++ b/src/h3lib/include/localij.h @@ -0,0 +1,29 @@ +/* + * Copyright 2018 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 localij.h + * @brief Local IJ coordinate space functions. + */ + +#ifndef LOCALIJ_H +#define LOCALIJ_H + +#include "coordijk.h" +#include "h3api.h" + +int h3ToLocalIjk(H3Index origin, H3Index h3, CoordIJK* out); +int localIjkToH3(H3Index origin, const CoordIJK* ijk, H3Index* out); + +#endif diff --git a/src/h3lib/lib/algos.c b/src/h3lib/lib/algos.c index e58e8b966..7f45991ba 100644 --- a/src/h3lib/lib/algos.c +++ b/src/h3lib/lib/algos.c @@ -369,7 +369,7 @@ H3Index h3NeighborRotations(H3Index origin, Direction dir, int* rotations) { // Account for differing orientation of the base cells (this edge // might not follow properties of some other edges.) if (oldBaseCell != newBaseCell) { - if (newBaseCell == 4 || newBaseCell == 117) { + if (_isBaseCellPolarPentagon(newBaseCell)) { // 'polar' base cells behave differently because they have all // i neighbors. if (oldBaseCell != 118 && oldBaseCell != 8 && diff --git a/src/h3lib/lib/baseCells.c b/src/h3lib/lib/baseCells.c index 92b41cba8..9dfc87e82 100644 --- a/src/h3lib/lib/baseCells.c +++ b/src/h3lib/lib/baseCells.c @@ -823,6 +823,12 @@ int _isBaseCellPentagon(int baseCell) { return baseCellData[baseCell].isPentagon; } +/** @brief Return whether the indicated base cell is a pentagon where all + * neighbors are oriented towards it. */ +bool _isBaseCellPolarPentagon(int baseCell) { + return baseCell == 4 || baseCell == 117; +} + /** @brief Find base cell given FaceIJK. * * Given the face number and a resolution 0 ijk+ coordinate in that face's diff --git a/src/h3lib/lib/coordijk.c b/src/h3lib/lib/coordijk.c index d1ac77bc7..a915ff4eb 100644 --- a/src/h3lib/lib/coordijk.c +++ b/src/h3lib/lib/coordijk.c @@ -504,4 +504,31 @@ int ijkDistance(const CoordIJK* c1, const CoordIJK* c2) { _ijkNormalize(&diff); CoordIJK absDiff = {abs(diff.i), abs(diff.j), abs(diff.k)}; return MAX(absDiff.i, MAX(absDiff.j, absDiff.k)); +} + +/** + * Transforms coordinates from the IJK+ coordinate system to the IJ coordinate + * system. + * + * @param ijk The input IJK+ coordinates + * @param ij The output IJ coordinates + */ +void ijkToIj(const CoordIJK* ijk, CoordIJ* ij) { + ij->i = ijk->i - ijk->k; + ij->j = ijk->j - ijk->k; +} + +/** + * Transforms coordinates from the IJ coordinate system to the IJK+ coordinate + * system. + * + * @param ij The input IJ coordinates + * @param ijk The output IJK+ coordinates + */ +void ijToIjk(const CoordIJ* ij, CoordIJK* ijk) { + ijk->i = ij->i; + ijk->j = ij->j; + ijk->k = 0; + + _ijkNormalize(ijk); } \ No newline at end of file diff --git a/src/h3lib/lib/h3Index.c b/src/h3lib/lib/h3Index.c index d17b19e18..9f5c56587 100644 --- a/src/h3lib/lib/h3Index.c +++ b/src/h3lib/lib/h3Index.c @@ -579,7 +579,8 @@ H3Index _faceIjkToH3(const FaceIJK* fijk, int res) { FaceIJK fijkBC = *fijk; // build the H3Index from finest res up - // adjust r for the fact that the res 0 base cell offsets the index array + // adjust r for the fact that the res 0 base cell offsets the indexing + // digits CoordIJK* ijk = &fijkBC.coord; for (int r = res - 1; r >= 0; r--) { CoordIJK lastIJK = *ijk; @@ -778,222 +779,3 @@ void H3_EXPORT(h3ToGeoBoundary)(H3Index h3, GeoBoundary* gb) { * a Class II grid. */ int isResClassIII(int res) { return res % 2; } - -/** - * 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. - * - * @param origin An anchoring index for the ijk+ coordinate system. - * @param index Index to find the coordinates of - * @param out ijk+ coordinates of the index will be placed here on success - * @return 0 on success, or another value on failure. - */ -int h3ToIjk(H3Index origin, H3Index h3, CoordIJK* out) { - if (H3_GET_MODE(origin) != H3_HEXAGON_MODE || - H3_GET_MODE(h3) != H3_HEXAGON_MODE) { - // Only hexagon mode is relevant, since we can't - // encode directionality in CoordIJK. - return 1; - } - - int res = H3_GET_RESOLUTION(origin); - - if (res != H3_GET_RESOLUTION(h3)) { - return 1; - } - - int originBaseCell = H3_GET_BASE_CELL(origin); - int baseCell = H3_GET_BASE_CELL(h3); - - // Direction from origin base cell to index base cell - Direction dir = 0; - Direction revDir = 0; - if (originBaseCell != baseCell) { - dir = _getBaseCellDirection(originBaseCell, baseCell); - if (dir == INVALID_DIGIT) { - // Base cells are not neighbors, can't unfold. - return 2; - } - revDir = _getBaseCellDirection(baseCell, originBaseCell); - assert(revDir != INVALID_DIGIT); - } - - int originOnPent = _isBaseCellPentagon(originBaseCell); - int indexOnPent = _isBaseCellPentagon(baseCell); - - FaceIJK indexFijk = {0}; - if (dir != 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 = baseCellNeighbor60CCWRots[originBaseCell][dir]; - if (indexOnPent) { - for (int i = 0; i < baseCellRotations; i++) { - h3 = _h3RotatePent60cw(h3); - - revDir = _rotate60cw(revDir); - if (revDir == K_AXES_DIGIT) revDir = _rotate60cw(revDir); - } - } else { - for (int i = 0; i < baseCellRotations; i++) { - h3 = _h3Rotate60cw(h3); - - revDir = _rotate60cw(revDir); - } - } - } - // Face is unused. This produces coordinates in base cell coordinate space. - _h3ToFaceIjkWithInitializedFijk(h3, &indexFijk); - - // Origin leading digit -> index leading digit -> rotations 60 cw - // Either being 1 (K axis) is invalid. - // No good default at 0. - const int PENTAGON_ROTATIONS[7][7] = { - {0, -1, 0, 0, 0, 0, 0}, // 0 - {-1, -1, -1, -1, -1, -1, -1}, // 1 - {0, -1, 0, 0, 0, 1, 0}, // 2 - {0, -1, 0, 0, 1, 1, 0}, // 3 - {0, -1, 0, 5, 0, 0, 0}, // 4 - {0, -1, 5, 5, 0, 0, 0}, // 5 - {0, -1, 0, 0, 0, 0, 0}, // 6 - }; - // Simply prohibit many pentagon distortion cases rather than handling them. - const bool FAILED_DIRECTIONS_II[7][7] = { - {false, false, false, false, false, false, false}, // 0 - {false, false, false, false, false, false, false}, // 1 - {false, false, false, false, true, false, false}, // 2 - {false, false, false, false, false, false, true}, // 3 - {false, false, false, true, false, false, false}, // 4 - {false, false, true, false, false, false, false}, // 5 - {false, false, false, false, false, true, false}, // 6 - }; - const bool FAILED_DIRECTIONS_III[7][7] = { - {false, false, false, false, false, false, false}, // 0 - {false, false, false, false, false, false, false}, // 1 - {false, false, false, false, false, true, false}, // 2 - {false, false, false, false, true, false, false}, // 3 - {false, false, true, false, false, false, false}, // 4 - {false, false, false, false, false, false, true}, // 5 - {false, false, false, true, false, false, false}, // 6 - }; - - if (dir != CENTER_DIGIT) { - assert(baseCell != originBaseCell); - assert(!(originOnPent && indexOnPent)); - - int pentagonRotations = 0; - int directionRotations = 0; - - if (originOnPent) { - int originLeadingDigit = _h3LeadingNonZeroDigit(origin); - - if ((isResClassIII(res) && - FAILED_DIRECTIONS_III[originLeadingDigit][dir]) || - (!isResClassIII(res) && - FAILED_DIRECTIONS_II[originLeadingDigit][dir])) { - // TODO this part of the pentagon might not be unfolded - // correctly. - return 3; - } - - directionRotations = PENTAGON_ROTATIONS[originLeadingDigit][dir]; - pentagonRotations = directionRotations; - } else if (indexOnPent) { - int indexLeadingDigit = _h3LeadingNonZeroDigit(h3); - - if ((isResClassIII(res) && - FAILED_DIRECTIONS_III[indexLeadingDigit][revDir]) || - (!isResClassIII(res) && - FAILED_DIRECTIONS_II[indexLeadingDigit][revDir])) { - // TODO this part of the pentagon might not be unfolded - // correctly. - return 4; - } - - pentagonRotations = PENTAGON_ROTATIONS[revDir][indexLeadingDigit]; - } - - assert(pentagonRotations >= 0); - assert(directionRotations >= 0); - - for (int i = 0; i < pentagonRotations; i++) { - _ijkRotate60cw(&indexFijk.coord); - } - - CoordIJK offset = {0}; - _neighbor(&offset, dir); - // Scale offset based on resolution - for (int r = res - 1; r >= 0; r--) { - if (isResClassIII(r + 1)) { - // rotate ccw - _downAp7(&offset); - } else { - // rotate cw - _downAp7r(&offset); - } - } - - for (int i = 0; i < directionRotations; i++) { - _ijkRotate60cw(&offset); - } - - // Perform necessary translation - _ijkAdd(&indexFijk.coord, &offset, &indexFijk.coord); - _ijkNormalize(&indexFijk.coord); - } else if (originOnPent && indexOnPent) { - // 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. - assert(baseCell == originBaseCell); - - int originLeadingDigit = _h3LeadingNonZeroDigit(origin); - int indexLeadingDigit = _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++) { - _ijkRotate60cw(&indexFijk.coord); - } - } - - *out = indexFijk.coord; - return 0; -} - -/** - * Produces the grid distance between the two indexes. - * - * This function may fail to find the distance between two indexes, for - * example if they are very far apart. It may also fail when finding - * distances for indexes on opposite sides of a pentagon. - * - * @param origin Index to find the distance from. - * @param index Index to find the distance to. - * @return The distance, or a negative number if the library could not - * compute the distance. - */ -int H3_EXPORT(h3Distance)(H3Index origin, H3Index h3) { - CoordIJK originIjk, h3Ijk; - if (h3ToIjk(origin, origin, &originIjk)) { - // Only possible if origin is invalid, for example if it's a - // unidirectional edge. - return -1; - } - if (h3ToIjk(origin, h3, &h3Ijk)) { - return -1; - } - - return ijkDistance(&originIjk, &h3Ijk); -} diff --git a/src/h3lib/lib/localij.c b/src/h3lib/lib/localij.c new file mode 100644 index 000000000..124601e3d --- /dev/null +++ b/src/h3lib/lib/localij.c @@ -0,0 +1,544 @@ +/* + * Copyright 2018 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 localij.c + * @brief Local IJ coordinate space functions + * + * These functions try to provide a useful coordinate space in the vicinity of + * an origin index. + */ +#include +#include +#include +#include +#include +#include "baseCells.h" +#include "faceijk.h" +#include "h3Index.h" +#include "mathExtensions.h" +#include "stackAlloc.h" + +/** + * Origin leading digit -> index leading digit -> rotations 60 cw + * Either being 1 (K axis) is invalid. + * No good default at 0. + */ +const int PENTAGON_ROTATIONS[7][7] = { + {0, -1, 0, 0, 0, 0, 0}, // 0 + {-1, -1, -1, -1, -1, -1, -1}, // 1 + {0, -1, 0, 0, 0, 1, 0}, // 2 + {0, -1, 0, 0, 1, 1, 0}, // 3 + {0, -1, 0, 5, 0, 0, 0}, // 4 + {0, -1, 5, 5, 0, 0, 0}, // 5 + {0, -1, 0, 0, 0, 0, 0}, // 6 +}; +/** + * Reverse base cell direction -> leading index digit -> rotations 60 ccw. + * For reversing the rotation introduced in PENTAGON_ROTATIONS when + * the origin is on a pentagon (regardless of the base cell of the index.) + */ +const int PENTAGON_ROTATIONS_REVERSE[7][7] = { + {0, 0, 0, 0, 0, 0, 0}, // 0 + {-1, -1, -1, -1, -1, -1, -1}, // 1 + {0, 1, 0, 0, 0, 0, 0}, // 2 + {0, 1, 0, 0, 0, 1, 0}, // 3 + {0, 5, 0, 0, 0, 0, 0}, // 4 + {0, 5, 0, 5, 0, 0, 0}, // 5 + {0, 0, 0, 0, 0, 0, 0}, // 6 +}; +/** + * Reverse base cell direction -> leading index digit -> rotations 60 ccw. + * For reversing the rotation introduced in PENTAGON_ROTATIONS when the index is + * on a pentagon and the origin is not. + */ +const int PENTAGON_ROTATIONS_REVERSE_NONPOLAR[7][7] = { + {0, 0, 0, 0, 0, 0, 0}, // 0 + {-1, -1, -1, -1, -1, -1, -1}, // 1 + {0, 1, 0, 0, 0, 0, 0}, // 2 + {0, 1, 0, 0, 0, 1, 0}, // 3 + {0, 5, 0, 0, 0, 0, 0}, // 4 + {0, 1, 0, 5, 1, 1, 0}, // 5 + {0, 0, 0, 0, 0, 0, 0}, // 6 +}; +/** + * Reverse base cell direction -> leading index digit -> rotations 60 ccw. + * For reversing the rotation introduced in PENTAGON_ROTATIONS when the index is + * on a polar pentagon and the origin is not. + */ +const int PENTAGON_ROTATIONS_REVERSE_POLAR[7][7] = { + {0, 0, 0, 0, 0, 0, 0}, // 0 + {-1, -1, -1, -1, -1, -1, -1}, // 1 + {0, 1, 1, 1, 1, 1, 1}, // 2 + {0, 1, 0, 0, 0, 1, 0}, // 3 + {0, 1, 0, 0, 1, 1, 1}, // 4 + {0, 1, 0, 5, 1, 1, 0}, // 5 + {0, 1, 1, 0, 1, 1, 1}, // 6 +}; + +// Simply prohibit many pentagon distortion cases rather than handling them. +const bool FAILED_DIRECTIONS_II[7][7] = { + {false, false, false, false, false, false, false}, // 0 + {false, false, false, false, false, false, false}, // 1 + {false, false, false, false, true, false, false}, // 2 + {false, false, false, false, false, false, true}, // 3 + {false, false, false, true, false, false, false}, // 4 + {false, false, true, false, false, false, false}, // 5 + {false, false, false, false, false, true, false}, // 6 +}; +const bool FAILED_DIRECTIONS_III[7][7] = { + {false, false, false, false, false, false, false}, // 0 + {false, false, false, false, false, false, false}, // 1 + {false, false, false, false, false, true, false}, // 2 + {false, false, false, false, true, false, false}, // 3 + {false, false, true, false, false, false, false}, // 4 + {false, false, false, false, false, false, true}, // 5 + {false, false, false, true, false, false, false}, // 6 +}; + +/** + * 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. + * + * @param origin An anchoring index for the ijk+ coordinate system. + * @param index Index to find the coordinates of + * @param out ijk+ coordinates of the index will be placed here on success + * @return 0 on success, or another value on failure. + */ +int h3ToLocalIjk(H3Index origin, H3Index h3, CoordIJK* out) { + int res = H3_GET_RESOLUTION(origin); + + if (res != H3_GET_RESOLUTION(h3)) { + return 1; + } + + int originBaseCell = H3_GET_BASE_CELL(origin); + int baseCell = H3_GET_BASE_CELL(h3); + + // Direction from origin base cell to index base cell + Direction dir = 0; + Direction revDir = 0; + if (originBaseCell != baseCell) { + dir = _getBaseCellDirection(originBaseCell, baseCell); + if (dir == INVALID_DIGIT) { + // Base cells are not neighbors, can't unfold. + return 2; + } + revDir = _getBaseCellDirection(baseCell, originBaseCell); + assert(revDir != INVALID_DIGIT); + } + + int originOnPent = _isBaseCellPentagon(originBaseCell); + int indexOnPent = _isBaseCellPentagon(baseCell); + + FaceIJK indexFijk = {0}; + if (dir != 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 = baseCellNeighbor60CCWRots[originBaseCell][dir]; + if (indexOnPent) { + for (int i = 0; i < baseCellRotations; i++) { + h3 = _h3RotatePent60cw(h3); + + revDir = _rotate60cw(revDir); + if (revDir == K_AXES_DIGIT) revDir = _rotate60cw(revDir); + } + } else { + for (int i = 0; i < baseCellRotations; i++) { + h3 = _h3Rotate60cw(h3); + + revDir = _rotate60cw(revDir); + } + } + } + // Face is unused. This produces coordinates in base cell coordinate space. + _h3ToFaceIjkWithInitializedFijk(h3, &indexFijk); + + if (dir != CENTER_DIGIT) { + assert(baseCell != originBaseCell); + assert(!(originOnPent && indexOnPent)); + + int pentagonRotations = 0; + int directionRotations = 0; + + if (originOnPent) { + int originLeadingDigit = _h3LeadingNonZeroDigit(origin); + + if ((isResClassIII(res) && + FAILED_DIRECTIONS_III[originLeadingDigit][dir]) || + (!isResClassIII(res) && + FAILED_DIRECTIONS_II[originLeadingDigit][dir])) { + // TODO this part of the pentagon might not be unfolded + // correctly. + return 3; + } + + directionRotations = PENTAGON_ROTATIONS[originLeadingDigit][dir]; + pentagonRotations = directionRotations; + } else if (indexOnPent) { + int indexLeadingDigit = _h3LeadingNonZeroDigit(h3); + + if ((isResClassIII(res) && + FAILED_DIRECTIONS_III[indexLeadingDigit][revDir]) || + (!isResClassIII(res) && + FAILED_DIRECTIONS_II[indexLeadingDigit][revDir])) { + // TODO this part of the pentagon might not be unfolded + // correctly. + return 4; + } + + pentagonRotations = PENTAGON_ROTATIONS[revDir][indexLeadingDigit]; + } + + assert(pentagonRotations >= 0); + assert(directionRotations >= 0); + + for (int i = 0; i < pentagonRotations; i++) { + _ijkRotate60cw(&indexFijk.coord); + } + + CoordIJK offset = {0}; + _neighbor(&offset, dir); + // Scale offset based on resolution + for (int r = res - 1; r >= 0; r--) { + if (isResClassIII(r + 1)) { + // rotate ccw + _downAp7(&offset); + } else { + // rotate cw + _downAp7r(&offset); + } + } + + for (int i = 0; i < directionRotations; i++) { + _ijkRotate60cw(&offset); + } + + // Perform necessary translation + _ijkAdd(&indexFijk.coord, &offset, &indexFijk.coord); + _ijkNormalize(&indexFijk.coord); + } else if (originOnPent && indexOnPent) { + // 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. + assert(baseCell == originBaseCell); + + int originLeadingDigit = _h3LeadingNonZeroDigit(origin); + int indexLeadingDigit = _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++) { + _ijkRotate60cw(&indexFijk.coord); + } + } + + *out = indexFijk.coord; + return 0; +} + +/** + * 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. + * + * @param origin An anchoring index for the ijk+ coordinate system. + * @param ijk IJK+ Coordinates to find the index of + * @param out The index will be placed here on success + * @return 0 on success, or another value on failure. + */ +int localIjkToH3(H3Index origin, const CoordIJK* ijk, H3Index* out) { + int res = H3_GET_RESOLUTION(origin); + int originBaseCell = H3_GET_BASE_CELL(origin); + int originOnPent = _isBaseCellPentagon(originBaseCell); + + // This logic is very similar to faceIjkToH3 + // initialize the index + *out = H3_INIT; + H3_SET_MODE(*out, H3_HEXAGON_MODE); + H3_SET_RESOLUTION(*out, res); + + // check for res 0/base cell + if (res == 0) { + if (ijk->i > 1 || ijk->i > 1 || ijk->i > 1) { + // out of range input + return 1; + } + + const Direction dir = _unitIjkToDigit(ijk); + const int newBaseCell = _getBaseCellNeighbor(originBaseCell, dir); + if (newBaseCell == INVALID_BASE_CELL) { + // Moving in an invalid direction off a pentagon. + return 1; + } + H3_SET_BASE_CELL(*out, 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 = *ijk; + + // 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 (isResClassIII(r + 1)) { + // rotate ccw + _upAp7(&ijkCopy); + lastCenter = ijkCopy; + _downAp7(&lastCenter); + } else { + // rotate cw + _upAp7r(&ijkCopy); + lastCenter = ijkCopy; + _downAp7r(&lastCenter); + } + + CoordIJK diff; + _ijkSub(&lastIJK, &lastCenter, &diff); + _ijkNormalize(&diff); + + H3_SET_INDEX_DIGIT(*out, r + 1, _unitIjkToDigit(&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 + Direction dir = _unitIjkToDigit(&ijkCopy); + int baseCell = _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 == INVALID_BASE_CELL ? 0 : _isBaseCellPentagon(baseCell)); + + if (dir != 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) { + const Direction originLeadingDigit = _h3LeadingNonZeroDigit(origin); + pentagonRotations = + PENTAGON_ROTATIONS_REVERSE[originLeadingDigit][dir]; + for (int i = 0; i < pentagonRotations; i++) { + dir = _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 == K_AXES_DIGIT) { + return 3; + } + baseCell = _getBaseCellNeighbor(originBaseCell, dir); + + // indexOnPent does not need to be checked again since no pentagon + // base cells border each other. + assert(baseCell != INVALID_BASE_CELL); + assert(!_isBaseCellPentagon(baseCell)); + } + + // Now we can determine the relation between the origin and target base + // cell. + const int baseCellRotations = + baseCellNeighbor60CCWRots[originBaseCell][dir]; + 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) { + const Direction revDir = + _getBaseCellDirection(baseCell, originBaseCell); + assert(revDir != 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 = _h3Rotate60ccw(*out); + } + + const Direction indexLeadingDigit = _h3LeadingNonZeroDigit(*out); + if (_isBaseCellPolarPentagon(baseCell)) { + pentagonRotations = + PENTAGON_ROTATIONS_REVERSE_POLAR[revDir][indexLeadingDigit]; + } else { + pentagonRotations = + PENTAGON_ROTATIONS_REVERSE_NONPOLAR[revDir] + [indexLeadingDigit]; + } + + assert(pentagonRotations >= 0); + for (int i = 0; i < pentagonRotations; i++) { + *out = _h3RotatePent60ccw(*out); + } + } else { + assert(pentagonRotations >= 0); + for (int i = 0; i < pentagonRotations; i++) { + *out = _h3Rotate60ccw(*out); + } + + // Adjust for the different coordinate space in the two base cells. + for (int i = 0; i < baseCellRotations; i++) { + *out = _h3Rotate60ccw(*out); + } + } + } else if (originOnPent && indexOnPent) { + const int originLeadingDigit = _h3LeadingNonZeroDigit(origin); + const int indexLeadingDigit = _h3LeadingNonZeroDigit(*out); + + const int withinPentagonRotations = + PENTAGON_ROTATIONS_REVERSE[originLeadingDigit][indexLeadingDigit]; + assert(withinPentagonRotations >= 0); + + for (int i = 0; i < withinPentagonRotations; i++) { + *out = _h3Rotate60ccw(*out); + } + } + + if (indexOnPent) { + // TODO: There are cases in h3ToLocalIjk which are failed but not + // accounted for here - instead just fail if the recovered index is + // invalid. + if (_h3LeadingNonZeroDigit(*out) == K_AXES_DIGIT) { + return 4; + } + } + + H3_SET_BASE_CELL(*out, baseCell); + return 0; +} + +/** + * Produces ij 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. + * + * This function is experimental, and its output is not guaranteed + * to be compatible across different versions of H3. + * + * @param origin An anchoring index for the ij coordinate system. + * @param index Index to find the coordinates of + * @param out ij coordinates of the index will be placed here on success + * @return 0 on success, or another value on failure. + */ +int H3_EXPORT(experimentalH3ToLocalIj)(H3Index origin, H3Index h3, + CoordIJ* out) { + // This function is currently experimental. Once ready to be part of the + // non-experimental API, this function (with the experimental prefix) will + // be marked as deprecated and to be removed in the next major version. It + // will be replaced with a non-prefixed function name. + CoordIJK ijk; + int failed = h3ToLocalIjk(origin, h3, &ijk); + if (failed) { + return failed; + } + + ijkToIj(&ijk, out); + + return 0; +} + +/** + * Produces an index for ij 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 index is too far away from the origin + * or if the index is on the other side of a pentagon. + * + * This function is experimental, and its output is not guaranteed + * to be compatible across different versions of H3. + * + * @param origin An anchoring index for the ij coordinate system. + * @param out ij coordinates to index. + * @param index Index will be placed here on success. + * @return 0 on success, or another value on failure. + */ +int H3_EXPORT(experimentalLocalIjToH3)(H3Index origin, const CoordIJ* ij, + H3Index* out) { + // This function is currently experimental. Once ready to be part of the + // non-experimental API, this function (with the experimental prefix) will + // be marked as deprecated and to be removed in the next major version. It + // will be replaced with a non-prefixed function name. + CoordIJK ijk; + ijToIjk(ij, &ijk); + + return localIjkToH3(origin, &ijk, out); +} + +/** + * Produces the grid distance between the two indexes. + * + * This function may fail to find the distance between two indexes, for + * example if they are very far apart. It may also fail when finding + * distances for indexes on opposite sides of a pentagon. + * + * @param origin Index to find the distance from. + * @param index Index to find the distance to. + * @return The distance, or a negative number if the library could not + * compute the distance. + */ +int H3_EXPORT(h3Distance)(H3Index origin, H3Index h3) { + CoordIJK originIjk, h3Ijk; + if (h3ToLocalIjk(origin, origin, &originIjk)) { + // Currently there are no tests that would cause getting the coordinates + // for an index the same as the origin to fail. + return -1; // LCOV_EXCL_LINE + } + if (h3ToLocalIjk(origin, h3, &h3Ijk)) { + return -1; + } + + return ijkDistance(&originIjk, &h3Ijk); +}