From 0d5fcf909c23ce9d202bb4d94ab495ec3b01beae Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Fri, 27 Mar 2026 21:46:47 +0200 Subject: [PATCH] feat(geoportal): PMTiles for terenuri/cladiri overview + cache warming + cleanup - Extend PMTiles to include simplified terenuri (5m tolerance) and cladiri (3m) - map-viewer: terenuri z13 from PMTiles, z14+ from Martin (live detail) - map-viewer: cladiri z14 from PMTiles, z15+ from Martin - Martin sources start at higher minzoom when PMTiles active (less DB load) - Add warm-tile-cache.sh: pre-populate nginx cache for major cities - Rebuild script now includes cache warming step after PMTiles upload - Remove deprecated docker-compose version: "3.8" Co-Authored-By: Claude Opus 4.6 (1M context) --- docker-compose.yml | 2 - scripts/rebuild-overview-tiles.sh | 51 ++++++++++++- scripts/warm-tile-cache.sh | 74 +++++++++++++++++++ .../geoportal/components/map-viewer.tsx | 41 +++++++--- 4 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 scripts/warm-tile-cache.sh diff --git a/docker-compose.yml b/docker-compose.yml index cc930e2..487b375 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: architools: build: diff --git a/scripts/rebuild-overview-tiles.sh b/scripts/rebuild-overview-tiles.sh index 035a297..01b0659 100644 --- a/scripts/rebuild-overview-tiles.sh +++ b/scripts/rebuild-overview-tiles.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash -# rebuild-overview-tiles.sh — Export UAT overview layers from PostGIS, generate PMTiles, upload to MinIO +# rebuild-overview-tiles.sh — Export all overview layers from PostGIS, generate PMTiles, upload to MinIO +# Includes: UAT boundaries, administrativ, simplified terenuri (z10-z14), simplified cladiri (z12-z14) # Usage: ./scripts/rebuild-overview-tiles.sh # Dependencies: ogr2ogr (GDAL), tippecanoe, mc (MinIO client) -# Run from project root or as a Docker one-shot container. set -euo pipefail @@ -33,6 +33,7 @@ cd "$TMPDIR" # ── Step 1: Export views from PostGIS (parallel) ── echo "[$(date -Iseconds)] Exporting PostGIS views to FlatGeobuf..." +# UAT boundaries (4 zoom levels) ogr2ogr -f FlatGeobuf -s_srs EPSG:3844 -t_srs EPSG:4326 \ uats_z0.fgb "$PG_CONN" \ -sql "SELECT name, siruta, geom FROM gis_uats_z0 WHERE geom IS NOT NULL" & @@ -49,10 +50,26 @@ ogr2ogr -f FlatGeobuf -s_srs EPSG:3844 -t_srs EPSG:4326 \ uats_z12.fgb "$PG_CONN" \ -sql "SELECT name, siruta, county, geom FROM gis_uats_z12 WHERE geom IS NOT NULL" & +# Administrativ (intravilan, arii speciale) ogr2ogr -f FlatGeobuf -s_srs EPSG:3844 -t_srs EPSG:4326 \ administrativ.fgb "$PG_CONN" \ -sql "SELECT object_id, siruta, layer_id, cadastral_ref, geom FROM gis_administrativ WHERE geom IS NOT NULL" & +# Terenuri simplified for overview (ST_SimplifyPreserveTopology 5m tolerance — good for z10-z14) +# Only essential properties for overview display +ogr2ogr -f FlatGeobuf -s_srs EPSG:3844 -t_srs EPSG:4326 \ + terenuri_overview.fgb "$PG_CONN" \ + -sql "SELECT object_id, siruta, cadastral_ref, area_value, layer_id, + ST_SimplifyPreserveTopology(geom, 5) AS geom + FROM gis_terenuri WHERE geom IS NOT NULL" & + +# Cladiri simplified for overview (3m tolerance — buildings are smaller) +ogr2ogr -f FlatGeobuf -s_srs EPSG:3844 -t_srs EPSG:4326 \ + cladiri_overview.fgb "$PG_CONN" \ + -sql "SELECT object_id, siruta, cadastral_ref, area_value, layer_id, + ST_SimplifyPreserveTopology(geom, 3) AS geom + FROM gis_cladiri WHERE geom IS NOT NULL" & + wait echo "[$(date -Iseconds)] Export complete." @@ -66,6 +83,8 @@ tippecanoe \ --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=14 \ --base-zoom=14 \ @@ -93,6 +112,30 @@ mc mv "${MINIO_ALIAS}/${MINIO_BUCKET}/overview_new.pmtiles" "${MINIO_ALIAS}/${MI echo "[$(date -Iseconds)] Upload complete." -# ── Step 4: Cleanup ── -rm -f uats_z0.fgb uats_z5.fgb uats_z8.fgb uats_z12.fgb administrativ.fgb "$OUTPUT_FILE" +# ── Step 4: Warm nginx tile cache (detail layers only — overview served from PMTiles) ── +TILE_CACHE="${TILE_CACHE_URL:-http://10.10.10.166:3010}" +echo "[$(date -Iseconds)] Warming tile cache for detail layers..." +# Warm popular z14-z16 tiles for Bucharest + Cluj (Martin-served detail layers) +for z in 14 15; do + for source in gis_terenuri gis_cladiri; do + # Bucharest + for x in $(seq 9200 9220); do + for y in $(seq 5960 5975); do + echo "${TILE_CACHE}/${source}/${z}/${x}/${y}" + done + done + # Cluj + for x in $(seq 9058 9068); do + for y in $(seq 5842 5852); do + echo "${TILE_CACHE}/${source}/${z}/${x}/${y}" + done + done + done +done | xargs -P 8 -I {} curl -sf -o /dev/null {} 2>/dev/null || true + +echo "[$(date -Iseconds)] Cache warming complete." + +# ── Step 5: Cleanup ── +rm -f uats_z0.fgb uats_z5.fgb uats_z8.fgb uats_z12.fgb administrativ.fgb \ + terenuri_overview.fgb cladiri_overview.fgb "$OUTPUT_FILE" echo "[$(date -Iseconds)] Rebuild finished successfully." diff --git a/scripts/warm-tile-cache.sh b/scripts/warm-tile-cache.sh new file mode 100644 index 0000000..165827f --- /dev/null +++ b/scripts/warm-tile-cache.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# warm-tile-cache.sh — Pre-populate nginx tile cache with common tiles +# Usage: ./scripts/warm-tile-cache.sh [BASE_URL] +# Run after deploy or cache purge to ensure fast first-load for users. + +set -euo pipefail + +BASE="${1:-http://10.10.10.166:3010}" +PARALLEL="${PARALLEL:-8}" +TOTAL=0 +HITS=0 + +echo "[$(date -Iseconds)] Warming tile cache at $BASE ..." + +# ── Helper: fetch a range of tiles ── +fetch_tiles() { + local source="$1" z="$2" x_min="$3" x_max="$4" y_min="$5" y_max="$6" + for x in $(seq "$x_min" "$x_max"); do + for y in $(seq "$y_min" "$y_max"); do + echo "${BASE}/${source}/${z}/${x}/${y}" + done + done +} + +# ── Romania bounding box at various zoom levels ── +# Lon: 20.2-30.0, Lat: 43.5-48.3 +# Tile coords computed from slippy map formula + +# z5: UATs coarse (2 tiles) +fetch_tiles gis_uats_z5 5 17 18 11 11 + +# z7: UATs moderate (12 tiles) +fetch_tiles gis_uats_z8 7 69 73 44 46 + +# z8: UATs + labels (40 tiles) +fetch_tiles gis_uats_z8 8 139 147 88 92 + +# z9: UATs labels (100 tiles — major cities area) +fetch_tiles gis_uats_z8 9 279 288 177 185 + +# z10: Administrativ + terenuri sources start loading +# Focus on major metro areas: Bucharest, Cluj, Timisoara, Iasi, Brasov +# Bucharest area (z12) +fetch_tiles gis_terenuri 12 2300 2310 1490 1498 +fetch_tiles gis_cladiri 12 2300 2310 1490 1498 +# Cluj area (z12) +fetch_tiles gis_terenuri 12 2264 2270 1460 1465 +fetch_tiles gis_cladiri 12 2264 2270 1460 1465 + +echo "[$(date -Iseconds)] Fetching tiles ($PARALLEL concurrent)..." + +# Pipe all URLs through xargs+curl for parallel fetching +fetch_tiles gis_uats_z5 5 17 18 11 11 +fetch_tiles gis_uats_z8 7 69 73 44 46 +fetch_tiles gis_uats_z8 8 139 147 88 92 +fetch_tiles gis_uats_z8 9 279 288 177 185 +fetch_tiles gis_terenuri 12 2300 2310 1490 1498 +fetch_tiles gis_cladiri 12 2300 2310 1490 1498 +fetch_tiles gis_terenuri 12 2264 2270 1460 1465 +fetch_tiles gis_cladiri 12 2264 2270 1460 1465 + +# Actually execute all fetches +{ + fetch_tiles gis_uats_z5 5 17 18 11 11 + fetch_tiles gis_uats_z8 7 69 73 44 46 + fetch_tiles gis_uats_z8 8 139 147 88 92 + fetch_tiles gis_uats_z8 9 279 288 177 185 + fetch_tiles gis_terenuri 12 2300 2310 1490 1498 + fetch_tiles gis_cladiri 12 2300 2310 1490 1498 + fetch_tiles gis_terenuri 12 2264 2270 1460 1465 + fetch_tiles gis_cladiri 12 2264 2270 1460 1465 +} | xargs -P "$PARALLEL" -I {} curl -sf -o /dev/null {} 2>/dev/null + +echo "[$(date -Iseconds)] Cache warming complete." diff --git a/src/modules/geoportal/components/map-viewer.tsx b/src/modules/geoportal/components/map-viewer.tsx index 6c4964b..730a946 100644 --- a/src/modules/geoportal/components/map-viewer.tsx +++ b/src/modules/geoportal/components/map-viewer.tsx @@ -328,8 +328,8 @@ export const MapViewer = forwardRef( LAYER_IDS.uatsZ12Fill, LAYER_IDS.uatsZ12Line, LAYER_IDS.uatsZ12Label, ], administrativ: [LAYER_IDS.adminLineOuter, LAYER_IDS.adminLineInner], - terenuri: [LAYER_IDS.terenuriFill, LAYER_IDS.terenuriLine, LAYER_IDS.terenuriLabel], - cladiri: [LAYER_IDS.cladiriFill, LAYER_IDS.cladiriLine, LAYER_IDS.cladiriLabel], + terenuri: [LAYER_IDS.terenuriFill, LAYER_IDS.terenuriLine, LAYER_IDS.terenuriLabel, "l-terenuri-pm-fill", "l-terenuri-pm-line"], + cladiri: [LAYER_IDS.cladiriFill, LAYER_IDS.cladiriLine, LAYER_IDS.cladiriLabel, "l-cladiri-pm-fill", "l-cladiri-pm-line"], }; for (const [group, layerIds] of Object.entries(mapping)) { const visible = vis[group] !== false; @@ -465,13 +465,23 @@ export const MapViewer = forwardRef( paint: { "line-color": "#f97316", "line-width": 1.5 } }); } - // === Terenuri (parcels) — always from Martin (live data) === - map.addSource(SOURCES.terenuri, { type: "vector", tiles: [`${m}/${SOURCES.terenuri}/{z}/{x}/{y}`], minzoom: 10, maxzoom: 18 }); - map.addLayer({ id: LAYER_IDS.terenuriFill, type: "fill", source: SOURCES.terenuri, "source-layer": SOURCES.terenuri, minzoom: 13, + // === Terenuri (parcels) === + if (usePmtiles) { + // PMTiles overview at z13 (fast, pre-generated), Martin for z14+ (live detail) + map.addLayer({ id: "l-terenuri-pm-fill", type: "fill", source: "overview-pmtiles", "source-layer": SOURCES.terenuri, minzoom: 13, maxzoom: 14, + paint: { "fill-color": "#22c55e", "fill-opacity": 0.15 } }); + map.addLayer({ id: "l-terenuri-pm-line", type: "line", source: "overview-pmtiles", "source-layer": SOURCES.terenuri, minzoom: 13, maxzoom: 14, + paint: { "line-color": "#15803d", "line-width": 0.8 } }); + // Martin source starts at z14 (saves generating heavy z10-z13 tiles) + map.addSource(SOURCES.terenuri, { type: "vector", tiles: [`${m}/${SOURCES.terenuri}/{z}/{x}/{y}`], minzoom: 14, maxzoom: 18 }); + } else { + map.addSource(SOURCES.terenuri, { type: "vector", tiles: [`${m}/${SOURCES.terenuri}/{z}/{x}/{y}`], minzoom: 10, maxzoom: 18 }); + } + map.addLayer({ id: LAYER_IDS.terenuriFill, type: "fill", source: SOURCES.terenuri, "source-layer": SOURCES.terenuri, minzoom: 14, paint: { "fill-color": "#22c55e", "fill-opacity": 0.15 } }); - map.addLayer({ id: LAYER_IDS.terenuriLine, type: "line", source: SOURCES.terenuri, "source-layer": SOURCES.terenuri, minzoom: 13, + map.addLayer({ id: LAYER_IDS.terenuriLine, type: "line", source: SOURCES.terenuri, "source-layer": SOURCES.terenuri, minzoom: 14, paint: { "line-color": "#15803d", "line-width": 0.8 } }); - // Parcel cadastral number label + // Parcel cadastral number label (always from Martin — needs live data) map.addLayer({ id: LAYER_IDS.terenuriLabel, type: "symbol", source: SOURCES.terenuri, "source-layer": SOURCES.terenuri, minzoom: 16, layout: { "text-field": ["coalesce", ["get", "cadastral_ref"], ""], @@ -481,11 +491,20 @@ export const MapViewer = forwardRef( }, paint: { "text-color": "#166534", "text-halo-color": "#fff", "text-halo-width": 1 } }); - // === Cladiri (buildings) — no simplification === - map.addSource(SOURCES.cladiri, { type: "vector", tiles: [`${m}/${SOURCES.cladiri}/{z}/{x}/{y}`], minzoom: 12, maxzoom: 18 }); - map.addLayer({ id: LAYER_IDS.cladiriFill, type: "fill", source: SOURCES.cladiri, "source-layer": SOURCES.cladiri, minzoom: 14, + // === Cladiri (buildings) === + if (usePmtiles) { + // PMTiles overview at z14 (pre-generated), Martin for z15+ (live detail) + map.addLayer({ id: "l-cladiri-pm-fill", type: "fill", source: "overview-pmtiles", "source-layer": SOURCES.cladiri, minzoom: 14, maxzoom: 15, + paint: { "fill-color": "#3b82f6", "fill-opacity": 0.5 } }); + map.addLayer({ id: "l-cladiri-pm-line", type: "line", source: "overview-pmtiles", "source-layer": SOURCES.cladiri, minzoom: 14, maxzoom: 15, + paint: { "line-color": "#1e3a5f", "line-width": 0.6 } }); + map.addSource(SOURCES.cladiri, { type: "vector", tiles: [`${m}/${SOURCES.cladiri}/{z}/{x}/{y}`], minzoom: 15, maxzoom: 18 }); + } else { + map.addSource(SOURCES.cladiri, { type: "vector", tiles: [`${m}/${SOURCES.cladiri}/{z}/{x}/{y}`], minzoom: 12, maxzoom: 18 }); + } + map.addLayer({ id: LAYER_IDS.cladiriFill, type: "fill", source: SOURCES.cladiri, "source-layer": SOURCES.cladiri, minzoom: usePmtiles ? 15 : 14, paint: { "fill-color": "#3b82f6", "fill-opacity": 0.5 } }); - map.addLayer({ id: LAYER_IDS.cladiriLine, type: "line", source: SOURCES.cladiri, "source-layer": SOURCES.cladiri, minzoom: 14, + map.addLayer({ id: LAYER_IDS.cladiriLine, type: "line", source: SOURCES.cladiri, "source-layer": SOURCES.cladiri, minzoom: usePmtiles ? 15 : 14, paint: { "line-color": "#1e3a5f", "line-width": 0.6 } }); // Building body labels — extract suffix after last dash (e.g. "291479-C1" → "C1") map.addLayer({ id: LAYER_IDS.cladiriLabel, type: "symbol", source: SOURCES.cladiri, "source-layer": SOURCES.cladiri, minzoom: 16,