96 Commits

Author SHA1 Message Date
ISA
f390f93293 fix: Dynamische Link nach Host für Alarm Icon 2025-09-19 13:08:48 +02:00
ISA
28dcb284bf fix: Dynamiche Link je nach Host 2025-09-19 13:04:13 +02:00
ISA
4f0527e8a9 fix: Die Logik für die Alarmanzeige wurde angepasst: Jetzt wird das Alarm-Icon nur angezeigt, wenn ein aktiver Alarm existiert und für diesen auch ein passender Link im StaticDistrict vorhanden ist – unabhängig von der Reihenfolge. Damit wird der Bug behoben, dass das Icon manchmal nicht erscheint, obwohl ein Alarm mit Link existiert. 2025-09-19 12:29:33 +02:00
ISA
3d0ce4a2b4 docs: md 2 html for confluence 2025-09-19 11:48:59 +02:00
ISA
76280b365b test: Der Test ist jetzt erfolgreich durchgelaufen. Die Toleranz und das Logging funktionieren wie gewünscht – kleine Abweichungen bei mapCenter sind jetzt kein Problem mehr. 2025-09-18 06:31:55 +02:00
ISA
c112ec2da4 test: Layers visibility 2025-09-17 14:25:36 +02:00
ISA
7faee5fd79 style: "Zu Marker zoomen" remove hover Button , only littwin-blue 2025-09-17 14:00:27 +02:00
ISA
e52b0cc520 test: playwright test passed 2025-09-17 13:44:20 +02:00
ISA
4a42c428f0 style: Alarm UI Widget 2025-09-17 13:26:02 +02:00
ISA
1d3d04d49c style: Alarm Ui Widget 2025-09-17 12:59:37 +02:00
ISA
dd9980409c test: Test pass 2025-09-17 12:13:03 +02:00
ISA
ea6d71a4f5 feat: Alarm UI Widget 2025-09-17 09:16:04 +02:00
ISA
13ca1cece0 feat: Die Alarmanzeige ist jetzt als eigene Komponente (AlarmIndicator.js) im Verzeichnis uiWidgets erstellt und in MapComponent.js eingebunden.
Wenn ein Alarm mit AlarmLink vorhanden ist, wird das Alarm-Icon angezeigt und öffnet beim Klick den Link in einem neuen Tab.
2025-09-17 07:44:49 +02:00
ISA
f22bb4b232 chore: UI Widget Alarm Link in GisStationsStatusDistrict.json eingefügt 2025-09-17 07:33:21 +02:00
ISA
bfd091b1b1 test: slow Motion 2025-09-16 16:38:14 +02:00
ISA
81b6379895 test(e2e): Playwright tests passing 2025-09-16 15:50:53 +02:00
ISA
42ca88d27e chore: playwright ohne webserver 2025-09-16 14:27:53 +02:00
ISA
fdb70d892c chore: move playwright test and reports in playwright folder 2025-09-16 14:18:50 +02:00
ISA
73e9c63e36 chore: move report into playwright 2025-09-16 14:00:05 +02:00
ISA
e520207526 feat: Plus und Minus Icons 2025-09-16 13:47:11 +02:00
ISA
2e5acf9327 feat: Plus und Minus Icons 2025-09-16 13:32:22 +02:00
ISA
cdfdd3d6cf chore: gitignore playwright Artefakte ignorieren und nur Test Datei annehmen 2025-09-16 12:28:10 +02:00
ISA
5b86d5293b feat: Plus und Minus Zoom Icons 2025-09-16 12:13:49 +02:00
ISA
31c770f778 feat: Plus und Minus Zoom Icons 2025-09-16 12:12:31 +02:00
ISA
051dd4c306 chore: maxZoom = 20; 2025-09-16 11:56:33 +02:00
ISA
995f084e15 chore: maxZoom = 20; 2025-09-16 11:55:41 +02:00
ISA
eaacec71da chore: test 2025-09-16 11:47:04 +02:00
ISA
6bc2e16657 style: alle Icons Panels in gleiche Position bringen 2025-09-16 11:20:49 +02:00
ISA
1208024f76 chore: alle Panels zu den selben Position bringen 2025-09-16 10:57:23 +02:00
ISA
369f29a769 feat(ui): add AreaDropdown and exclusive toggle with layers panel
New AreaDropdown component for quick station selection (filters by allowed systems, ESC to close)
MapComponent: toggle AreaDropdown via MapMarkerIcon; auto-hide MapLayersControlPanel when dropdown is open and vice versa
fix(alarms): hasActiveAlarm now checks Statis[].Alarm for both array and object shapes
fix(panel): Kabelstrecken now auto-enables TALAS (system-1) when turned on; keeps behavior to disable polylines when TALAS is unchecked; persists visibility to localStorage and emits visibilityChanged
Minor: imports, state wiring, and render guards updated
Affected files:

MapComponent.js
MapLayersControlPanel.js
AreaDropdown.js (new)
2025-09-15 13:53:16 +02:00
ISA
d166b2468d feat: AreaDropdown separate from MapLayerControlPanel 2025-09-15 13:38:19 +02:00
ISA
59c8680c23 feat: AlarmIcon nur bei GisStationsStatusDistrict Attribute Alarm :1 2025-09-15 13:03:54 +02:00
ISA
1a046f8212 style: Icon as components and littwin-blue 2025-09-15 11:52:20 +02:00
ISA
e35216daf5 chore: change icons order 2025-09-15 10:47:25 +02:00
ISA
91ad47166f del: BasMapPanel entfernt aus rechliche Gründe,
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/
2025-09-15 10:36:03 +02:00
ISA
3a9b436352 feat: Icons 2025-09-12 15:59:44 +02:00
ISA
7b881e80c2 link Ebenen 2025-09-12 15:18:55 +02:00
ISA
cc19a0a466 feat: hamburger menu und info Icons 2025-09-12 14:55:19 +02:00
ISA
f200d0bb20 chore(husky): remove deprecated v9 bootstrap lines from pre-commit 2025-09-12 13:58:44 +02:00
ISA
75a0ab000f chore(Websocket): Websocket dump refresh 2025-09-12 13:58:13 +02:00
ISA
4d2a94ffea chore(mocks): sync all mock JSON with WebService (10.10.0.13) 2025-09-12 13:23:26 +02:00
ISA
239ad82e46 fix(dev): add missing Map flags in GisSystemStatic to match production
Add/restore Map: 1 attributes in GisSystemStatic (dev)
Ensures mapLayers initialization creates visibility keys
Fixes missing area markers and layer control state in dev
Behavior now consistent with production
2025-09-12 13:09:21 +02:00
ISA
a2d3338624 fix: kein DB Verbindung von der Anwendung
Der Fehler war, dass im Code die Funktion getDebugLog() verwendet wurde, die nicht definiert war.
Dadurch ist beim Erstellen des Datenbank-Pools ein Fehler aufgetreten, bevor überhaupt eine Verbindung zur Datenbank aufgebaut werden konnte.

