Compare commits
88 Commits
feat/docs
...
4a42c428f0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a42c428f0 | ||
|
|
1d3d04d49c | ||
|
|
dd9980409c | ||
|
|
ea6d71a4f5 | ||
|
|
13ca1cece0 | ||
|
|
f22bb4b232 | ||
|
|
bfd091b1b1 | ||
|
|
81b6379895 | ||
|
|
42ca88d27e | ||
|
|
fdb70d892c | ||
|
|
73e9c63e36 | ||
|
|
e520207526 | ||
|
|
2e5acf9327 | ||
|
|
cdfdd3d6cf | ||
|
|
5b86d5293b | ||
|
|
31c770f778 | ||
|
|
051dd4c306 | ||
|
|
995f084e15 | ||
|
|
eaacec71da | ||
|
|
6bc2e16657 | ||
|
|
1208024f76 | ||
|
|
369f29a769 | ||
|
|
d166b2468d | ||
|
|
59c8680c23 | ||
|
|
1a046f8212 | ||
|
|
e35216daf5 | ||
|
|
91ad47166f | ||
|
|
3a9b436352 | ||
|
|
7b881e80c2 | ||
|
|
cc19a0a466 | ||
|
|
f200d0bb20 | ||
|
|
75a0ab000f | ||
|
|
4d2a94ffea | ||
|
|
239ad82e46 | ||
|
|
a2d3338624 | ||
|
|
598acb8441 | ||
|
|
f8512c485e | ||
|
|
fff2754b14 | ||
|
|
61ed542ea4 | ||
|
|
4befddd440 | ||
|
|
5b7868145c | ||
|
|
0a3c4c208f | ||
|
|
8cf520bb2c | ||
|
|
3896381a8f | ||
|
|
a013c07394 | ||
|
|
f2a322a91b | ||
|
|
bf7b62d110 | ||
|
|
c44a755077 | ||
|
|
aea439f135 | ||
|
|
c6871692aa | ||
|
|
e7192a7623 | ||
|
|
f11f64d4d7 | ||
|
|
da21cba186 | ||
|
|
d179c152c0 | ||
|
|
2da79c9318 | ||
|
|
2066cbb9e8 | ||
|
|
c8a14ee873 | ||
|
|
44b29469b9 | ||
|
|
ee5319a928 | ||
|
|
d68b17c382 | ||
|
|
22692f8153 | ||
|
|
819fde9605 | ||
|
|
ded0a9d5da | ||
|
|
4161bfa948 | ||
|
|
680c643ea5 | ||
|
|
7c525c11a1 | ||
|
|
9f05a4f63b | ||
|
|
26d869f029 | ||
|
|
0ea8e090d2 | ||
|
|
9a2b438eaf | ||
|
|
bf4fc95b8e | ||
|
|
db147543d9 | ||
|
|
418651a2af | ||
|
|
bf5ee377a4 | ||
|
|
5c06abc85e | ||
|
|
7c89d8dae5 | ||
|
|
85266aa1ed | ||
|
|
854eff668a | ||
|
|
8073c787bc | ||
|
|
4ee6d42a61 | ||
|
|
53c670feba | ||
|
|
d8567a9928 | ||
|
|
6d33be56c0 | ||
|
|
4755cefdd7 | ||
|
|
37aec649ee | ||
|
|
9d7a696f91 | ||
|
|
56a2e305f1 | ||
|
|
e1bfe7496b |
@@ -7,7 +7,6 @@ DB_NAME=talas_v5
|
||||
DB_PORT=3306
|
||||
|
||||
# Public Settings (Client braucht IP/Domain) , Variablen mit dem Präfix "NEXT_PUBLIC" ist in Browser sichtbar
|
||||
NEXT_PUBLIC_DEBUG_LOG=true
|
||||
|
||||
|
||||
|
||||
@@ -20,9 +19,8 @@ NEXT_PUBLIC_USE_MOCKS=true
|
||||
# Ein Unterordner in der dort hinter liegenden Ordnerstruktur (z.B. http://talasserver/talas5/nodemap/api/talas_v5_DB/ usw.)
|
||||
# kann bleiben da der Kunde diesen Unterordner talas:v5_db nicht ändert.
|
||||
#Füge in deiner .env.local Datei die folgende Zeile hinzu wenn du einen Unterordner verwenden möchtest mit entsprechende Bezeichnung.
|
||||
# z.B. http://10.10.0.13/talas5/index.aspx -> NEXT_PUBLIC_BASE_PATH=/talas5
|
||||
# z.B. http://10.10.0.13/xyz/index.aspx -> NEXT_PUBLIC_BASE_PATH=/xyz
|
||||
NEXT_PUBLIC_BASE_PATH=/talas5
|
||||
# Oder leer lassen für direkten Zugriff -> NEXT_PUBLIC_BASE_PATH=
|
||||
# z.B. http://10.10.0.13/talas5/index.aspx -> basePath in config.json auf /talas5 setzen
|
||||
# z.B. http://10.10.0.13/xyz/index.aspx -> basePath in config.json auf /xyz setzen
|
||||
# basePath wird jetzt in public/config.json gepflegt
|
||||
# App-Versionsnummer
|
||||
NEXT_PUBLIC_APP_VERSION=1.1.300
|
||||
NEXT_PUBLIC_APP_VERSION=1.1.388
|
||||
|
||||
@@ -7,7 +7,6 @@ DB_NAME=talas_v5
|
||||
DB_PORT=3306
|
||||
|
||||
# Public Settings (Client braucht IP/Domain) , Variablen mit dem Präfix "NEXT_PUBLIC" ist in Browser sichtbar
|
||||
NEXT_PUBLIC_DEBUG_LOG=false
|
||||
|
||||
|
||||
|
||||
@@ -20,10 +19,9 @@ NEXT_PUBLIC_USE_MOCKS=false
|
||||
# Ein Unterordner in der dort hinter liegenden Ordnerstruktur (z.B. http://talasserver/talas5/nodemap/api/talas_v5_DB/ usw.)
|
||||
# kann bleiben da der Kunde diesen Unterordner talas:v5_db nicht ändert.
|
||||
#Füge in deiner .env.local Datei die folgende Zeile hinzu wenn du einen Unterordner verwenden möchtest mit entsprechende Bezeichnung.
|
||||
# z.B. http://10.10.0.13/talas5/index.aspx -> NEXT_PUBLIC_BASE_PATH=/talas5
|
||||
# z.B. http://10.10.0.13/xyz/index.aspx -> NEXT_PUBLIC_BASE_PATH=/xyz
|
||||
NEXT_PUBLIC_BASE_PATH=/talas5
|
||||
# Oder leer lassen für direkten Zugriff -> NEXT_PUBLIC_BASE_PATH=
|
||||
# z.B. http://10.10.0.13/talas5/index.aspx -> basePath in config.json auf /talas5 setzen
|
||||
# z.B. http://10.10.0.13/xyz/index.aspx -> basePath in config.json auf /xyz setzen
|
||||
# basePath wird jetzt in public/config.json gepflegt
|
||||
|
||||
# App-Versionsnummer
|
||||
NEXT_PUBLIC_APP_VERSION=1.1.300
|
||||
NEXT_PUBLIC_APP_VERSION=1.1.388
|
||||
|
||||
22
.gitignore
vendored
@@ -35,3 +35,25 @@ docs.zip
|
||||
/mockData/
|
||||
/__mocks__/
|
||||
/__tests__/
|
||||
|
||||
# --- Playwright artifacts & test selection ---
|
||||
# Ignore Playwright output folders nested under playwright/
|
||||
/playwright/test-results/
|
||||
/playwright/playwright-report/
|
||||
/playwright/.last-run.json
|
||||
# If you ever enable these paths, keep them under playwright/ and ignore them
|
||||
/playwright/traces/
|
||||
/playwright/screenshots/
|
||||
/playwright/videos/
|
||||
# Ignore JUnit report artifacts under playwright/ (currently unused)
|
||||
/playwright/reports/junit/
|
||||
|
||||
# Track only spec files under playwright/tests; ignore other files in that folder
|
||||
/playwright/tests/**
|
||||
!/playwright/tests/**/*.spec.js
|
||||
!/playwright/tests/**/*.spec.ts
|
||||
|
||||
# Ignore Playwright cache if present
|
||||
/playwright/.cache/
|
||||
# playwright reports
|
||||
/playwright/reports/
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
echo "🔄 Version wird automatisch erhöht (bumpVersion.js)..."
|
||||
|
||||
# Version automatisch erhöhen
|
||||
|
||||
@@ -250,7 +250,7 @@ das Objekt selbst
|
||||
### ♻️ Refactor
|
||||
|
||||
- Alle hartkodierten `/talas5/`-Pfadangaben entfernt
|
||||
- Dynamischer `basePath` eingeführt über `.env.local → NEXT_PUBLIC_BASE_PATH`
|
||||
- Dynamischer `basePath` eingeführt über `public/config.json → basePath`
|
||||
- Unterstützt jetzt auch den Betrieb ohne Unterverzeichnis
|
||||
|
||||
### 🧠 Architektur
|
||||
|
||||
28
README.md
@@ -48,6 +48,30 @@ User-ID.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Kartenquellen-Konfiguration (public/config.json)
|
||||
|
||||
Die Datei `public/config.json` steuert, welche Kartenquelle (z.B. OSM oder lokale Tiles) für die
|
||||
Leaflet-Karte verwendet wird.
|
||||
|
||||
**Beispiel:**
|
||||
|
||||
```json
|
||||
{
|
||||
"//info": "tileSources: 'local' für offline, 'osm' für online",
|
||||
"tileSources": {
|
||||
"local": "http://localhost/talas5/TileMap/mapTiles/{z}/{x}/{y}.png",
|
||||
"osm": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
},
|
||||
"active": "osm"
|
||||
}
|
||||
```
|
||||
|
||||
- Mit `active` kann zwischen Online- und Offline-Karten umgeschaltet werden.
|
||||
- Die Datei wird beim Start der App automatisch geladen.
|
||||
- Für Offline-Betrieb muss das lokale Kartenmaterial vorhanden sein (siehe Installationsanleitung).
|
||||
|
||||
---
|
||||
|
||||
## 🧰 Erstinstallation auf Server
|
||||
|
||||
### Voraussetzungen
|
||||
@@ -61,8 +85,8 @@ User-ID.
|
||||
(Server-IP mit Port 3000)
|
||||
- Browser: Chrome ab Version 125.0.6420.142 empfohlen
|
||||
- Karten Material vorhanden in: `C:\inetpub\wwwroot\talas5\TileMap\mapTiles`
|
||||

|
||||
Falls nicht vorhanden hier downloaden: http://10.10.0.28/produkte/TALAS.map/mapTiles.zip
|
||||
 Falls nicht vorhanden hier downloaden:
