fix(geoportal): proxy PMTiles through HTTPS + fix click/selection + optimize rebuild

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) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-29 14:56:49 +03:00
parent b46eb7a70f
commit 9bf79a15ed
4 changed files with 54 additions and 15 deletions
+3 -3
View File
@@ -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)
+32
View File
@@ -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;
+11 -9
View File
@@ -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
@@ -480,7 +480,8 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
"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<MapViewerHandle, MapViewerProps>(
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<MapViewerHandle, MapViewerProps>(
});
/* ---- 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) => {