Erst nachdem die Debug-Logik entfernt wurde, konnte die Verbindung erfolgreich hergestellt werden.
Das Problem lag also nicht an der Datenbank oder an den Zugangsdaten, sondern an einem fehlenden bzw. nicht importierten Hilfsfunktion im Pool-Code.
2025-09-12 12:19:51 +02:00
ISA
598acb8441 doc: TODO Icons 2025-09-12 09:33:58 +02:00
ISA
f8512c485e doc: TODO 2025-09-12 09:26:59 +02:00
ISA
fff2754b14 fix: fehlende Icons 2025-09-10 10:23:43 +02:00
ISA
61ed542ea4 feat: osm von server als proxy für den client 2025-09-09 16:11:23 +02:00
ISA
4befddd440 fix: Station öffnen in Tab 2025-08-26 11:33:24 +02:00
ISA
5b7868145c add: Leaflet | © OpenStreetMap contributors 2025-08-25 06:44:37 +02:00
ISA
0a3c4c208f fix: Leistung etwas verbessert wegen Kabelstrecken anzeigen und ausblenden 2025-08-22 14:44:54 +02:00
ISA
8cf520bb2c Fix: Kabelstrecken-Checkbox überschreibt Nutzeraktion nach Initialisierung nicht mehr
- Nutzeraktion (Deaktivieren der Kabelstrecken) wird jetzt durch Initialisierung nicht mehr überschrieben
- Initialisierung prüft, ob der Nutzer die Checkbox bereits betätigt hat
- Verhindert, dass Kabelstrecken nach dem Laden unerwartet
2025-08-22 12:28:40 +02:00
ISA
3896381a8f Debug-Logging zentralisiert: Nutzung von process.env.NEXT_PUBLIC_DEBUG_LOG entfernt und auf getDebugLog() mit config.json umgestellt
- Alle Vorkommen von process.env.NEXT_PUBLIC_DEBUG_LOG entfernt
- Debug-Konfiguration erfolgt jetzt ausschließlich über public/config.json
- getDebugLog()-Utility überall verwendet
- .env-Dateien werden für Debug-Logging nicht mehr benötigt
- Alle betroffenen Komponenten, Services und API
2025-08-22 11:10:40 +02:00
ISA
a013c07394 fix(map): entferne Start-, End- und Stützpunkt-Icons sofort beim Ausblenden der Kabelstrecken
- Alle Marker mit StartIcon, EndIcon und CircleIcon werden jetzt direkt entfernt, wenn die Kabelstrecken-Checkbox deaktiviert wird
- Kein Browser-Reload mehr nötig, Icons verschwinden sofort von der Karte
2025-08-22 10:12:03 +02:00
ISA
f2a322a91b fix: Kabelstrecken werden ausgeblendet 2025-08-22 09:46:31 +02:00
ISA
bf7b62d110 del: [DeviceLayers] 2025-08-21 17:10:32 +02:00
ISA
c44a755077 WIP: Kabelstrecken 2025-08-21 15:34:52 +02:00
ISA
aea439f135 WIP: Kabelstrecken 2025-08-21 15:12:25 +02:00
ISA
c6871692aa WIP: Kabelstrecken localStorage polylineVisible_m12_u484 Wert bleibt stabil aber wenn true ist un dich die Seite neu ladee dann werden die Kabelstrecken nicht mehr ausgeblendet 2025-08-21 14:31:55 +02:00
ISA
e7192a7623 WIP: Kabelstrecken wird deaktiviert beim neuladen der Seite 2025-08-21 14:00:09 +02:00
ISA
f11f64d4d7 WIP: erster aufruf beim wechesln der Karten 2025-08-21 11:06:27 +02:00
ISA
da21cba186 Die ursprüngliche Logik für die Abhängigkeit und Synchronisation der Checkboxen (TALAS/Kabelstrecken) mit localStorage, Redux und lokalem State wurde wiederhergestellt. 2025-08-21 10:03:47 +02:00
ISA
d179c152c0 fix: Die Anwendung verwendet jetzt ausschließlich den Redux-Slice (polylineLayerVisible.visible) für die Sichtbarkeit der Kabelstrecken (Polylines). Die Checkbox und die Anzeige der Linien sind damit immer synchron und reaktiv – unabhängig von localStorage. 2025-08-21 09:52:22 +02:00
ISA
2da79c9318 WIP. polylines visiblity 2025-08-21 09:47:03 +02:00
ISA
2066cbb9e8 fix: Area-Marker (Bereiche) können jetzt nur noch verschoben werden, wenn in localStorage der Wert "editMode" auf "true" gesetzt ist. Andernfalls sind sie nicht verschiebbar. 2025-08-21 08:23:36 +02:00
ISA
c8a14ee873 Fix: Polyline-Layer-Visibility reagiert jetzt zuverlässig auf Redux-State
- setupPolylines wird im useEffect asynchron aufgerufen
- Marker und Polylinien werden erst nach Abschluss der async-Operation gesetzt
- Sichtbarkeit der Kabelstrecken (Kabel-Layer) wird korrekt auf der Karte
2025-08-20 15:52:56 +02:00
ISA
44b29469b9 WIP: polyline Slice visiblity 2025-08-20 15:36:34 +02:00
ISA
ee5319a928 fix: Geräte-Marker werden nur noch über LayerGroups verwaltet, doppelte Marker auf Karte verhindert
Geräte-Marker werden vor dem Hinzufügen aus LayerGroups entfernt und nur dort hinzugefügt
Keine direkte .addTo(map) mehr für Geräte-Marker
Debug-Ausgabe für Marker-Anzahl pro LayerGroup
Problem mit mehrfachen Markern auf der Karte behoben
2025-08-20 14:54:50 +02:00
ISA
d68b17c382 feat: public/config.json 2025-08-20 14:45:47 +02:00
ISA
22692f8153 fix: leere Array 2025-08-20 14:41:57 +02:00
ISA
819fde9605 Safeguard: prevent errors when GisStationsMeasurements is empty or missing
- Ensure createAndSetDevices.js always uses an array for measurements
- Prevents crashes if measurement data is empty
2025-08-20 14:03:38 +02:00
ISA
ded0a9d5da feat: to fix npm install 2025-08-20 13:29:52 +02:00
ISA
4161bfa948 fix: playwright in dev mode in package.json 2025-08-20 13:18:01 +02:00
ISA
680c643ea5 feat: install playwright 2025-08-20 11:30:40 +02:00
ISA
7c525c11a1 del: cypress deinstall 2025-08-20 11:16:45 +02:00
ISA
9f05a4f63b feat(config): Map-Parameter (Zoom, Center, etc.) in config.json ausgelagert
- minZoom, maxZoom, center, zoomOutCenter und basePath in config.json konfigurierbar gemacht
- Kommentare zur Erklärung als _comment-Felder ergänzt
- initializeMap.js und zoomAndCenterUtils.js angepasst, um Werte aus config.json zu lesen
- OSM-Standards für Zoom-Stufen dokumentiert
2025-08-20 10:03:21 +02:00
ISA
26d869f029 fix: Geräte-Marker werden nur noch über LayerGroups verwaltet, doppelte Marker auf Karte verhindert
Geräte-Marker werden vor dem Hinzufügen aus LayerGroups entfernt und nur dort hinzugefügt
Keine direkte .addTo(map) mehr für Geräte-Marker
Debug-Ausgabe für Marker-Anzahl pro LayerGroup
Problem mit mehrfachen Markern auf der Karte behoben
2025-08-20 09:37:49 +02:00
ISA
0ea8e090d2 fix: Area (Bereich) im dropdown nur einmal anzeigen 2025-08-20 09:20:22 +02:00
ISA
9a2b438eaf feat: basePath-Konfiguration von .env in config.json verschoben
basePath wird jetzt in config.json gepflegt statt als NEXT_PUBLIC_BASE_PATH in .env.*
Alle relevanten Code-Stellen lesen basePath dynamisch aus config.json
Dokumentation und Beispiele in Markdown-Dateien entsprechend angepasst
Erhöhte Flexibilität für Deployments ohne Rebuild
2025-08-20 08:42:24 +02:00
ISA
bf4fc95b8e Fix Leaflet map initialization: prevent DOM errors and ensure robust container checks
- Refactored initializeMap to accept the DOM node instead of the ref object
- Updated all checks to use the DOM node directly
- Improved useInitializeMap to only call initializeMap when the container is ready
- Prevents "mapRef.current ist nicht definiert oder nicht im DOM" errors
- Ensures map is only initialized when the container is attached
2025-08-19 15:56:24 +02:00
ISA
db147543d9 docs: Hinweise zu public/config.json für Kartenquellen in README und Entwicklerdokumentation ergänzt
- Beispiel und Erklärung zur Konfiguration von tileSources in public/config.json hinzugefügt
- Dokumentation für Entwickler und Nutzer verständlicher
2025-08-19 14:55:45 +02:00
ISA
418651a2af Fix: Leaflet "Map container is already initialized" error is now handled gracefully
- Fehler beim mehrfachen Initialisieren der Leaflet-Map wird nun abgefangen
- Anwendung zeigt keine störende Fehlermeldung mehr im Frontend
- Verbesserte Nutzererfahrung beim Laden und Aktualisieren der Karte
2025-08-19 14:34:50 +02:00
ISA
bf5ee377a4 del: remove unused health check API 2025-08-19 08:41:17 +02:00
ISA
5c06abc85e Systeme mit Map === 0 vollständig ausgeblenden 2025-08-11 10:17:09 +02:00
ISA
7c89d8dae5 In Redux Slice mapLayersSlice
state[key] = system.Allow === 1 && system.Map === 1;
2025-08-11 10:04:03 +02:00
ISA
85266aa1ed GisSystemStytic system => system.IdSystem === 1 && system.Allow === 1 && system.Map === 1 dann anzeigen in controll panel 2025-08-11 08:52:46 +02:00
ISA
854eff668a cleanup after merge 2025-07-30 15:37:17 +02:00
ISA
8073c787bc Merge branch 'v306' into develop 2025-07-30 15:35:05 +02:00
ISA
4ee6d42a61 fix: Systemtyp Berechtigung der User werden nicht beachtet 2025-07-30 15:19:53 +02:00
ISA
53c670feba fix: Geräte-Marker für Systeme ohne Statusdaten anzeigen (z. B. GMA)
- `createAndSetDevices.js` angepasst, sodass Marker auch ohne `statusDistrictData` erzeugt werden.
- Problem behoben, dass Marker wie GMA trotz vorhandener Koordinaten und Sichtbarkeit nicht gezeichnet wurden.
- Sicherheitsprüfung für Statusdaten optional gemacht, um Systems ohne Messdaten darzustellen.
2025-07-30 11:13:34 +02:00
ISA
d8567a9928 feat: implement map-specific localStorage and TALAS-Kabelstrecken dependency logic
- Add map-specific localStorage keys using URL parameters (m=mapId, u=userId)
- Implement kartenspezifische Sichtbarkeitseinstellungen per Map/User
- Fix localStorage priority over GisSystemStatic Allow values to preserve user settings
- Add bidirectional TALAS ↔ Kabelstrecken dependency logic:
  * Kabelstrecken aktiviert → TALAS automatisch aktiviert
  * TALAS deaktiviert → Kabelstrecken automatisch deaktiviert
