Compare commits

...

21 Commits

Author SHA1 Message Date
ISA
bfd091b1b1 test: slow Motion 2025-09-16 16:38:14 +02:00
ISA
81b6379895 test(e2e): Playwright tests passing 2025-09-16 15:50:53 +02:00
ISA
42ca88d27e chore: playwright ohne webserver 2025-09-16 14:27:53 +02:00
ISA
fdb70d892c chore: move playwright test and reports in playwright folder 2025-09-16 14:18:50 +02:00
ISA
73e9c63e36 chore: move report into playwright 2025-09-16 14:00:05 +02:00
ISA
e520207526 feat: Plus und Minus Icons 2025-09-16 13:47:11 +02:00
ISA
2e5acf9327 feat: Plus und Minus Icons 2025-09-16 13:32:22 +02:00
ISA
cdfdd3d6cf chore: gitignore playwright Artefakte ignorieren und nur Test Datei annehmen 2025-09-16 12:28:10 +02:00
ISA
5b86d5293b feat: Plus und Minus Zoom Icons 2025-09-16 12:13:49 +02:00
ISA
31c770f778 feat: Plus und Minus Zoom Icons 2025-09-16 12:12:31 +02:00
ISA
051dd4c306 chore: maxZoom = 20; 2025-09-16 11:56:33 +02:00
ISA
995f084e15 chore: maxZoom = 20; 2025-09-16 11:55:41 +02:00
ISA
eaacec71da chore: test 2025-09-16 11:47:04 +02:00
ISA
6bc2e16657 style: alle Icons Panels in gleiche Position bringen 2025-09-16 11:20:49 +02:00
ISA
1208024f76 chore: alle Panels zu den selben Position bringen 2025-09-16 10:57:23 +02:00
ISA
369f29a769 feat(ui): add AreaDropdown and exclusive toggle with layers panel
New AreaDropdown component for quick station selection (filters by allowed systems, ESC to close)
MapComponent: toggle AreaDropdown via MapMarkerIcon; auto-hide MapLayersControlPanel when dropdown is open and vice versa
fix(alarms): hasActiveAlarm now checks Statis[].Alarm for both array and object shapes
fix(panel): Kabelstrecken now auto-enables TALAS (system-1) when turned on; keeps behavior to disable polylines when TALAS is unchecked; persists visibility to localStorage and emits visibilityChanged
Minor: imports, state wiring, and render guards updated
Affected files:

MapComponent.js
MapLayersControlPanel.js
AreaDropdown.js (new)
2025-09-15 13:53:16 +02:00
ISA
d166b2468d feat: AreaDropdown separate from MapLayerControlPanel 2025-09-15 13:38:19 +02:00
ISA
59c8680c23 feat: AlarmIcon nur bei GisStationsStatusDistrict Attribute Alarm :1 2025-09-15 13:03:54 +02:00
ISA
1a046f8212 style: Icon as components and littwin-blue 2025-09-15 11:52:20 +02:00
ISA
e35216daf5 chore: change icons order 2025-09-15 10:47:25 +02:00
ISA
91ad47166f del: BasMapPanel entfernt aus rechliche Gründe,
Kurzantwort: Für kommerzielle Nutzung sind OSM‑Community‑Tile‑Server nicht geeignet. Nutze einen bezahlten Anbieter (z. B. Thunderforest, Tracestrack) oder hoste selbst. Attribution ist immer Pflicht.

Links und Hinweise je Layer/Provider:

OpenStreetMap Standard (osm-standard)

Lizenz/Daten: ODbL, Attribution Pflicht
Tile-Server-Policy (keine Produktion/hohe Last): https://operations.osmfoundation.org/policies/tiles/
Urheberrecht/Attribution: https://www.openstreetmap.org/copyright
HOT Humanitarian (osm-humanitarian)

Community-Server (OSM France); keine Produktion/hohe Last
Info/Policy OSM France Tiles: https://tile.openstreetmap.fr/
HOT: https://www.hotosm.org/
CyclOSM (cyclosm)

Community-Server (OSM France); keine Produktion/hohe Last
Projektseite: https://www.cyclosm.org/
Hinweise/Policy (OSM France): https://tile.openstreetmap.fr/
Wiki: https://wiki.openstreetmap.org/wiki/CyclOSM
Carto Light (carto-light / Positron)

Keylos nutzbar mit Attribution; Fair‑Use, für hohe Last über CARTO‑Pläne
Basemaps: https://carto.com/basemaps/
Attribution: https://carto.com/attributions
Pricing (Plattform): https://carto.com/pricing/ (bei großem Volumen Sales kontaktieren)
Thunderforest (Cycle/Transport u. a.)

Kommerziell mit API‑Key; Pläne von Free bis Pro
Pricing: https://www.thunderforest.com/pricing/
Terms/Attribution: https://www.thunderforest.com/terms/
Tracestrack Topo

API‑Key erforderlich; kostenlose und bezahlte Pläne
Übersicht/Pricing: https://www.tracestrack.com/en/maps/
Nutzungsbedingungen: https://www.tracestrack.com/en/terms/
2025-09-15 10:36:03 +02:00
40 changed files with 820 additions and 218 deletions

View File

@@ -23,4 +23,4 @@ NEXT_PUBLIC_USE_MOCKS=true
# z.B. http://10.10.0.13/xyz/index.aspx -> basePath in config.json auf /xyz setzen
# basePath wird jetzt in public/config.json gepflegt
# App-Versionsnummer
NEXT_PUBLIC_APP_VERSION=1.1.361
NEXT_PUBLIC_APP_VERSION=1.1.382

View File

@@ -24,4 +24,4 @@ NEXT_PUBLIC_USE_MOCKS=false
# basePath wird jetzt in public/config.json gepflegt
# App-Versionsnummer
NEXT_PUBLIC_APP_VERSION=1.1.361
NEXT_PUBLIC_APP_VERSION=1.1.382

