From 9bf79a15ed98616cfd7ecdadf7062677c09b7875 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sun, 29 Mar 2026 14:56:49 +0300 Subject: [PATCH] fix(geoportal): proxy PMTiles through HTTPS + fix click/selection + optimize rebuild MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PMTiles was loaded via HTTP from MinIO (10.10.10.166:9002) on an HTTPS page, causing browser mixed-content blocking — parcels invisible on geoportal. Fixes: - tile-cache nginx proxies /pmtiles/ → MinIO with Range header support - PMTILES_URL changed to relative path (resolves to HTTPS automatically) - clickableLayers includes PMTiles fill layers (click on parcels works) - Selection highlight uses PMTiles source at z13+ (was Martin z17+ only) - tippecanoe per-layer zoom ranges (terenuri z13-z18, cladiri z14-z18) skips processing millions of features at z0-z12 — faster rebuild Co-Authored-By: Claude Opus 4.6 (1M context) --- docker-compose.yml | 6 ++-- nginx/tile-cache.conf | 32 +++++++++++++++++++ scripts/rebuild-overview-tiles.sh | 20 ++++++------ .../geoportal/components/map-viewer.tsx | 11 +++++-- 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index bb68424..c2a5c18 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: - NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME:-ArchiTools} - NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-https://tools.beletage.ro} - NEXT_PUBLIC_MARTIN_URL=https://tools.beletage.ro/tiles - - NEXT_PUBLIC_PMTILES_URL=http://10.10.10.166:9002/tiles/overview.pmtiles + - NEXT_PUBLIC_PMTILES_URL=/tiles/pmtiles/overview.pmtiles container_name: architools restart: unless-stopped ports: @@ -58,8 +58,8 @@ services: - ILOVEPDF_PUBLIC_KEY=${ILOVEPDF_PUBLIC_KEY:-} # Martin vector tile server (geoportal) - NEXT_PUBLIC_MARTIN_URL=https://tools.beletage.ro/tiles - # PMTiles overview tiles from MinIO (empty = use Martin for all layers) - - NEXT_PUBLIC_PMTILES_URL=http://10.10.10.166:9002/tiles/overview.pmtiles + # PMTiles overview tiles — proxied through tile-cache nginx (HTTPS, no mixed-content) + - NEXT_PUBLIC_PMTILES_URL=/tiles/pmtiles/overview.pmtiles # DWG-to-DXF sidecar - DWG2DXF_URL=http://dwg2dxf:5001 # Email notifications (Brevo SMTP) diff --git a/nginx/tile-cache.conf b/nginx/tile-cache.conf index 464f315..b8517f4 100644 --- a/nginx/tile-cache.conf +++ b/nginx/tile-cache.conf @@ -37,6 +37,38 @@ server { proxy_set_header Host $host; } + # PMTiles from MinIO — HTTPS proxy for browser access (avoids mixed-content block) + # Browser fetches: /pmtiles/overview.pmtiles → MinIO http://10.10.10.166:9002/tiles/overview.pmtiles + location /pmtiles/ { + proxy_pass http://10.10.10.166:9002/tiles/; + proxy_set_header Host 10.10.10.166:9002; + proxy_http_version 1.1; + + # Range requests — essential for PMTiles (byte-range tile lookups) + proxy_set_header Range $http_range; + proxy_set_header If-Range $http_if_range; + proxy_pass_request_headers on; + + # Browser cache — file changes only on rebuild (~weekly) + add_header Cache-Control "public, max-age=3600, stale-while-revalidate=86400" always; + + # CORS — PMTiles loaded from tools.beletage.ro page + add_header Access-Control-Allow-Origin "*" always; + add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always; + add_header Access-Control-Allow-Headers "Range, If-None-Match, If-Range, Accept-Encoding" always; + add_header Access-Control-Expose-Headers "Content-Range, Content-Length, ETag, Accept-Ranges" always; + + # Preflight + if ($request_method = OPTIONS) { + add_header Access-Control-Allow-Origin "*"; + add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS"; + add_header Access-Control-Allow-Headers "Range, If-Range"; + add_header Access-Control-Max-Age 86400; + add_header Content-Length 0; + return 204; + } + } + # Tile requests — cache aggressively location / { proxy_pass http://martin:3000; diff --git a/scripts/rebuild-overview-tiles.sh b/scripts/rebuild-overview-tiles.sh index ddaa05a..805261b 100644 --- a/scripts/rebuild-overview-tiles.sh +++ b/scripts/rebuild-overview-tiles.sh @@ -73,21 +73,23 @@ echo "[$(date -Iseconds)] Export complete." # ── Step 2: Generate PMTiles with tippecanoe ── echo "[$(date -Iseconds)] Generating PMTiles..." +# Per-layer zoom ranges — avoids processing features at zoom levels where they won't appear +# UAT boundaries: only at their respective zoom bands (saves processing z13-z18 for simple polygons) +# Terenuri/Cladiri: only z13+/z14+ (the expensive layers skip z0-z12 entirely) tippecanoe \ -o "$OUTPUT_FILE" \ - --named-layer=gis_uats_z0:uats_z0.fgb \ - --named-layer=gis_uats_z5:uats_z5.fgb \ - --named-layer=gis_uats_z8:uats_z8.fgb \ - --named-layer=gis_uats_z12:uats_z12.fgb \ - --named-layer=gis_administrativ:administrativ.fgb \ - --named-layer=gis_terenuri:terenuri_overview.fgb \ - --named-layer=gis_cladiri:cladiri_overview.fgb \ - --minimum-zoom=0 \ - --maximum-zoom=18 \ + -L'{"layer":"gis_uats_z0","file":"uats_z0.fgb","minzoom":0,"maxzoom":5}' \ + -L'{"layer":"gis_uats_z5","file":"uats_z5.fgb","minzoom":5,"maxzoom":8}' \ + -L'{"layer":"gis_uats_z8","file":"uats_z8.fgb","minzoom":8,"maxzoom":12}' \ + -L'{"layer":"gis_uats_z12","file":"uats_z12.fgb","minzoom":12,"maxzoom":14}' \ + -L'{"layer":"gis_administrativ","file":"administrativ.fgb","minzoom":10,"maxzoom":16}' \ + -L'{"layer":"gis_terenuri","file":"terenuri_overview.fgb","minzoom":13,"maxzoom":18}' \ + -L'{"layer":"gis_cladiri","file":"cladiri_overview.fgb","minzoom":14,"maxzoom":18}' \ --base-zoom=18 \ --drop-densest-as-needed \ --detect-shared-borders \ --simplification=10 \ + --no-tile-stats \ --hilbert \ --force diff --git a/src/modules/geoportal/components/map-viewer.tsx b/src/modules/geoportal/components/map-viewer.tsx index 18565b1..6156e89 100644 --- a/src/modules/geoportal/components/map-viewer.tsx +++ b/src/modules/geoportal/components/map-viewer.tsx @@ -480,7 +480,8 @@ export const MapViewer = forwardRef( "text-max-width": 8, }, paint: { "text-color": "#166534", "text-halo-color": "#fff", "text-halo-width": 1 } }); - // Martin source kept for selection highlight (no minzoom layer means no tile requests) + // Martin source registered but unused (selection uses PMTiles source now) + // Kept as fallback reference — no tile requests since no layers target it map.addSource(SOURCES.terenuri, { type: "vector", tiles: [`${m}/${SOURCES.terenuri}/{z}/{x}/{y}`], minzoom: 17, maxzoom: 18 }); } else { map.addSource(SOURCES.terenuri, { type: "vector", tiles: [`${m}/${SOURCES.terenuri}/{z}/{x}/{y}`], minzoom: 10, maxzoom: 18 }); @@ -539,10 +540,12 @@ export const MapViewer = forwardRef( paint: { "text-color": "#1e3a5f", "text-halo-color": "#fff", "text-halo-width": 1 } }); // === Selection highlight === - map.addLayer({ id: LAYER_IDS.selectionFill, type: "fill", source: SOURCES.terenuri, "source-layer": SOURCES.terenuri, minzoom: 13, + // Use PMTiles source when available (has data at z13+), Martin only has z17+ + const selectionSrc = usePmtiles ? "overview-pmtiles" : SOURCES.terenuri; + map.addLayer({ id: LAYER_IDS.selectionFill, type: "fill", source: selectionSrc, "source-layer": SOURCES.terenuri, minzoom: 13, filter: ["==", "object_id", "__NONE__"], paint: { "fill-color": "#f59e0b", "fill-opacity": 0.5 } }); - map.addLayer({ id: LAYER_IDS.selectionLine, type: "line", source: SOURCES.terenuri, "source-layer": SOURCES.terenuri, minzoom: 13, + map.addLayer({ id: LAYER_IDS.selectionLine, type: "line", source: selectionSrc, "source-layer": SOURCES.terenuri, minzoom: 13, filter: ["==", "object_id", "__NONE__"], paint: { "line-color": "#d97706", "line-width": 2.5 } }); @@ -571,8 +574,10 @@ export const MapViewer = forwardRef( }); /* ---- Click handler — NO popup, only callback ---- */ + // Include both Martin and PMTiles fill layers — filter() skips non-existent ones const clickableLayers = [ LAYER_IDS.terenuriFill, LAYER_IDS.cladiriFill, + "l-terenuri-pm-fill", "l-cladiri-pm-fill", ]; map.on("click", (e) => {