Skip to content

fix(h3t): render H3 hexagons crossing the antimeridian#211

Open
bbest wants to merge 2 commits into
walkerke:mainfrom
bbest:fix/h3t-antimeridian
Open

fix(h3t): render H3 hexagons crossing the antimeridian#211
bbest wants to merge 2 commits into
walkerke:mainfrom
bbest:fix/h3t-antimeridian

Conversation

@bbest

@bbest bbest commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Problem

H3 hexagons that cross the antimeridian (±180°) render as a jagged tear/gap in
add_h3t_source() (and add_h3j_source() / setH3JData()). h3-js
h3ToGeoBoundary() returns such a cell's vertices split between +179° and
−179°, so the resulting polygon ring spans ~358° of longitude. geojson-vt then
mis-tiles that "wraps-the-whole-globe" polygon, leaving a gap of missing/
degenerate cells along 180°.

Fix

The cell→polygon builder (generate() for the Polygon path) now detects
boundaries whose longitude span exceeds 180° and unwraps them — shifting the
negative-longitude vertices by +360° — so the ring is continuous near +180°
before geojson-vt tiles it. geojson-vt's own antimeridian wrap then emits the
cell on both sides of the seam. Non-crossing cells are untouched.

o.generate = h3id => {
  if (o.geometry_type !== 'Polygon') return utils.h3.h3ToGeo(h3id).reverse();
  const ring = utils.h3.h3ToGeoBoundary(h3id, true);
  let min = 180, max = -180;
  for (const p of ring) { if (p[0] < min) min = p[0]; if (p[0] > max) max = p[0]; }
  return (max - min > 180)
    ? [ring.map(p => [p[0] < 0 ? p[0] + 360 : p[0], p[1]])]   // unwrap across 180°
    : [ring];
};

Applied to the three generate() definitions in the bundled
inst/htmlwidgets/lib/h3j-h3t/h3j_h3t.js (addH3TSource / addH3JSource /
setH3JData). The same one-liner applies upstream to
INSPIDE/h3j-h3t src/index.js
— happy to file that PR too so a future h3j-h3t bump keeps the fix.

Verification

Tested against a live global OBIS h3t endpoint
(h3t.marinesensitivity.org, ES50 by H3 cell). Before: a wide jagged tear down
180°. After: hexagons render continuously across the antimeridian on both the
globe and flat (mercator) projections. (Verified with a headless capture; a
faint hairline can remain exactly at the tile boundary under software
rendering — a geojson-vt seam characteristic, not the original gap.)

This is a follow-up to #209 / #199.

bbest added a commit to MarineSensitivity/apps that referenced this pull request Jun 24, 2026
H3 hexagons crossing ±180° rendered as a jagged tear; pin the antimeridian-fixed
fork branch (bbest/mapgl@fix/h3t-antimeridian) until the upstream PR merges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bbest
bbest force-pushed the fix/h3t-antimeridian branch 3 times, most recently from bd622e6 to e4c4717 Compare June 24, 2026 11:03
H3 cells straddling ±180° rendered as a jagged tear/gap: h3-js
`h3ToGeoBoundary()` returns their vertices split between +179 and -179, so the
polygon spans ~358° of longitude and geojson-vt mis-tiles it.

Two parts, in the bundled `h3j-h3t` cell builders (addH3TSource / addH3JSource /
setH3JData):

