Files
nodeMap/components/mainComponent/MapComponent.js

1300 lines
47 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// components/mainComponent/MapComponent.js
import React, { useEffect, useRef, useState, useCallback } from "react";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import "leaflet-contextmenu/dist/leaflet.contextmenu.css";
import "leaflet-contextmenu";
import "leaflet.smooth_marker_bouncing";
import "react-toastify/dist/ReactToastify.css";
import { Icon } from "@iconify/react";
import EditIcon from "@/components/icons/material-symbols/EditIcon";
import EditOffIcon from "@/components/icons/material-symbols/EditOffIcon";
import SearchIcon from "@/components/icons/material-symbols/SearchIcon";
import MenuIcon from "@/components/icons/material-symbols/MenuIcon";
import InfoIcon from "@/components/icons/material-symbols/InfoIcon";
import AlarmIcon from "@/components/icons/material-symbols/AlarmIcon";
import MapMarkerIcon from "@/components/icons/material-symbols/MapMarkerIcon";
import ExpandIcon from "@/components/icons/material-symbols/ExpandIcon";
import PoiUpdateModal from "@/components/pois/poiUpdateModal/PoiUpdateModal.js";
import { ToastContainer, toast } from "react-toastify";
import plusRoundIcon from "../icons/devices/overlapping/PlusRoundIcon.js";
import StartIcon from "@/components/gisPolylines/icons/StartIcon.js";
import EndIcon from "@/components/gisPolylines/icons/EndIcon.js";
import CircleIcon from "@/components/gisPolylines/icons/CircleIcon.js";
import { restoreMapSettings, checkOverlappingMarkers } from "../../utils/mapUtils.js";
import addItemsToMapContextMenu from "@/components/contextmenu/useMapContextMenu.js";
import useAreaMarkersLayer from "@/components/area/hooks/useAreaMarkersLayer.js";
import { setupPolylines } from "@/utils/polylines/setupPolylines.js";
import { setupPOIs } from "@/utils/setupPOIs.js";
import useLineData from "@/components/gisPolylines/tooltip/useLineData.js";
import { useMapComponentState } from "@/components/hooks/useMapComponentState.js";
import CoordinatePopup from "@/components/contextmenu/CoordinatePopup.js";
//----------Ui Widgets----------------
import MapLayersControlPanel from "@/components/uiWidgets/mapLayersControlPanel/MapLayersControlPanel.js";
import CoordinateInput from "@/components/uiWidgets/CoordinateInput.js";
import VersionInfoModal from "@/components/uiWidgets/VersionInfoModal.js";
import AreaDropdown from "@/components/uiWidgets/AreaDropdown";
//----------Daten aus API--------------------
import { fetchPoiDataService } from "@/services/database/pois/fetchPoiDataByIdService.js";
import AddPOIModal from "@/components/pois/AddPOIModal.js";
import { enablePolylineEvents, disablePolylineEvents } from "@/utils/polylines/eventHandlers";
//----------MapComponent.js hooks--------------------
import useInitializeMap from "@/components/mainComponent/hooks/useInitializeMap";
//-------------------Redux--------------------
import { useSelector, useDispatch } from "react-redux";
//-------------------Redux-Slices--------------------
import { setSelectedPoi } from "@/redux/slices/database/pois/selectedPoiSlice.js";
import { setDisabled } from "@/redux/slices/database/polylines/polylineEventsDisabledSlice.js";
import { setMapId, setUserId } from "@/redux/slices/urlParameterSlice";
import { selectMapLayersState, setLayerVisibility } from "@/redux/slices/mapLayersSlice";
import { setSelectedArea } from "@/redux/slices/selectedAreaSlice";
import { incrementZoomTrigger } from "@/redux/slices/zoomTriggerSlice";
import { setCurrentPoi } from "@/redux/slices/database/pois/currentPoiSlice.js";
import { selectGisLines } from "@/redux/slices/database/polylines/gisLinesSlice";
import { selectGisLinesStatus } from "@/redux/slices/webservice/gisLinesStatusSlice";
import { selectPoiTypData, selectPoiTypStatus } from "@/redux/slices/database/pois/poiTypSlice";
import { selectPriorityConfig } from "@/redux/slices/database/priorityConfigSlice.js";
import {
selectPoiIconsData,
selectPoiIconsStatus,
} from "@/redux/slices/database/pois/poiIconsDataSlice";
import { selectGisLinesStatusFromWebservice } from "@/redux/slices/webservice/gisLinesStatusSlice";
import { selectGisUserRightsFromWebservice } from "@/redux/slices/webservice/userRightsSlice";
import {
updateCountdown,
closePolylineContextMenu,
} from "@/redux/slices/database/polylines/polylineContextMenuSlice.js";
import {
selectPolylineVisible,
setPolylineVisible,
initializePolylineFromLocalStorageThunk,
} from "@/redux/slices/database/polylines/polylineLayerVisibleSlice.js";
import { selectGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice.js";
import {
selectGisSystemStatic,
setGisSystemStatic,
} from "@/redux/slices/webservice/gisSystemStaticSlice.js";
//-----------Redux-Thunks-------------------
import { fetchGisStationsMeasurementsThunk } from "@/redux/thunks/webservice/fetchGisStationsMeasurementsThunk";
import { fetchGisSystemStaticThunk } from "@/redux/thunks/webservice/fetchGisSystemStaticThunk";
import { fetchGisStationsStaticDistrictThunk } from "@/redux/thunks/webservice/fetchGisStationsStaticDistrictThunk";
import { fetchGisStationsStatusDistrictThunk } from "@/redux/thunks/webservice/fetchGisStationsStatusDistrictThunk";
import { fetchLocationDevicesThunk } from "@/redux/thunks/database/fetchLocationDevicesThunk";
import { fetchPriorityConfigThunk } from "@/redux/thunks/database/fetchPriorityConfigThunk.js";
import { fetchGisLinesThunk } from "@/redux/thunks/database/polylines/fetchGisLinesThunk.js";
import { fetchGisLinesStatusThunk } from "@/redux/thunks/webservice/fetchGisLinesStatusThunk";
import { fetchUserRightsThunk } from "@/redux/thunks/webservice/fetchUserRightsThunk";
import { fetchPoiIconsDataThunk } from "@/redux/thunks/database/pois/fetchPoiIconsDataThunk.js";
import { fetchPoiTypThunk } from "@/redux/thunks/database/pois/fetchPoiTypThunk.js";
import { updateAreaThunk } from "@/redux/thunks/database/area/updateAreaThunk";
import useDynamicDeviceLayers from "@/components/devices/hooks/useDynamicDeviceLayers.js";
import useDataUpdater from "@/components/hooks/useDataUpdater.js";
import { cleanupPolylinesForMemory } from "@/utils/polylines/cleanupPolylinesForMemory";
import { cleanupMarkers } from "@/utils/common/cleanupMarkers";
import { monitorHeapAndReload } from "@/utils/common/monitorMemory";
import { monitorHeapWithRedux } from "@/utils/common/monitorMemory";
import { io } from "socket.io-client";
import { setGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice.js";
import { getDebugLog } from "../../utils/configUtils";
//-----------------------------------------------------------------------------------------------------
const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
//-------------------------------
const appVersion = process.env.NEXT_PUBLIC_APP_VERSION;
const dispatch = useDispatch();
// useDataUpdater();
const [triggerUpdate, setTriggerUpdate] = useState(false);
const countdown = useSelector(state => state.polylineContextMenu.countdown);
const countdownActive = useSelector(state => state.polylineContextMenu.countdownActive);
const isPolylineContextMenuOpen = useSelector(state => state.polylineContextMenu.isOpen);
const polylineVisible = useSelector(selectPolylineVisible);
const polylineInitialized = useSelector(state => state.polylineLayerVisible.isInitialized);
const GisSystemStatic = useSelector(selectGisSystemStatic);
// Prüfen, ob TALAS (IdSystem 1) erlaubt ist
const isTalasAllowed = Array.isArray(GisSystemStatic)
? GisSystemStatic.some(
system => system.IdSystem === 1 && system.Allow === 1 && system.Map === 1
)
: false;
const isPoiTypLoaded = useSelector(state => state.poiTypes.status === "succeeded");
const statusMeasurements = useSelector(state => state.gisStationsMeasurements.status);
const statusSystem = useSelector(state => state.gisSystemStatic.status);
const statusStaticDistrict = useSelector(state => state.gisStationsStaticDistrict.status);
const statusStatusDistrict = useSelector(state => state.gisStationsStatusDistrict.status);
const priorityConfig = useSelector(selectPriorityConfig);
const poiLayerVisible = useSelector(state => state.poiLayerVisible.visible);
const zoomTrigger = useSelector(state => state.zoomTrigger.trigger);
const poiReadTrigger = useSelector(state => state.poiReadFromDbTrigger.trigger);
const GisStationsStaticDistrict = useSelector(selectGisStationsStaticDistrict);
const gisSystemStaticStatus = useSelector(state => state.gisSystemStatic.status);
const polylineEventsDisabled = useSelector(state => state.polylineEventsDisabled.disabled);
const mapLayersVisibility = useSelector(selectMapLayersState) || {};
const selectedArea = useSelector(state => state.selectedArea.area);
const linesData = useSelector(state => state.gisLinesFromDatabase.data);
const gisLinesStatus = useSelector(state => state.gisLinesStatusFromWebservice.status);
const { data: gisLinesStatusData, status: statusGisLinesStatus } = useSelector(
selectGisLinesStatusFromWebservice
);
// Alarm Status aus GisStationsStatusDistrict
const gisStationsStatusDistrict = useSelector(state => state.gisStationsStatusDistrict.data);
// Unterstützt sowohl Array-Shape (Statis[]) als auch Objekt mit Statis-Array
const hasActiveAlarm = Array.isArray(gisStationsStatusDistrict)
? gisStationsStatusDistrict.some(item => item?.Alarm === 1)
: gisStationsStatusDistrict?.Statis?.some(item => item?.Alarm === 1) || false;
const poiIconsData = useSelector(selectPoiIconsData);
const poiIconsStatus = useSelector(selectPoiIconsStatus);
const poiTypData = useSelector(selectPoiTypData);
const poiTypStatus = useSelector(state => state.poiTypes.status);
//const poiTypStatus = useSelector(selectPoiTypStatus);
//-------------------------------
const { deviceName, setDeviceName } = useMapComponentState();
const [locationDeviceData, setLocationDeviceData] = useState([]);
const [menuItemAdded, setMenuItemAdded] = useState(false);
const [isPopupOpen, setIsPopupOpen] = useState(false);
const closePopup = () => setIsPopupOpen(false);
const [currentCoordinates, setCurrentCoordinates] = useState("");
const [showPoiUpdateModal, setShowPoiUpdateModal] = useState(false);
const [currentPoiData, setCurrentPoiData] = useState(null);
const [showVersionInfoModal, setShowVersionInfoModal] = useState(false);
const [poiTypMap, setPoiTypMap] = useState(new Map());
const [showPopup, setShowPopup] = useState(false);
const [showAreaDropdown, setShowAreaDropdown] = useState(() => {
try {
const v = localStorage.getItem("showAreaDropdown");
return v === null ? false : v === "true";
} catch (_) {
return false;
}
});
const poiLayerRef = useRef(null); // Referenz auf die Layer-Gruppe für Datenbank-Marker
const mapRef = useRef(null); // Referenz auf das DIV-Element der Karte
const [map, setMap] = useState(null); // Zustand der Karteninstanz
const [oms, setOms] = useState(null); // State für OMS-Instanz
// Sichtbarkeit der App-Info-Karte (unten links)
const [showAppInfoCard, setShowAppInfoCard] = useState(() => {
try {
const v = localStorage.getItem("showAppInfoCard");
return v === null ? true : v === "true";
} catch (_) {
return true;
}
});
// Sichtbarkeit des Layer-Kontrollpanels (oben rechts)
const [showLayersPanel, setShowLayersPanel] = useState(() => {
try {
const v = localStorage.getItem("showLayersPanel");
return v === null ? true : v === "true";
} catch (_) {
return true;
}
});
// Base-Map Panel wurde entfernt
// Sichtbarkeit der Koordinaten-Suche (Lupe)
const [showCoordinateInput, setShowCoordinateInput] = useState(() => {
try {
const v = localStorage.getItem("showCoordinateInput");
return v === null ? false : v === "true";
} catch (_) {
return false;
}
});
// Zentrale Steuerung: Nur ein Overlay gleichzeitig
// Mögliche Werte: null | 'area' | 'layers' | 'coord' | 'info'
const [overlay, setOverlay] = useState(null);
// Initiale Bestimmung des aktiven Overlays basierend auf bestehenden Flags
useEffect(() => {
if (showAreaDropdown) setOverlay("area");
else if (showLayersPanel) setOverlay("layers");
else if (showCoordinateInput) setOverlay("coord");
else if (showAppInfoCard) setOverlay("info");
else setOverlay(null);
// nur beim Mount ausführen
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Flags mit Overlay-State synchronisieren (persistiert weiterhin in bestehenden Effects)
useEffect(() => {
setShowAreaDropdown(overlay === "area");
setShowLayersPanel(overlay === "layers");
setShowCoordinateInput(overlay === "coord");
setShowAppInfoCard(overlay === "info");
}, [overlay]);
// Flag, ob Nutzer die Polyline-Checkbox manuell betätigt hat
// Nutzer-Flag global auf window, damit auch Redux darauf zugreifen kann
if (typeof window !== "undefined" && window.userToggledPolyline === undefined) {
window.userToggledPolyline = false;
}
//-----userRights----------------
const isRightsLoaded = useSelector(
state => state.gisUserRightsFromWebservice.status === "succeeded"
);
const userRights = useSelector(selectGisUserRightsFromWebservice);
const hasRights = userRights.includes(56);
//-----------------------------
const openPopupWithCoordinates = e => {
const coordinates = `${e.latlng.lat.toFixed(5)}, ${e.latlng.lng.toFixed(5)}`;
setCurrentCoordinates(coordinates);
setIsPopupOpen(true);
setPopupCoordinates(e.latlng);
setPopupVisible(true);
};
// Konstanten für die URLs
//-----------------------------------------
const [linePositions, setLinePositions] = useState([]);
const { lineColors, tooltipContents } = useLineData();
const [polylines, setPolylines] = useState([]);
const [markers, setMarkers] = useState([]);
const [newPoint, setNewPoint] = useState(null);
const [newCoords, setNewCoords] = useState(null);
const [tempMarker, setTempMarker] = useState(null);
const [showPoiModal, setShowPoiModal] = useState(false);
const [showCoordinatesModal, setShowCoordinatesModal] = useState(false);
const [popupCoordinates, setPopupCoordinates] = useState(null);
const [popupVisible, setPopupVisible] = useState(false);
const [poiData, setPoiData] = useState([]);
// Edit mode state mirrors MapLayersControlPanel's behavior
const [editMode, setEditMode] = useState(() => {
try {
return localStorage.getItem("editMode") === "true";
} catch (_) {
return false;
}
});
const openVersionInfoModal = () => {
setShowVersionInfoModal(true);
};
const closeVersionInfoModal = () => {
setShowVersionInfoModal(false);
};
const [currentZoom, setCurrentZoom] = useState(() => {
const storedZoom = localStorage.getItem("mapZoom");
return storedZoom ? parseInt(storedZoom, 10) : 12;
});
const [currentCenter, setCurrentCenter] = useState(() => {
const storedCenter = localStorage.getItem("mapCenter");
try {
return storedCenter ? JSON.parse(storedCenter) : [53.111111, 8.4625];
} catch (e) {
console.error("Error parsing stored map center:", e);
return [53.111111, 8.4625];
}
});
// Persistiere Sichtbarkeit der App-Info-Karte
useEffect(() => {
try {
localStorage.setItem("showAppInfoCard", String(showAppInfoCard));
} catch (_) {}
}, [showAppInfoCard]);
// Persistiere Sichtbarkeit des Area-Dropdowns (Marker-Overlay)
useEffect(() => {
try {
localStorage.setItem("showAreaDropdown", String(showAreaDropdown));
} catch (_) {}
}, [showAreaDropdown]);
// Persistiere Sichtbarkeit des Layer-Panels
useEffect(() => {
try {
localStorage.setItem("showLayersPanel", String(showLayersPanel));
} catch (_) {}
}, [showLayersPanel]);
// Persist-Logik für Base-Map Panel entfernt
// Persistiere Sichtbarkeit der Koordinaten-Suche
useEffect(() => {
try {
localStorage.setItem("showCoordinateInput", String(showCoordinateInput));
} catch (_) {}
}, [showCoordinateInput]);
//--------------------------------------------
const handleCoordinatesSubmit = coords => {
const [lat, lng] = coords.split(",").map(Number);
if (map && lat && lng) {
map.flyTo([lat, lng], 12); // Zentriere die Karte auf die Koordinaten
}
};
//-----------------------------Map Initialisierung----------------
// Default map options for Leaflet
const mapOptions = {
center: currentCenter,
zoom: currentZoom,
zoomControl: true,
contextmenu: true,
contextmenuWidth: 180,
contextmenuItems: [],
};
useInitializeMap(
map,
mapRef,
setMap,
setOms,
setMenuItemAdded,
addItemsToMapContextMenu,
hasRights,
value => dispatch(setDisabled(value)),
mapOptions // pass mapOptions
);
//-------------------------React Hooks--------------------------------
// URL-Parameter extrahieren und kartenspezifische localStorage-Keys verwenden
useEffect(() => {
// Immer beim Umschalten der Kabelstrecken-Checkbox prüfen!
if (map) {
if (!polylineVisible) {
map.eachLayer(layer => {
// Entferne alle Marker mit StartIcon, EndIcon oder CircleIcon (Stützpunkt)
if (
layer instanceof L.Marker &&
layer.options &&
layer.options.icon &&
(layer.options.icon === StartIcon ||
layer.options.icon === EndIcon ||
layer.options.icon === CircleIcon)
) {
map.removeLayer(layer);
}
});
}
}
// Initialisierung der Layer-Visibility und Polyline-Redux-State nur beim Mount
if (typeof window !== "undefined") {
const params = new URLSearchParams(window.location.search);
const mapId = params.get("m");
const userId = params.get("u");
if (mapId && userId) {
// Speichere aktuelle Map- und User-ID
localStorage.setItem("currentMapId", mapId);
localStorage.setItem("currentUserId", userId);
// Kartenspezifischer localStorage-Key
const mapStorageKey = `mapLayersVisibility_m${mapId}_u${userId}`;
const storedMapLayersVisibility = localStorage.getItem(mapStorageKey);
if (storedMapLayersVisibility) {
try {
const parsedVisibility = JSON.parse(storedMapLayersVisibility);
// Nur initial setzen, wenn Nutzer noch nicht manuell eingegriffen hat
if (!userToggledPolyline.current) {
Object.keys(parsedVisibility).forEach(key => {
dispatch(setLayerVisibility({ layer: key, visibility: parsedVisibility[key] }));
});
console.log(
`🔄 mapLayersVisibility für Map ${mapId}/User ${userId} geladen:`,
parsedVisibility
);
}
} catch (error) {
console.error("❌ Fehler beim Laden von mapLayersVisibility:", error);
}
} else {
console.log(
`📝 Keine gespeicherten Einstellungen für Map ${mapId}/User ${userId} gefunden`
);
}
// Redux Polyline Sichtbarkeit initialisieren (map/user spezifisch)
if (!userToggledPolyline.current) {
dispatch(initializePolylineFromLocalStorageThunk());
}
}
}
}, [dispatch, polylineVisible, map]);
// Callback für Checkbox-Umschaltung (Kabelstrecken)
const handlePolylineCheckboxChange = useCallback(
checked => {
if (typeof window !== "undefined") {
window.userToggledPolyline = true;
}
dispatch(setPolylineVisible(checked));
},
[dispatch]
);
useEffect(() => {
if (linesData && Array.isArray(linesData)) {
const transformed = linesData.map(item => ({
coordinates: item.points.map(point => [point.x, point.y]),
idModul: item.idModul,
idLD: item.idLD,
}));
setLinePositions(transformed);
}
}, [linesData]);
//--------------------------------------------
useEffect(() => {
dispatch(fetchPoiIconsDataThunk());
dispatch(fetchPoiTypThunk());
}, [dispatch]);
//--------------------------------------------
// POIs auf die Karte zeichnen
useEffect(() => {
if (map && !poiLayerRef.current) {
poiLayerRef.current = new L.LayerGroup().addTo(map);
}
return () => {
if (map && poiLayerRef.current) {
map.removeLayer(poiLayerRef.current);
poiLayerRef.current = new L.LayerGroup().addTo(map);
}
locations.forEach(location => {});
};
}, [map, locations, poiReadTrigger]);
//--------------------------------------------
const { Points: gisDevices = [] } = useSelector(selectGisStationsStaticDistrict);
// POIs auf die Karte zeichnen
useEffect(() => {
if (poiData.length === 0) return;
setupPOIs(
map,
locations,
poiData,
poiTypMap,
userRights,
poiLayerRef,
setSelectedPoi,
setLocationDeviceData,
setDeviceName,
setCurrentPoi,
poiLayerVisible,
fetchPoiDataService,
toast,
setShowPoiUpdateModal,
setCurrentPoiData,
gisDevices,
dispatch
);
}, [
map,
locations,
onLocationUpdate,
poiReadTrigger,
isPoiTypLoaded,
userRights,
poiLayerVisible,
poiData,
poiTypMap,
dispatch,
]);
//---------------------------------------------
useEffect(() => {
if (map) {
}
}, [map, GisSystemStatic, priorityConfig]);
//--------------------------------------------
useEffect(() => {
if (map) {
var x = 51.41321407879154;
var y = 7.739617925303934;
var zoom = 7;
if (map && map.flyTo) {
map.flyTo([x, y], zoom);
} else {
console.error("Map object is not ready or does not have flyTo method");
}
}
}, [map, zoomTrigger]);
//--------------------------------------------
//-----------------------------------------------------------------
//Tooltip an mouse position anzeigen für die Linien
useEffect(() => {
if (!map) return;
console.log(
"[MapComponent/useEffect] polylineVisible:",
polylineVisible,
"isTalasAllowed:",
isTalasAllowed,
"poiLayerVisible:",
poiLayerVisible
);
// Wenn TALAS nicht erlaubt ist, Polyline-Checkbox und Anzeige deaktivieren
if (!isTalasAllowed) {
cleanupPolylinesForMemory(polylines, map);
setPolylines([]);
return;
}
// Die Sichtbarkeit der Polylines hängt nur noch vom Redux-Slice ab
// vorherige Marker & Polylinien vollständig bereinigen
(Array.isArray(markers) ? markers : []).forEach(marker => {
marker.remove();
});
cleanupPolylinesForMemory(polylines, map);
console.log("[MapComponent/useEffect] Nach cleanupPolylinesForMemory, polylines:", polylines);
// Setze neue Marker und Polylinien mit den aktuellen Daten (asynchron!)
const updatePolylines = async () => {
if (polylineVisible) {
const { markers: newMarkers, polylines: newPolylines } = await setupPolylines(
map,
linePositions,
lineColors,
tooltipContents,
setNewCoords,
tempMarker,
currentZoom,
currentCenter,
polylineVisible
);
(Array.isArray(newPolylines) ? newPolylines : []).forEach((polyline, index) => {
const tooltipContent =
tooltipContents[`${linePositions[index].idLD}-${linePositions[index].idModul}`] ||
"Die Linie ist noch nicht in Webservice vorhanden oder bekommt keine Daten";
polyline.bindTooltip(tooltipContent, {
permanent: false,
direction: "auto",
sticky: true,
offset: [20, 0],
pane: "tooltipPane",
});
polyline.on("mouseover", e => {
const tooltip = polyline.getTooltip();
if (tooltip) {
const mousePos = e.containerPoint;
const mapSize = map.getSize();
let direction = "right";
if (mousePos.x > mapSize.x - 100) {
direction = "left";
} else if (mousePos.x < 100) {
direction = "right";
}
if (mousePos.y > mapSize.y - 100) {
direction = "top";
} else if (mousePos.y < 100) {
direction = "bottom";
}
tooltip.options.direction = direction;
polyline.openTooltip(e.latlng);
}
});
polyline.on("mouseout", () => {
polyline.closeTooltip();
});
});
cleanupMarkers(markers, oms);
setMarkers(newMarkers);
setPolylines(newPolylines);
console.log("[MapComponent/useEffect] setPolylines (sichtbar):", newPolylines);
} else {
// Entferne wirklich alle Polylinien-Layer von der Karte
if (map) {
map.eachLayer(layer => {
if (layer instanceof L.Polyline) {
map.removeLayer(layer);
}
});
}
cleanupPolylinesForMemory(polylines, map);
setPolylines([]);
console.log("[MapComponent/useEffect] setPolylines ([]), alle Polylinien entfernt");
}
};
updatePolylines();
}, [
map,
linePositions,
lineColors,
tooltipContents,
newPoint,
newCoords,
tempMarker,
polylineVisible,
isTalasAllowed,
poiLayerVisible,
]);
//--------------------------------------------
//Test in useEffect
useEffect(() => {
if (map) {
if (getDebugLog()) {
console.log("🗺️ Map-Einstellungen werden wiederhergestellt...");
}
restoreMapSettings(map);
}
}, [map]);
//--------------------------------------------
useEffect(() => {
if (map) {
if (getDebugLog()) {
console.log("map in MapComponent: ", map);
}
const handleMapMoveEnd = event => {
const newCenter = map.getCenter();
const newZoom = map.getZoom();
setCurrentCenter([newCenter.lat, newCenter.lng]);
setCurrentZoom(newZoom);
localStorage.setItem("mapCenter", JSON.stringify([newCenter.lat, newCenter.lng]));
localStorage.setItem("mapZoom", newZoom);
};
map.on("moveend", handleMapMoveEnd);
map.on("zoomend", handleMapMoveEnd);
return () => {
map.off("moveend", handleMapMoveEnd);
map.off("zoomend", handleMapMoveEnd);
};
}
}, [map]);
//--------------------------------------------
// Area in DataSheet ->dropdownmenu
useEffect(() => {
// Sicherstellen, dass `Points` existiert und ein Array ist
const points = GisStationsStaticDistrict?.Points;
if (selectedArea && map) {
const station = points.find(s => s.Area_Name === selectedArea);
if (station) {
if (getDebugLog()) {
console.log("📌 Gefundene Station:", station);
}
map.flyTo([station.X, station.Y], 14);
} else {
console.warn("⚠️ Keine passende Station für die Area gefunden:", selectedArea);
}
}
}, [selectedArea, map, GisStationsStaticDistrict]);
//-------------------------------------
useEffect(() => {
if (zoomTrigger && map) {
map.flyTo([51.41321407879154, 7.739617925303934], 7);
}
}, [zoomTrigger, map]);
//--------------------------------------------
useEffect(() => {
if (map && poiLayerRef.current && isPoiTypLoaded && !menuItemAdded && isRightsLoaded) {
addItemsToMapContextMenu(
map,
menuItemAdded,
setMenuItemAdded,
hasRights,
setShowPopup,
setPopupCoordinates
);
}
}, [map, poiLayerRef, isPoiTypLoaded, menuItemAdded, hasRights, isRightsLoaded]);
//--------------------------------------------
// rote Marker ganz oben wenn überlappen sind
//--------------------------------------------
useEffect(() => {
if (map) {
if (polylineEventsDisabled) {
disablePolylineEvents(window.polylines);
} else {
enablePolylineEvents(window.polylines, window.lineColors);
}
}
}, [map, polylineEventsDisabled]);
//--------------------------------------------
useEffect(() => {
if (map) {
if (getDebugLog()) {
console.log("6- Karteninstanz (map) wurde jetzt erfolgreich initialisiert");
}
}
}, [map]);
//--------------------------------------------
let timeoutId;
useEffect(() => {
const initializeContextMenu = () => {
if (map) {
map.whenReady(() => {
timeoutId = setTimeout(() => {
if (map.contextmenu) {
if (getDebugLog()) {
console.log("Contextmenu ist vorhanden");
}
} else {
console.warn("Contextmenu ist nicht verfügbar.");
}
}, 500);
});
}
};
initializeContextMenu();
return () => {
clearTimeout(timeoutId); // Aufräumen
};
}, [map]);
//---------------------------------------
// Initialisiere Leaflet-Karte
// Rufe useAreaMarkersLayer direkt auf
const urlParams = new URLSearchParams(window.location.search); // URL-Parameter parsen
const mValue = urlParams.get("m"); // Wert von "m" holen
const hostname = window.location.hostname; // Dynamischer Hostname
const port = 3000; // Definiere den gewünschten Port
const areaUrl = `http://${hostname}:${port}/api/talas_v5_DB/area/readArea?m=${mValue}`; // Dynamischer Hostname und Port
// Areas-Marker basierend auf dynamischer URL laden
const handleLocationUpdate = async (idLocation, idMap, newCoords) => {
try {
await dispatch(updateAreaThunk({ idLocation, idMap, newCoords })).unwrap();
if (getDebugLog()) {
console.log("Koordinaten erfolgreich aktualisiert:", result);
}
} catch (error) {
console.error("Fehler beim Aktualisieren der Location:", error);
}
};
// Areas-Marker basierend auf dynamischer URL laden
const areaMarkers = useAreaMarkersLayer(map, oms, areaUrl, handleLocationUpdate);
/*
Areamarker werden jetzt nur angezeigt, wenn der editMode aktiviert ist.
Marker werden bei deaktiviertem editMode aus der Karte entfernt.
Dynamische Überwachung von Änderungen im editMode über localStorage und Event Listener implementiert.
Dragging für Marker im editMode aktiviert und Z-Index angepasst.
*/
useEffect(() => {
const editMode = localStorage.getItem("editMode") === "true";
// Prüfe, ob der editMode deaktiviert ist
if (!editMode) {
// Entferne alle Marker aus der Karte
if (!map) return; // Sicherstellen, dass map existiert
(Array.isArray(areaMarkers) ? areaMarkers : []).forEach(marker => {
if (map.hasLayer(marker)) {
map.removeLayer(marker);
}
});
} else {
// Wenn editMode aktiviert ist, füge die Marker hinzu und aktiviere Dragging
(Array.isArray(areaMarkers) ? areaMarkers : []).forEach(marker => {
if (!map.hasLayer(marker)) {
marker.addTo(map); // Layer hinzufügen
}
marker.dragging.enable();
marker.setZIndexOffset(1000); // Marker nach oben setzen
});
}
}, [areaMarkers, map]);
//----------------------------------
const { markerStates, layerRefs } = useDynamicDeviceLayers(
map,
GisSystemStatic,
mapLayersVisibility,
priorityConfig,
oms
);
useEffect(() => {
const handleVisibilityChange = () => {
if (!map) return;
const allMarkers = Object.values(markerStates).filter(Array.isArray).flat();
checkOverlappingMarkers(map, allMarkers, plusRoundIcon, oms);
};
window.addEventListener("visibilityChanged", handleVisibilityChange);
return () => {
window.removeEventListener("visibilityChanged", handleVisibilityChange);
};
}, [map, markerStates]);
//---------------------------------------
useEffect(() => {
if (map) {
// initGeocoderFeature(map); // Geocoder-Feature initialisieren, kann von .env.local ausgeschaltet werden
}
}, [map]);
//--------------------------------------------
useEffect(() => {
if (map && !menuItemAdded) {
addItemsToMapContextMenu(
map,
menuItemAdded,
setMenuItemAdded,
setShowCoordinatesModal,
setShowPoiModal,
setPopupCoordinates,
openPopupWithCoordinates // Diese Funktion wird jetzt übergeben!
);
}
}, [map, menuItemAdded]);
//--------------------------------------------
useEffect(() => {
dispatch(fetchUserRightsThunk());
}, [dispatch]);
//--------------------------------------------
// (Initialisierung erfolgt in MapLayersControlPanel)
//--------------------------------------------
// MapComponent reagiert nicht mehr direkt auf localStorage-Events für polylineVisible
//--------------------------------------------
useEffect(() => {
if (statusStaticDistrict === "idle") {
dispatch(fetchGisStationsStaticDistrictThunk());
}
}, [statusStaticDistrict, dispatch]);
useEffect(() => {
if (statusStatusDistrict === "idle") {
dispatch(fetchGisStationsStatusDistrictThunk());
}
}, [statusStatusDistrict, dispatch]);
useEffect(() => {
if (statusMeasurements === "idle") {
dispatch(fetchGisStationsMeasurementsThunk());
}
}, [statusMeasurements, dispatch]);
useEffect(() => {
if (statusSystem === "idle") {
dispatch(fetchGisSystemStaticThunk());
}
}, [statusSystem, dispatch]);
useEffect(() => {
dispatch(fetchGisSystemStaticThunk());
}, [dispatch]);
useEffect(() => {
dispatch(fetchLocationDevicesThunk());
}, [dispatch]);
//---------------------------------------------------------------
useEffect(() => {
const params = new URL(window.location.href).searchParams;
dispatch(setMapId(params.get("m")));
dispatch(setUserId(params.get("u")));
}, [dispatch]);
//---------------------------------------------------------------
useEffect(() => {
dispatch(fetchPriorityConfigThunk());
}, [dispatch]);
//--------------------------------------------------------
useEffect(() => {
if (gisLinesStatus === "idle") {
dispatch(fetchGisLinesThunk());
}
}, [gisLinesStatus, dispatch]);
//--------------------------------------------------------
useEffect(() => {
dispatch(fetchGisLinesStatusThunk());
}, [dispatch]);
//---------------------------------------------------------
const rights = useSelector(state => state.gisUserRightsFromWebservice.rights);
useEffect(() => {
dispatch(fetchUserRightsThunk());
}, [dispatch]);
//----------------------------------------------------
useEffect(() => {
if (poiTypStatus === "idle") {
dispatch(fetchPoiTypThunk());
}
}, [poiTypStatus, dispatch]);
//--------------------------------------
useEffect(() => {
if (isPolylineContextMenuOpen && countdownActive) {
const interval = setInterval(() => {
dispatch(updateCountdown());
// console.log(`⏳ Redux Countdown: ${countdown} Sekunden`);
if (countdown <= 2) {
if (getDebugLog()) {
console.log("🚀 Kontextmenü wird wegen Countdown < 2 geschlossen.");
}
dispatch(closePolylineContextMenu());
if (window.map?.contextmenu) {
window.map.contextmenu.hide();
}
clearInterval(interval);
}
}, 1000);
return () => {
clearInterval(interval);
};
}
}, [isPolylineContextMenuOpen, countdown, countdownActive, dispatch, window.map]);
//----------------------------------
// **Fehlerbehandlung für `contextmenu`**
// damit den Fehler mit contextmenu nicht angezeigt wird und überspringt wird und die Seite neu geladen wird
useEffect(() => {
let timeoutId;
window.onerror = function (message, source, lineno, colno, error) {
if (message.includes("Cannot read properties of null (reading 'contextmenu')")) {
console.warn("⚠️ Fehler mit `contextmenu` erkannt Neuladen der Seite.");
timeoutId = setTimeout(() => {
window.location.reload();
}, 0);
return true; // Fehler unterdrücken
}
};
return () => {
clearTimeout(timeoutId);
window.onerror = null; // Aufräumen beim Unmount
};
}, []);
//------------------------------------------------
useEffect(() => {
if (poiTypStatus === "succeeded" && Array.isArray(poiTypData)) {
const map = new Map();
(Array.isArray(poiTypData) ? poiTypData : []).forEach(item =>
map.set(item.idPoiTyp, item.name)
);
setPoiTypMap(map);
}
}, [poiTypData, poiTypStatus]);
//------------------------------------------------
useEffect(() => {
if (poiIconsStatus === "succeeded") {
setPoiData(poiIconsData);
}
}, [poiIconsData, poiIconsStatus]);
//-----------------------------------------------------------------
//----------------------------------------------
useEffect(() => {
if (process.env.NODE_ENV === "development") {
console.log("🚧 Development Mode aktiviert Mock-Daten werden verwendet!");
} else {
console.log("Production Mode aktiviert");
}
}, []);
//-------------------------------------------
useEffect(() => {
return () => {
cleanupMarkers(markers, oms);
};
}, []);
//--------------------------------------------
// Überwacht den Speicherverbrauch und lädt die Seite neu, wenn er zu hoch ist, 8GB ist das Limit dann lädt die Seite neu
useEffect(() => {
const interval = monitorHeapAndReload(8, 10000);
return () => clearInterval(interval); // wichtig: aufräumen!
}, []);
//--------------------------------------------
useEffect(() => {
const interval = monitorHeapWithRedux(10000); // alle 10s
return () => clearInterval(interval);
}, []);
//--------------------------------------------
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const m = params.get("m");
const u = params.get("u");
const socket = io(process.env.NEXT_PUBLIC_SOCKET_URL, {
query: { m, u },
});
socket.on("connect", () => {
console.log("🔗 WebSocket verbunden ");
});
socket.on("GisLinesStatusUpdated", data => {
dispatch(fetchGisLinesStatusThunk(data));
});
socket.on("GisStationsMeasurementsUpdated", data => {
dispatch(fetchGisStationsMeasurementsThunk(data));
});
socket.on("GisStationsStaticDistrictUpdated", () => {
console.log("🛰 WebSocket-Trigger empfangen → Thunk wird erneut ausgeführt");
dispatch(fetchGisStationsStaticDistrictThunk());
console.log("🛰 WebSocket-Trigger empfangen → Trigger wird gesetzt");
setTriggerUpdate(prev => !prev); // Trigger toggeln
});
socket.on("GisStationsStatusDistrictUpdated", data => {
dispatch(fetchGisStationsStatusDistrictThunk(data));
});
socket.on("GisSystemStaticUpdated", data => {
dispatch(fetchGisSystemStaticThunk(data));
});
return () => {
socket.disconnect();
};
}, [dispatch]);
//--------------------------------------------
useEffect(() => {
console.log("📦 Neue Daten empfangen:", GisStationsStaticDistrict);
}, [GisStationsStaticDistrict]);
const { Points = [] } = useSelector(selectGisStationsStaticDistrict);
useEffect(() => {}, [triggerUpdate]);
//--------------------------------------------------------------------------------
useEffect(() => {
console.log("📊 GisSystemStatic:", GisSystemStatic);
}, [GisSystemStatic]);
useEffect(() => {
if (Array.isArray(GisSystemStatic)) {
(Array.isArray(GisSystemStatic) ? GisSystemStatic : []).forEach(system => {
const key = `system-${system.IdSystem}`;
if (!(key in mapLayersVisibility)) {
dispatch(setLayerVisibility({ key, value: true })); // Sichtbarkeit aktivieren
}
});
}
}, [GisSystemStatic, mapLayersVisibility, dispatch]);
//---------------------------------------------
//--------------------------------------------
// Expand handler (same behavior as MapLayersControlPanel expand icon)
const handleExpandClick = () => {
dispatch(setSelectedArea("Station wählen"));
dispatch(incrementZoomTrigger());
};
// Toggle edit mode (same logic as EditModeToggle component)
const hasEditRight = Array.isArray(userRights)
? userRights.includes?.(56) || userRights.some?.(r => r?.IdRight === 56)
: false;
const toggleEditMode = () => {
if (!hasEditRight) return;
const next = !editMode;
setEditMode(next);
try {
localStorage.setItem("editMode", String(next));
} catch (_) {}
if (typeof window !== "undefined") {
window.location.reload();
}
};
//--------------------------------------------
return (
<>
{/* Zeigt das POI-Modal, wenn `showPoiModal` true ist */}
{showPoiModal && (
<AddPOIModal latlng={popupCoordinates} onClose={() => setShowPoiModal(false)} />
)}
<ToastContainer />
<div>
{showPoiUpdateModal && (
<PoiUpdateModal
onClose={() => setShowPoiUpdateModal(false)}
poiData={currentPoiData}
onSubmit={() => {}}
latlng={popupCoordinates}
/>
)}
</div>
<div>
{showPopup && (
<div
className="fixed inset-0 bg-black bg-opacity-10 flex justify-center items-center z-[1000]"
onClick={closePopup}
>
<div
className="relative bg-white p-6 rounded-lg shadow-lg"
onClick={e => e.stopPropagation()}
>
<button
onClick={closePopup}
className="absolute top-0 right-0 mt-2 mr-2 p-1 text-gray-700 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-600"
aria-label="Close"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
)}
</div>
{GisStationsStaticDistrict &&
GisStationsStaticDistrict.Points?.length > 0 &&
showLayersPanel &&
!showAreaDropdown && (
<MapLayersControlPanel
className="z-50"
handlePolylineCheckboxChange={handlePolylineCheckboxChange}
/>
)}
{showCoordinateInput && <CoordinateInput onCoordinatesSubmit={handleCoordinatesSubmit} />}
<div id="map" ref={mapRef} className="z-0" style={{ height: "100vh", width: "100vw" }}></div>
{/* Top-right controls: layers, info, expand, edit, and base map stack */}
<div className="absolute top-3 right-3 z-50 pointer-events-auto flex items-center gap-2">
{/* Alarm-Icon - nur anzeigen wenn Alarm aktiv */}
{hasActiveAlarm && (
<button
onClick={() => {}}
aria-label="Alarm aktiv"
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title="Alarm aktiv"
>
<AlarmIcon className="h-8 w-8 animate-pulse text-red-500" />
</button>
)}
{/* Marker-Icon (line-md) */}
<button
onClick={() => setOverlay(prev => (prev === "area" ? null : "area"))}
aria-label="Marker"
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title="Marker"
>
<MapMarkerIcon className="h-8 w-8" />
</button>
{/*Lupe: Koordinatensuche ein-/ausblenden */}
<button
onClick={() => setOverlay(prev => (prev === "coord" ? null : "coord"))}
aria-label={
showCoordinateInput ? "Koordinatensuche ausblenden" : "Koordinatensuche einblenden"
}
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title={
showCoordinateInput ? "Koordinatensuche ausblenden" : "Koordinatensuche einblenden"
}
>
<SearchIcon className="h-8 w-8" />
</button>
<button
onClick={toggleEditMode}
aria-label={editMode ? "Bearbeitungsmodus deaktivieren" : "Bearbeitungsmodus aktivieren"}
className={`rounded-full shadow p-1 ${
hasEditRight
? "bg-white/90 hover:bg-white"
: "bg-white/60 cursor-not-allowed opacity-50"
}`}
title={
hasEditRight
? editMode
? "Bearbeitungsmodus deaktivieren"
: "Bearbeitungsmodus aktivieren"
: "Keine Bearbeitungsrechte"
}
disabled={!hasEditRight}
>
{editMode ? <EditOffIcon className="h-8 w-8" /> : <EditIcon className="h-8 w-8" />}
</button>
{/* Expand: Karte auf Standardansicht */}
<button
onClick={handleExpandClick}
aria-label="Karte auf Standardansicht"
className="rounded-full bg-white/90 hover:bg-white shadow p-1 "
title="Karte auf Standardansicht"
>
<ExpandIcon className="h-8 w-8" />
</button>
{/* Lupe: Koordinaten-Suche ein-/ausblenden */}
<button
onClick={() => setOverlay(prev => (prev === "layers" ? null : "layers"))}
aria-label={showLayersPanel ? "Layer-Panel ausblenden" : "Layer-Panel einblenden"}
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title={showLayersPanel ? "Layer-Panel ausblenden" : "Layer-Panel einblenden"}
>
<MenuIcon className="h-8 w-8" />
</button>
<button
onClick={() => setOverlay(prev => (prev === "info" ? null : "info"))}
aria-label={showAppInfoCard ? "Info ausblenden" : "Info einblenden"}
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title={showAppInfoCard ? "Info ausblenden" : "Info einblenden"}
>
<InfoIcon
className="h-8 w-8 pr-1"
title={showAppInfoCard ? "Info ausblenden" : "Info einblenden"}
/>
</button>
</div>
{/* Marker/AreaDropdown Panel außerhalb der Button-Leiste platzieren, damit die Position mit den anderen Panels identisch ist */}
{overlay === "area" && <AreaDropdown onClose={() => setOverlay(null)} />}
{/* BaseMapPanel entfernt */}
<CoordinatePopup isOpen={isPopupOpen} coordinates={currentCoordinates} onClose={closePopup} />
{showAppInfoCard && (
<div className="absolute top-16 right-3 w-72 p-4 bg-white rounded-lg shadow-md z-50">
<div className="flex justify-between items-center">
<div>
<span className="text-black text-lg font-semibold"> TALAS.Map </span>
<br />
<span className="text-black text-lg">Version {appVersion}</span>
</div>
<div>
<button onClick={openVersionInfoModal}>
<InfoIcon className="h-8 w-8 pr-1" title="Weitere Infos" />
</button>
</div>
</div>
</div>
)}
<VersionInfoModal
showVersionInfoModal={showVersionInfoModal}
closeVersionInfoModal={closeVersionInfoModal}
APP_VERSION={appVersion}
/>
</>
);
};
export default MapComponent;