|
||||
http://10.10.0.28/produkte/TALAS.map/mapTiles.zip
|
||||
|
||||
---
|
||||
|
||||
|
||||
6
Start-Dev.ps1
Normal file
@@ -0,0 +1,6 @@
|
||||
# Navigiere zum Verzeichnis deines Projekts
|
||||
cd 'C:\inetpub\wwwroot\talas5\nodeMap'
|
||||
|
||||
# F<>hre den npm Befehl aus
|
||||
npm start
|
||||
|
||||
1
StartNodeApp.bat
Normal file
@@ -0,0 +1 @@
|
||||
PowerShell -ExecutionPolicy Bypass -File "C:\inetpub\wwwroot\talas5\nodeMap\Start-Dev.ps1"
|
||||
@@ -1,16 +0,0 @@
|
||||
describe("setupPOIs Icon-Mapping intern", () => {
|
||||
it("ordnet korrektes Icon anhand idPoi zu", () => {
|
||||
const mockPoiData = [{ idPoi: 7, path: "poi-marker-icon-2.png" }];
|
||||
const iconMap = new Map();
|
||||
mockPoiData.forEach((item) => iconMap.set(item.idPoi, item.path));
|
||||
const result = iconMap.get(7);
|
||||
expect(result).toBe("poi-marker-icon-2.png");
|
||||
});
|
||||
|
||||
it("gibt undefined zurück wenn idPoi nicht existiert", () => {
|
||||
const iconMap = new Map();
|
||||
iconMap.set(1, "icon-1.png");
|
||||
const result = iconMap.get(99);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getDebugLog } from "@/utils/configUtils.js";
|
||||
// /hooks/layers/useAreaMarkersLayer.js
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import L from "leaflet";
|
||||
@@ -22,7 +23,13 @@ const useAreaMarkersLayer = (map, oms, apiUrl, onUpdateSuccess) => {
|
||||
const updateMarkersVisibility = () => {
|
||||
if (!map || areaMarkers.length === 0) return;
|
||||
|
||||
const mapLayersVisibility = JSON.parse(localStorage.getItem("mapLayersVisibility")) || {};
|
||||
// Kartenspezifischer localStorage-Key verwenden
|
||||
const mapId = localStorage.getItem("currentMapId");
|
||||
const userId = localStorage.getItem("currentUserId");
|
||||
const mapStorageKey =
|
||||
mapId && userId ? `mapLayersVisibility_m${mapId}_u${userId}` : "mapLayersVisibility";
|
||||
|
||||
const mapLayersVisibility = JSON.parse(localStorage.getItem(mapStorageKey)) || {};
|
||||
const areAllLayersInvisible = Object.values(mapLayersVisibility).every(v => !v);
|
||||
|
||||
if (areAllLayersInvisible === prevVisibility.current) return;
|
||||
@@ -42,7 +49,8 @@ const useAreaMarkersLayer = (map, oms, apiUrl, onUpdateSuccess) => {
|
||||
updateMarkersVisibility();
|
||||
|
||||
const handleStorageChange = event => {
|
||||
if (event.key === "mapLayersVisibility") {
|
||||
// Überwache sowohl den alten als auch kartenspezifische Keys
|
||||
if (event.key === "mapLayersVisibility" || event.key?.startsWith("mapLayersVisibility_")) {
|
||||
updateMarkersVisibility();
|
||||
}
|
||||
};
|
||||
@@ -64,10 +72,11 @@ const useAreaMarkersLayer = (map, oms, apiUrl, onUpdateSuccess) => {
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const editMode = localStorage.getItem("editMode") === "true";
|
||||
const markers = data.map(item => {
|
||||
const marker = L.marker([item.x, item.y], {
|
||||
icon: customIcon,
|
||||
draggable: true,
|
||||
draggable: editMode,
|
||||
customType: "areaMarker",
|
||||
});
|
||||
|
||||
@@ -81,6 +90,7 @@ const useAreaMarkersLayer = (map, oms, apiUrl, onUpdateSuccess) => {
|
||||
}
|
||||
);
|
||||
|
||||
if (editMode) {
|
||||
marker.on("dragend", async e => {
|
||||
const { lat, lng } = e.target.getLatLng();
|
||||
try {
|
||||
@@ -91,7 +101,7 @@ const useAreaMarkersLayer = (map, oms, apiUrl, onUpdateSuccess) => {
|
||||
newCoords: { x: lat, y: lng },
|
||||
})
|
||||
).unwrap();
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("✔️ Koordinaten erfolgreich aktualisiert:", { lat, lng });
|
||||
}
|
||||
onUpdateSuccess?.(); // optionaler Callback
|
||||
@@ -99,6 +109,7 @@ const useAreaMarkersLayer = (map, oms, apiUrl, onUpdateSuccess) => {
|
||||
console.error("❌ Fehler beim Aktualisieren der Koordinaten:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return marker;
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getDebugLog } from "@/utils/configUtils.js";
|
||||
// components/contextmenu/useMapContextMenu.js
|
||||
import { toast } from "react-toastify";
|
||||
import { zoomIn, zoomOut, centerHere } from "../../utils/zoomAndCenterUtils";
|
||||
@@ -71,7 +72,7 @@ const addItemsToMapContextMenu = (
|
||||
if (!menuItemAdded && map && map.contextmenu) {
|
||||
const editMode = localStorage.getItem("editMode") === "true";
|
||||
if (editMode) {
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("editMode localStorage:", localStorage.getItem("editMode"));
|
||||
}
|
||||
|
||||
|
||||
@@ -30,30 +30,30 @@ const useDynamicDeviceLayers = (map, GisSystemStatic, mapLayersVisibility, prior
|
||||
if (!map || GisSystemStatic.length === 0) return;
|
||||
|
||||
GisSystemStatic.forEach(({ Name, IdSystem }) => {
|
||||
const key = `system-${IdSystem}`; // Einheitlicher Key
|
||||
|
||||
if (!layerRefs.current[key]) {
|
||||
layerRefs.current[key] = new L.LayerGroup().addTo(map);
|
||||
const key = `system-${IdSystem}`;
|
||||
// LayerGroup immer komplett neu erstellen, um doppelte Marker zu verhindern
|
||||
if (layerRefs.current[key]) {
|
||||
if (map.hasLayer(layerRefs.current[key])) {
|
||||
map.removeLayer(layerRefs.current[key]);
|
||||
}
|
||||
layerRefs.current[key].clearLayers();
|
||||
delete layerRefs.current[key];
|
||||
}
|
||||
layerRefs.current[key] = new L.LayerGroup();
|
||||
layerRefs.current[key].addTo(map);
|
||||
|
||||
createAndSetDevices(
|
||||
IdSystem,
|
||||
newMarkers => {
|
||||
const oldMarkers = markerStates[key];
|
||||
|
||||
// Entferne alte Marker aus Karte und OMS
|
||||
if (oldMarkers && Array.isArray(oldMarkers)) {
|
||||
oldMarkers.forEach(marker => {
|
||||
if (map.hasLayer(marker)) {
|
||||
map.removeLayer(marker);
|
||||
}
|
||||
if (oms) {
|
||||
oms.removeMarker(marker);
|
||||
}
|
||||
// Füge neue Marker der LayerGroup hinzu (nur Geräte-Marker)
|
||||
if (layerRefs.current[key]) {
|
||||
layerRefs.current[key].clearLayers();
|
||||
// Nur eindeutige Marker hinzufügen
|
||||
const uniqueMarkers = Array.isArray(newMarkers) ? Array.from(new Set(newMarkers)) : [];
|
||||
uniqueMarkers.forEach(marker => {
|
||||
marker.addTo(layerRefs.current[key]);
|
||||
});
|
||||
}
|
||||
|
||||
// Neue Marker setzen
|
||||
setMarkerStates(prev => ({ ...prev, [key]: newMarkers }));
|
||||
},
|
||||
GisSystemStatic,
|
||||
@@ -69,20 +69,21 @@ const useDynamicDeviceLayers = (map, GisSystemStatic, mapLayersVisibility, prior
|
||||
if (!map) return;
|
||||
const editMode = localStorage.getItem("editMode") === "true";
|
||||
|
||||
Object.entries(markerStates).forEach(([key, markers]) => {
|
||||
const isVisible = mapLayersVisibility[key];
|
||||
markers.forEach(marker => {
|
||||
const hasLayer = map.hasLayer(marker);
|
||||
if (editMode || !isVisible) {
|
||||
if (hasLayer) map.removeLayer(marker);
|
||||
} else {
|
||||
if (!hasLayer) marker.addTo(map);
|
||||
Object.entries(layerRefs.current).forEach(([key, layerGroup]) => {
|
||||
const isVisible = mapLayersVisibility[key] ?? true;
|
||||
if (editMode || isVisible === false) {
|
||||
if (map.hasLayer(layerGroup)) {
|
||||
map.removeLayer(layerGroup);
|
||||
}
|
||||
} else if (isVisible === true) {
|
||||
if (!map.hasLayer(layerGroup)) {
|
||||
layerGroup.addTo(map);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Overlapping-Check bleibt wie gehabt
|
||||
const allMarkers = Object.values(markerStates).filter(Array.isArray).flat();
|
||||
|
||||
checkOverlappingMarkers(map, allMarkers, plusRoundIcon);
|
||||
}, [map, markerStates, mapLayersVisibility]);
|
||||
|
||||
|
||||
0
components/hooks/useStationCache.js
Normal file
18
components/icons/material-symbols/AlarmIcon.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const AlarmIcon = ({ className = "h-8 w-8" }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="red"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="rgb(0, 174, 239)"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 6.9L3.87 4.78l1.41-1.41L7.4 5.5zM13 1v3h-2V1zm7.13 3.78L18 6.9l-1.4-1.4l2.12-2.13zM4.5 10.5v2h-3v-2zm15 0h3v2h-3zM6 20h12a2 2 0 0 1 2 2H4a2 2 0 0 1 2-2m6-15a6 6 0 0 1 6 6v8H6v-8a6 6 0 0 1 6-6"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default AlarmIcon;
|
||||
18
components/icons/material-symbols/EditIcon.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const EditIcon = ({ className = "h-8 w-8" }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="rgb(0, 174, 239)"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default EditIcon;
|
||||
18
components/icons/material-symbols/EditOffIcon.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const EditOffIcon = ({ className = "h-8 w-8" }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="rgb(0, 174, 239)"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 21l-3-3m-12.728-.364A9 9 0 015.636 5.636m0 0L3 3l3 3m9.364 9.364L18 21M5.636 5.636L3 3"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default EditOffIcon;
|
||||
18
components/icons/material-symbols/ExpandIcon.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const ExpandIcon = ({ className = "h-8 w-8" }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="rgb(0, 174, 239)"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default ExpandIcon;
|
||||
18
components/icons/material-symbols/InfoIcon.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const InfoIcon = ({ className = "h-8 w-8" }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="rgb(0, 174, 239)"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default InfoIcon;
|
||||
19
components/icons/material-symbols/MapMarkerIcon.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const MapMarkerIcon = ({ className = "h-8 w-8" }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="rgb(0, 174, 239)"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="rgb(0, 174, 239)"
|
||||
className={className}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25s-7.5-4.108-7.5-11.25a7.5 7.5 0 1115 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default MapMarkerIcon;
|
||||
18
components/icons/material-symbols/MenuIcon.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const MenuIcon = ({ className = "h-8 w-8" }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="rgb(0, 174, 239)"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default MenuIcon;
|
||||
14
components/icons/material-symbols/MinusIcon.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const MinusIcon = ({ className = "h-8 w-8" }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
className={className}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 12h14" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default MinusIcon;
|
||||
14
components/icons/material-symbols/PlusIcon.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const PlusIcon = ({ className = "h-8 w-8" }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
className={className}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default PlusIcon;
|
||||
18
components/icons/material-symbols/SearchIcon.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const SearchIcon = ({ className = "h-8 w-8" }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="rgb(0, 174, 239)"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default SearchIcon;
|
||||
@@ -6,10 +6,23 @@ import "leaflet-contextmenu/dist/leaflet.contextmenu.css";
|
||||
import "leaflet-contextmenu";
|
||||
import "leaflet.smooth_marker_bouncing";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import { InformationCircleIcon } from "@heroicons/react/20/solid";
|
||||
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 PlusIcon from "@/components/icons/material-symbols/PlusIcon";
|
||||
import MinusIcon from "@/components/icons/material-symbols/MinusIcon";
|
||||
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";
|
||||
@@ -21,8 +34,10 @@ 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 AlarmIndicator from "@/components/uiWidgets/AlarmIndicator";
|
||||
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";
|
||||
@@ -35,7 +50,9 @@ import { useSelector, useDispatch } from "react-redux";
|
||||
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 } from "@/redux/slices/mapLayersSlice";
|
||||
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";
|
||||
@@ -54,6 +71,7 @@ import {
|
||||
import {
|
||||
selectPolylineVisible,
|
||||
setPolylineVisible,
|
||||
initializePolylineFromLocalStorageThunk,
|
||||
} from "@/redux/slices/database/polylines/polylineLayerVisibleSlice.js";
|
||||
import { selectGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice.js";
|
||||
import {
|
||||
@@ -84,6 +102,7 @@ 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 }) => {
|
||||
//-------------------------------
|
||||
@@ -96,6 +115,14 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
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);
|
||||
@@ -106,8 +133,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
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 GisSystemStatic = useSelector(selectGisSystemStatic);
|
||||
// entfernt, da weiter unten dynamisch und mit Fallback deklariert
|
||||
const gisSystemStaticStatus = useSelector(state => state.gisSystemStatic.status);
|
||||
const polylineEventsDisabled = useSelector(state => state.polylineEventsDisabled.disabled);
|
||||
const mapLayersVisibility = useSelector(selectMapLayersState) || {};
|
||||
@@ -117,6 +143,38 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
const { data: gisLinesStatusData, status: statusGisLinesStatus } = useSelector(
|
||||
selectGisLinesStatusFromWebservice
|
||||
);
|
||||
|
||||
// Alarm Status und Link dynamisch aus GisStationsStaticDistrict
|
||||
const gisStationsStatusDistrict = useSelector(state => state.gisStationsStatusDistrict.data);
|
||||
const GisStationsStaticDistrict = useSelector(selectGisStationsStaticDistrict) || {};
|
||||
const pointsArr = GisStationsStaticDistrict.Points || [];
|
||||
let hasActiveAlarm = false;
|
||||
let alarmLink = "";
|
||||
let alarmText = "";
|
||||
let alarmIdLD = null;
|
||||
if (Array.isArray(gisStationsStatusDistrict)) {
|
||||
const alarmObj = gisStationsStatusDistrict.find(item => item?.Alarm === 1);
|
||||
hasActiveAlarm = !!alarmObj;
|
||||
alarmIdLD = alarmObj?.IdLD;
|
||||
alarmText = alarmObj?.Me || "Alarm aktiv";
|
||||
} else if (gisStationsStatusDistrict?.Statis) {
|
||||
const alarmObj = gisStationsStatusDistrict.Statis.find(item => item?.Alarm === 1);
|
||||
hasActiveAlarm = !!alarmObj;
|
||||
alarmIdLD = alarmObj?.IdLD;
|
||||
alarmText = alarmObj?.Me || "Alarm aktiv";
|
||||
}
|
||||
if (hasActiveAlarm && alarmIdLD) {
|
||||
const staticObj = pointsArr.find(p => p.IdLD === alarmIdLD);
|
||||
if (staticObj && staticObj.Link) {
|
||||
// Link kann relativ sein, ggf. mit Host ergänzen
|
||||
const isAbsolute =
|
||||
staticObj.Link.startsWith("http://") || staticObj.Link.startsWith("https://");
|
||||
alarmLink = isAbsolute
|
||||
? staticObj.Link
|
||||
: // : `${window.location.origin}/talas5/devices/${staticObj.Link}`;
|
||||
`http://10.10.0.13/talas5/devices/${staticObj.Link}`; // nur zum Testen
|
||||
}
|
||||
}
|
||||
const poiIconsData = useSelector(selectPoiIconsData);
|
||||
const poiIconsStatus = useSelector(selectPoiIconsStatus);
|
||||
const poiTypData = useSelector(selectPoiTypData);
|
||||
@@ -135,10 +193,75 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
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(
|
||||
@@ -170,6 +293,14 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
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);
|
||||
@@ -191,6 +322,32 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 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 => {
|
||||
@@ -200,6 +357,16 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
}
|
||||
};
|
||||
//-----------------------------Map Initialisierung----------------
|
||||
// Default map options for Leaflet
|
||||
const mapOptions = {
|
||||
center: currentCenter,
|
||||
zoom: currentZoom,
|
||||
zoomControl: true,
|
||||
contextmenu: true,
|
||||
contextmenuWidth: 180,
|
||||
contextmenuItems: [],
|
||||
};
|
||||
|
||||
useInitializeMap(
|
||||
map,
|
||||
mapRef,
|
||||
@@ -208,10 +375,85 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
setMenuItemAdded,
|
||||
addItemsToMapContextMenu,
|
||||
hasRights,
|
||||
value => dispatch(setDisabled(value))
|
||||
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 => ({
|
||||
@@ -222,6 +464,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
setLinePositions(transformed);
|
||||
}
|
||||
}, [linesData]);
|
||||
|
||||
//--------------------------------------------
|
||||
useEffect(() => {
|
||||
dispatch(fetchPoiIconsDataThunk());
|
||||
@@ -309,16 +552,34 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
//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
|
||||
|
||||
markers.forEach(marker => {
|
||||
(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
|
||||
const { markers: newMarkers, polylines: newPolylines } = setupPolylines(
|
||||
// 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,
|
||||
@@ -327,10 +588,10 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
tempMarker,
|
||||
currentZoom,
|
||||
currentCenter,
|
||||
polylineVisible // kommt aus Redux
|
||||
polylineVisible
|
||||
);
|
||||
|
||||
newPolylines.forEach((polyline, index) => {
|
||||
(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";
|
||||
@@ -375,6 +636,22 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
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,
|
||||
@@ -384,6 +661,8 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
newCoords,
|
||||
tempMarker,
|
||||
polylineVisible,
|
||||
isTalasAllowed,
|
||||
poiLayerVisible,
|
||||
]);
|
||||
|
||||
//--------------------------------------------
|
||||
@@ -391,7 +670,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
//Test in useEffect
|
||||
useEffect(() => {
|
||||
if (map) {
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("🗺️ Map-Einstellungen werden wiederhergestellt...");
|
||||
}
|
||||
restoreMapSettings(map);
|
||||
@@ -400,7 +679,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
//--------------------------------------------
|
||||
useEffect(() => {
|
||||
if (map) {
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("map in MapComponent: ", map);
|
||||
}
|
||||
const handleMapMoveEnd = event => {
|
||||
@@ -433,7 +712,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
const station = points.find(s => s.Area_Name === selectedArea);
|
||||
|
||||
if (station) {
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("📌 Gefundene Station:", station);
|
||||
}
|
||||
map.flyTo([station.X, station.Y], 14);
|
||||
@@ -479,7 +758,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
//--------------------------------------------
|
||||
useEffect(() => {
|
||||
if (map) {
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("6- Karteninstanz (map) wurde jetzt erfolgreich initialisiert");
|
||||
}
|
||||
}
|
||||
@@ -492,7 +771,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
map.whenReady(() => {
|
||||
timeoutId = setTimeout(() => {
|
||||
if (map.contextmenu) {
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("Contextmenu ist vorhanden");
|
||||
}
|
||||
} else {
|
||||
@@ -523,7 +802,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
const handleLocationUpdate = async (idLocation, idMap, newCoords) => {
|
||||
try {
|
||||
await dispatch(updateAreaThunk({ idLocation, idMap, newCoords })).unwrap();
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("Koordinaten erfolgreich aktualisiert:", result);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -547,14 +826,14 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
// Entferne alle Marker aus der Karte
|
||||
if (!map) return; // Sicherstellen, dass map existiert
|
||||
|
||||
areaMarkers.forEach(marker => {
|
||||
(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
|
||||
areaMarkers.forEach(marker => {
|
||||
(Array.isArray(areaMarkers) ? areaMarkers : []).forEach(marker => {
|
||||
if (!map.hasLayer(marker)) {
|
||||
marker.addTo(map); // Layer hinzufügen
|
||||
}
|
||||
@@ -615,11 +894,10 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
}, [dispatch]);
|
||||
|
||||
//--------------------------------------------
|
||||
// Beim ersten Client-Render den Wert aus localStorage laden
|
||||
useEffect(() => {
|
||||
const storedPolylineVisible = localStorage.getItem("polylineVisible") === "true";
|
||||
dispatch(setPolylineVisible(storedPolylineVisible));
|
||||
}, [dispatch]);
|
||||
|
||||
// (Initialisierung erfolgt in MapLayersControlPanel)
|
||||
//--------------------------------------------
|
||||
// MapComponent reagiert nicht mehr direkt auf localStorage-Events für polylineVisible
|
||||
//--------------------------------------------
|
||||
useEffect(() => {
|
||||
if (statusStaticDistrict === "idle") {
|
||||
@@ -693,7 +971,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
// console.log(`⏳ Redux Countdown: ${countdown} Sekunden`);
|
||||
|
||||
if (countdown <= 2) {
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("🚀 Kontextmenü wird wegen Countdown < 2 geschlossen.");
|
||||
}
|
||||
dispatch(closePolylineContextMenu());
|
||||
@@ -738,7 +1016,9 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
useEffect(() => {
|
||||
if (poiTypStatus === "succeeded" && Array.isArray(poiTypData)) {
|
||||
const map = new Map();
|
||||
poiTypData.forEach(item => map.set(item.idPoiTyp, item.name));
|
||||
(Array.isArray(poiTypData) ? poiTypData : []).forEach(item =>
|
||||
map.set(item.idPoiTyp, item.name)
|
||||
);
|
||||
setPoiTypMap(map);
|
||||
}
|
||||
}, [poiTypData, poiTypStatus]);
|
||||
@@ -749,30 +1029,6 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
}
|
||||
}, [poiIconsData, poiIconsStatus]);
|
||||
//-----------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
const editMode = localStorage.getItem("editMode") === "true";
|
||||
|
||||
Object.entries(markerStates).forEach(([systemName, markers]) => {
|
||||
const isVisible = mapLayersVisibility[systemName];
|
||||
markers.forEach(marker => {
|
||||
const hasLayer = map.hasLayer(marker);
|
||||
if (editMode || !isVisible) {
|
||||
if (hasLayer) map.removeLayer(marker);
|
||||
} else {
|
||||
if (!hasLayer) marker.addTo(map);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// optional für alle zusammen
|
||||
const allMarkers = Object.values(markerStates)
|
||||
.filter(entry => Array.isArray(entry))
|
||||
.flat();
|
||||
|
||||
checkOverlappingMarkers(map, allMarkers, plusRoundIcon);
|
||||
}, [map, markerStates, mapLayersVisibility]);
|
||||
|
||||
//----------------------------------------------
|
||||
useEffect(() => {
|
||||
@@ -848,7 +1104,45 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
}, [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 (
|
||||
<>
|
||||
@@ -903,15 +1197,122 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{GisStationsStaticDistrict && GisStationsStaticDistrict.Points?.length > 0 && (
|
||||
<MapLayersControlPanel className="z-50" />
|
||||
{GisStationsStaticDistrict &&
|
||||
GisStationsStaticDistrict.Points?.length > 0 &&
|
||||
showLayersPanel &&
|
||||
!showAreaDropdown && (
|
||||
<MapLayersControlPanel
|
||||
className="z-50"
|
||||
handlePolylineCheckboxChange={handlePolylineCheckboxChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CoordinateInput onCoordinatesSubmit={handleCoordinatesSubmit} />
|
||||
{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 und Link vorhanden */}
|
||||
<AlarmIndicator hasAlarm={hasActiveAlarm} alarmLink={alarmLink} alarmText={alarmText} />
|
||||
{/* 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>
|
||||
{/* Custom Zoom Controls bottom-right, styled in littwin-blue to match app icons */}
|
||||
<div className="absolute bottom-8 right-3 z-50 flex flex-col gap-1">
|
||||
<button
|
||||
data-testid="zoom-in"
|
||||
onClick={() => map?.zoomIn?.()}
|
||||
aria-label="Zoom in"
|
||||
className="rounded-md bg-white/90 hover:bg-white shadow-sm p-1"
|
||||
title="Zoom in"
|
||||
>
|
||||
<PlusIcon className="h-5 w-5 text-littwin-blue" />
|
||||
</button>
|
||||
<button
|
||||
data-testid="zoom-out"
|
||||
onClick={() => map?.zoomOut?.()}
|
||||
aria-label="Zoom out"
|
||||
className="rounded-md bg-white/90 hover:bg-white shadow-sm p-1"
|
||||
title="Zoom out"
|
||||
>
|
||||
<MinusIcon className="h-5 w-5 text-littwin-blue" />
|
||||
</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} />
|
||||
|
||||
<div className="absolute bottom-3 left-3 w-72 p-4 bg-white rounded-lg shadow-md z-50">
|
||||
{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>
|
||||
@@ -920,11 +1321,12 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={openVersionInfoModal}>
|
||||
<InformationCircleIcon className="text-blue-900 h-8 w-8 pr-1" title="Weitere Infos" />
|
||||
<InfoIcon className="h-8 w-8 pr-1" title="Weitere Infos" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<VersionInfoModal
|
||||
showVersionInfoModal={showVersionInfoModal}
|
||||
closeVersionInfoModal={closeVersionInfoModal}
|
||||
|
||||
@@ -2,12 +2,58 @@
|
||||
import { useEffect } from "react";
|
||||
import { initializeMap } from "../../../utils/initializeMap";
|
||||
|
||||
const useInitializeMap = (map, mapRef, setMap, setOms, setMenuItemAdded, addItemsToMapContextMenu, hasRights, setPolylineEventsDisabled) => {
|
||||
const useInitializeMap = (
|
||||
map,
|
||||
mapRef,
|
||||
setMap,
|
||||
setOms,
|
||||
setMenuItemAdded,
|
||||
addItemsToMapContextMenu,
|
||||
hasRights,
|
||||
setPolylineEventsDisabled,
|
||||
mapOptions
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (mapRef.current && !map) {
|
||||
initializeMap(mapRef, setMap, setOms, setMenuItemAdded, addItemsToMapContextMenu, hasRights, setPolylineEventsDisabled);
|
||||
let cancelled = false;
|
||||
function tryInit(firstAttempt = true) {
|
||||
if (cancelled) return;
|
||||
// Only try to initialize if mapRef.current is ready and in DOM
|
||||
if (
|
||||
mapRef.current &&
|
||||
mapRef.current instanceof HTMLElement &&
|
||||
document.body.contains(mapRef.current) &&
|
||||
!map &&
|
||||
!mapRef.current._leaflet_id
|
||||
) {
|
||||
try {
|
||||
const result = initializeMap(
|
||||
mapRef.current, // pass DOM node, not ref
|
||||
setMenuItemAdded,
|
||||
addItemsToMapContextMenu,
|
||||
hasRights,
|
||||
setPolylineEventsDisabled,
|
||||
firstAttempt // log error only on first real attempt
|
||||
);
|
||||
if (result && result.map && result.oms) {
|
||||
setMap(result.map);
|
||||
setOms(result.oms);
|
||||
}
|
||||
}, [mapRef, map, hasRights, setPolylineEventsDisabled]);
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Map initialization error:", error);
|
||||
}
|
||||
}
|
||||
} else if (!map && !cancelled) {
|
||||
// If not ready, just retry after a short delay, do not call initializeMap at all
|
||||
setTimeout(() => tryInit(false), 50);
|
||||
}
|
||||
}
|
||||
tryInit(true);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [mapRef, map, hasRights, setPolylineEventsDisabled, mapOptions]);
|
||||
};
|
||||
|
||||
export default useInitializeMap;
|
||||
|
||||
53
components/uiWidgets/AlarmIndicator.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
import AlarmIcon from "@/components/icons/material-symbols/AlarmIcon";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import styles from "./AlarmIndicator.module.css";
|
||||
|
||||
/**
|
||||
* AlarmIndicator zeigt ein Alarm-Icon, das bei Klick den AlarmLink in neuem Tab öffnet.
|
||||
* @param {boolean} hasAlarm - Ob ein Alarm aktiv ist
|
||||
* @param {string} alarmLink - Link zur Alarm-Detailseite
|
||||
* @param {string} [alarmText] - Optionaler Tooltip-Text
|
||||
* @param {string} [animation] - "shake" | "rotate" | "blink" | "pulse" (default: "shake")
|
||||
* @param {number} [pulseDuration] - Animationsdauer in Sekunden (default: 0.5)
|
||||
*/
|
||||
const AlarmIndicator = ({
|
||||
hasAlarm,
|
||||
alarmLink,
|
||||
alarmText,
|
||||
animation = "pulse",
|
||||
pulseDuration = 0.5, // default: 1
|
||||
}) => {
|
||||
if (!hasAlarm || !alarmLink) return null;
|
||||
// Animation-Klasse wählen
|
||||
let animationClass = styles.fastPulse;
|
||||
let style = { animationDuration: `${pulseDuration}s` };
|
||||
if (animation === "shake") {
|
||||
animationClass = styles.shakeAlarm;
|
||||
} else if (animation === "rotate") {
|
||||
animationClass = styles.rotateAlarm;
|
||||
} else if (animation === "blink") {
|
||||
animationClass = styles.blinkAlarm;
|
||||
} else if (animation === "pulse") {
|
||||
animationClass = styles.fastPulse;
|
||||
}
|
||||
return (
|
||||
<Tooltip title={alarmText || "Alarm aktiv"}>
|
||||
<span
|
||||
style={{ cursor: "pointer", color: "red" }}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
window.open(alarmLink, "_blank");
|
||||
}}
|
||||
aria-label="Alarm aktiv"
|
||||
>
|
||||
<AlarmIcon
|
||||
className={`h-14 w-14 mr-6 ${animationClass} text-red-800 bg-red-300`}
|
||||
style={style}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlarmIndicator;
|
||||
62
components/uiWidgets/AlarmIndicator.module.css
Normal file
@@ -0,0 +1,62 @@
|
||||
.fastPulse {
|
||||
animation: fast-pulse 0.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes fast-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.15);
|
||||
}
|
||||
}
|
||||
|
||||
.shakeAlarm {
|
||||
animation: shake-alarm 0.5s infinite cubic-bezier(0.36, 0.07, 0.19, 0.97);
|
||||
}
|
||||
|
||||
@keyframes shake-alarm {
|
||||
10%,
|
||||
90% {
|
||||
transform: translateX(-1px);
|
||||
}
|
||||
20%,
|
||||
80% {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
30%,
|
||||
50%,
|
||||
70% {
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
40%,
|
||||
60% {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
.rotateAlarm {
|
||||
animation: rotate-alarm 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate-alarm {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.blinkAlarm {
|
||||
animation: blink-alarm 0.7s steps(2, start) infinite;
|
||||
}
|
||||
|
||||
@keyframes blink-alarm {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
79
components/uiWidgets/AreaDropdown.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// /components/uiWidgets/AreaDropdown.js
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setSelectedArea } from "@/redux/slices/selectedAreaSlice";
|
||||
import { selectGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice";
|
||||
import { selectGisSystemStatic } from "@/redux/slices/webservice/gisSystemStaticSlice";
|
||||
|
||||
/**
|
||||
* Kleines Dropdown zur Auswahl der Station (Area_Name),
|
||||
* nutzt dieselbe Datenquelle wie das MapLayersControlPanel.
|
||||
*/
|
||||
const AreaDropdown = ({ onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const GisStationsStaticDistrict = useSelector(selectGisStationsStaticDistrict) || {};
|
||||
const GisSystemStatic = useSelector(selectGisSystemStatic) || [];
|
||||
|
||||
// Erlaubte Systeme: Allow === 1 und Map === 1
|
||||
const allowedSystems = useMemo(() => {
|
||||
return new Set(
|
||||
(Array.isArray(GisSystemStatic) ? GisSystemStatic : [])
|
||||
.filter(sys => sys.Allow === 1 && sys.Map === 1)
|
||||
.map(sys => sys.IdSystem)
|
||||
);
|
||||
}, [GisSystemStatic]);
|
||||
|
||||
// Uniqe Areas basierend auf Allowed Systems
|
||||
const areaOptions = useMemo(() => {
|
||||
const points = GisStationsStaticDistrict?.Points || [];
|
||||
const seen = new Set();
|
||||
const filtered = points.filter(p => {
|
||||
if (!p?.Area_Name) return false;
|
||||
if (!allowedSystems.has(p.System)) return false;
|
||||
if (seen.has(p.Area_Name)) return false;
|
||||
seen.add(p.Area_Name);
|
||||
return true;
|
||||
});
|
||||
return filtered.map(p => ({ label: p.Area_Name, value: p.IdLD }));
|
||||
}, [GisStationsStaticDistrict, allowedSystems]);
|
||||
|
||||
const handleChange = e => {
|
||||
const selectedIndex = e.target.options.selectedIndex;
|
||||
const label = e.target.options[selectedIndex].text;
|
||||
dispatch(setSelectedArea(label));
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
// Schließe mit ESC
|
||||
useEffect(() => {
|
||||
const onKey = e => {
|
||||
if (e.key === "Escape") onClose?.();
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<div className="absolute top-16 right-3 w-72 z-50 bg-white rounded-lg shadow-md">
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div className="text-sm font-semibold mb-2">Station wählen</div>
|
||||
<select
|
||||
onChange={handleChange}
|
||||
className="border p-2 rounded w-full"
|
||||
defaultValue="__default__"
|
||||
>
|
||||
<option value="__default__" disabled>
|
||||
Bitte wählen…
|
||||
</option>
|
||||
{areaOptions.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AreaDropdown;
|
||||
@@ -4,7 +4,7 @@ import React, { useState } from "react";
|
||||
const CoordinateInput = ({ onCoordinatesSubmit }) => {
|
||||
const [coordinates, setCoordinates] = useState("");
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
const handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
if (onCoordinatesSubmit) {
|
||||
onCoordinatesSubmit(coordinates);
|
||||
@@ -12,9 +12,21 @@ const CoordinateInput = ({ onCoordinatesSubmit }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="fixed top-5 left-5 z-50 bg-white shadow-lg rounded-lg p-4 w-72">
|
||||
<input type="text" placeholder="Koordinaten eingeben (lat,lng)" value={coordinates} onChange={(e) => setCoordinates(e.target.value)} className="border p-2 rounded w-full mb-2" />
|
||||
<button type="submit" className="bg-blue-500 text-white p-2 rounded w-full hover:bg-blue-600">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="absolute top-16 right-3 z-50 bg-white rounded-lg shadow-md p-4 w-72"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Koordinaten eingeben (lat,lng)"
|
||||
value={coordinates}
|
||||
onChange={e => setCoordinates(e.target.value)}
|
||||
className="border p-2 rounded w-full mb-2"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-littwin-blue text-white p-2 rounded w-full hover:bg-blue-600"
|
||||
>
|
||||
Zu Marker zoomen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -31,7 +31,7 @@ const VersionInfoModal = ({ showVersionInfoModal, closeVersionInfoModal, APP_VER
|
||||
</p>
|
||||
<button
|
||||
onClick={closeVersionInfoModal}
|
||||
className="mt-4 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-700 mx-auto block"
|
||||
className="mt-4 bg-littwin-blue text-white px-4 py-2 rounded mx-auto block"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
|
||||
15
components/uiWidgets/alarm-indicator-fastpulse.css
Normal file
@@ -0,0 +1,15 @@
|
||||
@keyframes fast-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.15);
|
||||
}
|
||||
}
|
||||
|
||||
.fast-pulse {
|
||||
animation: fast-pulse 0.5s infinite;
|
||||
}
|
||||
163
components/uiWidgets/baseMapPanel/BaseMapPanel.js
Normal file
@@ -0,0 +1,163 @@
|
||||
// components/uiWidgets/baseMapPanel/BaseMapPanel.js , aus rechliche Grunde nur OSM, dieses Feature ist optional, aktuell nicht genutzt
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import L from "leaflet";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
// Minimal, safe defaults (no API key required). You can extend via config later.
|
||||
const DEFAULT_BASE_LAYERS = [
|
||||
{
|
||||
id: "osm-standard",
|
||||
name: "Standard",
|
||||
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
minZoom: 0,
|
||||
maxZoom: 19,
|
||||
},
|
||||
{
|
||||
id: "osm-humanitarian",
|
||||
name: "Humanitarian",
|
||||
url: "https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png",
|
||||
attribution: "© OpenStreetMap contributors, Humanitarian OpenStreetMap Team",
|
||||
minZoom: 0,
|
||||
maxZoom: 19,
|
||||
},
|
||||
{
|
||||
id: "cyclosm",
|
||||
name: "CyclOSM",
|
||||
url: "https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png",
|
||||
attribution: "© OpenStreetMap contributors, CyclOSM",
|
||||
minZoom: 0,
|
||||
maxZoom: 20,
|
||||
},
|
||||
{
|
||||
id: "carto-light",
|
||||
name: "Carto Light",
|
||||
url: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
|
||||
attribution: "© OpenStreetMap contributors, © CARTO",
|
||||
subdomains: "abcd",
|
||||
minZoom: 0,
|
||||
maxZoom: 20,
|
||||
},
|
||||
];
|
||||
|
||||
function getCurrentTileLayer(map) {
|
||||
let found = null;
|
||||
if (!map) return null;
|
||||
map.eachLayer(layer => {
|
||||
if (!found && layer instanceof L.TileLayer) {
|
||||
found = layer;
|
||||
}
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
export default function BaseMapPanel({ map, onSelect, onClose, initialId }) {
|
||||
const [activeId, setActiveId] = useState(initialId || null);
|
||||
const layerCacheRef = useRef({});
|
||||
const bases = useMemo(() => {
|
||||
try {
|
||||
if (typeof window !== "undefined") {
|
||||
const cfg = window.__leafletConfig;
|
||||
if (cfg && cfg.tileSources) {
|
||||
return Object.entries(cfg.tileSources).map(([key, ts]) => ({
|
||||
id: key,
|
||||
name: ts.name || key,
|
||||
url: ts.url,
|
||||
attribution: ts.attribution || "© OpenStreetMap contributors",
|
||||
minZoom: ts.minZoom ?? cfg.minZoom ?? 0,
|
||||
maxZoom: ts.maxZoom ?? cfg.maxZoom ?? 19,
|
||||
subdomains: ts.subdomains,
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return DEFAULT_BASE_LAYERS;
|
||||
}, []);
|
||||
|
||||
const applyBase = id => {
|
||||
if (!map) return;
|
||||
const base = bases.find(b => b.id === id) || bases[0];
|
||||
if (!base) return;
|
||||
|
||||
// Remove current tile layer
|
||||
const current = getCurrentTileLayer(map);
|
||||
if (current) {
|
||||
try {
|
||||
map.removeLayer(current);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Get or create the new layer
|
||||
let nextLayer = layerCacheRef.current[id];
|
||||
if (!nextLayer) {
|
||||
nextLayer = L.tileLayer(base.url, {
|
||||
attribution: base.attribution,
|
||||
subdomains: base.subdomains || "abc",
|
||||
tileSize: 256,
|
||||
minZoom: base.minZoom ?? 0,
|
||||
maxZoom: base.maxZoom ?? 19,
|
||||
noWrap: true,
|
||||
// Ensure base tiles stay behind overlays
|
||||
zIndex: 1,
|
||||
});
|
||||
layerCacheRef.current[id] = nextLayer;
|
||||
}
|
||||
|
||||
nextLayer.addTo(map);
|
||||
try {
|
||||
if (typeof map.setMinZoom === "function") map.setMinZoom(base.minZoom ?? 0);
|
||||
if (typeof map.setMaxZoom === "function") map.setMaxZoom(base.maxZoom ?? 19);
|
||||
if (typeof window !== "undefined") {
|
||||
window.__tileSourceMinZoom = base.minZoom ?? 0;
|
||||
window.__tileSourceMaxZoom = base.maxZoom ?? 19;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
setActiveId(id);
|
||||
try {
|
||||
localStorage.setItem("baseMapId", id);
|
||||
} catch (_) {}
|
||||
onSelect && onSelect(id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const saved = (() => {
|
||||
try {
|
||||
return localStorage.getItem("baseMapId");
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
const targetId = initialId || saved || bases[0]?.id;
|
||||
if (targetId) {
|
||||
applyBase(targetId);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [map]);
|
||||
|
||||
return (
|
||||
<div className="absolute top-16 right-3 z-50 w-64 bg-white rounded-lg shadow-lg p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-sm font-semibold">Map Layers</h3>
|
||||
<button onClick={onClose} aria-label="Schließen" title="Schließen">
|
||||
<Icon icon="material-symbols:close-rounded" className="h-5 w-5 text-gray-700" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{bases.map(b => (
|
||||
<button
|
||||
key={b.id}
|
||||
onClick={() => applyBase(b.id)}
|
||||
className={`text-left rounded-md border p-2 hover:bg-gray-50 ${
|
||||
activeId === b.id ? "ring-2 ring-blue-500" : ""
|
||||
}`}
|
||||
title={b.name}
|
||||
>
|
||||
<div className="font-medium text-sm">{b.name}</div>
|
||||
<div className="text-[10px] text-gray-500 truncate">{b.url}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
// /components/uiWidgets/mapLayersControlPanel/MapLayersControlPanel.js
|
||||
import { getDebugLog } from "../../../utils/configUtils";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { setSelectedArea } from "@/redux/slices/selectedAreaSlice";
|
||||
import EditModeToggle from "@/components/uiWidgets/mapLayersControlPanel/EditModeToggle";
|
||||
@@ -13,8 +14,10 @@ import { selectMapLayersState, setLayerVisibility } from "@/redux/slices/mapLaye
|
||||
import { setVisible } from "@/redux/slices/database/pois/poiLayerVisibleSlice";
|
||||
import { incrementZoomTrigger } from "@/redux/slices/zoomTriggerSlice";
|
||||
|
||||
function MapLayersControlPanel() {
|
||||
function MapLayersControlPanel({ handlePolylineCheckboxChange }) {
|
||||
const [editMode, setEditMode] = useState(false); // Zustand für editMode
|
||||
const [localStorageLoaded, setLocalStorageLoaded] = useState(false); // Tracking ob localStorage geladen wurde
|
||||
const kabelstreckenVisible = useSelector(selectPolylineVisible); // Nur noch Redux
|
||||
const poiVisible = useSelector(state => state.poiLayerVisible.visible);
|
||||
const setPoiVisible = value => dispatch(setVisible(value));
|
||||
const dispatch = useDispatch();
|
||||
@@ -24,46 +27,60 @@ function MapLayersControlPanel() {
|
||||
const GisStationsStaticDistrict = useSelector(selectGisStationsStaticDistrict) || [];
|
||||
const GisSystemStatic = useSelector(selectGisSystemStatic) || [];
|
||||
|
||||
const polylineVisible = useSelector(selectPolylineVisible);
|
||||
// Debug: Kabelstrecken state verfolgen
|
||||
useEffect(() => {
|
||||
console.log("🎯 kabelstreckenVisible state changed to:", kabelstreckenVisible);
|
||||
}, [kabelstreckenVisible]);
|
||||
|
||||
const handlePolylineCheckboxChange = event => {
|
||||
const checked = event.target.checked;
|
||||
dispatch(setPolylineVisible(checked));
|
||||
localStorage.setItem("polylineVisible", checked);
|
||||
// Prüfen, ob TALAS (IdSystem 1) erlaubt & sichtbar auf Karte (Allow + Map)
|
||||
const isTalasAllowed = Array.isArray(GisSystemStatic)
|
||||
? GisSystemStatic.some(
|
||||
system => system.IdSystem === 1 && system.Allow === 1 && system.Map === 1
|
||||
)
|
||||
: false;
|
||||
|
||||
if (checked) {
|
||||
dispatch(setLayerVisibility({ layer: "TALAS", visibility: true }));
|
||||
localStorage.setItem(
|
||||
"mapLayersVisibility",
|
||||
JSON.stringify({ ...mapLayersVisibility, TALAS: true })
|
||||
);
|
||||
}
|
||||
};
|
||||
// handlePolylineCheckboxChange kommt jetzt als Prop von MapComponent
|
||||
|
||||
useEffect(() => {
|
||||
// LocalStorage Werte laden
|
||||
// LocalStorage Werte beim ersten Laden der Komponente wiederherstellen (nur für POI und mapLayersVisibility, nicht mehr für Kabelstrecken)
|
||||
const storedPoiVisible = localStorage.getItem("poiVisible");
|
||||
if (storedPoiVisible !== null) {
|
||||
setPoiVisible(storedPoiVisible === "true");
|
||||
}
|
||||
const storedPolylineVisible = localStorage.getItem("polylineVisible");
|
||||
if (storedPolylineVisible !== null) {
|
||||
dispatch(setPolylineVisible(storedPolylineVisible === "true"));
|
||||
}
|
||||
|
||||
// Layer-Sichtbarkeiten aus localStorage laden
|
||||
const storedMapLayersVisibility = localStorage.getItem("mapLayersVisibility");
|
||||
// Kartenspezifischer localStorage-Key verwenden
|
||||
const mapId = localStorage.getItem("currentMapId");
|
||||
const userId = localStorage.getItem("currentUserId");
|
||||
const mapStorageKey =
|
||||
mapId && userId ? `mapLayersVisibility_m${mapId}_u${userId}` : "mapLayersVisibility";
|
||||
|
||||
const storedMapLayersVisibility = localStorage.getItem(mapStorageKey);
|
||||
if (storedMapLayersVisibility) {
|
||||
const parsedVisibility = JSON.parse(storedMapLayersVisibility);
|
||||
Object.keys(parsedVisibility).forEach(key => {
|
||||
dispatch(setLayerVisibility({ layer: key, visibility: parsedVisibility[key] }));
|
||||
});
|
||||
} else {
|
||||
// Initialisiere mapLayersVisibility basierend auf Allow & Map (nur Systeme mit Map===1 anzeigen)
|
||||
if (Array.isArray(GisSystemStatic)) {
|
||||
const initialVisibility = {};
|
||||
GisSystemStatic.forEach(system => {
|
||||
const systemKey = `system-${system.IdSystem}`;
|
||||
const visibility = system.Allow === 1 && system.Map === 1;
|
||||
if (visibility) {
|
||||
initialVisibility[systemKey] = visibility;
|
||||
dispatch(setLayerVisibility({ layer: systemKey, visibility }));
|
||||
}
|
||||
});
|
||||
localStorage.setItem(mapStorageKey, JSON.stringify(initialVisibility));
|
||||
}
|
||||
}
|
||||
|
||||
// EditMode lesen
|
||||
const storedEditMode = localStorage.getItem("editMode");
|
||||
setEditMode(storedEditMode === "true");
|
||||
}, [setPoiVisible, dispatch]); // ✅ `setMapLayersVisibility` entfernt
|
||||
|
||||
setLocalStorageLoaded(true);
|
||||
}, []); // Läuft nur beim Mount
|
||||
|
||||
const handleAreaChange = event => {
|
||||
const selectedIndex = event.target.options.selectedIndex;
|
||||
@@ -72,8 +89,13 @@ function MapLayersControlPanel() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Allowed systems jetzt nach Allow && Map filtern
|
||||
const allowedSystems = Array.isArray(GisSystemStatic)
|
||||
? new Set(GisSystemStatic.filter(system => system.Allow === 1).map(system => system.IdSystem))
|
||||
? new Set(
|
||||
GisSystemStatic.filter(system => system.Allow === 1 && system.Map === 1).map(
|
||||
system => system.IdSystem
|
||||
)
|
||||
)
|
||||
: new Set();
|
||||
|
||||
const seenNames = new Set();
|
||||
@@ -97,7 +119,7 @@ function MapLayersControlPanel() {
|
||||
const seenSystemNames = new Set();
|
||||
const filteredSystems = Array.isArray(GisSystemStatic)
|
||||
? GisSystemStatic.filter(item => {
|
||||
const isUnique = !seenSystemNames.has(item.Name) && item.Allow === 1;
|
||||
const isUnique = !seenSystemNames.has(item.Name) && item.Allow === 1 && item.Map === 1; // <— Map Bedingung hinzugefügt
|
||||
if (isUnique) {
|
||||
seenSystemNames.add(item.Name);
|
||||
}
|
||||
@@ -108,8 +130,8 @@ function MapLayersControlPanel() {
|
||||
setSystemListing(
|
||||
filteredSystems.map((system, index) => ({
|
||||
id: index + 1,
|
||||
name: system.Name, // Verwende den Originalnamen für die Anzeige
|
||||
key: `system-${system.IdSystem}`, // Internen Schlüssel für die MapLayersVisibility-Logik
|
||||
name: system.Name, // Anzeige
|
||||
key: `system-${system.IdSystem}`, // interner Schlüssel
|
||||
}))
|
||||
);
|
||||
}, [GisStationsStaticDistrict, GisSystemStatic]);
|
||||
@@ -117,13 +139,38 @@ function MapLayersControlPanel() {
|
||||
const handleCheckboxChange = (key, event) => {
|
||||
if (editMode) return;
|
||||
const { checked } = event.target;
|
||||
|
||||
dispatch(setLayerVisibility({ layer: key, visibility: checked }));
|
||||
localStorage.setItem(
|
||||
"mapLayersVisibility",
|
||||
JSON.stringify({ ...mapLayersVisibility, [key]: checked })
|
||||
// Debug-Ausgabe
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const mapId = params.get("m");
|
||||
const userId = params.get("u");
|
||||
const polylineKey =
|
||||
mapId && userId ? `polylineVisible_m${mapId}_u${userId}` : "polylineVisible";
|
||||
console.log(
|
||||
"[UI/handleCheckboxChange] key:",
|
||||
key,
|
||||
"checked:",
|
||||
checked,
|
||||
"Redux:",
|
||||
kabelstreckenVisible,
|
||||
"localStorage:",
|
||||
localStorage.getItem(polylineKey)
|
||||
);
|
||||
|
||||
dispatch(setLayerVisibility({ layer: key, visibility: checked }));
|
||||
const mapId2 = localStorage.getItem("currentMapId");
|
||||
const userId2 = localStorage.getItem("currentUserId");
|
||||
const mapStorageKey =
|
||||
mapId2 && userId2 ? `mapLayersVisibility_m${mapId2}_u${userId2}` : "mapLayersVisibility";
|
||||
localStorage.setItem(mapStorageKey, JSON.stringify({ ...mapLayersVisibility, [key]: checked }));
|
||||
// Wenn TALAS (system-1) deaktiviert wird, Kabelstrecken deaktivieren
|
||||
if (key === "system-1" && !checked) {
|
||||
localStorage.setItem("kabelstreckenVisible", "false");
|
||||
localStorage.setItem("polylineVisible", "false");
|
||||
dispatch(setPolylineVisible(false));
|
||||
setTimeout(() => {
|
||||
const polylineEvent = new Event("polylineVisibilityChanged");
|
||||
window.dispatchEvent(polylineEvent);
|
||||
}, 50);
|
||||
}
|
||||
setTimeout(() => {
|
||||
const event = new Event("visibilityChanged");
|
||||
window.dispatchEvent(event);
|
||||
@@ -133,7 +180,7 @@ function MapLayersControlPanel() {
|
||||
const handlePoiCheckboxChange = event => {
|
||||
const { checked } = event.target;
|
||||
setPoiVisible(checked);
|
||||
localStorage.setItem("poiVisible", checked); // Store POI visibility in localStorage
|
||||
localStorage.setItem("poiVisible", checked.toString()); // Store POI visibility in localStorage
|
||||
};
|
||||
|
||||
const handleIconClick = () => {
|
||||
@@ -143,7 +190,7 @@ function MapLayersControlPanel() {
|
||||
|
||||
//------------------------------
|
||||
useEffect(() => {
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
window.__debug = window.__debug || {};
|
||||
window.__debug.gisStations = GisStationsStaticDistrict;
|
||||
}
|
||||
@@ -175,7 +222,7 @@ function MapLayersControlPanel() {
|
||||
}
|
||||
return isUnique;
|
||||
});
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("📌 stationListing aktualisiert:", filteredAreas);
|
||||
}
|
||||
}, [GisStationsStaticDistrict, GisSystemStatic]);
|
||||
@@ -194,44 +241,42 @@ function MapLayersControlPanel() {
|
||||
}, [GisStationsStaticDistrict]);
|
||||
//---------------------------
|
||||
|
||||
// Polyline (Kabelstrecken) abhängig von TALAS (system-1)
|
||||
const onPolylineToggle = checked => {
|
||||
if (editMode) return;
|
||||
|
||||
// Wenn Nutzer Kabelstrecken einschaltet, aber TALAS aktuell ausgeblendet ist,
|
||||
// dann TALAS automatisch aktivieren (sofern erlaubt)
|
||||
const talasKey = "system-1";
|
||||
const talasVisible = !!mapLayersVisibility[talasKey];
|
||||
if (checked && isTalasAllowed && !talasVisible) {
|
||||
dispatch(setLayerVisibility({ layer: talasKey, visibility: true }));
|
||||
|
||||
// Persistiere Sichtbarkeit map/user-spezifisch
|
||||
const mapId2 = localStorage.getItem("currentMapId");
|
||||
const userId2 = localStorage.getItem("currentUserId");
|
||||
const mapStorageKey =
|
||||
mapId2 && userId2 ? `mapLayersVisibility_m${mapId2}_u${userId2}` : "mapLayersVisibility";
|
||||
localStorage.setItem(
|
||||
mapStorageKey,
|
||||
JSON.stringify({ ...mapLayersVisibility, [talasKey]: true })
|
||||
);
|
||||
|
||||
// Event feuern wie an anderer Stelle
|
||||
setTimeout(() => {
|
||||
const event = new Event("visibilityChanged");
|
||||
window.dispatchEvent(event);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Sichtbarkeit der Kabelstrecken setzen
|
||||
handlePolylineCheckboxChange(checked);
|
||||
};
|
||||
|
||||
//---------------------------
|
||||
return (
|
||||
<div
|
||||
id="mainDataSheet"
|
||||
className="absolute top-3 right-3 w-1/6 min-w-[300px] max-w-[400px] z-10 bg-white p-2 rounded-lg shadow-lg"
|
||||
>
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<select
|
||||
onChange={handleAreaChange}
|
||||
id="stationListing"
|
||||
className="border-solid-1 p-2 rounded ml-1 font-semibold"
|
||||
style={{ minWidth: "150px", maxWidth: "200px" }}
|
||||
>
|
||||
<option value="Station wählen">Station wählen</option>
|
||||
{[
|
||||
...new Map(
|
||||
(GisStationsStaticDistrict.Points || [])
|
||||
.filter(p => !!p.Area_Name)
|
||||
.map(p => [p.Area_Name, p])
|
||||
).values(),
|
||||
].map((item, index) => (
|
||||
<option key={item.Area_Name} value={item.IdLD}>
|
||||
{item.Area_Name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex items-center space-x-2">
|
||||
<EditModeToggle />
|
||||
<img
|
||||
src="/img/expand-icon.svg"
|
||||
alt="Expand"
|
||||
className="h-6 w-6 cursor-pointer"
|
||||
onClick={handleIconClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-16 right-3 w-72 z-50 bg-white rounded-lg shadow-md">
|
||||
<div id="mainDataSheet" className="flex flex-col gap-4 p-4">
|
||||
{/* Checkboxen mit Untermenüs */}
|
||||
<div className="flex flex-col gap-2">
|
||||
{systemListing.map(system => (
|
||||
@@ -255,9 +300,10 @@ function MapLayersControlPanel() {
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={polylineVisible} // Zustand für Kabelstrecken
|
||||
onChange={handlePolylineCheckboxChange}
|
||||
checked={kabelstreckenVisible}
|
||||
onChange={e => onPolylineToggle(e.target.checked)}
|
||||
id="polyline-checkbox"
|
||||
disabled={!isTalasAllowed || editMode}
|
||||
/>
|
||||
<label htmlFor="polyline-checkbox" className="text-sm ml-2">
|
||||
Kabelstrecken
|
||||
@@ -279,25 +325,6 @@ function MapLayersControlPanel() {
|
||||
POIs
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Areas
|
||||
<div className="flex items-center">
|
||||
<input type="checkbox" checked={areaVisible} onChange={handleAreaCheckboxChange} id="area-checkbox" />
|
||||
<label htmlFor="area-checkbox" className="text-sm ml-2">
|
||||
Bereiche
|
||||
</label>
|
||||
</div>
|
||||
*/}
|
||||
|
||||
{/* Standorte
|
||||
<div className="flex items-center">
|
||||
<input type="checkbox" checked={standordVisible} onChange={handleStandorteCheckboxChange} id="area-checkbox" />
|
||||
<label htmlFor="area-checkbox" className="text-sm ml-2">
|
||||
Standorte
|
||||
</label>
|
||||
</div>
|
||||
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
// config/paths.js
|
||||
const basePathRaw = process.env.NEXT_PUBLIC_BASE_PATH || "";
|
||||
const BASE_PATH = basePathRaw.replace(/^\/|\/$/g, "");
|
||||
export const BASE_URL = BASE_PATH ? `/${BASE_PATH}` : "";
|
||||
let __configCache;
|
||||
export async function getBaseUrl() {
|
||||
if (__configCache) return __configCache;
|
||||
const res = await fetch("/config.json");
|
||||
if (!res.ok) throw new Error("config.json konnte nicht geladen werden");
|
||||
const config = await res.json();
|
||||
const basePath = (config.basePath || "").replace(/^\/|\/$/g, "");
|
||||
__configCache = basePath ? `/${basePath}` : "";
|
||||
return __configCache;
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
const { defineConfig } = require("cypress");
|
||||
|
||||
module.exports = defineConfig({
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// Node-Event-Listeners hier konfigurieren
|
||||
},
|
||||
experimentalStudio: true, // Studio aktivieren
|
||||
},
|
||||
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "next",
|
||||
bundler: "webpack",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
describe("contextmenuTest", () => {
|
||||
it("contetmenu Station öffnen (Tab)", () => {
|
||||
cy.log("Test startet jetzt");
|
||||
|
||||
// 1. Viewport einstellen
|
||||
cy.viewport(1920, 1080);
|
||||
cy.log("Viewport eingestellt auf 1920x1080");
|
||||
|
||||
// 2. Seite besuchen
|
||||
cy.visit("http://10.10.0.13:3000/?m=12&u=484");
|
||||
cy.wait(5000); // Wartezeit nach dem Laden
|
||||
cy.log("Seite geöffnet");
|
||||
|
||||
// 3. Sicherstellen, dass die Karte geladen ist
|
||||
cy.get("#map", { timeout: 15000 }).should("be.visible");
|
||||
cy.log("Karte geladen");
|
||||
|
||||
// 4. Wartezeit zum Stabilisieren der Karte
|
||||
cy.wait(2000);
|
||||
|
||||
// 5. Marker suchen und Rechtsklick simulieren
|
||||
cy.get('img[src*="img/icons/marker-icon-20.svg"]') // Marker suchen
|
||||
.filter(":visible") // Nur sichtbare Marker
|
||||
.first() // Ersten Marker auswählen
|
||||
.scrollIntoView() // Marker in den sichtbaren Area scrollen
|
||||
.should("be.visible") // Sicherstellen, dass Marker sichtbar ist
|
||||
.trigger("mouseover") // Mouseover simulieren
|
||||
.wait(500) // Wartezeit nach Mouseover
|
||||
.rightclick({ force: true }); // Rechtsklick auf den Marker
|
||||
cy.log("Rechtsklick auf Marker ausgeführt");
|
||||
|
||||
// Screenshot nach Rechtsklick zum Debugging
|
||||
// cy.screenshot("nach-rechtsklick");
|
||||
|
||||
// 6. Kontextmenü prüfen mit explizitem Selektor
|
||||
cy.get(".leaflet-contextmenu-item") // Suche alle Menüeinträge mit der Klasse
|
||||
.contains("Station öffnen (Tab)", { timeout: 5000 }) // Prüfe Text innerhalb des Eintrags
|
||||
.should("be.visible"); // Sichtbarkeit sicherstellen
|
||||
cy.log("Menüeintrag gefunden");
|
||||
|
||||
// 7. URL abfangen und testen, bevor der Tab geöffnet wird
|
||||
const targetUrl = "http://10.10.0.13/talas5/devices/cpl.aspx?ver=35&kue=24&id=50922";
|
||||
|
||||
// HTTP-Anfrage zur Überprüfung des Status
|
||||
cy.request(targetUrl).then((response) => {
|
||||
expect(response.status).to.eq(200); // Erwartet HTTP 200 OK
|
||||
cy.log("URL ist erreichbar, Status 200");
|
||||
});
|
||||
|
||||
// 8. Menüeintrag auswählen (öffnet den neuen Tab)
|
||||
cy.get(".leaflet-contextmenu-item") // Explizit Menüeintrag mit Klasse auswählen
|
||||
.contains("Station öffnen (Tab)")
|
||||
.click(); // Menüeintrag anklicken
|
||||
cy.log("Menüeintrag ausgewählt");
|
||||
|
||||
// 9. Klick auf die Karte, um Kontextmenü zu schließen
|
||||
cy.get("#map").click(100, 100); // Klick auf eine leere Stelle
|
||||
cy.log("Kontextmenü geschlossen");
|
||||
|
||||
// 10. Optionaler Screenshot nach Abschluss
|
||||
// cy.screenshot("test-abgeschlossen");
|
||||
});
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
describe("GMA Markers Layer", () => {
|
||||
before(() => {});
|
||||
|
||||
it("Der Test stellt sicher, dass das GMA Tooltip-Element für 'Rastede' angezeigt ist und die erwarteten Werte wie LT:, FBT:, GT: und RLF: enthält.", () => {
|
||||
// Testbeschreibung: Dieser Test überprüft, ob der Tooltip selbst korrekt dargestellt wird und den erwarteten Inhalt anzeigt.
|
||||
|
||||
// Besuche die Map-Seite
|
||||
//cy.visit("http://10.10.0.13:3000/?m=12&u=484"); // Passe die URL an
|
||||
cy.visit("http://127.0.0.1:3000/?m=12&u=484");
|
||||
|
||||
cy.contains(".leaflet-tooltip", "Rastede")
|
||||
// Wählt das Tooltip-Element mit der Klasse `leaflet-tooltip`, das den Text "Rastede" enthält.
|
||||
.first();
|
||||
|
||||
cy.get(".leaflet-tooltip")
|
||||
// Wählt das Tooltip-Element erneut aus, um weitere Überprüfungen durchzuführen.
|
||||
.should("be.visible")
|
||||
// Überprüft, ob das Tooltip sichtbar ist.
|
||||
.and("contain", "LT:")
|
||||
// Stellt sicher, dass der Tooltip den Text "LT :" enthält.
|
||||
.and("contain", "FBT:")
|
||||
// Stellt sicher, dass der Tooltip auch den Text "FBT:" enthält.
|
||||
.and("contain", "GT:")
|
||||
// Stellt sicher, dass der Tooltip auch den Text "GT:" enthält.
|
||||
.and("contain", "RLF:");
|
||||
// Stellt sicher, dass der Tooltip auch den Text "RLF:" enthält.
|
||||
});
|
||||
|
||||
it("should open context menu on right-click on tooltip", () => {
|
||||
// Testbeschreibung: Dieser Test überprüft, ob ein Rechtsklick auf den Tooltip das Kontextmenü öffnet.
|
||||
|
||||
// Besuche die Map-Seite
|
||||
cy.visit("http://127.0.0.1:3000/?m=12&u=484"); // Passe die URL an
|
||||
//warte 2 Sekunden
|
||||
cy.wait(2000);
|
||||
|
||||
cy.contains(".leaflet-tooltip", "Rastede")
|
||||
// Wählt das Tooltip-Element mit der Klasse `leaflet-tooltip`, das den Text "Rastede" enthält.
|
||||
.first()
|
||||
.should("be.visible") // Überprüft, ob das Tooltip sichtbar ist.
|
||||
.trigger("contextmenu"); // Führt einen Rechtsklick (Kontextmenü-Ereignis) auf das Tooltip aus.
|
||||
|
||||
cy.get(".custom-context-menu")
|
||||
// Wählt das Element aus, das das Kontextmenü darstellt.
|
||||
.should("be.visible") // Überprüft, ob das Kontextmenü sichtbar ist.
|
||||
.and("contain", "Station öffnen (Tab)") // Überprüft, ob der Eintrag "Station öffnen (Tab)" vorhanden ist.
|
||||
.and("contain", "Koordinaten anzeigen") // Überprüft, ob der Eintrag "Koordinaten anzeigen" vorhanden ist.
|
||||
.and("contain", "Reinzoomen") // Überprüft, ob der Eintrag "Reinzoomen" vorhanden ist.
|
||||
.and("contain", "Rauszoomen") // Überprüft, ob der Eintrag "Rauszoomen" vorhanden ist.
|
||||
.and("contain", "Hier zentrieren"); // Überprüft, ob der Eintrag "Hier zentrieren" vorhanden ist.
|
||||
});
|
||||
|
||||
//-----------------------------------------------
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
//TDD Test Driven Development
|
||||
// Dieser Test überprüft die Karteninteraktion: Eingabe von Koordinaten und Zentrieren der Karte
|
||||
// Schritte:
|
||||
// 1. Öffnen der Karte auf einer bestimmten Seite.
|
||||
// 2. Eingeben von Koordinaten in ein Eingabefeld.
|
||||
// 3. Klicken auf einen Button, um die Karte zu den Koordinaten zu zoomen.
|
||||
// 4. Überprüfen, ob die Karte korrekt zentriert wurde.
|
||||
//--------------------------------------------------------------Test4 git config --global user.email "ismailali1553@gmail.com" in Terminal eingegeben und
|
||||
// benutzer email adresse eingegeben git config --global user.name "ismailali1553"
|
||||
describe("Karteninteraktion", () => {
|
||||
it("zoomt zu den eingegebenen Koordinaten", () => {
|
||||
// Öffne die Seite mit der Karte
|
||||
cy.visit("http://127.0.0.1:3000/?m=12&u=484"); // Passe den Pfad an deine Karte an
|
||||
|
||||
// Gebe Koordinaten in das Eingabefeld ein
|
||||
cy.get('input[placeholder="Koordinaten eingeben (lat,lng)"]').type("52.52,13.405");
|
||||
|
||||
// Klicke auf den Button "Zu Marker zoomen"
|
||||
cy.get("button").contains("Zu Marker zoomen").click();
|
||||
|
||||
// Überprüfe, ob die Karte die eingegebenen Koordinaten korrekt zentriert hat
|
||||
cy.window().then((win) => {
|
||||
const map = win.map; // Zugriff auf die Leaflet-Instanz
|
||||
const center = map.getCenter(); // Aktuelles Zentrum der Karte abrufen
|
||||
expect(center.lat).to.be.closeTo(52.52, 0.01); // Latitude überprüfen
|
||||
expect(center.lng).to.be.closeTo(13.405, 0.01); // Longitude überprüfen
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
//cypress/e2e/poiUpdateModal.cy.js
|
||||
describe("POI bearbeiten – Typ-Auswahl prüfen", () => {
|
||||
beforeEach(() => {
|
||||
cy.visit("http://localhost:3000/?m=12&u=484");
|
||||
cy.get(".leaflet-container", { timeout: 10000 }).should("be.visible");
|
||||
cy.get(".leaflet-marker-icon", { timeout: 10000 }).should("have.length.greaterThan", 0);
|
||||
});
|
||||
|
||||
it("sollte beim Öffnen des Modals den richtigen POI-Typ anzeigen", () => {
|
||||
cy.get('svg[aria-label="Bearbeitungsmodus aktivieren"]').click({ force: true });
|
||||
cy.wait(5000);
|
||||
|
||||
cy.get('img[src="/img/icons/pois/poi-marker-icon-4.png"]', { timeout: 10000 }).should("be.visible");
|
||||
cy.get('img[src="/img/icons/pois/poi-marker-icon-4.png"]').first().rightclick({ force: true });
|
||||
|
||||
cy.contains("POI Bearbeiten").click({ force: true });
|
||||
|
||||
cy.get("#idPoiTyp", { timeout: 10000 }).should("exist").find("[class*='singleValue']").should("not.contain.text", "Typ auswählen");
|
||||
});
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
describe("TK-Komponenten", () => {
|
||||
before(() => {
|
||||
// Lade die Seite nur einmal vor allen Tests
|
||||
cy.visit("http://10.10.0.13:3000/?m=12&u=484");
|
||||
//cy.wait(5000); // Wartezeit, bis die Seite vollständig geladen ist, cypress macht automatisch , alsobrauchen wir im moment kein wait() wenn cy. schafft
|
||||
});
|
||||
|
||||
it("soll alle Tests in Reihenfolge ausführen", () => {
|
||||
// Test 1: Sicherstellen, dass die Checkbox vorhanden und sichtbar ist
|
||||
cy.get("input[type='checkbox'][id='system-10']")
|
||||
.should("exist")
|
||||
.and("be.visible")
|
||||
.then(() => {
|
||||
cy.log("Die Checkbox mit ID 'system-10' ist vorhanden und sichtbar.");
|
||||
});
|
||||
|
||||
// Test 2: Sicherstellen, dass die Checkbox aktiviert ist
|
||||
cy.get("input[type='checkbox'][id='system-10']").then(($checkbox) => {
|
||||
if (!$checkbox.prop("checked")) {
|
||||
// Falls die Checkbox nicht aktiviert ist, aktiviere sie
|
||||
cy.wrap($checkbox).check({ force: true });
|
||||
cy.log("Die Checkbox war deaktiviert und wurde jetzt aktiviert.");
|
||||
} else {
|
||||
cy.log("Die Checkbox ist bereits aktiviert.");
|
||||
}
|
||||
});
|
||||
|
||||
// Test 3: Checkbox deaktivieren und Marker verschwinden lassen
|
||||
cy.get("input[type='checkbox'][id='system-10']")
|
||||
.uncheck({ force: true })
|
||||
.then(() => {
|
||||
cy.log("Die Checkbox wurde deaktiviert.");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
describe("Map Initial Load Test", () => {
|
||||
it("should load the map with the correct center and zoom", () => {
|
||||
// Besuche die Seite, auf der die Karte angezeigt wird
|
||||
cy.visit("http://192.168.10.167:3000/?m=12&u=485");
|
||||
|
||||
// Überprüfe, ob das Kartenelement existiert
|
||||
cy.get("#map").should("be.visible");
|
||||
|
||||
// Überprüfe, ob die Karte das korrekte Zentrum und den korrekten Zoom hat
|
||||
cy.window().then((win) => {
|
||||
const map = win.L.map;
|
||||
const center = map.getCenter();
|
||||
const zoom = map.getZoom();
|
||||
|
||||
expect(center.lat).to.be.closeTo(53.111111, 0.0001);
|
||||
expect(center.lng).to.be.closeTo(8.4625, 0.0001);
|
||||
expect(zoom).to.eq(12);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 618 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 587 KiB |
@@ -1,25 +0,0 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
@@ -1,14 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Components App</title>
|
||||
<!-- Used by Next.js to inject CSS. -->
|
||||
<div id="__next_css__DO_NOT_USE__"></div>
|
||||
</head>
|
||||
<body>
|
||||
<div data-cy-root></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,27 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example support/component.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
import { mount } from 'cypress/react18'
|
||||
|
||||
Cypress.Commands.add('mount', mount)
|
||||
|
||||
// Example use:
|
||||
// cy.mount(<MyComponent />)
|
||||
@@ -1,56 +0,0 @@
|
||||
// ***********************************************************
|
||||
// Diese Datei wird automatisch geladen, bevor Tests ausgeführt werden.
|
||||
//
|
||||
// Dies ist ein guter Platz für globale Konfigurationen
|
||||
// und Änderungen, die Cypress beeinflussen.
|
||||
//
|
||||
// Weitere Infos: https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Importiere zusätzliche Befehle
|
||||
import "./commands";
|
||||
|
||||
// Alternativ: CommonJS-Syntax verwenden
|
||||
// require('./commands')
|
||||
|
||||
// Verstecke unnötige Logs für XHR- und Fetch-Anfragen
|
||||
const app = window.top;
|
||||
if (!app.document.head.querySelector("[data-hide-command-log-request]")) {
|
||||
const style = app.document.createElement("style");
|
||||
style.innerHTML = `
|
||||
.command-name-request, .command-name-xhr {
|
||||
display: none; /* Verstecke Fetch- und XHR-Logs */
|
||||
}
|
||||
.runnable-pass .collapsible-header {
|
||||
display: none; /* Verstecke den Header erfolgreicher Tests */
|
||||
}
|
||||
`;
|
||||
style.setAttribute("data-hide-command-log-request", "");
|
||||
app.document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// Globale Ereignisse für Cypress konfigurieren
|
||||
Cypress.on("test:after:run", (test, runnable) => {
|
||||
// Minimiert die Logs für erfolgreiche Tests
|
||||
if (test.state === "passed") {
|
||||
runnable._testConfigBody = null; // Löscht den Test Body
|
||||
}
|
||||
});
|
||||
|
||||
// Schließe automatisch erfolgreiche Tests in der GUI
|
||||
Cypress.on("log:added", (log) => {
|
||||
if (log.state === "passed") {
|
||||
log.displayName = ""; // Versteckt Log-Details für erfolgreiche Schritte
|
||||
}
|
||||
});
|
||||
|
||||
// Screenshot nach jedem fehlgeschlagenen Test
|
||||
Cypress.on("fail", (error, runnable) => {
|
||||
cy.screenshot(`error-${runnable.title}`); // Screenshot für Fehler erstellen
|
||||
throw error; // Fehler weitergeben
|
||||
});
|
||||
|
||||
// Logge die Dauer jedes Tests
|
||||
Cypress.on("test:after:run", (test) => {
|
||||
cy.log(`Test "${test.title}" abgeschlossen in ${test.duration} ms`);
|
||||
});
|
||||
@@ -6,10 +6,6 @@ DB_PASSWORD="root#$"
|
||||
DB_NAME=talas_v5
|
||||
DB_PORT=3306
|
||||
|
||||
# Public Settings (Client braucht IP/Domain) , Variablen mit dem Präfix "NEXT_PUBLIC" ist in Browser sichtbar
|
||||
NEXT_PUBLIC_DEBUG_LOG=false
|
||||
|
||||
|
||||
|
||||
#auf dem Entwicklungsrechner dev läuft auf Port 3000 und auf dem Server prod auf Port 80, aber der WebService ist immer auf PORT 80
|
||||
NEXT_PUBLIC_API_PORT_MODE=prod
|
||||
|
||||
@@ -25,6 +25,31 @@ Entwicklung, Architekturverständnis und Erweiterung.
|
||||
### ⚙️ Konfiguration
|
||||
|
||||
- [Allgemeine Übersicht](config/README.md)
|
||||
- [Kartenquellen-Konfiguration (public/config.json)](#kartenquellen-konfiguration-publicconfigjson)
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Kartenquellen-Konfiguration (public/config.json)
|
||||
|
||||
Die Datei `public/config.json` steuert, welche Kartenquelle (z.B. OSM oder lokale Tiles) für die
|
||||
Leaflet-Karte verwendet wird.
|
||||
|
||||
**Beispiel:**
|
||||
|
||||
```json
|
||||
{
|
||||
"//info": "tileSources: 'local' für offline, 'osm' für online",
|
||||
"tileSources": {
|
||||
"local": "http://localhost/talas5/TileMap/mapTiles/{z}/{x}/{y}.png",
|
||||
"osm": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
},
|
||||
"active": "osm"
|
||||
}
|
||||
```
|
||||
|
||||
- Mit `active` kann zwischen Online- und Offline-Karten umgeschaltet werden.
|
||||
- Die Datei wird beim Start der App automatisch geladen.
|
||||
- Für Offline-Betrieb muss das lokale Kartenmaterial vorhanden sein (siehe Installationsanleitung).
|
||||
|
||||
### 🧩 Hauptkomponenten
|
||||
|
||||
|
||||
34
docs/TODO.md
@@ -69,3 +69,37 @@ die Daten von DB auch mit WebSocket gelöst werden
|
||||
|
||||
- [ ] Redundante Kontextmenülogik auflösen
|
||||
- [ ] Bessere Trennung zwischen Mock- und Live-API in Service-Funktionen
|
||||
|
||||
---
|
||||
|
||||
28.07.2025 IdSystem 11 GMA Glätemeldeanlagen, werden neu neu laden das Browser nich mehr geladen in
|
||||
DB maps idsystem ändern und testen
|
||||
|
||||
# 12.09.2025
|
||||
|
||||
Die aktuelle Ansicht ist bei kleineren Auflösungen unübersichtlich bzw. es wird zuviel von der
|
||||
eigentlichen Karte verdeckt. Unquittierter Alarm, critical
|
||||
|
||||
Zu Marker zoomen
|
||||
|
||||
Station suchen
|
||||
|
||||
- [ ] TODO: Unquittierter Alarm, critical 🚨 Alarm
|
||||
- [ ] TODO: Zu Marker zoomen: Dropdown-Menu Station auswählen und hinein zoomen bis zu ausgewählte
|
||||
in einem betimmten Zoom-Stufe der Leaflet (OSM) Station mit flyto in Leaflet 📍 POI
|
||||
- [ ] TODO: Station suchen: CoordinateInput.js Modal soll über einem Icon 'Suche / Lupe' oben rechts
|
||||
eingeblendet und ausgeblendet um mehr von der Karte zu sehen 🔍 Suche
|
||||
- [ ] TODO: Editiermodus: EditMode Stift Icon aktivieren und deaktivieren um POI Position ändern zu
|
||||
können wenn der User Berechtigung hat ✏️ Edit
|
||||
- [ ] TODO: Vergrössern: Maximieren Icon Button um rauszoomen zu einem bestimmten Bereich, z.B.
|
||||
Deutschland Karte im Fenster sichtbar ⬜ Fenster maximieren
|
||||
|
||||
- [ ] TODO: Ebenen (Openstreetmap): Stack/Stapel/Ebenen Icon um den Ansicht der Karten zu ändern 🗂️
|
||||
Stapel
|
||||
|
||||
- [ ] TODO: Menü öffenen mit Kiste der Systeme: MapLayerControlPanel.js Modal soll über einem Icon
|
||||
'Hamburger menu' oben rechts eingeblendet und ausgeblendet um mehr von der Karte zu sehen ☰
|
||||
Hamburger-Menü
|
||||
- [ ] TODO: Info Karte: VersionInfoModal.js Modal soll über einem Icon 'Info' oben rechts
|
||||
eingeblendet und ausgeblendet um mehr von der Karte zu sehen ℹ️ Info
|
||||
https://www.openstreetmap.org/#map=13/51.80097/9.33495&layers=P
|
||||
|
||||
@@ -87,7 +87,7 @@ sequenceDiagram
|
||||
|
||||
- **Konfigurierbarer basePath:**
|
||||
- **Konfigurierbarer basePath:**
|
||||
Pfad wie `/talas5` ist optional und kann per Umgebungsvariable `NEXT_PUBLIC_BASE_PATH` gesetzt
|
||||
Pfad wie `/talas5` ist optional und wird jetzt in `public/config.json` als `basePath` gepflegt
|
||||
werden.
|
||||
Die Konfiguration erfolgt je nach Umgebung über:
|
||||
|
||||
|
||||
@@ -22,4 +22,73 @@ Verzeichnisstruktur funktioniert.
|
||||
|
||||
---
|
||||
|
||||
## OSM‑basierte, „open“ Quellen
|
||||
|
||||
Diese sind datenrechtlich offen (ODbL bzw. Community-Lizenzen), aber das „kostenlos“ gilt nicht im
|
||||
Sinne unbegrenzter Tile‑Nutzung. Die Tile‑Server werden als Community‑Ressource bereitgestellt –
|
||||
bitte Policies respektieren.
|
||||
|
||||
- osm-standard (OpenStreetMap)
|
||||
- - Key: Nein
|
||||
- - Nutzung: Fair‑Use; für produktive/hohe Last eigenen Tile‑Server/Provider verwenden.
|
||||
- - Attribution: „© OpenStreetMap contributors“
|
||||
|
||||
- osm-humanitarian (HOT)
|
||||
- - Key: Nein
|
||||
- - Nutzung: Fair‑Use; für größere Last die Betreiber kontaktieren bzw. andere Infrastruktur nutzen.
|
||||
- - Attribution: „© OpenStreetMap contributors <br>
|
||||
-
|
||||
- Humanitarian OpenStreetMap Team“ cyclosm
|
||||
- - Key: Nein
|
||||
- - Nutzung: Fair‑Use (bereitgestellt u. a. über OSM France). Für höhere Last
|
||||
Unterstützung/Hostingoptionen prüfen.
|
||||
- - Attribution: „CyclOSM“ + „OpenStreetMap contributors“
|
||||
- Praxis‑Tipps Kleine bis mittlere Nutzung: Die oben genannten „keyless“ Quellen sind oft
|
||||
ausreichend, solange du Attribution setzt und Limits respektierst. Produktion/hohe Last: Nutze
|
||||
einen Anbieter mit Vertrag/Key (z. B. Thunderforest, Tracestrack, MapTiler, Mapbox) oder hoste
|
||||
Tiles selbst. Schlüssel im Client: Für Thunderforest/Tracestrack stehen die Keys im Frontend. Das
|
||||
ist üblich, aber der Key ist sichtbar. Wenn du ihn verbergen willst, richte einen kleinen
|
||||
Tile‑Proxy auf deinem Server ein, der den Key serverseitig anhängt und optional cached.
|
||||
Attribution: Dein BaseMapPanel setzt bereits Attributionsstrings aus config.json. Achte darauf,
|
||||
dass sie je Quelle korrekt sind.
|
||||
|
||||
---
|
||||
|
||||
Kurzantwort: Für kommerzielle Nutzung sind OSM‑Community‑Tile‑Server nicht geeignet. Nutze einen
|
||||
bezahlten Anbieter (z. B. Thunderforest, Tracestrack) oder hoste selbst. Attribution ist immer
|
||||
Pflicht.
|
||||
|
||||
Links und Hinweise je Layer/Provider:
|
||||
|
||||
OpenStreetMap Standard (osm-standard)
|
||||
|
||||
Lizenz/Daten: ODbL, Attribution Pflicht Tile-Server-Policy (keine Produktion/hohe Last):
|
||||
https://operations.osmfoundation.org/policies/tiles/ Urheberrecht/Attribution:
|
||||
https://www.openstreetmap.org/copyright HOT Humanitarian (osm-humanitarian)
|
||||
|
||||
Community-Server (OSM France); keine Produktion/hohe Last Info/Policy OSM France Tiles:
|
||||
https://tile.openstreetmap.fr/ HOT: https://www.hotosm.org/ CyclOSM (cyclosm)
|
||||
|
||||
Community-Server (OSM France); keine Produktion/hohe Last Projektseite: https://www.cyclosm.org/
|
||||
Hinweise/Policy (OSM France): https://tile.openstreetmap.fr/ Wiki:
|
||||
https://wiki.openstreetmap.org/wiki/CyclOSM Carto Light (carto-light / Positron)
|
||||
|
||||
Keylos nutzbar mit Attribution; Fair‑Use, für hohe Last über CARTO‑Pläne Basemaps:
|
||||
https://carto.com/basemaps/ Attribution: https://carto.com/attributions Pricing (Plattform):
|
||||
https://carto.com/pricing/ (bei großem Volumen Sales kontaktieren) Thunderforest (Cycle/Transport u.
|
||||
a.)
|
||||
|
||||
Kommerziell mit API‑Key; Pläne von Free bis Pro Pricing: https://www.thunderforest.com/pricing/
|
||||
Terms/Attribution: https://www.thunderforest.com/terms/ Tracestrack Topo
|
||||
|
||||
API‑Key erforderlich; kostenlose und bezahlte Pläne Übersicht/Pricing:
|
||||
https://www.tracestrack.com/en/maps/ Nutzungsbedingungen: https://www.tracestrack.com/en/terms/
|
||||
Empfehlung:
|
||||
|
||||
Produktion: Nimm Thunderforest oder Tracestrack (oder MapTiler: https://www.maptiler.com/pricing/)
|
||||
oder hoste Tiles selbst. Attribution in der Karte anzeigen (OSM + jeweiliger Anbieter). Soll ich
|
||||
diese Links samt klarer Hinweise kompakt in eure README.md einpflegen?
|
||||
|
||||
---
|
||||
|
||||
[Zurück zur Übersicht](../README.md)
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
# 📁 paths.js
|
||||
|
||||
Berechnet den sauberen `BASE_URL`-Pfad basierend auf `.env.production` oder
|
||||
`.env.development → NEXT_PUBLIC_BASE_PATH`.
|
||||
`public/config.json → basePath`.
|
||||
Entfernt führende und abschließende Slashes.
|
||||
|
||||
## Beispiel
|
||||
|
||||
Wenn `NEXT_PUBLIC_BASE_PATH = "/talas5/"`, wird `BASE_URL = "/talas5"` gesetzt.
|
||||
Wenn `basePath = "/talas5/"` in config.json gesetzt ist, wird `BASE_URL = "/talas5"` verwendet.
|
||||
|
||||
```js
|
||||
const BASE_PATH = basePathRaw.replace(/^\/|\/$/g, "");
|
||||
|
||||
@@ -13,7 +13,7 @@ NodeMap verwendet Umgebungsvariablen zur Steuerung von API-Verhalten, Serverpfad
|
||||
## 🔧 Wichtige Variablen
|
||||
|
||||
| Variable | Beispielwert | Beschreibung |
|
||||
| --------------------------- | ------------------- | --------------------------------------------------------------------- |
|
||||
| --------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `DB_HOST` | `localhost` | Adresse des Datenbankservers (MySQL) |
|
||||
| `DB_PORT` | `3306` | Port für die Datenbankverbindung |
|
||||
| `DB_NAME` | `talas` | Datenbankname |
|
||||
@@ -21,7 +21,7 @@ NodeMap verwendet Umgebungsvariablen zur Steuerung von API-Verhalten, Serverpfad
|
||||
| `DB_PASSWORD` | `geheim` | Passwort für MySQL |
|
||||
| `NEXT_PUBLIC_API_PORT_MODE` | `prod` oder `dev` | Steuert API-Routing bei Services (z. B. Portwechsel für lokal) |
|
||||
| `NEXT_PUBLIC_USE_MOCKS` | `true` oder `false` | Aktiviert den Mockdaten-Modus über `/api/mocks/...` |
|
||||
| `NEXT_PUBLIC_BASE_PATH` | `/talas5` oder leer | Optionaler Pfad, falls App unter Subpfad läuft (z. B. IIS) |
|
||||
| `basePath` (in config.json) | `/talas5` oder leer | Optionaler Pfad, falls App unter Subpfad läuft (z. B. IIS). Wird jetzt in `public/config.json` gepflegt. |
|
||||
| `NEXT_PUBLIC_DEBUG` | `true` oder `false` | Aktiviert zusätzliche `console.log` Ausgaben für Debugging im Browser |
|
||||
|
||||
## 📦 Beispiel `.env.production`
|
||||
@@ -34,7 +34,11 @@ DB_USER=root
|
||||
DB_PASSWORD=geheim
|
||||
NEXT_PUBLIC_API_PORT_MODE=prod
|
||||
NEXT_PUBLIC_USE_MOCKS=false
|
||||
NEXT_PUBLIC_BASE_PATH=/talas5
|
||||
// public/config.json
|
||||
{
|
||||
...
|
||||
"basePath": "/talas5"
|
||||
}
|
||||
NEXT_PUBLIC_DEBUG=false
|
||||
```
|
||||
|
||||
@@ -48,7 +52,11 @@ DB_USER=root
|
||||
DB_PASSWORD=geheim
|
||||
NEXT_PUBLIC_API_PORT_MODE=dev
|
||||
NEXT_PUBLIC_USE_MOCKS=true
|
||||
NEXT_PUBLIC_BASE_PATH=/talas5
|
||||
// public/config.json
|
||||
{
|
||||
...
|
||||
"basePath": "/talas5"
|
||||
}
|
||||
NEXT_PUBLIC_DEBUG=true
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import L from "leaflet";
|
||||
import { getDebugLog } from "../utils/configUtils";
|
||||
|
||||
export class OverlappingMarkerSpiderfier {
|
||||
constructor(map, options = {}) {
|
||||
@@ -118,7 +119,7 @@ export class OverlappingMarkerSpiderfier {
|
||||
// 🔥 Künstliches Click-Event auslösen, um die UI zu aktualisieren
|
||||
setTimeout(() => {
|
||||
this.map.fire("click");
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("Click-Event ausgelöst in OverlappingMarkerspiderfier.js in unspiderfy ");
|
||||
}
|
||||
}, 10); // Kurze Verzögerung, um sicherzustellen, dass die UI neu gerendert wird
|
||||
|
||||
48
nssm.exe Installation.md
Normal file
@@ -0,0 +1,48 @@
|
||||
```markdown
|
||||
- Als Administrator Eingabeaufforderung oder PowerShell öffnen
|
||||
|
||||
- Navigiere zu dem NodeMap Projekt Verzeichnis:
|
||||
```shell
|
||||
C:\Users\Administrator>cd C:\inetpub\wwwroot\talas5\nodeMap
|
||||
```
|
||||
|
||||
- Befehl zum Erstellen eines Dienstes:
|
||||
Führen Sie den folgenden Befehl aus, um einen neuen Dienst zu erstellen:
|
||||
```shell
|
||||
nssm.exe install NodeMapService
|
||||
```
|
||||
Nachdem Sie diesen Befehl ausgeführt haben, öffnet sich ein NSSM-Dialogfenster.
|
||||
|
||||
**Dienstkonfiguration:**
|
||||
In dem geöffneten NSSM-Dialogfenster müssen Sie einige Parameter angeben:
|
||||
|
||||
- **Path:** Der Pfad zur ausführbaren Datei, die der Dienst ausführen soll.
|
||||
```shell
|
||||
C:\inetpub\wwwroot\talas5\nodeMap\StartNodeApp.bat
|
||||
```
|
||||
- **Startup directory:** Das Verzeichnis, in dem die Anwendung gestartet werden soll.
|
||||
```shell
|
||||
C:\inetpub\wwwroot\talas5\nodeMap
|
||||
```
|
||||
- **Arguments:** kann leer gelassen werden.
|
||||
|
||||
- Dienst starten:
|
||||
Sobald der Dienst erstellt wurde, können Sie ihn starten.
|
||||
Das können Sie entweder über die Eingabeaufforderung oder über die Diensteverwaltung von Windows tun.
|
||||
Um den Dienst über die Eingabeaufforderung zu starten, verwenden Sie den folgenden Befehl:
|
||||
```shell
|
||||
nssm.exe start DienstName
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
- **Dienst bearbeiten:**
|
||||
```shell
|
||||
nssm.exe edit NodeMapService
|
||||
```
|
||||
- **Dienst entfernen:**
|
||||
```shell
|
||||
nssm.exe remove NodeMapService confirm
|
||||
```
|
||||
dauert bis 1 Minute
|
||||
```
|
||||
2164
package-lock.json
generated
14
package.json
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "nodemap",
|
||||
"version": "1.1.300",
|
||||
"version": "1.1.388",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@heroicons/react": "^2.1.5",
|
||||
"@iconify/react": "^6.0.1",
|
||||
"@mui/icons-material": "^6.0.2",
|
||||
"@reduxjs/toolkit": "^2.5.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
@@ -39,17 +40,22 @@
|
||||
"scripts": {
|
||||
"dev": "node server.js",
|
||||
"build": "next build",
|
||||
"build:deploy": "next build && npm run create-zip",
|
||||
"create-zip": "powershell -ExecutionPolicy Bypass -File ./scripts/create-deployment-zip.ps1",
|
||||
"start": "cross-env NODE_ENV=production node server.js",
|
||||
"export": "next export",
|
||||
"test": "jest",
|
||||
"cypress": "cypress open",
|
||||
"cypress:run": "cypress run",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:slow": "cross-env PW_HEADED=1 PW_SLOWMO=1000 playwright test",
|
||||
"test:e2e:slow:ui": "cross-env PW_HEADED=1 PW_SLOWMO=1000 playwright test --ui",
|
||||
"test:e2e:report": "playwright show-report ./playwright/reports",
|
||||
"prepare": "husky",
|
||||
"bump-version": "node ./scripts/bumpVersion.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.54.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^13.17.0",
|
||||
"husky": "^9.1.7",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
// /pages/api/health.js
|
||||
export default async function handler(req, res) {
|
||||
const basePath = "talas5";
|
||||
const protocol = "http";
|
||||
const hostname = "10.10.0.70";
|
||||
const port = "80";
|
||||
const idMap = "12";
|
||||
const idUser = "484";
|
||||
const idLD = "50922";
|
||||
|
||||
const buildUrl = method =>
|
||||
`${protocol}://${hostname}:${port}/${basePath}/ClientData/WebServiceMap.asmx/${method}?idMap=${idMap}&idUser=${idUser}`;
|
||||
|
||||
const externalUrls = {
|
||||
GisStationsStaticDistrict: buildUrl("GisStationsStaticDistrict"),
|
||||
GisLinesStatus: `${protocol}://${hostname}:${port}/${basePath}/ClientData/WebServiceMap.asmx/GisLinesStatus?idMap=${idMap}`,
|
||||
GisStationsMeasurements: buildUrl("GisStationsMeasurements"),
|
||||
GisStationsStatusDistrict: buildUrl("GisStationsStatusDistrict"),
|
||||
GisSystemStatic: buildUrl("GisSystemStatic"),
|
||||
};
|
||||
|
||||
const internalApiBase = "http://localhost:3000";
|
||||
|
||||
const internalApis = {
|
||||
//area
|
||||
readArea: `${internalApiBase}/api/talas_v5_DB/area/readArea?m=${idMap}`,
|
||||
//device
|
||||
getAllStationsNames: `${internalApiBase}/api/talas_v5_DB/device/getAllStationsNames`,
|
||||
getDevices: `${internalApiBase}/api/talas_v5_DB/device/getDevices`,
|
||||
//gisLines
|
||||
readGisLines: `${internalApiBase}/api/talas_v5_DB/gisLines/readGisLines`,
|
||||
//locationDevice
|
||||
getDeviceId: `${internalApiBase}/api/talas_v5_DB/locationDevice/getDeviceId`,
|
||||
locationDeviceNameById: `${internalApiBase}/api/talas_v5_DB/locationDevice/locationDeviceNameById?idLD=${idLD}`,
|
||||
locationDevices: `${internalApiBase}/api/talas_v5_DB/locationDevice/locationDevices`,
|
||||
//pois
|
||||
addPoi: `${internalApiBase}/api/talas_v5_DB/pois/addPoi`,
|
||||
deletePoi: `${internalApiBase}/api/talas_v5_DB/pois/deletePoi`,
|
||||
getPoiById: `${internalApiBase}/api/talas_v5_DB/pois/getPoiById`,
|
||||
poiIcons: `${internalApiBase}/api/talas_v5_DB/pois/poi-icons`,
|
||||
readAllPOIs: `${internalApiBase}/api/talas_v5_DB/pois/readAllPOIs`,
|
||||
updateLocation: `${internalApiBase}/api/talas_v5_DB/pois/updateLocation`,
|
||||
updatePoi: `${internalApiBase}/api/talas_v5_DB/pois/updatePoi`,
|
||||
//poiTyp
|
||||
readPoiTyp: `${internalApiBase}/api/talas_v5_DB/poiTyp/readPoiTyp`,
|
||||
//station
|
||||
getAllStationsNames: `${internalApiBase}/api/talas_v5_DB/station/getAllStationsNames`,
|
||||
getDevices: `${internalApiBase}/api/talas_v5_DB/station/getDevices`,
|
||||
//
|
||||
priorityConfig: `${internalApiBase}/api/talas_v5_DB/priorityConfig`,
|
||||
};
|
||||
|
||||
const results = {};
|
||||
|
||||
// Prüfe externe Webservices
|
||||
await Promise.all(
|
||||
Object.entries(externalUrls).map(async ([name, url]) => {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
results[name] = {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
url,
|
||||
};
|
||||
|
||||
if (name === "GisSystemStatic" && response.ok) {
|
||||
const data = await response.json();
|
||||
results["UserRights"] = {
|
||||
ok: Array.isArray(data.Rights),
|
||||
length: data.Rights?.length || 0,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
results[name] = {
|
||||
ok: false,
|
||||
error: error.message,
|
||||
url,
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Prüfe interne API-Routen
|
||||
await Promise.all(
|
||||
Object.entries(internalApis).map(async ([name, url]) => {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
results[`API_${name}`] = {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
url,
|
||||
};
|
||||
} catch (error) {
|
||||
results[`API_${name}`] = {
|
||||
ok: false,
|
||||
error: error.message,
|
||||
url,
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Prüfe Mock-Status
|
||||
const useMocksEnv = process.env.NEXT_PUBLIC_USE_MOCKS;
|
||||
results["MockMode"] = {
|
||||
expected: "false",
|
||||
actual: useMocksEnv,
|
||||
ok: useMocksEnv === "false",
|
||||
info:
|
||||
useMocksEnv === "false"
|
||||
? "✅ Mockdaten deaktiviert – Live-Daten aktiv."
|
||||
: "⚠️ Mockdaten aktiv – nicht für Produktion geeignet!",
|
||||
};
|
||||
|
||||
// Prüfe Konfiguration der .env.production
|
||||
results["envConfig"] = {
|
||||
NODE_ENV: process.env.NODE_ENV || "undefined",
|
||||
DB_HOST: process.env.DB_HOST || "❌ fehlt",
|
||||
NEXT_PUBLIC_USE_MOCKS: useMocksEnv || "❌ fehlt",
|
||||
status:
|
||||
process.env.NODE_ENV === "production" && useMocksEnv === "false" && process.env.DB_HOST
|
||||
? "✅ .env.production scheint korrekt."
|
||||
: "⚠️ Bitte .env.production prüfen – möglicherweise fehlt etwas.",
|
||||
};
|
||||
|
||||
const allOk = Object.values(results).every(r => r.ok);
|
||||
|
||||
res.status(allOk ? 200 : 207).json({
|
||||
status: allOk ? "ok" : "partial",
|
||||
version: "1.0.0",
|
||||
services: results,
|
||||
});
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
// /pages/api/talas_v5_DB/area/updateArea.js
|
||||
import getPool from "../../../../utils/mysqlPool";
|
||||
import { getDebugLog } from "../../../../utils/configUtils";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("Request erhalten:", req.method, req.body); // Debugging
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// /pages/api/talas_v5_DB/gisLines/updateLineCoordinates.js
|
||||
import getPool from "../../../../utils/mysqlPool"; // Singleton-Pool importieren
|
||||
import { getDebugLog } from "../../../../utils/configUtils";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const pool = getPool(); // Singleton-Pool verwenden
|
||||
@@ -34,7 +35,7 @@ export default async function handler(req, res) {
|
||||
|
||||
// Commit der Transaktion
|
||||
await connection.commit();
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("Transaction Complete.");
|
||||
}
|
||||
res.status(200).json({
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// pages/api/talas_v5_DB/pois/addPoi.js
|
||||
import getPool from "../../../../utils/mysqlPool"; // Singleton-Pool importieren
|
||||
import { getDebugLog } from "../../../../utils/configUtils";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const pool = getPool(); // Singleton-Pool verwenden
|
||||
|
||||
if (req.method === "POST") {
|
||||
const { name, poiTypeId, latitude, longitude, idLD } = req.body;
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("Received data:", req.body); // Überprüfen der empfangenen Daten
|
||||
}
|
||||
|
||||
|
||||
18
pages/api/testDbConnection.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// pages/api/testDbConnection.js
|
||||
import getPool from "../../utils/mysqlPool";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const pool = getPool();
|
||||
let connection;
|
||||
try {
|
||||
connection = await pool.getConnection();
|
||||
const [rows] = await connection.query("SELECT 1 AS test");
|
||||
console.log("DB-Verbindung erfolgreich! Ergebnis:", rows);
|
||||
res.status(200).json({ success: true, result: rows });
|
||||
} catch (error) {
|
||||
console.error("DB-Verbindungsfehler:", error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
} finally {
|
||||
if (connection) connection.release();
|
||||
}
|
||||
}
|
||||
35
playwright.config.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// Playwright test configuration for the NodeMap project
|
||||
// Starts the local Next.js custom server (server.js) and runs tests against http://localhost:3000
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { defineConfig, devices } = require("@playwright/test");
|
||||
|
||||
module.exports = defineConfig({
|
||||
testDir: "./playwright/tests",
|
||||
timeout: 60_000,
|
||||
expect: { timeout: 10_000 },
|
||||
fullyParallel: true,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
// Reporters: keep console-friendly list and generate an HTML report under playwright/reports
|
||||
reporter: [["list"], ["html", { outputFolder: "playwright/reports", open: "never" }]],
|
||||
// Store any runner outputs (attachments, logs) under playwright/test-results
|
||||
outputDir: "playwright/test-results",
|
||||
use: {
|
||||
baseURL: "http://localhost:3000",
|
||||
// Disable artifact generation locally to avoid creating files
|
||||
trace: "off",
|
||||
video: "off",
|
||||
screenshot: "off",
|
||||
headless: process.env.PW_HEADED ? false : true,
|
||||
// Apply slow motion to all actions when PW_SLOWMO is set
|
||||
launchOptions: {
|
||||
slowMo: process.env.PW_SLOWMO ? Number(process.env.PW_SLOWMO) : 0,
|
||||
},
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
156
playwright/tests/mapcomponent.spec.js
Normal file
@@ -0,0 +1,156 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
// Helper: robust selection for native <select> or custom ARIA comboboxes
|
||||
async function selectStation(page, value) {
|
||||
// Try to find by accessible name first
|
||||
let combo = page.getByRole("combobox", { name: /Station wählen/i });
|
||||
if (!(await combo.count())) {
|
||||
// Fallback: find a container with the label text and locate a select inside
|
||||
const container = page.locator("div").filter({ hasText: "Station wählen" }).last();
|
||||
const selectInContainer = container.locator("select");
|
||||
if (await selectInContainer.count()) {
|
||||
combo = selectInContainer.first();
|
||||
} else {
|
||||
// Final fallback: first visible native select (overlay has only one)
|
||||
combo = page.locator("select:visible").first();
|
||||
}
|
||||
}
|
||||
await expect(combo).toBeVisible();
|
||||
|
||||
const isNative = await combo.evaluate(el => el.tagName === "SELECT");
|
||||
if (isNative) {
|
||||
await expect(combo).toBeEnabled();
|
||||
await expect(combo.locator(`option[value="${value}"]`)).toBeAttached();
|
||||
await combo.selectOption({ value });
|
||||
} else {
|
||||
await combo.click();
|
||||
await page.getByRole("option", { name: new RegExp(value) }).click();
|
||||
}
|
||||
}
|
||||
|
||||
test("MapComponent", async ({ page }) => {
|
||||
// Login auf 13.er TALAS
|
||||
await page.goto("http://10.10.0.13/talas5/login.aspx");
|
||||
await page.locator("#m_textboxUserName_I").click();
|
||||
await page.locator("#m_textboxUserName_I").fill("admin");
|
||||
await page.locator("#m_textboxUserName_I").press("Tab");
|
||||
await page.locator("#m_textboxPassword_I").fill("admin");
|
||||
await page.getByRole("cell", { name: "Anmelden Anmelden" }).locator("span").click();
|
||||
|
||||
// Set initial localStorage BEFORE navigation so the app reads them on load
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem("editMode", "false");
|
||||
localStorage.setItem("polylineVisible_m12_u484", "true");
|
||||
localStorage.setItem("currentMapId", "12");
|
||||
localStorage.setItem("currentUserId", "484");
|
||||
localStorage.setItem("mapZoom", "13");
|
||||
localStorage.setItem("kabelstreckenVisible", "false");
|
||||
localStorage.setItem("showBaseMapPanel", "false");
|
||||
localStorage.setItem(
|
||||
"mapLayersVisibility_m12_u484",
|
||||
JSON.stringify({
|
||||
"system-1": true,
|
||||
"system-2": false,
|
||||
"system-3": false,
|
||||
"system-5": false,
|
||||
"system-6": false,
|
||||
"system-7": false,
|
||||
"system-8": false,
|
||||
"system-9": false,
|
||||
"system-10": false,
|
||||
"system-11": false,
|
||||
"system-13": false,
|
||||
"system-30": false,
|
||||
"system-100": false,
|
||||
"system-110": false,
|
||||
"system-111": false,
|
||||
"system-200": false,
|
||||
})
|
||||
);
|
||||
localStorage.setItem("mapCenter", JSON.stringify([53.23938294961826, 8.21434020996094]));
|
||||
localStorage.setItem("markerLink", "undefined");
|
||||
localStorage.setItem("lastElementType", "marker");
|
||||
localStorage.setItem("polylineVisible", "false");
|
||||
localStorage.setItem("showAppInfoCard", "false");
|
||||
localStorage.setItem("showCoordinateInput", "false");
|
||||
localStorage.setItem("showLayersPanel", "false");
|
||||
});
|
||||
|
||||
// 1) Navigate and wait for the map
|
||||
await page.goto("http://localhost:3000/?m=12&u=484");
|
||||
await page.locator("#map").waitFor({ state: "visible", timeout: 20_000 });
|
||||
|
||||
// 2) Optional: verify a key from localStorage at runtime
|
||||
await expect(page.evaluate(() => localStorage.getItem("showLayersPanel"))).resolves.toBe("false");
|
||||
|
||||
// 3) Layer-Panel toggle: expect "einblenden" first (since showLayersPanel=false), then toggle
|
||||
await expect(page.getByRole("button", { name: "Layer-Panel einblenden" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Layer-Panel einblenden" }).click();
|
||||
await expect(page.getByRole("button", { name: "Layer-Panel ausblenden" })).toBeVisible();
|
||||
// 4) Collapse again to restore state
|
||||
await page.getByRole("button", { name: "Layer-Panel ausblenden" }).click();
|
||||
|
||||
// 5) Info-Card toggle: start hidden -> show -> hide -> show again
|
||||
await expect(page.getByRole("button", { name: "Info einblenden" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Info einblenden" }).click();
|
||||
await expect(page.getByRole("button", { name: "Info ausblenden" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Info ausblenden" }).click();
|
||||
await expect(page.getByRole("button", { name: "Info einblenden" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Info einblenden" }).click();
|
||||
await expect(page.locator("div").filter({ hasText: "TALAS.Map Version" }).nth(3)).toBeVisible();
|
||||
|
||||
// 6) Koordinatensuche toggle
|
||||
await page.getByRole("button", { name: "Koordinatensuche einblenden" }).click();
|
||||
await expect(page.locator("form")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Koordinatensuche ausblenden" }).click();
|
||||
|
||||
// 7) Marker setzen und Stationen wählen
|
||||
await page.getByLabel("Marker").click();
|
||||
// ...existing code...
|
||||
// ...existing code...
|
||||
await expect(page.getByText("Station wählen")).toBeVisible();
|
||||
const select = page.locator("select");
|
||||
await expect(select).toBeVisible();
|
||||
await expect(select).toBeEnabled();
|
||||
// Prüfe, ob die gewünschten Optionen existieren (attached)
|
||||
await expect(select.locator('option[value="50977"]')).toBeAttached();
|
||||
await expect(select.locator('option[value="50986"]')).toBeAttached();
|
||||
|
||||
// ----------------------------------------------
|
||||
await selectStation(page, "50977");
|
||||
await page.getByLabel("Marker").click();
|
||||
await selectStation(page, "50986");
|
||||
await page.getByLabel("Marker").click();
|
||||
await page.getByLabel("Marker").click();
|
||||
await page.getByLabel("Marker").click();
|
||||
await selectStation(page, "50977");
|
||||
await page.getByRole("button", { name: "Karte auf Standardansicht" }).click();
|
||||
//minusIcon
|
||||
await page.getByTestId("zoom-out").click();
|
||||
//wait 3 seconds
|
||||
// plusIcon
|
||||
await page.getByTestId("zoom-in").click(); //plus
|
||||
//--------------------------------------------
|
||||
// Prüfe Alarm-Icon
|
||||
|
||||
await page.goto("http://10.10.0.13/talas5/login.aspx");
|
||||
await page.locator("#m_textboxUserName_I").click();
|
||||
await page.locator("#m_textboxUserName_I").fill("admin");
|
||||
await page.locator("#m_textboxUserName_I").press("Tab");
|
||||
await page.locator("#m_textboxPassword_I").fill("admin");
|
||||
await page.getByRole("cell", { name: "Anmelden Anmelden" }).locator("span").click();
|
||||
console.log("Login auf 13.er TALAS erfolgreich");
|
||||
//warte 10 Sekunden
|
||||
await page.waitForTimeout(10000);
|
||||
await page.goto("http://localhost:3000/?m=12&u=484");
|
||||
const page1Promise = page.waitForEvent("popup");
|
||||
await page.getByLabel("Alarm aktiv").locator("path").click();
|
||||
const page1 = await page1Promise;
|
||||
await expect(
|
||||
page1.getByText("Standort Rastede > Bereich Littwin > TALAS CPL V3.5", { exact: true })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
/* Powershell Befehl ->das führt langsam aus mit 1 Sekunde Pause zwischen den Aktionen
|
||||
$env:PW_HEADED=1; $env:PW_SLOWMO=1000; npx playwright test
|
||||
*/
|
||||
22
public/config.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"_comment": [
|
||||
"tileSources: 'local' für offline, 'osm' für online",
|
||||
"center: Startmittelpunkt der Karte (lat, lng)",
|
||||
"zoomOutCenter: Zielkoordinaten für Herauszoomen (lat, lng)",
|
||||
"minZoom/maxZoom: erlaubte Zoomstufen pro Quelle"
|
||||
],
|
||||
|
||||
"center": [53.111111, 8.4625],
|
||||
"_comment_center": "Startmittelpunkt der Karte (lat, lng)",
|
||||
|
||||
"zoomOutCenter": [51.41321407879154, 7.739617925303934],
|
||||
"_comment_zoomOutCenter": "Zielkoordinaten für Herauszoomen (lat, lng)",
|
||||
|
||||
"basePath": "/talas5",
|
||||
"_comment_basePath": "Basis-URL für API und Routing",
|
||||
"minZoom": 5,
|
||||
"maxZoom": 20,
|
||||
"_comment_zoom": "Globale Zoom-Grenzen (min/max). Kann durch tileSources überschrieben werden.",
|
||||
"debugLog": false,
|
||||
"_comment_debugLog": "Debug-Logging für Client "
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 349 B |
BIN
public/img/icons/critical-marker-icon-17.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/img/icons/critical-marker-icon-18.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/img/icons/major-marker-icon-25.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/img/icons/major-marker-icon-26.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/img/icons/major-marker-icon-4.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/img/icons/major-marker-icon-5.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/img/icons/marker-icon-storage-upright.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/img/icons/marker-icon-thermo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
3
public/img/icons/material-symbols/alarm.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 396 B |
3
public/img/icons/material-symbols/edit-off.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 21l-3-3m-12.728-.364A9 9 0 015.636 5.636m0 0L3 3l3 3m9.364 9.364L18 21M5.636 5.636L3 3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 351 B |
3
public/img/icons/material-symbols/edit.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 432 B |
3
public/img/icons/material-symbols/info.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 333 B |
4
public/img/icons/material-symbols/map-marker.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="rgb(0, 174, 239)" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25s-7.5-4.108-7.5-11.25a7.5 7.5 0 1115 0z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 373 B |
3
public/img/icons/material-symbols/menu.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 231 B |
3
public/img/icons/material-symbols/search.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 261 B |
BIN
public/img/icons/minor-marker-icon-19.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/img/icons/minor-marker-icon-20.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/img/icons/system-marker-icon-15.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/img/icons/system-marker-icon-16.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
@@ -1,8 +1,39 @@
|
||||
// /redux/slices/database7polylines/polylineLayerVisibleSlice.js
|
||||
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
// Thunk to initialize polyline visibility from localStorage using mapId/userId from URL
|
||||
export const initializePolylineFromLocalStorageThunk = () => dispatch => {
|
||||
try {
|
||||
// Prüfe globales Nutzer-Flag
|
||||
if (typeof window !== "undefined" && window.userToggledPolyline) {
|
||||
console.log(
|
||||
"[Redux] Initialisierung abgebrochen: Nutzer hat Polyline bereits manuell geändert."
|
||||
);
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const mapId = params.get("m");
|
||||
const userId = params.get("u");
|
||||
if (mapId && userId) {
|
||||
const key = `polylineVisible_m${mapId}_u${userId}`;
|
||||
const stored = localStorage.getItem(key);
|
||||
const visible = stored === "true";
|
||||
dispatch(initializePolylineFromLocalStorage(visible));
|
||||
// Optional: log for debugging
|
||||
console.log(
|
||||
`Redux: Initialized polyline visibility from localStorage key '${key}':`,
|
||||
visible
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error initializing polyline visibility from localStorage:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
visible: false, // oder Standardwert
|
||||
visible: false, // Standardwert - wird in der Komponente aus localStorage überschrieben
|
||||
isInitialized: false, // Flag um zu verfolgen, ob der Wert aus localStorage geladen wurde
|
||||
};
|
||||
|
||||
const polylineLayerVisibleSlice = createSlice({
|
||||
@@ -11,11 +42,51 @@ const polylineLayerVisibleSlice = createSlice({
|
||||
reducers: {
|
||||
setPolylineVisible: (state, action) => {
|
||||
state.visible = action.payload;
|
||||
localStorage.setItem("polylineVisible", action.payload);
|
||||
state.isInitialized = true;
|
||||
// Save to localStorage using mapId/userId key
|
||||
try {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const mapId = params.get("m");
|
||||
const userId = params.get("u");
|
||||
if (mapId && userId) {
|
||||
const key = `polylineVisible_m${mapId}_u${userId}`;
|
||||
localStorage.setItem(key, action.payload.toString());
|
||||
console.log(
|
||||
"[Redux/setPolylineVisible] payload:",
|
||||
action.payload,
|
||||
"key:",
|
||||
key,
|
||||
"localStorage:",
|
||||
localStorage.getItem(key)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// fallback: do nothing
|
||||
}
|
||||
console.log("💾 Redux: setPolylineVisible called with:", action.payload);
|
||||
},
|
||||
initializePolylineFromLocalStorage: (state, action) => {
|
||||
// Diese Action wird nur beim Initialisieren aus localStorage verwendet
|
||||
if (typeof window !== "undefined" && window.userToggledPolyline) {
|
||||
console.log(
|
||||
"[Redux] Initialisierung im Reducer abgebrochen: Nutzer hat Polyline bereits manuell geändert."
|
||||
);
|
||||
return;
|
||||
}
|
||||
state.visible = action.payload;
|
||||
state.isInitialized = true;
|
||||
console.log(
|
||||
"🔧 Redux: initializePolylineFromLocalStorage called with:",
|
||||
action.payload,
|
||||
"visible:",
|
||||
state.visible
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setPolylineVisible } = polylineLayerVisibleSlice.actions;
|
||||
export const selectPolylineVisible = (state) => state.polylineLayerVisible.visible;
|
||||
export const { setPolylineVisible, initializePolylineFromLocalStorage } =
|
||||
polylineLayerVisibleSlice.actions;
|
||||
export const selectPolylineVisible = state => state.polylineLayerVisible.visible;
|
||||
export const selectPolylineInitialized = state => state.polylineLayerVisible.isInitialized;
|
||||
export default polylineLayerVisibleSlice.reducer;
|
||||
|
||||
@@ -15,20 +15,68 @@ const mapLayersSlice = createSlice({
|
||||
},
|
||||
setLayerVisibility: (state, action) => {
|
||||
const { layer, visibility } = action.payload;
|
||||
if (state[layer] !== undefined) {
|
||||
state[layer] = visibility;
|
||||
}
|
||||
state[layer] = visibility; // Sicher setzen
|
||||
},
|
||||
setInitialLayers: (state, action) => {
|
||||
const systems = action.payload; // Array of GisSystem
|
||||
systems.forEach((system) => {
|
||||
|
||||
const mapId =
|
||||
typeof localStorage !== "undefined" ? localStorage.getItem("currentMapId") : null;
|
||||
const userId =
|
||||
typeof localStorage !== "undefined" ? localStorage.getItem("currentUserId") : null;
|
||||
const mapStorageKey =
|
||||
mapId && userId ? `mapLayersVisibility_m${mapId}_u${userId}` : "mapLayersVisibility";
|
||||
|
||||
let existingVisibility = {};
|
||||
if (typeof localStorage !== "undefined") {
|
||||
try {
|
||||
const stored = localStorage.getItem(mapStorageKey);
|
||||
if (stored) {
|
||||
existingVisibility = JSON.parse(stored) || {};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading stored visibility:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Baue ein Set der gültigen Keys (nur Systeme mit Map===1)
|
||||
const validKeys = new Set();
|
||||
|
||||
systems.forEach(system => {
|
||||
if (system.Map !== 1) return; // Komplett überspringen, wenn Map==0
|
||||
const key = `system-${system.IdSystem}`;
|
||||
state[key] = true; // oder false, je nach Default
|
||||
validKeys.add(key);
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(existingVisibility, key)) {
|
||||
state[key] = existingVisibility[key];
|
||||
} else {
|
||||
state[key] = system.Allow === 1 && system.Map === 1; // Allow + Map Bedingung
|
||||
}
|
||||
});
|
||||
|
||||
// Entferne aus dem State alle Keys, die nicht mehr gültig sind (Map wurde evtl. auf 0 gesetzt)
|
||||
Object.keys(state).forEach(k => {
|
||||
if (!validKeys.has(k)) {
|
||||
delete state[k];
|
||||
}
|
||||
});
|
||||
|
||||
// Bereinige auch den gespeicherten localStorage-Eintrag
|
||||
if (typeof localStorage !== "undefined") {
|
||||
const cleaned = {};
|
||||
Object.keys(state).forEach(k => {
|
||||
cleaned[k] = state[k];
|
||||
});
|
||||
try {
|
||||
localStorage.setItem(mapStorageKey, JSON.stringify(cleaned));
|
||||
} catch (e) {
|
||||
console.warn("Could not persist cleaned map layer visibility", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { toggleLayer, setLayerVisibility, setInitialLayers } = mapLayersSlice.actions;
|
||||
export const selectMapLayersState = (state) => state.mapLayers || initialState;
|
||||
export const selectMapLayersState = state => state.mapLayers || initialState;
|
||||
export default mapLayersSlice.reducer;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites name="jest tests" tests="0" failures="0" errors="0" time="4.822">
|
||||
</testsuites>
|
||||
105
scripts/create-deployment-zip.ps1
Normal file
@@ -0,0 +1,105 @@
|
||||
# PowerShell Script to create deployment ZIP after successful build
|
||||
param(
|
||||
[string]$ProjectRoot = $PSScriptRoot + "\.."
|
||||
)
|
||||
|
||||
# Read package.json to get name and version
|
||||
$packageJsonPath = Join-Path $ProjectRoot "package.json"
|
||||
$packageJson = Get-Content $packageJsonPath | ConvertFrom-Json
|
||||
$projectName = $packageJson.name
|
||||
$version = $packageJson.version
|
||||
|
||||
# Create ZIP filename
|
||||
$zipFileName = "${projectName}-${version}.zip"
|
||||
$zipPath = Join-Path $ProjectRoot $zipFileName
|
||||
|
||||
# Remove existing ZIP if it exists
|
||||
if (Test-Path $zipPath) {
|
||||
Remove-Item $zipPath -Force
|
||||
Write-Host "[DELETE] Removed existing ZIP: $zipFileName" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Define files and directories to include
|
||||
$itemsToInclude = @(
|
||||
".next",
|
||||
"public",
|
||||
".env.production",
|
||||
"server.js",
|
||||
"nssm.exe",
|
||||
"nssm.exe Installation.md",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"Start-Dev.ps1",
|
||||
"StartNodeApp.bat"
|
||||
)
|
||||
|
||||
# Check which items exist
|
||||
$existingItems = @()
|
||||
foreach ($item in $itemsToInclude) {
|
||||
$itemPath = Join-Path $ProjectRoot $item
|
||||
if (Test-Path $itemPath) {
|
||||
$existingItems += $item
|
||||
Write-Host "[FOUND] $item" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[MISSING] $item" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
if ($existingItems.Count -eq 0) {
|
||||
Write-Host "[ERROR] No items found to include in ZIP!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Create temporary directory for ZIP contents
|
||||
$tempDir = Join-Path $ProjectRoot "temp-deployment"
|
||||
if (Test-Path $tempDir) {
|
||||
Remove-Item $tempDir -Recurse -Force
|
||||
}
|
||||
New-Item -ItemType Directory -Path $tempDir | Out-Null
|
||||
|
||||
# Copy items to temporary directory
|
||||
foreach ($item in $existingItems) {
|
||||
$sourcePath = Join-Path $ProjectRoot $item
|
||||
$destPath = Join-Path $tempDir $item
|
||||
|
||||
if (Test-Path $sourcePath -PathType Container) {
|
||||
# It's a directory
|
||||
Copy-Item $sourcePath $destPath -Recurse -Force
|
||||
Write-Host "[COPY DIR] $item" -ForegroundColor Cyan
|
||||
} else {
|
||||
# It's a file
|
||||
Copy-Item $sourcePath $destPath -Force
|
||||
Write-Host "[COPY FILE] $item" -ForegroundColor Cyan
|
||||
}
|
||||
}
|
||||
|
||||
# Create ZIP file
|
||||
try {
|
||||
Write-Host "[CREATE] Creating ZIP: $zipFileName..." -ForegroundColor Blue
|
||||
|
||||
# Use .NET compression to create ZIP
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
[System.IO.Compression.ZipFile]::CreateFromDirectory($tempDir, $zipPath)
|
||||
|
||||
# Get ZIP file size
|
||||
$zipSize = (Get-Item $zipPath).Length
|
||||
$zipSizeMB = [math]::Round($zipSize / 1MB, 2)
|
||||
|
||||
Write-Host "[SUCCESS] Successfully created deployment ZIP!" -ForegroundColor Green
|
||||
Write-Host "[FILE] $zipFileName" -ForegroundColor White
|
||||
Write-Host "[SIZE] $zipSizeMB MB" -ForegroundColor White
|
||||
Write-Host "[LOCATION] $zipPath" -ForegroundColor White
|
||||
|
||||
} catch {
|
||||
Write-Host "[ERROR] Failed to create ZIP: $($_.Exception.Message)" -ForegroundColor Red
|
||||
exit 1
|
||||
} finally {
|
||||
# Clean up temporary directory
|
||||
if (Test-Path $tempDir) {
|
||||
Remove-Item $tempDir -Recurse -Force
|
||||
Write-Host "[CLEANUP] Cleaned up temporary files" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[COMPLETE] Deployment ZIP ready for production deployment!" -ForegroundColor Green
|
||||
64
server.js
@@ -27,16 +27,46 @@ const extractData = (json, name) => {
|
||||
};
|
||||
|
||||
app.prepare().then(() => {
|
||||
const server = createServer((req, res) => {
|
||||
const express = require("express");
|
||||
const expressApp = express();
|
||||
// Proxy-Route für Karten-Tiles
|
||||
expressApp.get("/tiles/:z/:x/:y.png", async (req, res) => {
|
||||
const { z, x, y } = req.params;
|
||||
// OSM-Subdomain (a, b, c) zufällig wählen
|
||||
const subdomains = ["a", "b", "c"];
|
||||
const s = subdomains[Math.floor(Math.random() * subdomains.length)];
|
||||
const tileUrl = `https://${s}.tile.openstreetmap.org/${z}/${x}/${y}.png`;
|
||||
try {
|
||||
const response = await fetch(tileUrl);
|
||||
if (!response.ok) {
|
||||
res.status(response.status).send("Tile not found");
|
||||
return;
|
||||
}
|
||||
res.set("Content-Type", "image/png");
|
||||
response.body.pipe(res);
|
||||
} catch (err) {
|
||||
res.status(500).send("Error fetching tile");
|
||||
}
|
||||
});
|
||||
|
||||
// Alle anderen Routen an Next.js
|
||||
expressApp.all("*", (req, res) => {
|
||||
handle(req, res);
|
||||
});
|
||||
|
||||
const server = createServer(expressApp);
|
||||
|
||||
const io = new Server(server);
|
||||
|
||||
// ✅ Globaler Cache für alle aktuellen Daten
|
||||
const globalDataCache = new Map();
|
||||
|
||||
io.on("connection", socket => {
|
||||
const { m: idMap, u: idUser } = socket.handshake.query;
|
||||
console.log(`🔌 WebSocket verbunden (idMap=${idMap}, idUser=${idUser})`);
|
||||
|
||||
const cacheKey = `${idMap}_${idUser}`;
|
||||
|
||||
const endpoints = [
|
||||
{
|
||||
name: "GisLinesStatus",
|
||||
@@ -69,6 +99,22 @@ app.prepare().then(() => {
|
||||
|
||||
const lastDataMap = {};
|
||||
|
||||
// ✅ Funktion um sofort alle verfügbaren Daten zu senden (für Browser-Reload)
|
||||
const sendAllCurrentData = () => {
|
||||
const cachedData = globalDataCache.get(cacheKey);
|
||||
if (cachedData && Object.keys(cachedData).length > 0) {
|
||||
console.log(
|
||||
`📦 Sending all current data to client (${Object.keys(cachedData).length} endpoints)`
|
||||
);
|
||||
Object.entries(cachedData).forEach(([name, data]) => {
|
||||
socket.emit(`${name}Updated`, data);
|
||||
console.log(`🔄 Browser-Reload: ${name} data sent`);
|
||||
});
|
||||
} else {
|
||||
console.log(`📭 No cached data available for ${cacheKey}, will fetch fresh data`);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
for (const { name, getUrl, mock } of endpoints) {
|
||||
try {
|
||||
@@ -99,6 +145,14 @@ app.prepare().then(() => {
|
||||
}
|
||||
|
||||
const newDataStr = JSON.stringify(statis);
|
||||
|
||||
// ✅ Cache aktualisieren
|
||||
if (!globalDataCache.has(cacheKey)) {
|
||||
globalDataCache.set(cacheKey, {});
|
||||
}
|
||||
globalDataCache.get(cacheKey)[name] = statis;
|
||||
|
||||
// ✅ Nur bei Änderungen senden (setInterval-Logik)
|
||||
if (newDataStr !== lastDataMap[name]) {
|
||||
lastDataMap[name] = newDataStr;
|
||||
socket.emit(`${name}Updated`, statis);
|
||||
@@ -113,9 +167,15 @@ app.prepare().then(() => {
|
||||
}
|
||||
};
|
||||
|
||||
// fetchData immer ausführen – unabhängig vom Modus
|
||||
// ✅ Beim Connect: Sofort alle aktuellen Daten senden (für Browser-Reload)
|
||||
sendAllCurrentData();
|
||||
|
||||
// ✅ Dann erste Datenabfrage durchführen
|
||||
fetchData();
|
||||
|
||||
// ✅ setInterval für regelmäßige Updates (nur bei Änderungen)
|
||||
const interval = setInterval(fetchData, 5000); // 5 Sekunden ,TALAS.web nutzt 12 Sekunden
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
clearInterval(interval);
|
||||
console.log("❌ WebSocket getrennt");
|
||||
|
||||
0
services/localStorage/stationCacheService.js
Normal file
@@ -1,12 +1,23 @@
|
||||
import { getDebugLog } from "../../utils/configUtils";
|
||||
// /services/webservice/fetchGisLinesStatusService.js
|
||||
let __configCache;
|
||||
async function getConfig() {
|
||||
if (__configCache) return __configCache;
|
||||
const res = await fetch("/config.json");
|
||||
if (!res.ok) throw new Error("config.json konnte nicht geladen werden");
|
||||
__configCache = await res.json();
|
||||
return __configCache;
|
||||
}
|
||||
|
||||
export const fetchGisLinesStatusService = async () => {
|
||||
const useMocks = process.env.NEXT_PUBLIC_USE_MOCKS === "true";
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
|
||||
const config = await getConfig();
|
||||
const basePath = config.basePath || "";
|
||||
|
||||
if (useMocks) {
|
||||
const mockBasePath = "/api/mocks/webservice/gisLinesStatus";
|
||||
const mockURL = `${window.location.origin}${mockBasePath}`;
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("🧪 Mock-Modus aktiviert: fetchGisLinesStatusService ", mockURL);
|
||||
}
|
||||
|
||||
@@ -16,11 +27,9 @@ export const fetchGisLinesStatusService = async () => {
|
||||
}
|
||||
|
||||
const mockData = await response.json();
|
||||
|
||||
if (!Array.isArray(mockData.Statis)) {
|
||||
throw new Error("Ungültige Struktur: 'Status' fehlt im Mock");
|
||||
}
|
||||
|
||||
return mockData.Statis;
|
||||
} else {
|
||||
const baseUrl = `${window.location.protocol}//${window.location.hostname}:80${basePath}/ClientData/WebServiceMap.asmx`;
|
||||
@@ -29,7 +38,7 @@ export const fetchGisLinesStatusService = async () => {
|
||||
const idMap = params.get("m");
|
||||
|
||||
const url = `${baseUrl}/GisLinesStatus?idMap=${idMap}`;
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("📡 fetchGisLinesStatusService URL:", url);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import { getDebugLog } from "../../utils/configUtils";
|
||||
let __configCache;
|
||||
async function getConfig() {
|
||||
if (__configCache) return __configCache;
|
||||
const res = await fetch("/config.json");
|
||||
if (!res.ok) throw new Error("config.json konnte nicht geladen werden");
|
||||
__configCache = await res.json();
|
||||
return __configCache;
|
||||
}
|
||||
|
||||
export const fetchGisStationsMeasurementsService = async () => {
|
||||
const useMocks = process.env.NEXT_PUBLIC_USE_MOCKS === "true";
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
|
||||
const config = await getConfig();
|
||||
const basePath = config.basePath || "";
|
||||
|
||||
if (useMocks) {
|
||||
const mockBasePath = "/api/mocks/webservice/gisStationsMeasurements";
|
||||
const mockURL = `${window.location.origin}${mockBasePath}`;
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("🧪 Mock-Modus aktiviert: fetchGisStationsMeasurementsService ", mockURL);
|
||||
}
|
||||
|
||||
@@ -28,7 +39,7 @@ export const fetchGisStationsMeasurementsService = async () => {
|
||||
const idUser = params.get("u");
|
||||
|
||||
const url = `${baseUrl}/GisStationsMeasurements?idMap=${idMap}&idUser=${idUser}`;
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("📡 fetchGisStationsMeasurementsService URL:", url);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,18 @@
|
||||
* @returns {Promise<Array>} Liste mit Points[]
|
||||
* @throws {Error} bei Fehler oder ungültiger Antwortstruktur
|
||||
*/
|
||||
|
||||
import { getDebugLog, getConfig } from "../../utils/configUtils";
|
||||
|
||||
export const fetchGisStationsStaticDistrictService = async () => {
|
||||
const useMocks = process.env.NEXT_PUBLIC_USE_MOCKS === "true";
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
|
||||
const config = await getConfig();
|
||||
const basePath = config.basePath || "";
|
||||
|
||||
if (useMocks) {
|
||||
const mockBasePath = "/api/mocks/webservice/gisStationsStaticDistrict";
|
||||
const mockURL = `${window.location.origin}${mockBasePath}`;
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("🧪 Mock-Modus aktiviert: fetchGisStationsStaticDistrictService ", mockURL);
|
||||
}
|
||||
|
||||
@@ -35,7 +39,7 @@ export const fetchGisStationsStaticDistrictService = async () => {
|
||||
const idUser = params.get("u");
|
||||
|
||||
const url = `${baseUrl}/GisStationsStaticDistrict?idMap=${idMap}&idUser=${idUser}`;
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("📡 fetchGisStationsStaticDistrictService URL:", url);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,18 @@
|
||||
* @returns {Promise<Array>} Liste mit Statis[]
|
||||
* @throws {Error} bei Fehler oder ungültiger Antwortstruktur
|
||||
*/
|
||||
|
||||
import { getDebugLog, getConfig } from "../../utils/configUtils";
|
||||
|
||||
export const fetchGisStationsStatusDistrictService = async () => {
|
||||
const useMocks = process.env.NEXT_PUBLIC_USE_MOCKS === "true";
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
|
||||
const config = await getConfig();
|
||||
const basePath = config.basePath || "";
|
||||
|
||||
if (useMocks) {
|
||||
const mockBasePath = "/api/mocks/webservice/gisStationsStatusDistrict";
|
||||
const mockURL = `${window.location.origin}${mockBasePath}`;
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("🧪 Mock-Modus aktiviert: fetchGisStationsStatusDistrictService ", mockURL);
|
||||
}
|
||||
|
||||
@@ -35,7 +39,7 @@ export const fetchGisStationsStatusDistrictService = async () => {
|
||||
const idUser = params.get("u");
|
||||
|
||||
const url = `${baseUrl}/GisStationsStatusDistrict?idMap=${idMap}&idUser=${idUser}`;
|
||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
||||
if (getDebugLog()) {
|
||||
console.log("📡 fetchGisStationsStatusDistrictService URL:", url);
|
||||
}
|
||||
|
||||
|
||||