feat: Icons
This commit is contained in:
@@ -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
|
# 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
|
# basePath wird jetzt in public/config.json gepflegt
|
||||||
# App-Versionsnummer
|
# App-Versionsnummer
|
||||||
NEXT_PUBLIC_APP_VERSION=1.1.360
|
NEXT_PUBLIC_APP_VERSION=1.1.361
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ NEXT_PUBLIC_USE_MOCKS=false
|
|||||||
# basePath wird jetzt in public/config.json gepflegt
|
# basePath wird jetzt in public/config.json gepflegt
|
||||||
|
|
||||||
# App-Versionsnummer
|
# App-Versionsnummer
|
||||||
NEXT_PUBLIC_APP_VERSION=1.1.360
|
NEXT_PUBLIC_APP_VERSION=1.1.361
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ 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 BaseMapPanel from "@/components/uiWidgets/baseMapPanel/BaseMapPanel.js";
|
||||||
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";
|
||||||
//----------Daten aus API--------------------
|
//----------Daten aus API--------------------
|
||||||
@@ -171,6 +172,24 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
return true;
|
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
|
// Flag, ob Nutzer die Polyline-Checkbox manuell betätigt hat
|
||||||
// Nutzer-Flag global auf window, damit auch Redux darauf zugreifen kann
|
// 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));
|
localStorage.setItem("showLayersPanel", String(showLayersPanel));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}, [showLayersPanel]);
|
}, [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 }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<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: info toggle + hamburger (layers) */}
|
{/* 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">
|
<div className="absolute top-3 right-3 z-50 pointer-events-auto flex items-center gap-2">
|
||||||
<button
|
|
||||||
onClick={() => setShowLayersPanel(v => !v)}
|
|
||||||
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" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowAppInfoCard(v => !v)}
|
|
||||||
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"
|
|
||||||
title={showAppInfoCard ? "Info ausblenden" : "Info einblenden"}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleExpandClick}
|
|
||||||
aria-label="Karte auf Standardansicht"
|
|
||||||
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" />
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={toggleEditMode}
|
onClick={toggleEditMode}
|
||||||
aria-label={editMode ? "Bearbeitungsmodus deaktivieren" : "Bearbeitungsmodus aktivieren"}
|
aria-label={editMode ? "Bearbeitungsmodus deaktivieren" : "Bearbeitungsmodus aktivieren"}
|
||||||
@@ -1162,7 +1165,79 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
|
|||||||
className="h-8 w-8 text-blue-900"
|
className="h-8 w-8 text-blue-900"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleExpandClick}
|
||||||
|
aria-label="Karte auf Standardansicht"
|
||||||
|
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" />
|
||||||
|
</button>
|
||||||
|
<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)}
|
||||||
|
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" />
|
||||||
|
</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)}
|
||||||
|
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"
|
||||||
|
title={showAppInfoCard ? "Info ausblenden" : "Info einblenden"}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{showBaseMapPanel && map && (
|
||||||
|
<BaseMapPanel map={map} onClose={() => setShowBaseMapPanel(false)} />
|
||||||
|
)}
|
||||||
<CoordinatePopup isOpen={isPopupOpen} coordinates={currentCoordinates} onClose={closePopup} />
|
<CoordinatePopup isOpen={isPopupOpen} coordinates={currentCoordinates} onClose={closePopup} />
|
||||||
|
|
||||||
{showAppInfoCard && (
|
{showAppInfoCard && (
|
||||||
|
|||||||
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
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "nodemap",
|
"name": "nodemap",
|
||||||
"version": "1.1.360",
|
"version": "1.1.361",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "nodemap",
|
"name": "nodemap",
|
||||||
"version": "1.1.360",
|
"version": "1.1.361",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.13.3",
|
||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nodemap",
|
"name": "nodemap",
|
||||||
"version": "1.1.360",
|
"version": "1.1.361",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.13.3",
|
||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user