1. fixTransmeridian (Nick Rabinowitz,
   https://observablehq.com/@nrabinowitz/mapbox-utils): a boundary ring with any
   arc > 180° lon has its negative-lon vertices shifted +360°, so it stays
   continuous near +180° instead of wrapping the globe. Complete for the
   non-tiled add_h3j_source / setH3JData.

2. For the tiled add_h3t_source, a crossing cell is fetched into more than one
   {z}/{x}/{y} tile; each cell is normalized by ±360° onto the side of the
   antimeridian the rendered tile sits on, so its halves draw in the correct
   tiles and meet seamlessly. Pairs with an antimeridian-aware tile filter on
   the h3t server that returns a crossing cell to both edge tiles.

No extra libraries; non-crossing cells are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bbest
bbest force-pushed the fix/h3t-antimeridian branch from e4c4717 to 1e52f60 Compare June 24, 2026 12:20
@bbest

bbest commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Here is

BEFORE

old

AFTER

new

Using code:

v <- list(
  new = "/Users/bbest/Github/bbest/mapgl",
  old = "/Users/bbest/Github/walkerke/mapgl")

for (i in seq_along(v)) {
  out <- names(v)[[i]]
  pkg <- v[[i]]

  devtools::load_all(pkg)
  library(webshot2)
  library(glue)
  source("/Users/bbest/Github/marinebon/obisindicators/R/h3t.R")

  out_html <- glue("{getwd()}/{out}.html")
  out_png <- glue("{getwd()}/{out}.png")
  message(glue("Creating {out_html} and {out_png}"))

  tiles <- obis_h3t_url(
    base_url = "h3tiles://h3t.marinesensitivity.org/h3t/{z}/{x}/{y}.h3t",
    indicator = "es", res_max = 4, release = "v20260624c")
  maplibre(
    style = carto_style("dark-matter"), center=c(180,5), zoom=1.9) |>
    add_h3t_source(id = "obis", tiles=tiles) |>
    add_fill_layer(
      id = "obis_fill", source = "obis", source_layer = "obis",
      fill_color = interpolate(
        column = "value", values = c(1,12,25,38,50),
        stops = c("#440154","#3b528b","#21918c","#5ec962","#fde725")), 
        fill_opacity = 0.5) |>
    htmlwidgets::saveWidget(out_html, selfcontained = TRUE)

  webshot(out_html, file = out_png, vwidth = 1200, vheight = 800, delay = 1)
}

bbest added a commit to marinebon/obisindicators that referenced this pull request Jul 21, 2026
0.4.1 only rendered the SQL text; the actual maps were still eval=FALSE. The
mapgl chunks build client-side MapLibre widgets that fetch tiles from the
deployed h3t service in the reader's browser, so they need no store/S3 at
build time — enable them:

- h3t: `map` chunk -> live ES50 hexagon map. Also fixes a latent bug: the
  interpolate(values=) had length 2 for a 3-stop viridis ramp (would have
  errored when evaluated) -> values = c(1, 25, 50).
- taxon_children: `map-records` (Cetacea records) and `spue-map` (SPUE) ->
  live maps.

Requires the h3t-antimeridian fix in mapgl (renders H3 hexes crossing the
dateline): pin Remotes: mapgl=bbest/mapgl@fix/h3t-antimeridian (walkerke/mapgl#211)
so CI installs it instead of CRAN mapgl (which lacks add_h3t_source).

scaling stays code-only — its chunks run live benchmark queries against the
deployed DuckDB store, which CI cannot reach.

Verified: both vignettes render with 0 errors and the interactive maps paint
real OBIS tiles from h3t.marinesensitivity.org (browser-checked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bbest added a commit to MarineSensitivity/server that referenced this pull request Jul 25, 2026
…ommit

Base image R 4.4.1 -> 4.6.1 (latest rocker/geospatial), pinned to an exact
patch release rather than a floating tag. The newer p3m snapshot also
resolves otel + snowflakeauth, which the 4.4.1 snapshot could not, so the
declared package set is back to exact parity with the audited container --
all 92 packages verified available against 4.6.1 before building.

GitHub packages now pin exact commits; a bare user/repo tracks a moving
default branch and is not reproducible:

  bbest/mapgl@1e52f60  head of walkerke/mapgl PR #211, "fix(h3t): render H3
    hexagons crossing the antimeridian". Still a FORK because that fix is
    NOT merged upstream -- the species app's h3t hexagons render wrongly
    without it. Swap back to a released walkerke/mapgl once #211 lands.
    walkerke/mapgl#211
  crazycapivara/h3-r@6b658e8 (3.7.2)
  yogevherz/plotme@ff9ea91 (0.1.0)

msens is deliberately left unpinned: release_marine-atlas.qmd reinstalls it
from the server checkout on every deploy, so a pin here would mislead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dSowFsQNd882KvLCuDXBF
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