Add add_h3t_source() for tiled H3J (h3t) data#199
Open
bbest wants to merge 12 commits into
Open
Conversation
Exposes the bundled h3j-h3t.js addH3TSource() method to R. Lets users drive H3 hex layers from a tiled HTTP endpoint (one request per viewport) instead of loading a single monolithic H3J file up front. - R: add_h3t_source(map, id, tiles, sourcelayer, geometry_type, minzoom, maxzoom, promote_id, debug). Supports static maps and maplibre_proxy updates. - JS init: x.h3t_sources handler in maplibregl.js and maplibregl_compare.js. - JS proxy: add_h3t_sources message handler; drive-by fix for the missing add_h3j_sources handler (add_h3j_source()'s proxy path previously had no JS counterpart). R CMD check passes 0E/0W/0N. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
|
Very nice! |
Upstream fix in bbest/h3j-h3t@v0.9.3: empty tiles (no H3 cells in the requested bbox) now return an empty-but-valid MVT instead of throwing through MapLibre as "Cannot read properties of undefined (reading 'data')". This is mechanical — rebuilds the bundled JS and bumps the yaml version. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The compare widget has its own yaml — missed it in the previous bump. This is what the CalCOFI int-app uses (dual-panel sp/env compare map), which is why the browser was still loading h3j-h3t-0.9.2 after the mapgl reinstall and missing the empty-tile fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bundled maplibre-gl in this package uses the promise-based
addProtocol signature (v3+). h3j-h3t <= 0.9.3 assumed the old
callback signature, so MapLibre was calling our handler with
(params, abortController) and we tried to invoke the
AbortController as a callback, throwing
"Uncaught (in promise) TypeError: e is not a function" which
MapLibre reported as "Cannot read properties of undefined
(reading 'data')".
bbest/h3j-h3t@0.9.4 auto-detects the signature and returns a
Promise<{data}> on v3+, keeping callback compat for v2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… map In the compare widget each side only has its own layers, but the layers_control may legitimately reference ids from the other side so the toggle can fire on both maps simultaneously. The initial-visibility check was calling getLayoutProperty() on every id without try/catch, which threw "Cannot get style of non-existing layer" and halted applyMapModifications mid-init — nothing that ran after that step (no terrain, no controls, and more importantly the deeper parts of the flow) could execute, leaving the map blank. - Wrap the initial getLayoutProperty() in try/catch, defaulting to "visible". - Same pattern for the click handler: read visibility from whichever map actually has the layer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Author
…tion Logs '[mapgl] maplibregl_compare.js build: <date>' to the console so it's visible at a glance which version is actually loaded in the browser vs what's on disk. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…yer) So we can see which map (before/after) gets which sources and layers in the compare widget, to diagnose why only one side renders hexagons. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
getLayoutProperty() on a missing layer fires an error event via this.fire() rather than throwing, so the try/catch could not silence the "Cannot get style of non-existing layer" console noise. Pre-check with map.getLayer(id) in both the init visibility read and the onclick toggle path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These were added to diagnose the per-side addProtocol collision in h3j-h3t; that is fixed (v0.9.7 mints a unique scheme per source), so the logs are noise now. Build marker retained for install verification. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Author
|
All is working now, but hope to update with an easier reproducible example in the next couple of weeks. This update also depends on INSPIDE/h3j-h3t#8, and that JS library has not been updated in 4 years. Here's the (somewhat complicated) code of where it's working using
|
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.

