diff --git a/.env.development b/.env.development
index 7f83d8928..f805fb272 100644
--- a/.env.development
+++ b/.env.development
@@ -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.360
+NEXT_PUBLIC_APP_VERSION=1.1.361
diff --git a/.env.production b/.env.production
index 269a7b105..29b7f4646 100644
--- a/.env.production
+++ b/.env.production
@@ -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.360
+NEXT_PUBLIC_APP_VERSION=1.1.361
diff --git a/components/mainComponent/MapComponent.js b/components/mainComponent/MapComponent.js
index e3fba6abb..3ab5e9a94 100644
--- a/components/mainComponent/MapComponent.js
+++ b/components/mainComponent/MapComponent.js
@@ -24,6 +24,7 @@ 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";
//----------Daten aus API--------------------
@@ -171,6 +172,24 @@ 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;
+ }
+ });
+ // Sichtbarkeit der Koordinaten-Suche (Lupe)
+ const [showCoordinateInput, setShowCoordinateInput] = useState(() => {
+ try {
+ const v = localStorage.getItem("showCoordinateInput");
+ return v === null ? false : v === "true";
+ } catch (_) {
+ return false;
+ }
+ });
// Flag, ob Nutzer die Polyline-Checkbox manuell betätigt hat
// Nutzer-Flag global auf window, damit auch Redux darauf zugreifen kann
@@ -249,6 +268,18 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
localStorage.setItem("showLayersPanel", String(showLayersPanel));
} catch (_) {}
}, [showLayersPanel]);
+ // Persistiere Sichtbarkeit des Base-Map Panels
+ useEffect(() => {
+ try {
+ localStorage.setItem("showBaseMapPanel", String(showBaseMapPanel));
+ } catch (_) {}
+ }, [showBaseMapPanel]);
+ // Persistiere Sichtbarkeit der Koordinaten-Suche
+ useEffect(() => {
+ try {
+ localStorage.setItem("showCoordinateInput", String(showCoordinateInput));
+ } catch (_) {}
+ }, [showCoordinateInput]);
//--------------------------------------------
@@ -1108,38 +1139,10 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
/>
)}
-
+ {showCoordinateInput && }
- {/* Top-right controls: info toggle + hamburger (layers) */}
+ {/* Top-right controls: layers, info, expand, edit, and base map stack */}
-
-
-
+
+
+
+ {/* Lupe: Koordinaten-Suche ein-/ausblenden */}
+
+ {/* Marker-Icon (line-md) */}
+
+ {/* Alarm-Icon (mdi) */}
+
+
+ {showBaseMapPanel && map && (
+ setShowBaseMapPanel(false)} />
+ )}
{showAppInfoCard && (
diff --git a/components/uiWidgets/baseMapPanel/BaseMapPanel.js b/components/uiWidgets/baseMapPanel/BaseMapPanel.js
new file mode 100644
index 000000000..ddffdbf72
--- /dev/null
+++ b/components/uiWidgets/baseMapPanel/BaseMapPanel.js
@@ -0,0 +1,163 @@
+// components/uiWidgets/baseMapPanel/BaseMapPanel.js
+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 (
+
+
+
Map Layers
+
+
+
+ {bases.map(b => (
+
+ ))}
+
+
+ );
+}
diff --git a/package-lock.json b/package-lock.json
index 1b92df5a5..8c45b8daf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "nodemap",
- "version": "1.1.360",
+ "version": "1.1.361",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nodemap",
- "version": "1.1.360",
+ "version": "1.1.361",
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
diff --git a/package.json b/package.json
index 9aa88fd8d..1788d4817 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "nodemap",
- "version": "1.1.360",
+ "version": "1.1.361",
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",