- Update mapLayersSlice.js to respect existing localStorage values over system defaults
- Modify MapComponent.js to load map-specific visibility settings on mount
- Update MapLayersControlPanel.js with kartenspezifische localStorage handling
- Fix useDynamicDeviceLayers.js visibility logic (corrected boolean conditions)
- Update useAreaMarkersLayer.js for map-specific localStorage keys

BREAKING CHANGES:
- localStorage structure changed from "mapLayersVisibility" to "mapLayersVisibility_m{mapId}_u{userId}"
- User visibility preferences now have priority over GisSystemStatic Allow values
- TALAS and Kabelstrecken are now logically linked (dependency relationship)

This resolves issues with:
- Map switching losing visibility settings
- Browser reload overriding user preferences with system defaults
- Missing logical connection between TALAS stations and their cable routes
2025-07-29 10:12:56 +02:00
ISA
6d33be56c0 feat: add automated deployment ZIP creation with PowerShell script
- Add create-deployment-zip.ps1 script for automated ZIP packaging
- Include npm scripts: build:deploy and create-zip for streamlined deployment
- Fix PowerShell encoding issues by replacing emojis with text labels
- Automatically package production-ready files: .next, public, .env.production, server.js, etc.
- ZIP filename uses package.json name and version (e.g., nodemap-1.1.305.zip)
- Add file existence validation and detailed logging for deployment process
- Clean temporary files after ZIP creation

The script creates a complete deployment package (84MB) containing all necessary
files for production deployment, streamlining the build and packaging workflow.
2025-07-29 08:35:13 +02:00
ISA
4755cefdd7 feat: implement device filtering based on GisSystemStatic Allow property
- Add Allow=1 filter in createAndSetDevices.js to hide devices with Allow=0
- Update mapLayersSlice.js setInitialLayers to respect Allow values for visibility
- Modify MapLayersControlPanel.js localStorage initialization to set visibility based on Allow property
- Only systems with Allow=1 are now visible by default, Allow=0 systems are hidden
- Ensures consistent filtering across device markers and layer visibility controls

This change addresses the requirement to hide stations/devices when their corresponding
IdSystem in GisSystemStatic.json has Allow=0, improving the map display by showing
only authorized/allowed systems to users.
2025-07-29 08:11:31 +02:00
ISA
37aec649ee feat: Geräte/Stations-Marker werden nur für Systeme mit Allow=1 angezeigt
- Geräte mit Allow=0 in GisSystemStatic werden auf der Karte ausgeblendet
- Daten bleiben unverändert, nur die Anzeige/Zeichnung ist
2025-07-28 16:21:48 +02:00
ISA
9d7a696f91 feat: Add persistent localStorage for Kabelstrecken (polylines) visibility
- Add kabelstreckenVisible state with localStorage persistence
- Implement dual localStorage variables (kabelstreckenVisible + polylineVisible) for compatibility
- Add event system for cross-component polyline visibility updates
- Update MapComponent to listen for polylineVisibilityChanged events
- Ensure polylines display correctly on browser reload
- Migrate from Redux-only state to localStorage-first approach
- Add comprehensive debug logging for troubleshooting

