feat(geoportal-v2): on-map selection highlight
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) <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,7 @@ export function GeoportalV2() {
|
|||||||
ref={mapRef}
|
ref={mapRef}
|
||||||
basemap={basemap}
|
basemap={basemap}
|
||||||
onFeatureClick={handleFeatureClick}
|
onFeatureClick={handleFeatureClick}
|
||||||
|
selectedFeature={clicked}
|
||||||
className="h-full w-full"
|
className="h-full w-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -96,11 +96,14 @@ export type MapViewerHandle = {
|
|||||||
interface Props {
|
interface Props {
|
||||||
basemap: BasemapId;
|
basemap: BasemapId;
|
||||||
onFeatureClick: (f: ClickedFeatureLite | null) => void;
|
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;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MapViewer = forwardRef<MapViewerHandle, Props>(function MapViewer(
|
export const MapViewer = forwardRef<MapViewerHandle, Props>(function MapViewer(
|
||||||
{ basemap, onFeatureClick, className },
|
{ basemap, onFeatureClick, selectedFeature, className },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -276,6 +279,54 @@ export const MapViewer = forwardRef<MapViewerHandle, Props>(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
|
// Building suffix label — "354686-C1" → "C1". Same logic eterra.live
|
||||||
// uses: slice after the last dash. z16+ so we don't clutter the
|
// uses: slice after the last dash. z16+ so we don't clutter the
|
||||||
// overview when zoomed out.
|
// overview when zoomed out.
|
||||||
@@ -425,6 +476,49 @@ export const MapViewer = forwardRef<MapViewerHandle, Props>(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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
|
|||||||
Reference in New Issue
Block a user