feat: Icons
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user