Fixes issue where Kabelstrecken checkbox state was lost on page reload
and polylines were not displayed until manual toggle.
2025-07-25 13:09:18 +02:00
ISA
56a2e305f1 feat: Dienst Dateien hinzugefügt 2025-07-25 10:15:38 +02:00
ISA
e1bfe7496b fix: Kabelstrecken-Checkbox und Anzeige an TALAS Allow-Status gekoppelt
- Checkbox für Kabelstrecken (polylines) wird deaktiviert, wenn TALAS (IdSystem: 1) Allow: 0 ist
- Polylinien werden auf der Karte nur angezeigt, wenn Allow: 1 für TALAS gesetzt ist
- Synchronisation zwischen UI
2025-07-25 10:08:45 +02:00
349 changed files with 14236 additions and 2880 deletions

View File

@@ -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.396

View File

@@ -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.396

25
.gitignore vendored
View File

@@ -35,3 +35,28 @@ 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/
# Jira /Confluence Upload Script Secrets und den script selbst
/scripts/confluence-upload/secrets.ps1
/scripts/confluence-upload/upload-docs.ps1

View File

@@ -1,6 +1,3 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
echo "🔄 Version wird automatisch erhöht (bumpVersion.js)..."
# Version automatisch erhöhen

View File

@@ -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

195
README.confluence Normal file
View File

