Skip to content

Willjnz/antimeridian crossing test/fixes - #673

Draft
willjnz wants to merge 14 commits into
developmentseed:kyle/antimeridian-crossingfrom
willjnz:willjnz/antimeridian-crossing-fixes
Draft

willjnz wants to merge 14 commits into
developmentseed:kyle/antimeridian-crossingfrom
willjnz:willjnz/antimeridian-crossing-fixes

Conversation

@willjnz

@willjnz willjnz commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

This PR aims to test the open antimeridian PR and make it functional. I am using tiled Digital Earth Pacific data that is in PDC (EPSG:3832) which passes the vertical (not slanted) cut condition. One column of tiled COGs span the antimeridian.

An earlier revision of this PR included a heuristic that checked which side of common-space x=256 a reprojected point was on — it failed for a piece spanning more than ~180° of the earth. That heuristic has been replaced with a per-point correction that derives its reference from each point's own position (reusing the same GeoJSON west>east idiom already used for cut detection) rather than a fixed midpoint, so it's width-independent: the only remaining limit is a tile's total width staying under 360°, which isn't a new limitation — data ≥360° wide is self-overlapping and has no correct rendering regardless. See the antimeridian design doc's "Seam handling" section for the full derivation, including the two narrower fixes tried and superseded along the way.

Before After
Visualises 1 am-crossing, and 1 normal COG bad am before Pink AM line is just a visual guide. Visualises 9 normal and 3 am-crossing COGs as a MosaicLayer. Screenshot 2026-09-29 at 5 48 35 pm Screenshot 2026-09-29 at 5 48 38 pm

The above linked PR sets up a good approach. This PR continues this split-in-two mesh designand also aims to follow https://www.gadom.ski/antimeridian/latest/ where relevant.

This PR has significant overlap with my earlier PR #639, and should replace it.

This PR contains 2 examples, a simple COGLayer spanning the antimeridian, and a MosaicLayer/MultiCOGLayer example (supporting both requires seperate testing).

