Add antimeridian_split on the OverlayNG face surface#444
Draft
asinghvi17 wants to merge 2 commits into
Draft
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_faceslinks every half-edge to its face-ring successor (onext ∘ sym) through the existingnext_resultfield and reuses the unchanged_OverlayEdgeRing/free-hole machinery;_face_ring_locationreads a face's per-input location off the shared labels (the face lies on the right of every ring half-edge);_build_face_polygonskeeps the faces akeep(loc_a, loc_b)predicate selects._is_result_of_opgains 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 servescut, 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 aPolygon/MultiPolygonalong a pole-to-pole meridian into aMultiPolygonwhose pieces each stay within one 360°-wide longitude branch. An encoding repair, not a geometry correction — the spherical region is unchanged, andPolygon → MultiPolygonis why this is a transformation rather than aGeometryCorrection.loc_A == INTERIORare 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_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;nothingemits corners only.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).north_pole = (λp, φp)): the input is rotated into the CFrotated_latitude_longitude/ PROJ+proj=ob_tranframe (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.|Δlon|heuristics, which miss near-pole crossing edges; non-interacting parts pass through verbatim.antimeridian_splitaftersegmentize(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); Pythonantimeridianpackage fixtures;@testset_implementationsacross GeoInterface/LibGEOS/ArchGDAL; unsupported-trait errors.overlay_ng.jl(154) andoverlay_graph.jl(37,279) unchanged after the face-enumeration refactor.Test plan
a46d56570(full package suite + docs, 5 Julia matrices) — run 29543622992, all 7 jobs🤖 Generated with Claude Code