From 449d19a7284bb10bf4c3bc6d525b7c1169a494df Mon Sep 17 00:00:00 2001 From: ISA Date: Tue, 11 Mar 2025 16:19:11 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20Duplizierte=20Kontextmen=C3=BC-Eintr?= =?UTF-8?q?=C3=A4ge=20verhindert=20und=20Cleanup=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Kontextmenü wird jetzt nur einmal hinzugefügt, wenn es noch nicht existiert. - Vor dem Hinzufügen wird geprüft, ob bereits Einträge existieren, um Duplikate zu vermeiden. - Kontextmenü wird entfernt, wenn außerhalb geklickt wird, um Speicherlecks zu verhindern. - Nutzung eines `Set()` für Menü-IDs, um doppelte Einträge sicher zu verhindern. --- components/mainComponent/MapComponent.js | 13 +- config/appVersion.js | 2 +- redux/slices/selectedDeviceSlice.js | 14 ++ redux/slices/selectedPoiSlice.js | 16 +- redux/store.js | 4 + utils/contextMenuUtils.js | 35 +++-- utils/createAndSetDevices.js | 117 ++++++++++---- utils/initializeMap.js | 191 ++++++++++++----------- utils/setupDevices.js | 18 +++ utils/setupPOIs.js | 10 +- 10 files changed, 279 insertions(+), 141 deletions(-) create mode 100644 redux/slices/selectedDeviceSlice.js create mode 100644 utils/setupDevices.js diff --git a/components/mainComponent/MapComponent.js b/components/mainComponent/MapComponent.js index 18a74c18f..36a2155b0 100644 --- a/components/mainComponent/MapComponent.js +++ b/components/mainComponent/MapComponent.js @@ -85,9 +85,14 @@ import useLoadUserRights from "./hooks/useLoadUserRights"; import useFetchWebServiceMap from "./hooks/useFetchWebServiceMap"; import useFetchPoiData from "./hooks/useFetchPoiData.js"; import useRestoreMapSettings from "./hooks/useRestoreMapSettings"; +import { setSelectedPoi, clearSelectedPoi } from "../../redux/slices/selectedPoiSlice"; +import { setupDevices } from "../../utils/setupDevices"; const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => { const dispatch = useDispatch(); + // Redux-States holen + const selectedPoi = useSelector((state) => state.selectedPoi); + const selectedDevice = useSelector((state) => state.selectedDevice); const countdown = useSelector((state) => state.polylineContextMenu.countdown); const countdownActive = useSelector((state) => state.polylineContextMenu.countdownActive); const isPolylineContextMenuOpen = useSelector((state) => state.polylineContextMenu.isOpen); @@ -132,7 +137,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => { const [userId, setUserId] = useRecoilState(userIdState); const [AddPoiModalWindowState, setAddPoiModalWindowState] = useState(false); const [userRights, setUserRights] = useState(null); - const setSelectedPoi = useSetRecoilState(selectedPoiState); + const [showPoiUpdateModal, setShowPoiUpdateModal] = useState(false); const [currentPoiData, setCurrentPoiData] = useState(null); const [showVersionInfoModal, setShowVersionInfoModal] = useState(false); @@ -324,8 +329,8 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => { useEffect(() => { if (poiData.length === 0) return; - setupPOIs(map, locations, poiData, poiTypMap, userRights, poiLayerRef, setSelectedPoi, setLocationDeviceData, setDeviceName, setCurrentPoi, poiLayerVisible, fetchPoiData, toast, setShowPoiUpdateModal, setCurrentPoiData, deviceName); - }, [map, locations, onLocationUpdate, poiReadTrigger, isPoiTypLoaded, userRights, poiLayerVisible, poiData, poiTypMap]); + setupPOIs(map, locations, poiData, poiTypMap, userRights, poiLayerRef, setSelectedPoi, setLocationDeviceData, setDeviceName, setCurrentPoi, poiLayerVisible, fetchPoiData, toast, setShowPoiUpdateModal, setCurrentPoiData, deviceName, dispatch); + }, [map, locations, onLocationUpdate, poiReadTrigger, isPoiTypLoaded, userRights, poiLayerVisible, poiData, poiTypMap, dispatch]); //--------------------------------------------- //console.log("priorityConfig in MapComponent2: ", priorityConfig); @@ -996,6 +1001,8 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => { //--------------------------------------------- + //-------------------------------------------- + return ( <> {useSelector((state) => state.addPoiOnPolyline.isOpen) && } diff --git a/config/appVersion.js b/config/appVersion.js index a03bfcf9c..7b9f40448 100644 --- a/config/appVersion.js +++ b/config/appVersion.js @@ -1,2 +1,2 @@ // /config/appVersion -export const APP_VERSION = "1.1.53"; +export const APP_VERSION = "1.1.54"; diff --git a/redux/slices/selectedDeviceSlice.js b/redux/slices/selectedDeviceSlice.js new file mode 100644 index 000000000..a3db6fa0d --- /dev/null +++ b/redux/slices/selectedDeviceSlice.js @@ -0,0 +1,14 @@ +// redux/slices/selectedDeviceSlice.js +import { createSlice } from "@reduxjs/toolkit"; + +export const selectedDeviceSlice = createSlice({ + name: "selectedDevice", + initialState: null, + reducers: { + setSelectedDevice: (state, action) => action.payload, // Speichert Device-Daten + clearSelectedDevice: () => null, // Entfernt das Gerät aus dem State + }, +}); + +export const { setSelectedDevice, clearSelectedDevice } = selectedDeviceSlice.actions; +export default selectedDeviceSlice.reducer; diff --git a/redux/slices/selectedPoiSlice.js b/redux/slices/selectedPoiSlice.js index e6c6761cf..776b1e716 100644 --- a/redux/slices/selectedPoiSlice.js +++ b/redux/slices/selectedPoiSlice.js @@ -1,8 +1,14 @@ // redux/slices/selectedPoiSlice.js -//Ist gedacht um ausgewählte Poi Informationen zu speichern und zu PoiUpdateModal.js zu übergeben -import { atom } from "recoil"; +import { createSlice } from "@reduxjs/toolkit"; -export const selectedPoiState = atom({ - key: "poiState", // Einzigartiger Key (mit der gesamten Anwendung) - default: null, // Standardwert +export const selectedPoiSlice = createSlice({ + name: "selectedPoi", + initialState: null, + reducers: { + setSelectedPoi: (state, action) => action.payload, // Speichert POI-Daten + clearSelectedPoi: () => null, // Entfernt POI aus dem State + }, }); + +export const { setSelectedPoi, clearSelectedPoi } = selectedPoiSlice.actions; +export default selectedPoiSlice.reducer; diff --git a/redux/store.js b/redux/store.js index 03ffc2838..a58f51a96 100644 --- a/redux/store.js +++ b/redux/store.js @@ -12,6 +12,8 @@ import gisStationsStaticReducer from "./slices/webService/gisStationsStaticSlice import poiTypesReducer from "./slices/db/poiTypesSlice"; import addPoiOnPolylineReducer from "./slices/addPoiOnPolylineSlice"; import polylineContextMenuReducer from "./slices/polylineContextMenuSlice"; +import selectedPoiReducer from "./slices/selectedPoiSlice"; +import selectedDeviceReducer from "./slices/selectedDeviceSlice"; export const store = configureStore({ reducer: { @@ -27,5 +29,7 @@ export const store = configureStore({ poiTypes: poiTypesReducer, addPoiOnPolyline: addPoiOnPolylineReducer, polylineContextMenu: polylineContextMenuReducer, + selectedPoi: selectedPoiReducer, + selectedDevice: selectedDeviceReducer, }, }); diff --git a/utils/contextMenuUtils.js b/utils/contextMenuUtils.js index 039d0c971..4f4fdacd0 100644 --- a/utils/contextMenuUtils.js +++ b/utils/contextMenuUtils.js @@ -1,31 +1,38 @@ -// contextMenuUtils.js +// utils/contextMenuUtils.js import { BASE_URL } from "../config/urls"; +import { store } from "../redux/store"; // Redux-Store importieren + export function addContextMenuToMarker(marker) { marker.unbindContextMenu(); // Entferne das Kontextmenü, um Duplikate zu vermeiden + const selectedDevice = store.getState().selectedDevice; // Redux-Wert abrufen + + const contextMenuItems = []; + + // ✅ Nur hinzufügen, wenn `selectedDevice` vorhanden ist + if (selectedDevice && marker.options?.idDevice) { + contextMenuItems.push({ + text: "Station öffnen (Tab)", + icon: "/img/screen_new.png", + callback: (e) => openInNewTab(e, marker), + }); + } + + // Falls weitere Kontextmenü-Optionen nötig sind, können sie hier hinzugefügt werden. + marker.bindContextMenu({ contextmenu: true, contextmenuWidth: 140, - contextmenuItems: [ - /* { - text: "Station öffnen (Tab)", - icon: "/img/screen_new.png", - callback: (e) => openInNewTab(e, marker), - }, - { - text: "Station öffnen", - icon: "/img/screen_same.png", - callback: (e) => openInSameWindow(e, marker), - }, */ - ], + contextmenuItems: contextMenuItems, }); } + // Funktion zum Öffnen in einem neuen Tab export function openInNewTab(e, marker) { const baseUrl = BASE_URL; console.log("baseUrl:", baseUrl); + if (marker && marker.options && marker.options.link) { - //console.log("Marker data:", baseUrl + marker.options.link); window.open(baseUrl + marker.options.link, "_blank"); } else { console.error("Fehler: Marker hat keine gültige 'link' Eigenschaft"); diff --git a/utils/createAndSetDevices.js b/utils/createAndSetDevices.js index 646b5d51e..878bf2fca 100644 --- a/utils/createAndSetDevices.js +++ b/utils/createAndSetDevices.js @@ -6,6 +6,8 @@ import * as config from "../config/config.js"; import { disablePolylineEvents, enablePolylineEvents } from "./polylines/setupPolylines.js"; import { store } from "../redux/store"; import { updateLineStatus } from "../redux/slices/lineVisibilitySlice"; +import { setSelectedDevice, clearSelectedDevice } from "../redux/slices/selectedDeviceSlice"; +import { addContextMenuToMarker } from "./contextMenuUtils.js"; const determinePriority = (iconPath, priorityConfig) => { for (let priority of priorityConfig) { @@ -80,39 +82,98 @@ export const createAndSetDevices = async (systemId, setMarkersFunction, GisSyste areaName: station.Area_Name, link: station.Link, zIndexOffset: zIndexOffset, + deviceName: station.Device, + idDevice: station.IdLD, // ✅ Sicherstellen, dass `idDevice` existiert }); - - marker.bindPopup(` -
- ${station.LD_Name} - ${station.Device}
- ${station.Area_Short} (${station.Area_Name})
- ${station.Location_Short} (${station.Location_Name}) -
- ${statusDistrictData.Statis.filter((status) => status.IdLD === station.IdLD) - .reverse() - .map( - (status) => ` -
-
- ${status.Me} (${status.Na}) -
- ` - ) - .join("")} -
+ const popupContent = ` +
+ ${station.LD_Name} + ${station.Device}
+ ${station.Area_Short} (${station.Area_Name})
+ ${station.Location_Short} (${station.Location_Name}) +
+ ${statusDistrictData.Statis.filter((status) => status.IdLD === station.IdLD) + .reverse() + .map( + (status) => ` +
+
+ ${status.Me} (${status.Na})
- `); + ` + ) + .join("")} +
+
+ `; + marker.bindPopup(popupContent); - marker.on("mouseover", () => marker.openPopup()); - marker.on("mouseout", () => marker.closePopup()); - marker.on("contextmenu", (event) => { - if (event.originalEvent?.preventDefault) { - event.originalEvent.preventDefault(); - } - marker.openPopup(); + // Redux: Gerät setzen + marker.on("mouseover", () => { + //console.log("✅ Gerät erkannt:", station); + store.dispatch(setSelectedDevice({ id: station.IdLD, name: station.Device, area: station.Area_Name })); + marker.openPopup(); // Bestehender Code bleibt erhalten }); + //----------------------------------- + //----------------------------------- + // Variable zum Speichern der hinzugefügten Menüeinträge + let contextMenuItemIds = new Set(); + + const addDeviceContextMenu = (map, marker) => { + if (map && map.contextmenu) { + // Vor dem Hinzufügen sicherstellen, dass vorherige Menüeinträge entfernt werden + if (contextMenuItemIds.size > 0) { + contextMenuItemIds.forEach((id) => map.contextmenu.removeItem(id)); + contextMenuItemIds.clear(); // Set zurücksetzen + } + + // Menüeinträge hinzufügen + const editItem = map.contextmenu.addItem({ + text: "Gerät bearbeiten", + icon: "img/edit.png", + callback: () => console.log(`Bearbeite Gerät: ${marker.options.deviceName}`), + }); + + const deleteItem = map.contextmenu.addItem({ + text: "Gerät entfernen", + icon: "img/delete.png", + callback: () => console.log(`Entferne Gerät: ${marker.options.deviceName}`), + }); + + const separator = map.contextmenu.addItem({ separator: true }); + + const detailsItem = map.contextmenu.addItem({ + text: "Details anzeigen", + icon: "img/details.png", + callback: () => console.log(`Details für Gerät: ${marker.options.deviceName}`), + }); + + // IDs speichern + contextMenuItemIds.add(editItem); + contextMenuItemIds.add(deleteItem); + contextMenuItemIds.add(separator); + contextMenuItemIds.add(detailsItem); + } + }; + + // `contextmenu`-Event für Marker + marker.on("contextmenu", (event) => { + event.originalEvent?.preventDefault(); + marker.openPopup(); + addDeviceContextMenu(map, marker); + }); + + // Menü entfernen, wenn außerhalb des Markers geklickt wird + map.on("click", () => { + if (map.contextmenu && contextMenuItemIds.size > 0) { + contextMenuItemIds.forEach((id) => map.contextmenu.removeItem(id)); + contextMenuItemIds.clear(); + } + }); + + //----------------------------------- + //----------------------------------- if (typeof marker.bounce === "function" && statis) { marker.on("add", () => marker.bounce(3)); } diff --git a/utils/initializeMap.js b/utils/initializeMap.js index 27703a01d..b28ca1485 100644 --- a/utils/initializeMap.js +++ b/utils/initializeMap.js @@ -3,104 +3,119 @@ import L from "leaflet"; import "leaflet-contextmenu"; import "leaflet/dist/leaflet.css"; import "leaflet-contextmenu/dist/leaflet.contextmenu.css"; -import "leaflet-control-geocoder"; - +import { store } from "../redux/store"; import * as urls from "../config/urls.js"; import * as layers from "../config/layers.js"; -import { openInNewTab } from "./openInNewTab.js"; export const initializeMap = (mapRef, setMap, setOms, setMenuItemAdded, addItemsToMapContextMenu, hasRights, setPolylineEventsDisabled) => { - if (mapRef.current) { - const initMap = L.map(mapRef.current, { - center: [53.111111, 8.4625], - zoom: 12, - minZoom: 5, - maxZoom: 15, - layers: [ - layers.MAP_LAYERS.TALAS, - layers.MAP_LAYERS.ECI, - layers.MAP_LAYERS.GSMModem, - layers.MAP_LAYERS.CiscoRouter, - layers.MAP_LAYERS.WAGO, - layers.MAP_LAYERS.Siemens, - layers.MAP_LAYERS.OTDR, - layers.MAP_LAYERS.WDM, - layers.MAP_LAYERS.GMA, - layers.MAP_LAYERS.TALASICL, - layers.MAP_LAYERS.Sonstige, - layers.MAP_LAYERS.ULAF, - ], + if (!mapRef.current) { + console.error("❌ Fehler: mapRef.current ist nicht definiert."); + return; + } - zoomControl: false, - contextmenu: true, - contextmenuItems: [ - { - text: "Station öffnen (Tab)", - icon: "/img/screen_new.png", - callback: (e) => { - const editMode = localStorage.getItem("editMode") === "true"; - const clickedElement = e.relatedTarget; + if (mapRef.current._leaflet_id) { + console.log("⚠️ Karte ist bereits initialisiert – `dragging.enable()` wird sichergestellt."); + setTimeout(() => { + if (mapRef.current) { + mapRef.current.dragging.enable(); + } + }, 100); + return; + } - if (!clickedElement) { - console.error("No valid target for the context menu entry (Element is null)."); - return; - } + // Leaflet-Karte erstellen + const initMap = L.map(mapRef.current, { + center: [53.111111, 8.4625], + zoom: 12, + minZoom: 5, + maxZoom: 15, + zoomControl: false, + dragging: true, + contextmenu: true, // ✅ Sicherstellen, dass Kontextmenü aktiviert ist + layers: [ + layers.MAP_LAYERS.TALAS, + layers.MAP_LAYERS.ECI, + layers.MAP_LAYERS.GSMModem, + layers.MAP_LAYERS.CiscoRouter, + layers.MAP_LAYERS.WAGO, + layers.MAP_LAYERS.Siemens, + layers.MAP_LAYERS.OTDR, + layers.MAP_LAYERS.WDM, + layers.MAP_LAYERS.GMA, + layers.MAP_LAYERS.TALASICL, + layers.MAP_LAYERS.Sonstige, + layers.MAP_LAYERS.ULAF, + ], + }); - // Prüfen, ob es ein POI ist (POIs haben idPoi in den Optionen) - if (clickedElement instanceof L.Marker && clickedElement.options?.idPoi) { - console.log("POI erkannt - Station öffnen deaktiviert."); - return; // Keine Aktion ausführen - } + initMap.dragging.enable(); - try { - if (clickedElement instanceof L.Marker || clickedElement?.options?.link) { - const link = "http://" + window.location.hostname + "/talas5/devices/" + clickedElement.options.link; - if (link) { - console.log("Opening link in a new tab:", link); - //window.open(link, "_blank"); POI-Link öffnen in neuem Tab - } else { - console.error("No link found in the Marker options."); - } - } else { - console.error("No valid target for the context menu entry"); - } - } catch (error) { - console.error("Error processing the context menu:", error); - } - }, - }, - "-", - ], - }); + // 🌍 **🚀 Kontextmenü sicherstellen** + if (!initMap.contextmenu) { + console.error("❌ `contextmenu` ist nicht verfügbar."); + return; + } - // Füge die Tile-Layer hinzu - L.tileLayer(urls.OFFLINE_TILE_LAYER, { - attribution: '© OpenStreetMap contributors', - }).addTo(initMap); + // **Kontextmenü für Geräte aktualisieren** + initMap.on("contextmenu.show", (e) => { + const clickedElement = e.relatedTarget; + const selectedDevice = store.getState().selectedDevice; // Redux-Wert abrufen - // Suchfeld hinzufügen - /* const geocoder = L.Control.geocoder({ - defaultMarkGeocode: false, - }) - .on("markgeocode", function (e) { - const latlng = e.geocode.center; - initMap.setView(latlng, 15); - L.marker(latlng).addTo(initMap).bindPopup(e.geocode.name).openPopup(); - }) - .addTo(initMap); */ - - // Initialisiere OverlappingMarkerSpiderfier - const overlappingMarkerSpiderfier = new OverlappingMarkerSpiderfier(initMap, { - nearbyDistance: 20, - }); - - // Setze die Map und OMS in den State - setMap(initMap); - setOms(overlappingMarkerSpiderfier); - - // Wenn Rechte geladen sind und es noch nicht hinzugefügt wurde, füge das Kontextmenü hinzu - if (hasRights && !setMenuItemAdded) { - addItemsToMapContextMenu(initMap, setMenuItemAdded, setPolylineEventsDisabled); + if (!initMap.contextmenu || !initMap.contextmenu.items) { + console.error("❌ Fehler: `contextmenu` oder `items` ist nicht definiert."); + return; } + + try { + initMap.contextmenu.removeAllItems(); // **Sicherstellen, dass vorherige Einträge entfernt werden** + } catch (error) { + console.error("❌ Fehler beim Entfernen der Menüelemente:", error); + } + + if (!clickedElement) { + console.error("❌ Kein gültiges Ziel für das Kontextmenü."); + return; + } + + // 🛑 Falls `selectedDevice === null`, kein "Station öffnen" anzeigen + if (!selectedDevice || !clickedElement.options?.idDevice) { + console.log("ℹ️ Kein Gerät ausgewählt – 'Station öffnen' wird nicht hinzugefügt."); + return; + } + + // ✅ Falls `selectedDevice` gesetzt ist, "Station öffnen" anzeigen + console.log("✅ Gerät erkannt – 'Station öffnen' wird hinzugefügt."); + initMap.contextmenu.addItem({ + text: "Station öffnen (Tab)", + icon: "/img/screen_new.png", + callback: () => { + const link = `http://${window.location.hostname}/talas5/devices/${selectedDevice.id}`; + if (link) { + console.log("🟢 Öffne Link in neuem Tab:", link); + window.open(link, "_blank"); + } else { + console.error("❌ Kein Link in den Marker-Optionen gefunden."); + } + }, + }); + }); + + // Tile-Layer hinzufügen + L.tileLayer(urls.OFFLINE_TILE_LAYER, { + attribution: '© OpenStreetMap contributors', + }).addTo(initMap); + + // Initialisiere OverlappingMarkerSpiderfier + const overlappingMarkerSpiderfier = new OverlappingMarkerSpiderfier(initMap, { + nearbyDistance: 20, + }); + + // Setze die Map und OMS in den State + setMap(initMap); + setOms(overlappingMarkerSpiderfier); + + // Falls Rechte geladen sind, füge das Kontextmenü hinzu + if (hasRights && !setMenuItemAdded) { + addItemsToMapContextMenu(initMap, setMenuItemAdded, setPolylineEventsDisabled); } }; diff --git a/utils/setupDevices.js b/utils/setupDevices.js new file mode 100644 index 000000000..5528c58f1 --- /dev/null +++ b/utils/setupDevices.js @@ -0,0 +1,18 @@ +// utils/setupDevices.js +import { setSelectedDevice, clearSelectedDevice } from "../redux/slices/selectedDeviceSlice"; + +export const setupDevices = async (map, deviceMarkers, dispatch) => { + for (const marker of deviceMarkers) { + marker.on("mouseover", function () { + console.log("✅ Gerät ausgewählt:", marker); + dispatch(setSelectedDevice(marker.options)); // Gerät in Redux speichern + }); + + marker.on("mouseout", function () { + console.log("❌ Gerät abgewählt"); + dispatch(clearSelectedDevice()); // Gerät aus Redux entfernen + }); + + marker.addTo(map); + } +}; diff --git a/utils/setupPOIs.js b/utils/setupPOIs.js index 6789faa61..6a5af4177 100644 --- a/utils/setupPOIs.js +++ b/utils/setupPOIs.js @@ -10,6 +10,8 @@ import endIcon from "../components/gisPolylines/icons/EndIcon"; import { redrawPolyline } from "./polylines/redrawPolyline"; import { openInNewTab } from "../utils/openInNewTab"; import { disablePolylineEvents, enablePolylineEvents } from "./polylines/setupPolylines"; // Importiere die Funktionen +import { setSelectedPoi, clearSelectedPoi } from "../redux/slices/selectedPoiSlice"; +import { useDispatch } from "react-redux"; export const setupPOIs = async ( map, @@ -27,7 +29,8 @@ export const setupPOIs = async ( toast, setShowPoiUpdateModal, setCurrentPoiData, - deviceName + deviceName, + dispatch ) => { const editMode = localStorage.getItem("editMode") === "true"; // Prüfen, ob der Bearbeitungsmodus aktiv ist userRights = editMode ? userRights : undefined; // Nur Berechtigungen anwenden, wenn editMode true ist @@ -83,7 +86,9 @@ export const setupPOIs = async ( `); marker.on("mouseover", function () { - //console.log("Device Name:", marker); // Debugging + console.log("Device Name in setupPOIs.js :", marker); // Debugging + dispatch(setSelectedPoi(location)); // POI in Redux setzen + handlePoiSelect( { id: location.idPoi, @@ -105,6 +110,7 @@ export const setupPOIs = async ( }); marker.on("mouseout", function () { + dispatch(clearSelectedPoi()); // POI aus Redux entfernen this.closePopup(); });