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",