22
.gitignore vendored
View File

@@ -35,3 +35,25 @@ docs.zip
/mockData/
/__mocks__/
/__tests__/
# --- Playwright artifacts & test selection ---
# Ignore Playwright output folders nested under playwright/
/playwright/test-results/
/playwright/playwright-report/
/playwright/.last-run.json
# If you ever enable these paths, keep them under playwright/ and ignore them
/playwright/traces/
/playwright/screenshots/
/playwright/videos/
# Ignore JUnit report artifacts under playwright/ (currently unused)
/playwright/reports/junit/
# Track only spec files under playwright/tests; ignore other files in that folder
/playwright/tests/**
!/playwright/tests/**/*.spec.js
!/playwright/tests/**/*.spec.ts
# Ignore Playwright cache if present
/playwright/.cache/
# playwright reports
/playwright/reports/

View File

@@ -0,0 +1,18 @@
const AlarmIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="red"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 6.9L3.87 4.78l1.41-1.41L7.4 5.5zM13 1v3h-2V1zm7.13 3.78L18 6.9l-1.4-1.4l2.12-2.13zM4.5 10.5v2h-3v-2zm15 0h3v2h-3zM6 20h12a2 2 0 0 1 2 2H4a2 2 0 0 1 2-2m6-15a6 6 0 0 1 6 6v8H6v-8a6 6 0 0 1 6-6"
/>
</svg>
);
export default AlarmIcon;

View File

@@ -0,0 +1,18 @@
const EditIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
/>
</svg>
);
export default EditIcon;

View File

@@ -0,0 +1,18 @@
const EditOffIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 21l-3-3m-12.728-.364A9 9 0 015.636 5.636m0 0L3 3l3 3m9.364 9.364L18 21M5.636 5.636L3 3"
/>
</svg>
);
export default EditOffIcon;

View File

@@ -0,0 +1,18 @@
const ExpandIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"
/>
</svg>
);
export default ExpandIcon;

View File

@@ -0,0 +1,18 @@
const InfoIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
/>
</svg>
);
export default InfoIcon;

View File

@@ -0,0 +1,19 @@
const MapMarkerIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="rgb(0, 174, 239)"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25s-7.5-4.108-7.5-11.25a7.5 7.5 0 1115 0z"
/>
</svg>
);
export default MapMarkerIcon;

View File

@@ -0,0 +1,18 @@
const MenuIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
);
export default MenuIcon;

View File

@@ -0,0 +1,14 @@
const MinusIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
strokeWidth="1.5"
stroke="currentColor"
className={className}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 12h14" />
</svg>
);
export default MinusIcon;

View File

@@ -0,0 +1,14 @@
const PlusIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
strokeWidth="1.5"
stroke="currentColor"
className={className}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 5v14M5 12h14" />
</svg>
);
export default PlusIcon;

View File

@@ -0,0 +1,18 @@
const SearchIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
/>
</svg>
);
export default SearchIcon;

View File

