Skip to content

h3: add geodesic polygon filling - #179

Open
justinhwang wants to merge 1 commit into
masterfrom
feat/geodesic-polyfill
Open

justinhwang wants to merge 1 commit into
masterfrom
feat/geodesic-polyfill

Conversation

@justinhwang

Copy link
Copy Markdown
Collaborator

Summary

Adds geodesic polygon filling to PolygonToCellsExperimental, ported from the
reference library's open proposal. Setting the new ContainmentGeodesic flag
(bit 16) on ContainmentCenter, ContainmentFull or
ContainmentOverlapping treats polygon edges as great-circle arcs instead of
straight lines in latitude and longitude.

The planar fill is unchanged; the flag selects a separate traversal:

  • Loops are converted once to unit vectors on the sphere. Each edge keeps its
    great-circle normal and a Cartesian bounding box that accounts for the arc's
    extrema, and each loop keeps its centroid and a hemisphere pole used to
    reject far-side points before the spherical winding test.
  • The traversal walks the base cells and descends through CenterChild,
    pruning any coarse cell whose bounding cap cannot touch the polygon.
  • Edge crossings use plane side tests with the reference's epsilons, and the
    three containment modes decide from center containment, crossing, and
    whether a loop vertex lies inside the cell.
  • An outer loop that does not fit in a hemisphere, or any loop with every
    vertex on one great circle, is ErrDomain, and ContainmentGeodesic with
    ContainmentOverlappingBbox or an invalid base mode is ErrOptionInvalid.

Cell boundaries are now computed as unit vectors first and converted to
latitude and longitude in a final loop. The geodesic fill reads the vectors
directly, and CellToBoundary gets about 25% faster from the tighter loop.

Two deviations from the reference

Cap scale factor. The reference bounds a cell with a cap of radius
1.01 × maxEdgeLength[res]. Measured against every cell at resolutions 0 to
2, that holds the cell and its children but not its grandchildren, whose
boundary vertices reach about 3.5% beyond the edge length, and deeper
descendants reach about 4.5%. A polygon touching only such a vertex is pruned
at the coarse level and the descendant is dropped from an OVERLAPPING fill.
This port uses 1.05, which encloses every descendant three levels down for
all cells at resolutions 0 to 2. The reference fixtures' cell counts are
unchanged. I will report the finding upstream.

Bounding cap of the polygon. The reference prunes a coarse cell by
testing its cap against the polygon's Cartesian box through the box's far
corner. That corner lies outside the unit sphere, so the test is loose: for
Colorado, resolution 4 cells up to about 14° away survive and the traversal
examined 41,370 resolution 5 cells to return 1,051. This port also keeps a
bounding cap of the outer loop, centered on its hemisphere pole with the
farthest vertex as radius (an arc's farthest point from the pole is always a
vertex once the loop fits a hemisphere), and prunes a cell when the two caps
are disjoint. Both tests are conservative, so the results are identical.

polygon cells returned examined before examined after
Colorado, res 5 1,051 41,370 2,541
ellipse, res 4 904 14,398 1,624
London to New York sliver, res 3 57 4,101 2,457

Accuracy

  • The six reference fixtures (west US with and without a hole, Pacific, and a
    degenerate, a point and a line loop) produce exactly the reference counts in
    each mode.
  • Two oracle tests classify every cell center without the algorithm under
    test: a 360-gon inscribed in a 20° spherical circle, and a quadrilateral
    between 30°N and 40°N spanning 120°W to 60°W, where the arc latitude between
    two points on a parallel has a closed form. The geodesic center fill matches
    the oracle on every cell. The planar fill is wrong on 1,737 of the
    quadrilateral's resolution 4 cells, the band between the 40°N chord and the
    arc that climbs to 44.1°N at 90°W.
  • Grid-disk roundtrips at resolutions 0 to 2, orientation invariance,
    FULL-mode cells smaller than a polygon or holding a hole, and a tiny
    triangle on every boundary vertex of every resolution 2 cell to pin the cap
    factor.

Benchmarks

Overlapping mode, -count=10, benchstat against master:

CellToBoundary                               407.6n ± 0%   305.2n ± 4%  -25.13% (p=0.000 n=10)
Polyfill                                      1.508 ± 12%   1.513 ± 1%        ~ (p=0.393 n=10)
PolygonToCellsExperimental/colorado/planar                  291.2µ ± 7%
PolygonToCellsExperimental/colorado/geodesic                1.164m ± 1%
PolygonToCellsExperimental/ellipse/planar                   610.4µ ± 0%
PolygonToCellsExperimental/ellipse/geodesic                 1.917m ± 1%
PolygonToCellsExperimental/london_ny/planar                 557.1µ ± 1%
PolygonToCellsExperimental/london_ny/geodesic               1.138m ± 2%

Geodesic filling costs 2 to 4 times the planar fill on these polygons. With
the reference's box-only prune it cost 30 to 70 times.

Test plan

  • go test -count=1 -race -covermode atomic -coverprofile ./... passes with 100% coverage on both packages
  • golangci-lint run ./... reports no issues
  • Conformance suite passes unchanged; the planar fill is untouched

@coveralls

coveralls commented Oct 30, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 41139745511

Coverage remained the same at 100.0%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: 411 of 411 lines across 3 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 4076
Covered Lines: 4076
Line Coverage: 100.0%
Coverage Strength: 5682563.25 hits per line

💛 - Coveralls

PolygonToCellsExperimental accepts a ContainmentGeodesic flag on the
center, full and overlapping modes that treats polygon edges as
great-circle arcs instead of straight lines in latitude and longitude.
Loops become unit vectors with per-edge plane normals and bounding
boxes, points are tested by spherical winding behind a hemisphere-pole
reject, and the traversal descends from the base cells, pruning coarse
cells whose caps cannot touch the polygon.

Two measured changes from the reference proposal: the cell cap radius
is 1.05 edge lengths rather than 1.01, because grandchildren reach 3.5%
past their grandparent's edge length and were pruned away; and a cap
around the outer loop is tested alongside the Cartesian box, whose far
corner lies off the sphere and let cells 14° away survive, so Colorado
examines 2,541 cells instead of 41,370 for 1,051 returned.

Cell boundaries are built as unit vectors and converted in a final
loop, which the geodesic fill reads directly and which makes
CellToBoundary about 25% faster.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@justinhwang
justinhwang force-pushed the feat/geodesic-polyfill branch from 2fdaafa to a77dfdf Compare October 30, 2026 22:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants