From 1d435dadaf2ac5ac11237b285cd6368ff044a74b Mon Sep 17 00:00:00 2001 From: Ben Best Date: Wed, 24 Jun 2026 13:36:13 +0200 Subject: [PATCH 1/2] fix: return antimeridian-crossing cells to both edge tiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A hexagon straddling ±180° has its centroid on one side, so the tile centroid filter returned it to only ONE edge tile; its far half (clipped at 180° in that tile) never rendered, leaving a seam — most visibly on a globe projection. Make the longitude filter antimeridian-aware: also match h3_cell_to_lng ± 360 against the (already buffered) tile bbox, so a crossing cell is returned to BOTH edge tiles. The client (h3j-h3t / mapgl add_h3t_source) normalizes each cell onto the side of the antimeridian the rendered tile sits on, so the halves meet seamlessly. Golden wrap_tile_sql fixtures regenerated; 67 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/h3t_query.py | 9 +++- tests/fixtures/r_golden.json | 82 +++++++++++++++++++++++++++++++++--- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/app/h3t_query.py b/app/h3t_query.py index 1a5368c..d72d5fc 100644 --- a/app/h3t_query.py +++ b/app/h3t_query.py @@ -98,7 +98,14 @@ def tile_bbox(z: int, x: int, y: int) -> TileBBox: " value,\n" " n\n" "FROM cells\n" - "WHERE h3_cell_to_lng(_cell) BETWEEN {lm:.10f} AND {lM:.10f}\n" + # antimeridian-aware: a cell straddling +/-180 must be returned to BOTH + # edge tiles (its centroid is on one side, but its geometry overhangs the + # other), so also match the +/-360 wrapped longitude against the (buffered) + # tile bbox. the client then places each cell on the side of the tile it + # is rendering. + "WHERE (h3_cell_to_lng(_cell) BETWEEN {lm:.10f} AND {lM:.10f}\n" + " OR h3_cell_to_lng(_cell) + 360 BETWEEN {lm:.10f} AND {lM:.10f}\n" + " OR h3_cell_to_lng(_cell) - 360 BETWEEN {lm:.10f} AND {lM:.10f})\n" " AND h3_cell_to_lat(_cell) BETWEEN {am:.10f} AND {aM:.10f}\n" "LIMIT {max_rows:d}" ) diff --git a/tests/fixtures/r_golden.json b/tests/fixtures/r_golden.json index 7d5c735..3028ecc 100644 --- a/tests/fixtures/r_golden.json +++ b/tests/fixtures/r_golden.json @@ -1,11 +1,79 @@ { "zoom_to_res": { - "z": [-1, 0, 0.5, 1, 1.5, 2.2, 2.5, 3.4, 4.6, 5.8, 7, 8.2, 9.4, 10.6, 11.8, 12, 13, 15, 20, 22, 22.5, 25], - "res": [1, 1, 1, 1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10, 10, 10, 10, 10] + "z": [ + -1, + 0, + 0.5, + 1, + 1.5, + 2.2, + 2.5, + 3.4, + 4.6, + 5.8, + 7, + 8.2, + 9.4, + 10.6, + 11.8, + 12, + 13, + 15, + 20, + 22, + 22.5, + 25 + ], + "res": [ + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 10 + ] }, "h3_edge_length_deg": { - "r": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - "deg": [3.75703205141601, 1.42002463939223, 0.536718864488001, 0.202860662770318, 0.0766741234982859, 0.028980094681474, 0.0109534462140408, 0.00414001352592486, 0.00156477803057726, 0.000591430503703552] + "r": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + ], + "deg": [ + 3.75703205141601, + 1.42002463939223, + 0.536718864488001, + 0.202860662770318, + 0.0766741234982859, + 0.028980094681474, + 0.0109534462140408, + 0.00414001352592486, + 0.00156477803057726, + 0.000591430503703552 + ] }, "tile_bbox": [ { @@ -72,7 +140,7 @@ "lat_max": 37.2827946491104 } ], - "wrap_tile_sql_has_n": "WITH user_q AS (\nSELECT hex_h3res5 AS cell_id, AVG(std_tally) AS value, COUNT(*) AS n FROM bio_obs WHERE species_id = 42 GROUP BY 1\n),\ncells AS (\n SELECT\n CAST(cell_id AS BIGINT) AS _cell,\n value::DOUBLE AS value,\n TRY_CAST(n AS BIGINT) AS n\n FROM user_q\n WHERE value IS NOT NULL\n AND NOT isnan(value::DOUBLE)\n AND isfinite(value::DOUBLE)\n)\nSELECT\n h3_h3_to_string(_cell) AS h3id,\n value,\n n\nFROM cells\nWHERE h3_cell_to_lng(_cell) BETWEEN -146.3650111852 AND -134.8849888148\n AND h3_cell_to_lat(_cell) BETWEEN 31.8371510528 AND 41.0949092549\nLIMIT 50000", - "wrap_tile_sql_no_n": "WITH user_q AS (\nSELECT hex_h3res5 AS cell_id, AVG(temperature) AS value FROM env_obs\n),\ncells AS (\n SELECT\n CAST(cell_id AS BIGINT) AS _cell,\n value::DOUBLE AS value,\n NULL::BIGINT AS n\n FROM user_q\n WHERE value IS NOT NULL\n AND NOT isnan(value::DOUBLE)\n AND isfinite(value::DOUBLE)\n)\nSELECT\n h3_h3_to_string(_cell) AS h3id,\n value,\n n\nFROM cells\nWHERE h3_cell_to_lng(_cell) BETWEEN -146.2500000000 AND -135.0000000000\n AND h3_cell_to_lat(_cell) BETWEEN 31.9521622380 AND 40.9798980696\nLIMIT 50000", + "wrap_tile_sql_has_n": "WITH user_q AS (\nSELECT hex_h3res5 AS cell_id, AVG(std_tally) AS value, COUNT(*) AS n FROM bio_obs WHERE species_id = 42 GROUP BY 1\n),\ncells AS (\n SELECT\n CAST(cell_id AS BIGINT) AS _cell,\n value::DOUBLE AS value,\n TRY_CAST(n AS BIGINT) AS n\n FROM user_q\n WHERE value IS NOT NULL\n AND NOT isnan(value::DOUBLE)\n AND isfinite(value::DOUBLE)\n)\nSELECT\n h3_h3_to_string(_cell) AS h3id,\n value,\n n\nFROM cells\nWHERE (h3_cell_to_lng(_cell) BETWEEN -146.3650111852 AND -134.8849888148\n OR h3_cell_to_lng(_cell) + 360 BETWEEN -146.3650111852 AND -134.8849888148\n OR h3_cell_to_lng(_cell) - 360 BETWEEN -146.3650111852 AND -134.8849888148)\n AND h3_cell_to_lat(_cell) BETWEEN 31.8371510528 AND 41.0949092549\nLIMIT 50000", + "wrap_tile_sql_no_n": "WITH user_q AS (\nSELECT hex_h3res5 AS cell_id, AVG(temperature) AS value FROM env_obs\n),\ncells AS (\n SELECT\n CAST(cell_id AS BIGINT) AS _cell,\n value::DOUBLE AS value,\n NULL::BIGINT AS n\n FROM user_q\n WHERE value IS NOT NULL\n AND NOT isnan(value::DOUBLE)\n AND isfinite(value::DOUBLE)\n)\nSELECT\n h3_h3_to_string(_cell) AS h3id,\n value,\n n\nFROM cells\nWHERE (h3_cell_to_lng(_cell) BETWEEN -146.2500000000 AND -135.0000000000\n OR h3_cell_to_lng(_cell) + 360 BETWEEN -146.2500000000 AND -135.0000000000\n OR h3_cell_to_lng(_cell) - 360 BETWEEN -146.2500000000 AND -135.0000000000)\n AND h3_cell_to_lat(_cell) BETWEEN 31.9521622380 AND 40.9798980696\nLIMIT 50000", "wrap_stats_sql": "WITH user_q AS (\nSELECT hex_h3res5 AS cell_id, AVG(std_tally) AS value, COUNT(*) AS n FROM bio_obs WHERE species_id = 42 GROUP BY 1\n)\nSELECT\n MIN(value::DOUBLE) AS min,\n MAX(value::DOUBLE) AS max,\n approx_quantile(value::DOUBLE, 0.02) AS p02,\n approx_quantile(value::DOUBLE, 0.98) AS p98,\n COUNT(*) AS n\nFROM user_q\nWHERE value IS NOT NULL\n AND NOT isnan(value::DOUBLE)\n AND isfinite(value::DOUBLE)" -} +} \ No newline at end of file From 67dae4ef93bdbb9a43e00b3f6c6c405bcd282a38 Mon Sep 17 00:00:00 2001 From: Ben Best Date: Wed, 1 Jul 2026 16:23:09 +0200 Subject: [PATCH 2/2] =?UTF-8?q?rename:=20int-app=20=E2=86=92=20db-viz-hex?= =?UTF-8?q?=20references?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- app/h3t_query.py | 2 +- deploy.md | 20 ++++++++++---------- scripts/parity_check.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/h3t_query.py b/app/h3t_query.py index d72d5fc..de80ad1 100644 --- a/app/h3t_query.py +++ b/app/h3t_query.py @@ -13,7 +13,7 @@ # --- zoom / resolution --------------------------------------------------- -# mirrors int-app/app/global.R:175-181 (and api-h3t/h3t_query.R:7-13) +# mirrors db-viz-hex/app/global.R:175-181 (and api-h3t/h3t_query.R:7-13) def _make_zoom_breaks() -> list[float]: min_res, max_res = 1, 10 n_breaks = (max_res - min_res + 1) + 1 # 11 diff --git a/deploy.md b/deploy.md index 957da67..6148103 100644 --- a/deploy.md +++ b/deploy.md @@ -62,7 +62,7 @@ Varnish keeps hitting the R service until step 6. # always serve plain JSON. H3T_APP_GZIP: "false" volumes: - - /share/github/int-app/data:/data:ro + - /share/github/db-viz-hex/data:/data:ro expose: - "8889" ``` @@ -144,11 +144,11 @@ ETags match. If any divergence shows up, **do not proceed**. > `docker network ls | grep server` will reveal it > (typically `server_default` if your compose project is in `server/`). -### Alternative — extend parity to int-app real queries +### Alternative — extend parity to db-viz-hex real queries For a more thorough test, extend `scripts/parity_check.py`'s `QUERIES` dict with the actual SQL templates from -`int-app/app/functions_h3t.R` (species + env queries with real species +`db-viz-hex/app/functions_h3t.R` (species + env queries with real species ids and date ranges). Run again before flipping Varnish. ## Step 6 — flip Varnish to the new backend @@ -200,14 +200,14 @@ sudo docker compose exec varnish varnishadm ban 'req.url ~ "^/h3t/"' curl -s https://h3t.calcofi.io/h3t/health | jq . # expect: {"ok":true, "default_db":"default", "dbs":{...}} # — note the new shape (R returned a different shape). -# If int-app or other clients parsed /health, audit them. +# If db-viz-hex or other clients parsed /health, audit them. curl -sI "https://h3t.calcofi.io/h3t/4/3/6.h3t?q=$Q&release=v2026.04.08" \ | grep -Ei '^(HTTP|X-Cache|ETag|Cache-Control|X-Calcofi-Release)' # first hit: X-Cache: MISS; repeat: X-Cache: HIT ``` -Open `int-app` (`https://app.calcofi.io` or wherever it's routed) and +Open `db-viz-hex` (`https://app.calcofi.io` or wherever it's routed) and pan/zoom on the map. Confirm: - Tiles render without visible seams. - Legend populates (stats endpoint works). @@ -266,7 +266,7 @@ Same flow as the R service: ```bash # 1. flip the symlink to the new release sudo ln -sfn calcofi_v2026.MM.DD.duckdb \ - /share/github/int-app/data/calcofi_latest.duckdb + /share/github/db-viz-hex/data/calcofi_latest.duckdb # 2. bounce the API so it reopens the DuckDB file sudo docker compose restart h3t_api_py @@ -275,10 +275,10 @@ sudo docker compose restart h3t_api_py # won't hit them, but this kills any stale entries explicitly) sudo docker compose exec varnish varnishadm ban 'req.url ~ "^/h3t/"' -# 4. update H3T_RELEASE in the int-app's .Renviron and restart it +# 4. update H3T_RELEASE in the db-viz-hex's .Renviron and restart it echo 'H3T_RELEASE=v2026.MM.DD' \ - | sudo tee /srv/shiny-server/int-app/.Renviron > /dev/null -sudo touch /srv/shiny-server/int-app/restart.txt + | sudo tee /srv/shiny-server/db-viz-hex/.Renviron > /dev/null +sudo touch /srv/shiny-server/db-viz-hex/restart.txt ``` ## Troubleshooting @@ -302,7 +302,7 @@ sudo docker compose exec varnish curl -sI \ ## Breaking-change call-outs These are intentional changes from the R service. Clients other than -`int-app` should be audited: +`db-viz-hex` should be audited: 1. **`/h3t/health` response shape** — R returned `{"ok":true, "db":"...", "db_mtime":"..."}`. Python returns diff --git a/scripts/parity_check.py b/scripts/parity_check.py index 1ac724c..d01729b 100644 --- a/scripts/parity_check.py +++ b/scripts/parity_check.py @@ -28,7 +28,7 @@ # Curated query fixture — keep these synthetic so they don't depend on the -# specific table schema. Extend with real int-app queries before cutover. +# specific table schema. Extend with real db-viz-hex queries before cutover. QUERIES: dict[str, str] = { "minimal_const": ( "SELECT 599405990948208639::BIGINT AS cell_id, 1.0 AS value, 1 AS n"