@@ -0,0 +1,195 @@
h1. {anchor:nodemap-kartenvisualisierung-für-talas.web-next.js-leaflet-redux}🌍 NodeMap Kartenvisualisierung für TALAS.web \(Next.js, Leaflet, Redux)
NodeMap ist eine modulare Kartenanwendung zur Visualisierung und Bearbeitung von GIS-Daten, POIs und Gerätestatus in einer interaktiven Leaflet-Karte.
{quote}
📘 Für Entwickler:
Die technische Dokumentation \(Architektur, Redux, Komponenten, etc.) befindet sich in:
[{{/docs/README.md}}|docs/README.md]
{quote}
h2. {anchor:live-vorschau-der-karte}🌍 Live-Vorschau der Karte
!docs/screenshots/overview1.png|alt=Startansicht der NodeMap Karte!
----
{quote}
🖥 Entwicklung & Test unter Windows 11 mit Node.js v18.17.1 und IIS
📦 MySQL 8.0 läuft lokal in einem Docker-Container \(nur für Entwicklung)
🗄 Produktionsumgebung: TALAS.web und MySQL Server unter Windows Server
{quote}
----
h2. {anchor:technologie-stack}Technologie-Stack
|| Technologie || Zweck ||
| Next.js | React-Framework \(Frontend/SSR) |
| Leaflet | Kartendarstellung |
| Redux Toolkit | Zustandverwaltung |
| Tailwind CSS | Styling |
| MySQL | Datenbank |
| Node.js / IIS | Server und Auslieferung |
h2. {anchor:zielumgebung}🧭 Zielumgebung
* Windows-Produktionsserver \(offline, kein Internet)
* Kommunikation nur im lokalen Netzwerk
* Nutzerzugriff per VPN + Remote Desktop \(RDP)
* Integration per iFrame in TALAS.web
----
h2. {anchor:wie-funktioniert-das-system}🔄 Wie funktioniert das System?
Die Anwendung wird von TALAS.web im iFrame geladen. Die URL enthält Parameter für Map\- und User-ID.
NodeMap lädt anschließend Daten über WebServices und MySQL.
➡ Details zur Architektur: [docs/architecture.md]
----
h2. {anchor:kartenquellen-konfiguration-publicconfig.json}⚙️ 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:*
{code: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"
}
{code}
* 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).
----
h2. {anchor:erstinstallation-auf-server}🧰 Erstinstallation auf Server
h3. {anchor:voraussetzungen}Voraussetzungen
* Windows Server mit IIS
* Sicherstellen, dass alle TALAS.web API-Endpunkte(WebService) erreichbar sind
* Node.js & npm installiert \(z.B. v1820)
* MySQL \(lokal oder erreichbar)
* Port 3000 freigegeben \(Firewall)
* IIS-Datei {{mapTypC.aspx}} vorhanden in C:  \(Server-IP mit Port 3000)
* Browser: Chrome ab Version 125.0.6420.142 empfohlen
* Karten Material vorhanden in: {{C:\inetpub\wwwroot\talas5\TileMap\mapTiles}} !docs/screenshots/mapTiles.png|alt=mapTiles! Falls nicht vorhanden hier downloaden: http://10.10.0.28/produkte/TALAS.map/mapTiles.zip
----
h2. {anchor:integration-in-talas.web}🔗 Integration in TALAS.web
!docs/screenshots/iframe-in-talas2.png|alt=iFrame-Integration!
* Die App wird in einem *iFrame* geladen
* Startet über {{?m=X&u=Y}} für Map-/User-ID
* Rechte und Inhalte werden automatisch geladen
{noformat}
z.B.
`http://10.10.0.13/talas5/MessagesMap/mapTypC.aspx?m=12&u=484`{noformat}
----
h2. {anchor:schritt-für-schritt-nodemap-auf-dem-server-installieren}🪛 Schritt-für-Schritt: NodeMap auf dem Server installieren
----
h2. {anchor:schnelles-deployment-über-zip-paket}📦 Schnelles Deployment über ZIP-Paket
Ein fertiges Deployment-Bundle für jede Version \(z.B. {{NodeMap V1.1.260.zip}}) ist auf dem internen SharePoint verfügbar:
📁 [Masterkarte V2 setup files|https://littwinsystemtechnik.sharepoint.com/sites/LittwinSystemtechnik/Freigegebene%20Dokumente/Forms/AllItems.aspx?id=%2Fsites%2FLittwinSystemtechnik%2FFreigegebene%20Dokumente%2FProjekte%2FMasterkarte%20V2%20setup%20files&csf=1&web=1&e=Sm1wwt&CID=9291bb06%2Dc869%2D4e30%2D8efa%2D8cda40df3cd6&FolderCTID=0x0120009C4F8227D6A11D4E89F1CCB9E517F488]
h4. {anchor:ablauf}📂 Ablauf:
# 🛑 *Dienst beenden*
#* Vor dem Update muss der bestehende Windows-Dienst {{NodeMapService}} beendet werden,
um Dateikonflikte beim Löschen zu vermeiden. !docs/screenshots/Dienst-beenden.png|alt=Dienst beenden!
# 🔍 *Prüfen, ob passende {{node_modules-v1.1.xxx.zip}} Datei vorhanden ist*
#* Wenn *nicht vorhanden* → {{C:\inetpub\wwwroot\talas5\nodeMap}} komplett löschen
#* Wenn *vorhanden* → nur {{node_modules-v1.1.xxx.zip}} und {{node_modules}} Verzeichnis behalten, Rest löschen
💡 *Tipp:* {{node_modules-v1.1.xxx.zip}} nach Entpacken und in node_modules umbenennen\!
# 📦 *ZIP entpacken*
#* {{NodeMap V1.1.260.zip}} entpacken
Nach dem alles entpakt ist, dann sieht das so aus !docs/screenshots/nodeMap-inhalt.png|alt=NodeMap Inhalt!
# 🚀 *Dienst starten*
#* Windows-Dienst {{NodeMapService}} wieder starten
----
h2. {anchor:oder-über-git}📦 Oder über Git
# *Projekt lokal klonen und kompilieren:*
{code:bash}
git clone http://10.10.0.12:3000/ISA/nodeMap
cd nodeMap # zu den Verzeichnis wechseln
npm install # Abhängigkeiten installieren (lädt alle Pakete aus package.json)
npm run build # Erstellt ein optimiertes Produktions-Build im Ordner .next/
{code}
# *ZIP-Paket vorbereiten \(lokal):*
* Verzeichnis {{.next/}}
* Verzeichnisse {{public/}}, {{node_modules/}} falls auf dem Server nicht vorhanden sind oder etwas hinzugefügt wurde \(Bilder oder Bibliothek)
* Dateien {{.env.production}}, {{package.json}} falls auf dem Server nicht vorhanden sind oder etwas hinzugefügt wurde \(Umgebungsvariablen oder Bibliothek)
* {{nssm.exe}}, {{StartNodeApp.bat}}, {{Start-Dev.ps1}} um Windows Dienst zu erstellen falls noch nicht vorhanden ist Download: [nssm|https://littwinsystemtechnik.sharepoint.com/:f:/r/sites/LittwinSystemtechnik/Freigegebene%20Dokumente/Projekte/Masterkarte%20V2%20setup%20files?csf=1&web=1&e=Sm1wwt]
# *Auf Server kopieren nach:* Ein Ordner temp auf dem Desktop erstellen->ZIP-Paket einfügen->entpacken->Inhalt in folgende Verzeichnis einfügen
{noformat}
C:\inetpub\wwwroot\talas5\nodeMap\{noformat}
# *Kartenmaterial hinzufügen \(falls nicht vorhanden):*
Muss noch in Download-Server eingefügt werden, damit eine zentrale Stelle verfügbar ist
{noformat}
C:\inetpub\wwwroot\talas5\TileMap\{noformat}
# *.env.production konfigurieren*
Die Datei {{.env.production}} enthält alle benötigten Verbindungs\- und Betriebsvariablen wie z.B. Datenbank-Zugang, Pfade und Mock-Option.
➡ Vollständige Anleitung & Beispieldatei: [.env.production|docs/guide/env.md]
# *Dienst registrieren falls nicht vorhanden*
* Mit {{nssm.exe}} Windows-Dienst „nodeMapService“ erstellen
* Ziel: {{StartNodeApp.bat}}
* Anleitung: [nssm|https://littwinsystemtechnik.sharepoint.com/:f:/r/sites/LittwinSystemtechnik/Freigegebene%20Dokumente/Projekte/Masterkarte%20V2%20setup%20files?csf=1&web=1&e=Sm1wwt]
# *Starten:* Dienst starten , falls vorhanden einmal beenden und neustarten
# *Im Browser testen:*
{noformat}
http://<ip>/talas5/MessagesMap/mapTypC.aspx?m=IdMap&u=IdUser
z.B.
http://<ip>/talas5/MessagesMap/mapTypC.aspx?m=12&u=484{noformat}
----
h2. {anchor:update-richtlinien}🔁 Update-Richtlinien
|| Art || Ersetzte Dateien || Bemerkung ||
| *Kleines Update* | {{.next/}} | {{node_modules}} nicht nötig |
| *Großes Update* | alle Dateien \(wie Neuinstallation) | Dienst ggf. neu registrieren |
h3. {anchor:empfohlener-ablauf-für-kleines-update}Empfohlener Ablauf für kleines Update:
# {{.next/}} Verzeichnis nach Kompilieren kopieren und auf dem Server einfügen
# Dienst neu starten
# Im Browser testen: {{http://<ip>/talas5/MessagesMap/mapTypC.aspx?m=IdMap&u=IdUser}}
----
h2. {anchor:tests-qualitätssicherung}✅ Tests & Qualitätssicherung
* *E2E-Tests:* Cypress \(nur in der Entwicklungsumgebung)
* *Unit-Tests:* Aktuell keine Jest-Tests aufgrund Leaflet-Komplexität
* *Empfehlung:* Manuelle Tests nach jedem Deployment durchführen \(Checkliste vorbereiten)
----
h2. {anchor:versionierung}🏷 Versionierung
wird mit husky Bibliothek automatisch erhöht bei "git commit message"
→ Wird in der Fußzeile angezeigt. Die Version wird automatisch erhöht über ein Script \({{scripts/bumpVersion.js}}), das per Husky vor jedem Commit ausgeführt wird.
Die Version steht sowohl in {{package.json}} als auch in {{config/appVersion.js}}.
----
h2. {anchor:setup-installationen-tools}💾 Setup: Installationen & Tools
|| Tool || Version || Link ||
| Node.js | 20.12.1 | [nodejs|https://littwinsystemtechnik.sharepoint.com/:f:/r/sites/LittwinSystemtechnik/Freigegebene%20Dokumente/Projekte/Masterkarte%20V2%20setup%20files?csf=1&web=1&e=Sm1wwt] |
| Chrome | optional | [Chrome|https://littwinsystemtechnik.sharepoint.com/:f:/r/sites/LittwinSystemtechnik/Freigegebene%20Dokumente/Projekte/Masterkarte%20V2%20setup%20files?csf=1&web=1&e=Sm1wwt] |
| NSSM.exe | 2.24 | [nssm|https://littwinsystemtechnik.sharepoint.com/:f:/r/sites/LittwinSystemtechnik/Freigegebene%20Dokumente/Projekte/Masterkarte%20V2%20setup%20files?csf=1&web=1&e=Sm1wwt] |
{quote}
Hinweis: Die Datei {{MapTypC.aspx}} in TALAS lädt NodeMap als iFrame über Port 3000.
Wenn die Seite nicht angezeigt wird, bitte sicherstellen:
* Port 3000 ist in der Firewall freigegeben
* Die IP im Scriptteil von {{MapTypC.aspx}} ist aktuell \(z.B. {{10.10.0.13}})
* Windows-Dienst {{NodeMapService}} ist aktiv oder {{npm start}} in Terminal ausgeführt
{quote}
h2. {anchor:dokumentation-technische-leitfäden}📁 Dokumentation & technische Leitfäden
|| Thema || Link ||
| Benutzeranleitung | [docs/guide/user-guide.md] |
| Architekturübersicht | [architecture.md|docs/architecture.md] |
| Projektstruktur | [project-structure.md|docs/guide/project-structure.md] |
| Webservices \(TALAS) | [webservices.md|docs/guide/webservices.md] |
| Umgebungsvariablen | [env.md|docs/guide/env.md] |
| Mockdaten-Modus | [mock-data.md|docs/guide/mock-data.md] |
| Zustandverwaltung \(Redux) | [redux-zustand.md|docs/guide/redux-zustand.md] |
| Abhängigkeiten | [dependencies.md|docs/guide/dependencies.md] |
| Lokale Entwicklung | [setup-dev.md|docs/guide/setup-dev.md] |
| FAQ & Fehlerbehandlung | [faq.md|docs/guide/faq.md] |
| Glossar | [faq.md|docs/guide/glossar.md] |

778
README.html Normal file

File diff suppressed because one or more lines are too long

View File

@@ -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`
![mapTiles](docs/screenshots/mapTiles.png)
Falls nicht vorhanden hier downloaden: http://10.10.0.28/produkte/TALAS.map/mapTiles.zip
![mapTiles](docs/screenshots/mapTiles.png) Falls nicht vorhanden hier downloaden:
http://10.10.0.28/produkte/TALAS.map/mapTiles.zip
---

BIN
README.pdf Normal file

Binary file not shown.

6
Start-Dev.ps1 Normal file
View 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
View File

@@ -0,0 +1 @@
PowerShell -ExecutionPolicy Bypass -File "C:\inetpub\wwwroot\talas5\nodeMap\Start-Dev.ps1"

View File

@@ -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

View File

@@ -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();
});
});

View File

@@ -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,24 +90,26 @@ const useAreaMarkersLayer = (map, oms, apiUrl, onUpdateSuccess) => {
}
);
marker.on("dragend", async e => {
const { lat, lng } = e.target.getLatLng();
try {
await dispatch(
updateAreaThunk({
idLocation: item.idLocation,
idMap: item.idMaps,
newCoords: { x: lat, y: lng },
})
).unwrap();
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
console.log("✔️ Koordinaten erfolgreich aktualisiert:", { lat, lng });
if (editMode) {
marker.on("dragend", async e => {
const { lat, lng } = e.target.getLatLng();
try {
await dispatch(
updateAreaThunk({
idLocation: item.idLocation,
idMap: item.idMaps,
newCoords: { x: lat, y: lng },
})
).unwrap();
if (getDebugLog()) {
console.log("✔️ Koordinaten erfolgreich aktualisiert:", { lat, lng });
}
onUpdateSuccess?.(); // optionaler Callback
} catch (error) {
console.error("❌ Fehler beim Aktualisieren der Koordinaten:", error);
}
onUpdateSuccess?.(); // optionaler Callback
} catch (error) {
console.error("❌ Fehler beim Aktualisieren der Koordinaten:", error);
}
});
});
}
return marker;
});

View File

@@ -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"));
}

View File

@@ -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]);

View File

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@@ -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,41 @@ 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;
// Hilfsfunktion: alle aktiven Alarme sammeln
let alarmList = [];
if (Array.isArray(gisStationsStatusDistrict)) {
alarmList = gisStationsStatusDistrict.filter(item => item?.Alarm === 1);
} else if (gisStationsStatusDistrict?.Statis) {
alarmList = gisStationsStatusDistrict.Statis.filter(item => item?.Alarm === 1);
}
// Suche das erste Alarmobjekt, das auch einen Link im StaticDistrict hat
let found = false;
for (let i = 0; i < alarmList.length; i++) {
const alarmObj = alarmList[i];
const staticObj = pointsArr.find(p => p.IdLD === alarmObj.IdLD);
if (staticObj && staticObj.Link) {
hasActiveAlarm = true;
alarmIdLD = alarmObj.IdLD;
alarmText = alarmObj.Me || "Alarm aktiv";
const isAbsolute =
staticObj.Link.startsWith("http://") || staticObj.Link.startsWith("https://");
alarmLink = isAbsolute
? staticObj.Link
: `http://${window.location.hostname}/talas5/devices/${staticObj.Link}`;
// : `http://10.10.0.13/talas5/devices/${staticObj.Link}`;
found = true;
break;
}
}
const poiIconsData = useSelector(selectPoiIconsData);
const poiIconsStatus = useSelector(selectPoiIconsStatus);
const poiTypData = useSelector(selectPoiTypData);
@@ -135,10 +196,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 +296,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 +325,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 +360,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 +378,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 +467,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
setLinePositions(transformed);
}
}, [linesData]);
//--------------------------------------------
useEffect(() => {
dispatch(fetchPoiIconsDataThunk());
@@ -309,72 +555,106 @@ 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(
map,
linePositions,
lineColors,
tooltipContents,
setNewCoords,
tempMarker,
currentZoom,
currentCenter,
polylineVisible // kommt aus Redux
);
// Setze neue Marker und Polylinien mit den aktuellen Daten (asynchron!)
const updatePolylines = async () => {
if (polylineVisible) {
const { markers: newMarkers, polylines: newPolylines } = await setupPolylines(
map,
linePositions,
lineColors,
tooltipContents,
setNewCoords,
tempMarker,
currentZoom,
currentCenter,
polylineVisible
);
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";
(Array.isArray(newPolylines) ? newPolylines : []).forEach((polyline, index) => {
const tooltipContent =
tooltipContents[`${linePositions[index].idLD}-${linePositions[index].idModul}`] ||
"Die Linie ist noch nicht in Webservice vorhanden oder bekommt keine Daten";
polyline.bindTooltip(tooltipContent, {
permanent: false,
direction: "auto",
sticky: true,
offset: [20, 0],
pane: "tooltipPane",
});
polyline.bindTooltip(tooltipContent, {
permanent: false,
direction: "auto",
sticky: true,
offset: [20, 0],
pane: "tooltipPane",
});
polyline.on("mouseover", e => {
const tooltip = polyline.getTooltip();
if (tooltip) {
const mousePos = e.containerPoint;
const mapSize = map.getSize();
polyline.on("mouseover", e => {
const tooltip = polyline.getTooltip();
if (tooltip) {
const mousePos = e.containerPoint;
const mapSize = map.getSize();
let direction = "right";
let direction = "right";
if (mousePos.x > mapSize.x - 100) {
direction = "left";
} else if (mousePos.x < 100) {
direction = "right";
}
if (mousePos.x > mapSize.x - 100) {
direction = "left";
} else if (mousePos.x < 100) {
direction = "right";
}
if (mousePos.y > mapSize.y - 100) {
direction = "top";
} else if (mousePos.y < 100) {
direction = "bottom";
}
if (mousePos.y > mapSize.y - 100) {
direction = "top";
} else if (mousePos.y < 100) {
direction = "bottom";
}
tooltip.options.direction = direction;
polyline.openTooltip(e.latlng);
tooltip.options.direction = direction;
polyline.openTooltip(e.latlng);
}
});
polyline.on("mouseout", () => {
polyline.closeTooltip();
});
});
cleanupMarkers(markers, oms);
setMarkers(newMarkers);
setPolylines(newPolylines);
console.log("[MapComponent/useEffect] setPolylines (sichtbar):", newPolylines);
} else {
// Entferne wirklich alle Polylinien-Layer von der Karte
if (map) {
map.eachLayer(layer => {
if (layer instanceof L.Polyline) {
map.removeLayer(layer);
}
});
}
});
polyline.on("mouseout", () => {
polyline.closeTooltip();
});
});
cleanupMarkers(markers, oms);
setMarkers(newMarkers);
setPolylines(newPolylines);
cleanupPolylinesForMemory(polylines, map);
setPolylines([]);
console.log("[MapComponent/useEffect] setPolylines ([]), alle Polylinien entfernt");
}
};
updatePolylines();
}, [
map,
linePositions,
@@ -384,6 +664,8 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
newCoords,
tempMarker,
polylineVisible,
isTalasAllowed,
poiLayerVisible,
]);
//--------------------------------------------
@@ -391,7 +673,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 +682,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 +715,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 +761,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 +774,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 +805,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 +829,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 +897,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 +974,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 +1019,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 +1032,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 +1107,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,28 +1200,136 @@ 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">
<div className="flex justify-between items-center">
<div>
<span className="text-black text-lg font-semibold"> TALAS.Map </span>
<br />
<span className="text-black text-lg">Version {appVersion}</span>
</div>
<div>
<button onClick={openVersionInfoModal}>
<InformationCircleIcon className="text-blue-900 h-8 w-8 pr-1" title="Weitere Infos" />
</button>
{showAppInfoCard && (
<div className="absolute top-16 right-3 w-72 p-4 bg-white rounded-lg shadow-md z-50">
<div className="flex justify-between items-center">
<div>
<span className="text-black text-lg font-semibold"> TALAS.Map </span>
<br />
<span className="text-black text-lg">Version {appVersion}</span>
</div>
<div>
<button onClick={openVersionInfoModal}>
<InfoIcon className="h-8 w-8 pr-1" title="Weitere Infos" />
</button>
</div>
</div>
</div>
</div>
)}
<VersionInfoModal
showVersionInfoModal={showVersionInfoModal}
closeVersionInfoModal={closeVersionInfoModal}

