Files
2025-09-16 11:47:04 +02:00

164 lines
4.9 KiB
JavaScript

// 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>
);
}