diff --git a/CHANGELOG.md b/CHANGELOG.md index f9a0d237fe..7c9c8631b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The public API of this library consists of the functions declared in file [h3api.h.in](./src/h3lib/include/h3api.h.in). ## [Unreleased] +### Added +- (Undirected) edge index mode. (#654) ## [4.1.0] - 2023-01-18 ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b7edd1158..67bbc07b68 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,6 +127,7 @@ set(LIB_SOURCE_FILES src/h3lib/include/polygonAlgos.h src/h3lib/include/h3Index.h src/h3lib/include/directedEdge.h + src/h3lib/include/edge.h src/h3lib/include/latLng.h src/h3lib/include/vec2d.h src/h3lib/include/vec3d.h @@ -153,6 +154,7 @@ set(LIB_SOURCE_FILES src/h3lib/lib/localij.c src/h3lib/lib/latLng.c src/h3lib/lib/directedEdge.c + src/h3lib/lib/edge.c src/h3lib/lib/mathExtensions.c src/h3lib/lib/iterators.c src/h3lib/lib/vertexGraph.c @@ -217,6 +219,8 @@ set(OTHER_SOURCE_FILES src/apps/testapps/testVec3d.c src/apps/testapps/testDirectedEdge.c src/apps/testapps/testDirectedEdgeExhaustive.c + src/apps/testapps/testEdge.c + src/apps/testapps/testEdgeExhaustive.c src/apps/testapps/testLinkedGeo.c src/apps/testapps/mkRandGeo.c src/apps/testapps/testH3Api.c @@ -254,6 +258,7 @@ set(OTHER_SOURCE_FILES src/apps/fuzzers/fuzzerVertexes.c src/apps/fuzzers/fuzzerCompact.c src/apps/fuzzers/fuzzerDirectedEdge.c + src/apps/fuzzers/fuzzerEdge.c src/apps/fuzzers/fuzzerLocalIj.c src/apps/fuzzers/fuzzerPolygonToCells.c src/apps/fuzzers/fuzzerPolygonToCellsNoHoles.c @@ -266,6 +271,7 @@ set(OTHER_SOURCE_FILES src/apps/benchmarks/benchmarkGridDiskCells.c src/apps/benchmarks/benchmarkGridPathCells.c src/apps/benchmarks/benchmarkDirectedEdge.c + src/apps/benchmarks/benchmarkEdge.c src/apps/benchmarks/benchmarkVertex.c src/apps/benchmarks/benchmarkIsValidCell.c src/apps/benchmarks/benchmarkH3Api.c) @@ -508,6 +514,7 @@ if(BUILD_FUZZERS) add_h3_fuzzer(fuzzerVertexes src/apps/fuzzers/fuzzerVertexes.c) add_h3_fuzzer(fuzzerCompact src/apps/fuzzers/fuzzerCompact.c) add_h3_fuzzer(fuzzerDirectedEdge src/apps/fuzzers/fuzzerDirectedEdge.c) + add_h3_fuzzer(fuzzerEdge src/apps/fuzzers/fuzzerEdge.c) add_h3_fuzzer(fuzzerLocalIj src/apps/fuzzers/fuzzerLocalIj.c) add_h3_fuzzer(fuzzerPolygonToCells src/apps/fuzzers/fuzzerPolygonToCells.c) add_h3_fuzzer(fuzzerPolygonToCellsNoHoles src/apps/fuzzers/fuzzerPolygonToCellsNoHoles.c) @@ -532,6 +539,7 @@ if(BUILD_BENCHMARKS) add_h3_benchmark(benchmarkGridDiskCells src/apps/benchmarks/benchmarkGridDiskCells.c) add_h3_benchmark(benchmarkGridPathCells src/apps/benchmarks/benchmarkGridPathCells.c) add_h3_benchmark(benchmarkDirectedEdge src/apps/benchmarks/benchmarkDirectedEdge.c) + add_h3_benchmark(benchmarkEdge src/apps/benchmarks/benchmarkEdge.c) add_h3_benchmark(benchmarkVertex src/apps/benchmarks/benchmarkVertex.c) add_h3_benchmark(benchmarkIsValidCell src/apps/benchmarks/benchmarkIsValidCell.c) add_h3_benchmark(benchmarkCellsToLinkedMultiPolygon src/apps/benchmarks/benchmarkCellsToLinkedMultiPolygon.c) diff --git a/CMakeTests.cmake b/CMakeTests.cmake index dd9069c0e1..5dbad0b0b6 100644 --- a/CMakeTests.cmake +++ b/CMakeTests.cmake @@ -187,6 +187,7 @@ add_h3_test(testPolygonToCells src/apps/testapps/testPolygonToCells.c) add_h3_test(testPolygonToCellsReported src/apps/testapps/testPolygonToCellsReported.c) add_h3_test(testVertexGraph src/apps/testapps/testVertexGraph.c) add_h3_test(testDirectedEdge src/apps/testapps/testDirectedEdge.c) +add_h3_test(testEdge src/apps/testapps/testEdge.c) add_h3_test(testLatLng src/apps/testapps/testLatLng.c) add_h3_test(testBBox src/apps/testapps/testBBox.c) add_h3_test(testVertex src/apps/testapps/testVertex.c) @@ -210,6 +211,7 @@ add_h3_test_with_arg(testH3NeighborRotations src/apps/testapps/testH3NeighborRot # The "Exhaustive" part of the test name is used by the test-fast to exclude these files. # test-fast exists so that Travis CI can run Valgrind on tests without taking a very long time. +add_h3_test(testEdgeExhaustive src/apps/testapps/testEdgeExhaustive.c) add_h3_test(testDirectedEdgeExhaustive src/apps/testapps/testDirectedEdgeExhaustive.c) add_h3_test(testVertexExhaustive src/apps/testapps/testVertexExhaustive.c) add_h3_test(testCellToLocalIjExhaustive src/apps/testapps/testCellToLocalIjExhaustive.c) diff --git a/src/apps/benchmarks/benchmarkEdge.c b/src/apps/benchmarks/benchmarkEdge.c new file mode 100644 index 0000000000..9b319a3ace --- /dev/null +++ b/src/apps/benchmarks/benchmarkEdge.c @@ -0,0 +1,35 @@ +/* + * Copyright 2022 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. + */ +#include "benchmark.h" +#include "h3api.h" +#include "latLng.h" + +// Fixtures (arbitrary res 9 hexagon) +H3Index edges[6] = {0}; +H3Index hex = 0x89283080ddbffff; + +BEGIN_BENCHMARKS(); + +CellBoundary outBoundary; +H3_EXPORT(cellToEdges)(hex, edges); + +BENCHMARK(edgeToBoundary, 10000, { + for (int i = 0; i < 6; i++) { + H3_EXPORT(edgeToBoundary)(edges[i], &outBoundary); + } +}); + +END_BENCHMARKS(); diff --git a/src/apps/fuzzers/README.md b/src/apps/fuzzers/README.md index 392c9c206d..bbda8ed47a 100644 --- a/src/apps/fuzzers/README.md +++ b/src/apps/fuzzers/README.md @@ -53,6 +53,12 @@ The public API of H3 is covered in the following fuzzers: | directedEdgeToCells | [fuzzerDirectedEdge](./fuzzerDirectedEdge.c) | originToDirectedEdges | [fuzzerDirectedEdge](./fuzzerDirectedEdge.c) | directedEdgeToBoundary | [fuzzerDirectedEdge](./fuzzerDirectedEdge.c) +| cellsToEdge | [fuzzerEdge](./fuzzerEdge.c) +| isValidEdge | [fuzzerEdge](./fuzzerEdge.c) +| edgeToCells | [fuzzerEdge](./fuzzerEdge.c) +| cellToEdges | [fuzzerEdge](./fuzzerEdge.c) +| edgeToBoundary | [fuzzerEdge](./fuzzerEdge.c) +| directedEdgeToEdge | [fuzzerEdge](./fuzzerEdge.c) | cellToVertex | [fuzzerVertexes](./fuzzerVertexes.c) | cellToVertexes | [fuzzerVertexes](./fuzzerVertexes.c) | vertexToLatLng | [fuzzerVertexes](./fuzzerVertexes.c) diff --git a/src/apps/fuzzers/fuzzerEdge.c b/src/apps/fuzzers/fuzzerEdge.c new file mode 100644 index 0000000000..5d6da60467 --- /dev/null +++ b/src/apps/fuzzers/fuzzerEdge.c @@ -0,0 +1,51 @@ +/* + * Copyright 2022 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 Fuzzer program for cellsToEdge and related functions + */ + +#include "aflHarness.h" +#include "h3api.h" +#include "utility.h" + +typedef struct { + H3Index index; + H3Index index2; +} inputArgs; + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size < sizeof(inputArgs)) { + return 0; + } + const inputArgs *args = (const inputArgs *)data; + + // Note that index and index2 need to be plausibly neighbors + // for most of these + H3Index out; + H3_EXPORT(cellsToEdge)(args->index, args->index2, &out); + H3_EXPORT(isValidEdge)(args->index); + H3Index out2[2]; + H3_EXPORT(edgeToCells)(args->index, out2); + H3Index out6[6]; + H3_EXPORT(cellToEdges)(args->index, out6); + CellBoundary bndry; + H3_EXPORT(edgeToBoundary)(args->index, &bndry); + H3_EXPORT(directedEdgeToEdge)(args->index, out2); + + return 0; +} + +AFL_HARNESS_MAIN(sizeof(inputArgs)); diff --git a/src/apps/testapps/testEdge.c b/src/apps/testapps/testEdge.c new file mode 100644 index 0000000000..40bd831fb6 --- /dev/null +++ b/src/apps/testapps/testEdge.c @@ -0,0 +1,299 @@ +/* + * Copyright 2022 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 testEdge.c + * @brief Tests functions for manipulating undirected edge H3Indexes + * + * usage: `testEdge` + */ + +#include + +#include "algos.h" +#include "constants.h" +#include "h3Index.h" +#include "latLng.h" +#include "test.h" +#include "utility.h" + +// Fixtures +static LatLng sfGeo = {0.659966917655, -2.1364398519396}; + +SUITE(edge) { + TEST(cellsToEdgeAndFriends) { + H3Index sf; + t_assertSuccess(H3_EXPORT(latLngToCell)(&sfGeo, 9, &sf)); + H3Index ring[7] = {H3_NULL}; + t_assertSuccess(H3_EXPORT(gridRingUnsafe)(sf, 1, ring)); + H3Index sf2 = ring[0]; + + H3Index edge; + t_assertSuccess(H3_EXPORT(cellsToEdge)(sf, sf2, &edge)); + H3Index cells[2] = {H3_NULL}; + t_assertSuccess(H3_EXPORT(edgeToCells)(edge, cells)); + t_assert(cells[0] == sf || cells[1] == sf, + "One of the cells is the origin"); + t_assert(cells[0] == sf2 || cells[1] == sf2, + "One of the cells is the destination"); + t_assert(sf != sf2, "Sanity check for cells not being the same"); + // Uses an implementation detail that the origin cell is returned first + t_assert(cells[0] < cells[1], + "Undirected edge cells are in normalized order"); + + t_assert(H3_EXPORT(edgeToCells)(0, cells) == E_UNDIR_EDGE_INVALID, + "edgeToCells fails for invalid edges"); + H3Index invalidEdge; + setH3Index(&invalidEdge, 1, 4, 0); + H3_SET_RESERVED_BITS(invalidEdge, INVALID_DIGIT); + H3_SET_MODE(invalidEdge, H3_EDGE_MODE); + t_assert(H3_EXPORT(edgeToCells)(invalidEdge, cells) != E_SUCCESS, + "edgeToCells fails for invalid edges"); + + H3Index largerRing[19] = {H3_NULL}; + t_assertSuccess(H3_EXPORT(gridRingUnsafe)(sf, 2, largerRing)); + H3Index sf3 = largerRing[0]; + + H3Index notEdge; + t_assert(H3_EXPORT(cellsToEdge)(sf, sf3, ¬Edge) == E_NOT_NEIGHBORS, + "Non-neighbors can't have edges"); + } + + TEST(cellsToEdgeFromPentagon) { + H3Index pentagons[NUM_PENTAGONS] = {H3_NULL}; + H3Index ring[7] = {H3_NULL}; + H3Index pentagon; + H3Index edge, edge2; + + for (int res = 0; res < MAX_H3_RES; res++) { + t_assertSuccess(H3_EXPORT(getPentagons)(res, pentagons)); + for (int p = 0; p < NUM_PENTAGONS; p++) { + pentagon = pentagons[p]; + t_assertSuccess(H3_EXPORT(gridDisk)(pentagon, 1, ring)); + + for (int i = 0; i < 7; i++) { + H3Index neighbor = ring[i]; + if (neighbor == pentagon || neighbor == H3_NULL) continue; + t_assertSuccess( + H3_EXPORT(cellsToEdge)(pentagon, neighbor, &edge)); + t_assert(H3_EXPORT(isValidEdge)(edge), + "pentagon-to-neighbor is a valid edge"); + t_assertSuccess( + H3_EXPORT(cellsToEdge)(neighbor, pentagon, &edge2)); + t_assert(H3_EXPORT(isValidEdge)(edge2), + "neighbor-to-pentagon is a valid edge"); + t_assert(edge == edge2, + "direction does not matter for edge"); + } + } + } + } + + TEST(isValidEdge) { + H3Index sf; + t_assertSuccess(H3_EXPORT(latLngToCell)(&sfGeo, 9, &sf)); + H3Index ring[7] = {H3_NULL}; + t_assertSuccess(H3_EXPORT(gridRingUnsafe)(sf, 1, ring)); + H3Index sf2 = ring[0]; + + H3Index edge; + t_assertSuccess(H3_EXPORT(cellsToEdge)(sf, sf2, &edge)); + t_assert(H3_EXPORT(isValidEdge)(edge) == 1, "edges validate correctly"); + t_assert(H3_EXPORT(isValidEdge)(sf) == 0, "hexagons do not validate"); + + H3Index directedEdge = edge; + H3_SET_MODE(directedEdge, H3_DIRECTEDEDGE_MODE); + t_assert(H3_EXPORT(isValidEdge)(directedEdge) == 0, + "directed edges do not validate"); + + H3Index hexagonWithReserved = sf; + H3_SET_RESERVED_BITS(hexagonWithReserved, 1); + t_assert(H3_EXPORT(isValidEdge)(hexagonWithReserved) == 0, + "hexagons with reserved bits do not validate"); + + H3Index fakeEdge = sf; + H3_SET_MODE(fakeEdge, H3_EDGE_MODE); + t_assert(H3_EXPORT(isValidEdge)(fakeEdge) == 0, + "edges without an edge specified don't work"); + H3Index invalidEdge = sf; + H3_SET_MODE(invalidEdge, H3_EDGE_MODE); + H3_SET_RESERVED_BITS(invalidEdge, INVALID_DIGIT); + t_assert(H3_EXPORT(isValidEdge)(invalidEdge) == 0, + "edges with an invalid edge specified don't work"); + + H3Index pentagon = 0x821c07fffffffff; + H3Index goodPentagonalEdge = pentagon; + H3_SET_MODE(goodPentagonalEdge, H3_EDGE_MODE); + H3_SET_RESERVED_BITS(goodPentagonalEdge, 2); + t_assert(H3_EXPORT(isValidEdge)(goodPentagonalEdge) == 1, + "pentagonal edge validates"); + + H3Index badPentagonalEdge = goodPentagonalEdge; + H3_SET_RESERVED_BITS(badPentagonalEdge, 1); + t_assert(H3_EXPORT(isValidEdge)(badPentagonalEdge) == 0, + "missing pentagonal edge does not validate"); + // Case discovered by fuzzer that triggers pentagon deleted direction + // condition + H3Index badPentagonalEdge2 = 0x990d6700ff00ffff; + t_assert(H3_EXPORT(isValidEdge)(badPentagonalEdge2) == 0, + "missing pentagonal edge 2 does not validate"); + + H3Index highBitEdge = edge; + H3_SET_HIGH_BIT(highBitEdge, 1); + t_assert(H3_EXPORT(isValidEdge)(highBitEdge) == 0, + "high bit set edge does not validate"); + } + + TEST(cellToEdges) { + H3Index sf; + t_assertSuccess(H3_EXPORT(latLngToCell)(&sfGeo, 9, &sf)); + H3Index edges[6] = {H3_NULL}; + t_assertSuccess(H3_EXPORT(cellToEdges)(sf, edges)); + + for (int i = 0; i < 6; i++) { + t_assert(H3_EXPORT(isValidEdge)(edges[i]) == 1, "edge is an edge"); + H3Index owner, destination; + owner = edges[i]; + Direction dir = H3_GET_RESERVED_BITS(edges[i]); + H3_SET_MODE(owner, H3_CELL_MODE); + H3_SET_RESERVED_BITS(owner, 0); + int rotations = 0; + t_assertSuccess( + h3NeighborRotations(owner, dir, &rotations, &destination)); + t_assert(owner == sf || destination == sf, + "original cell is owner or neighbor"); + t_assert(owner < destination, "owning cell sorts first"); + H3Index cells[2] = {0}; + t_assertSuccess(H3_EXPORT(edgeToCells)(edges[i], cells)); + t_assert(owner == cells[0], "owning cell is returned first"); + t_assert(destination == cells[1], + "destination cell is returned second"); + } + } + + TEST(cellToEdges_invalid) { + // Test case discovered by fuzzer that triggers cellsToEdge to fail + // within cellToEdges + H3Index invalid = 0x26262626262600fa; + H3Index edges[6] = {H3_NULL}; + t_assert(H3_EXPORT(cellToEdges)(invalid, edges) == E_NOT_NEIGHBORS, + "cellToEdges fails"); + } + + TEST(getEdgesFromPentagon) { + H3Index pentagon = 0x821c07fffffffff; + H3Index edges[6] = {H3_NULL}; + t_assertSuccess(H3_EXPORT(cellToEdges)(pentagon, edges)); + + int missingEdgeCount = 0; + for (int i = 0; i < 6; i++) { + if (edges[i] == 0) { + missingEdgeCount++; + } else { + t_assert(H3_EXPORT(isValidEdge)(edges[i]) == 1, + "edge is an edge"); + H3Index cells[2] = {0}; + t_assertSuccess(H3_EXPORT(edgeToCells)(edges[i], cells)); + t_assert(pentagon == cells[0] || pentagon == cells[1], + "origin is correct"); + t_assert(cells[0] < cells[1], + "destination is not origin and origin is lower"); + } + } + t_assert(missingEdgeCount == 1, + "Only one edge was deleted for the pentagon"); + } + + TEST(invalid_pentagon_edge) { + H3Index pentagonEdge = 0x821c07fffffffff; + H3_SET_MODE(pentagonEdge, H3_EDGE_MODE); + H3_SET_RESERVED_BITS(pentagonEdge, K_AXES_DIGIT); + t_assert(H3_EXPORT(isValidEdge)(pentagonEdge) == 0, + "Invalid edge off a pentagon"); + } + + TEST(nonNormalizedEdge) { + H3Index pentagon = 0x821c07fffffffff; + H3Index neighbors[7] = {H3_NULL}; + t_assertSuccess(H3_EXPORT(gridDisk)(pentagon, 1, neighbors)); + + for (int i = 0; i < 7; i++) { + if (neighbors[i] != H3_NULL && neighbors[i] != pentagon) { + H3Index edge; + t_assertSuccess( + H3_EXPORT(cellsToEdge)(pentagon, neighbors[i], &edge)); + H3Index originDestination[2] = {H3_NULL}; + t_assertSuccess( + H3_EXPORT(edgeToCells)(edge, originDestination)); + Direction revDir = directionForNeighbor(originDestination[1], + originDestination[0]); + H3Index fakeEdge = originDestination[1]; + H3_SET_MODE(fakeEdge, H3_EDGE_MODE); + H3_SET_RESERVED_BITS(fakeEdge, revDir); + t_assert(H3_EXPORT(isValidEdge)(fakeEdge) == 0, + "Edge in non normalized form is invalid"); + } + } + } + + TEST(cellToEdgesFailed) { + H3Index edges[6] = {H3_NULL}; + t_assert( + H3_EXPORT(cellToEdges)(0x7fffffffffffffff, edges) == E_CELL_INVALID, + "cellToEdges of invalid index"); + } + + TEST(edgeToBoundary_invalid) { + H3Index sf; + t_assertSuccess(H3_EXPORT(latLngToCell)(&sfGeo, 9, &sf)); + H3Index invalidEdge = sf; + H3_SET_MODE(invalidEdge, H3_EDGE_MODE); + CellBoundary cb; + t_assert( + H3_EXPORT(edgeToBoundary)(invalidEdge, &cb) == E_UNDIR_EDGE_INVALID, + "edgeToBoundary fails on invalid edge direction"); + + H3Index invalidEdge2 = sf; + H3_SET_RESERVED_BITS(invalidEdge2, 1); + H3_SET_BASE_CELL(invalidEdge2, NUM_BASE_CELLS + 1); + H3_SET_MODE(invalidEdge2, H3_EDGE_MODE); + t_assert(H3_EXPORT(edgeToBoundary)(invalidEdge2, &cb) != E_SUCCESS, + "edgeToBoundary fails on invalid edge indexing digit"); + } + + TEST(directedEdgeToEdge) { + H3Index edge; + t_assert(H3_EXPORT(directedEdgeToEdge)(0, &edge) == E_DIR_EDGE_INVALID, + "can't convert 0"); + H3Index sf; + t_assertSuccess(H3_EXPORT(latLngToCell)(&sfGeo, 9, &sf)); + H3Index edges[6] = {H3_NULL}; + t_assertSuccess(H3_EXPORT(originToDirectedEdges)(sf, edges)); + for (int i = 0; i < 6; i++) { + t_assertSuccess(H3_EXPORT(directedEdgeToEdge)(edges[i], &edge)); + t_assert(H3_EXPORT(isValidEdge)(edge) == 1, + "resulting edge is valid"); + H3Index originDestination[2]; + t_assertSuccess(H3_EXPORT(edgeToCells)(edge, originDestination)); + t_assert(originDestination[0] == sf || originDestination[1] == sf, + "one of the cells is the origin"); + t_assert(originDestination[0] != originDestination[1], + "there is another cell"); + int areNeighbors; + t_assertSuccess(H3_EXPORT(areNeighborCells)( + originDestination[0], originDestination[1], &areNeighbors)); + t_assert(areNeighbors == 1, "are neighbors"); + } + } +} diff --git a/src/apps/testapps/testEdgeExhaustive.c b/src/apps/testapps/testEdgeExhaustive.c new file mode 100644 index 0000000000..4b2889a124 --- /dev/null +++ b/src/apps/testapps/testEdgeExhaustive.c @@ -0,0 +1,161 @@ +/* + * Copyright 2022 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 testEdgeExhaustive.c + * @brief Tests functions for manipulating undirected edge H3Indexes + * + * usage: `testEdgeExhaustive` + */ + +#include +#include + +#include "constants.h" +#include "h3Index.h" +#include "latLng.h" +#include "test.h" +#include "utility.h" + +static void edge_correctness_assertions(H3Index h3) { + H3Index edges[6] = {H3_NULL}; + int pentagon = H3_EXPORT(isPentagon)(h3); + t_assertSuccess(H3_EXPORT(cellToEdges(h3, edges))); + + int emptyEdgeCount = 0; + for (int i = 0; i < 6; i++) { + if (edges[i] == H3_NULL) { + emptyEdgeCount++; + continue; + } + t_assert(H3_EXPORT(isValidEdge)(edges[i]) == 1, "edge is an edge"); + + H3Index originDestination[2] = {H3_NULL}; + t_assertSuccess(H3_EXPORT(edgeToCells)(edges[i], originDestination)); + t_assert(h3 == originDestination[0] || h3 == originDestination[1], + "origin is one of the cells"); + t_assert(originDestination[0] != originDestination[1], + "origin and destination are not the same"); + + H3Index reencode1, reencode2; + t_assertSuccess(H3_EXPORT(cellsToEdge)( + originDestination[0], originDestination[1], &reencode1)); + t_assertSuccess(H3_EXPORT(cellsToEdge)( + originDestination[1], originDestination[0], &reencode2)); + t_assert(reencode1 == reencode2, + "origin and destination produce same the edge either way"); + t_assert(reencode1 == edges[i], + "reencoded edges are the same as the original edge"); + + int isNeighbor; + t_assertSuccess(H3_EXPORT(areNeighborCells)( + originDestination[0], originDestination[1], &isNeighbor)); + t_assert(isNeighbor, "destination is a neighbor"); + } + if (pentagon) { + t_assert(emptyEdgeCount == 1, "last pentagon edge is empty"); + } else { + t_assert(emptyEdgeCount == 0, "non pentagon edges cannot be empty"); + } +} + +static void edge_length_assertions(H3Index h3) { + H3Index edges[6] = {H3_NULL}; + t_assertSuccess(H3_EXPORT(cellToEdges)(h3, edges)); + + for (int i = 0; i < 6; i++) { + if (edges[i] == H3_NULL) continue; + + double length; + t_assertSuccess(H3_EXPORT(edgeLengthRads)(edges[i], &length)); + double lengthKm; + t_assertSuccess(H3_EXPORT(edgeLengthKm)(edges[i], &lengthKm)); + double lengthM; + t_assertSuccess(H3_EXPORT(edgeLengthM)(edges[i], &lengthM)); + double directedEdgeLength; + H3Index asDirectedEdge = edges[i]; + H3_SET_MODE(asDirectedEdge, H3_DIRECTEDEDGE_MODE); + t_assertSuccess( + H3_EXPORT(edgeLengthRads)(asDirectedEdge, &directedEdgeLength)); + t_assert(length > 0, "length is positive"); + t_assert(lengthKm > length, "length in KM is greater than rads"); + t_assert(lengthM > lengthKm, "length in M is greater than KM"); + t_assert(fabs(length - directedEdgeLength) < EPSILON_RAD, + "edge and directed edge length are approximately equal"); + } +} + +static void edge_boundary_assertions(H3Index h3) { + H3Index edges[6] = {H3_NULL}; + t_assertSuccess(H3_EXPORT(cellToEdges)(h3, edges)); + H3Index revEdge; + CellBoundary edgeBoundary; + CellBoundary revEdgeBoundary; + + for (int i = 0; i < 6; i++) { + if (edges[i] == H3_NULL) continue; + t_assertSuccess(H3_EXPORT(edgeToBoundary)(edges[i], &edgeBoundary)); + H3Index originDestination[2] = {H3_NULL}; + t_assertSuccess(H3_EXPORT(edgeToCells)(edges[i], originDestination)); + t_assertSuccess(H3_EXPORT(cellsToDirectedEdge)( + originDestination[1], originDestination[0], &revEdge)); + t_assertSuccess( + H3_EXPORT(directedEdgeToBoundary)(revEdge, &revEdgeBoundary)); + + t_assert(edgeBoundary.numVerts == revEdgeBoundary.numVerts, + "numVerts is equal for edge and reverse"); + + for (int j = 0; j < edgeBoundary.numVerts; j++) { + int almostEqual = geoAlmostEqualThreshold( + &edgeBoundary.verts[j], + &revEdgeBoundary.verts[revEdgeBoundary.numVerts - 1 - j], + 0.000001); + t_assert(almostEqual, "Got expected vertex"); + } + } +} + +SUITE(edge) { + TEST(edge_correctness) { + iterateAllIndexesAtRes(0, edge_correctness_assertions); + iterateAllIndexesAtRes(1, edge_correctness_assertions); + iterateAllIndexesAtRes(2, edge_correctness_assertions); + iterateAllIndexesAtRes(3, edge_correctness_assertions); + iterateAllIndexesAtRes(4, edge_correctness_assertions); + } + + TEST(edge_length) { + iterateAllIndexesAtRes(0, edge_length_assertions); + iterateAllIndexesAtRes(1, edge_length_assertions); + iterateAllIndexesAtRes(2, edge_length_assertions); + iterateAllIndexesAtRes(3, edge_length_assertions); + iterateAllIndexesAtRes(4, edge_length_assertions); + } + + TEST(edge_boundary) { + iterateAllIndexesAtRes(0, edge_boundary_assertions); + iterateAllIndexesAtRes(1, edge_boundary_assertions); + iterateAllIndexesAtRes(2, edge_boundary_assertions); + iterateAllIndexesAtRes(3, edge_boundary_assertions); + iterateAllIndexesAtRes(4, edge_boundary_assertions); + // Res 5: normal base cell + iterateBaseCellIndexesAtRes(5, edge_boundary_assertions, 0); + // Res 5: pentagon base cell + iterateBaseCellIndexesAtRes(5, edge_boundary_assertions, 14); + // Res 5: polar pentagon base cell + iterateBaseCellIndexesAtRes(5, edge_boundary_assertions, 117); + // Res 6: Test one pentagon just to check for new edge cases + iterateBaseCellIndexesAtRes(6, edge_boundary_assertions, 14); + } +} diff --git a/src/h3lib/include/edge.h b/src/h3lib/include/edge.h new file mode 100644 index 0000000000..960c520727 --- /dev/null +++ b/src/h3lib/include/edge.h @@ -0,0 +1,29 @@ +/* + * Copyright 2022 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 edge.h + * @brief Edge functions for manipulating (undirected) edge indexes. + */ + +#ifndef EDGE_H +#define EDGE_H + +#include "algos.h" +#include "h3Index.h" + +H3Error wrapDirectedEdgeError(H3Error err); +H3Index edgeAsDirectedEdge(H3Index edge); + +#endif diff --git a/src/h3lib/include/h3api.h.in b/src/h3lib/include/h3api.h.in index 6d9be68bed..587decaab0 100644 --- a/src/h3lib/include/h3api.h.in +++ b/src/h3lib/include/h3api.h.in @@ -688,6 +688,58 @@ DECLSPEC H3Error H3_EXPORT(directedEdgeToBoundary)(H3Index edge, CellBoundary *gb); /** @} */ +/** @defgroup cellsToEdge cellsToEdge + * Functions for cellsToEdge + * @{ + */ +/** @brief returns the undirected edge H3Index for the specified cells */ +DECLSPEC H3Error H3_EXPORT(cellsToEdge)(H3Index cell1, H3Index cell2, + H3Index *out); +/** @} */ + +/** @defgroup isValidEdge isValidEdge + * Functions for isValidEdge + * @{ + */ +/** @brief returns whether the H3Index is a valid (undirected) edge */ +DECLSPEC int H3_EXPORT(isValidEdge)(H3Index edge); +/** @} */ + +/** @defgroup edgeToCells \ + * edgeToCells + * Functions for edgeToCells + * @{ + */ +/** @brief Returns the neighboring cells from the (undirected) edge H3Index */ +DECLSPEC H3Error H3_EXPORT(edgeToCells)(H3Index edge, H3Index *cells); +/** @} */ + +/** @defgroup cellToEdges \ + * cellToEdges + * Functions for cellToEdges + * @{ + */ +/** @brief Returns the 6 (or 5 for pentagons) edges associated with the H3Index + */ +DECLSPEC H3Error H3_EXPORT(cellToEdges)(H3Index origin, H3Index *edges); +/** @} */ + +/** @defgroup edgeToBoundary edgeToBoundary + * Functions for edgeToBoundary + * @{ + */ +/** @brief Returns the CellBoundary containing the coordinates of the edge */ +DECLSPEC H3Error H3_EXPORT(edgeToBoundary)(H3Index edge, CellBoundary *gb); +/** @} */ + +/** @defgroup directedEdgeToEdge directedEdgeToEdge + * Functions for directedEdgeToEdge + * @{ + */ +/** @brief Returns the undirected edge for the given directed edge */ +DECLSPEC H3Error H3_EXPORT(directedEdgeToEdge)(H3Index edge, H3Index *out); +/** @} */ + /** @defgroup cellToVertex cellToVertex * Functions for cellToVertex * @{ diff --git a/src/h3lib/lib/edge.c b/src/h3lib/lib/edge.c new file mode 100644 index 0000000000..aea4842b25 --- /dev/null +++ b/src/h3lib/lib/edge.c @@ -0,0 +1,179 @@ +/* + * Copyright 2022 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 edge.c + * @brief Edge functions for manipulating (undirected) edge indexes. + */ + +#include +#include + +#include "algos.h" +#include "constants.h" +#include "coordijk.h" +#include "h3Assert.h" +#include "h3Index.h" +#include "latLng.h" +#include "vertex.h" + +/** + * Wrap the error code from a directed edge function and present + * undirected edge errors instead. + */ +H3Error wrapDirectedEdgeError(H3Error err) { + if (err == E_DIR_EDGE_INVALID) { + return E_UNDIR_EDGE_INVALID; + } + return err; +} + +/** + * Allows for operations on an edge index as if it were a directed edge + * from the edge owner to the neighboring cell. + * @param edge Undirected edge index + * @return H3Index Directed edge index + */ +H3Index edgeAsDirectedEdge(H3Index edge) { + // Do not make indexes that are not edges look "valid". + if (H3_GET_MODE(edge) == H3_EDGE_MODE) { + H3_SET_MODE(edge, H3_DIRECTEDEDGE_MODE); + } + return edge; +} + +/** + * Returns an edge H3 index based on the provided neighboring cells + * @param cell1 An H3 hexagon index + * @param cell2 A neighboring H3 hexagon index + * @param out Output: the edge H3Index + */ +H3Error H3_EXPORT(cellsToEdge)(H3Index cell1, H3Index cell2, H3Index *out) { + bool cell1IsOrigin = cell1 < cell2; + H3Index origin = cell1IsOrigin ? cell1 : cell2; + H3Index dest = cell1IsOrigin ? cell2 : cell1; + H3Error edgeErr = H3_EXPORT(cellsToDirectedEdge)(origin, dest, out); + if (!edgeErr) { + H3_SET_MODE(*out, H3_EDGE_MODE); + return edgeErr; + } else { + return edgeErr; + } +} + +/** + * Determines if the provided H3Index is a valid edge index + * @param edge The edge H3Index + * @return 1 if it is an edge H3Index, otherwise 0. + */ +int H3_EXPORT(isValidEdge)(H3Index edge) { + if (H3_GET_MODE(edge) != H3_EDGE_MODE) { + return 0; + } + Direction neighborDirection = H3_GET_RESERVED_BITS(edge); + if (neighborDirection <= CENTER_DIGIT || neighborDirection >= NUM_DIGITS) { + return 0; + } + + H3Index cells[2] = {0}; + // We also rely on the first returned cell being the "owning" cell. + H3Error cellsResult = H3_EXPORT(edgeToCells)(edge, cells); + if (cellsResult) { + return 0; + } + if (H3_EXPORT(isPentagon)(cells[0]) && neighborDirection == K_AXES_DIGIT) { + // Deleted direction from a pentagon. + return 0; + } + if (cells[1] < cells[0]) { + // Not normalized + return 0; + } + + // If the owning cell is valid, we expect the destination cell will always + // be valid. + return H3_EXPORT(isValidCell)(cells[0]) && + ALWAYS(H3_EXPORT(isValidCell)(cells[1])); +} + +/** + * Returns the cell pair of hexagon IDs for the given edge ID. + * + * The first cell returned is always the "owning" cell of the edge. + * @param edge The edge H3Index + * @param cells Pointer to memory to store cell IDs + */ +H3Error H3_EXPORT(edgeToCells)(H3Index edge, H3Index *cells) { + // Note: this function will accept directed edges as well, but report + // E_UNDIR_EDGE_INVALID errors. + H3Index directedEdge = edgeAsDirectedEdge(edge); + H3Error cellsResult = H3_EXPORT(directedEdgeToCells)(directedEdge, cells); + if (cellsResult) { + return wrapDirectedEdgeError(cellsResult); + } + return E_SUCCESS; +} + +/** + * Provides all of the edges from the current H3Index. + * @param origin The origin hexagon H3Index to find edges for. + * @param edges The memory to store all of the edges inside. + */ +H3Error H3_EXPORT(cellToEdges)(H3Index origin, H3Index *edges) { + H3Index neighborRing[7] = {0}; + H3Error gridDiskErr = H3_EXPORT(gridDisk)(origin, 1, neighborRing); + if (gridDiskErr) { + return gridDiskErr; + } + int edgesIndex = 0; + for (int i = 0; i < 7; i++) { + if (neighborRing[i] != origin && neighborRing[i]) { + H3Error error = H3_EXPORT(cellsToEdge)(origin, neighborRing[i], + &edges[edgesIndex]); + if (error) { + return error; + } + edgesIndex++; + } + } + return E_SUCCESS; +} + +/** + * Provides the coordinates defining the edge. + * @param edge The edge H3Index + * @param cb The cellboundary object to store the edge coordinates. + */ +H3Error H3_EXPORT(edgeToBoundary)(H3Index edge, CellBoundary *cb) { + // Note: this function will accept directed edges as well, but report + // E_UNDIR_EDGE_INVALID errors. + H3Index directedEdge = edgeAsDirectedEdge(edge); + return wrapDirectedEdgeError( + H3_EXPORT(directedEdgeToBoundary)(directedEdge, cb)); +} + +/** + * Provides the undirected edge for a given directed edge. + * @param edge Directed edge + * @param out Output undirected edge + */ +H3Error H3_EXPORT(directedEdgeToEdge)(H3Index edge, H3Index *out) { + H3Index originDestination[2] = {0}; + H3Error odError = H3_EXPORT(directedEdgeToCells)(edge, originDestination); + if (odError) { + return odError; + } + return H3_EXPORT(cellsToEdge)(originDestination[0], originDestination[1], + out); +} diff --git a/src/h3lib/lib/latLng.c b/src/h3lib/lib/latLng.c index 42fd820291..6b531b9686 100644 --- a/src/h3lib/lib/latLng.c +++ b/src/h3lib/lib/latLng.c @@ -23,6 +23,7 @@ #include #include "constants.h" +#include "edge.h" #include "h3Assert.h" #include "h3api.h" #include "mathExtensions.h" @@ -423,13 +424,18 @@ H3Error H3_EXPORT(cellAreaM2)(H3Index cell, double *out) { /** * Length of a directed edge in radians. * - * @param edge H3 directed edge + * @param edge H3 directed or undirected edge * * @return length in radians */ H3Error H3_EXPORT(edgeLengthRads)(H3Index edge, double *length) { CellBoundary cb; + if (H3_EXPORT(isValidEdge)(edge)) { + // This function could potentially generate a E_DIREDGE_INVALID error + // later which would be confusing. + edge = edgeAsDirectedEdge(edge); + } H3Error err = H3_EXPORT(directedEdgeToBoundary)(edge, &cb); if (err) { return err; @@ -445,7 +451,7 @@ H3Error H3_EXPORT(edgeLengthRads)(H3Index edge, double *length) { } /** - * Length of a directed edge in kilometers. + * Length of a directed or undirected edge in kilometers. */ H3Error H3_EXPORT(edgeLengthKm)(H3Index edge, double *length) { H3Error err = H3_EXPORT(edgeLengthRads)(edge, length); @@ -454,7 +460,7 @@ H3Error H3_EXPORT(edgeLengthKm)(H3Index edge, double *length) { } /** - * Length of a directed edge in meters. + * Length of a directed or undirected edge in meters. */ H3Error H3_EXPORT(edgeLengthM)(H3Index edge, double *length) { H3Error err = H3_EXPORT(edgeLengthKm)(edge, length); diff --git a/website/docs/library/index/cell.md b/website/docs/library/index/cell.md index bda7e6aeb5..076dbec8d0 100644 --- a/website/docs/library/index/cell.md +++ b/website/docs/library/index/cell.md @@ -22,7 +22,7 @@ Child hexagons are linearly smaller than their parent hexagons. An H3 Cell index (mode 1) represents a cell (hexagon or pentagon) in the H3 grid system at a particular resolution. The components of the H3 Cell index are packed into a 64-bit integer in order, highest bit first, as follows: * 1 bit reserved and set to 0, -* 4 bits to indicate the H3 Cell index mode, +* 4 bits to indicate the H3 Cell index mode (1), * 3 bits reserved and set to 0, * 4 bits to indicate the cell resolution 0-15, * 7 bits to indicate the base cell 0-121, diff --git a/website/docs/library/index/directededge.md b/website/docs/library/index/directededge.md index 20a3dd5da1..ac0c6821a8 100644 --- a/website/docs/library/index/directededge.md +++ b/website/docs/library/index/directededge.md @@ -6,13 +6,13 @@ slug: /library/index/directededge ---
- +
An H3 Directed Edge index (mode 2) represents a single directed edge between two cells (an "origin" cell and a neighboring "destination" cell). The components of the H3 Directed Edge index are packed into a 64-bit integer in order, highest bit first, as follows: * 1 bit reserved and set to 0, -* 4 bits to indicate the H3 Unidirectional Edge index mode, +* 4 bits to indicate the H3 Directed Edge index mode (2), * 3 bits to indicate the edge (1-6) of the origin cell, * Subsequent bits matching the index bits of the [origin cell](./cell#h3-cell-index). diff --git a/website/docs/library/index/edge.md b/website/docs/library/index/edge.md new file mode 100644 index 0000000000..3f65bbb0fb --- /dev/null +++ b/website/docs/library/index/edge.md @@ -0,0 +1,76 @@ +--- +id: edge +title: Edge mode +sidebar_label: Edge mode +slug: /library/index/edge +--- + +An H3 Edge index (mode 3) represents a single undirected edge between two cells. One of the two cells is picked as an *origin* cell, which is used to calculate the canonical index of the edge. The components of the H3 Edge index are packed into a 64-bit integer in order, highest bit first, as follows: + +* 1 bit reserved and set to 0, +* 4 bits to indicate the H3 Edge index mode (3), +* 3 bits to indicate the edge (1-6) of the origin cell, +* Subsequent bits matching the index bits of the [origin cell](./cell#h3-cell-index). + +Of the two cells, the cell with the numerically lower `H3Index` is picked as the origin. + +## Bit layout of H3Index for directed edges + +The layout of an `H3Index` is shown below in table form. The interpretation of the "Mode-Dependent" field differs depending on the mode of the index. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
0x0F0x0E0x0D0x0C0x0B0x0A0x090x080x070x060x050x040x030x020x010x00
0x30Reserved (0)Mode (3)EdgeResolutionBase cell
0x20Base cellDigit 1Digit 2Digit 3Digit 4Digit 5
0x10Digit 5Digit 6Digit 7Digit 8Digit 9Digit 10
0x00Digit 10Digit 11Digit 12Digit 13Digit 14Digit 15
diff --git a/website/docs/library/index/h3indexing.md b/website/docs/library/index/h3indexing.md index 610bd68c65..7f60b629f8 100644 --- a/website/docs/library/index/h3indexing.md +++ b/website/docs/library/index/h3indexing.md @@ -7,7 +7,7 @@ slug: /core-library/h3Indexing ## Introduction -The H3 system assigns a unique hierarchical index to each cell. Each directed edge and vertex is assigned an index based on its origin or owner cell, respectively. +The H3 system assigns a unique hierarchical index to each cell. Each directed edge, edge, and vertex is assigned an index based on its origin or owner cell, respectively. ## H3Index Representation @@ -16,7 +16,7 @@ An `H3Index` is the 64-bit integer representation of an H3 index, which may be o * Mode 0 is reserved and indicates an [invalid H3 index](#invalid-index). * Mode 1 is an *[H3 Cell](../library/index/cell)* (Hexagon/Pentagon) index. * Mode 2 is an *[H3 Directed Edge](../library/index/directededge)* (Cell A -> Cell B) index. -* Mode 3 is planned to be a bidirectional edge (Cell A <-> Cell B). +* Mode 3 is an *[H3 Edge](../library/index/edge)* (Cell A <-> Cell B) index. * Mode 4 is an *[H3 Vertex](../library/index/vertex)* (i.e. a single vertex of an H3 Cell). The canonical string representation of an `H3Index` is the hexadecimal representation of the integer, using lowercase letters. The string representation is variable length (no zero padding) and is not prefixed or suffixed. diff --git a/website/docs/library/index/vertex.md b/website/docs/library/index/vertex.md index 5b9c5e6479..800d0769e3 100644 --- a/website/docs/library/index/vertex.md +++ b/website/docs/library/index/vertex.md @@ -12,7 +12,7 @@ slug: /library/index/vertex An H3 Vertex index (mode 4) represents a single topological vertex in H3 grid system, shared by three cells. Note that this does not include the distortion vertexes occasionally present in a cell's geographic boundary. An H3 Vertex is arbitrarily assigned one of the three neighboring cells as its "owner", which is used to calculate the canonical index and geographic coordinates for the vertex. The components of the H3 Vertex index are packed into a 64-bit integer in order, highest bit first, as follows: * 1 bit reserved and set to 0, -* 4 bits to indicate the H3 Vertex index mode, +* 4 bits to indicate the H3 Vertex index mode (4), * 3 bits to indicate the vertex number (0-5) of vertex on the owner cell, * Subsequent bits matching the index bits of the [owner cell](./cell#h3-cell-index). diff --git a/website/static/images/edge_mode.png b/website/static/images/diredge_mode.png similarity index 100% rename from website/static/images/edge_mode.png rename to website/static/images/diredge_mode.png