Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f390f93293 | ||
|
|
28dcb284bf | ||
|
|
4f0527e8a9 | ||
|
|
3d0ce4a2b4 | ||
|
|
76280b365b | ||
|
|
c112ec2da4 | ||
|
|
7faee5fd79 | ||
|
|
e52b0cc520 | ||
|
|
4a42c428f0 | ||
|
|
1d3d04d49c | ||
|
|
dd9980409c | ||
|
|
ea6d71a4f5 | ||
|
|
13ca1cece0 | ||
|
|
f22bb4b232 | ||
|
|
bfd091b1b1 | ||
|
|
81b6379895 | ||
|
|
42ca88d27e | ||
|
|
fdb70d892c | ||
|
|
73e9c63e36 | ||
|
|
e520207526 | ||
|
|
2e5acf9327 | ||
|
|
cdfdd3d6cf | ||
|
|
5b86d5293b | ||
|
|
31c770f778 | ||
|
|
051dd4c306 | ||
|
|
995f084e15 | ||
|
|
eaacec71da | ||
|
|
6bc2e16657 | ||
|
|
1208024f76 | ||
|
|
369f29a769 | ||
|
|
d166b2468d | ||
|
|
59c8680c23 | ||
|
|
1a046f8212 | ||
|
|
e35216daf5 | ||
|
|
91ad47166f | ||
|
|
3a9b436352 | ||
|
|
7b881e80c2 | ||
|
|
cc19a0a466 | ||
|
|
f200d0bb20 | ||
|
|
75a0ab000f | ||
|
|
4d2a94ffea | ||
|
|
239ad82e46 | ||
|
|
a2d3338624 | ||
|
|
598acb8441 | ||
|
|
f8512c485e | ||
|
|
fff2754b14 | ||
|
|
61ed542ea4 | ||
|
|
4befddd440 | ||
|
|
5b7868145c | ||
|
|
0a3c4c208f | ||
|
|
8cf520bb2c | ||
|
|
3896381a8f | ||
|
|
a013c07394 | ||
|
|
f2a322a91b | ||
|
|
bf7b62d110 | ||
|
|
c44a755077 | ||
|
|
aea439f135 | ||
|
|
c6871692aa | ||
|
|
e7192a7623 | ||
|
|
f11f64d4d7 | ||
|
|
da21cba186 | ||
|
|
d179c152c0 | ||
|
|
2da79c9318 | ||
|
|
2066cbb9e8 | ||
|
|
c8a14ee873 | ||
|
|
44b29469b9 | ||
|
|
ee5319a928 | ||
|
|
d68b17c382 | ||
|
|
22692f8153 | ||
|
|
819fde9605 | ||
|
|
ded0a9d5da | ||
|
|
4161bfa948 | ||
|
|
680c643ea5 | ||
|
|
7c525c11a1 | ||
|
|
9f05a4f63b | ||
|
|
26d869f029 | ||
|
|
0ea8e090d2 | ||
|
|
9a2b438eaf | ||
|
|
bf4fc95b8e | ||
|
|
db147543d9 | ||
|
|
418651a2af | ||
|
|
bf5ee377a4 | ||
|
|
5c06abc85e | ||
|
|
7c89d8dae5 | ||
|
|
85266aa1ed | ||
|
|
854eff668a | ||
|
|
8073c787bc | ||
|
|
4ee6d42a61 | ||
|
|
53c670feba | ||
|
|
d8567a9928 | ||
|
|
6d33be56c0 | ||
|
|
4755cefdd7 | ||
|
|
37aec649ee | ||
|
|
9d7a696f91 | ||
|
|
56a2e305f1 | ||
|
|
e1bfe7496b |
@@ -7,7 +7,6 @@ DB_NAME=talas_v5
|
|||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
|
|
||||||
# Public Settings (Client braucht IP/Domain) , Variablen mit dem Präfix "NEXT_PUBLIC" ist in Browser sichtbar
|
# 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.)
|
# 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.
|
# 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.
|
#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/talas5/index.aspx -> basePath in config.json auf /talas5 setzen
|
||||||
# z.B. http://10.10.0.13/xyz/index.aspx -> NEXT_PUBLIC_BASE_PATH=/xyz
|
# z.B. http://10.10.0.13/xyz/index.aspx -> basePath in config.json auf /xyz setzen
|
||||||
NEXT_PUBLIC_BASE_PATH=/talas5
|
# basePath wird jetzt in public/config.json gepflegt
|
||||||
# Oder leer lassen für direkten Zugriff -> NEXT_PUBLIC_BASE_PATH=
|
|
||||||
# App-Versionsnummer
|
# App-Versionsnummer
|
||||||
NEXT_PUBLIC_APP_VERSION=1.1.300
|
NEXT_PUBLIC_APP_VERSION=1.1.396
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ DB_NAME=talas_v5
|
|||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
|
|
||||||
# Public Settings (Client braucht IP/Domain) , Variablen mit dem Präfix "NEXT_PUBLIC" ist in Browser sichtbar
|
# 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.)
|
# 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.
|
# 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.
|
#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/talas5/index.aspx -> basePath in config.json auf /talas5 setzen
|
||||||
# z.B. http://10.10.0.13/xyz/index.aspx -> NEXT_PUBLIC_BASE_PATH=/xyz
|
# z.B. http://10.10.0.13/xyz/index.aspx -> basePath in config.json auf /xyz setzen
|
||||||
NEXT_PUBLIC_BASE_PATH=/talas5
|
# basePath wird jetzt in public/config.json gepflegt
|
||||||
# Oder leer lassen für direkten Zugriff -> NEXT_PUBLIC_BASE_PATH=
|
|
||||||
|
|
||||||
# App-Versionsnummer
|
# App-Versionsnummer
|
||||||
NEXT_PUBLIC_APP_VERSION=1.1.300
|
NEXT_PUBLIC_APP_VERSION=1.1.396
|
||||||
|
|||||||
25
.gitignore
vendored
@@ -35,3 +35,28 @@ docs.zip
|
|||||||
/mockData/
|
/mockData/
|
||||||
/__mocks__/
|
/__mocks__/
|
||||||
/__tests__/
|
/__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
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
echo "🔄 Version wird automatisch erhöht (bumpVersion.js)..."
|
echo "🔄 Version wird automatisch erhöht (bumpVersion.js)..."
|
||||||
|
|
||||||
# Version automatisch erhöhen
|
# Version automatisch erhöhen
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ das Objekt selbst
|
|||||||
### ♻️ Refactor
|
### ♻️ Refactor
|
||||||
|
|
||||||
- Alle hartkodierten `/talas5/`-Pfadangaben entfernt
|
- 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
|
- Unterstützt jetzt auch den Betrieb ohne Unterverzeichnis
|
||||||
|
|
||||||
### 🧠 Architektur
|
### 🧠 Architektur
|
||||||
|
|||||||
195
README.confluence
Normal 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. v18–20)
|
||||||
|
* 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
28
README.md
@@ -48,6 +48,30 @@ User-ID.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ⚙️ Kartenquellen-Konfiguration (public/config.json)
|
||||||
|
|
||||||
|
Die Datei `public/config.json` steuert, welche Kartenquelle (z.B. OSM oder lokale Tiles) für die
|
||||||
|
Leaflet-Karte verwendet wird.
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"//info": "tileSources: 'local' für offline, 'osm' für online",
|
||||||
|
"tileSources": {
|
||||||
|
"local": "http://localhost/talas5/TileMap/mapTiles/{z}/{x}/{y}.png",
|
||||||
|
"osm": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
},
|
||||||
|
"active": "osm"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Mit `active` kann zwischen Online- und Offline-Karten umgeschaltet werden.
|
||||||
|
- Die Datei wird beim Start der App automatisch geladen.
|
||||||
|
- Für Offline-Betrieb muss das lokale Kartenmaterial vorhanden sein (siehe Installationsanleitung).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🧰 Erstinstallation auf Server
|
## 🧰 Erstinstallation auf Server
|
||||||
|
|
||||||
### Voraussetzungen
|
### Voraussetzungen
|
||||||
@@ -61,8 +85,8 @@ User-ID.
|
|||||||
(Server-IP mit Port 3000)
|
(Server-IP mit Port 3000)
|
||||||
- Browser: Chrome ab Version 125.0.6420.142 empfohlen
|
- Browser: Chrome ab Version 125.0.6420.142 empfohlen
|
||||||
- Karten Material vorhanden in: `C:\inetpub\wwwroot\talas5\TileMap\mapTiles`
|
- Karten Material vorhanden in: `C:\inetpub\wwwroot\talas5\TileMap\mapTiles`
|
||||||

|
 Falls nicht vorhanden hier downloaden:
|
||||||
Falls nicht vorhanden hier downloaden: http://10.10.0.28/produkte/TALAS.map/mapTiles.zip
|
http://10.10.0.28/produkte/TALAS.map/mapTiles.zip
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
BIN
README.pdf
Normal file
6
Start-Dev.ps1
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Navigiere zum Verzeichnis deines Projekts
|
||||||
|
cd 'C:\inetpub\wwwroot\talas5\nodeMap'
|
||||||
|
|
||||||
|
# F<>hre den npm Befehl aus
|
||||||
|
npm start
|
||||||
|
|
||||||
1
StartNodeApp.bat
Normal file
@@ -0,0 +1 @@
|
|||||||
|
PowerShell -ExecutionPolicy Bypass -File "C:\inetpub\wwwroot\talas5\nodeMap\Start-Dev.ps1"
|
||||||
@@ -69,3 +69,37 @@ die Daten von DB auch mit WebSocket gelöst werden
|
|||||||
|
|
||||||
- [ ] Redundante Kontextmenülogik auflösen
|
- [ ] Redundante Kontextmenülogik auflösen
|
||||||
- [ ] Bessere Trennung zwischen Mock- und Live-API in Service-Funktionen
|
- [ ] 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
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
describe("setupPOIs Icon-Mapping intern", () => {
|
|
||||||
it("ordnet korrektes Icon anhand idPoi zu", () => {
|
|
||||||
const mockPoiData = [{ idPoi: 7, path: "poi-marker-icon-2.png" }];
|
|
||||||
const iconMap = new Map();
|
|
||||||
mockPoiData.forEach((item) => iconMap.set(item.idPoi, item.path));
|
|
||||||
const result = iconMap.get(7);
|
|
||||||
expect(result).toBe("poi-marker-icon-2.png");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("gibt undefined zurück wenn idPoi nicht existiert", () => {
|
|
||||||
const iconMap = new Map();
|
|
||||||
iconMap.set(1, "icon-1.png");
|
|
||||||
const result = iconMap.get(99);
|
|
||||||
expect(result).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getDebugLog } from "@/utils/configUtils.js";
|
||||||
// /hooks/layers/useAreaMarkersLayer.js
|
// /hooks/layers/useAreaMarkersLayer.js
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import L from "leaflet";
|
import L from "leaflet";
|
||||||
@@ -22,7 +23,13 @@ const useAreaMarkersLayer = (map, oms, apiUrl, onUpdateSuccess) => {
|
|||||||
const updateMarkersVisibility = () => {
|
const updateMarkersVisibility = () => {
|
||||||
if (!map || areaMarkers.length === 0) return;
|
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);
|
const areAllLayersInvisible = Object.values(mapLayersVisibility).every(v => !v);
|
||||||
|
|
||||||
if (areAllLayersInvisible === prevVisibility.current) return;
|
if (areAllLayersInvisible === prevVisibility.current) return;
|
||||||
@@ -42,7 +49,8 @@ const useAreaMarkersLayer = (map, oms, apiUrl, onUpdateSuccess) => {
|
|||||||
updateMarkersVisibility();
|
updateMarkersVisibility();
|
||||||
|
|
||||||
const handleStorageChange = event => {
|
const handleStorageChange = event => {
|
||||||
if (event.key === "mapLayersVisibility") {
|
// Überwache sowohl den alten als auch kartenspezifische Keys
|
||||||
|
if (event.key === "mapLayersVisibility" || event.key?.startsWith("mapLayersVisibility_")) {
|
||||||
updateMarkersVisibility();
|
updateMarkersVisibility();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -64,10 +72,11 @@ const useAreaMarkersLayer = (map, oms, apiUrl, onUpdateSuccess) => {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
const editMode = localStorage.getItem("editMode") === "true";
|
||||||
const markers = data.map(item => {
|
const markers = data.map(item => {
|
||||||
const marker = L.marker([item.x, item.y], {
|
const marker = L.marker([item.x, item.y], {
|
||||||
icon: customIcon,
|
icon: customIcon,
|
||||||
draggable: true,
|
draggable: editMode,
|
||||||
customType: "areaMarker",
|
customType: "areaMarker",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,24 +90,26 @@ const useAreaMarkersLayer = (map, oms, apiUrl, onUpdateSuccess) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
marker.on("dragend", async e => {
|
if (editMode) {
|
||||||
const { lat, lng } = e.target.getLatLng();
|
marker.on("dragend", async e => {
|
||||||
try {
|
const { lat, lng } = e.target.getLatLng();
|
||||||
await dispatch(
|
try {
|
||||||
updateAreaThunk({
|
await dispatch(
|
||||||
idLocation: item.idLocation,
|
updateAreaThunk({
|
||||||
idMap: item.idMaps,
|
idLocation: item.idLocation,
|
||||||
newCoords: { x: lat, y: lng },
|
idMap: item.idMaps,
|
||||||
})
|
newCoords: { x: lat, y: lng },
|
||||||
).unwrap();
|
})
|
||||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
).unwrap();
|
||||||
console.log("✔️ Koordinaten erfolgreich aktualisiert:", { lat, lng });
|
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;
|
return marker;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getDebugLog } from "@/utils/configUtils.js";
|
||||||
// components/contextmenu/useMapContextMenu.js
|
// components/contextmenu/useMapContextMenu.js
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { zoomIn, zoomOut, centerHere } from "../../utils/zoomAndCenterUtils";
|
import { zoomIn, zoomOut, centerHere } from "../../utils/zoomAndCenterUtils";
|
||||||
@@ -71,7 +72,7 @@ const addItemsToMapContextMenu = (
|
|||||||
if (!menuItemAdded && map && map.contextmenu) {
|
if (!menuItemAdded && map && map.contextmenu) {
|
||||||
const editMode = localStorage.getItem("editMode") === "true";
|
const editMode = localStorage.getItem("editMode") === "true";
|
||||||
if (editMode) {
|
if (editMode) {
|
||||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
if (getDebugLog()) {
|
||||||
console.log("editMode localStorage:", localStorage.getItem("editMode"));
|
console.log("editMode localStorage:", localStorage.getItem("editMode"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,30 +30,30 @@ const useDynamicDeviceLayers = (map, GisSystemStatic, mapLayersVisibility, prior
|
|||||||
if (!map || GisSystemStatic.length === 0) return;
|
if (!map || GisSystemStatic.length === 0) return;
|
||||||
|
|
||||||
GisSystemStatic.forEach(({ Name, IdSystem }) => {
|
GisSystemStatic.forEach(({ Name, IdSystem }) => {
|
||||||
const key = `system-${IdSystem}`; // Einheitlicher Key
|
const key = `system-${IdSystem}`;
|
||||||
|
// LayerGroup immer komplett neu erstellen, um doppelte Marker zu verhindern
|
||||||
if (!layerRefs.current[key]) {
|
if (layerRefs.current[key]) {
|
||||||
layerRefs.current[key] = new L.LayerGroup().addTo(map);
|
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(
|
createAndSetDevices(
|
||||||
IdSystem,
|
IdSystem,
|
||||||
newMarkers => {
|
newMarkers => {
|
||||||
const oldMarkers = markerStates[key];
|
// Füge neue Marker der LayerGroup hinzu (nur Geräte-Marker)
|
||||||
|
if (layerRefs.current[key]) {
|
||||||
// Entferne alte Marker aus Karte und OMS
|
layerRefs.current[key].clearLayers();
|
||||||
if (oldMarkers && Array.isArray(oldMarkers)) {
|
// Nur eindeutige Marker hinzufügen
|
||||||
oldMarkers.forEach(marker => {
|
const uniqueMarkers = Array.isArray(newMarkers) ? Array.from(new Set(newMarkers)) : [];
|
||||||
if (map.hasLayer(marker)) {
|
uniqueMarkers.forEach(marker => {
|
||||||
map.removeLayer(marker);
|
marker.addTo(layerRefs.current[key]);
|
||||||
}
|
|
||||||
if (oms) {
|
|
||||||
oms.removeMarker(marker);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Neue Marker setzen
|
|
||||||
setMarkerStates(prev => ({ ...prev, [key]: newMarkers }));
|
setMarkerStates(prev => ({ ...prev, [key]: newMarkers }));
|
||||||
},
|
},
|
||||||
GisSystemStatic,
|
GisSystemStatic,
|
||||||
@@ -69,20 +69,21 @@ const useDynamicDeviceLayers = (map, GisSystemStatic, mapLayersVisibility, prior
|
|||||||
if (!map) return;
|
if (!map) return;
|
||||||
const editMode = localStorage.getItem("editMode") === "true";
|
const editMode = localStorage.getItem("editMode") === "true";
|
||||||
|
|
||||||
Object.entries(markerStates).forEach(([key, markers]) => {
|
Object.entries(layerRefs.current).forEach(([key, layerGroup]) => {
|
||||||
const isVisible = mapLayersVisibility[key];
|
const isVisible = mapLayersVisibility[key] ?? true;
|
||||||
markers.forEach(marker => {
|
if (editMode || isVisible === false) {
|
||||||
const hasLayer = map.hasLayer(marker);
|
if (map.hasLayer(layerGroup)) {
|
||||||
if (editMode || !isVisible) {
|
map.removeLayer(layerGroup);
|
||||||
if (hasLayer) map.removeLayer(marker);
|
|
||||||
} else {
|
|
||||||
if (!hasLayer) marker.addTo(map);
|
|
||||||
}
|
}
|
||||||
});
|
} 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();
|
const allMarkers = Object.values(markerStates).filter(Array.isArray).flat();
|
||||||
|
|
||||||
checkOverlappingMarkers(map, allMarkers, plusRoundIcon);
|
checkOverlappingMarkers(map, allMarkers, plusRoundIcon);
|
||||||
}, [map, markerStates, mapLayersVisibility]);
|
}, [map, markerStates, mapLayersVisibility]);
|
||||||
|
|
||||||
|
|||||||
0
components/hooks/useStationCache.js
Normal file
18
components/icons/material-symbols/AlarmIcon.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const AlarmIcon = ({ className = "h-8 w-8" }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="red"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
stroke="rgb(0, 174, 239)"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M6 6.9L3.87 4.78l1.41-1.41L7.4 5.5zM13 1v3h-2V1zm7.13 3.78L18 6.9l-1.4-1.4l2.12-2.13zM4.5 10.5v2h-3v-2zm15 0h3v2h-3zM6 20h12a2 2 0 0 1 2 2H4a2 2 0 0 1 2-2m6-15a6 6 0 0 1 6 6v8H6v-8a6 6 0 0 1 6-6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AlarmIcon;
|
||||||
18
components/icons/material-symbols/EditIcon.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const EditIcon = ({ className = "h-8 w-8" }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
stroke="rgb(0, 174, 239)"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default EditIcon;
|
||||||
18
components/icons/material-symbols/EditOffIcon.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const EditOffIcon = ({ className = "h-8 w-8" }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
stroke="rgb(0, 174, 239)"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 21l-3-3m-12.728-.364A9 9 0 015.636 5.636m0 0L3 3l3 3m9.364 9.364L18 21M5.636 5.636L3 3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default EditOffIcon;
|
||||||
18
components/icons/material-symbols/ExpandIcon.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const ExpandIcon = ({ className = "h-8 w-8" }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
stroke="rgb(0, 174, 239)"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ExpandIcon;
|
||||||
18
components/icons/material-symbols/InfoIcon.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const InfoIcon = ({ className = "h-8 w-8" }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
stroke="rgb(0, 174, 239)"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default InfoIcon;
|
||||||
19
components/icons/material-symbols/MapMarkerIcon.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const MapMarkerIcon = ({ className = "h-8 w-8" }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="rgb(0, 174, 239)"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
stroke="rgb(0, 174, 239)"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25s-7.5-4.108-7.5-11.25a7.5 7.5 0 1115 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MapMarkerIcon;
|
||||||
18
components/icons/material-symbols/MenuIcon.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const MenuIcon = ({ className = "h-8 w-8" }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
stroke="rgb(0, 174, 239)"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MenuIcon;
|
||||||
14
components/icons/material-symbols/MinusIcon.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const MinusIcon = ({ className = "h-8 w-8" }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 12h14" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MinusIcon;
|
||||||
14
components/icons/material-symbols/PlusIcon.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const PlusIcon = ({ className = "h-8 w-8" }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 5v14M5 12h14" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default PlusIcon;
|
||||||
18
components/icons/material-symbols/SearchIcon.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const SearchIcon = ({ className = "h-8 w-8" }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
stroke="rgb(0, 174, 239)"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SearchIcon;
|
||||||
@@ -6,10 +6,23 @@ import "leaflet-contextmenu/dist/leaflet.contextmenu.css";
|
|||||||
import "leaflet-contextmenu";
|
import "leaflet-contextmenu";
|
||||||
import "leaflet.smooth_marker_bouncing";
|
import "leaflet.smooth_marker_bouncing";
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
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 PoiUpdateModal from "@/components/pois/poiUpdateModal/PoiUpdateModal.js";
|
||||||
import { ToastContainer, toast } from "react-toastify";
|
import { ToastContainer, toast } from "react-toastify";
|
||||||
import plusRoundIcon from "../icons/devices/overlapping/PlusRoundIcon.js";
|
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 { restoreMapSettings, checkOverlappingMarkers } from "../../utils/mapUtils.js";
|
||||||
|
|
||||||
import addItemsToMapContextMenu from "@/components/contextmenu/useMapContextMenu.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";
|
import CoordinatePopup from "@/components/contextmenu/CoordinatePopup.js";
|
||||||
//----------Ui Widgets----------------
|
//----------Ui Widgets----------------
|
||||||
import MapLayersControlPanel from "@/components/uiWidgets/mapLayersControlPanel/MapLayersControlPanel.js";
|
import MapLayersControlPanel from "@/components/uiWidgets/mapLayersControlPanel/MapLayersControlPanel.js";
|
||||||
|
import AlarmIndicator from "@/components/uiWidgets/AlarmIndicator";
|
||||||
import CoordinateInput from "@/components/uiWidgets/CoordinateInput.js";
|
import CoordinateInput from "@/components/uiWidgets/CoordinateInput.js";
|
||||||
import VersionInfoModal from "@/components/uiWidgets/VersionInfoModal.js";
|
import VersionInfoModal from "@/components/uiWidgets/VersionInfoModal.js";
|
||||||
|
import AreaDropdown from "@/components/uiWidgets/AreaDropdown";
|
||||||
//----------Daten aus API--------------------
|
//----------Daten aus API--------------------
|
||||||
import { fetchPoiDataService } from "@/services/database/pois/fetchPoiDataByIdService.js";
|
import { fetchPoiDataService } from "@/services/database/pois/fetchPoiDataByIdService.js";
|
||||||
import AddPOIModal from "@/components/pois/AddPOIModal.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 { setSelectedPoi } from "@/redux/slices/database/pois/selectedPoiSlice.js";
|
||||||
import { setDisabled } from "@/redux/slices/database/polylines/polylineEventsDisabledSlice.js";
|
import { setDisabled } from "@/redux/slices/database/polylines/polylineEventsDisabledSlice.js";
|
||||||
import { setMapId, setUserId } from "@/redux/slices/urlParameterSlice";
|
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 { setCurrentPoi } from "@/redux/slices/database/pois/currentPoiSlice.js";
|
||||||
import { selectGisLines } from "@/redux/slices/database/polylines/gisLinesSlice";
|
import { selectGisLines } from "@/redux/slices/database/polylines/gisLinesSlice";
|
||||||
import { selectGisLinesStatus } from "@/redux/slices/webservice/gisLinesStatusSlice";
|
import { selectGisLinesStatus } from "@/redux/slices/webservice/gisLinesStatusSlice";
|
||||||
@@ -54,6 +71,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
selectPolylineVisible,
|
selectPolylineVisible,
|
||||||
setPolylineVisible,
|
setPolylineVisible,
|
||||||
|
initializePolylineFromLocalStorageThunk,
|
||||||
} from "@/redux/slices/database/polylines/polylineLayerVisibleSlice.js";
|
} from "@/redux/slices/database/polylines/polylineLayerVisibleSlice.js";
|
||||||
import { selectGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice.js";
|
import { selectGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice.js";
|
||||||
import {
|
import {
|
||||||
@@ -84,6 +102,7 @@ import { monitorHeapWithRedux } from "@/utils/common/monitorMemory";
|
|||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
|
|
||||||
import { setGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice.js";
|
import { setGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice.js";
|
||||||
|
import { getDebugLog } from "../../utils/configUtils";
|
||||||
//-----------------------------------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------------------------------
|
||||||
const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
||||||
//-------------------------------
|
//-------------------------------
|
||||||
@@ -96,6 +115,14 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
const countdownActive = useSelector(state => state.polylineContextMenu.countdownActive);
|
const countdownActive = useSelector(state => state.polylineContextMenu.countdownActive);
|
||||||
const isPolylineContextMenuOpen = useSelector(state => state.polylineContextMenu.isOpen);
|
const isPolylineContextMenuOpen = useSelector(state => state.polylineContextMenu.isOpen);
|
||||||
const polylineVisible = useSelector(selectPolylineVisible);
|
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 isPoiTypLoaded = useSelector(state => state.poiTypes.status === "succeeded");
|
||||||
const statusMeasurements = useSelector(state => state.gisStationsMeasurements.status);
|
const statusMeasurements = useSelector(state => state.gisStationsMeasurements.status);
|
||||||
@@ -106,8 +133,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
const poiLayerVisible = useSelector(state => state.poiLayerVisible.visible);
|
const poiLayerVisible = useSelector(state => state.poiLayerVisible.visible);
|
||||||
const zoomTrigger = useSelector(state => state.zoomTrigger.trigger);
|
const zoomTrigger = useSelector(state => state.zoomTrigger.trigger);
|
||||||
const poiReadTrigger = useSelector(state => state.poiReadFromDbTrigger.trigger);
|
const poiReadTrigger = useSelector(state => state.poiReadFromDbTrigger.trigger);
|
||||||
const GisStationsStaticDistrict = useSelector(selectGisStationsStaticDistrict);
|
// entfernt, da weiter unten dynamisch und mit Fallback deklariert
|
||||||
const GisSystemStatic = useSelector(selectGisSystemStatic);
|
|
||||||
const gisSystemStaticStatus = useSelector(state => state.gisSystemStatic.status);
|
const gisSystemStaticStatus = useSelector(state => state.gisSystemStatic.status);
|
||||||
const polylineEventsDisabled = useSelector(state => state.polylineEventsDisabled.disabled);
|
const polylineEventsDisabled = useSelector(state => state.polylineEventsDisabled.disabled);
|
||||||
const mapLayersVisibility = useSelector(selectMapLayersState) || {};
|
const mapLayersVisibility = useSelector(selectMapLayersState) || {};
|
||||||
@@ -117,6 +143,41 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
const { data: gisLinesStatusData, status: statusGisLinesStatus } = useSelector(
|
const { data: gisLinesStatusData, status: statusGisLinesStatus } = useSelector(
|
||||||
selectGisLinesStatusFromWebservice
|
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 poiIconsData = useSelector(selectPoiIconsData);
|
||||||
const poiIconsStatus = useSelector(selectPoiIconsStatus);
|
const poiIconsStatus = useSelector(selectPoiIconsStatus);
|
||||||
const poiTypData = useSelector(selectPoiTypData);
|
const poiTypData = useSelector(selectPoiTypData);
|
||||||
@@ -135,10 +196,75 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
const [showVersionInfoModal, setShowVersionInfoModal] = useState(false);
|
const [showVersionInfoModal, setShowVersionInfoModal] = useState(false);
|
||||||
const [poiTypMap, setPoiTypMap] = useState(new Map());
|
const [poiTypMap, setPoiTypMap] = useState(new Map());
|
||||||
const [showPopup, setShowPopup] = useState(false);
|
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 poiLayerRef = useRef(null); // Referenz auf die Layer-Gruppe für Datenbank-Marker
|
||||||
const mapRef = useRef(null); // Referenz auf das DIV-Element der Karte
|
const mapRef = useRef(null); // Referenz auf das DIV-Element der Karte
|
||||||
const [map, setMap] = useState(null); // Zustand der Karteninstanz
|
const [map, setMap] = useState(null); // Zustand der Karteninstanz
|
||||||
const [oms, setOms] = useState(null); // State für OMS-Instanz
|
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----------------
|
//-----userRights----------------
|
||||||
const isRightsLoaded = useSelector(
|
const isRightsLoaded = useSelector(
|
||||||
@@ -170,6 +296,14 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
const [popupCoordinates, setPopupCoordinates] = useState(null);
|
const [popupCoordinates, setPopupCoordinates] = useState(null);
|
||||||
const [popupVisible, setPopupVisible] = useState(false);
|
const [popupVisible, setPopupVisible] = useState(false);
|
||||||
const [poiData, setPoiData] = useState([]);
|
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 = () => {
|
const openVersionInfoModal = () => {
|
||||||
setShowVersionInfoModal(true);
|
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 => {
|
const handleCoordinatesSubmit = coords => {
|
||||||
@@ -200,6 +360,16 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
//-----------------------------Map Initialisierung----------------
|
//-----------------------------Map Initialisierung----------------
|
||||||
|
// Default map options for Leaflet
|
||||||
|
const mapOptions = {
|
||||||
|
center: currentCenter,
|
||||||
|
zoom: currentZoom,
|
||||||
|
zoomControl: true,
|
||||||
|
contextmenu: true,
|
||||||
|
contextmenuWidth: 180,
|
||||||
|
contextmenuItems: [],
|
||||||
|
};
|
||||||
|
|
||||||
useInitializeMap(
|
useInitializeMap(
|
||||||
map,
|
map,
|
||||||
mapRef,
|
mapRef,
|
||||||
@@ -208,10 +378,85 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
setMenuItemAdded,
|
setMenuItemAdded,
|
||||||
addItemsToMapContextMenu,
|
addItemsToMapContextMenu,
|
||||||
hasRights,
|
hasRights,
|
||||||
value => dispatch(setDisabled(value))
|
value => dispatch(setDisabled(value)),
|
||||||
|
mapOptions // pass mapOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
//-------------------------React Hooks--------------------------------
|
//-------------------------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(() => {
|
useEffect(() => {
|
||||||
if (linesData && Array.isArray(linesData)) {
|
if (linesData && Array.isArray(linesData)) {
|
||||||
const transformed = linesData.map(item => ({
|
const transformed = linesData.map(item => ({
|
||||||
@@ -222,6 +467,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
setLinePositions(transformed);
|
setLinePositions(transformed);
|
||||||
}
|
}
|
||||||
}, [linesData]);
|
}, [linesData]);
|
||||||
|
|
||||||
//--------------------------------------------
|
//--------------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchPoiIconsDataThunk());
|
dispatch(fetchPoiIconsDataThunk());
|
||||||
@@ -309,72 +555,106 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
//Tooltip an mouse position anzeigen für die Linien
|
//Tooltip an mouse position anzeigen für die Linien
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map) return;
|
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
|
// vorherige Marker & Polylinien vollständig bereinigen
|
||||||
|
(Array.isArray(markers) ? markers : []).forEach(marker => {
|
||||||
markers.forEach(marker => {
|
|
||||||
marker.remove();
|
marker.remove();
|
||||||
});
|
});
|
||||||
cleanupPolylinesForMemory(polylines, map);
|
cleanupPolylinesForMemory(polylines, map);
|
||||||
|
console.log("[MapComponent/useEffect] Nach cleanupPolylinesForMemory, polylines:", polylines);
|
||||||
|
|
||||||
// Setze neue Marker und Polylinien mit den aktuellen Daten
|
// Setze neue Marker und Polylinien mit den aktuellen Daten (asynchron!)
|
||||||
const { markers: newMarkers, polylines: newPolylines } = setupPolylines(
|
const updatePolylines = async () => {
|
||||||
map,
|
if (polylineVisible) {
|
||||||
linePositions,
|
const { markers: newMarkers, polylines: newPolylines } = await setupPolylines(
|
||||||
lineColors,
|
map,
|
||||||
tooltipContents,
|
linePositions,
|
||||||
setNewCoords,
|
lineColors,
|
||||||
tempMarker,
|
tooltipContents,
|
||||||
currentZoom,
|
setNewCoords,
|
||||||
currentCenter,
|
tempMarker,
|
||||||
polylineVisible // kommt aus Redux
|
currentZoom,
|
||||||
);
|
currentCenter,
|
||||||
|
polylineVisible
|
||||||
|
);
|
||||||
|
|
||||||
newPolylines.forEach((polyline, index) => {
|
(Array.isArray(newPolylines) ? newPolylines : []).forEach((polyline, index) => {
|
||||||
const tooltipContent =
|
const tooltipContent =
|
||||||
tooltipContents[`${linePositions[index].idLD}-${linePositions[index].idModul}`] ||
|
tooltipContents[`${linePositions[index].idLD}-${linePositions[index].idModul}`] ||
|
||||||
"Die Linie ist noch nicht in Webservice vorhanden oder bekommt keine Daten";
|
"Die Linie ist noch nicht in Webservice vorhanden oder bekommt keine Daten";
|
||||||
|
|
||||||
polyline.bindTooltip(tooltipContent, {
|
polyline.bindTooltip(tooltipContent, {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
direction: "auto",
|
direction: "auto",
|
||||||
sticky: true,
|
sticky: true,
|
||||||
offset: [20, 0],
|
offset: [20, 0],
|
||||||
pane: "tooltipPane",
|
pane: "tooltipPane",
|
||||||
});
|
});
|
||||||
|
|
||||||
polyline.on("mouseover", e => {
|
polyline.on("mouseover", e => {
|
||||||
const tooltip = polyline.getTooltip();
|
const tooltip = polyline.getTooltip();
|
||||||
if (tooltip) {
|
if (tooltip) {
|
||||||
const mousePos = e.containerPoint;
|
const mousePos = e.containerPoint;
|
||||||
const mapSize = map.getSize();
|
const mapSize = map.getSize();
|
||||||
|
|
||||||
let direction = "right";
|
let direction = "right";
|
||||||
|
|
||||||
if (mousePos.x > mapSize.x - 100) {
|
if (mousePos.x > mapSize.x - 100) {
|
||||||
direction = "left";
|
direction = "left";
|
||||||
} else if (mousePos.x < 100) {
|
} else if (mousePos.x < 100) {
|
||||||
direction = "right";
|
direction = "right";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mousePos.y > mapSize.y - 100) {
|
if (mousePos.y > mapSize.y - 100) {
|
||||||
direction = "top";
|
direction = "top";
|
||||||
} else if (mousePos.y < 100) {
|
} else if (mousePos.y < 100) {
|
||||||
direction = "bottom";
|
direction = "bottom";
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip.options.direction = direction;
|
tooltip.options.direction = direction;
|
||||||
polyline.openTooltip(e.latlng);
|
polyline.openTooltip(e.latlng);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
polyline.on("mouseout", () => {
|
||||||
|
polyline.closeTooltip();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cleanupMarkers(markers, oms);
|
||||||
|
setMarkers(newMarkers);
|
||||||
|
setPolylines(newPolylines);
|
||||||
|
console.log("[MapComponent/useEffect] setPolylines (sichtbar):", newPolylines);
|
||||||
|
} else {
|
||||||
|
// Entferne wirklich alle Polylinien-Layer von der Karte
|
||||||
|
if (map) {
|
||||||
|
map.eachLayer(layer => {
|
||||||
|
if (layer instanceof L.Polyline) {
|
||||||
|
map.removeLayer(layer);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
cleanupPolylinesForMemory(polylines, map);
|
||||||
|
setPolylines([]);
|
||||||
polyline.on("mouseout", () => {
|
console.log("[MapComponent/useEffect] setPolylines ([]), alle Polylinien entfernt");
|
||||||
polyline.closeTooltip();
|
}
|
||||||
});
|
};
|
||||||
});
|
updatePolylines();
|
||||||
cleanupMarkers(markers, oms);
|
|
||||||
setMarkers(newMarkers);
|
|
||||||
setPolylines(newPolylines);
|
|
||||||
}, [
|
}, [
|
||||||
map,
|
map,
|
||||||
linePositions,
|
linePositions,
|
||||||
@@ -384,6 +664,8 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
newCoords,
|
newCoords,
|
||||||
tempMarker,
|
tempMarker,
|
||||||
polylineVisible,
|
polylineVisible,
|
||||||
|
isTalasAllowed,
|
||||||
|
poiLayerVisible,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
//--------------------------------------------
|
//--------------------------------------------
|
||||||
@@ -391,7 +673,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
//Test in useEffect
|
//Test in useEffect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (map) {
|
if (map) {
|
||||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
if (getDebugLog()) {
|
||||||
console.log("🗺️ Map-Einstellungen werden wiederhergestellt...");
|
console.log("🗺️ Map-Einstellungen werden wiederhergestellt...");
|
||||||
}
|
}
|
||||||
restoreMapSettings(map);
|
restoreMapSettings(map);
|
||||||
@@ -400,7 +682,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
//--------------------------------------------
|
//--------------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (map) {
|
if (map) {
|
||||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
if (getDebugLog()) {
|
||||||
console.log("map in MapComponent: ", map);
|
console.log("map in MapComponent: ", map);
|
||||||
}
|
}
|
||||||
const handleMapMoveEnd = event => {
|
const handleMapMoveEnd = event => {
|
||||||
@@ -433,7 +715,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
const station = points.find(s => s.Area_Name === selectedArea);
|
const station = points.find(s => s.Area_Name === selectedArea);
|
||||||
|
|
||||||
if (station) {
|
if (station) {
|
||||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
if (getDebugLog()) {
|
||||||
console.log("📌 Gefundene Station:", station);
|
console.log("📌 Gefundene Station:", station);
|
||||||
}
|
}
|
||||||
map.flyTo([station.X, station.Y], 14);
|
map.flyTo([station.X, station.Y], 14);
|
||||||
@@ -479,7 +761,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
//--------------------------------------------
|
//--------------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (map) {
|
if (map) {
|
||||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
if (getDebugLog()) {
|
||||||
console.log("6- Karteninstanz (map) wurde jetzt erfolgreich initialisiert");
|
console.log("6- Karteninstanz (map) wurde jetzt erfolgreich initialisiert");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -492,7 +774,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
map.whenReady(() => {
|
map.whenReady(() => {
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
if (map.contextmenu) {
|
if (map.contextmenu) {
|
||||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
if (getDebugLog()) {
|
||||||
console.log("Contextmenu ist vorhanden");
|
console.log("Contextmenu ist vorhanden");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -523,7 +805,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
const handleLocationUpdate = async (idLocation, idMap, newCoords) => {
|
const handleLocationUpdate = async (idLocation, idMap, newCoords) => {
|
||||||
try {
|
try {
|
||||||
await dispatch(updateAreaThunk({ idLocation, idMap, newCoords })).unwrap();
|
await dispatch(updateAreaThunk({ idLocation, idMap, newCoords })).unwrap();
|
||||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
if (getDebugLog()) {
|
||||||
console.log("Koordinaten erfolgreich aktualisiert:", result);
|
console.log("Koordinaten erfolgreich aktualisiert:", result);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -547,14 +829,14 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
// Entferne alle Marker aus der Karte
|
// Entferne alle Marker aus der Karte
|
||||||
if (!map) return; // Sicherstellen, dass map existiert
|
if (!map) return; // Sicherstellen, dass map existiert
|
||||||
|
|
||||||
areaMarkers.forEach(marker => {
|
(Array.isArray(areaMarkers) ? areaMarkers : []).forEach(marker => {
|
||||||
if (map.hasLayer(marker)) {
|
if (map.hasLayer(marker)) {
|
||||||
map.removeLayer(marker);
|
map.removeLayer(marker);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Wenn editMode aktiviert ist, füge die Marker hinzu und aktiviere Dragging
|
// 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)) {
|
if (!map.hasLayer(marker)) {
|
||||||
marker.addTo(map); // Layer hinzufügen
|
marker.addTo(map); // Layer hinzufügen
|
||||||
}
|
}
|
||||||
@@ -615,11 +897,10 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
//--------------------------------------------
|
//--------------------------------------------
|
||||||
// Beim ersten Client-Render den Wert aus localStorage laden
|
|
||||||
useEffect(() => {
|
// (Initialisierung erfolgt in MapLayersControlPanel)
|
||||||
const storedPolylineVisible = localStorage.getItem("polylineVisible") === "true";
|
//--------------------------------------------
|
||||||
dispatch(setPolylineVisible(storedPolylineVisible));
|
// MapComponent reagiert nicht mehr direkt auf localStorage-Events für polylineVisible
|
||||||
}, [dispatch]);
|
|
||||||
//--------------------------------------------
|
//--------------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statusStaticDistrict === "idle") {
|
if (statusStaticDistrict === "idle") {
|
||||||
@@ -693,7 +974,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
// console.log(`⏳ Redux Countdown: ${countdown} Sekunden`);
|
// console.log(`⏳ Redux Countdown: ${countdown} Sekunden`);
|
||||||
|
|
||||||
if (countdown <= 2) {
|
if (countdown <= 2) {
|
||||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
if (getDebugLog()) {
|
||||||
console.log("🚀 Kontextmenü wird wegen Countdown < 2 geschlossen.");
|
console.log("🚀 Kontextmenü wird wegen Countdown < 2 geschlossen.");
|
||||||
}
|
}
|
||||||
dispatch(closePolylineContextMenu());
|
dispatch(closePolylineContextMenu());
|
||||||
@@ -738,7 +1019,9 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (poiTypStatus === "succeeded" && Array.isArray(poiTypData)) {
|
if (poiTypStatus === "succeeded" && Array.isArray(poiTypData)) {
|
||||||
const map = new Map();
|
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);
|
setPoiTypMap(map);
|
||||||
}
|
}
|
||||||
}, [poiTypData, poiTypStatus]);
|
}, [poiTypData, poiTypStatus]);
|
||||||
@@ -749,30 +1032,6 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
}
|
}
|
||||||
}, [poiIconsData, poiIconsStatus]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
@@ -848,7 +1107,45 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
}, [GisStationsStaticDistrict]);
|
}, [GisStationsStaticDistrict]);
|
||||||
const { Points = [] } = useSelector(selectGisStationsStaticDistrict);
|
const { Points = [] } = useSelector(selectGisStationsStaticDistrict);
|
||||||
useEffect(() => {}, [triggerUpdate]);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -903,28 +1200,136 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{GisStationsStaticDistrict && GisStationsStaticDistrict.Points?.length > 0 && (
|
{GisStationsStaticDistrict &&
|
||||||
<MapLayersControlPanel className="z-50" />
|
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>
|
<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} />
|
<CoordinatePopup isOpen={isPopupOpen} coordinates={currentCoordinates} onClose={closePopup} />
|
||||||
|
|
||||||
<div className="absolute bottom-3 left-3 w-72 p-4 bg-white rounded-lg shadow-md z-50">
|
{showAppInfoCard && (
|
||||||
<div className="flex justify-between items-center">
|
<div className="absolute top-16 right-3 w-72 p-4 bg-white rounded-lg shadow-md z-50">
|
||||||
<div>
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-black text-lg font-semibold"> TALAS.Map </span>
|
<div>
|
||||||
<br />
|
<span className="text-black text-lg font-semibold"> TALAS.Map </span>
|
||||||
<span className="text-black text-lg">Version {appVersion}</span>
|
<br />
|
||||||
</div>
|
<span className="text-black text-lg">Version {appVersion}</span>
|
||||||
<div>
|
</div>
|
||||||
<button onClick={openVersionInfoModal}>
|
<div>
|
||||||
<InformationCircleIcon className="text-blue-900 h-8 w-8 pr-1" title="Weitere Infos" />
|
<button onClick={openVersionInfoModal}>
|
||||||
</button>
|
<InfoIcon className="h-8 w-8 pr-1" title="Weitere Infos" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<VersionInfoModal
|
<VersionInfoModal
|
||||||
showVersionInfoModal={showVersionInfoModal}
|
showVersionInfoModal={showVersionInfoModal}
|
||||||
closeVersionInfoModal={closeVersionInfoModal}
|
closeVersionInfoModal={closeVersionInfoModal}
|
||||||
|
|||||||
@@ -2,12 +2,58 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { initializeMap } from "../../../utils/initializeMap";
|
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(() => {
|
useEffect(() => {
|
||||||
if (mapRef.current && !map) {
|
let cancelled = false;
|
||||||
initializeMap(mapRef, setMap, setOms, setMenuItemAdded, addItemsToMapContextMenu, hasRights, setPolylineEventsDisabled);
|
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;
|
export default useInitializeMap;
|
||||||
|
|||||||
53
components/uiWidgets/AlarmIndicator.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React from "react";
|
||||||
|
import AlarmIcon from "@/components/icons/material-symbols/AlarmIcon";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import styles from "./AlarmIndicator.module.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AlarmIndicator zeigt ein Alarm-Icon, das bei Klick den AlarmLink in neuem Tab öffnet.
|
||||||
|
* @param {boolean} hasAlarm - Ob ein Alarm aktiv ist
|
||||||
|
* @param {string} alarmLink - Link zur Alarm-Detailseite
|
||||||
|
* @param {string} [alarmText] - Optionaler Tooltip-Text
|
||||||
|
* @param {string} [animation] - "shake" | "rotate" | "blink" | "pulse" (default: "shake")
|
||||||
|
* @param {number} [pulseDuration] - Animationsdauer in Sekunden (default: 0.5)
|
||||||
|
*/
|
||||||
|
const AlarmIndicator = ({
|
||||||
|
hasAlarm,
|
||||||
|
alarmLink,
|
||||||
|
alarmText,
|
||||||
|
animation = "pulse",
|
||||||
|
pulseDuration = 0.5, // default: 1
|
||||||
|
}) => {
|
||||||
|
if (!hasAlarm || !alarmLink) return null;
|
||||||
|
// Animation-Klasse wählen
|
||||||
|
let animationClass = styles.fastPulse;
|
||||||
|
let style = { animationDuration: `${pulseDuration}s` };
|
||||||
|
if (animation === "shake") {
|
||||||
|
animationClass = styles.shakeAlarm;
|
||||||
|
} else if (animation === "rotate") {
|
||||||
|
animationClass = styles.rotateAlarm;
|
||||||
|
} else if (animation === "blink") {
|
||||||
|
animationClass = styles.blinkAlarm;
|
||||||
|
} else if (animation === "pulse") {
|
||||||
|
animationClass = styles.fastPulse;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Tooltip title={alarmText || "Alarm aktiv"}>
|
||||||
|
<span
|
||||||
|
style={{ cursor: "pointer", color: "red" }}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
window.open(alarmLink, "_blank");
|
||||||
|
}}
|
||||||
|
aria-label="Alarm aktiv"
|
||||||
|
>
|
||||||
|
<AlarmIcon
|
||||||
|
className={`h-14 w-14 mr-6 ${animationClass} text-red-800 bg-red-300`}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlarmIndicator;
|
||||||
62
components/uiWidgets/AlarmIndicator.module.css
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
.fastPulse {
|
||||||
|
animation: fast-pulse 0.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fast-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shakeAlarm {
|
||||||
|
animation: shake-alarm 0.5s infinite cubic-bezier(0.36, 0.07, 0.19, 0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake-alarm {
|
||||||
|
10%,
|
||||||
|
90% {
|
||||||
|
transform: translateX(-1px);
|
||||||
|
}
|
||||||
|
20%,
|
||||||
|
80% {
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
30%,
|
||||||
|
50%,
|
||||||
|
70% {
|
||||||
|
transform: translateX(-4px);
|
||||||
|
}
|
||||||
|
40%,
|
||||||
|
60% {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotateAlarm {
|
||||||
|
animation: rotate-alarm 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate-alarm {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blinkAlarm {
|
||||||
|
animation: blink-alarm 0.7s steps(2, start) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink-alarm {
|
||||||
|
to {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
79
components/uiWidgets/AreaDropdown.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// /components/uiWidgets/AreaDropdown.js
|
||||||
|
import React, { useEffect, useMemo } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { setSelectedArea } from "@/redux/slices/selectedAreaSlice";
|
||||||
|
import { selectGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice";
|
||||||
|
import { selectGisSystemStatic } from "@/redux/slices/webservice/gisSystemStaticSlice";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kleines Dropdown zur Auswahl der Station (Area_Name),
|
||||||
|
* nutzt dieselbe Datenquelle wie das MapLayersControlPanel.
|
||||||
|
*/
|
||||||
|
const AreaDropdown = ({ onClose }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const GisStationsStaticDistrict = useSelector(selectGisStationsStaticDistrict) || {};
|
||||||
|
const GisSystemStatic = useSelector(selectGisSystemStatic) || [];
|
||||||
|
|
||||||
|
// Erlaubte Systeme: Allow === 1 und Map === 1
|
||||||
|
const allowedSystems = useMemo(() => {
|
||||||
|
return new Set(
|
||||||
|
(Array.isArray(GisSystemStatic) ? GisSystemStatic : [])
|
||||||
|
.filter(sys => sys.Allow === 1 && sys.Map === 1)
|
||||||
|
.map(sys => sys.IdSystem)
|
||||||
|
);
|
||||||
|
}, [GisSystemStatic]);
|
||||||
|
|
||||||
|
// Uniqe Areas basierend auf Allowed Systems
|
||||||
|
const areaOptions = useMemo(() => {
|
||||||
|
const points = GisStationsStaticDistrict?.Points || [];
|
||||||
|
const seen = new Set();
|
||||||
|
const filtered = points.filter(p => {
|
||||||
|
if (!p?.Area_Name) return false;
|
||||||
|
if (!allowedSystems.has(p.System)) return false;
|
||||||
|
if (seen.has(p.Area_Name)) return false;
|
||||||
|
seen.add(p.Area_Name);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return filtered.map(p => ({ label: p.Area_Name, value: p.IdLD }));
|
||||||
|
}, [GisStationsStaticDistrict, allowedSystems]);
|
||||||
|
|
||||||
|
const handleChange = e => {
|
||||||
|
const selectedIndex = e.target.options.selectedIndex;
|
||||||
|
const label = e.target.options[selectedIndex].text;
|
||||||
|
dispatch(setSelectedArea(label));
|
||||||
|
onClose?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Schließe mit ESC
|
||||||
|
useEffect(() => {
|
||||||
|
const onKey = e => {
|
||||||
|
if (e.key === "Escape") onClose?.();
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", onKey);
|
||||||
|
return () => window.removeEventListener("keydown", onKey);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute top-16 right-3 w-72 z-50 bg-white rounded-lg shadow-md">
|
||||||
|
<div className="flex flex-col gap-4 p-4">
|
||||||
|
<div className="text-sm font-semibold mb-2">Station wählen</div>
|
||||||
|
<select
|
||||||
|
onChange={handleChange}
|
||||||
|
className="border p-2 rounded w-full"
|
||||||
|
defaultValue="__default__"
|
||||||
|
>
|
||||||
|
<option value="__default__" disabled>
|
||||||
|
Bitte wählen…
|
||||||
|
</option>
|
||||||
|
{areaOptions.map(opt => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AreaDropdown;
|
||||||
@@ -4,7 +4,7 @@ import React, { useState } from "react";
|
|||||||
const CoordinateInput = ({ onCoordinatesSubmit }) => {
|
const CoordinateInput = ({ onCoordinatesSubmit }) => {
|
||||||
const [coordinates, setCoordinates] = useState("");
|
const [coordinates, setCoordinates] = useState("");
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (onCoordinatesSubmit) {
|
if (onCoordinatesSubmit) {
|
||||||
onCoordinatesSubmit(coordinates);
|
onCoordinatesSubmit(coordinates);
|
||||||
@@ -12,9 +12,18 @@ const CoordinateInput = ({ onCoordinatesSubmit }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="fixed top-5 left-5 z-50 bg-white shadow-lg rounded-lg p-4 w-72">
|
<form
|
||||||
<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" />
|
onSubmit={handleSubmit}
|
||||||
<button type="submit" className="bg-blue-500 text-white p-2 rounded w-full hover:bg-blue-600">
|
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
|
Zu Marker zoomen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const VersionInfoModal = ({ showVersionInfoModal, closeVersionInfoModal, APP_VER
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={closeVersionInfoModal}
|
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
|
Schließen
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
15
components/uiWidgets/alarm-indicator-fastpulse.css
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
@keyframes fast-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fast-pulse {
|
||||||
|
animation: fast-pulse 0.5s infinite;
|
||||||
|
}
|
||||||
163
components/uiWidgets/baseMapPanel/BaseMapPanel.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
// components/uiWidgets/baseMapPanel/BaseMapPanel.js , aus rechliche Grunde nur OSM, dieses Feature ist optional, aktuell nicht genutzt
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import L from "leaflet";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
|
||||||
|
// Minimal, safe defaults (no API key required). You can extend via config later.
|
||||||
|
const DEFAULT_BASE_LAYERS = [
|
||||||
|
{
|
||||||
|
id: "osm-standard",
|
||||||
|
name: "Standard",
|
||||||
|
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||||
|
attribution: "© OpenStreetMap contributors",
|
||||||
|
minZoom: 0,
|
||||||
|
maxZoom: 19,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "osm-humanitarian",
|
||||||
|
name: "Humanitarian",
|
||||||
|
url: "https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png",
|
||||||
|
attribution: "© OpenStreetMap contributors, Humanitarian OpenStreetMap Team",
|
||||||
|
minZoom: 0,
|
||||||
|
maxZoom: 19,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cyclosm",
|
||||||
|
name: "CyclOSM",
|
||||||
|
url: "https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png",
|
||||||
|
attribution: "© OpenStreetMap contributors, CyclOSM",
|
||||||
|
minZoom: 0,
|
||||||
|
maxZoom: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "carto-light",
|
||||||
|
name: "Carto Light",
|
||||||
|
url: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
|
||||||
|
attribution: "© OpenStreetMap contributors, © CARTO",
|
||||||
|
subdomains: "abcd",
|
||||||
|
minZoom: 0,
|
||||||
|
maxZoom: 20,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function getCurrentTileLayer(map) {
|
||||||
|
let found = null;
|
||||||
|
if (!map) return null;
|
||||||
|
map.eachLayer(layer => {
|
||||||
|
if (!found && layer instanceof L.TileLayer) {
|
||||||
|
found = layer;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BaseMapPanel({ map, onSelect, onClose, initialId }) {
|
||||||
|
const [activeId, setActiveId] = useState(initialId || null);
|
||||||
|
const layerCacheRef = useRef({});
|
||||||
|
const bases = useMemo(() => {
|
||||||
|
try {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const cfg = window.__leafletConfig;
|
||||||
|
if (cfg && cfg.tileSources) {
|
||||||
|
return Object.entries(cfg.tileSources).map(([key, ts]) => ({
|
||||||
|
id: key,
|
||||||
|
name: ts.name || key,
|
||||||
|
url: ts.url,
|
||||||
|
attribution: ts.attribution || "© OpenStreetMap contributors",
|
||||||
|
minZoom: ts.minZoom ?? cfg.minZoom ?? 0,
|
||||||
|
maxZoom: ts.maxZoom ?? cfg.maxZoom ?? 19,
|
||||||
|
subdomains: ts.subdomains,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return DEFAULT_BASE_LAYERS;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const applyBase = id => {
|
||||||
|
if (!map) return;
|
||||||
|
const base = bases.find(b => b.id === id) || bases[0];
|
||||||
|
if (!base) return;
|
||||||
|
|
||||||
|
// Remove current tile layer
|
||||||
|
const current = getCurrentTileLayer(map);
|
||||||
|
if (current) {
|
||||||
|
try {
|
||||||
|
map.removeLayer(current);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create the new layer
|
||||||
|
let nextLayer = layerCacheRef.current[id];
|
||||||
|
if (!nextLayer) {
|
||||||
|
nextLayer = L.tileLayer(base.url, {
|
||||||
|
attribution: base.attribution,
|
||||||
|
subdomains: base.subdomains || "abc",
|
||||||
|
tileSize: 256,
|
||||||
|
minZoom: base.minZoom ?? 0,
|
||||||
|
maxZoom: base.maxZoom ?? 19,
|
||||||
|
noWrap: true,
|
||||||
|
// Ensure base tiles stay behind overlays
|
||||||
|
zIndex: 1,
|
||||||
|
});
|
||||||
|
layerCacheRef.current[id] = nextLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextLayer.addTo(map);
|
||||||
|
try {
|
||||||
|
if (typeof map.setMinZoom === "function") map.setMinZoom(base.minZoom ?? 0);
|
||||||
|
if (typeof map.setMaxZoom === "function") map.setMaxZoom(base.maxZoom ?? 19);
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.__tileSourceMinZoom = base.minZoom ?? 0;
|
||||||
|
window.__tileSourceMaxZoom = base.maxZoom ?? 19;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
setActiveId(id);
|
||||||
|
try {
|
||||||
|
localStorage.setItem("baseMapId", id);
|
||||||
|
} catch (_) {}
|
||||||
|
onSelect && onSelect(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = (() => {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem("baseMapId");
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
const targetId = initialId || saved || bases[0]?.id;
|
||||||
|
if (targetId) {
|
||||||
|
applyBase(targetId);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [map]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute top-16 right-3 z-50 w-64 bg-white rounded-lg shadow-lg p-3">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-semibold">Map Layers</h3>
|
||||||
|
<button onClick={onClose} aria-label="Schließen" title="Schließen">
|
||||||
|
<Icon icon="material-symbols:close-rounded" className="h-5 w-5 text-gray-700" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{bases.map(b => (
|
||||||
|
<button
|
||||||
|
key={b.id}
|
||||||
|
onClick={() => applyBase(b.id)}
|
||||||
|
className={`text-left rounded-md border p-2 hover:bg-gray-50 ${
|
||||||
|
activeId === b.id ? "ring-2 ring-blue-500" : ""
|
||||||
|
}`}
|
||||||
|
title={b.name}
|
||||||
|
>
|
||||||
|
<div className="font-medium text-sm">{b.name}</div>
|
||||||
|
<div className="text-[10px] text-gray-500 truncate">{b.url}</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
// /components/uiWidgets/mapLayersControlPanel/MapLayersControlPanel.js
|
// /components/uiWidgets/mapLayersControlPanel/MapLayersControlPanel.js
|
||||||
|
import { getDebugLog } from "../../../utils/configUtils";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { setSelectedArea } from "@/redux/slices/selectedAreaSlice";
|
import { setSelectedArea } from "@/redux/slices/selectedAreaSlice";
|
||||||
import EditModeToggle from "@/components/uiWidgets/mapLayersControlPanel/EditModeToggle";
|
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 { setVisible } from "@/redux/slices/database/pois/poiLayerVisibleSlice";
|
||||||
import { incrementZoomTrigger } from "@/redux/slices/zoomTriggerSlice";
|
import { incrementZoomTrigger } from "@/redux/slices/zoomTriggerSlice";
|
||||||
|
|
||||||
function MapLayersControlPanel() {
|
function MapLayersControlPanel({ handlePolylineCheckboxChange }) {
|
||||||
const [editMode, setEditMode] = useState(false); // Zustand für editMode
|
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 poiVisible = useSelector(state => state.poiLayerVisible.visible);
|
||||||
const setPoiVisible = value => dispatch(setVisible(value));
|
const setPoiVisible = value => dispatch(setVisible(value));
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -24,46 +27,60 @@ function MapLayersControlPanel() {
|
|||||||
const GisStationsStaticDistrict = useSelector(selectGisStationsStaticDistrict) || [];
|
const GisStationsStaticDistrict = useSelector(selectGisStationsStaticDistrict) || [];
|
||||||
const GisSystemStatic = useSelector(selectGisSystemStatic) || [];
|
const GisSystemStatic = useSelector(selectGisSystemStatic) || [];
|
||||||
|
|
||||||
const polylineVisible = useSelector(selectPolylineVisible);
|
// Debug: Kabelstrecken state verfolgen
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("🎯 kabelstreckenVisible state changed to:", kabelstreckenVisible);
|
||||||
|
}, [kabelstreckenVisible]);
|
||||||
|
|
||||||
const handlePolylineCheckboxChange = event => {
|
// Prüfen, ob TALAS (IdSystem 1) erlaubt & sichtbar auf Karte (Allow + Map)
|
||||||
const checked = event.target.checked;
|
const isTalasAllowed = Array.isArray(GisSystemStatic)
|
||||||
dispatch(setPolylineVisible(checked));
|
? GisSystemStatic.some(
|
||||||
localStorage.setItem("polylineVisible", checked);
|
system => system.IdSystem === 1 && system.Allow === 1 && system.Map === 1
|
||||||
|
)
|
||||||
|
: false;
|
||||||
|
|
||||||
if (checked) {
|
// handlePolylineCheckboxChange kommt jetzt als Prop von MapComponent
|
||||||
dispatch(setLayerVisibility({ layer: "TALAS", visibility: true }));
|
|
||||||
localStorage.setItem(
|
|
||||||
"mapLayersVisibility",
|
|
||||||
JSON.stringify({ ...mapLayersVisibility, TALAS: true })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
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");
|
const storedPoiVisible = localStorage.getItem("poiVisible");
|
||||||
if (storedPoiVisible !== null) {
|
if (storedPoiVisible !== null) {
|
||||||
setPoiVisible(storedPoiVisible === "true");
|
setPoiVisible(storedPoiVisible === "true");
|
||||||
}
|
}
|
||||||
const storedPolylineVisible = localStorage.getItem("polylineVisible");
|
|
||||||
if (storedPolylineVisible !== null) {
|
|
||||||
dispatch(setPolylineVisible(storedPolylineVisible === "true"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layer-Sichtbarkeiten aus localStorage laden
|
// Kartenspezifischer localStorage-Key verwenden
|
||||||
const storedMapLayersVisibility = localStorage.getItem("mapLayersVisibility");
|
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) {
|
if (storedMapLayersVisibility) {
|
||||||
const parsedVisibility = JSON.parse(storedMapLayersVisibility);
|
const parsedVisibility = JSON.parse(storedMapLayersVisibility);
|
||||||
Object.keys(parsedVisibility).forEach(key => {
|
Object.keys(parsedVisibility).forEach(key => {
|
||||||
dispatch(setLayerVisibility({ layer: key, visibility: parsedVisibility[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");
|
const storedEditMode = localStorage.getItem("editMode");
|
||||||
setEditMode(storedEditMode === "true");
|
setEditMode(storedEditMode === "true");
|
||||||
}, [setPoiVisible, dispatch]); // ✅ `setMapLayersVisibility` entfernt
|
|
||||||
|
setLocalStorageLoaded(true);
|
||||||
|
}, []); // Läuft nur beim Mount
|
||||||
|
|
||||||
const handleAreaChange = event => {
|
const handleAreaChange = event => {
|
||||||
const selectedIndex = event.target.options.selectedIndex;
|
const selectedIndex = event.target.options.selectedIndex;
|
||||||
@@ -72,8 +89,13 @@ function MapLayersControlPanel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Allowed systems jetzt nach Allow && Map filtern
|
||||||
const allowedSystems = Array.isArray(GisSystemStatic)
|
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();
|
: new Set();
|
||||||
|
|
||||||
const seenNames = new Set();
|
const seenNames = new Set();
|
||||||
@@ -97,7 +119,7 @@ function MapLayersControlPanel() {
|
|||||||
const seenSystemNames = new Set();
|
const seenSystemNames = new Set();
|
||||||
const filteredSystems = Array.isArray(GisSystemStatic)
|
const filteredSystems = Array.isArray(GisSystemStatic)
|
||||||
? GisSystemStatic.filter(item => {
|
? 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) {
|
if (isUnique) {
|
||||||
seenSystemNames.add(item.Name);
|
seenSystemNames.add(item.Name);
|
||||||
}
|
}
|
||||||
@@ -108,8 +130,8 @@ function MapLayersControlPanel() {
|
|||||||
setSystemListing(
|
setSystemListing(
|
||||||
filteredSystems.map((system, index) => ({
|
filteredSystems.map((system, index) => ({
|
||||||
id: index + 1,
|
id: index + 1,
|
||||||
name: system.Name, // Verwende den Originalnamen für die Anzeige
|
name: system.Name, // Anzeige
|
||||||
key: `system-${system.IdSystem}`, // Internen Schlüssel für die MapLayersVisibility-Logik
|
key: `system-${system.IdSystem}`, // interner Schlüssel
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}, [GisStationsStaticDistrict, GisSystemStatic]);
|
}, [GisStationsStaticDistrict, GisSystemStatic]);
|
||||||
@@ -117,13 +139,38 @@ function MapLayersControlPanel() {
|
|||||||
const handleCheckboxChange = (key, event) => {
|
const handleCheckboxChange = (key, event) => {
|
||||||
if (editMode) return;
|
if (editMode) return;
|
||||||
const { checked } = event.target;
|
const { checked } = event.target;
|
||||||
|
// Debug-Ausgabe
|
||||||
dispatch(setLayerVisibility({ layer: key, visibility: checked }));
|
const params = new URLSearchParams(window.location.search);
|
||||||
localStorage.setItem(
|
const mapId = params.get("m");
|
||||||
"mapLayersVisibility",
|
const userId = params.get("u");
|
||||||
JSON.stringify({ ...mapLayersVisibility, [key]: checked })
|
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(() => {
|
setTimeout(() => {
|
||||||
const event = new Event("visibilityChanged");
|
const event = new Event("visibilityChanged");
|
||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
@@ -133,7 +180,7 @@ function MapLayersControlPanel() {
|
|||||||
const handlePoiCheckboxChange = event => {
|
const handlePoiCheckboxChange = event => {
|
||||||
const { checked } = event.target;
|
const { checked } = event.target;
|
||||||
setPoiVisible(checked);
|
setPoiVisible(checked);
|
||||||
localStorage.setItem("poiVisible", checked); // Store POI visibility in localStorage
|
localStorage.setItem("poiVisible", checked.toString()); // Store POI visibility in localStorage
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleIconClick = () => {
|
const handleIconClick = () => {
|
||||||
@@ -143,7 +190,7 @@ function MapLayersControlPanel() {
|
|||||||
|
|
||||||
//------------------------------
|
//------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
if (getDebugLog()) {
|
||||||
window.__debug = window.__debug || {};
|
window.__debug = window.__debug || {};
|
||||||
window.__debug.gisStations = GisStationsStaticDistrict;
|
window.__debug.gisStations = GisStationsStaticDistrict;
|
||||||
}
|
}
|
||||||
@@ -175,7 +222,7 @@ function MapLayersControlPanel() {
|
|||||||
}
|
}
|
||||||
return isUnique;
|
return isUnique;
|
||||||
});
|
});
|
||||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
if (getDebugLog()) {
|
||||||
console.log("📌 stationListing aktualisiert:", filteredAreas);
|
console.log("📌 stationListing aktualisiert:", filteredAreas);
|
||||||
}
|
}
|
||||||
}, [GisStationsStaticDistrict, GisSystemStatic]);
|
}, [GisStationsStaticDistrict, GisSystemStatic]);
|
||||||
@@ -194,44 +241,42 @@ function MapLayersControlPanel() {
|
|||||||
}, [GisStationsStaticDistrict]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div className="absolute top-16 right-3 w-72 z-50 bg-white rounded-lg shadow-md">
|
||||||
id="mainDataSheet"
|
<div id="mainDataSheet" className="flex flex-col gap-4 p-4">
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Checkboxen mit Untermenüs */}
|
{/* Checkboxen mit Untermenüs */}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{systemListing.map(system => (
|
{systemListing.map(system => (
|
||||||
@@ -255,9 +300,10 @@ function MapLayersControlPanel() {
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={polylineVisible} // Zustand für Kabelstrecken
|
checked={kabelstreckenVisible}
|
||||||
onChange={handlePolylineCheckboxChange}
|
onChange={e => onPolylineToggle(e.target.checked)}
|
||||||
id="polyline-checkbox"
|
id="polyline-checkbox"
|
||||||
|
disabled={!isTalasAllowed || editMode}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="polyline-checkbox" className="text-sm ml-2">
|
<label htmlFor="polyline-checkbox" className="text-sm ml-2">
|
||||||
Kabelstrecken
|
Kabelstrecken
|
||||||
@@ -279,25 +325,6 @@ function MapLayersControlPanel() {
|
|||||||
POIs
|
POIs
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
// config/paths.js
|
// config/paths.js
|
||||||
const basePathRaw = process.env.NEXT_PUBLIC_BASE_PATH || "";
|
let __configCache;
|
||||||
const BASE_PATH = basePathRaw.replace(/^\/|\/$/g, "");
|
export async function getBaseUrl() {
|
||||||
export const BASE_URL = BASE_PATH ? `/${BASE_PATH}` : "";
|
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;
|
||||||
|
}
|
||||||
|
|||||||
19
convert-md-to-confluence.ps1
Normal 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."
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
const { defineConfig } = require("cypress");
|
|
||||||
|
|
||||||
module.exports = defineConfig({
|
|
||||||
e2e: {
|
|
||||||
setupNodeEvents(on, config) {
|
|
||||||
// Node-Event-Listeners hier konfigurieren
|
|
||||||
},
|
|
||||||
experimentalStudio: true, // Studio aktivieren
|
|
||||||
},
|
|
||||||
|
|
||||||
component: {
|
|
||||||
devServer: {
|
|
||||||
framework: "next",
|
|
||||||
bundler: "webpack",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
describe("contextmenuTest", () => {
|
|
||||||
it("contetmenu Station öffnen (Tab)", () => {
|
|
||||||
cy.log("Test startet jetzt");
|
|
||||||
|
|
||||||
// 1. Viewport einstellen
|
|
||||||
cy.viewport(1920, 1080);
|
|
||||||
cy.log("Viewport eingestellt auf 1920x1080");
|
|
||||||
|
|
||||||
// 2. Seite besuchen
|
|
||||||
cy.visit("http://10.10.0.13:3000/?m=12&u=484");
|
|
||||||
cy.wait(5000); // Wartezeit nach dem Laden
|
|
||||||
cy.log("Seite geöffnet");
|
|
||||||
|
|
||||||
// 3. Sicherstellen, dass die Karte geladen ist
|
|
||||||
cy.get("#map", { timeout: 15000 }).should("be.visible");
|
|
||||||
cy.log("Karte geladen");
|
|
||||||
|
|
||||||
// 4. Wartezeit zum Stabilisieren der Karte
|
|
||||||
cy.wait(2000);
|
|
||||||
|
|
||||||
// 5. Marker suchen und Rechtsklick simulieren
|
|
||||||
cy.get('img[src*="img/icons/marker-icon-20.svg"]') // Marker suchen
|
|
||||||
.filter(":visible") // Nur sichtbare Marker
|
|
||||||
.first() // Ersten Marker auswählen
|
|
||||||
.scrollIntoView() // Marker in den sichtbaren Area scrollen
|
|
||||||
.should("be.visible") // Sicherstellen, dass Marker sichtbar ist
|
|
||||||
.trigger("mouseover") // Mouseover simulieren
|
|
||||||
.wait(500) // Wartezeit nach Mouseover
|
|
||||||
.rightclick({ force: true }); // Rechtsklick auf den Marker
|
|
||||||
cy.log("Rechtsklick auf Marker ausgeführt");
|
|
||||||
|
|
||||||
// Screenshot nach Rechtsklick zum Debugging
|
|
||||||
// cy.screenshot("nach-rechtsklick");
|
|
||||||
|
|
||||||
// 6. Kontextmenü prüfen mit explizitem Selektor
|
|
||||||
cy.get(".leaflet-contextmenu-item") // Suche alle Menüeinträge mit der Klasse
|
|
||||||
.contains("Station öffnen (Tab)", { timeout: 5000 }) // Prüfe Text innerhalb des Eintrags
|
|
||||||
.should("be.visible"); // Sichtbarkeit sicherstellen
|
|
||||||
cy.log("Menüeintrag gefunden");
|
|
||||||
|
|
||||||
// 7. URL abfangen und testen, bevor der Tab geöffnet wird
|
|
||||||
const targetUrl = "http://10.10.0.13/talas5/devices/cpl.aspx?ver=35&kue=24&id=50922";
|
|
||||||
|
|
||||||
// HTTP-Anfrage zur Überprüfung des Status
|
|
||||||
cy.request(targetUrl).then((response) => {
|
|
||||||
expect(response.status).to.eq(200); // Erwartet HTTP 200 OK
|
|
||||||
cy.log("URL ist erreichbar, Status 200");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 8. Menüeintrag auswählen (öffnet den neuen Tab)
|
|
||||||
cy.get(".leaflet-contextmenu-item") // Explizit Menüeintrag mit Klasse auswählen
|
|
||||||
.contains("Station öffnen (Tab)")
|
|
||||||
.click(); // Menüeintrag anklicken
|
|
||||||
cy.log("Menüeintrag ausgewählt");
|
|
||||||
|
|
||||||
// 9. Klick auf die Karte, um Kontextmenü zu schließen
|
|
||||||
cy.get("#map").click(100, 100); // Klick auf eine leere Stelle
|
|
||||||
cy.log("Kontextmenü geschlossen");
|
|
||||||
|
|
||||||
// 10. Optionaler Screenshot nach Abschluss
|
|
||||||
// cy.screenshot("test-abgeschlossen");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
describe("GMA Markers Layer", () => {
|
|
||||||
before(() => {});
|
|
||||||
|
|
||||||
it("Der Test stellt sicher, dass das GMA Tooltip-Element für 'Rastede' angezeigt ist und die erwarteten Werte wie LT:, FBT:, GT: und RLF: enthält.", () => {
|
|
||||||
// Testbeschreibung: Dieser Test überprüft, ob der Tooltip selbst korrekt dargestellt wird und den erwarteten Inhalt anzeigt.
|
|
||||||
|
|
||||||
// Besuche die Map-Seite
|
|
||||||
//cy.visit("http://10.10.0.13:3000/?m=12&u=484"); // Passe die URL an
|
|
||||||
cy.visit("http://127.0.0.1:3000/?m=12&u=484");
|
|
||||||
|
|
||||||
cy.contains(".leaflet-tooltip", "Rastede")
|
|
||||||
// Wählt das Tooltip-Element mit der Klasse `leaflet-tooltip`, das den Text "Rastede" enthält.
|
|
||||||
.first();
|
|
||||||
|
|
||||||
cy.get(".leaflet-tooltip")
|
|
||||||
// Wählt das Tooltip-Element erneut aus, um weitere Überprüfungen durchzuführen.
|
|
||||||
.should("be.visible")
|
|
||||||
// Überprüft, ob das Tooltip sichtbar ist.
|
|
||||||
.and("contain", "LT:")
|
|
||||||
// Stellt sicher, dass der Tooltip den Text "LT :" enthält.
|
|
||||||
.and("contain", "FBT:")
|
|
||||||
// Stellt sicher, dass der Tooltip auch den Text "FBT:" enthält.
|
|
||||||
.and("contain", "GT:")
|
|
||||||
// Stellt sicher, dass der Tooltip auch den Text "GT:" enthält.
|
|
||||||
.and("contain", "RLF:");
|
|
||||||
// Stellt sicher, dass der Tooltip auch den Text "RLF:" enthält.
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should open context menu on right-click on tooltip", () => {
|
|
||||||
// Testbeschreibung: Dieser Test überprüft, ob ein Rechtsklick auf den Tooltip das Kontextmenü öffnet.
|
|
||||||
|
|
||||||
// Besuche die Map-Seite
|
|
||||||
cy.visit("http://127.0.0.1:3000/?m=12&u=484"); // Passe die URL an
|
|
||||||
//warte 2 Sekunden
|
|
||||||
cy.wait(2000);
|
|
||||||
|
|
||||||
cy.contains(".leaflet-tooltip", "Rastede")
|
|
||||||
// Wählt das Tooltip-Element mit der Klasse `leaflet-tooltip`, das den Text "Rastede" enthält.
|
|
||||||
.first()
|
|
||||||
.should("be.visible") // Überprüft, ob das Tooltip sichtbar ist.
|
|
||||||
.trigger("contextmenu"); // Führt einen Rechtsklick (Kontextmenü-Ereignis) auf das Tooltip aus.
|
|
||||||
|
|
||||||
cy.get(".custom-context-menu")
|
|
||||||
// Wählt das Element aus, das das Kontextmenü darstellt.
|
|
||||||
.should("be.visible") // Überprüft, ob das Kontextmenü sichtbar ist.
|
|
||||||
.and("contain", "Station öffnen (Tab)") // Überprüft, ob der Eintrag "Station öffnen (Tab)" vorhanden ist.
|
|
||||||
.and("contain", "Koordinaten anzeigen") // Überprüft, ob der Eintrag "Koordinaten anzeigen" vorhanden ist.
|
|
||||||
.and("contain", "Reinzoomen") // Überprüft, ob der Eintrag "Reinzoomen" vorhanden ist.
|
|
||||||
.and("contain", "Rauszoomen") // Überprüft, ob der Eintrag "Rauszoomen" vorhanden ist.
|
|
||||||
.and("contain", "Hier zentrieren"); // Überprüft, ob der Eintrag "Hier zentrieren" vorhanden ist.
|
|
||||||
});
|
|
||||||
|
|
||||||
//-----------------------------------------------
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
//TDD Test Driven Development
|
|
||||||
// Dieser Test überprüft die Karteninteraktion: Eingabe von Koordinaten und Zentrieren der Karte
|
|
||||||
// Schritte:
|
|
||||||
// 1. Öffnen der Karte auf einer bestimmten Seite.
|
|
||||||
// 2. Eingeben von Koordinaten in ein Eingabefeld.
|
|
||||||
// 3. Klicken auf einen Button, um die Karte zu den Koordinaten zu zoomen.
|
|
||||||
// 4. Überprüfen, ob die Karte korrekt zentriert wurde.
|
|
||||||
//--------------------------------------------------------------Test4 git config --global user.email "ismailali1553@gmail.com" in Terminal eingegeben und
|
|
||||||
// benutzer email adresse eingegeben git config --global user.name "ismailali1553"
|
|
||||||
describe("Karteninteraktion", () => {
|
|
||||||
it("zoomt zu den eingegebenen Koordinaten", () => {
|
|
||||||
// Öffne die Seite mit der Karte
|
|
||||||
cy.visit("http://127.0.0.1:3000/?m=12&u=484"); // Passe den Pfad an deine Karte an
|
|
||||||
|
|
||||||
// Gebe Koordinaten in das Eingabefeld ein
|
|
||||||
cy.get('input[placeholder="Koordinaten eingeben (lat,lng)"]').type("52.52,13.405");
|
|
||||||
|
|
||||||
// Klicke auf den Button "Zu Marker zoomen"
|
|
||||||
cy.get("button").contains("Zu Marker zoomen").click();
|
|
||||||
|
|
||||||
// Überprüfe, ob die Karte die eingegebenen Koordinaten korrekt zentriert hat
|
|
||||||
cy.window().then((win) => {
|
|
||||||
const map = win.map; // Zugriff auf die Leaflet-Instanz
|
|
||||||
const center = map.getCenter(); // Aktuelles Zentrum der Karte abrufen
|
|
||||||
expect(center.lat).to.be.closeTo(52.52, 0.01); // Latitude überprüfen
|
|
||||||
expect(center.lng).to.be.closeTo(13.405, 0.01); // Longitude überprüfen
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
//cypress/e2e/poiUpdateModal.cy.js
|
|
||||||
describe("POI bearbeiten – Typ-Auswahl prüfen", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("http://localhost:3000/?m=12&u=484");
|
|
||||||
cy.get(".leaflet-container", { timeout: 10000 }).should("be.visible");
|
|
||||||
cy.get(".leaflet-marker-icon", { timeout: 10000 }).should("have.length.greaterThan", 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sollte beim Öffnen des Modals den richtigen POI-Typ anzeigen", () => {
|
|
||||||
cy.get('svg[aria-label="Bearbeitungsmodus aktivieren"]').click({ force: true });
|
|
||||||
cy.wait(5000);
|
|
||||||
|
|
||||||
cy.get('img[src="/img/icons/pois/poi-marker-icon-4.png"]', { timeout: 10000 }).should("be.visible");
|
|
||||||
cy.get('img[src="/img/icons/pois/poi-marker-icon-4.png"]').first().rightclick({ force: true });
|
|
||||||
|
|
||||||
cy.contains("POI Bearbeiten").click({ force: true });
|
|
||||||
|
|
||||||
cy.get("#idPoiTyp", { timeout: 10000 }).should("exist").find("[class*='singleValue']").should("not.contain.text", "Typ auswählen");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
describe("TK-Komponenten", () => {
|
|
||||||
before(() => {
|
|
||||||
// Lade die Seite nur einmal vor allen Tests
|
|
||||||
cy.visit("http://10.10.0.13:3000/?m=12&u=484");
|
|
||||||
//cy.wait(5000); // Wartezeit, bis die Seite vollständig geladen ist, cypress macht automatisch , alsobrauchen wir im moment kein wait() wenn cy. schafft
|
|
||||||
});
|
|
||||||
|
|
||||||
it("soll alle Tests in Reihenfolge ausführen", () => {
|
|
||||||
// Test 1: Sicherstellen, dass die Checkbox vorhanden und sichtbar ist
|
|
||||||
cy.get("input[type='checkbox'][id='system-10']")
|
|
||||||
.should("exist")
|
|
||||||
.and("be.visible")
|
|
||||||
.then(() => {
|
|
||||||
cy.log("Die Checkbox mit ID 'system-10' ist vorhanden und sichtbar.");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 2: Sicherstellen, dass die Checkbox aktiviert ist
|
|
||||||
cy.get("input[type='checkbox'][id='system-10']").then(($checkbox) => {
|
|
||||||
if (!$checkbox.prop("checked")) {
|
|
||||||
// Falls die Checkbox nicht aktiviert ist, aktiviere sie
|
|
||||||
cy.wrap($checkbox).check({ force: true });
|
|
||||||
cy.log("Die Checkbox war deaktiviert und wurde jetzt aktiviert.");
|
|
||||||
} else {
|
|
||||||
cy.log("Die Checkbox ist bereits aktiviert.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test 3: Checkbox deaktivieren und Marker verschwinden lassen
|
|
||||||
cy.get("input[type='checkbox'][id='system-10']")
|
|
||||||
.uncheck({ force: true })
|
|
||||||
.then(() => {
|
|
||||||
cy.log("Die Checkbox wurde deaktiviert.");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Using fixtures to represent data",
|
|
||||||
"email": "hello@cypress.io",
|
|
||||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
describe("Map Initial Load Test", () => {
|
|
||||||
it("should load the map with the correct center and zoom", () => {
|
|
||||||
// Besuche die Seite, auf der die Karte angezeigt wird
|
|
||||||
cy.visit("http://192.168.10.167:3000/?m=12&u=485");
|
|
||||||
|
|
||||||
// Überprüfe, ob das Kartenelement existiert
|
|
||||||
cy.get("#map").should("be.visible");
|
|
||||||
|
|
||||||
// Überprüfe, ob die Karte das korrekte Zentrum und den korrekten Zoom hat
|
|
||||||
cy.window().then((win) => {
|
|
||||||
const map = win.L.map;
|
|
||||||
const center = map.getCenter();
|
|
||||||
const zoom = map.getZoom();
|
|
||||||
|
|
||||||
expect(center.lat).to.be.closeTo(53.111111, 0.0001);
|
|
||||||
expect(center.lng).to.be.closeTo(8.4625, 0.0001);
|
|
||||||
expect(zoom).to.eq(12);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 618 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 587 KiB |
@@ -1,25 +0,0 @@
|
|||||||
// ***********************************************
|
|
||||||
// This example commands.js shows you how to
|
|
||||||
// create various custom commands and overwrite
|
|
||||||
// existing commands.
|
|
||||||
//
|
|
||||||
// For more comprehensive examples of custom
|
|
||||||
// commands please read more here:
|
|
||||||
// https://on.cypress.io/custom-commands
|
|
||||||
// ***********************************************
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a parent command --
|
|
||||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a child command --
|
|
||||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a dual command --
|
|
||||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This will overwrite an existing command --
|
|
||||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<title>Components App</title>
|
|
||||||
<!-- Used by Next.js to inject CSS. -->
|
|
||||||
<div id="__next_css__DO_NOT_USE__"></div>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div data-cy-root></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// ***********************************************************
|
|
||||||
// This example support/component.js is processed and
|
|
||||||
// loaded automatically before your test files.
|
|
||||||
//
|
|
||||||
// This is a great place to put global configuration and
|
|
||||||
// behavior that modifies Cypress.
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off
|
|
||||||
// automatically serving support files with the
|
|
||||||
// 'supportFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/configuration
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
|
||||||
import './commands'
|
|
||||||
|
|
||||||
// Alternatively you can use CommonJS syntax:
|
|
||||||
// require('./commands')
|
|
||||||
|
|
||||||
import { mount } from 'cypress/react18'
|
|
||||||
|
|
||||||
Cypress.Commands.add('mount', mount)
|
|
||||||
|
|
||||||
// Example use:
|
|
||||||
// cy.mount(<MyComponent />)
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
// ***********************************************************
|
|
||||||
// Diese Datei wird automatisch geladen, bevor Tests ausgeführt werden.
|
|
||||||
//
|
|
||||||
// Dies ist ein guter Platz für globale Konfigurationen
|
|
||||||
// und Änderungen, die Cypress beeinflussen.
|
|
||||||
//
|
|
||||||
// Weitere Infos: https://on.cypress.io/configuration
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// Importiere zusätzliche Befehle
|
|
||||||
import "./commands";
|
|
||||||
|
|
||||||
// Alternativ: CommonJS-Syntax verwenden
|
|
||||||
// require('./commands')
|
|
||||||
|
|
||||||
// Verstecke unnötige Logs für XHR- und Fetch-Anfragen
|
|
||||||
const app = window.top;
|
|
||||||
if (!app.document.head.querySelector("[data-hide-command-log-request]")) {
|
|
||||||
const style = app.document.createElement("style");
|
|
||||||
style.innerHTML = `
|
|
||||||
.command-name-request, .command-name-xhr {
|
|
||||||
display: none; /* Verstecke Fetch- und XHR-Logs */
|
|
||||||
}
|
|
||||||
.runnable-pass .collapsible-header {
|
|
||||||
display: none; /* Verstecke den Header erfolgreicher Tests */
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
style.setAttribute("data-hide-command-log-request", "");
|
|
||||||
app.document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Globale Ereignisse für Cypress konfigurieren
|
|
||||||
Cypress.on("test:after:run", (test, runnable) => {
|
|
||||||
// Minimiert die Logs für erfolgreiche Tests
|
|
||||||
if (test.state === "passed") {
|
|
||||||
runnable._testConfigBody = null; // Löscht den Test Body
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Schließe automatisch erfolgreiche Tests in der GUI
|
|
||||||
Cypress.on("log:added", (log) => {
|
|
||||||
if (log.state === "passed") {
|
|
||||||
log.displayName = ""; // Versteckt Log-Details für erfolgreiche Schritte
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Screenshot nach jedem fehlgeschlagenen Test
|
|
||||||
Cypress.on("fail", (error, runnable) => {
|
|
||||||
cy.screenshot(`error-${runnable.title}`); // Screenshot für Fehler erstellen
|
|
||||||
throw error; // Fehler weitergeben
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logge die Dauer jedes Tests
|
|
||||||
Cypress.on("test:after:run", (test) => {
|
|
||||||
cy.log(`Test "${test.title}" abgeschlossen in ${test.duration} ms`);
|
|
||||||
});
|
|
||||||
@@ -6,10 +6,6 @@ DB_PASSWORD="root#$"
|
|||||||
DB_NAME=talas_v5
|
DB_NAME=talas_v5
|
||||||
DB_PORT=3306
|
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
|
#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
|
NEXT_PUBLIC_API_PORT_MODE=prod
|
||||||
|
|||||||
BIN
docs/NodeMap.pdf
@@ -25,6 +25,31 @@ Entwicklung, Architekturverständnis und Erweiterung.
|
|||||||
### ⚙️ Konfiguration
|
### ⚙️ Konfiguration
|
||||||
|
|
||||||
- [Allgemeine Übersicht](config/README.md)
|
- [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
|
### 🧩 Hauptkomponenten
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ sequenceDiagram
|
|||||||
|
|
||||||
- **Konfigurierbarer basePath:**
|
- **Konfigurierbarer basePath:**
|
||||||
- **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.
|
werden.
|
||||||
Die Konfiguration erfolgt je nach Umgebung über:
|
Die Konfiguration erfolgt je nach Umgebung über:
|
||||||
|
|
||||||
@@ -22,4 +22,73 @@ Verzeichnisstruktur funktioniert.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## OSM‑basierte, „open“ Quellen
|
||||||
|
|
||||||
|
Diese sind datenrechtlich offen (ODbL bzw. Community-Lizenzen), aber das „kostenlos“ gilt nicht im
|
||||||
|
Sinne unbegrenzter Tile‑Nutzung. Die Tile‑Server werden als Community‑Ressource bereitgestellt –
|
||||||
|
bitte Policies respektieren.
|
||||||
|
|
||||||
|
- osm-standard (OpenStreetMap)
|
||||||
|
- - Key: Nein
|
||||||
|
- - Nutzung: Fair‑Use; für produktive/hohe Last eigenen Tile‑Server/Provider verwenden.
|
||||||
|
- - Attribution: „© OpenStreetMap contributors“
|
||||||
|
|
||||||
|
- osm-humanitarian (HOT)
|
||||||
|
- - Key: Nein
|
||||||
|
- - Nutzung: Fair‑Use; für größere Last die Betreiber kontaktieren bzw. andere Infrastruktur nutzen.
|
||||||
|
- - Attribution: „© OpenStreetMap contributors <br>
|
||||||
|
-
|
||||||
|
- Humanitarian OpenStreetMap Team“ cyclosm
|
||||||
|
- - Key: Nein
|
||||||
|
- - Nutzung: Fair‑Use (bereitgestellt u. a. über OSM France). Für höhere Last
|
||||||
|
Unterstützung/Hostingoptionen prüfen.
|
||||||
|
- - Attribution: „CyclOSM“ + „OpenStreetMap contributors“
|
||||||
|
- Praxis‑Tipps Kleine bis mittlere Nutzung: Die oben genannten „keyless“ Quellen sind oft
|
||||||
|
ausreichend, solange du Attribution setzt und Limits respektierst. Produktion/hohe Last: Nutze
|
||||||
|
einen Anbieter mit Vertrag/Key (z. B. Thunderforest, Tracestrack, MapTiler, Mapbox) oder hoste
|
||||||
|
Tiles selbst. Schlüssel im Client: Für Thunderforest/Tracestrack stehen die Keys im Frontend. Das
|
||||||
|
ist üblich, aber der Key ist sichtbar. Wenn du ihn verbergen willst, richte einen kleinen
|
||||||
|
Tile‑Proxy auf deinem Server ein, der den Key serverseitig anhängt und optional cached.
|
||||||
|
Attribution: Dein BaseMapPanel setzt bereits Attributionsstrings aus config.json. Achte darauf,
|
||||||
|
dass sie je Quelle korrekt sind.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Kurzantwort: Für kommerzielle Nutzung sind OSM‑Community‑Tile‑Server nicht geeignet. Nutze einen
|
||||||
|
bezahlten Anbieter (z. B. Thunderforest, Tracestrack) oder hoste selbst. Attribution ist immer
|
||||||
|
Pflicht.
|
||||||
|
|
||||||
|
Links und Hinweise je Layer/Provider:
|
||||||
|
|
||||||
|
OpenStreetMap Standard (osm-standard)
|
||||||
|
|
||||||
|
Lizenz/Daten: ODbL, Attribution Pflicht Tile-Server-Policy (keine Produktion/hohe Last):
|
||||||
|
https://operations.osmfoundation.org/policies/tiles/ Urheberrecht/Attribution:
|
||||||
|
https://www.openstreetmap.org/copyright HOT Humanitarian (osm-humanitarian)
|
||||||
|
|
||||||
|
Community-Server (OSM France); keine Produktion/hohe Last Info/Policy OSM France Tiles:
|
||||||
|
https://tile.openstreetmap.fr/ HOT: https://www.hotosm.org/ CyclOSM (cyclosm)
|
||||||
|
|
||||||
|
Community-Server (OSM France); keine Produktion/hohe Last Projektseite: https://www.cyclosm.org/
|
||||||
|
Hinweise/Policy (OSM France): https://tile.openstreetmap.fr/ Wiki:
|
||||||
|
https://wiki.openstreetmap.org/wiki/CyclOSM Carto Light (carto-light / Positron)
|
||||||
|
|
||||||
|
Keylos nutzbar mit Attribution; Fair‑Use, für hohe Last über CARTO‑Pläne Basemaps:
|
||||||
|
https://carto.com/basemaps/ Attribution: https://carto.com/attributions Pricing (Plattform):
|
||||||
|
https://carto.com/pricing/ (bei großem Volumen Sales kontaktieren) Thunderforest (Cycle/Transport u.
|
||||||
|
a.)
|
||||||
|
|
||||||
|
Kommerziell mit API‑Key; Pläne von Free bis Pro Pricing: https://www.thunderforest.com/pricing/
|
||||||
|
Terms/Attribution: https://www.thunderforest.com/terms/ Tracestrack Topo
|
||||||
|
|
||||||
|
API‑Key erforderlich; kostenlose und bezahlte Pläne Übersicht/Pricing:
|
||||||
|
https://www.tracestrack.com/en/maps/ Nutzungsbedingungen: https://www.tracestrack.com/en/terms/
|
||||||
|
Empfehlung:
|
||||||
|
|
||||||
|
Produktion: Nimm Thunderforest oder Tracestrack (oder MapTiler: https://www.maptiler.com/pricing/)
|
||||||
|
oder hoste Tiles selbst. Attribution in der Karte anzeigen (OSM + jeweiliger Anbieter). Soll ich
|
||||||
|
diese Links samt klarer Hinweise kompakt in eure README.md einpflegen?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
[Zurück zur Übersicht](../README.md)
|
[Zurück zur Übersicht](../README.md)
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
# 📁 paths.js
|
# 📁 paths.js
|
||||||
|
|
||||||
Berechnet den sauberen `BASE_URL`-Pfad basierend auf `.env.production` oder
|
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.
|
Entfernt führende und abschließende Slashes.
|
||||||
|
|
||||||
## Beispiel
|
## 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
|
```js
|
||||||
const BASE_PATH = basePathRaw.replace(/^\/|\/$/g, "");
|
const BASE_PATH = basePathRaw.replace(/^\/|\/$/g, "");
|
||||||
|
|||||||
@@ -12,17 +12,17 @@ NodeMap verwendet Umgebungsvariablen zur Steuerung von API-Verhalten, Serverpfad
|
|||||||
|
|
||||||
## 🔧 Wichtige Variablen
|
## 🔧 Wichtige Variablen
|
||||||
|
|
||||||
| Variable | Beispielwert | Beschreibung |
|
| Variable | Beispielwert | Beschreibung |
|
||||||
| --------------------------- | ------------------- | --------------------------------------------------------------------- |
|
| --------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||||
| `DB_HOST` | `localhost` | Adresse des Datenbankservers (MySQL) |
|
| `DB_HOST` | `localhost` | Adresse des Datenbankservers (MySQL) |
|
||||||
| `DB_PORT` | `3306` | Port für die Datenbankverbindung |
|
| `DB_PORT` | `3306` | Port für die Datenbankverbindung |
|
||||||
| `DB_NAME` | `talas` | Datenbankname |
|
| `DB_NAME` | `talas` | Datenbankname |
|
||||||
| `DB_USER` | `root` | Benutzername für MySQL |
|
| `DB_USER` | `root` | Benutzername für MySQL |
|
||||||
| `DB_PASSWORD` | `geheim` | Passwort 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_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_USE_MOCKS` | `true` oder `false` | Aktiviert den Mockdaten-Modus über `/api/mocks/...` |
|
||||||
| `NEXT_PUBLIC_BASE_PATH` | `/talas5` oder leer | Optionaler Pfad, falls App unter Subpfad läuft (z. B. IIS) |
|
| `basePath` (in config.json) | `/talas5` oder leer | Optionaler Pfad, falls App unter Subpfad läuft (z. B. IIS). Wird jetzt in `public/config.json` gepflegt. |
|
||||||
| `NEXT_PUBLIC_DEBUG` | `true` oder `false` | Aktiviert zusätzliche `console.log` Ausgaben für Debugging im Browser |
|
| `NEXT_PUBLIC_DEBUG` | `true` oder `false` | Aktiviert zusätzliche `console.log` Ausgaben für Debugging im Browser |
|
||||||
|
|
||||||
## 📦 Beispiel `.env.production`
|
## 📦 Beispiel `.env.production`
|
||||||
|
|
||||||
@@ -34,7 +34,11 @@ DB_USER=root
|
|||||||
DB_PASSWORD=geheim
|
DB_PASSWORD=geheim
|
||||||
NEXT_PUBLIC_API_PORT_MODE=prod
|
NEXT_PUBLIC_API_PORT_MODE=prod
|
||||||
NEXT_PUBLIC_USE_MOCKS=false
|
NEXT_PUBLIC_USE_MOCKS=false
|
||||||
NEXT_PUBLIC_BASE_PATH=/talas5
|
// public/config.json
|
||||||
|
{
|
||||||
|
...
|
||||||
|
"basePath": "/talas5"
|
||||||
|
}
|
||||||
NEXT_PUBLIC_DEBUG=false
|
NEXT_PUBLIC_DEBUG=false
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -48,7 +52,11 @@ DB_USER=root
|
|||||||
DB_PASSWORD=geheim
|
DB_PASSWORD=geheim
|
||||||
NEXT_PUBLIC_API_PORT_MODE=dev
|
NEXT_PUBLIC_API_PORT_MODE=dev
|
||||||
NEXT_PUBLIC_USE_MOCKS=true
|
NEXT_PUBLIC_USE_MOCKS=true
|
||||||
NEXT_PUBLIC_BASE_PATH=/talas5
|
// public/config.json
|
||||||
|
{
|
||||||
|
...
|
||||||
|
"basePath": "/talas5"
|
||||||
|
}
|
||||||
NEXT_PUBLIC_DEBUG=true
|
NEXT_PUBLIC_DEBUG=true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
30
fix-confluence-headings.ps1
Normal 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."
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import L from "leaflet";
|
import L from "leaflet";
|
||||||
|
import { getDebugLog } from "../utils/configUtils";
|
||||||
|
|
||||||
export class OverlappingMarkerSpiderfier {
|
export class OverlappingMarkerSpiderfier {
|
||||||
constructor(map, options = {}) {
|
constructor(map, options = {}) {
|
||||||
@@ -118,7 +119,7 @@ export class OverlappingMarkerSpiderfier {
|
|||||||
// 🔥 Künstliches Click-Event auslösen, um die UI zu aktualisieren
|
// 🔥 Künstliches Click-Event auslösen, um die UI zu aktualisieren
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.map.fire("click");
|
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 ");
|
console.log("Click-Event ausgelöst in OverlappingMarkerspiderfier.js in unspiderfy ");
|
||||||
}
|
}
|
||||||
}, 10); // Kurze Verzögerung, um sicherzustellen, dass die UI neu gerendert wird
|
}, 10); // Kurze Verzögerung, um sicherzustellen, dass die UI neu gerendert wird
|
||||||
|
|||||||
48
nssm.exe Installation.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
```markdown
|
||||||
|
- Als Administrator Eingabeaufforderung oder PowerShell öffnen
|
||||||
|
|
||||||
|
- Navigiere zu dem NodeMap Projekt Verzeichnis:
|
||||||
|
```shell
|
||||||
|
C:\Users\Administrator>cd C:\inetpub\wwwroot\talas5\nodeMap
|
||||||
|
```
|
||||||
|
|
||||||
|
- Befehl zum Erstellen eines Dienstes:
|
||||||
|
Führen Sie den folgenden Befehl aus, um einen neuen Dienst zu erstellen:
|
||||||
|
```shell
|
||||||
|
nssm.exe install NodeMapService
|
||||||
|
```
|
||||||
|
Nachdem Sie diesen Befehl ausgeführt haben, öffnet sich ein NSSM-Dialogfenster.
|
||||||
|
|
||||||
|
**Dienstkonfiguration:**
|
||||||
|
In dem geöffneten NSSM-Dialogfenster müssen Sie einige Parameter angeben:
|
||||||
|
|
||||||
|
- **Path:** Der Pfad zur ausführbaren Datei, die der Dienst ausführen soll.
|
||||||
|
```shell
|
||||||
|
C:\inetpub\wwwroot\talas5\nodeMap\StartNodeApp.bat
|
||||||
|
```
|
||||||
|
- **Startup directory:** Das Verzeichnis, in dem die Anwendung gestartet werden soll.
|
||||||
|
```shell
|
||||||
|
C:\inetpub\wwwroot\talas5\nodeMap
|
||||||
|
```
|
||||||
|
- **Arguments:** kann leer gelassen werden.
|
||||||
|
|
||||||
|
- Dienst starten:
|
||||||
|
Sobald der Dienst erstellt wurde, können Sie ihn starten.
|
||||||
|
Das können Sie entweder über die Eingabeaufforderung oder über die Diensteverwaltung von Windows tun.
|
||||||
|
Um den Dienst über die Eingabeaufforderung zu starten, verwenden Sie den folgenden Befehl:
|
||||||
|
```shell
|
||||||
|
nssm.exe start DienstName
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- **Dienst bearbeiten:**
|
||||||
|
```shell
|
||||||
|
nssm.exe edit NodeMapService
|
||||||
|
```
|
||||||
|
- **Dienst entfernen:**
|
||||||
|
```shell
|
||||||
|
nssm.exe remove NodeMapService confirm
|
||||||
|
```
|
||||||
|
dauert bis 1 Minute
|
||||||
|
```
|
||||||
2164
package-lock.json
generated
14
package.json
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "nodemap",
|
"name": "nodemap",
|
||||||
"version": "1.1.300",
|
"version": "1.1.396",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.13.3",
|
||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
"@heroicons/react": "^2.1.5",
|
"@heroicons/react": "^2.1.5",
|
||||||
|
"@iconify/react": "^6.0.1",
|
||||||
"@mui/icons-material": "^6.0.2",
|
"@mui/icons-material": "^6.0.2",
|
||||||
"@reduxjs/toolkit": "^2.5.1",
|
"@reduxjs/toolkit": "^2.5.1",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
@@ -39,17 +40,22 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node server.js",
|
"dev": "node server.js",
|
||||||
"build": "next build",
|
"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",
|
"start": "cross-env NODE_ENV=production node server.js",
|
||||||
"export": "next export",
|
"export": "next export",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"cypress": "cypress open",
|
"test:e2e": "playwright test",
|
||||||
"cypress:run": "cypress run",
|
"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",
|
"prepare": "husky",
|
||||||
"bump-version": "node ./scripts/bumpVersion.js"
|
"bump-version": "node ./scripts/bumpVersion.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.54.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress": "^13.17.0",
|
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
// /pages/api/health.js
|
|
||||||
export default async function handler(req, res) {
|
|
||||||
const basePath = "talas5";
|
|
||||||
const protocol = "http";
|
|
||||||
const hostname = "10.10.0.70";
|
|
||||||
const port = "80";
|
|
||||||
const idMap = "12";
|
|
||||||
const idUser = "484";
|
|
||||||
const idLD = "50922";
|
|
||||||
|
|
||||||
const buildUrl = method =>
|
|
||||||
`${protocol}://${hostname}:${port}/${basePath}/ClientData/WebServiceMap.asmx/${method}?idMap=${idMap}&idUser=${idUser}`;
|
|
||||||
|
|
||||||
const externalUrls = {
|
|
||||||
GisStationsStaticDistrict: buildUrl("GisStationsStaticDistrict"),
|
|
||||||
GisLinesStatus: `${protocol}://${hostname}:${port}/${basePath}/ClientData/WebServiceMap.asmx/GisLinesStatus?idMap=${idMap}`,
|
|
||||||
GisStationsMeasurements: buildUrl("GisStationsMeasurements"),
|
|
||||||
GisStationsStatusDistrict: buildUrl("GisStationsStatusDistrict"),
|
|
||||||
GisSystemStatic: buildUrl("GisSystemStatic"),
|
|
||||||
};
|
|
||||||
|
|
||||||
const internalApiBase = "http://localhost:3000";
|
|
||||||
|
|
||||||
const internalApis = {
|
|
||||||
//area
|
|
||||||
readArea: `${internalApiBase}/api/talas_v5_DB/area/readArea?m=${idMap}`,
|
|
||||||
//device
|
|
||||||
getAllStationsNames: `${internalApiBase}/api/talas_v5_DB/device/getAllStationsNames`,
|
|
||||||
getDevices: `${internalApiBase}/api/talas_v5_DB/device/getDevices`,
|
|
||||||
//gisLines
|
|
||||||
readGisLines: `${internalApiBase}/api/talas_v5_DB/gisLines/readGisLines`,
|
|
||||||
//locationDevice
|
|
||||||
getDeviceId: `${internalApiBase}/api/talas_v5_DB/locationDevice/getDeviceId`,
|
|
||||||
locationDeviceNameById: `${internalApiBase}/api/talas_v5_DB/locationDevice/locationDeviceNameById?idLD=${idLD}`,
|
|
||||||
locationDevices: `${internalApiBase}/api/talas_v5_DB/locationDevice/locationDevices`,
|
|
||||||
//pois
|
|
||||||
addPoi: `${internalApiBase}/api/talas_v5_DB/pois/addPoi`,
|
|
||||||
deletePoi: `${internalApiBase}/api/talas_v5_DB/pois/deletePoi`,
|
|
||||||
getPoiById: `${internalApiBase}/api/talas_v5_DB/pois/getPoiById`,
|
|
||||||
poiIcons: `${internalApiBase}/api/talas_v5_DB/pois/poi-icons`,
|
|
||||||
readAllPOIs: `${internalApiBase}/api/talas_v5_DB/pois/readAllPOIs`,
|
|
||||||
updateLocation: `${internalApiBase}/api/talas_v5_DB/pois/updateLocation`,
|
|
||||||
updatePoi: `${internalApiBase}/api/talas_v5_DB/pois/updatePoi`,
|
|
||||||
//poiTyp
|
|
||||||
readPoiTyp: `${internalApiBase}/api/talas_v5_DB/poiTyp/readPoiTyp`,
|
|
||||||
//station
|
|
||||||
getAllStationsNames: `${internalApiBase}/api/talas_v5_DB/station/getAllStationsNames`,
|
|
||||||
getDevices: `${internalApiBase}/api/talas_v5_DB/station/getDevices`,
|
|
||||||
//
|
|
||||||
priorityConfig: `${internalApiBase}/api/talas_v5_DB/priorityConfig`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const results = {};
|
|
||||||
|
|
||||||
// Prüfe externe Webservices
|
|
||||||
await Promise.all(
|
|
||||||
Object.entries(externalUrls).map(async ([name, url]) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
results[name] = {
|
|
||||||
ok: response.ok,
|
|
||||||
status: response.status,
|
|
||||||
url,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (name === "GisSystemStatic" && response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
results["UserRights"] = {
|
|
||||||
ok: Array.isArray(data.Rights),
|
|
||||||
length: data.Rights?.length || 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
results[name] = {
|
|
||||||
ok: false,
|
|
||||||
error: error.message,
|
|
||||||
url,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Prüfe interne API-Routen
|
|
||||||
await Promise.all(
|
|
||||||
Object.entries(internalApis).map(async ([name, url]) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
results[`API_${name}`] = {
|
|
||||||
ok: response.ok,
|
|
||||||
status: response.status,
|
|
||||||
url,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
results[`API_${name}`] = {
|
|
||||||
ok: false,
|
|
||||||
error: error.message,
|
|
||||||
url,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Prüfe Mock-Status
|
|
||||||
const useMocksEnv = process.env.NEXT_PUBLIC_USE_MOCKS;
|
|
||||||
results["MockMode"] = {
|
|
||||||
expected: "false",
|
|
||||||
actual: useMocksEnv,
|
|
||||||
ok: useMocksEnv === "false",
|
|
||||||
info:
|
|
||||||
useMocksEnv === "false"
|
|
||||||
? "✅ Mockdaten deaktiviert – Live-Daten aktiv."
|
|
||||||
: "⚠️ Mockdaten aktiv – nicht für Produktion geeignet!",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Prüfe Konfiguration der .env.production
|
|
||||||
results["envConfig"] = {
|
|
||||||
NODE_ENV: process.env.NODE_ENV || "undefined",
|
|
||||||
DB_HOST: process.env.DB_HOST || "❌ fehlt",
|
|
||||||
NEXT_PUBLIC_USE_MOCKS: useMocksEnv || "❌ fehlt",
|
|
||||||
status:
|
|
||||||
process.env.NODE_ENV === "production" && useMocksEnv === "false" && process.env.DB_HOST
|
|
||||||
? "✅ .env.production scheint korrekt."
|
|
||||||
: "⚠️ Bitte .env.production prüfen – möglicherweise fehlt etwas.",
|
|
||||||
};
|
|
||||||
|
|
||||||
const allOk = Object.values(results).every(r => r.ok);
|
|
||||||
|
|
||||||
res.status(allOk ? 200 : 207).json({
|
|
||||||
status: allOk ? "ok" : "partial",
|
|
||||||
version: "1.0.0",
|
|
||||||
services: results,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
// /pages/api/talas_v5_DB/area/updateArea.js
|
// /pages/api/talas_v5_DB/area/updateArea.js
|
||||||
import getPool from "../../../../utils/mysqlPool";
|
import getPool from "../../../../utils/mysqlPool";
|
||||||
|
import { getDebugLog } from "../../../../utils/configUtils";
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
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
|
console.log("Request erhalten:", req.method, req.body); // Debugging
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// /pages/api/talas_v5_DB/gisLines/updateLineCoordinates.js
|
// /pages/api/talas_v5_DB/gisLines/updateLineCoordinates.js
|
||||||
import getPool from "../../../../utils/mysqlPool"; // Singleton-Pool importieren
|
import getPool from "../../../../utils/mysqlPool"; // Singleton-Pool importieren
|
||||||
|
import { getDebugLog } from "../../../../utils/configUtils";
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const pool = getPool(); // Singleton-Pool verwenden
|
const pool = getPool(); // Singleton-Pool verwenden
|
||||||
@@ -34,7 +35,7 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
// Commit der Transaktion
|
// Commit der Transaktion
|
||||||
await connection.commit();
|
await connection.commit();
|
||||||
if (process.env.NEXT_PUBLIC_DEBUG_LOG === "true") {
|
if (getDebugLog()) {
|
||||||
console.log("Transaction Complete.");
|
console.log("Transaction Complete.");
|
||||||
}
|
}
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
// pages/api/talas_v5_DB/pois/addPoi.js
|
// pages/api/talas_v5_DB/pois/addPoi.js
|
||||||
import getPool from "../../../../utils/mysqlPool"; // Singleton-Pool importieren
|
import getPool from "../../../../utils/mysqlPool"; // Singleton-Pool importieren
|
||||||
|
import { getDebugLog } from "../../../../utils/configUtils";
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const pool = getPool(); // Singleton-Pool verwenden
|
const pool = getPool(); // Singleton-Pool verwenden
|
||||||
|
|
||||||
if (req.method === "POST") {
|
if (req.method === "POST") {
|
||||||
const { name, poiTypeId, latitude, longitude, idLD } = req.body;
|
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
|
console.log("Received data:", req.body); // Überprüfen der empfangenen Daten
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
pages/api/testDbConnection.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// pages/api/testDbConnection.js
|
||||||
|
import getPool from "../../utils/mysqlPool";
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
const pool = getPool();
|
||||||
|
let connection;
|
||||||
|
try {
|
||||||
|
connection = await pool.getConnection();
|
||||||
|
const [rows] = await connection.query("SELECT 1 AS test");
|
||||||
|
console.log("DB-Verbindung erfolgreich! Ergebnis:", rows);
|
||||||
|
res.status(200).json({ success: true, result: rows });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("DB-Verbindungsfehler:", error);
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
} finally {
|
||||||
|
if (connection) connection.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
35
playwright.config.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Playwright test configuration for the NodeMap project
|
||||||
|
// Starts the local Next.js custom server (server.js) and runs tests against http://localhost:3000
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const { defineConfig, devices } = require("@playwright/test");
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
testDir: "./playwright/tests",
|
||||||
|
timeout: 60_000,
|
||||||
|
expect: { timeout: 10_000 },
|
||||||
|
fullyParallel: true,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
// Reporters: keep console-friendly list and generate an HTML report under playwright/reports
|
||||||
|
reporter: [["list"], ["html", { outputFolder: "playwright/reports", open: "never" }]],
|
||||||
|
// Store any runner outputs (attachments, logs) under playwright/test-results
|
||||||
|
outputDir: "playwright/test-results",
|
||||||
|
use: {
|
||||||
|
baseURL: "http://localhost:3000",
|
||||||
|
// Disable artifact generation locally to avoid creating files
|
||||||
|
trace: "off",
|
||||||
|
video: "off",
|
||||||
|
screenshot: "off",
|
||||||
|
headless: process.env.PW_HEADED ? false : true,
|
||||||
|
// Apply slow motion to all actions when PW_SLOWMO is set
|
||||||
|
launchOptions: {
|
||||||
|
slowMo: process.env.PW_SLOWMO ? Number(process.env.PW_SLOWMO) : 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
244
playwright/tests/mapcomponent.spec.js
Normal 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
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"_comment": [
|
||||||
|
"tileSources: 'local' für offline, 'osm' für online",
|
||||||
|
"center: Startmittelpunkt der Karte (lat, lng)",
|
||||||
|
"zoomOutCenter: Zielkoordinaten für Herauszoomen (lat, lng)",
|
||||||
|
"minZoom/maxZoom: erlaubte Zoomstufen pro Quelle"
|
||||||
|
],
|
||||||
|
|
||||||
|
"center": [53.111111, 8.4625],
|
||||||
|
"_comment_center": "Startmittelpunkt der Karte (lat, lng)",
|
||||||
|
|
||||||
|
"zoomOutCenter": [51.41321407879154, 7.739617925303934],
|
||||||
|
"_comment_zoomOutCenter": "Zielkoordinaten für Herauszoomen (lat, lng)",
|
||||||
|
|
||||||
|
"basePath": "/talas5",
|
||||||
|
"_comment_basePath": "Basis-URL für API und Routing",
|
||||||
|
"minZoom": 5,
|
||||||
|
"maxZoom": 20,
|
||||||
|
"_comment_zoom": "Globale Zoom-Grenzen (min/max). Kann durch tileSources überschrieben werden.",
|
||||||
|
"debugLog": false,
|
||||||
|
"_comment_debugLog": "Debug-Logging für Client "
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 349 B |
BIN
public/img/icons/critical-marker-icon-17.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/img/icons/critical-marker-icon-18.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/img/icons/major-marker-icon-25.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/img/icons/major-marker-icon-26.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/img/icons/major-marker-icon-4.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/img/icons/major-marker-icon-5.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/img/icons/marker-icon-storage-upright.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/img/icons/marker-icon-thermo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
3
public/img/icons/material-symbols/alarm.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 396 B |
3
public/img/icons/material-symbols/edit-off.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 21l-3-3m-12.728-.364A9 9 0 015.636 5.636m0 0L3 3l3 3m9.364 9.364L18 21M5.636 5.636L3 3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 351 B |
3
public/img/icons/material-symbols/edit.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 432 B |
3
public/img/icons/material-symbols/info.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 333 B |
4
public/img/icons/material-symbols/map-marker.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="rgb(0, 174, 239)" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25s-7.5-4.108-7.5-11.25a7.5 7.5 0 1115 0z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 373 B |
3
public/img/icons/material-symbols/menu.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 231 B |
3
public/img/icons/material-symbols/search.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 261 B |
BIN
public/img/icons/minor-marker-icon-19.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/img/icons/minor-marker-icon-20.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/img/icons/system-marker-icon-15.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/img/icons/system-marker-icon-16.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
@@ -1,8 +1,39 @@
|
|||||||
// /redux/slices/database7polylines/polylineLayerVisibleSlice.js
|
// /redux/slices/database7polylines/polylineLayerVisibleSlice.js
|
||||||
|
|
||||||
import { createSlice } from "@reduxjs/toolkit";
|
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 = {
|
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({
|
const polylineLayerVisibleSlice = createSlice({
|
||||||
@@ -11,11 +42,51 @@ const polylineLayerVisibleSlice = createSlice({
|
|||||||
reducers: {
|
reducers: {
|
||||||
setPolylineVisible: (state, action) => {
|
setPolylineVisible: (state, action) => {
|
||||||
state.visible = action.payload;
|
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 { setPolylineVisible, initializePolylineFromLocalStorage } =
|
||||||
export const selectPolylineVisible = (state) => state.polylineLayerVisible.visible;
|
polylineLayerVisibleSlice.actions;
|
||||||
|
export const selectPolylineVisible = state => state.polylineLayerVisible.visible;
|
||||||
|
export const selectPolylineInitialized = state => state.polylineLayerVisible.isInitialized;
|
||||||
export default polylineLayerVisibleSlice.reducer;
|
export default polylineLayerVisibleSlice.reducer;
|
||||||
|
|||||||
@@ -15,20 +15,68 @@ const mapLayersSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setLayerVisibility: (state, action) => {
|
setLayerVisibility: (state, action) => {
|
||||||
const { layer, visibility } = action.payload;
|
const { layer, visibility } = action.payload;
|
||||||
if (state[layer] !== undefined) {
|
state[layer] = visibility; // Sicher setzen
|
||||||
state[layer] = visibility;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
setInitialLayers: (state, action) => {
|
setInitialLayers: (state, action) => {
|
||||||
const systems = action.payload; // Array of GisSystem
|
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}`;
|
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 { toggleLayer, setLayerVisibility, setInitialLayers } = mapLayersSlice.actions;
|
||||||
export const selectMapLayersState = (state) => state.mapLayers || initialState;
|
export const selectMapLayersState = state => state.mapLayers || initialState;
|
||||||
export default mapLayersSlice.reducer;
|
export default mapLayersSlice.reducer;
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<testsuites name="jest tests" tests="0" failures="0" errors="0" time="4.822">
|
|
||||||
</testsuites>
|
|
||||||