From 7b01744fad04311d9d2b624eebc284c0f5d097c7 Mon Sep 17 00:00:00 2001 From: Claude VM Date: Wed, 20 May 2026 09:10:39 +0300 Subject: [PATCH] feat(geoportal-v2): on-map selection highlight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the user clicks a parcel or building, render a subtle overlay so they can tell at a glance which feature corresponds to the open info panel. Four new MapLibre layers: v2-terenuri-selected-fill — green tint (#15803d/0.25) v2-terenuri-selected-line — darker green stroke (#14532d/2.5px) v2-cladiri-selected-fill — strong blue (#1d4ed8/0.55) v2-cladiri-selected-line — navy stroke (#0c2050/1.6px) All four start with a filter that matches nothing (==,object_id,-1). A new useEffect in MapViewer watches `selectedFeature` (passed down from GeoportalV2's `clicked` state) and updates the filter on the matching layer pair via map.setFilter on every change. Switching TERENURI ↔ CLADIRI clears the other layer pair, so the highlight never doubles up. selectedFeature.objectId is the filter key — comes straight from PMTiles' object_id property and is reliable across both layers. If the value isn't numeric (search-dropdown features sometimes lack it), the filter falls back to no-match — the panel still works, just no on-map glow. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/modules/geoportal/v2/geoportal-v2.tsx | 1 + src/modules/geoportal/v2/map-viewer.tsx | 96 ++++++++++++++++++++++- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/modules/geoportal/v2/geoportal-v2.tsx b/src/modules/geoportal/v2/geoportal-v2.tsx index fd75d27..1d9db77 100644 --- a/src/modules/geoportal/v2/geoportal-v2.tsx +++ b/src/modules/geoportal/v2/geoportal-v2.tsx @@ -60,6 +60,7 @@ export function GeoportalV2() { ref={mapRef} basemap={basemap} onFeatureClick={handleFeatureClick} + selectedFeature={clicked} className="h-full w-full" /> diff --git a/src/modules/geoportal/v2/map-viewer.tsx b/src/modules/geoportal/v2/map-viewer.tsx index d4d7e17..5a247b4 100644 --- a/src/modules/geoportal/v2/map-viewer.tsx +++ b/src/modules/geoportal/v2/map-viewer.tsx @@ -96,11 +96,14 @@ export type MapViewerHandle = { interface Props { basemap: BasemapId; onFeatureClick: (f: ClickedFeatureLite | null) => void; + /** Currently selected feature — drives the highlight overlay so the + * user sees which parcel/building corresponds to the open info panel. */ + selectedFeature: ClickedFeatureLite | null; className?: string; } export const MapViewer = forwardRef(function MapViewer( - { basemap, onFeatureClick, className }, + { basemap, onFeatureClick, selectedFeature, className }, ref, ) { const containerRef = useRef(null); @@ -276,6 +279,54 @@ export const MapViewer = forwardRef(function MapViewer( }, }); + // ── Selection highlight ── + // Subtle on-map feedback for whichever feature the info panel is + // showing. Filter is keyed on object_id (more reliable than the + // sometimes-missing uuid). Default filter matches nothing — gets + // updated by the selectedFeature useEffect below on every panel + // change. + const NO_MATCH: maplibregl.FilterSpecification = [ + "==", + ["get", "object_id"], + -1, + ]; + map.addLayer({ + id: "v2-terenuri-selected-fill", + type: "fill", + source: PM_SRC, + "source-layer": "gis_terenuri", + minzoom: 13, + filter: NO_MATCH, + paint: { "fill-color": "#15803d", "fill-opacity": 0.25 }, + }); + map.addLayer({ + id: "v2-terenuri-selected-line", + type: "line", + source: PM_SRC, + "source-layer": "gis_terenuri", + minzoom: 13, + filter: NO_MATCH, + paint: { "line-color": "#14532d", "line-width": 2.5 }, + }); + map.addLayer({ + id: "v2-cladiri-selected-fill", + type: "fill", + source: PM_SRC, + "source-layer": "gis_cladiri", + minzoom: 13, + filter: NO_MATCH, + paint: { "fill-color": "#1d4ed8", "fill-opacity": 0.55 }, + }); + map.addLayer({ + id: "v2-cladiri-selected-line", + type: "line", + source: PM_SRC, + "source-layer": "gis_cladiri", + minzoom: 13, + filter: NO_MATCH, + paint: { "line-color": "#0c2050", "line-width": 1.6 }, + }); + // Building suffix label — "354686-C1" → "C1". Same logic eterra.live // uses: slice after the last dash. z16+ so we don't clutter the // overview when zoomed out. @@ -425,6 +476,49 @@ export const MapViewer = forwardRef(function MapViewer( [], ); + // Sync the selection-highlight filters with the currently-clicked + // feature. Filter on object_id (numeric, comes straight from PMTiles + // properties). When nothing is selected — or when the basemap is + // still loading — both filters fall back to "match nothing". + useEffect(() => { + const map = mapRef.current; + if (!map || !ready) return; + const TERENURI_LAYERS = ["v2-terenuri-selected-fill", "v2-terenuri-selected-line"]; + const CLADIRI_LAYERS = ["v2-cladiri-selected-fill", "v2-cladiri-selected-line"]; + const NO_MATCH: maplibregl.FilterSpecification = ["==", ["get", "object_id"], -1]; + const setLayerFilters = ( + ids: string[], + f: maplibregl.FilterSpecification, + ) => { + for (const id of ids) { + if (map.getLayer(id)) map.setFilter(id, f); + } + }; + if (!selectedFeature || !selectedFeature.objectId) { + setLayerFilters(TERENURI_LAYERS, NO_MATCH); + setLayerFilters(CLADIRI_LAYERS, NO_MATCH); + return; + } + const objectIdNum = Number(selectedFeature.objectId); + if (!Number.isFinite(objectIdNum)) { + setLayerFilters(TERENURI_LAYERS, NO_MATCH); + setLayerFilters(CLADIRI_LAYERS, NO_MATCH); + return; + } + const matchFilter: maplibregl.FilterSpecification = [ + "==", + ["get", "object_id"], + objectIdNum, + ]; + if (selectedFeature.layerId === "CLADIRI_ACTIVE") { + setLayerFilters(TERENURI_LAYERS, NO_MATCH); + setLayerFilters(CLADIRI_LAYERS, matchFilter); + } else { + setLayerFilters(TERENURI_LAYERS, matchFilter); + setLayerFilters(CLADIRI_LAYERS, NO_MATCH); + } + }, [selectedFeature, ready]); + return (