@@ -7,6 +7,16 @@ import "leaflet-contextmenu";
import "leaflet.smooth_marker_bouncing";
import "react-toastify/dist/ReactToastify.css";
import { Icon } from "@iconify/react";
import EditIcon from "@/components/icons/material-symbols/EditIcon";
import EditOffIcon from "@/components/icons/material-symbols/EditOffIcon";
import SearchIcon from "@/components/icons/material-symbols/SearchIcon";
import MenuIcon from "@/components/icons/material-symbols/MenuIcon";
import InfoIcon from "@/components/icons/material-symbols/InfoIcon";
import AlarmIcon from "@/components/icons/material-symbols/AlarmIcon";
import MapMarkerIcon from "@/components/icons/material-symbols/MapMarkerIcon";
import ExpandIcon from "@/components/icons/material-symbols/ExpandIcon";
import PlusIcon from "@/components/icons/material-symbols/PlusIcon";
import MinusIcon from "@/components/icons/material-symbols/MinusIcon";
import PoiUpdateModal from "@/components/pois/poiUpdateModal/PoiUpdateModal.js";
import { ToastContainer, toast } from "react-toastify";
import plusRoundIcon from "../icons/devices/overlapping/PlusRoundIcon.js";
@@ -24,9 +34,9 @@ import { useMapComponentState } from "@/components/hooks/useMapComponentState.js
import CoordinatePopup from "@/components/contextmenu/CoordinatePopup.js";
//----------Ui Widgets----------------
import MapLayersControlPanel from "@/components/uiWidgets/mapLayersControlPanel/MapLayersControlPanel.js";
import BaseMapPanel from "@/components/uiWidgets/baseMapPanel/BaseMapPanel.js";
import CoordinateInput from "@/components/uiWidgets/CoordinateInput.js";
import VersionInfoModal from "@/components/uiWidgets/VersionInfoModal.js";
import AreaDropdown from "@/components/uiWidgets/AreaDropdown";
//----------Daten aus API--------------------
import { fetchPoiDataService } from "@/services/database/pois/fetchPoiDataByIdService.js";
import AddPOIModal from "@/components/pois/AddPOIModal.js";
@@ -132,6 +142,13 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
const { data: gisLinesStatusData, status: statusGisLinesStatus } = useSelector(
selectGisLinesStatusFromWebservice
);
// Alarm Status aus GisStationsStatusDistrict
const gisStationsStatusDistrict = useSelector(state => state.gisStationsStatusDistrict.data);
// Unterstützt sowohl Array-Shape (Statis[]) als auch Objekt mit Statis-Array
const hasActiveAlarm = Array.isArray(gisStationsStatusDistrict)
? gisStationsStatusDistrict.some(item => item?.Alarm === 1)
: gisStationsStatusDistrict?.Statis?.some(item => item?.Alarm === 1) || false;
const poiIconsData = useSelector(selectPoiIconsData);
const poiIconsStatus = useSelector(selectPoiIconsStatus);
const poiTypData = useSelector(selectPoiTypData);
@@ -150,6 +167,14 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
const [showVersionInfoModal, setShowVersionInfoModal] = useState(false);
const [poiTypMap, setPoiTypMap] = useState(new Map());
const [showPopup, setShowPopup] = useState(false);
const [showAreaDropdown, setShowAreaDropdown] = useState(() => {
try {
const v = localStorage.getItem("showAreaDropdown");
return v === null ? false : v === "true";
} catch (_) {
return false;
}
});
const poiLayerRef = useRef(null); // Referenz auf die Layer-Gruppe für Datenbank-Marker
const mapRef = useRef(null); // Referenz auf das DIV-Element der Karte
const [map, setMap] = useState(null); // Zustand der Karteninstanz
@@ -172,15 +197,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
return true;
}
});
// Sichtbarkeit des Base-Map Panels (oben rechts, unter Toolbar)
const [showBaseMapPanel, setShowBaseMapPanel] = useState(() => {
try {
const v = localStorage.getItem("showBaseMapPanel");
return v === null ? false : v === "true";
} catch (_) {
return false;
}
});
// Base-Map Panel wurde entfernt
// Sichtbarkeit der Koordinaten-Suche (Lupe)
const [showCoordinateInput, setShowCoordinateInput] = useState(() => {
try {
@@ -191,6 +208,29 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
}
});
// 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) {
@@ -262,18 +302,19 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
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]);
// Persistiere Sichtbarkeit des Base-Map Panels
useEffect(() => {
try {
localStorage.setItem("showBaseMapPanel", String(showBaseMapPanel));
} catch (_) {}
}, [showBaseMapPanel]);
// Persist-Logik für Base-Map Panel entfernt
// Persistiere Sichtbarkeit der Koordinaten-Suche
useEffect(() => {
try {
@@ -1132,7 +1173,8 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
{GisStationsStaticDistrict &&
GisStationsStaticDistrict.Points?.length > 0 &&
showLayersPanel && (
showLayersPanel &&
!showAreaDropdown && (
<MapLayersControlPanel
className="z-50"
handlePolylineCheckboxChange={handlePolylineCheckboxChange}
@@ -1143,6 +1185,39 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
<div id="map" ref={mapRef} className="z-0" style={{ height: "100vh", width: "100vw" }}></div>
{/* Top-right controls: layers, info, expand, edit, and base map stack */}
<div className="absolute top-3 right-3 z-50 pointer-events-auto flex items-center gap-2">
{/* Alarm-Icon - nur anzeigen wenn Alarm aktiv */}
{hasActiveAlarm && (
<button
onClick={() => {}}
aria-label="Alarm aktiv"
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title="Alarm aktiv"
>
<AlarmIcon className="h-8 w-8 animate-pulse text-red-500" />
</button>
)}
{/* Marker-Icon (line-md) */}
<button
onClick={() => setOverlay(prev => (prev === "area" ? null : "area"))}
aria-label="Marker"
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title="Marker"
>
<MapMarkerIcon className="h-8 w-8" />
</button>
{/*Lupe: Koordinatensuche ein-/ausblenden */}
<button
onClick={() => setOverlay(prev => (prev === "coord" ? null : "coord"))}
aria-label={
showCoordinateInput ? "Koordinatensuche ausblenden" : "Koordinatensuche einblenden"
}
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title={
showCoordinateInput ? "Koordinatensuche ausblenden" : "Koordinatensuche einblenden"
}
>
<SearchIcon className="h-8 w-8" />
</button>
<button
onClick={toggleEditMode}
aria-label={editMode ? "Bearbeitungsmodus deaktivieren" : "Bearbeitungsmodus aktivieren"}
@@ -1160,88 +1235,67 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
}
disabled={!hasEditRight}
>
<Icon
icon={editMode ? "material-symbols:edit-off-rounded" : "material-symbols:edit-rounded"}
className="h-8 w-8 text-blue-900"
/>
{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"
className="rounded-full bg-white/90 hover:bg-white shadow p-1 "
title="Karte auf Standardansicht"
>
<img src="/img/expand-icon.svg" alt="Expand" className="h-8 w-8" />
<ExpandIcon className="h-8 w-8" />
</button>
{/* Lupe: Koordinaten-Suche ein-/ausblenden */}
<button
onClick={() => setShowBaseMapPanel(v => !v)}
aria-label={
showBaseMapPanel ? "Kartenhintergrund ausblenden" : "Kartenhintergrund wählen"
}
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title={showBaseMapPanel ? "Kartenhintergrund ausblenden" : "Kartenhintergrund wählen"}
>
<Icon icon="material-symbols:layers-rounded" className="h-8 w-8 text-blue-900" />
</button>
<button
onClick={() => setShowLayersPanel(v => !v)}
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"}
>
<Icon icon="material-symbols:menu-rounded" className="h-8 w-8 text-blue-900" />
<MenuIcon className="h-8 w-8" />
</button>
{/* Lupe: Koordinaten-Suche ein-/ausblenden */}
<button
onClick={() => setShowCoordinateInput(v => !v)}
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"
}
>
<Icon icon="material-symbols:search-rounded" className="h-8 w-8 text-blue-900" />
</button>
{/* Marker-Icon (line-md) */}
<button
onClick={() => {}}
aria-label="Marker"
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title="Marker"
>
<Icon icon="line-md:map-marker-filled" className="h-8 w-8 text-blue-900" />
</button>
{/* Alarm-Icon (mdi) */}
<button
onClick={() => {}}
aria-label="Alarm"
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title="Alarm"
>
<Icon icon="mdi:alarm-light-outline" className="h-8 w-8 text-blue-900" />
</button>
<button
onClick={() => setShowAppInfoCard(v => !v)}
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"}
>
<Icon
icon="material-symbols:info-rounded"
className="text-blue-900 h-8 w-8 pr-1"
<InfoIcon
className="h-8 w-8 pr-1"
title={showAppInfoCard ? "Info ausblenden" : "Info einblenden"}
/>
</button>
</div>
{showBaseMapPanel && map && (
<BaseMapPanel map={map} onClose={() => setShowBaseMapPanel(false)} />
)}
{/* 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} />
{showAppInfoCard && (
<div className="absolute bottom-3 left-3 w-72 p-4 bg-white rounded-lg shadow-md z-50">
<div className="absolute top-16 right-3 w-72 p-4 bg-white rounded-lg shadow-md z-50">
<div className="flex justify-between items-center">
<div>
<span className="text-black text-lg font-semibold"> TALAS.Map </span>
@@ -1250,11 +1304,7 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
</div>
<div>
<button onClick={openVersionInfoModal}>
<Icon
icon="material-symbols:info-rounded"
className="text-blue-900 h-8 w-8 pr-1"
title="Weitere Infos"
/>
<InfoIcon className="h-8 w-8 pr-1" title="Weitere Infos" />
</button>
</div>
</div>

View File

@@ -0,0 +1,79 @@
// /components/uiWidgets/AreaDropdown.js
import React, { useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { setSelectedArea } from "@/redux/slices/selectedAreaSlice";
import { selectGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice";
import { selectGisSystemStatic } from "@/redux/slices/webservice/gisSystemStaticSlice";
/**
* Kleines Dropdown zur Auswahl der Station (Area_Name),
* nutzt dieselbe Datenquelle wie das MapLayersControlPanel.
*/
const AreaDropdown = ({ onClose }) => {
const dispatch = useDispatch();
const GisStationsStaticDistrict = useSelector(selectGisStationsStaticDistrict) || {};
const GisSystemStatic = useSelector(selectGisSystemStatic) || [];
// Erlaubte Systeme: Allow === 1 und Map === 1
const allowedSystems = useMemo(() => {
return new Set(
(Array.isArray(GisSystemStatic) ? GisSystemStatic : [])
.filter(sys => sys.Allow === 1 && sys.Map === 1)
.map(sys => sys.IdSystem)
);
}, [GisSystemStatic]);
// Uniqe Areas basierend auf Allowed Systems
const areaOptions = useMemo(() => {
const points = GisStationsStaticDistrict?.Points || [];
const seen = new Set();
const filtered = points.filter(p => {
if (!p?.Area_Name) return false;
if (!allowedSystems.has(p.System)) return false;
if (seen.has(p.Area_Name)) return false;
seen.add(p.Area_Name);
return true;
});
return filtered.map(p => ({ label: p.Area_Name, value: p.IdLD }));
}, [GisStationsStaticDistrict, allowedSystems]);
const handleChange = e => {
const selectedIndex = e.target.options.selectedIndex;
const label = e.target.options[selectedIndex].text;
dispatch(setSelectedArea(label));
onClose?.();
};
// Schließe mit ESC
useEffect(() => {
const onKey = e => {
if (e.key === "Escape") onClose?.();
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [onClose]);
return (
<div className="absolute top-16 right-3 w-72 z-50 bg-white rounded-lg shadow-md">
<div className="flex flex-col gap-4 p-4">
<div className="text-sm font-semibold mb-2">Station wählen</div>
<select
onChange={handleChange}
className="border p-2 rounded w-full"
defaultValue="__default__"
>
<option value="__default__" disabled>
Bitte wählen
</option>
{areaOptions.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
</div>
);
};
export default AreaDropdown;

View File

@@ -4,7 +4,7 @@ import React, { useState } from "react";
const CoordinateInput = ({ onCoordinatesSubmit }) => {
const [coordinates, setCoordinates] = useState("");
const handleSubmit = (e) => {
const handleSubmit = e => {
e.preventDefault();
if (onCoordinatesSubmit) {
onCoordinatesSubmit(coordinates);
@@ -12,9 +12,21 @@ const CoordinateInput = ({ onCoordinatesSubmit }) => {
};
return (
<form onSubmit={handleSubmit} className="fixed top-5 left-5 z-50 bg-white shadow-lg rounded-lg p-4 w-72">
<input type="text" placeholder="Koordinaten eingeben (lat,lng)" value={coordinates} onChange={(e) => setCoordinates(e.target.value)} className="border p-2 rounded w-full mb-2" />
<button type="submit" className="bg-blue-500 text-white p-2 rounded w-full hover:bg-blue-600">
<form
onSubmit={handleSubmit}
className="absolute top-16 right-3 z-50 bg-white rounded-lg shadow-md p-4 w-72"
>
<input
type="text"
placeholder="Koordinaten eingeben (lat,lng)"
value={coordinates}
onChange={e => setCoordinates(e.target.value)}
className="border p-2 rounded w-full mb-2"
/>
<button
type="submit"
className="bg-littwin-blue text-white p-2 rounded w-full hover:bg-blue-600"
>
Zu Marker zoomen
</button>
</form>

View File

@@ -31,7 +31,7 @@ const VersionInfoModal = ({ showVersionInfoModal, closeVersionInfoModal, APP_VER
</p>
<button
onClick={closeVersionInfoModal}
className="mt-4 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-700 mx-auto block"
className="mt-4 bg-littwin-blue text-white px-4 py-2 rounded mx-auto block"
>
Schließen
</button>

View File

@@ -1,4 +1,4 @@
// components/uiWidgets/baseMapPanel/BaseMapPanel.js
// 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";

View File

@@ -241,53 +241,42 @@ function MapLayersControlPanel({ handlePolylineCheckboxChange }) {
}, [GisStationsStaticDistrict]);
//---------------------------
// Polyline (Kabelstrecken) abhängig von TALAS (system-1)
const onPolylineToggle = checked => {
if (editMode) return;
// Wenn Nutzer Kabelstrecken einschaltet, aber TALAS aktuell ausgeblendet ist,
// dann TALAS automatisch aktivieren (sofern erlaubt)
const talasKey = "system-1";
const talasVisible = !!mapLayersVisibility[talasKey];
if (checked && isTalasAllowed && !talasVisible) {
dispatch(setLayerVisibility({ layer: talasKey, visibility: true }));
// Persistiere Sichtbarkeit map/user-spezifisch
const mapId2 = localStorage.getItem("currentMapId");
const userId2 = localStorage.getItem("currentUserId");
const mapStorageKey =
mapId2 && userId2 ? `mapLayersVisibility_m${mapId2}_u${userId2}` : "mapLayersVisibility";
localStorage.setItem(
mapStorageKey,
JSON.stringify({ ...mapLayersVisibility, [talasKey]: true })
);
// Event feuern wie an anderer Stelle
setTimeout(() => {
const event = new Event("visibilityChanged");
window.dispatchEvent(event);
}, 0);
}
// Sichtbarkeit der Kabelstrecken setzen
handlePolylineCheckboxChange(checked);
};
//---------------------------
return (
<div
id="mainDataSheet"
className="absolute top-3 right-3 w-1/6 min-w-[300px] max-w-[400px] z-10 bg-white p-2 rounded-lg shadow-lg"
>
<div className="flex flex-col gap-4 p-4">
<div className="flex items-center justify-between space-x-2">
<select
onChange={handleAreaChange}
id="stationListing"
className="border-solid-1 p-2 rounded ml-1 font-semibold"
style={{ minWidth: "150px", maxWidth: "200px" }}
>
<option value="Station wählen">Station wählen</option>
{/*
...new Map(
(GisStationsStaticDistrict.Points || [])
.filter(p => !!p.Area_Name)
.map(p => [p.Area_Name, p])
).values(),
*/}
{[
...new Map(
(GisStationsStaticDistrict.Points || [])
.filter(p => !!p.Area_Name)
.map(p => [p.Area_Name, p])
).values(),
].map(item => (
<option key={item.Area_Name} value={item.IdLD}>
{item.Area_Name}
</option>
))}
</select>
{/*
<div className="flex items-center space-x-2">
<EditModeToggle />
<img
src="/img/expand-icon.svg"
alt="Expand"
className="h-6 w-6 cursor-pointer"
onClick={handleIconClick}
/>
</div>
*/}
</div>
<div className="absolute top-16 right-3 w-72 z-50 bg-white rounded-lg shadow-md">
<div id="mainDataSheet" className="flex flex-col gap-4 p-4">
{/* Checkboxen mit Untermenüs */}
<div className="flex flex-col gap-2">
{systemListing.map(system => (
@@ -312,7 +301,7 @@ function MapLayersControlPanel({ handlePolylineCheckboxChange }) {
<input
type="checkbox"
checked={kabelstreckenVisible}
onChange={e => handlePolylineCheckboxChange(e.target.checked)}
onChange={e => onPolylineToggle(e.target.checked)}
id="polyline-checkbox"
disabled={!isTalasAllowed || editMode}
/>

View File

@@ -22,4 +22,73 @@ Verzeichnisstruktur funktioniert.
---
## OSMbasierte, „open“ Quellen
Diese sind datenrechtlich offen (ODbL bzw. Community-Lizenzen), aber das „kostenlos“ gilt nicht im
Sinne unbegrenzter TileNutzung. Die TileServer werden als CommunityRessource bereitgestellt
bitte Policies respektieren.
- osm-standard (OpenStreetMap)
- - Key: Nein
- - Nutzung: FairUse; für produktive/hohe Last eigenen TileServer/Provider verwenden.
- - Attribution: „© OpenStreetMap contributors“
- osm-humanitarian (HOT)
- - Key: Nein
- - Nutzung: FairUse; für größere Last die Betreiber kontaktieren bzw. andere Infrastruktur nutzen.
- - Attribution: „© OpenStreetMap contributors <br>
-
- Humanitarian OpenStreetMap Team“ cyclosm
- - Key: Nein
- - Nutzung: FairUse (bereitgestellt u. a. über OSM France). Für höhere Last
Unterstützung/Hostingoptionen prüfen.
- - Attribution: „CyclOSM“ + „OpenStreetMap contributors“
- PraxisTipps Kleine bis mittlere Nutzung: Die oben genannten „keyless“ Quellen sind oft
ausreichend, solange du Attribution setzt und Limits respektierst. Produktion/hohe Last: Nutze
einen Anbieter mit Vertrag/Key (z. B. Thunderforest, Tracestrack, MapTiler, Mapbox) oder hoste
Tiles selbst. Schlüssel im Client: Für Thunderforest/Tracestrack stehen die Keys im Frontend. Das
ist üblich, aber der Key ist sichtbar. Wenn du ihn verbergen willst, richte einen kleinen
TileProxy auf deinem Server ein, der den Key serverseitig anhängt und optional cached.
Attribution: Dein BaseMapPanel setzt bereits Attributionsstrings aus config.json. Achte darauf,
dass sie je Quelle korrekt sind.
---
Kurzantwort: Für kommerzielle Nutzung sind OSMCommunityTileServer nicht geeignet. Nutze einen
bezahlten Anbieter (z.B. Thunderforest, Tracestrack) oder hoste selbst. Attribution ist immer
Pflicht.
Links und Hinweise je Layer/Provider:
OpenStreetMap Standard (osm-standard)
Lizenz/Daten: ODbL, Attribution Pflicht Tile-Server-Policy (keine Produktion/hohe Last):
https://operations.osmfoundation.org/policies/tiles/ Urheberrecht/Attribution:
https://www.openstreetmap.org/copyright HOT Humanitarian (osm-humanitarian)
Community-Server (OSM France); keine Produktion/hohe Last Info/Policy OSM France Tiles:
https://tile.openstreetmap.fr/ HOT: https://www.hotosm.org/ CyclOSM (cyclosm)
Community-Server (OSM France); keine Produktion/hohe Last Projektseite: https://www.cyclosm.org/
Hinweise/Policy (OSM France): https://tile.openstreetmap.fr/ Wiki:
https://wiki.openstreetmap.org/wiki/CyclOSM Carto Light (carto-light / Positron)
Keylos nutzbar mit Attribution; FairUse, für hohe Last über CARTOPläne Basemaps:
https://carto.com/basemaps/ Attribution: https://carto.com/attributions Pricing (Plattform):
https://carto.com/pricing/ (bei großem Volumen Sales kontaktieren) Thunderforest (Cycle/Transport u.
a.)
Kommerziell mit APIKey; Pläne von Free bis Pro Pricing: https://www.thunderforest.com/pricing/
Terms/Attribution: https://www.thunderforest.com/terms/ Tracestrack Topo
APIKey erforderlich; kostenlose und bezahlte Pläne Übersicht/Pricing:
https://www.tracestrack.com/en/maps/ Nutzungsbedingungen: https://www.tracestrack.com/en/terms/
Empfehlung:
Produktion: Nimm Thunderforest oder Tracestrack (oder MapTiler: https://www.maptiler.com/pricing/)
oder hoste Tiles selbst. Attribution in der Karte anzeigen (OSM + jeweiliger Anbieter). Soll ich
diese Links samt klarer Hinweise kompakt in eure README.md einpflegen?
---
[Zurück zur Übersicht](../README.md)

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "nodemap",
"version": "1.1.361",
"version": "1.1.382",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nodemap",
"version": "1.1.361",
"version": "1.1.382",
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",

View File

@@ -1,6 +1,6 @@
{
"name": "nodemap",
"version": "1.1.361",
"version": "1.1.382",
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
@@ -45,6 +45,11 @@
"start": "cross-env NODE_ENV=production node server.js",
"export": "next export",
"test": "jest",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:slow": "cross-env PW_HEADED=1 PW_SLOWMO=1000 playwright test",
"test:e2e:slow:ui": "cross-env PW_HEADED=1 PW_SLOWMO=1000 playwright test --ui",
"test:e2e:report": "playwright show-report ./playwright/reports",
"prepare": "husky",
"bump-version": "node ./scripts/bumpVersion.js"
},

35
playwright.config.js Normal file
View File

@@ -0,0 +1,35 @@
// Playwright test configuration for the NodeMap project
// Starts the local Next.js custom server (server.js) and runs tests against http://localhost:3000
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { defineConfig, devices } = require("@playwright/test");
module.exports = defineConfig({
testDir: "./playwright/tests",
timeout: 60_000,
expect: { timeout: 10_000 },
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
// Reporters: keep console-friendly list and generate an HTML report under playwright/reports
reporter: [["list"], ["html", { outputFolder: "playwright/reports", open: "never" }]],
// Store any runner outputs (attachments, logs) under playwright/test-results
outputDir: "playwright/test-results",
use: {
baseURL: "http://localhost:3000",
// Disable artifact generation locally to avoid creating files
trace: "off",
video: "off",
screenshot: "off",
headless: process.env.PW_HEADED ? false : true,
// Apply slow motion to all actions when PW_SLOWMO is set
launchOptions: {
slowMo: process.env.PW_SLOWMO ? Number(process.env.PW_SLOWMO) : 0,
},
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
});

View File

@@ -1,7 +0,0 @@
// example.spec.js
const { test, expect } = require("@playwright/test");
test("simple test", async ({ page }) => {
await page.goto("https://playwright.dev");
await expect(page).toHaveTitle(/Playwright/);
});

View File

@@ -0,0 +1,119 @@
import { test, expect } from "@playwright/test";
// Helper: robust selection for native <select> or custom ARIA comboboxes
async function selectStation(page, value) {
// Try to find by accessible name first
let combo = page.getByRole("combobox", { name: /Station wählen/i });
if (!(await combo.count())) {
// Fallback: find a container with the label text and locate a select inside
const container = page.locator("div").filter({ hasText: "Station wählen" }).last();
const selectInContainer = container.locator("select");
if (await selectInContainer.count()) {
combo = selectInContainer.first();
} else {
// Final fallback: first visible native select (overlay has only one)
combo = page.locator("select:visible").first();
}
}
await expect(combo).toBeVisible();
const isNative = await combo.evaluate(el => el.tagName === "SELECT");
if (isNative) {
await expect(combo).toBeEnabled();
await expect(combo.locator(`option[value="${value}"]`)).toBeAttached();
await combo.selectOption({ value });
} else {
await combo.click();
await page.getByRole("option", { name: new RegExp(value) }).click();
}
}
test("MapComponent", async ({ page }) => {
// Set initial localStorage BEFORE navigation so the app reads them on load
await page.addInitScript(() => {
localStorage.setItem("editMode", "false");
localStorage.setItem("polylineVisible_m12_u484", "true");
localStorage.setItem("currentMapId", "12");
localStorage.setItem("currentUserId", "484");
localStorage.setItem("mapZoom", "13");
localStorage.setItem("kabelstreckenVisible", "false");
localStorage.setItem("showBaseMapPanel", "false");
localStorage.setItem(
"mapLayersVisibility_m12_u484",
JSON.stringify({
"system-1": true,
"system-2": false,
"system-3": false,
"system-5": false,
"system-6": false,
"system-7": false,
"system-8": false,
"system-9": false,
"system-10": false,
"system-11": false,
"system-13": false,
"system-30": false,
"system-100": false,
"system-110": false,
"system-111": false,
"system-200": false,
})
);
localStorage.setItem("mapCenter", JSON.stringify([53.23938294961826, 8.21434020996094]));
localStorage.setItem("markerLink", "undefined");
localStorage.setItem("lastElementType", "marker");
localStorage.setItem("polylineVisible", "false");
localStorage.setItem("showAppInfoCard", "false");
localStorage.setItem("showCoordinateInput", "false");
localStorage.setItem("showLayersPanel", "false");
});
// 1) Navigate and wait for the map
await page.goto("http://localhost:3000/?m=12&u=484");
await page.locator("#map").waitFor({ state: "visible", timeout: 20_000 });
// 2) Optional: verify a key from localStorage at runtime
await expect(page.evaluate(() => localStorage.getItem("showLayersPanel"))).resolves.toBe("false");
// 3) Layer-Panel toggle: expect "einblenden" first (since showLayersPanel=false), then toggle
await expect(page.getByRole("button", { name: "Layer-Panel einblenden" })).toBeVisible();
await page.getByRole("button", { name: "Layer-Panel einblenden" }).click();
await expect(page.getByRole("button", { name: "Layer-Panel ausblenden" })).toBeVisible();
// 4) Collapse again to restore state
await page.getByRole("button", { name: "Layer-Panel ausblenden" }).click();
// 5) Info-Card toggle: start hidden -> show -> hide -> show again
await expect(page.getByRole("button", { name: "Info einblenden" })).toBeVisible();
await page.getByRole("button", { name: "Info einblenden" }).click();
await expect(page.getByRole("button", { name: "Info ausblenden" })).toBeVisible();
await page.getByRole("button", { name: "Info ausblenden" }).click();
await expect(page.getByRole("button", { name: "Info einblenden" })).toBeVisible();
await page.getByRole("button", { name: "Info einblenden" }).click();
await expect(page.locator("div").filter({ hasText: "TALAS.Map Version" }).nth(3)).toBeVisible();
// 6) Koordinatensuche toggle
await page.getByRole("button", { name: "Koordinatensuche einblenden" }).click();
await expect(page.locator("form")).toBeVisible();
await page.getByRole("button", { name: "Koordinatensuche ausblenden" }).click();
// 7) Marker setzen und Stationen wählen
await page.getByLabel("Marker").click();
await expect(page.getByText("Station wählenBitte wählen…")).toBeVisible();
await selectStation(page, "50977");
await page.getByLabel("Marker").click();
await selectStation(page, "50986");
await page.getByLabel("Marker").click();
await page.getByLabel("Marker").click();
await page.getByLabel("Marker").click();
await selectStation(page, "50977");
await page.getByRole("button", { name: "Karte auf Standardansicht" }).click();
//minusIcon
await page.getByTestId("zoom-out").click();
//wait 3 seconds
// plusIcon
await page.getByTestId("zoom-in").click(); //plus
});
/* Powershell Befehl ->das führt langsam aus mit 1 Sekunde Pause zwischen den Aktionen
$env:PW_HEADED=1; $env:PW_SLOWMO=1000; npx playwright test
*/

View File

@@ -5,22 +5,7 @@
"zoomOutCenter: Zielkoordinaten für Herauszoomen (lat, lng)",
"minZoom/maxZoom: erlaubte Zoomstufen pro Quelle"
],
"tileSources": {
"local": {
"url": "http://localhost/talas5/TileMap/mapTiles/{z}/{x}/{y}.png",
"_comment": "Offline-Kartenquelle (lokal)",
"minZoom": 5,
"maxZoom": 15
},
"osm": {
"url": "/tiles/{z}/{x}/{y}.png",
"_comment": "OpenStreetMap Online-Kartenquelle über Server-Proxy (relativ)",
"minZoom": 0,
"maxZoom": 19
}
},
"active": "osm",
"_comment_active": "Aktive Kartenquelle: 'local' oder 'osm'",
"center": [53.111111, 8.4625],
"_comment_center": "Startmittelpunkt der Karte (lat, lng)",
@@ -29,6 +14,9 @@
"basePath": "/talas5",
"_comment_basePath": "Basis-URL für API und Routing",
"minZoom": 5,
"maxZoom": 20,
"_comment_zoom": "Globale Zoom-Grenzen (min/max). Kann durch tileSources überschrieben werden.",
"debugLog": false,
"_comment_debugLog": "Debug-Logging für Client "
}

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
</svg>

Before

Width:  |  Height:  |  Size: 349 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
</svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 21l-3-3m-12.728-.364A9 9 0 015.636 5.636m0 0L3 3l3 3m9.364 9.364L18 21M5.636 5.636L3 3" />
</svg>

After

Width:  |  Height:  |  Size: 351 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
</svg>

After

Width:  |  Height:  |  Size: 432 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="rgb(0, 174, 239)" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25s-7.5-4.108-7.5-11.25a7.5 7.5 0 1115 0z" />
</svg>

After

Width:  |  Height:  |  Size: 373 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>

After

Width:  |  Height:  |  Size: 231 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="jest tests" tests="0" failures="0" errors="0" time="4.822">
</testsuites>

View File

@@ -1,6 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}", "./hooks/**/*.{js,ts,jsx,tsx}"],
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
"./hooks/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
zIndex: {
@@ -10,6 +14,9 @@ module.exports = {
90: "90",
100: "100",
},
colors: {
"littwin-blue": "#00aeef",
},
},
},
plugins: [],

View File

@@ -1,4 +0,0 @@
{
"status": "passed",
"failedTests": []
}

View File

@@ -67,7 +67,7 @@ export const initializeMap = (
let mapCenter = [53.111111, 8.4625];
let mapZoom = 12;
let minZoom = 5;
let maxZoom = 15;
let maxZoom = 20;
try {
if (window && window.__leafletConfig) {
config = window.__leafletConfig;
@@ -125,6 +125,7 @@ export const initializeMap = (
zoom: mapZoom,
minZoom: minZoom,
maxZoom: maxZoom,
// Disable default position; we'll add our own control at bottom-right
zoomControl: false,
dragging: true,
contextmenu: true,
@@ -142,6 +143,8 @@ export const initializeMap = (
initMap.dragging.enable();
// Do not add the default Leaflet zoom control; we'll render custom React controls in MapComponent
L.tileLayer(tileLayerUrl, {
attribution: "&copy; OpenStreetMap contributors",
tileSize: 256,

View File

@@ -15,7 +15,7 @@ export const zoomIn = (e, map) => {
return;
}
let maxZoom = 19;
let maxZoom = 20;
try {
if (window && window.__tileSourceMaxZoom !== undefined) {
maxZoom = window.__tileSourceMaxZoom;

View File

@@ -6,7 +6,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 01 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -15,7 +16,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 31 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -24,7 +26,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 17 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -33,7 +36,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 05 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50984,
@@ -42,7 +46,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 20 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -51,7 +56,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 32 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50984,
@@ -60,7 +66,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 01 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50977,
@@ -69,7 +76,8 @@
"Co": "#FF00FF",
"Me": "Station offline",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50977,
@@ -78,7 +86,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 01 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -87,7 +96,8 @@
"Co": "#FF00FF",
"Me": "Station offline",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50066,
@@ -96,7 +106,8 @@
"Co": "#FF00FF",
"Me": "CPL offline",
"Feld": 5,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50011,
@@ -105,7 +116,8 @@
"Co": "#FF00FF",
"Me": "CPL offline",
"Feld": 16,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50011,
@@ -114,7 +126,8 @@
"Co": "#FF00FF",
"Me": "Wasserdruck aus",
"Feld": 16,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50011,
@@ -123,7 +136,8 @@
"Co": "#FF00FF",
"Me": "Ein",
"Feld": 16,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50011,
@@ -132,7 +146,8 @@
"Co": "#FF00FF",
"Me": "Digitaleingang 1 ON",
"Feld": 16,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50000,
@@ -141,7 +156,8 @@
"Co": "#FF00FF",
"Me": "Ein",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -150,7 +166,8 @@
"Co": "#FF00FF",
"Me": "Station offline",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -159,7 +176,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 32 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -168,7 +186,8 @@
"Co": "#FFFF00",
"Me": "KÜG 07: Überspannung kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -177,7 +196,8 @@
"Co": "#FFFF00",
"Me": "KÜG 08: Überspannung gehend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50977,
@@ -186,7 +206,8 @@
"Co": "#FFFF00",
"Me": "KÜG 08: Überspannung gehend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50984,
@@ -195,7 +216,8 @@
"Co": "#FFFF00",
"Me": "KÜG 08: Überspannung gehend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -204,7 +226,8 @@
"Co": "#FFFF00",
"Me": "Eingang DE 02 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -213,7 +236,8 @@
"Co": "#FFFF00",
"Me": "KÜG 08: Überspannung gehend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -222,7 +246,8 @@
"Co": "#FF9900",
"Me": "Eingang DE 03 Test Karte kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -231,7 +256,8 @@
"Co": "#FF0000",
"Me": "KÜG 01: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -240,7 +266,8 @@
"Co": "#FF0000",
"Me": "KÜG 02: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -249,7 +276,8 @@
"Co": "#FF0000",
"Me": "KÜG 03: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50000,
@@ -258,7 +286,8 @@
"Co": "#FF0000",
"Me": "über 8V kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50984,
@@ -267,7 +296,8 @@
"Co": "#FF0000",
"Me": "KÜG 05: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50984,
@@ -276,7 +306,8 @@
"Co": "#FF0000",
"Me": "KÜG 02: Isolationsminderung kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50984,
@@ -285,7 +316,8 @@
"Co": "#FF0000",
"Me": "KÜG 06: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50977,
@@ -294,7 +326,8 @@
"Co": "#FF0000",
"Me": "KÜG 01: Isolationsminderung kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50977,
@@ -303,7 +336,8 @@
"Co": "#FF0000",
"Me": "KÜG 06: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50977,
@@ -312,7 +346,8 @@
"Co": "#FF0000",
"Me": "KÜG 05: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50976,
@@ -321,7 +356,8 @@
"Co": "#FF0000",
"Me": "CPL offline",
"Feld": 3,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50976,
@@ -330,7 +366,8 @@
"Co": "#FF0000",
"Me": "KÜG 03: Isolationsminderung kommend",
"Feld": 3,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -339,7 +376,8 @@
"Co": "#FF0000",
"Me": "KÜG 04: Isolationsminderung kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -348,7 +386,8 @@
"Co": "#FF0000",
"Me": "KÜG 02: Isolationsminderung kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -357,7 +396,8 @@
"Co": "#FF0000",
"Me": "KÜG 01: Isolationsminderung kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50001,
@@ -366,7 +406,8 @@
"Co": "#FF0000",
"Me": "Sammelstörung kommend",
"Feld": 5,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -375,7 +416,8 @@
"Co": "#FF0000",
"Me": "KÜG 06: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -384,7 +426,8 @@
"Co": "#FF0000",
"Me": "KÜG 05: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50963,
@@ -393,7 +436,8 @@
"Co": "#FF0000",
"Me": "CPL offline",
"Feld": 3,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50063,
@@ -402,7 +446,8 @@
"Co": "#FF0000",
"Me": "Digitaleingang 1 EIN",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50000,
@@ -411,6 +456,7 @@
"Co": "#FF0000",
"Me": "über 10V kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
}
]