Skip to content

Add antimeridian_split on the OverlayNG face surface#444

Draft
asinghvi17 wants to merge 2 commits into
overlayng-enginefrom
antimeridian-split
Draft

Add antimeridian_split on the OverlayNG face surface#444
asinghvi17 wants to merge 2 commits into
overlayng-enginefrom
antimeridian-split

Conversation

@asinghvi17

@asinghvi17 asinghvi17 commented Jul 17, 2026

Copy link
Copy Markdown
Member

Adds antimeridian_split — splitting lon/lat polygons at the antimeridian (or any seam, in any rotated frame) — built directly on the OverlayNG exact arrangement. Stacked on #443 (→ #442#416#434); merge in stack order.

What's here

Face enumeration on the OverlayNG graph (polygon_builder.jl +66, overlay_labeller.jl +11): the non-dissolving companion to the set-op pipeline. _build_faces links every half-edge to its face-ring successor (onext ∘ sym) through the existing next_result field and reuses the unchanged _OverlayEdgeRing/free-hole machinery; _face_ring_location reads a face's per-input location off the shared labels (the face lies on the right of every ring half-edge); _build_face_polygons keeps the faces a keep(loc_a, loc_b) predicate selects. _is_result_of_op gains a predicate-dispatch overload, so custom result semantics can also ride the dissolving pipeline. Pure refactor for existing behavior — the phase-2 engine and graph suites pass unchanged. This is the general surface that later serves cut, polygonize-from-linework, and coverage cleanup (design doc §4 / spike S6).

antimeridian_split(geom; antimeridian = 180.0, north_pole = nothing, pole_spacing = 5.0) (src/transformations/antimeridian_split.jl, ~375 lines with literate docs): cuts a Polygon/MultiPolygon along a pole-to-pole meridian into a MultiPolygon whose pieces each stay within one 360°-wide longitude branch. An encoding repair, not a geometry correction — the spherical region is unchanged, and Polygon → MultiPolygon is why this is a transformation rather than a GeometryCorrection.

  • Exact arrangement, no heuristics: the polygon and the 2-segment cutting meridian go through the phase-1 noder and phase-2 labeller; faces with loc_A == INTERIOR are kept. Seam vertices take their longitude branch from the incident cutting-arc half-edge's direction; an enclosed pole (a degree-1 dangle in the arrangement) emits the two-branch pole pair structurally — pole enclosure is proved by the labeller, never by winding heuristics.
  • Pole-edge resampling (pole_spacing, default 5°): the pole edge is a zero-length geodesic that pseudocylindrical projections map to a line, so emission densifies it into a strictly monotone constant-latitude row between the (always-present) branch corners. The infill points are the same kernel point — exactly zero spherical area, collinear in plate carrée; nothing emits corners only.
  • Arbitrary seam (antimeridian = λ): the cut runs along meridian λ with output longitudes in the closed branch [λn−360, λn] (lips at λn / λn−360). Input coordinates are untouched — only the cutting curve and the output encoding move — so exactness is fully preserved; the default seam is bit-identical to the fixed-±180 form (asserted).
  • Rotated pole (north_pole = (λp, φp)): the input is rotated into the CF rotated_latitude_longitude / PROJ +proj=ob_tran frame (north_pole_grid_longitude = 0) and split there; output coordinates are in the rotated frame, deliberately — a cutting curve that dead-ends inside the polygon needs the target frame's dual longitude encoding to emit valid (non-slit) pieces. The rotation is the only coordinate-moving step in the stack.
  • Fast path: per polygon part, a spherical open-hemisphere-convexity test on kernel points (an arc with both endpoints strictly on one side of the seam great circle, or both in the far hemisphere, cannot reach the seam) — no false negatives, unlike planar |Δlon| heuristics, which miss near-pole crossing edges; non-interacting parts pass through verbatim.
  • Documented composition rule: run antimeridian_split after segmentize (spherical segmentize of an on-meridian edge emits interpolated points whose atan2 longitude is +180 regardless of branch).

