From 284886826376481073a8833239185f4b36b28110 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Tue, 24 Mar 2026 15:38:15 +0200 Subject: [PATCH] fix(parcel-sync): fitBounds zoom + Martin config for enrichment tiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Map tab now uses fitBounds (not flyTo with fixed zoom) to show entire UAT extent when selected. Bounds are fetched and applied after map ready. - Added gis_terenuri_status to martin.yaml so Martin serves enrichment tiles (has_enrichment, has_building, build_legal properties). - Removed center/zoom props from MapViewer — use fitBounds via handle. - Requires `docker restart martin` on server for Martin to reload config. Co-Authored-By: Claude Opus 4.6 (1M context) --- martin.yaml | 20 ++ .../parcel-sync/components/tabs/map-tab.tsx | 200 +++++++++--------- 2 files changed, 116 insertions(+), 104 deletions(-) diff --git a/martin.yaml b/martin.yaml index a7e789f..af9c505 100644 --- a/martin.yaml +++ b/martin.yaml @@ -92,6 +92,26 @@ postgres: area_value: float8 layer_id: text + # ── Terenuri cu status enrichment (ParcelSync Harta tab) ── + + gis_terenuri_status: + schema: public + table: gis_terenuri_status + geometry_column: geom + srid: 3844 + bounds: [20.2, 43.5, 30.0, 48.3] + minzoom: 10 + maxzoom: 18 + properties: + object_id: text + siruta: text + cadastral_ref: text + area_value: float8 + layer_id: text + has_enrichment: int4 + has_building: int4 + build_legal: int4 + # ── Cladiri (buildings) — NO simplification ── gis_cladiri: diff --git a/src/modules/parcel-sync/components/tabs/map-tab.tsx b/src/modules/parcel-sync/components/tabs/map-tab.tsx index 5820fd2..5c20c8d 100644 --- a/src/modules/parcel-sync/components/tabs/map-tab.tsx +++ b/src/modules/parcel-sync/components/tabs/map-tab.tsx @@ -56,7 +56,7 @@ type MapTabProps = { }; /* ------------------------------------------------------------------ */ -/* Helpers — typed map operations */ +/* Typed map handle (avoids importing maplibregl types) */ /* ------------------------------------------------------------------ */ type MapLike = { @@ -64,9 +64,14 @@ type MapLike = { getSource(id: string): unknown; addSource(id: string, source: Record): void; addLayer(layer: Record, before?: string): void; + removeLayer(id: string): void; + removeSource(id: string): void; setFilter(id: string, filter: unknown[] | null): void; setLayoutProperty(id: string, prop: string, value: unknown): void; - fitBounds(bounds: [number, number, number, number], opts?: Record): void; + fitBounds( + bounds: [number, number, number, number], + opts?: Record, + ): void; isStyleLoaded(): boolean; }; @@ -89,40 +94,17 @@ export function MapTab({ siruta, sirutaValid }: MapTabProps) { [], ); const [boundsLoading, setBoundsLoading] = useState(false); - const [flyTarget, setFlyTarget] = useState< - { center: [number, number]; zoom?: number } | undefined - >(); const [mapReady, setMapReady] = useState(false); - const [viewsReady, setViewsReady] = useState(null); const appliedSirutaRef = useRef(""); + const boundsRef = useRef<[number, number, number, number] | null>(null); - /* Layer visibility: show terenuri + cladiri, hide admin */ + /* Layer visibility: show terenuri + cladiri, hide admin + UATs */ const [layerVisibility] = useState({ terenuri: true, cladiri: true, administrativ: false, }); - /* ── Check if enrichment views exist, create if not ────────── */ - useEffect(() => { - fetch("/api/geoportal/setup-enrichment-views") - .then((r) => r.json()) - .then((data: { ready?: boolean }) => { - if (data.ready) { - setViewsReady(true); - } else { - // Auto-create views - fetch("/api/geoportal/setup-enrichment-views", { method: "POST" }) - .then((r) => r.json()) - .then((res: { status?: string }) => { - setViewsReady(res.status === "ok"); - }) - .catch(() => setViewsReady(false)); - } - }) - .catch(() => setViewsReady(false)); - }, []); - /* ── Detect when map is ready ──────────────────────────────── */ useEffect(() => { if (!sirutaValid) return; @@ -136,7 +118,46 @@ export function MapTab({ siruta, sirutaValid }: MapTabProps) { return () => clearInterval(check); }, [sirutaValid]); - /* ── Apply siruta filter on base map layers ────────────────── */ + /* ── Fetch UAT bounds ──────────────────────────────────────── */ + const prevBoundsSirutaRef = useRef(""); + useEffect(() => { + if (!sirutaValid || !siruta) return; + if (prevBoundsSirutaRef.current === siruta) return; + prevBoundsSirutaRef.current = siruta; + + setBoundsLoading(true); + fetch(`/api/geoportal/uat-bounds?siruta=${siruta}`) + .then((r) => (r.ok ? r.json() : null)) + .then( + (data: { bounds?: [[number, number], [number, number]] } | null) => { + if (data?.bounds) { + const [[minLng, minLat], [maxLng, maxLat]] = data.bounds; + boundsRef.current = [minLng, minLat, maxLng, maxLat]; + + // If map already ready, fitBounds immediately + const map = asMap(mapHandleRef.current); + if (map) { + map.fitBounds([minLng, minLat, maxLng, maxLat], { + padding: 40, + duration: 1500, + }); + } + } + }, + ) + .catch(() => {}) + .finally(() => setBoundsLoading(false)); + }, [siruta, sirutaValid]); + + /* ── When map becomes ready, fitBounds if we have bounds ───── */ + useEffect(() => { + if (!mapReady || !boundsRef.current) return; + const map = asMap(mapHandleRef.current); + if (!map) return; + map.fitBounds(boundsRef.current, { padding: 40, duration: 1500 }); + }, [mapReady]); + + /* ── Apply siruta filter + enrichment overlay ──────────────── */ useEffect(() => { if (!mapReady || !sirutaValid || !siruta) return; if (appliedSirutaRef.current === siruta) return; @@ -147,28 +168,29 @@ export function MapTab({ siruta, sirutaValid }: MapTabProps) { appliedSirutaRef.current = siruta; const filter = ["==", ["get", "siruta"], siruta]; + // Filter base layers by siruta for (const layerId of BASE_LAYERS) { try { - if (!map.getLayer(layerId)) continue; - map.setFilter(layerId, filter); + if (map.getLayer(layerId)) map.setFilter(layerId, filter); } catch { - /* layer may not exist */ + /* noop */ } } - }, [mapReady, siruta, sirutaValid]); - /* ── Add enrichment overlay source + layers ────────────────── */ - useEffect(() => { - if (!mapReady || !viewsReady || !sirutaValid || !siruta) return; + // Hide base terenuri fill — we'll add enrichment overlay instead + try { + if (map.getLayer("l-terenuri-fill")) + map.setLayoutProperty("l-terenuri-fill", "visibility", "none"); + } catch { + /* noop */ + } - const map = asMap(mapHandleRef.current); - if (!map) return; + const martinBase = + typeof window !== "undefined" + ? `${window.location.origin}/tiles` + : "/tiles"; - const martinBase = typeof window !== "undefined" - ? `${window.location.origin}/tiles` - : "/tiles"; - - // Add gis_terenuri_status source (only once) + // Add enrichment source + layers (or update filter if already added) if (!map.getSource("gis_terenuri_status")) { map.addSource("gis_terenuri_status", { type: "vector", @@ -185,23 +207,21 @@ export function MapTab({ siruta, sirutaValid }: MapTabProps) { source: "gis_terenuri_status", "source-layer": "gis_terenuri_status", minzoom: 13, - filter: ["==", ["get", "siruta"], siruta], + filter, paint: { "fill-color": [ "case", - // Enriched parcels: darker green ["==", ["get", "has_enrichment"], 1], - "#15803d", - // No enrichment: lighter green - "#86efac", + "#15803d", // dark green: enriched + "#86efac", // light green: no enrichment ], "fill-opacity": 0.25, }, }, - "l-terenuri-line", // insert before line layer + "l-terenuri-line", // insert before outline ); - // Data-driven outline + // Data-driven outline: color by building status map.addLayer( { id: "l-ps-terenuri-line", @@ -209,7 +229,7 @@ export function MapTab({ siruta, sirutaValid }: MapTabProps) { source: "gis_terenuri_status", "source-layer": "gis_terenuri_status", minzoom: 13, - filter: ["==", ["get", "siruta"], siruta], + filter, paint: { "line-color": [ "case", @@ -237,61 +257,17 @@ export function MapTab({ siruta, sirutaValid }: MapTabProps) { "l-cladiri-fill", ); } else { - // Source already exists — just update filters for new siruta - const sirutaFilter = ["==", ["get", "siruta"], siruta]; + // Source already exists — update filters for new siruta try { if (map.getLayer("l-ps-terenuri-fill")) - map.setFilter("l-ps-terenuri-fill", sirutaFilter); + map.setFilter("l-ps-terenuri-fill", filter); if (map.getLayer("l-ps-terenuri-line")) - map.setFilter("l-ps-terenuri-line", sirutaFilter); + map.setFilter("l-ps-terenuri-line", filter); } catch { /* noop */ } } - - // Hide the base terenuri-fill (we replaced it with enrichment-aware version) - try { - if (map.getLayer("l-terenuri-fill")) - map.setLayoutProperty("l-terenuri-fill", "visibility", "none"); - } catch { - /* noop */ - } - }, [mapReady, viewsReady, siruta, sirutaValid]); - - /* ── Fetch UAT bounds and zoom ─────────────────────────────── */ - const prevBoundsSirutaRef = useRef(""); - useEffect(() => { - if (!sirutaValid || !siruta) return; - if (prevBoundsSirutaRef.current === siruta) return; - prevBoundsSirutaRef.current = siruta; - - setBoundsLoading(true); - fetch(`/api/geoportal/uat-bounds?siruta=${siruta}`) - .then((r) => (r.ok ? r.json() : null)) - .then( - (data: { - bounds?: [[number, number], [number, number]]; - } | null) => { - if (data?.bounds) { - const [[minLng, minLat], [maxLng, maxLat]] = data.bounds; - const centerLng = (minLng + maxLng) / 2; - const centerLat = (minLat + maxLat) / 2; - setFlyTarget({ center: [centerLng, centerLat], zoom: 13 }); - - // Fit bounds if map is already ready - const map = asMap(mapHandleRef.current); - if (map) { - map.fitBounds([minLng, minLat, maxLng, maxLat], { - padding: 40, - duration: 1500, - }); - } - } - }, - ) - .catch(() => {}) - .finally(() => setBoundsLoading(false)); - }, [siruta, sirutaValid]); + }, [mapReady, siruta, sirutaValid]); /* ── Feature click handler ─────────────────────────────────── */ const handleFeatureClick = useCallback( @@ -344,8 +320,6 @@ export function MapTab({ siruta, sirutaValid }: MapTabProps) { onFeatureClick={handleFeatureClick} onSelectionChange={setSelectedFeatures} layerVisibility={layerVisibility} - center={flyTarget?.center} - zoom={flyTarget?.zoom} /> {/* Top-right: basemap switcher + feature panel */} @@ -375,19 +349,37 @@ export function MapTab({ siruta, sirutaValid }: MapTabProps) { {/* Bottom-right: legend */}
- + Fără enrichment
- + Cu enrichment
- + Cu clădire
- + Clădire fără acte