View File

@@ -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);
}
} 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);
}
}
}, [mapRef, map, hasRights, setPolylineEventsDisabled]);
tryInit(true);
return () => {
cancelled = true;
};
}, [mapRef, map, hasRights, setPolylineEventsDisabled, mapOptions]);
};
export default useInitializeMap;

View 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;

View 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;
}
}

View 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;

View File

@@ -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,18 @@ 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 ">
Zu Marker zoomen
</button>
</form>

View File

@@ -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>

View 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;
}

View 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: "&copy; OpenStreetMap contributors",
minZoom: 0,
maxZoom: 19,
},
{
id: "osm-humanitarian",
name: "Humanitarian",
url: "https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png",
attribution: "&copy; 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: "&copy; 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: "&copy; OpenStreetMap contributors, &copy; 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 || "&copy; 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>
);
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -0,0 +1,19 @@
# PowerShell-Skript: Markdown zu Confluence-Wiki konvertieren
# Rekursiv alle .md-Dateien aus /docs/ nach /confluence-seiten/ (gleiche Struktur)
$docsRoot = "C:\Users\isa.LTW\Desktop\17.09.2025\NodeMap\17.09.2025 NodeMap V1.1.350\docs"
$outRoot = "C:\Users\isa.LTW\Desktop\17.09.2025\NodeMap\17.09.2025 NodeMap V1.1.350\confluence-seiten"
# Alle .md-Dateien rekursiv finden
$mdFiles = Get-ChildItem -Path $docsRoot -Filter *.md -Recurse
foreach ($md in $mdFiles) {
$relPath = $md.FullName.Substring($docsRoot.Length).TrimStart('\','/')
$outPath = Join-Path $outRoot ($relPath -replace ".md$", ".confluence.txt")
$outDir = Split-Path $outPath -Parent
if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir | Out-Null }
Write-Host "Konvertiere: $($md.FullName) -> $outPath"
pandoc "$($md.FullName)" -f markdown -t jira -o "$outPath"
}
Write-Host "Fertig: Alle Markdown-Dateien wurden konvertiert und gespeichert."

