From 3bd051ea8b5231c50fb8117ac3c01bb9107ca812 Mon Sep 17 00:00:00 2001 From: Thorsten Hell Date: Thu, 20 Nov 2025 16:24:38 +0100 Subject: [PATCH 1/6] add phase 1 - filtering for wc-karte --- .../generic/public/dev/wc_karte/config.json | 9 +- .../public/dev/wc_karte/createInfoBoxInfo.js | 56 +++++ apps/topicmaps/generic/src/app/App.css | 7 + apps/topicmaps/generic/src/app/App.jsx | 61 +++++ apps/topicmaps/generic/src/app/Map.jsx | 24 +- .../src/app/components/FilterButtons.jsx | 221 ++++++++++++++++++ 6 files changed, 369 insertions(+), 9 deletions(-) create mode 100644 apps/topicmaps/generic/public/dev/wc_karte/createInfoBoxInfo.js create mode 100644 apps/topicmaps/generic/src/app/components/FilterButtons.jsx diff --git a/apps/topicmaps/generic/public/dev/wc_karte/config.json b/apps/topicmaps/generic/public/dev/wc_karte/config.json index 920aee69a..035309286 100644 --- a/apps/topicmaps/generic/public/dev/wc_karte/config.json +++ b/apps/topicmaps/generic/public/dev/wc_karte/config.json @@ -5,15 +5,14 @@ "applicationMenuIntroductionMarkdown": "Die WC-Karte Wuppertal ist eine Anwendung der **GenericTopicMap Wuppertal**. Dies ist eine Komponente aus dem Gesamtsystem des Digitalen Zwillings der Stadt Wuppertal (**DigiTal Zwilling**), mit der eine einfache, für den mobilen Einsatz prädestinierte Web-Karte erzeugt werden kann, indem zwei Konfigurationsdateien angepasst werden. Über **Einstellungen** können Sie die Darstellung der Hintergrundkarte an Ihre Vorlieben anpassen. Wählen Sie **Kompaktanleitung** für detailliertere Bedienungsinformationen und **Urbaner Digitaler Zwilling** für eine Einordnung der WC-Karte in den Kontext des DigiTal Zwillings.", "previewMapPosition": "lat=51.26041096180761&lng=7.175016403198243&zoom=13", "applicationMenuSkipSymbolsizeSetting": false, - + "filteringEnabled": true, "vectorLayers": [ { "name": "öffentliche Toiletten", - "layer": "poi_toiletten@https://maps.wuppertal.de/poi?service=WMS&request=GetCapabilities&version=1.1.1", - "addMetaInfoToHelp": true, "layerType": "vector", - "style": "https://tiles.cismet.de/poi/offentliche-toiletten.style.json", - "opacity": 1 + "style": "https://tiles.cismet.de/toiletten/style.json", + "opacity": 1, + "infoBoxMappingFunction":"@createInfoBoxInfo.js" } ] } diff --git a/apps/topicmaps/generic/public/dev/wc_karte/createInfoBoxInfo.js b/apps/topicmaps/generic/public/dev/wc_karte/createInfoBoxInfo.js new file mode 100644 index 000000000..362da1a0e --- /dev/null +++ b/apps/topicmaps/generic/public/dev/wc_karte/createInfoBoxInfo.js @@ -0,0 +1,56 @@ +function createInfoBoxInfo(p) { + const isToilette = p.carmaInfo && p.carmaInfo.sourceLayer === 'toiletten'; + if (!isToilette) return null; + const c_Konfiguration = ''; + const iconBaseUrl = 'https://tiles.cismet.de/toiletten/assets/icons/'; + const fotoUrlPrefix = 'https://tiles.cismet.de/toiletten/assets/pics/'; + const item = p; + const c_Foto_bauen = ''; + let foto; + if (item.HAUPTBILD) { + foto = fotoUrlPrefix + item.HAUPTBILD.split('/').pop(); + } + const c_Icon_Liste_aufbauen = ''; + const icons = []; + const c_24_7 = ''; + if (item['Q_24/7_OFF'] === 'ja') { + icons.push(iconBaseUrl + 'Infobox_24_7_Geoeffnet.svg'); + } + const c_Entgelt = ''; + if (item.ENTGELT === 'ja') { + icons.push(iconBaseUrl + 'Infobox_Kostenpflichtig.svg'); + } else if (item.ENTGELT === 'nein') { + icons.push(iconBaseUrl + 'Infobox_Kostenfrei.svg'); + } + const c_Rollstuhlgerecht = ''; + if (item.ROLLGER === 'ja') { + icons.push(iconBaseUrl + 'Infobox_Rollstuhlgerecht.svg'); + } + const c_Wickeltisch = ''; + if (item.WICKELTIS === 'ja') { + icons.push(iconBaseUrl + 'Infobox_Wickeltisch.svg'); + } + let barrHinwText = item.BARR_HINW || ''; + const c_Subtitle_HTML = ''; + let subtitleHtml = null; + if (icons.length > 0 || item.OEFFNUNGS || barrHinwText) { + let iconImgs = ''; + for (let i = 0; i < icons.length; i++) { + iconImgs += ''; + } + const opening = item.OEFFNUNGS ? '
' + item.OEFFNUNGS + '
' : ''; + const iconsRow = icons.length > 0 ? '
' + iconImgs + '
' : ''; + const barrHinw = barrHinwText ? '
' + barrHinwText + '
' : ''; + subtitleHtml = '' + '
' + opening + iconsRow + barrHinw + '
' + ''; + } + const additionalInfoHtml = '' + (item.STRASSE || '') + (item.HAUSNUMMER ? ' ' + item.HAUSNUMMER : '') + '

' + (item.ORTSBESCHR || '') + ''; + const info = { + headerColor: '#4378CC', + header: 'Öffentliche Toiletten (' + item.NUTZUNG + ')', + title: item.NAME || 'Öffentliche Toilette', + additionalInfo: additionalInfoHtml, + subtitle: subtitleHtml, + foto: foto + }; + return info; +} \ No newline at end of file diff --git a/apps/topicmaps/generic/src/app/App.css b/apps/topicmaps/generic/src/app/App.css index 74b5e0534..67c07e16f 100644 --- a/apps/topicmaps/generic/src/app/App.css +++ b/apps/topicmaps/generic/src/app/App.css @@ -36,3 +36,10 @@ transform: rotate(360deg); } } + +/* Filter button responsive text hiding */ +@media (max-width: 590px) { + .filter-button-text { + display: none; + } +} diff --git a/apps/topicmaps/generic/src/app/App.jsx b/apps/topicmaps/generic/src/app/App.jsx index 94d37c5b2..4b0f636c9 100644 --- a/apps/topicmaps/generic/src/app/App.jsx +++ b/apps/topicmaps/generic/src/app/App.jsx @@ -173,6 +173,7 @@ function App({ name }) { style: layer?.other?.vectorStyle, infoboxMapping: layer?.conf?.infoboxMapping, }; + const styleVal = layer.conf?.vectorStyle; if (styleVal && styleVal !== "") { layerObj.style = styleVal; @@ -521,6 +522,66 @@ function App({ name }) { loadStyleManipulation(layer, configServer, configPath, slugName) ); await Promise.all(manipulationPromises); + + // --- InfoBox Mapping: Load mapping function from JS file if needed --- + function getInfoBoxMappingFunctionUrl( + infoBoxMappingFunction, + configServer, + configPath, + slugName + ) { + if ( + typeof infoBoxMappingFunction === "string" && + infoBoxMappingFunction.startsWith("@") + ) { + const filename = infoBoxMappingFunction.slice(1); + const path = configPath.endsWith("/") + ? configPath + : configPath + "/"; + const server = configServer.endsWith("/") + ? configServer.slice(0, -1) + : configServer; + return `${server}${path}${slugName}/${filename}`; + } + return null; + } + async function loadInfoBoxMappingFunction( + layer, + configServer, + configPath, + slugName + ) { + if ( + layer.infoBoxMappingFunction && + typeof layer.infoBoxMappingFunction === "string" && + layer.infoBoxMappingFunction.startsWith("@") && + !layer.infoboxMapping + ) { + const url = getInfoBoxMappingFunctionUrl( + layer.infoBoxMappingFunction, + configServer, + configPath, + slugName + ); + try { + const code = await fetch(url).then((r) => r.text()); + // The infoboxMapping expects an array with the function as a string + // Remove newlines to avoid syntax errors in sandboxed eval + const singleLineCode = code.replace(/\n/g, " "); + layer.infoboxMapping = [singleLineCode]; + } catch (e) { + log( + `Failed to fetch/parse infoBoxMappingFunction for layer ${ + layer.name || layer.id + }: ${e}` + ); + } + } + } + const infoBoxMappingPromises = config.tm.vectorLayers.map((layer) => + loadInfoBoxMappingFunction(layer, configServer, configPath, slugName) + ); + await Promise.all(infoBoxMappingPromises); } // Normalize layers: if only 'layer' is present, extract 'capabilitiesLayer' and 'capabilities' diff --git a/apps/topicmaps/generic/src/app/Map.jsx b/apps/topicmaps/generic/src/app/Map.jsx index 2a64eb2ba..cc63e5827 100644 --- a/apps/topicmaps/generic/src/app/Map.jsx +++ b/apps/topicmaps/generic/src/app/Map.jsx @@ -27,6 +27,7 @@ import { } from "@carma-appframeworks/portals"; import { EmptySearchComponent } from "@carma-mapping/fuzzy-search"; import FuzzySearchWrapper from "./components/FuzzySearchWrapper"; +import { FilterButtons } from "./components/FilterButtons"; import { Control, ControlLayout } from "@carma-mapping/map-controls-layout"; import { FullscreenControl, @@ -89,7 +90,8 @@ function renderCismapLayers( markerSymbolSize, setGlobalHits, initialVisualSelection, - layerInformation + layerInformation, + setMaplibreMap ) { return ( <> @@ -128,6 +130,11 @@ function renderCismapLayers( return ret; }); }} + onMapLibreCoreMapReady={(map) => { + if (setMaplibreMap) { + setMaplibreMap(map); + } + }} /> ); })} @@ -149,12 +156,13 @@ const Map = ({ const [cl_key, setClKey] = useState(""); const { routedMapRef } = useSelectionTopicMap() ?? {}; const [selectedVectorObject, setSelectedVectorObject] = useState(undefined); + const [maplibreMap, setMaplibreMap] = useState(null); // console.log("xxx markerSymbolSize", markerSymbolSize); // lets assume we will only have vector layers useEffect(() => { const getFeature = async (infoboxMapping, hit) => { - // console.log("xxx infoboxMapping", infoboxMapping); + console.log("xxx infoboxMapping", infoboxMapping); const feature = await createVectorFeature(infoboxMapping, hit); setFeature(feature); }; @@ -269,6 +277,12 @@ const Map = ({ )} + {config?.tm?.filteringEnabled && config?.tm?.vectorLayers && ( + + + + )} + diff --git a/apps/topicmaps/generic/src/app/components/FilterButtons.jsx b/apps/topicmaps/generic/src/app/components/FilterButtons.jsx new file mode 100644 index 000000000..2f09a4fab --- /dev/null +++ b/apps/topicmaps/generic/src/app/components/FilterButtons.jsx @@ -0,0 +1,221 @@ +import { useState, useEffect } from "react"; + +export const FilterButtons = ({ maplibreMap }) => { + console.log("xxx maplibreMap", maplibreMap); + + // Filter button selection state + const [selectedFilters, setSelectedFilters] = useState({ + alle: true, + kostenfrei: false, + rollstuhlgerecht: false, + wickeltisch: false, + }); + + // Apply filters to the map whenever selectedFilters or maplibreMap changes + useEffect(() => { + if (!maplibreMap) return; + + try { + // Get all layers from the map style + const layers = maplibreMap.getStyle()?.layers || []; + + // Find toiletten layers (looking for both "toiletten" and "poi" patterns) + const targetLayerIds = layers + .filter( + (layer) => + layer.id.toLowerCase().includes("toiletten") || + layer.id.toLowerCase().includes("poi") + ) + .map((layer) => layer.id); + + console.log("xxx Target layers found:", targetLayerIds); + console.log("xxx All layers:", layers.map((l) => l.id)); + + // Build the filter expression + let filterExpression; + + if (selectedFilters.alle) { + // Show all features + filterExpression = null; + } else { + // Build an 'all' filter for selected criteria + const conditions = []; + + if (selectedFilters.kostenfrei) { + // ENTGELT === "nein" means free (kostenfrei) + conditions.push(["==", ["get", "ENTGELT"], "nein"]); + } + + if (selectedFilters.rollstuhlgerecht) { + // ROLLGER === "ja" means wheelchair accessible + conditions.push(["==", ["get", "ROLLGER"], "ja"]); + } + + if (selectedFilters.wickeltisch) { + // WICKELTIS === "ja" means changing table available + conditions.push(["==", ["get", "WICKELTIS"], "ja"]); + } + + // Combine conditions with 'all' operator (AND logic) + if (conditions.length > 0) { + filterExpression = ["all", ...conditions]; + } else { + filterExpression = null; + } + } + + console.log("xxx Applying filter:", JSON.stringify(filterExpression)); + + // Apply the filter to all target layers + targetLayerIds.forEach((layerId) => { + try { + maplibreMap.setFilter(layerId, filterExpression); + } catch (error) { + console.error(`Error setting filter on layer ${layerId}:`, error); + } + }); + } catch (error) { + console.error("Error applying filters:", error); + } + }, [selectedFilters, maplibreMap]); + + const handleFilterClick = (filterName) => { + if (filterName === "alle") { + // When "Alle" is clicked, deselect all icon buttons + setSelectedFilters({ + alle: true, + kostenfrei: false, + rollstuhlgerecht: false, + wickeltisch: false, + }); + } else { + // When any icon button is clicked, toggle it and deselect "Alle" + setSelectedFilters((prev) => { + const newFilters = { + ...prev, + alle: false, + [filterName]: !prev[filterName], + }; + + // If no icon buttons are selected, select "Alle" again + const hasIconSelection = + newFilters.kostenfrei || + newFilters.rollstuhlgerecht || + newFilters.wickeltisch; + if (!hasIconSelection) { + newFilters.alle = true; + } + + return newFilters; + }); + } + }; + + return ( +
+
handleFilterClick("alle")} + style={{ + display: "flex", + alignItems: "center", + gap: "6px", + backgroundColor: "white", + padding: "6px 12px", + borderRadius: "10px", + boxShadow: "0 2px 4px rgba(0,0,0,0.2)", + cursor: "pointer", + height: "32px", + border: selectedFilters.alle + ? "3px solid #4378ccCC" + : "3px solid transparent", + }} + > + Alle +
+
handleFilterClick("kostenfrei")} + style={{ + display: "flex", + alignItems: "center", + gap: "6px", + backgroundColor: "white", + padding: "6px 12px", + borderRadius: "10px", + boxShadow: "0 2px 4px rgba(0,0,0,0.2)", + cursor: "pointer", + height: "32px", + border: selectedFilters.kostenfrei + ? "3px solid #4378ccCC" + : "3px solid transparent", + }} + > + + Kostenfrei +
+
handleFilterClick("rollstuhlgerecht")} + style={{ + display: "flex", + alignItems: "center", + gap: "6px", + backgroundColor: "white", + padding: "6px 12px", + borderRadius: "10px", + boxShadow: "0 2px 4px rgba(0,0,0,0.2)", + cursor: "pointer", + height: "32px", + border: selectedFilters.rollstuhlgerecht + ? "3px solid #4378ccCC" + : "3px solid transparent", + }} + > + + Rollstuhlgerecht +
+
handleFilterClick("wickeltisch")} + style={{ + display: "flex", + alignItems: "center", + gap: "6px", + backgroundColor: "white", + padding: "6px 12px", + borderRadius: "10px", + boxShadow: "0 2px 4px rgba(0,0,0,0.2)", + cursor: "pointer", + height: "32px", + border: selectedFilters.wickeltisch + ? "3px solid #4378ccCC" + : "3px solid transparent", + }} + > + + Wickeltisch +
+
+ ); +}; From 0eb4abb4643b02dff396601b07aae71ac861b465 Mon Sep 17 00:00:00 2001 From: Thorsten Hell Date: Thu, 20 Nov 2025 17:36:17 +0100 Subject: [PATCH 2/6] UI Changes due to customer request --- .../src/app/components/FilterButtons.jsx | 114 ++++++++++++++++-- .../src/app/App.tsx | 35 ++++-- 2 files changed, 129 insertions(+), 20 deletions(-) diff --git a/apps/topicmaps/generic/src/app/components/FilterButtons.jsx b/apps/topicmaps/generic/src/app/components/FilterButtons.jsx index 2f09a4fab..cc93d7671 100644 --- a/apps/topicmaps/generic/src/app/components/FilterButtons.jsx +++ b/apps/topicmaps/generic/src/app/components/FilterButtons.jsx @@ -9,6 +9,7 @@ export const FilterButtons = ({ maplibreMap }) => { kostenfrei: false, rollstuhlgerecht: false, wickeltisch: false, + dauergeoffnet: false, }); // Apply filters to the map whenever selectedFilters or maplibreMap changes @@ -29,7 +30,10 @@ export const FilterButtons = ({ maplibreMap }) => { .map((layer) => layer.id); console.log("xxx Target layers found:", targetLayerIds); - console.log("xxx All layers:", layers.map((l) => l.id)); + console.log( + "xxx All layers:", + layers.map((l) => l.id) + ); // Build the filter expression let filterExpression; @@ -56,6 +60,11 @@ export const FilterButtons = ({ maplibreMap }) => { conditions.push(["==", ["get", "WICKELTIS"], "ja"]); } + if (selectedFilters.dauergeoffnet) { + // Q_24/7_OFF === "ja" means open 24/7 + conditions.push(["==", ["get", "Q_24/7_OFF"], "ja"]); + } + // Combine conditions with 'all' operator (AND logic) if (conditions.length > 0) { filterExpression = ["all", ...conditions]; @@ -87,6 +96,7 @@ export const FilterButtons = ({ maplibreMap }) => { kostenfrei: false, rollstuhlgerecht: false, wickeltisch: false, + dauergeoffnet: false, }); } else { // When any icon button is clicked, toggle it and deselect "Alle" @@ -101,7 +111,8 @@ export const FilterButtons = ({ maplibreMap }) => { const hasIconSelection = newFilters.kostenfrei || newFilters.rollstuhlgerecht || - newFilters.wickeltisch; + newFilters.wickeltisch || + newFilters.dauergeoffnet; if (!hasIconSelection) { newFilters.alle = true; } @@ -142,7 +153,14 @@ export const FilterButtons = ({ maplibreMap }) => { : "3px solid transparent", }} > - Alle + + Alle +
handleFilterClick("kostenfrei")} @@ -164,9 +182,21 @@ export const FilterButtons = ({ maplibreMap }) => { - Kostenfrei + + Kostenfrei +
handleFilterClick("rollstuhlgerecht")} @@ -188,9 +218,25 @@ export const FilterButtons = ({ maplibreMap }) => { - Rollstuhlgerecht + + Rollstuhlgerecht +
handleFilterClick("wickeltisch")} @@ -212,9 +258,59 @@ export const FilterButtons = ({ maplibreMap }) => { + + Wickeltisch + +
+
handleFilterClick("dauergeoffnet")} + style={{ + display: "flex", + alignItems: "center", + gap: "6px", + backgroundColor: "white", + padding: "6px 12px", + borderRadius: "10px", + boxShadow: "0 2px 4px rgba(0,0,0,0.2)", + cursor: "pointer", + height: "32px", + border: selectedFilters.dauergeoffnet + ? "3px solid #4378ccCC" + : "3px solid transparent", + }} + > + - Wickeltisch + + geöffnet +
); diff --git a/playgrounds/vectorlayer-filtering-playground/src/app/App.tsx b/playgrounds/vectorlayer-filtering-playground/src/app/App.tsx index 7b1b5acdf..1b70fa0c3 100644 --- a/playgrounds/vectorlayer-filtering-playground/src/app/App.tsx +++ b/playgrounds/vectorlayer-filtering-playground/src/app/App.tsx @@ -1,7 +1,7 @@ import TopicMapComponent from "react-cismap/topicmaps/TopicMapComponent"; import { suppressReactCismapErrors } from "@carma-commons/utils"; import { useMapLibreMap } from "@carma-commons/measurements"; -import { ZoomControl } from "@carma-mapping/components"; +import { LayerButton, ZoomControl } from "@carma-mapping/components"; import { Control, ControlLayout } from "@carma-mapping/map-controls-layout"; import { EmptySearchComponent } from "@carma-mapping/fuzzy-search"; import { LibFuzzySearch } from "@carma-mapping/fuzzy-search"; @@ -14,6 +14,8 @@ import { import CismapLayer from "react-cismap/CismapLayer"; import { getActionLinksForFeature } from "react-cismap/tools/uiHelper"; import { TopicMapDispatchContext } from "react-cismap/contexts/TopicMapContextProvider"; +import { Layer } from "leaflet"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; suppressReactCismapErrors(true); @@ -32,10 +34,19 @@ export function App({ const { maplibreMap, setMaplibreMap } = useMapLibreMap(); const { zoomToFeature } = useContext(TopicMapDispatchContext) as any; const [filterText, setFilterText] = useState(""); + const [defaultVectorStyle, setDefaultVectorStyle] = useState( + null + ); const pixelwidth = responsiveState === "normal" ? "300px" : (windowSize?.width || 300) - gap; + // Combine default vector style with user-provided styles + const allVectorStyles = [ + ...(defaultVectorStyle ? [defaultVectorStyle] : []), + ...vectorStyles, + ]; + // Apply filter whenever filterText or maplibreMap changes useEffect(() => { if (!maplibreMap) return; @@ -43,11 +54,7 @@ export function App({ try { // Check if the layers exist const layers = maplibreMap.getStyle()?.layers || []; - const poiLayers = [ - "poi-images", - "poi-labels", - "poi-images-selection" - ]; + const poiLayers = ["poi-images", "poi-labels", "poi-images-selection"]; poiLayers.forEach((layerId) => { const hasLayer = layers.some((l: any) => l.id === layerId); @@ -135,7 +142,9 @@ export function App({ }} /> -
+
); From 294f26c2558ef5d1dd4c5c995fe701002dcb1717 Mon Sep 17 00:00:00 2001 From: Thorsten Hell Date: Fri, 21 Nov 2025 18:58:04 +0100 Subject: [PATCH 4/6] minor style changes --- .../src/app/components/FilterButtons.jsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/topicmaps/generic/src/app/components/FilterButtons.jsx b/apps/topicmaps/generic/src/app/components/FilterButtons.jsx index 3c605a08d..78cfab5c5 100644 --- a/apps/topicmaps/generic/src/app/components/FilterButtons.jsx +++ b/apps/topicmaps/generic/src/app/components/FilterButtons.jsx @@ -149,8 +149,8 @@ export const FilterButtons = ({ maplibreMap }) => { cursor: "pointer", height: "32px", border: selectedFilters.alle - ? "1.5px solid #4378ccCC" - : "1.5px solid transparent", + ? "2px solid #4378ccCC" + : "2px solid transparent", }} > Alle @@ -168,8 +168,8 @@ export const FilterButtons = ({ maplibreMap }) => { cursor: "pointer", height: "32px", border: selectedFilters.kostenfrei - ? "1.5px solid #4378ccCC" - : "1.5px solid transparent", + ? "2px solid #4378ccCC" + : "2px solid transparent", }} > { cursor: "pointer", height: "32px", border: selectedFilters.rollstuhlgerecht - ? "1.5px solid #4378ccCC" - : "1.5px solid transparent", + ? "2px solid #4378ccCC" + : "2px solid transparent", }} > { cursor: "pointer", height: "32px", border: selectedFilters.wickeltisch - ? "1.5px solid #4378ccCC" - : "1.5px solid transparent", + ? "2px solid #4378ccCC" + : "2px solid transparent", }} > { cursor: "pointer", height: "32px", border: selectedFilters.dauergeoffnet - ? "1.5px solid #4378ccCC" - : "1.5px solid transparent", + ? "2px solid #4378ccCC" + : "2px solid transparent", }} > Date: Mon, 24 Nov 2025 16:40:29 +0100 Subject: [PATCH 5/6] add sourceFeature to the return value of createVectorFeature --- .../appframeworks/portals/src/lib/utils/featureInfo.ts | 3 +++ libraries/types/src/lib/feature-info.d.ts | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/libraries/appframeworks/portals/src/lib/utils/featureInfo.ts b/libraries/appframeworks/portals/src/lib/utils/featureInfo.ts index 603ecc2c2..5d1c32152 100644 --- a/libraries/appframeworks/portals/src/lib/utils/featureInfo.ts +++ b/libraries/appframeworks/portals/src/lib/utils/featureInfo.ts @@ -147,6 +147,7 @@ export const createUrl = ({ export const createVectorFeature = async (mapping, selectedVectorFeature) => { let feature: any = undefined; + let properties = selectedVectorFeature.properties; properties = { ...properties, @@ -176,6 +177,7 @@ export const createVectorFeature = async (mapping, selectedVectorFeature) => { const genericLinks = featureProperties.properties.genericLinks || []; feature = { + sourceFeature: selectedVectorFeature, properties: { ...featureProperties.properties, genericLinks: genericLinks, @@ -184,6 +186,7 @@ export const createVectorFeature = async (mapping, selectedVectorFeature) => { geometry: selectedVectorFeature.geometry, }; } + return feature; }; diff --git a/libraries/types/src/lib/feature-info.d.ts b/libraries/types/src/lib/feature-info.d.ts index e19d77143..427de8938 100644 --- a/libraries/types/src/lib/feature-info.d.ts +++ b/libraries/types/src/lib/feature-info.d.ts @@ -1,7 +1,17 @@ +/** + * Feature information for displaying in the infobox + * @property id - Unique identifier + * @property showMarker - Whether to show a marker for this feature + * @property properties - Feature properties for display + * @property sourceFeature - (Optional) Original MapLibre feature from the map, used for filter checks + * @property geometry - (Optional) GeoJSON geometry of the feature + */ export type FeatureInfo = { id: string; showMarker?: boolean; properties: FeatureInfoProperties; + sourceFeature?: any; + geometry?: any; }; export type FeatureInfoProperties = { From e2add9598109649a2cd630a1da0cc40112c13cdd Mon Sep 17 00:00:00 2001 From: Thorsten Hell Date: Mon, 24 Nov 2025 16:41:34 +0100 Subject: [PATCH 6/6] if a selected Feature is filtered then the selected feature should not be selected anymore --- apps/topicmaps/generic/src/app/Map.jsx | 9 +- .../src/app/components/FilterButtons.jsx | 243 +++++++++--------- 2 files changed, 127 insertions(+), 125 deletions(-) diff --git a/apps/topicmaps/generic/src/app/Map.jsx b/apps/topicmaps/generic/src/app/Map.jsx index cc63e5827..7db0594d5 100644 --- a/apps/topicmaps/generic/src/app/Map.jsx +++ b/apps/topicmaps/generic/src/app/Map.jsx @@ -164,6 +164,9 @@ const Map = ({ const getFeature = async (infoboxMapping, hit) => { console.log("xxx infoboxMapping", infoboxMapping); const feature = await createVectorFeature(infoboxMapping, hit); + + // console.log("xxx a2c setFeature", feature); + setFeature(feature); }; @@ -279,7 +282,11 @@ const Map = ({ {config?.tm?.filteringEnabled && config?.tm?.vectorLayers && ( - + )} diff --git a/apps/topicmaps/generic/src/app/components/FilterButtons.jsx b/apps/topicmaps/generic/src/app/components/FilterButtons.jsx index 78cfab5c5..504a4b4b0 100644 --- a/apps/topicmaps/generic/src/app/components/FilterButtons.jsx +++ b/apps/topicmaps/generic/src/app/components/FilterButtons.jsx @@ -1,8 +1,10 @@ import { useState, useEffect } from "react"; -export const FilterButtons = ({ maplibreMap }) => { - console.log("xxx maplibreMap", maplibreMap); - +export const FilterButtons = ({ + maplibreMap, + selectedFeature, + setSelectedFeature, +}) => { // Filter button selection state const [selectedFilters, setSelectedFilters] = useState({ alle: true, @@ -12,6 +14,87 @@ export const FilterButtons = ({ maplibreMap }) => { dauergeoffnet: false, }); + // Function to build filter expression from selected filters + const buildFilterExpression = (filters) => { + if (filters.alle) { + // Show all features + return null; + } + + // Build an 'all' filter for selected criteria + const conditions = []; + + if (filters.kostenfrei) { + // ENTGELT === "nein" means free (kostenfrei) + conditions.push(["==", ["get", "ENTGELT"], "nein"]); + } + + if (filters.rollstuhlgerecht) { + // ROLLGER === "ja" means wheelchair accessible + conditions.push(["==", ["get", "ROLLGER"], "ja"]); + } + + if (filters.wickeltisch) { + // WICKELTIS === "ja" means changing table available + conditions.push(["==", ["get", "WICKELTIS"], "ja"]); + } + + if (filters.dauergeoffnet) { + // Q_24/7_OFF === "ja" means open 24/7 + conditions.push(["==", ["get", "Q_24/7_OFF"], "ja"]); + } + + // Combine conditions with 'all' operator (AND logic) + if (conditions.length > 0) { + return ["all", ...conditions]; + } + + return null; + }; + + // Function to check if a feature matches the current filter criteria + const checkFeatureMatchesFilter = (feature, filters) => { + if (!feature?.properties) return false; + + const props = feature.properties; + + // If showing all, feature is always visible + if (filters.alle) return true; + + // Check each active filter - ALL must match (AND logic) + if (filters.kostenfrei && props.ENTGELT !== "nein") { + return false; + } + + if (filters.rollstuhlgerecht && props.ROLLGER !== "ja") { + return false; + } + + if (filters.wickeltisch && props.WICKELTIS !== "ja") { + return false; + } + + if (filters.dauergeoffnet && props["Q_24/7_OFF"] !== "ja") { + return false; + } + + return true; + }; + + // Style function for filter buttons + const getFilterButtonStyle = (isSelected) => ({ + display: "flex", + alignItems: "center", + gap: "6px", + backgroundColor: "white", + padding: "6px 12px", + borderRadius: "10px", + boxShadow: "0 2px 4px rgba(0,0,0,0.2)", + cursor: "pointer", + height: "32px", + border: isSelected ? "2px solid #4378ccCC" : "2px solid transparent", + }); + // Apply filters to the map whenever selectedFilters or maplibreMap changes useEffect(() => { if (!maplibreMap) return; @@ -22,58 +105,11 @@ export const FilterButtons = ({ maplibreMap }) => { // Find toiletten layers (looking for both "toiletten" and "poi" patterns) const targetLayerIds = layers - .filter( - (layer) => - layer.id.toLowerCase().includes("toiletten") || - layer.id.toLowerCase().includes("poi") - ) + .filter((layer) => layer.id.toLowerCase().includes("toiletten")) .map((layer) => layer.id); - console.log("xxx Target layers found:", targetLayerIds); - console.log( - "xxx All layers:", - layers.map((l) => l.id) - ); - - // Build the filter expression - let filterExpression; - - if (selectedFilters.alle) { - // Show all features - filterExpression = null; - } else { - // Build an 'all' filter for selected criteria - const conditions = []; - - if (selectedFilters.kostenfrei) { - // ENTGELT === "nein" means free (kostenfrei) - conditions.push(["==", ["get", "ENTGELT"], "nein"]); - } - - if (selectedFilters.rollstuhlgerecht) { - // ROLLGER === "ja" means wheelchair accessible - conditions.push(["==", ["get", "ROLLGER"], "ja"]); - } - - if (selectedFilters.wickeltisch) { - // WICKELTIS === "ja" means changing table available - conditions.push(["==", ["get", "WICKELTIS"], "ja"]); - } - - if (selectedFilters.dauergeoffnet) { - // Q_24/7_OFF === "ja" means open 24/7 - conditions.push(["==", ["get", "Q_24/7_OFF"], "ja"]); - } - - // Combine conditions with 'all' operator (AND logic) - if (conditions.length > 0) { - filterExpression = ["all", ...conditions]; - } else { - filterExpression = null; - } - } - - console.log("xxx Applying filter:", JSON.stringify(filterExpression)); + // Build the filter expression from selected filters + const filterExpression = buildFilterExpression(selectedFilters); // Apply the filter to all target layers targetLayerIds.forEach((layerId) => { @@ -83,10 +119,34 @@ export const FilterButtons = ({ maplibreMap }) => { console.error(`Error setting filter on layer ${layerId}:`, error); } }); + + // Check if selected feature still matches the new filter criteria + // If not, clear the selection and hide the infobox + if (selectedFeature?.sourceFeature) { + const matchesFilter = checkFeatureMatchesFilter( + selectedFeature.sourceFeature, + selectedFilters + ); + + if (!matchesFilter) { + // Feature no longer matches filter - deselect it + maplibreMap.setFeatureState( + { + source: selectedFeature.sourceFeature.source, + sourceLayer: selectedFeature.sourceFeature.sourceLayer, + id: selectedFeature.sourceFeature.id, + }, + { selected: false } + ); + + // Clear the selected feature to hide the infobox + setSelectedFeature(undefined); + } + } } catch (error) { console.error("Error applying filters:", error); } - }, [selectedFilters, maplibreMap]); + }, [selectedFilters, maplibreMap, selectedFeature]); const handleFilterClick = (filterName) => { if (filterName === "alle") { @@ -138,39 +198,13 @@ export const FilterButtons = ({ maplibreMap }) => { >
handleFilterClick("alle")} - style={{ - display: "flex", - alignItems: "center", - gap: "6px", - backgroundColor: "white", - padding: "6px 12px", - borderRadius: "10px", - boxShadow: "0 2px 4px rgba(0,0,0,0.2)", - cursor: "pointer", - height: "32px", - border: selectedFilters.alle - ? "2px solid #4378ccCC" - : "2px solid transparent", - }} + style={getFilterButtonStyle(selectedFilters.alle)} > Alle
handleFilterClick("kostenfrei")} - style={{ - display: "flex", - alignItems: "center", - gap: "6px", - backgroundColor: "white", - padding: "6px 12px", - borderRadius: "10px", - boxShadow: "0 2px 4px rgba(0,0,0,0.2)", - cursor: "pointer", - height: "32px", - border: selectedFilters.kostenfrei - ? "2px solid #4378ccCC" - : "2px solid transparent", - }} + style={getFilterButtonStyle(selectedFilters.kostenfrei)} > {
handleFilterClick("rollstuhlgerecht")} - style={{ - display: "flex", - alignItems: "center", - gap: "6px", - backgroundColor: "white", - padding: "6px 12px", - borderRadius: "10px", - boxShadow: "0 2px 4px rgba(0,0,0,0.2)", - cursor: "pointer", - height: "32px", - border: selectedFilters.rollstuhlgerecht - ? "2px solid #4378ccCC" - : "2px solid transparent", - }} + style={getFilterButtonStyle(selectedFilters.rollstuhlgerecht)} > {
handleFilterClick("wickeltisch")} - style={{ - display: "flex", - alignItems: "center", - gap: "6px", - backgroundColor: "white", - padding: "6px 12px", - borderRadius: "10px", - boxShadow: "0 2px 4px rgba(0,0,0,0.2)", - cursor: "pointer", - height: "32px", - border: selectedFilters.wickeltisch - ? "2px solid #4378ccCC" - : "2px solid transparent", - }} + style={getFilterButtonStyle(selectedFilters.wickeltisch)} > {
handleFilterClick("dauergeoffnet")} - style={{ - display: "flex", - alignItems: "center", - gap: "6px", - backgroundColor: "white", - padding: "6px 12px", - borderRadius: "10px", - boxShadow: "0 2px 4px rgba(0,0,0,0.2)", - cursor: "pointer", - height: "32px", - border: selectedFilters.dauergeoffnet - ? "2px solid #4378ccCC" - : "2px solid transparent", - }} + style={getFilterButtonStyle(selectedFilters.dauergeoffnet)} >