fix: render H3 cells crossing the antimeridian - #9
Open
bbest wants to merge 6 commits into
Open
Conversation
When a client pans to a tile whose bbox contains no H3 cells from the source (e.g. open ocean for a coastal dataset), geojson-vt's getTile() returns null. Passing null into vt-pbf.fromGeojsonVt() throws, and the promise rejection reaches MapLibre as a mangled "Cannot read properties of undefined (reading 'data')" error that prevents any subsequent tiles from rendering on the layer. Short-circuit empty tiles with callback(null, new Uint8Array(0)), which is a valid empty vector tile that MapLibre handles as "this tile has no features" without going through the error path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MapLibre GL JS v3 switched addProtocol's handler signature from
callback-style `(params, callback) => { callback(null, data) }` to
promise-style `(params, abortController) => Promise<{data}>`. MapLibre
GL JS v5 removed the callback signature entirely.
Previously the h3tsource handler assumed callback style. On v3+/v4
MapLibre invokes the handler as `e(params, abortController)` — trying
to call the AbortController as a function threw
"Uncaught (in promise) TypeError: e is not a function", which
propagated through MapLibre as
"Cannot read properties of undefined (reading 'data')" and tore down
the layer.
Handler now:
- Detects signature via typeof cbOrCtl === 'function'
- Reuses the caller-supplied AbortController on v3+, avoiding
double abort-timer setup
- Returns a Promise<{data}> for v3+/v4/v5
- Keeps the callback call for v2 compatibility
- Preserves the empty-tile short-circuit from v0.9.3
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- MapLibre GL JS v3+/v4/v5 promise API accepts Uint8Array OR ArrayBuffer per docs, but some internal code paths assume ArrayBuffer. Convert the vt-pbf Uint8Array output via .buffer.slice() to be explicit. - Empty-tile return: ArrayBuffer(0) instead of Uint8Array(0). - debug: true now logs tile zxy, feature count, source-layer name, and byte size so we can diagnose tile-decode issues from the browser console. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s (v0.9.6) The original parser split the whole URL on both '/' and '.' and took the last 4 segments as [z, x, y, 'h3t']. That works for plain URLs like .../5/5/12.h3t but breaks when a query string with dots is appended: .../5/5/12.h3t?q=...&release=v2026.04.08 split(/\/|\./) -> ['...', '5', '5', '12', 'h3t?q=...&release=v2026', '04', '08'] slice(-4, -1) -> ['12', 'h3t?q=...&release=v2026', '04'] * 1 -> [12, NaN, 4] getTile(z, NaN, y) returns nothing, every tile came back empty, and no hexagons rendered in the client even though the HTTP response was valid. Fix: strip the query string, then match '/<z>/<x>/<y>.h3t$' with a regex. Reject with a clear error if the path doesn't match instead of silently serving empty tiles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (v0.9.7)
MapLibre's addProtocol() registers into a window-level map:
REGISTERED_PROTOCOLS[scheme] = handler
so the last registration of a given scheme wins. When two sources
(e.g. two sides of a compare widget) both called addH3TSource(),
the second one clobbered the first's closure — including its
sourcelayer name. The surviving closure would then emit tiles with
the wrong sourcelayer, so only ONE side's layer ever found features
to render. Which side won depended on the race between the two
map 'load' events (hence the flip-flop on reload).
Fix: mint a unique scheme per source ('h3t1', 'h3t2', ...) and
rewrite h3tiles:// in the tile template to match. Each closure now
lives under its own key, so they coexist and both sides render.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cells straddling ±180° rendered as a tear/gap: h3ToGeoBoundary() returns their vertices split between +179 and -179, so the polygon spans ~358° of longitude and geojson-vt mis-tiles it. - generate(): apply Nick Rabinowitz fixTransmeridian (https://observablehq.com/@nrabinowitz/mapbox-utils) — a ring with any arc > 180° lon has its negative-lon vertices shifted +360°, staying continuous near +180° instead of wrapping the globe. Complete for the geojson (addH3JSource / setH3JData) path. - h3tsource handler: for the tiled vector path, a crossing cell is fetched into more than one {z}/{x}/{y} tile; normalize each cell by ±360° onto the side of the antimeridian the rendered tile sits on, so its halves draw in their respective edge tiles and meet at 180°. Pairs with an antimeridian-aware tile filter on the h3t server (returns a crossing cell to both edge tiles). No new dependencies; non-crossing cells are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <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.
Cells straddling ±180° rendered as a tear/gap (
h3ToGeoBoundary()splits vertices +179/−179 → ~358° polygon → geojson-vt mis-tiles).generate()now applies Nick Rabinowitz'sfixTransmeridian: a ring with any arc > 180° lon has its negative-lon vertices shifted +360°, staying continuous near +180°. Complete for the non-tiledaddH3JSource/setH3JData.h3tsourcehandler: for the tiled path, a crossing cell is fetched into more than one{z}/{x}/{y}tile, so each cell is normalized by ±360° onto the side of the antimeridian the rendered tile sits on — its halves then draw in their respective edge tiles and meet at 180°. Pairs with an antimeridian-aware tile filter on the h3t server (returns a crossing cell to both edge tiles; e.g. CalCOFI/api-h3t-py#1).src/index.jsonly (dist/is gitignored / rebuilt on publish). No new deps. Also carried in walkerke/mapgl#211 (which vendors this bundle).