Validation

  • test/methods/clipping/overlayng/faces.jl (96 assertions): predicate-vs-enum equivalence on all 4 ops × 16 location pairs, offset-squares face census (A∩B/A\B/B\A/outer with labels, orientations, areas), polygon-with-hole faces, dangle doubling, the one-extraction-per-graph contract.
  • test/transformations/antimeridian_split.jl (2,604 assertions): seam-crossing box (exact lip vertex sets both branches); south/north caps with 73-point pole rows (strictly monotone, step ≤ 5°, corners exactly (±180, ±90)); corners-only mode with bit-equal areas; vertex-at-pole-without-enclosure (no row); touch-without-crossing; fast-path verbatim passthrough; seam-crossing and one-sided holes; arbitrary-seam cases (Greenwich, Pacific 330°≡−30°, default-vs-explicit-180 bit-identity); rotated-pole cap with a rotated-frame pole row and isometry-conserved area (measured ~2e-15, asserted 1e-12); Python antimeridian package fixtures; @testset_implementations across GeoInterface/LibGEOS/ArchGDAL; unsupported-trait errors.
  • Every emitted piece, in every case: LibGEOS-valid, no |Δlon| > 180° between consecutive vertices anywhere (no pole exemption), longitudes within the documented branch, spherical area conservation at rtol 1e-12.
  • Natural Earth 110m (data-availability-gated, 2,069 assertions): Russia → 14 pieces, Fiji → 3, Antarctica → 8 with the pole row, conservation ≤ 1e-13, all valid; rotated-pole Russia valid + conserved. (NE Antarctica is deliberately not rotated: its antimeridian-slit encoding is only OGC-valid because the slit sits on ±180 — a finding the tests document.)
  • Phase-2 regression: overlay_ng.jl (154) and overlay_graph.jl (37,279) unchanged after the face-enumeration refactor.

Test plan

  • CI green on the tip a46d56570 (full package suite + docs, 5 Julia matrices) — run 29543622992, all 7 jobs

🤖 Generated with Claude Code

asinghvi17 and others added 2 commits July 16, 2026 19:37
Enumerate every minimal ring (face) of the noded arrangement, the
non-dissolving companion to the op pipeline. `_build_faces` links each
half-edge to its face-ring successor (`onext ∘ sym`) through the existing
`next_result` field and reuses the unchanged `_OverlayEdgeRing` machinery,
`_face_ring_location` reads a face's per-input location off the shared
labels (the face lies on the RIGHT of every ring half-edge), and
`_build_face_polygons` keeps the faces a `keep(loc_a, loc_b)` predicate
selects. `_is_result_of_op` gains a predicate-dispatch overload and
`_mark_result_area_edges!` / `_mark_in_result_area!` loosen their `op`
signatures so any location predicate can stand in for a set-op code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`antimeridian_split(geom; antimeridian = 180.0, north_pole = nothing,
pole_spacing = 5.0)` re-expresses a seam-crossing lon/lat `Polygon` /
`MultiPolygon` as a `MultiPolygon` whose pieces each stay within one
360°-wide longitude branch, preserving the spherical region exactly (an
encoding repair, not a geometry change).

Each polygon part is noded against the cutting meridian as a dim-1
`NodedArrangement` on `Spherical()`, the faces with interior A-location are
extracted, and each face ring is emitted with seam-vertex branch assignment
(southward seam half-edge ⇒ west lip, northward ⇒ east lip) and pole
handling. A pole-enclosing piece's pole edge is a zero-length geodesic, so
`pole_spacing` resamples it into a monotone constant-latitude row (corners
bit-exact, `nothing` = corners only) that projects correctly under
pseudocylindrical maps while adding exactly zero area. A part that cannot
reach the seam (tested with a spherical open-hemisphere convexity primitive,
not a planar longitude heuristic, so pole-enclosing and near-pole edges are
caught) passes through unchanged on a fast path.

`antimeridian = λ` cuts along an arbitrary meridian (output branch
`[λn - 360, λn]`, default reproduced bit-for-bit); `north_pole = (λp, φp)`
rotates the input into a rotated-pole frame (CF `rotated_latitude_longitude`,
`north_pole_grid_longitude = 0`) and splits there, returning rotated-frame
coordinates. Rotation is the only coordinate-moving step; the exact
arrangement then operates exactly on the rotated copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant