164 lines
4.9 KiB
JavaScript
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>
|
|
);
|
|
}
|