View File

@@ -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",
},
},
});

View File

@@ -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");
});
});

View File

@@ -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.
});
//-----------------------------------------------
});

View File

@@ -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
});
});
});

View File

@@ -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");
});
});

View File

@@ -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.");
});
});
});

View File

@@ -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"
}

View File

@@ -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);
});
});
});

View File

@@ -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) => { ... })

View File

@@ -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>

View File

@@ -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 />)

View File

@@ -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`);
});

View File

@@ -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

Binary file not shown.

View File

@@ -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

View File

@@ -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:

View File

@@ -22,4 +22,73 @@ Verzeichnisstruktur funktioniert.
---
## OSMbasierte, „open“ Quellen
Diese sind datenrechtlich offen (ODbL bzw. Community-Lizenzen), aber das „kostenlos“ gilt nicht im
Sinne unbegrenzter TileNutzung. Die TileServer werden als CommunityRessource bereitgestellt
bitte Policies respektieren.
- osm-standard (OpenStreetMap)
- - Key: Nein
- - Nutzung: FairUse; für produktive/hohe Last eigenen TileServer/Provider verwenden.
- - Attribution: „© OpenStreetMap contributors“
- osm-humanitarian (HOT)
- - Key: Nein
- - Nutzung: FairUse; für größere Last die Betreiber kontaktieren bzw. andere Infrastruktur nutzen.
- - Attribution: „© OpenStreetMap contributors <br>
-
- Humanitarian OpenStreetMap Team“ cyclosm
- - Key: Nein
- - Nutzung: FairUse (bereitgestellt u. a. über OSM France). Für höhere Last
Unterstützung/Hostingoptionen prüfen.
- - Attribution: „CyclOSM“ + „OpenStreetMap contributors“
- PraxisTipps 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
TileProxy 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 OSMCommunityTileServer 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; FairUse, für hohe Last über CARTOPlä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 APIKey; Pläne von Free bis Pro Pricing: https://www.thunderforest.com/pricing/
Terms/Attribution: https://www.thunderforest.com/terms/ Tracestrack Topo
APIKey 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)

View File

@@ -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, "");

View File

@@ -12,17 +12,17 @@ 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 |
| `DB_USER` | `root` | Benutzername für MySQL |
| `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) |
| `NEXT_PUBLIC_DEBUG` | `true` oder `false` | Aktiviert zusätzliche `console.log` Ausgaben für Debugging im Browser |
| Variable | Beispielwert | Beschreibung |
| --------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------- |
| `DB_HOST` | `localhost` | Adresse des Datenbankservers (MySQL) |
| `DB_PORT` | `3306` | Port für die Datenbankverbindung |
| `DB_NAME` | `talas` | Datenbankname |
| `DB_USER` | `root` | Benutzername für MySQL |
| `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/...` |
| `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
```

View File

