Compare commits
106 Commits
585c9ad9d7
...
develop
| 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 | ||
|
|
4e396a1b10 | ||
|
|
bc331eb7b2 | ||
|
|
016e1a927d | ||
|
|
89403164c6 | ||
|
|
7c4fbc3988 | ||
|
|
4070429193 | ||
|
|
b6c6fad3b3 | ||
|
|
12fec717d8 | ||
|
|
fe3cc48769 | ||
|
|
82963ead23 |
@@ -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.290
|
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.290
|
NEXT_PUBLIC_APP_VERSION=1.1.396
|
||||||
|
|||||||
25
.gitignore
vendored
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
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
778
README.html
Normal file
File diff suppressed because one or more lines are too long
27
README.md
27
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,7 +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:
|
||||||
|
http://10.10.0.28/produkte/TALAS.map/mapTiles.zip
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
BIN
README.pdf
Normal file
BIN
README.pdf
Normal file
Binary file not shown.
6
Start-Dev.ps1
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
1
StartNodeApp.bat
Normal file
@@ -0,0 +1 @@
|
|||||||
|
PowerShell -ExecutionPolicy Bypass -File "C:\inetpub\wwwroot\talas5\nodeMap\Start-Dev.ps1"
|
||||||
52
TODO.md
52
TODO.md
@@ -51,3 +51,55 @@ die Daten von DB auch mit WebSocket gelöst werden
|
|||||||
- [x] TODO: POI bearbeiten funktioniert es nicht
|
- [x] TODO: POI bearbeiten funktioniert es nicht
|
||||||
- [ ] TODO: Linien Links noch mit Port 3000
|
- [ ] TODO: Linien Links noch mit Port 3000
|
||||||
- [ ] TODO: Checkliste für README.md vorbereiten
|
- [ ] TODO: Checkliste für README.md vorbereiten
|
||||||
|
|
||||||
|
## 🐞 Aktuelle Bugs
|
||||||
|
|
||||||
|
- [ ] Tooltip zeigt `unknown` bei bestimmten Linien
|
||||||
|
→ prüfen in `setupPolylines.js`, ob alle Geräte korrekt benannt sind
|
||||||
|
|
||||||
|
- [ ] Tooltip zeigt `N/A` bei Station → untersuchen, ob `station.Name` in `createAndSetDevices.js`
|
||||||
|
gesetzt wird
|
||||||
|
|
||||||
|
## ✨ Ideen & Verbesserungen
|
||||||
|
|
||||||
|
- [ ] ESC-Taste schließt `VersionInfoModal`
|
||||||
|
- [ ] Zurück-Link in jedem `.md` Footer automatisieren
|
||||||
|
|
||||||
|
## 🧹 Technische Schulden
|
||||||
|
|
||||||
|
- [ ] Redundante Kontextmenülogik auflösen
|
||||||
|
- [ ] Bessere Trennung zwischen Mock- und Live-API in Service-Funktionen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
28.07.2025 IdSystem 11 GMA Glätemeldeanlagen, werden neu neu laden das Browser nich mehr geladen in
|
||||||
|
DB maps idsystem ändern und testen
|
||||||
|
|
||||||
|
# 12.09.2025
|
||||||
|
|
||||||
|
Die aktuelle Ansicht ist bei kleineren Auflösungen unübersichtlich bzw. es wird zuviel von der
|
||||||
|
eigentlichen Karte verdeckt. Unquittierter Alarm, critical
|
||||||
|
|
||||||
|
Zu Marker zoomen
|
||||||
|
|
||||||
|
Station suchen
|
||||||
|
|
||||||
|
- [ ] TODO: Unquittierter Alarm, critical 🚨 Alarm
|
||||||
|
- [ ] TODO: Zu Marker zoomen: Dropdown-Menu Station auswählen und hinein zoomen bis zu ausgewählte
|
||||||
|
in einem betimmten Zoom-Stufe der Leaflet (OSM) Station mit flyto in Leaflet 📍 POI
|
||||||
|
- [ ] TODO: Station suchen: CoordinateInput.js Modal soll über einem Icon 'Suche / Lupe' oben rechts
|
||||||
|
eingeblendet und ausgeblendet um mehr von der Karte zu sehen 🔍 Suche
|
||||||
|
- [ ] TODO: Editiermodus: EditMode Stift Icon aktivieren und deaktivieren um POI Position ändern zu
|
||||||
|
können wenn der User Berechtigung hat ✏️ Edit
|
||||||
|
- [ ] TODO: Vergrössern: Maximieren Icon Button um rauszoomen zu einem bestimmten Bereich, z.B.
|
||||||
|
Deutschland Karte im Fenster sichtbar ⬜ Fenster maximieren
|
||||||
|
|
||||||
|
- [ ] TODO: Ebenen (Openstreetmap): Stack/Stapel/Ebenen Icon um den Ansicht der Karten zu ändern 🗂️
|
||||||
|
Stapel
|
||||||
|
|
||||||
|
- [ ] TODO: Menü öffenen mit Kiste der Systeme: MapLayerControlPanel.js Modal soll über einem Icon
|
||||||
|
'Hamburger menu' oben rechts eingeblendet und ausgeblendet um mehr von der Karte zu sehen ☰
|
||||||
|
Hamburger-Menü
|
||||||
|
- [ ] TODO: Info Karte: VersionInfoModal.js Modal soll über einem Icon 'Info' oben rechts
|
||||||
|
eingeblendet und ausgeblendet um mehr von der Karte zu sehen ℹ️ Info
|
||||||
|
https://www.openstreetmap.org/#map=13/51.80097/9.33495&layers=P
|
||||||
|
|||||||
@@ -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"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// /hooks/layers/useDynamicDeviceLayers.js
|
// /hooks/layers/useDynamicDeviceLayers.js
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import L from "leaflet";
|
import L from "leaflet";
|
||||||
import { createAndSetDevices } from "../utils/devices/createAndSetDevices";
|
import { createAndSetDevices } from "@/utils/devices/createAndSetDevices";
|
||||||
import { checkOverlappingMarkers } from "../utils/mapUtils";
|
import { checkOverlappingMarkers } from "@/utils/mapUtils";
|
||||||
import plusRoundIcon from "@/components/icons/devices/overlapping/PlusRoundIcon";
|
import plusRoundIcon from "@/components/icons/devices/overlapping/PlusRoundIcon";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { selectGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice.js";
|
import { selectGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice.js";
|
||||||
@@ -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,0 +1,40 @@
|
|||||||
|
// /components/gisPolylines/tooltip/generateLineTooltipContent.js
|
||||||
|
// KEIN useSelector hier!
|
||||||
|
export function generateLineTooltipContent(statis, matchingLine, values, stations) {
|
||||||
|
const station = stations.find(s => s.IdLD === statis.IdLD);
|
||||||
|
const stationName = station?.LD_Name || "N/A";
|
||||||
|
|
||||||
|
const messageDisplay = values.messages
|
||||||
|
.map(
|
||||||
|
msg =>
|
||||||
|
`<span class="inline-block text-gray-800">
|
||||||
|
<span class="inline-block w-2 h-2 rounded-full mr-2" style="background-color: ${msg.prioColor};"></span>
|
||||||
|
${msg.message}
|
||||||
|
</span><br>`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="bg-white rounded-lg m-0 p-2 w-[210px]">
|
||||||
|
<span class="text-lg font-semibold text-gray-900">${statis.ModulName || "Unknown"}</span><br>
|
||||||
|
<span class="text-md font-bold text-gray-800">${statis.ModulTyp || "N/A"}</span><br>
|
||||||
|
<span class="text-md font-bold text-gray-800">Slot: ${statis.Modul || "N/A"}</span><br>
|
||||||
|
<span class="text-md font-bold text-gray-800">Station: ${stationName}</span>
|
||||||
|
<br>
|
||||||
|
<div style="max-width: 100%; overflow-wrap: break-word; word-break: break-word; white-space: normal;">
|
||||||
|
${messageDisplay}
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
${
|
||||||
|
values.messwert
|
||||||
|
? `<span class="inline-block text-gray-800">Messwert: ${values.messwert}</span><br>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
values.schleifenwert
|
||||||
|
? `<span class="inline-block text-gray-800">Schleifenwert: ${values.schleifenwert}</span>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
91
components/gisPolylines/tooltip/useLineData.js
Normal file
91
components/gisPolylines/tooltip/useLineData.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// /components/gisPolylines/tooltip/useLineData.js
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
|
import { selectGisLinesStatusFromWebservice } from "@/redux/slices/webservice/gisLinesStatusSlice";
|
||||||
|
import { fetchGisLinesThunk } from "@/redux/thunks/database/polylines/fetchGisLinesThunk";
|
||||||
|
import { fetchGisLinesStatusThunk } from "@/../redux/thunks/webservice/fetchGisLinesStatusThunk";
|
||||||
|
import { generateLineTooltipContent } from "./generateLineTooltipContent";
|
||||||
|
import { selectGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice";
|
||||||
|
|
||||||
|
const useLineData = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { data: statisData } = useSelector(selectGisLinesStatusFromWebservice);
|
||||||
|
const linesData = useSelector(state => state.gisLinesFromDatabase.data);
|
||||||
|
const [lineColors, setLineColors] = useState({});
|
||||||
|
const [tooltipContents, setTooltipContents] = useState({});
|
||||||
|
const stations = useSelector(selectGisStationsStaticDistrict)?.Points ?? [];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchGisLinesThunk());
|
||||||
|
dispatch(fetchGisLinesStatusThunk());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!statisData || !Array.isArray(statisData)) return;
|
||||||
|
|
||||||
|
const colorsByModule = {};
|
||||||
|
const newTooltipContents = {};
|
||||||
|
const valueMap = {};
|
||||||
|
|
||||||
|
const sortedStatis = [...statisData].sort((a, b) => a.Level - b.Level);
|
||||||
|
|
||||||
|
sortedStatis.forEach(statis => {
|
||||||
|
const key = `${statis.IdLD}-${statis.Modul}`;
|
||||||
|
if (!valueMap[key]) {
|
||||||
|
valueMap[key] = {
|
||||||
|
messages: [],
|
||||||
|
messwert: undefined,
|
||||||
|
schleifenwert: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
statis.DpName.endsWith("_Messwert") &&
|
||||||
|
statis.Value !== "True" &&
|
||||||
|
!valueMap[key].messwert
|
||||||
|
) {
|
||||||
|
valueMap[key].messwert = statis.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statis.DpName.endsWith("_Schleifenwert") && !valueMap[key].schleifenwert) {
|
||||||
|
valueMap[key].schleifenwert = statis.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statis.Message && statis.Message !== "?") {
|
||||||
|
valueMap[key].messages.push({
|
||||||
|
message: statis.Message,
|
||||||
|
prioColor:
|
||||||
|
statis.PrioColor && statis.PrioColor !== "#ffffff" ? statis.PrioColor : "green",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sortedStatis.forEach(statis => {
|
||||||
|
const key = `${statis.IdLD}-${statis.Modul}`;
|
||||||
|
const matchingLine = linesData.find(
|
||||||
|
item => item.idLD === statis.IdLD && item.idModul === statis.Modul
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingLine) {
|
||||||
|
const values = valueMap[key];
|
||||||
|
|
||||||
|
colorsByModule[key] = values.messages.length > 0 ? values.messages[0].prioColor : "green";
|
||||||
|
|
||||||
|
newTooltipContents[key] = generateLineTooltipContent(
|
||||||
|
statis,
|
||||||
|
matchingLine,
|
||||||
|
values,
|
||||||
|
stations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setLineColors(colorsByModule);
|
||||||
|
setTooltipContents(newTooltipContents);
|
||||||
|
}, [statisData, linesData, stations]);
|
||||||
|
|
||||||
|
return { lineColors, tooltipContents };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useLineData;
|
||||||
@@ -4,11 +4,11 @@ import { useDispatch } from "react-redux";
|
|||||||
// import type { AppDispatch } from "../redux/store";
|
// import type { AppDispatch } from "../redux/store";
|
||||||
|
|
||||||
// ✅ Thunks aus korrektem Pfad importieren
|
// ✅ Thunks aus korrektem Pfad importieren
|
||||||
import { fetchGisLinesStatusThunk } from "../redux/thunks/webservice/fetchGisLinesStatusThunk";
|
import { fetchGisLinesStatusThunk } from "@/redux/thunks/webservice/fetchGisLinesStatusThunk";
|
||||||
import { fetchGisStationsMeasurementsThunk } from "../redux/thunks/webservice/fetchGisStationsMeasurementsThunk";
|
import { fetchGisStationsMeasurementsThunk } from "@/redux/thunks/webservice/fetchGisStationsMeasurementsThunk";
|
||||||
import { fetchGisStationsStaticDistrictThunk } from "../redux/thunks/webservice/fetchGisStationsStaticDistrictThunk";
|
import { fetchGisStationsStaticDistrictThunk } from "@/redux/thunks/webservice/fetchGisStationsStaticDistrictThunk";
|
||||||
import { fetchGisStationsStatusDistrictThunk } from "../redux/thunks/webservice/fetchGisStationsStatusDistrictThunk";
|
import { fetchGisStationsStatusDistrictThunk } from "@/redux/thunks/webservice/fetchGisStationsStatusDistrictThunk";
|
||||||
import { fetchGisSystemStaticThunk } from "../redux/thunks/webservice/fetchGisSystemStaticThunk";
|
import { fetchGisSystemStaticThunk } from "@/redux/thunks/webservice/fetchGisSystemStaticThunk";
|
||||||
|
|
||||||
const REFRESH_INTERVAL = parseInt(process.env.NEXT_PUBLIC_REFRESH_INTERVAL || "10000");
|
const REFRESH_INTERVAL = parseInt(process.env.NEXT_PUBLIC_REFRESH_INTERVAL || "10000");
|
||||||
|
|
||||||
0
components/hooks/useStationCache.js
Normal file
0
components/hooks/useStationCache.js
Normal file
18
components/icons/material-symbols/AlarmIcon.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
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
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
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
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
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
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
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
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
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,23 +6,38 @@ 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";
|
||||||
import useAreaMarkersLayer from "@/hooks/useAreaMarkersLayer.js";
|
import useAreaMarkersLayer from "@/components/area/hooks/useAreaMarkersLayer.js";
|
||||||
import { setupPolylines } from "@/utils/polylines/setupPolylines.js";
|
import { setupPolylines } from "@/utils/polylines/setupPolylines.js";
|
||||||
import { setupPOIs } from "@/utils/setupPOIs.js";
|
import { setupPOIs } from "@/utils/setupPOIs.js";
|
||||||
import useLineData from "@/hooks/useLineData.js";
|
import useLineData from "@/components/gisPolylines/tooltip/useLineData.js";
|
||||||
import { useMapComponentState } from "@/hooks/useMapComponentState.js";
|
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 {
|
||||||
@@ -74,9 +92,9 @@ import { fetchPoiIconsDataThunk } from "@/redux/thunks/database/pois/fetchPoiIco
|
|||||||
import { fetchPoiTypThunk } from "@/redux/thunks/database/pois/fetchPoiTypThunk.js";
|
import { fetchPoiTypThunk } from "@/redux/thunks/database/pois/fetchPoiTypThunk.js";
|
||||||
import { updateAreaThunk } from "@/redux/thunks/database/area/updateAreaThunk";
|
import { updateAreaThunk } from "@/redux/thunks/database/area/updateAreaThunk";
|
||||||
|
|
||||||
import useDynamicDeviceLayers from "@/hooks/useDynamicDeviceLayers.js";
|
import useDynamicDeviceLayers from "@/components/devices/hooks/useDynamicDeviceLayers.js";
|
||||||
|
|
||||||
import useDataUpdater from "@/hooks/useDataUpdater";
|
import useDataUpdater from "@/components/hooks/useDataUpdater.js";
|
||||||
import { cleanupPolylinesForMemory } from "@/utils/polylines/cleanupPolylinesForMemory";
|
import { cleanupPolylinesForMemory } from "@/utils/polylines/cleanupPolylinesForMemory";
|
||||||
import { cleanupMarkers } from "@/utils/common/cleanupMarkers";
|
import { cleanupMarkers } from "@/utils/common/cleanupMarkers";
|
||||||
import { monitorHeapAndReload } from "@/utils/common/monitorMemory";
|
import { monitorHeapAndReload } from "@/utils/common/monitorMemory";
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ const PoiUpdateModal = ({ onClose, poiData }) => {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col mb-4">
|
<div className="flex flex-col mb-4">
|
||||||
<label htmlFor="description" className="block mb-2 font-bold text-sm text-gray-700">
|
<label htmlFor="description" className="block mb-2 font-bold text-sm text-gray-700">
|
||||||
Beschreibung:
|
Bezeichnung:
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -167,7 +167,7 @@ const PoiUpdateModal = ({ onClose, poiData }) => {
|
|||||||
name="description"
|
name="description"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={e => setDescription(e.target.value)}
|
onChange={e => setDescription(e.target.value)}
|
||||||
placeholder="Beschreibung der Station"
|
placeholder="Bezeichnung eingeben..."
|
||||||
className="block p-2 w-full border-2 border-gray-200 rounded-md text-sm"
|
className="block p-2 w-full border-2 border-gray-200 rounded-md text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
53
components/uiWidgets/AlarmIndicator.js
Normal file
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
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
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
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
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
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 618 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
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
BIN
docs/NodeMap.pdf
Binary file not shown.
@@ -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:
|
||||||
|
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
Zeigt ein modales Fenster mit Koordinateninformationen an, z. B. aus einem Kontextmenü heraus.
|
Zeigt ein modales Fenster mit Koordinateninformationen an, z. B. aus einem Kontextmenü heraus.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Darstellung eines Koordinatenwerts (`lat,lng`)
|
- Darstellung eines Koordinatenwerts (`lat,lng`)
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
# 🖱️ `contextmenu/` – Kontextmenü-Komponenten
|
# 🖱️ `contextmenu/` – Kontextmenü-Komponenten
|
||||||
|
|
||||||
Dieses Verzeichnis enthält Komponenten und Hooks zur Anzeige und Steuerung von Kontextmenüs in der Leaflet-Kartenanwendung. Sie dienen der Interaktion mit POIs, Koordinaten und Layer-Objekten per Rechtsklick.
|
Dieses Verzeichnis enthält Komponenten und Hooks zur Anzeige und Steuerung von Kontextmenüs in der
|
||||||
|
Leaflet-Kartenanwendung. Sie dienen der Interaktion mit POIs, Koordinaten und Layer-Objekten per
|
||||||
|
Rechtsklick.
|
||||||
|
|
||||||
---
|
## 
|
||||||
|
|
||||||
## 📂 Enthaltene Dateien
|
## 📂 Enthaltene Dateien
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
Initialisiert Kontextmenüeinträge für die Leaflet-Karte.
|
Initialisiert Kontextmenüeinträge für die Leaflet-Karte.
|
||||||
Wird typischerweise in `initializeMap()` oder `MapComponent` verwendet.
|
Wird typischerweise in `initializeMap()` oder `MapComponent` verwendet.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Kontextmenüeinträge
|
## Kontextmenüeinträge
|
||||||
|
|
||||||
| Eintrag | Funktion |
|
| Eintrag | Funktion |
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
Ein einfaches benutzerdefiniertes Kontextmenü zur Interaktion mit Linien (Polylinien) auf der Karte.
|
Ein einfaches benutzerdefiniertes Kontextmenü zur Interaktion mit Linien (Polylinien) auf der Karte.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Zweck
|
## Zweck
|
||||||
|
|
||||||
Das Menü erlaubt folgende Interaktionen:
|
Das Menü erlaubt folgende Interaktionen:
|
||||||
|
|||||||
@@ -1,3 +1,124 @@
|
|||||||
|
<!-- /docs/components/gisPolylines/README.md -->
|
||||||
|
|
||||||
# 📄 Übersicht: docs/components/gisPolylines
|
# 📄 Übersicht: docs/components/gisPolylines
|
||||||
|
|
||||||
- [PolylineContextMenu](PolylineContextMenu.md)
|
Diese Komponente verwaltet die Darstellung und Interaktion von GIS-Polylinien in der
|
||||||
|
Leaflet-Karte.
|
||||||
|
Sie kombiniert Statusdaten, statische Linien und Stationsdaten zu Tooltips und Farben.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧬 Data Flow Diagram
|
||||||
|
|
||||||
|
Dieses Diagramm zeigt den **Datenfluss** zwischen den Redux-Slices, Thunks, Service-Funktionen und
|
||||||
|
den React-Komponenten,
|
||||||
|
die für die Anzeige der **GIS-Polylinien** zuständig sind.
|
||||||
|
|
||||||
|
### 📖 Ablauf erklärt
|
||||||
|
|
||||||
|
1. **Beim Laden der Seite** ruft die Hook `useLineData` mehrere Thunks auf:
|
||||||
|
- Diese laden Linien, Statusdaten und Stationsdaten über entsprechende Service-Funktionen.
|
||||||
|
2. Die Thunks speichern die geladenen Daten in Redux-Slices.
|
||||||
|
3. `useLineData` liest diese Redux-Daten aus und kombiniert sie:
|
||||||
|
- Zuordnung nach `idLD` und `Modul`
|
||||||
|
4. Daraus entsteht:
|
||||||
|
- Eine Prioritätsfarbe für die Linie
|
||||||
|
- Ein Tooltip-HTML mit Modulname, Slot, Station (LD_Name) und Statusmeldungen.
|
||||||
|
5. Diese Daten werden weitergegeben an:
|
||||||
|
- `generateLineTooltipContent` → für Tooltips bei Hover
|
||||||
|
- `PolylineContextMenu` → für Kontextmenü bei Rechtsklick
|
||||||
|
- Leaflet Polyline-Komponente → für farbige Darstellung
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
|
||||||
|
subgraph Redux Store
|
||||||
|
A1[gisLinesFromDatabase Slice]
|
||||||
|
A2[gisLinesStatusFromWebservice Slice]
|
||||||
|
A3[gisStationsStaticDistrict Slice]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Thunks
|
||||||
|
T1[fetchGisLinesThunk]
|
||||||
|
T2[fetchGisLinesStatusThunk]
|
||||||
|
T3[fetchGisStationsStaticDistrictThunk]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Services
|
||||||
|
S1[fetchGisLinesService]
|
||||||
|
S2[fetchGisLinesStatusService]
|
||||||
|
S3[fetchGisStationsStaticDistrictService]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Komponenten
|
||||||
|
C1[useLineData Hook]
|
||||||
|
C2[generateLineTooltipContent]
|
||||||
|
C3[PolylineContextMenu.js]
|
||||||
|
C4[Leaflet Polyline Rendering]
|
||||||
|
end
|
||||||
|
|
||||||
|
T1 --> S1
|
||||||
|
T2 --> S2
|
||||||
|
T3 --> S3
|
||||||
|
|
||||||
|
T1 --> A1
|
||||||
|
T2 --> A2
|
||||||
|
T3 --> A3
|
||||||
|
|
||||||
|
A1 --> C1
|
||||||
|
A2 --> C1
|
||||||
|
A3 --> C1
|
||||||
|
|
||||||
|
C1 --> C2
|
||||||
|
C2 --> C4
|
||||||
|
C1 --> C3
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Wichtige Dateien
|
||||||
|
|
||||||
|
| Datei | Zweck |
|
||||||
|
| ------------------------------------------------------------------------ | -------------------------------------------------------------------- |
|
||||||
|
| [`useLineData.js`](tooltip/useLineData.js) | Holt Linien-, Status- und Stationsdaten aus Redux und kombiniert sie |
|
||||||
|
| [`generateLineTooltipContent.js`](tooltip/generateLineTooltipContent.js) | Baut HTML für die Tooltips |
|
||||||
|
| [`PolylineContextMenu.js`](PolylineContextMenu.md) | Kontextmenü zur Interaktion mit Linien |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[Zurück zur Übersicht](../../README.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📘 Technischer Hintergrund für Einsteiger
|
||||||
|
|
||||||
|
Diese Komponente verbindet Daten aus **zwei unterschiedlichen Systemen**:
|
||||||
|
|
||||||
|
1. **NodeMap App (Next.js API)**
|
||||||
|
→ liefert die **Geometrie der Linien** direkt aus der Datenbank (ohne Webservice).
|
||||||
|
2. **TALAS.web WebService**
|
||||||
|
→ liefert **Statusinformationen und Stationsnamen** (LD_Name).
|
||||||
|
|
||||||
|
### 🔄 Ablauf im Detail
|
||||||
|
|
||||||
|
- **Liniengeometrie** (`idLD`, `idModul`, `points`) kommt über `fetchGisLinesThunk` aus der
|
||||||
|
Datenbank.
|
||||||
|
- **Statusinformationen** (Meldungen, Farben, Modulname, Slot) kommen über
|
||||||
|
`fetchGisLinesStatusThunk`.
|
||||||
|
- **Stationsnamen** (LD_Name) kommen über `fetchGisStationsStaticDistrictThunk`.
|
||||||
|
- Die Hook `useLineData.js` verbindet alle Infos → erzeugt Tooltip-HTML & Farblogik.
|
||||||
|
- `generateLineTooltipContent.js` erstellt den konkreten Tooltip-HTML-String.
|
||||||
|
|
||||||
|
### 🧠 Wichtig für Debugging
|
||||||
|
|
||||||
|
- **Zuordnung** erfolgt immer über `idLD` und `Modul`.
|
||||||
|
- Stationen findest du im Slice `gisStationsStaticDistrict.Points[] → LD_Name`
|
||||||
|
- Linien findest du im Slice `gisLinesFromDatabase`
|
||||||
|
- Statusinfos findest du im Slice `gisLinesStatusFromWebservice`
|
||||||
|
|
||||||
|
🛠 **Fehler wie "Station: N/A"** entstehen, wenn `idLD` im Status vorhanden ist, aber keine passende
|
||||||
|
Station in `gisStationsStaticDistrict` gefunden wurde.
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
Ein einfacher, grauer runder Marker als Stützpunkt in einer Polyline.
|
Ein einfacher, grauer runder Marker als Stützpunkt in einer Polyline.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Eigenschaften
|
## Eigenschaften
|
||||||
|
|
||||||
- Stil: grauer Kreis mit schwarzem Rand
|
- Stil: grauer Kreis mit schwarzem Rand
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
Ein Viereck zur Markierung des Endpunkts einer Polyline.
|
Ein Viereck zur Markierung des Endpunkts einer Polyline.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Eigenschaften
|
## Eigenschaften
|
||||||
|
|
||||||
- Stil: graues Quadrat mit schwarzem Rand
|
- Stil: graues Quadrat mit schwarzem Rand
|
||||||
|
|||||||
@@ -4,3 +4,5 @@
|
|||||||
- [EndIcon](EndIcon.md)
|
- [EndIcon](EndIcon.md)
|
||||||
- [StartIcon](StartIcon.md)
|
- [StartIcon](StartIcon.md)
|
||||||
- [SupportPointIcons](SupportPointIcons.md)
|
- [SupportPointIcons](SupportPointIcons.md)
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
Ein SVG-Dreieck zur Markierung des Startpunkts einer Polyline.
|
Ein SVG-Dreieck zur Markierung des Startpunkts einer Polyline.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Eigenschaften
|
## Eigenschaften
|
||||||
|
|
||||||
- Schwarzes Dreieck mit grauem Overlay (Polygon SVG)
|
- Schwarzes Dreieck mit grauem Overlay (Polygon SVG)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
Definiert zwei Icons für interaktive Stützpunkte in einer Polyline:
|
Definiert zwei Icons für interaktive Stützpunkte in einer Polyline:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## AddSupportPointIcon
|
## AddSupportPointIcon
|
||||||
|
|
||||||
- Grüner Kreis mit weißem Rand und Pluszeichen
|
- Grüner Kreis mit weißem Rand und Pluszeichen
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
Ein einfaches Leaflet-Icon, das ein rundes Pluszeichen darstellt.
|
Ein einfaches Leaflet-Icon, das ein rundes Pluszeichen darstellt.
|
||||||
Wird für zusätzliche UI-Markierungen auf Geräten oder überlappenden Icons verwendet.
|
Wird für zusätzliche UI-Markierungen auf Geräten oder überlappenden Icons verwendet.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Eigenschaften
|
## Eigenschaften
|
||||||
|
|
||||||
| Attribut | Wert |
|
| Attribut | Wert |
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
# 📄 Übersicht: docs/components/icons/devices/overlapping
|
# 📄 Übersicht: docs/components/icons/devices/overlapping
|
||||||
|
|
||||||
- [PlusRoundIcon](PlusRoundIcon.md)
|
- [PlusRoundIcon](PlusRoundIcon.md)
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
Die zentrale React-Komponente zur Darstellung und Steuerung der Leaflet-Karte.
|
Die zentrale React-Komponente zur Darstellung und Steuerung der Leaflet-Karte.
|
||||||
Bindet alle Marker, Layer, POIs, Linien und das Kontextmenü dynamisch ein.
|
Bindet alle Marker, Layer, POIs, Linien und das Kontextmenü dynamisch ein.
|
||||||
|
|
||||||
---
|

|
||||||
|
|
||||||
## 🎯 Zweck
|
## 🎯 Zweck
|
||||||
|
|
||||||
@@ -41,6 +41,8 @@ Verwendet umfangreiche Redux-Slices zur Steuerung von:
|
|||||||
- Sichtbarkeit einzelner Layergruppen
|
- Sichtbarkeit einzelner Layergruppen
|
||||||
- Aktuelle Selektion (Area, Gerät, POI)
|
- Aktuelle Selektion (Area, Gerät, POI)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔧 Lokale Steuerung
|
## 🔧 Lokale Steuerung
|
||||||
@@ -49,6 +51,8 @@ Verwendet umfangreiche Redux-Slices zur Steuerung von:
|
|||||||
- Karte speichert Zoom & Center dauerhaft im Browser
|
- Karte speichert Zoom & Center dauerhaft im Browser
|
||||||
- Kontextmenü-Einträge ändern sich je nach Rechten & Modus
|
- Kontextmenü-Einträge ändern sich je nach Rechten & Modus
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧪 Besonderheiten
|
## 🧪 Besonderheiten
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
# 📄 Übersicht: docs/components/mainComponent
|
# 📄 Übersicht: docs/components/mainComponent
|
||||||
|
|
||||||
|
Die zentrale React-Komponente zur Darstellung und Steuerung der Leaflet-Karte.
|
||||||
|
Bindet alle Marker, Layer, POIs, Linien und das Kontextmenü dynamisch ein.
|
||||||
|
|
||||||
- [MapComponent](MapComponent.md)
|
- [MapComponent](MapComponent.md)
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
Zeigt ein modales Formular an, um einen neuen POI auf der Karte zu erstellen.
|
Zeigt ein modales Formular an, um einen neuen POI auf der Karte zu erstellen.
|
||||||
Die Koordinaten (`latlng`) werden automatisch übernommen.
|
Die Koordinaten (`latlng`) werden automatisch übernommen.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Funktionen
|
## Funktionen
|
||||||
|
|
||||||
- POI-Name, Typ und zugehöriges Gerät auswählbar
|
- POI-Name, Typ und zugehöriges Gerät auswählbar
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
Ein Dialog zur Aktualisierung oder Löschung bestehender POIs.
|
Ein Dialog zur Aktualisierung oder Löschung bestehender POIs.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Zeigt aktuellen Namen, Beschreibung, Gerät und Typ
|
- Zeigt aktuellen Namen, Beschreibung, Gerät und Typ
|
||||||
|
|||||||
@@ -2,3 +2,5 @@
|
|||||||
|
|
||||||
- [AddPOIModal](AddPOIModal.md)
|
- [AddPOIModal](AddPOIModal.md)
|
||||||
- [PoiUpdateModal](PoiUpdateModal.md)
|
- [PoiUpdateModal](PoiUpdateModal.md)
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!-- /docs/components/uiWidgets/CoordinateInput.md -->
|
<!-- /docs/components/uiWidgets/CoordinateInput.md -->
|
||||||
|
|
||||||
# 📍 CoordinateInput.js
|
# CoordinateInput.js
|
||||||
|
|
||||||
Die Komponente `CoordinateInput` stellt ein einfaches Eingabefeld für geografische Koordinaten
|
Die Komponente `CoordinateInput` stellt ein einfaches Eingabefeld für geografische Koordinaten
|
||||||
(Latitude, Longitude) bereit.
|
(Latitude, Longitude) bereit.
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# 📄 Übersicht: docs/components/uiWidgets
|
# 📄 UI-Widgets
|
||||||
|
|
||||||
|
- [mapLayersControlPanel](mapLayersControlPanel/mapLayersControlPanel.md)
|
||||||
- [CoordinateInput](CoordinateInput.md)
|
- [CoordinateInput](CoordinateInput.md)
|
||||||
- [VersionInfoModal](VersionInfoModal.md)
|
- [VersionInfoModal](VersionInfoModal.md)
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -8,6 +8,10 @@ Es wird meist im Footer oder als Info-Schaltfläche in der Benutzeroberfläche e
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🔧 Pfad
|
## 🔧 Pfad
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -24,6 +28,7 @@ Die Komponente informiert Nutzer über:
|
|||||||
- Die **Firmenadresse und Kontaktdaten** der Littwin Systemtechnik GmbH & Co. KG
|
- Die **Firmenadresse und Kontaktdaten** der Littwin Systemtechnik GmbH & Co. KG
|
||||||
- Eine zentral platzierte Grafik mit dem TALAS-Logo
|
- Eine zentral platzierte Grafik mit dem TALAS-Logo
|
||||||
- Eine Schaltfläche zum Schließen des Modals
|
- Eine Schaltfläche zum Schließen des Modals
|
||||||
|

|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -46,20 +51,6 @@ Die Komponente informiert Nutzer über:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧩 Inhalt im Modal
|
|
||||||
|
|
||||||
```plaintext
|
|
||||||
+--------------------------+
|
|
||||||
| [Logo_TALAS.png] |
|
|
||||||
| Littwin GmbH Adresse |
|
|
||||||
| Telefon & E-Mail |
|
|
||||||
| Version: 1.1.188 |
|
|
||||||
| [Schließen] Button |
|
|
||||||
+--------------------------+
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 Gestaltung
|
## 🎨 Gestaltung
|
||||||
|
|
||||||
- Modal-Layout mit Tailwind CSS (`fixed`, `z-50`, `bg-white`, `rounded`, `shadow`)
|
- Modal-Layout mit Tailwind CSS (`fixed`, `z-50`, `bg-white`, `rounded`, `shadow`)
|
||||||
@@ -75,7 +66,7 @@ Die Komponente informiert Nutzer über:
|
|||||||
| `showVersionInfoModal = true` | Modal wird angezeigt |
|
| `showVersionInfoModal = true` | Modal wird angezeigt |
|
||||||
| Klick auf Hintergrund | Modal wird geschlossen |
|
| Klick auf Hintergrund | Modal wird geschlossen |
|
||||||
| Klick auf „Schließen“-Button | Modal wird geschlossen |
|
| Klick auf „Schließen“-Button | Modal wird geschlossen |
|
||||||
| Version `APP_VERSION = 1.1.188` | Text „TALAS.Map Version 1.1.188“ sichtbar |
|
| Version `APP_VERSION = 1.1.290` | Text „TALAS.Map Version 1.1.290“ sichtbar |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -89,7 +80,6 @@ Die Komponente informiert Nutzer über:
|
|||||||
## 🛠 Verbesserungsideen
|
## 🛠 Verbesserungsideen
|
||||||
|
|
||||||
- ESC-Taste als Schließen-Funktion ergänzen
|
- ESC-Taste als Schließen-Funktion ergänzen
|
||||||
- Option für dynamische Anzeige von Changelog-Link
|
|
||||||
- Automatischer Import von Version via `process.env.NEXT_PUBLIC_APP_VERSION`
|
- Automatischer Import von Version via `process.env.NEXT_PUBLIC_APP_VERSION`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
<!-- /docs/hooks/layers/useCiscoRouterMarkersLayer.md -->
|
|
||||||
|
|
||||||
# 🌐 useCiscoRouterMarkersLayer.js
|
|
||||||
|
|
||||||
Hook zur Verwaltung aller Cisco-Router-Marker in der Leaflet-Karte.
|
|
||||||
|
|
||||||
## Funktionen
|
|
||||||
|
|
||||||
- Lädt Geräte per `createAndSetDevices(6, ...)`
|
|
||||||
- Fügt Marker hinzu & registriert Popup/Kontextmenü
|
|
||||||
- Verwendet `checkOverlappingMarkers(...)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[Zurück zur Übersicht](../../README.md)
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<!-- /docs/hooks/layers/useDauzMarkersLayer.md -->
|
|
||||||
|
|
||||||
# 🔧 useDauzMarkersLayer.js
|
|
||||||
|
|
||||||
Spezialisierter Hook zur Verwaltung von DAUZ-Gerätemarkern (System-ID: 110)
|
|
||||||
|
|
||||||
## Verhalten
|
|
||||||
|
|
||||||
- Marker mit Popup & Kontextmenü
|
|
||||||
- Nutzung von `createAndSetDevices(...)`
|
|
||||||
- Sichtbarkeit direkt über Kartenlayer steuerbar
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[Zurück zur Übersicht](../../README.md)
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<!-- /docs/hooks/layers/useEciMarkersLayer.md -->
|
|
||||||
|
|
||||||
# 🛰️ useEciMarkersLayer.js
|
|
||||||
|
|
||||||
Verwaltet die Darstellung und Events für ECI-Marker (System-ID: 2)
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Kontextmenü & Popup für jeden Marker
|
|
||||||
- Erkennung überlappender Marker (`checkOverlappingMarkers`)
|
|
||||||
- Nutzung von `createAndSetDevices(...)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[Zurück zur Übersicht](../../README.md)
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<!-- /docs/hooks/layers/useGmaMarkersLayer.md -->
|
|
||||||
|
|
||||||
# 🌡️ useGmaMarkersLayer.js
|
|
||||||
|
|
||||||
Spezialhook für GMA-Marker mit Messwertanzeige (LT, FBT, GT, RLF).
|
|
||||||
|
|
||||||
## Besonderheiten
|
|
||||||
|
|
||||||
- Tooltip enthält Temperatur-/Feuchtigkeitswerte aus Redux
|
|
||||||
- Eigenes Kontextmenü mit Zoom/Zentrieren
|
|
||||||
- Verwendet `marker.options.areaName` zur Messzuordnung
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[Zurück zur Übersicht](../../README.md)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<!-- /docs/hooks/layers/useLteModemMarkersLayer.md -->
|
|
||||||
|
|
||||||
# 📶 useLteModemMarkersLayer.js
|
|
||||||
|
|
||||||
Steuert Marker vom Typ LTE-Modem (System-ID: 5)
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Standard-Kontextmenü + Popup
|
|
||||||
- Integration mit OMS und Overlap-Check
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[Zurück zur Übersicht](../../README.md)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<!-- /docs/hooks/layers/useMessstellenMarkersLayer.md -->
|
|
||||||
|
|
||||||
# 🧾 useMessstellenMarkersLayer.js
|
|
||||||
|
|
||||||
Für Messstellen-Marker (System-ID: 13)
|
|
||||||
|
|
||||||
## Verhalten
|
|
||||||
|
|
||||||
- Einfache Marker mit Tooltip
|
|
||||||
- Nutzung von `createAndSetDevices(...)` + Kontextmenü
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[Zurück zur Übersicht](../../README.md)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<!-- /docs/hooks/layers/useOtdrMarkersLayer.md -->
|
|
||||||
|
|
||||||
# 🔍 useOtdrMarkersLayer.js
|
|
||||||
|
|
||||||
Darstellung von OTDR-Messpunkten (System-ID: 9)
|
|
||||||
|
|
||||||
## Funktionen
|
|
||||||
|
|
||||||
- Popup-Interaktion beim Hover
|
|
||||||
- Marker mit Kontextmenü via `addContextMenuToMarker`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[Zurück zur Übersicht](../../README.md)
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# 🏭 useSiemensMarkersLayer.js
|
|
||||||
|
|
||||||
Für Siemens-Geräte (System-ID: 8).
|
|
||||||
|
|
||||||
- Marker mit Kontextmenü und Overlap-Prüfung
|
|
||||||
- Integration mit OMS
|
|
||||||
- Nutzung von `checkOverlappingMarkers(...)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[Zurück zur Übersicht](../../README.md)
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# 📡 useSmsfunkmodemMarkersLayer.js
|
|
||||||
|
|
||||||
Filtert `GisSystemStatic` nach SMS Modem (System 111 oder Name).
|
|
||||||
|
|
||||||
- Icon: `/img/icons/pois/sms-funkmodem.png`
|
|
||||||
- Kontextmenü & Popup
|
|
||||||
- Sichtbarkeit über `isVisible` steuerbar
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[Zurück zur Übersicht](../../README.md)
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# ❔ useSonstigeMarkersLayer.js
|
|
||||||
|
|
||||||
Für alle Geräte mit System-ID 200 (Sonstige).
|
|
||||||
|
|
||||||
- Klassische Leaflet-Marker
|
|
||||||
- Kontextmenü und Popup
|
|
||||||
- Nutzung von `createAndSetDevices(...)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[Zurück zur Übersicht](../../README.md)
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# 🌐 useTalasMarkersLayer.js
|
|
||||||
|
|
||||||
Für TALAS-Systeme (System-ID: 1).
|
|
||||||
|
|
||||||
- Popup + Kontextmenü auf Marker
|
|
||||||
- Fügt Marker zuerst zu OMS, dann zu Karte hinzu
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[Zurück zur Übersicht](../../README.md)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user