Summary
Adds
add_h3t_source()— an R wrapper for theh3tiles://MapLibre protocol that's already bundled ininst/htmlwidgets/lib/h3j-h3t/h3j_h3t.jsbut previously had no R entry point. This lets users drive H3 hex layers from a tiled HTTP endpoint (one request per viewport) instead of loading a single monolithic H3J file up front.While wiring this up I also hit five bugs in the bundled
h3j-h3tJS that blocked it from working on MapLibre v3+/v4/v5 and from supporting multiple sources per page. Those are now fixed in the bundled copy (v0.9.7) and upstreamed in INSPIDE/h3j-h3t#8 — happy to swap the bundled file for INSPIDE's release once that PR lands.Motivation
add_h3j_source()is great when your whole dataset fits in a single JSON file, but it breaks down once you have hundreds of thousands of cells or want to filter on the fly. Theh3ttile protocol solves both — the browser only fetches cells inside the current viewport/zoom, and the HTTP layer becomes a natural cache key.Representative use case (a Shiny app doing species × environment compare): preloading all 10 H3 resolutions of a 500 k-row DuckDB aggregate was taking ~10 s per session and couldn't cache user filter changes. Moving to
add_h3t_source()dropped initial load to <2 s and made every filter change a server-side operation only.What's included
R / widget wiring
add_h3t_source(map, id, tiles, sourcelayer, geometry_type, minzoom, maxzoom, promote_id, debug)inR/h3j-h3t.R. Supports both static maps andmaplibre_proxyupdates.x.h3t_sourcesinit handler ininst/htmlwidgets/maplibregl.jsandinst/htmlwidgets/maplibregl_compare.js.add_h3t_sourcesproxy-message handler. Drive-by: also adds the missingadd_h3j_sourcesproxy handler —add_h3j_source()'s proxy branch previously had no JS counterpart.man/add_h3t_source.Rdwith a runnable example.maplibregl_compare.js's layers_control: pre-check withmap.getLayer(id)before callinggetLayoutProperty/setLayoutProperty, since the latter emits an error event (not a throw) when the layer isn't on that map. In a compare widget a singlelayers_controlcan reference ids from both sides, so this check is necessary to avoid console noise likeCannot get style of non-existing layer "x".Bundled
h3j-h3tJS: 0.9.2 → 0.9.7Upstreamed in INSPIDE/h3j-h3t#8. Summary:
ArrayBuffer(0)instead of nullCannot read properties of undefined (reading 'data')on ocean tiles of coastal datae is not a functionon every tile requestArrayBufferinstead ofUint8ArrayUint8Arrayvt-pbfreturns.h3t$rather than splitting on/and.?release=v2026.04.08) parsedNaNasx, every tile emptyh3t1://,h3t2://…) instead of the fixedh3tiles://map.addProtocol('h3tiles', handler)is a global registry — the second call overwrote the first, so two sources on one map (or one source on each side of a compare widget) clobbered each other'ssourcelayerNet:
add_h3t_source()now works cleanly against MapLibre 3.6.x – 5.22.x, with multiple sources on a single page and on both sides ofcompare().R CMD checkpasses with 0 errors / 0 warnings / 0 notes (with_R_CHECK_FORCE_SUGGESTS_=falseto skip the optionalmapboxapidep).Reproducible demo
A companion tile server lives at CalCOFI/api-h3t (MIT, standalone, pairs with any DuckDB file that has H3 cells). To reproduce:
The
{{res}}placeholder is a convention of the example server —add_h3t_source()itself is source-agnostic and will work with any endpoint that returns h3j JSON.Scope / non-goals
mapboxgl.jshas noaddH3TSourcesymbol today (and noaddH3JSourceeither). Happy to mirror if you'd like it in this PR.add_h3t_source()on amaplibre_proxyadds a new source, it doesn't mutate an existing one. Worth a separate PR once a clear API emerges.Test plan
R CMD check --no-manual→ 0E / 0W / 0Nmaplibre() |> add_h3t_source(...) |> add_fill_layer(...)renders; tiles requested on pan/zoom; MVT conversion happens client-sidemaplibre_proxy("map") |> add_h3t_source(...)triggersadd_h3t_sourcesmessage; source registerscompare(m1, m2)with h3t sources on both sides — both load and do not clobber each other (0.9.7 fix)🤖 Generated with Claude Code