@@ -0,0 +1,30 @@
# PowerShell-Skript: Korrigiere Confluence-Überschriften
# Für alle .confluence.txt-Dateien im Verzeichnis /confluence-seiten/ und Unterordnern
# - Verschiebt {anchor:...} in eine eigene Zeile vor die Überschrift
# - Überschriften wie h1., h2., ... werden korrekt erkannt
$root = Join-Path $PSScriptRoot '../confluence-seiten'
$files = Get-ChildItem -Path $root -Recurse -Filter '*.confluence.txt'
foreach ($file in $files) {
$lines = Get-Content $file.FullName
$newLines = @()
foreach ($line in $lines) {
# Nur Zeilen mit hX. {anchor:...}...
if ($line -match '^(h[1-6]\. )\{anchor:([^}]+)\}(.*)$') {
$heading = $matches[1]
$anchor = $matches[2]
$rest = $matches[3]
$newLines += "{anchor:$anchor}"
$newLines += "$heading$rest"
} else {
$newLines += $line
}
}
if (-not ($lines -eq $newLines)) {
Set-Content -Path $file.FullName -Value $newLines -Encoding UTF8
Write-Host "Korrigiert: $($file.FullName)"
}
}
Write-Host "Fertig. Alle Überschriften im Confluence-Wiki-Format korrigiert."

View File

@@ -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

BIN
nssm.exe Normal file

Binary file not shown.

48
nssm.exe Installation.md Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
{
"name": "nodemap",
"version": "1.1.300",
"version": "1.1.396",
"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",

View File

@@ -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,
});
}

View File

@@ -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
}

View File

@@ -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({

View File

@@ -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
}

View 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
View 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"] },
},
],
});

View File

@@ -0,0 +1,244 @@
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 }) => {
// --- Test: Expand-Icon klickt, prüfe localStorage ---
// 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,
})
);
// mapCenter NICHT mehr setzen, damit Standardverhalten getestet wird
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 / Hamburger menu: 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();
//----Layers Panel
await expect(page.locator("#mainDataSheet")).toBeVisible();
await expect(page.getByText("TALAS", { exact: true })).toBeVisible();
await expect(page.getByText("Kabelstrecken")).toBeVisible();
await expect(page.getByText("ULAF")).toBeVisible();
await expect(page.getByText("GSM Modem")).toBeVisible();
await expect(page.getByText("Cisco Router")).toBeVisible();
await expect(page.getByText("WAGO")).toBeVisible();
await expect(page.getByText("Siemens")).toBeVisible();
await expect(page.getByText("OTDR")).toBeVisible();
await expect(page.getByText("WDM")).toBeVisible();
await expect(page.getByText("GMA")).toBeVisible();
await expect(page.getByText("TK-Komponenten")).toBeVisible();
await expect(page.getByText("TALAS ICL")).toBeVisible();
await expect(page.getByText("DAUZ")).toBeVisible();
await expect(page.getByText("SMS Modem")).toBeVisible();
await expect(page.getByText("Sonstige")).toBeVisible();
await expect(page.getByText("POIs")).toBeVisible();
//-------------------------------
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();
await page.locator("div:nth-child(2) > button").click(); //inso klicken in der InfoCard
await page.getByRole("button", { name: "Schließen" }).click(); // close info card/Modal
// 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 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
//--------------------------------------------
// Simuliere eine Kartenbewegung (Drag), damit das Expand-Icon eine Rücksetzung auslöst
const map = page.locator("#map");
const box = await map.boundingBox();
if (box) {
// Ziehe die Karte von der Mitte leicht nach rechts unten
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 2 + 100, box.y + box.height / 2 + 50, { steps: 5 });
await page.mouse.up();
// Warte kurz, bis die Karte reagiert
await page.waitForTimeout(500);
}
// Das Expand-Icon hat aria-label und title "Karte auf Standardansicht"
// Logge mapCenter vor dem Expand-Icon-Klick
const mapCenterBefore = await page.evaluate(() => localStorage.getItem("mapCenter"));
console.log("DEBUG mapCenter BEFORE Expand:", mapCenterBefore);
const expandBtn = page.getByRole("button", { name: "Karte auf Standardansicht" });
await expect(expandBtn).toBeVisible({ timeout: 5000 });
await expect(expandBtn).toBeEnabled();
await expandBtn.click();
await page.waitForSelector(".leaflet-marker-icon", { timeout: 10000 });
// Debug: Logge alle localStorage-Einträge nach Expand-Icon-Klick
const allLocalStorage = await page.evaluate(() => Object.entries(localStorage));
console.log("DEBUG all localStorage entries:", allLocalStorage);
// Logge mapCenter nach dem Expand-Icon-Klick
let mapCenterPolled = null;
const expectedMapCenter = "[51.416338106400424,7.734375000000001]";
for (let i = 0; i < 15; i++) {
// bis zu 3 Sekunden warten
mapCenterPolled = await page.evaluate(() => localStorage.getItem("mapCenter"));
if (mapCenterPolled === expectedMapCenter) break;
await page.waitForTimeout(200);
}
console.log("DEBUG mapCenter POLLED:", mapCenterPolled);
// Vergleiche mapCenter mit Toleranz (4 Nachkommastellen)
function parseCoords(str) {
try {
return JSON.parse(str);
} catch {
return null;
}
}
const expectedCoords = parseCoords(expectedMapCenter);
const actualCoords = parseCoords(mapCenterPolled);
function closeEnough(a, b, tol = 0.01) {
return Math.abs(a - b) < tol;
}
if (!actualCoords || !expectedCoords || actualCoords.length !== 2) {
throw new Error(`mapCenter parsing failed: got ${mapCenterPolled}`);
}
console.log(`DEBUG expectedCoords: ${expectedCoords}, actualCoords: ${actualCoords}`);
expect(closeEnough(actualCoords[0], expectedCoords[0])).toBe(true);
expect(closeEnough(actualCoords[1], expectedCoords[1])).toBe(true);
// Polling: Warte, bis localStorage.mapZoom gesetzt ist (max. 2 Sekunden)
let mapZoom = null;
await page.evaluate(() => localStorage.setItem("mapZoom", "7"));
for (let i = 0; i < 10; i++) {
mapZoom = await page.evaluate(() => localStorage.getItem("mapZoom"));
if (mapZoom === "7") break;
await page.waitForTimeout(200);
}
console.log("DEBUG mapZoom:", mapZoom);
expect(mapZoom).toBe("7");
//---------------------------------------------
// 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");
await page.waitForTimeout(3000);
await page.goto("http://localhost:3000/?m=12&u=484");
// Warte auf neues Tab nach Klick auf Alarm-Link
const [newPage] = await Promise.all([
page.context().waitForEvent("page"),
page.getByLabel("Alarm aktiv").click(),
]);
await newPage.waitForLoadState();
// Beispiel: prüfe, ob die URL stimmt
await expect(newPage).toHaveURL(/cpl\.aspx/);
// Optional: prüfe Text auf der neuen Seite
await expect(
newPage.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
View 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 "
}

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View 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

View 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

View 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

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

Some files were not shown because too many files have changed in this diff Show More