260 lines
6.3 KiB
JavaScript
260 lines
6.3 KiB
JavaScript
"use client";
|
|
|
|
import {
|
|
MapContainer,
|
|
TileLayer,
|
|
Marker,
|
|
Popup,
|
|
LayersControl,
|
|
useMapEvents,
|
|
useMap,
|
|
} from "react-leaflet";
|
|
import L from "leaflet";
|
|
import "leaflet/dist/leaflet.css";
|
|
import { useEffect } from "react";
|
|
import { mapLayers } from "./mapLayers";
|
|
|
|
// Custom WMS Layer component using Leaflet's native WMS support
|
|
function WMSLayer({ url, params, opacity = 1, attribution }) {
|
|
const map = useMap();
|
|
|
|
useEffect(() => {
|
|
if (!map || !url) return;
|
|
|
|
// Clean up params for L.tileLayer.wms
|
|
const wmsOptions = {
|
|
layers: params.layers,
|
|
styles: params.styles || '',
|
|
format: params.format || 'image/png',
|
|
transparent: params.transparent !== false,
|
|
version: params.version || '1.1.1',
|
|
attribution: attribution,
|
|
opacity: opacity
|
|
};
|
|
|
|
// Add CRS/SRS parameter based on version
|
|
const version = parseFloat(params.version || '1.1.1');
|
|
if (version >= 1.3) {
|
|
wmsOptions.crs = L.CRS.EPSG3857;
|
|
} else {
|
|
wmsOptions.srs = 'EPSG:3857';
|
|
}
|
|
|
|
// Create WMS layer using Leaflet's native support
|
|
const wmsLayer = L.tileLayer.wms(url, wmsOptions);
|
|
|
|
wmsLayer.addTo(map);
|
|
|
|
return () => {
|
|
map.removeLayer(wmsLayer);
|
|
};
|
|
}, [map, url, params, opacity, attribution]);
|
|
|
|
return null;
|
|
}
|
|
|
|
// Fix for default markers in react-leaflet
|
|
const fixLeafletIcons = () => {
|
|
if (typeof window !== "undefined") {
|
|
delete L.Icon.Default.prototype._getIconUrl;
|
|
L.Icon.Default.mergeOptions({
|
|
iconRetinaUrl: "/leaflet/marker-icon-2x.png",
|
|
iconUrl: "/leaflet/marker-icon.png",
|
|
shadowUrl: "/leaflet/marker-shadow.png",
|
|
});
|
|
}
|
|
};
|
|
|
|
// Create colored marker icons
|
|
const createColoredMarkerIcon = (color) => {
|
|
if (typeof window !== "undefined") {
|
|
return new L.Icon({
|
|
iconUrl: `data:image/svg+xml;base64,${btoa(`
|
|
<svg width="25" height="41" viewBox="0 0 25 41" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M12.5 0C5.596 0 0 5.596 0 12.5c0 12.5 12.5 28.5 12.5 28.5S25 25 25 12.5C25 5.596 19.404 0 12.5 0z" fill="${color}" stroke="#ffffff" stroke-width="2"/>
|
|
<circle cx="12.5" cy="12.5" r="6" fill="#ffffff"/>
|
|
</svg>
|
|
`)}`,
|
|
shadowUrl: "/leaflet/marker-shadow.png",
|
|
iconSize: [25, 41],
|
|
iconAnchor: [12, 41],
|
|
popupAnchor: [1, -34],
|
|
shadowSize: [41, 41],
|
|
});
|
|
} return null;
|
|
};
|
|
|
|
// Component to handle map events
|
|
function MapEventHandler({ onViewChange }) {
|
|
const map = useMapEvents({
|
|
moveend: () => {
|
|
if (onViewChange) {
|
|
const center = map.getCenter();
|
|
const zoom = map.getZoom();
|
|
onViewChange([center.lat, center.lng], zoom);
|
|
}
|
|
},
|
|
zoomend: () => {
|
|
if (onViewChange) {
|
|
const center = map.getCenter();
|
|
const zoom = map.getZoom();
|
|
onViewChange([center.lat, center.lng], zoom);
|
|
}
|
|
},
|
|
});
|
|
|
|
return null;
|
|
}
|
|
|
|
// Custom zoom control component that handles external events
|
|
function CustomZoomHandler() {
|
|
const map = useMap();
|
|
|
|
useEffect(() => {
|
|
if (!map) return;
|
|
|
|
const handleZoomIn = () => {
|
|
map.zoomIn();
|
|
};
|
|
|
|
const handleZoomOut = () => {
|
|
map.zoomOut();
|
|
};
|
|
|
|
// Listen for custom zoom events
|
|
window.addEventListener('mapZoomIn', handleZoomIn);
|
|
window.addEventListener('mapZoomOut', handleZoomOut);
|
|
|
|
return () => {
|
|
window.removeEventListener('mapZoomIn', handleZoomIn);
|
|
window.removeEventListener('mapZoomOut', handleZoomOut);
|
|
};
|
|
}, [map]);
|
|
|
|
return null;
|
|
}
|
|
|
|
export default function EnhancedLeafletMap({
|
|
center,
|
|
zoom = 13,
|
|
markers = [],
|
|
showLayerControl = true,
|
|
defaultLayer = "OpenStreetMap",
|
|
activeOverlays = [],
|
|
onViewChange,
|
|
}) {
|
|
useEffect(() => {
|
|
fixLeafletIcons();
|
|
}, []); const { BaseLayer, Overlay } = LayersControl;
|
|
return (
|
|
<MapContainer
|
|
center={center}
|
|
zoom={zoom}
|
|
style={{ height: "100%", width: "100%" }}
|
|
scrollWheelZoom={true}
|
|
zoomControl={false}
|
|
>
|
|
<CustomZoomHandler />
|
|
{onViewChange && <MapEventHandler onViewChange={onViewChange} />}
|
|
|
|
{showLayerControl ? (
|
|
<LayersControl position="topright">
|
|
{/* Base Layers */}
|
|
{mapLayers.base.map((layer, index) => (
|
|
<BaseLayer
|
|
key={index}
|
|
checked={layer.checked || layer.name === defaultLayer}
|
|
name={layer.name}
|
|
>
|
|
<TileLayer
|
|
attribution={layer.attribution}
|
|
url={layer.url}
|
|
maxZoom={layer.maxZoom}
|
|
tileSize={layer.tileSize || 256}
|
|
/>
|
|
</BaseLayer>
|
|
))}
|
|
{/* Overlay Layers */}
|
|
{mapLayers.overlays && mapLayers.overlays.map((layer, index) => (
|
|
<Overlay
|
|
key={`overlay-${index}`}
|
|
checked={layer.checked}
|
|
name={layer.name}
|
|
>
|
|
{layer.type === "wms" ? (
|
|
<WMSLayer
|
|
attribution={layer.attribution}
|
|
url={layer.url}
|
|
params={layer.params}
|
|
opacity={layer.opacity}
|
|
/>
|
|
) : (
|
|
<TileLayer
|
|
attribution={layer.attribution}
|
|
url={layer.url}
|
|
maxZoom={layer.maxZoom}
|
|
opacity={layer.opacity}
|
|
/>
|
|
)}
|
|
</Overlay>
|
|
))}
|
|
</LayersControl> ) : (
|
|
// Custom layer rendering when no layer control
|
|
<>
|
|
{/* Base Layer */}
|
|
{(() => {
|
|
const baseLayer = mapLayers.base.find(layer => layer.name === defaultLayer) || mapLayers.base[0];
|
|
return (
|
|
<TileLayer
|
|
attribution={baseLayer.attribution}
|
|
url={baseLayer.url}
|
|
maxZoom={baseLayer.maxZoom}
|
|
tileSize={baseLayer.tileSize || 256}
|
|
/>
|
|
);
|
|
})()}
|
|
|
|
{/* Active Overlay Layers */}
|
|
{mapLayers.overlays && mapLayers.overlays
|
|
.filter(layer => activeOverlays.includes(layer.name))
|
|
.map((layer, index) => {
|
|
if (layer.type === "wms") {
|
|
return (
|
|
<WMSLayer
|
|
key={`custom-overlay-${index}`}
|
|
attribution={layer.attribution}
|
|
url={layer.url}
|
|
params={layer.params}
|
|
opacity={layer.opacity}
|
|
/>
|
|
);
|
|
} else {
|
|
return (
|
|
<TileLayer
|
|
key={`custom-overlay-${index}`}
|
|
attribution={layer.attribution}
|
|
url={layer.url}
|
|
maxZoom={layer.maxZoom}
|
|
opacity={layer.opacity}
|
|
/>
|
|
);
|
|
}
|
|
})
|
|
}
|
|
</>
|
|
)}{" "}
|
|
{markers.map((marker, index) => (
|
|
<Marker
|
|
key={index}
|
|
position={marker.position}
|
|
icon={
|
|
marker.color ? createColoredMarkerIcon(marker.color) : undefined
|
|
}
|
|
>
|
|
{marker.popup && <Popup>{marker.popup}</Popup>}
|
|
</Marker>
|
|
))}
|
|
</MapContainer>
|
|
);
|
|
}
|