-
Notifications
You must be signed in to change notification settings - Fork 550
Geodesic polygon coverage #1052
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
Thanks @holoskii for the contribution! Geodesic polyfill is an exciting addition that I think a lot of folks would use, myself included! Please bear with us as we try to find the time to properly review this PR. Some initial questions from my end:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your contribution and opening this PR! Please let me know if you have questions. I am beginning to review the contribution and will add more comments as I read & test.
| void _geoToHex2d(const LatLng *g, int res, int *face, Vec2d *v) { | ||
| Vec3d p = latLngToVec3(g); | ||
| _vec3dToHex2d(&p, res, face, v); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From a code change perspective, I note this is a little surprising because it appears it affects the indexing code path as well. (Not necessarily wrong, just something to review in detail)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extracted common bits into a helper to remove duplication. The original planar path behavior should be unchanged
| if (flags == CONTAINMENT_FULL || flags == CONTAINMENT_OVERLAPPING) { | ||
| uint32_t geodesicFlags = flags; | ||
| FLAG_SET_GEODESIC(geodesicFlags); | ||
| run(&geoPolygon, geodesicFlags, res); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may want geodesic mode to be in a separate fuzzer to avoid fuzzer timeout issues.
Come to think of it, if we have more complicated input for the flags, we may want to also want to begin having a fuzzer for polygons (with or without holes) that also tries various flag values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- The fuzzer timeout was due to a bug, not algorithmic slowness. After the fix, there should be no more timeouts.
- I kept planar and geodesic in a single fuzzer since they share the same entry point/interface. If runtime becomes an issue, we can first raise the timeout before splitting targets.
- For flags, there’s a test that enumerates all combinations:
TEST(invalidFlagsExperimental).
c9be803 to
11cf632
Compare
Fixes / changes
PerformanceI added a dedicated geodesic benchmark that exercises small, medium, and large shapes.
Notes:
|
1.1 - Yes. 1.2 - The geodesic approach behaves better for large polygons and near poles/antimeridian due to great-circle behavior, but it’s architecturally different from the planar path; there isn’t a practical feature we can “back-port” without re-implementing substantial pieces. 2 - We only share 3 - Performance metrics are now included in the main PR comment (see “Performance” above) with the new benchmark. |
|
A few small fixes for the CI:
|
|
@isaacbrodsky was right, separate fuzzer is a way to go. Old fuzzers left not enough time until timeout in each iteration. In the last commit I've added a separate geodesic fuzzer. PR is now fully ready for review |
|
Hi @nrabinowitz @dfellis @isaacbrodsky - friendly bump on this. |
Thanks for updating the PR and for the bump! We appreciate the contribution and know we need to review this, so it's just a matter of finding time to read through it in depth. We would like to bring this in for the next release of H3. |
| src/h3lib/include/polyfill_iterator.h | ||
| src/h3lib/include/geodesic_iterator.h | ||
| src/h3lib/include/geodesic_polygon.h | ||
| src/h3lib/include/geodesic_polygon_internal.h | ||
| src/h3lib/include/geodesic_cell_boundary.h |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very minor, but we can rename these files to camelCase as with other source files
| LatLng stateVerts[] = {{0.645778804554910, -1.903190792178713}, | ||
| {0.645682811446050, -1.780975856637062}, | ||
| {0.715595465293187, -1.781167842854781}, | ||
| {0.715578012000667, -1.903262350678044}, | ||
| {0.645778804554910, -1.903190792178713}}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think either these coordinates are inverted compared to the London/NYC coordinates below, or other way around?
| static int populateGeoLoop(GeoLoop *g, const uint8_t *data, size_t *offset, | ||
| size_t size) { | ||
| if (size < *offset + sizeof(int)) { | ||
| return 1; | ||
| } | ||
| int numVerts = *(const int *)(data + *offset); | ||
| *offset = *offset + sizeof(int); | ||
| g->numVerts = numVerts; | ||
| if (size < *offset + sizeof(LatLng) * numVerts) { | ||
| return 1; | ||
| } | ||
| g->verts = (LatLng *)(data + *offset); | ||
| *offset = *offset + sizeof(LatLng) * numVerts; | ||
| return 0; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could make a follow up issue to deduplicate the populateGeoLoop function between the three files its now in
| if (res == 0) { | ||
| res = 1; // resolution 1 tests more code paths compared to 0 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If res=0 is a plausible input we should exercise it under the fuzzer too.
| uint32_t flags = CONTAINMENT_OVERLAPPING; | ||
| FLAG_SET_GEODESIC(flags); | ||
|
|
||
| resetMemoryCounters(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to test the case that maxPolygonToCellsSizeExperimental fails due to memory allocation failing?
| // of a distortion vertex on the last edge | ||
| int additionalIteration = length == NUM_HEX_VERTS ? 1 : 0; | ||
|
|
||
| // convert each vertex to lat/lng |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // convert each vertex to lat/lng | |
| // convert each vertex to xyz |
| * Encodes a Vec3d coordinate to the corresponding icosahedral face and | ||
| * squared euclidean distance to that face center. | ||
| * | ||
| * @param g The spherical coordinates to encode. | ||
| * @param res The desired H3 resolution for the encoding. | ||
| * @param h The FaceIJK address of the containing cell at resolution res. | ||
| * @param v3d The Vec3d coordinates to encode. | ||
| * @param face The icosahedral face containing the coordinates. | ||
| * @param sqd The squared euclidean distance to its icosahedral face center. | ||
| */ | ||
| void _geoToFaceIjk(const LatLng *g, int res, FaceIJK *h) { | ||
| // first convert to hex2d | ||
| Vec2d v; | ||
| _geoToHex2d(g, res, &h->face, &v); | ||
| static void _vec3dToClosestFace(const Vec3d *v3d, int *face, double *sqd) { | ||
| // determine the icosahedron face | ||
| *face = 0; | ||
| // The distance between two farthest points is 2.0, therefore the square of | ||
| // the distance between two points should always be less or equal than 4.0 . | ||
| *sqd = 5.0; | ||
| for (int f = 0; f < NUM_ICOSA_FACES; ++f) { | ||
| double sqdT = vec3DistSq(&faceCenterPoint[f], v3d); | ||
| if (sqdT < *sqd) { | ||
| *face = f; | ||
| *sqd = sqdT; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would move this function to the bottom of the file if possible to minimize the diff
| if (iter->_polygon->geoloop.numVerts <= 0) { | ||
| iterErrorPolygonCompact(iter, E_DOMAIN); | ||
| return NULL; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a zero-vertex geodesic polygon is not allowed?
| if (iter->_polygon->numHoles > 0 && iter->_polygon->holes == NULL) { | ||
| iterErrorPolygonCompact(iter, E_DOMAIN); | ||
| return NULL; | ||
| } | ||
| for (int i = 0; i < iter->_polygon->numHoles; i++) { | ||
| if (iter->_polygon->holes[i].numVerts <= 0) { | ||
| iterErrorPolygonCompact(iter, E_DOMAIN); | ||
| return NULL; | ||
| } | ||
| } | ||
| GeodesicPolygon *poly = geodesicPolygonCreate(iter->_polygon); | ||
| if (!poly) { | ||
| iterErrorPolygonCompact(iter, E_MEMORY_ALLOC); | ||
| return NULL; | ||
| } | ||
| iter->geodesicPoly = poly; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot of these cases don't seem to be tested. numHoles being set while holes is NULL makes sense to check although if someone mismatched the holse and numHoles there is not a lot we can do about it in general.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding test coverage, do we want to update this PR to cover all cases, or merge this PR and then open additional PRs to complete the coverage of new code?
| return E_DOMAIN; | ||
| } | ||
|
|
||
| *out = (GeodesicLoop){0}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| *out = (GeodesicLoop){0}; | |
| *out = {0}; |
I assume this is not needed?
Summary
Adds the geodesic traversal mode to
polygonToCellsExperimental, letting us follow true great-circle edges when filling massive polygons (think continental footprints and polar caps). The PR builds the geodesic stack from the ground up—new vector math helpers, bounding caps, iterator wiring, and plenty of validation coverage—while keeping the default planar behavior unchanged (and safely rejecting geodesic flags on the legacy API).Highlights
New geodesic mode
FLAG_GEODESIC_MASK(with helpers) so callers can opt into geodesic coverage; validates acceptable containment modes (FULL,OVERLAPPING) at bothmaxPolygonToCellsSizeExperimentalandpolygonToCellsExperimental.Core geometry plumbing
GeodesicPolygonacceleration structures + helpers (geodesicPolygonCreate/Destroy, containment, boundary/cap intersection tests).latLngToVec3, cross/dot/normalize, distance helpers) and new conversions:vec3dToCell,cellToVec3,cellToGeodesicBoundary,cellToSphereCap.sphereCapTables.hfor both prod code and tests; exposesSphereCap/AABBhelpers.Iterator & polyfill updates
polyfill_iterator.h(with_extracontext) and wires ingeodesic_iterator.c.polygonToCellspath.Benchmarks, fuzzers, and tests
testGeodesicPolygonToCellsExperimental,testSphereCap,testVec3d, plus extendedtestBBoxInternal,testPolygonToCells(Experimental)flag validation, and memory-failure coverage.CMakeLists.txt/CMakeTests.cmake.Documentation
Attribution
This geodesic polyfill work originated at Yellowbrick Data (where I’m currently employed). We’re upstreaming it to the H3 project so the broader community can benefit and use it for coverage.