Technical details:

  • RasterTileLayer._renderAntimeridianTile emits two RasterLayers (west/east), each seeded via triangulateRectangle from its own UV sub-domain, sharing one texture — exactly the "split in the sublayer factory, everything else keeps its single-mesh contract" design.

  • Cut detection matches the MVP's stated scope. antimeridianCut/edgeUCut — vertical-only, rejects slanted (U_EPSILON tolerance), falls back to full mesh otherwise. Matches the code's own docstring; the design doc's broader "vertical and slanted" scope claim was already aspirational in docs(specs): add antimeridian seam-handling detail #576, not something this branch touched either way.

  • All 3 of kylebarron's review comments on antimeridian-cut.ts are addressed:

    • U_EPSILON now has a docstring
    • edgeUCut was hoisted to a top-level function
    • antimeridianCut's docstring now explicitly says general antimeridian handling should eventually be supported, just isn't yet
  • Test plan items present: cut-location unit tests (antimeridian-cut.test.ts), piece-reprojection unit tests (raster-tileset-2d-antimeridian.test.ts), and the cog-basic dev-only antimeridian.tif fixture entry from docs(specs): add antimeridian seam-handling detail #576 is still intact and untouched.

  • One real discrepancy — the design doc (2026-05-27-antimeridian-crossing-tile-design.md) is now stale on seam handling:

    • What the doc says: its "Seam handling" section describes the fix as "local to the wrapped (negative-side) piece only... if the projected X comes back positive, subtract one world-width." That's the original docs(specs): add antimeridian seam-handling detail #576 mechanism.
    • Why it's wrong: a single constant-per-piece shift breaks at the piece's own boundary corner, which coincides with the other piece's edge and needs no shift at all.
    • What this branch does instead: a per-point correction, in common-space, after projectPosition — each point's own fractional position along the tile derives its own expected value (reusing antimeridianCut's already-located seam and total width), snapped to the nearest representative of that expectation. Not piece-specific, not a native-CRS sign check, and — unlike an earlier revision of this same fix — not a fixed midpoint test either, which turned out to have its own width limit (full derivation in the design doc's "Seam handling" section).
    • Status: fixed — dev-docs/specs/2026-05-27-antimeridian-crossing-tile-design.md's "Seam handling" section now describes the shipped mechanism (both superseded mechanisms kept, one struck through, for history), and its "Traversal" bullet, "Edge cases", and "Test plan" sections are updated to match. Also added a new "Locating and selecting a crossing tile in the traversal" section documenting the fix described below, and dev-docs/world-copies.md documents why the world-copy offset cap (MAX_MAPS) didn't need to scale with piece width.
  • MosaicLayer/MultiCOGLayer support: normalizeSourceBbox (mosaic-layer.ts) unwraps a GeoJSON-flipped source bbox (RFC 7946 §5.2: minX > maxX marks a crossing) onto a continuous frame before it's indexed into Flatbush, so the spatial index, the priority-queue distance calculation, and MosaicTileset2D's own viewport search all agree on one bbox per source. MultiCOGLayer's debug overlay (_renderDebugLayers/pieceBoxWgs84) splits into matching west/east boxes for a crossing tile — MultiCOGLayer extends RasterTileLayer, so the actual mesh-splitting/reprojection path is shared automatically; only the debug-outline drawing (which works in plain lng/lat, not common-space) needed its own antimeridian handling.

  • This PR also covers visualising just one side of the AM-split COG/Mosaic. An earlier version of this PR would not show the right/east side of an am-split texture, if the left/west side was not in view. Root cause: raster-tile-traversal.ts's frustum-culling traversal had zero antimeridian awareness, three compounding defects:

    1. _getGenericBoundingVolume sampled 9 reference points per tile with no antimeridian correction — for a crossing tile some land on each edge of common space, producing a bounding volume wide enough to be found only by accident (when the seam itself was in view). Fixed by extracting the wrap-correction already used in buildPieceReprojection into a shared unwrapCommonSpaceX helper (antimeridian-cut.ts) and applying it before fitting the box. (This helper's own signature and internals were revised again in a later pass — see the "one symmetric per-point test" bullet above and the design doc's "Seam handling" section — but the traversal integration point described here didn't change.)
    2. Once (1) was fixed, the dataset-level insideBounds pre-filter in getTileIndices — built from the same lossy min/max wgs84Bounds — only touched the now-tight per-tile box at the seam instead of overlapping it, rejecting the tile at every zoom. Fixed by recomputing the dataset's own bounds from its real corner longitudes when the dataset itself crosses, instead of patching the already-mixed-up min/max.
    3. buildPieceReprojection built one shared reprojection for both pieces, always anchoring both near common-space x≈512 — correct when both pieces are in view together, wrong once only the east piece is in view (its mesh stayed a full world away from the camera, selected but never drawn on screen). Fixed by mirroring the correction per piece so each renders at its own natural position; deck.gl's existing repeat-rendering (subViewports) already draws whatever world copies the camera needs.
    • The world-copy offset-pass gate also changed from subViewports.length > 1 to subViewports != null in both traversal files — the former only tests whether the viewport itself currently straddles a seam, not whether a tile's (or, for MosaicTileset2D's Flatbush-based, lng/lat bbox search in mosaic-tileset-2d.ts, a source's) own position needs a shifted pass to be found once zoomed in tight to one side alone.

Test PR:

I have made a frankenstein monster of my 3 PRs to test, and they work well together for my needs (a combo of MosaicLayer, MultiCOGLayer that spans the antimeridian and needs nearest sampling). https://github.com/willjnz/deck.gl-raster/tree/willjnz/frankenstein. This combines:

willjnz and others added 14 commits September 29, 2026 09:44
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lify render-gray; fix stale-index crash in MosaicTileset2D
…r ramp

Drop WhiteIsZero (color inversion, not needed for this example) and
the texture-format-inference indirection (hardcode the known r16unorm
format). Swap in BlackIsZero, which still does the required single-band
-> RGB broadcast so the image doesn't render red-tinted, but leaves
values un-inverted: dark = low, bright = high.

This branch has not been deployed

No deployments
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