feat: Implement layer control and custom zoom functionality in ProjectsMapPage and LeafletMap components

This commit is contained in:
2025-06-24 12:01:48 +02:00
parent 96da5212d4
commit 4c11801ab1
2 changed files with 445 additions and 176 deletions

View File

@@ -5,6 +5,7 @@ import Link from "next/link";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useSearchParams, useRouter } from "next/navigation"; import { useSearchParams, useRouter } from "next/navigation";
import Button from "@/components/ui/Button"; import Button from "@/components/ui/Button";
import { mapLayers } from "@/components/ui/mapLayers";
// Dynamically import the map component to avoid SSR issues // Dynamically import the map component to avoid SSR issues
const DynamicMap = dynamic(() => import("@/components/ui/LeafletMap"), { const DynamicMap = dynamic(() => import("@/components/ui/LeafletMap"), {
@@ -29,6 +30,9 @@ export default function ProjectsMapPage() {
in_progress_construction: true, in_progress_construction: true,
fulfilled: true, fulfilled: true,
}); });
const [activeBaseLayer, setActiveBaseLayer] = useState("Polish Geoportal Orthophoto");
const [activeOverlays, setActiveOverlays] = useState([]);
const [showLayerPanel, setShowLayerPanel] = useState(true);
// Status configuration with colors and labels // Status configuration with colors and labels
const statusConfig = { const statusConfig = {
@@ -68,6 +72,7 @@ export default function ProjectsMapPage() {
); );
setStatusFilters(newState); setStatusFilters(newState);
}; };
// Toggle status filter // Toggle status filter
const toggleStatusFilter = (status) => { const toggleStatusFilter = (status) => {
setStatusFilters((prev) => ({ setStatusFilters((prev) => ({
@@ -75,6 +80,26 @@ export default function ProjectsMapPage() {
[status]: !prev[status], [status]: !prev[status],
})); }));
}; };
// Layer control functions
const handleBaseLayerChange = (layerName) => {
setActiveBaseLayer(layerName);
};
const toggleOverlay = (layerName) => {
setActiveOverlays((prev) => {
if (prev.includes(layerName)) {
return prev.filter((name) => name !== layerName);
} else {
return [...prev, layerName];
}
});
};
const toggleLayerPanel = () => {
setShowLayerPanel(!showLayerPanel);
};
// Update URL with current map state (debounced to avoid too many updates) // Update URL with current map state (debounced to avoid too many updates)
const updateURL = (center, zoom) => { const updateURL = (center, zoom) => {
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -96,7 +121,9 @@ export default function ProjectsMapPage() {
window.mapUpdateTimeout = setTimeout(() => { window.mapUpdateTimeout = setTimeout(() => {
updateURL(center, zoom); updateURL(center, zoom);
}, 500); // Wait 500ms after the last move to update URL }, 500); // Wait 500ms after the last move to update URL
};// Hide navigation and ensure full-screen layout };
// Hide navigation and ensure full-screen layout
useEffect(() => { useEffect(() => {
// Check for URL parameters for coordinates and zoom // Check for URL parameters for coordinates and zoom
const lat = searchParams.get('lat'); const lat = searchParams.get('lat');
@@ -127,6 +154,7 @@ export default function ProjectsMapPage() {
// Prevent scrolling on body // Prevent scrolling on body
document.body.style.overflow = "hidden"; document.body.style.overflow = "hidden";
document.documentElement.style.overflow = "hidden"; document.documentElement.style.overflow = "hidden";
// Cleanup when leaving page // Cleanup when leaving page
return () => { return () => {
if (nav) { if (nav) {
@@ -141,6 +169,7 @@ export default function ProjectsMapPage() {
} }
}; };
}, [searchParams]); }, [searchParams]);
useEffect(() => { useEffect(() => {
fetch("/api/projects") fetch("/api/projects")
.then((res) => res.json()) .then((res) => res.json())
@@ -182,6 +211,7 @@ export default function ProjectsMapPage() {
setLoading(false); setLoading(false);
}); });
}, [searchParams]); }, [searchParams]);
// Convert projects to map markers with filtering // Convert projects to map markers with filtering
const markers = projects const markers = projects
.filter((project) => project.coordinates) .filter((project) => project.coordinates)
@@ -258,7 +288,7 @@ export default function ProjectsMapPage() {
{project.plot} {project.plot}
</div> </div>
)} )}
</div>{" "} </div>
{project.project_status && ( {project.project_status && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-medium text-gray-700">Status:</span> <span className="font-medium text-gray-700">Status:</span>
@@ -303,6 +333,7 @@ export default function ProjectsMapPage() {
}; };
}) })
.filter((marker) => marker !== null); .filter((marker) => marker !== null);
if (loading) { if (loading) {
return ( return (
<div className="fixed inset-0 bg-gray-50 flex items-center justify-center"> <div className="fixed inset-0 bg-gray-50 flex items-center justify-center">
@@ -317,99 +348,54 @@ export default function ProjectsMapPage() {
); );
} }
return ( return (
<div className="fixed inset-0 bg-gray-50 overflow-hidden"> <div
{" "} className="fixed inset-0 bg-gray-50 overflow-hidden"
{/* Floating Header with Controls */} >
<div className="absolute top-4 left-4 right-4 z-[1000] flex items-center justify-between"> {/* Floating Header - Left Side */}
<div className="flex gap-3"> <div className="absolute top-4 left-4 z-[1000]">
<div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg px-4 py-3 border border-gray-200"> {/* Title Box */}
<div className="flex items-center gap-3"> <div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg px-4 py-3 border border-gray-200">
<h1 className="text-lg font-semibold text-gray-900"> <div className="flex items-center gap-3">
Projects Map <h1 className="text-lg font-semibold text-gray-900">
</h1> Projects Map
<div className="text-sm text-gray-600"> </h1>
{markers.length} of {projects.length} projects with coordinates <div className="text-sm text-gray-600">
</div> {markers.length} of {projects.length} projects with coordinates
</div> </div>
</div> </div> </div>
{/* Status Filter Panel */} </div>
<div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg px-4 py-3 border border-gray-200">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-700 mr-2">
Filters:
</span>
{/* Toggle All Button */} {/* Zoom Controls - Below Title */}
<button <div className="absolute top-20 left-4 z-[1000]">
onClick={toggleAllFilters} <div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg border border-gray-200 flex flex-col">
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-gray-100 hover:bg-gray-200 transition-colors duration-200 mr-2" <button
title="Toggle all filters" className="px-3 py-2 hover:bg-gray-50 transition-colors duration-200 border-b border-gray-200 text-gray-700 font-medium text-lg"
> onClick={() => {
<svg // This will be handled by the map component
className="w-3 h-3" const event = new CustomEvent('mapZoomIn');
fill="none" window.dispatchEvent(event);
stroke="currentColor" }}
viewBox="0 0 24 24" title="Zoom In"
> >
<path +
strokeLinecap="round" </button>
strokeLinejoin="round" <button
strokeWidth={2} className="px-3 py-2 hover:bg-gray-50 transition-colors duration-200 text-gray-700 font-medium text-lg"
d="M4 6h16M4 12h16M4 18h16" onClick={() => {
/> // This will be handled by the map component
</svg> const event = new CustomEvent('mapZoomOut');
<span className="text-gray-600"> window.dispatchEvent(event);
{Object.values(statusFilters).every((v) => v) }}
? "Hide All" title="Zoom Out"
: "Show All"} >
</span>
</button> </button>
{/* Individual Status Filters */}
{Object.entries(statusConfig).map(([status, config]) => {
const isActive = statusFilters[status];
const projectCount = projects.filter(
(p) => p.project_status === status && p.coordinates
).length;
return (
<button
key={status}
onClick={() => toggleStatusFilter(status)}
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-all duration-200 hover:bg-gray-100 ${
isActive ? "opacity-100 scale-100" : "opacity-40 scale-95"
}`}
title={`Toggle ${config.label} (${projectCount} projects)`}
>
<div
className={`w-3 h-3 rounded-full border-2 transition-all duration-200 ${
isActive ? "border-white shadow-sm" : "border-gray-300"
}`}
style={{
backgroundColor: isActive ? config.color : "#e5e7eb",
}}
></div>
<span
className={`transition-colors duration-200 ${
isActive ? "text-gray-700" : "text-gray-400"
}`}
>
{config.shortLabel}
</span>
<span
className={`ml-1 text-xs transition-colors duration-200 ${
isActive ? "text-gray-500" : "text-gray-300"
}`}
>
({projectCount})
</span>
</button>
);
})}
</div>
</div>
</div> </div>
</div>
{/* Layer Control Panel - Right Side */}
<div className="absolute top-4 right-4 z-[1000] flex flex-col gap-3">
{/* Action Buttons */}
<div className="flex gap-2"> <div className="flex gap-2">
<Link href="/projects"> <Link href="/projects">
<Button <Button
@@ -452,80 +438,292 @@ export default function ProjectsMapPage() {
</Button> </Button>
</Link> </Link>
</div> </div>
</div>{" "}
{/* Stats Panel - Bottom Left */}
{markers.length > 0 && (
<div className="absolute bottom-4 left-4 z-[1000] bg-white/95 backdrop-blur-sm rounded-lg shadow-lg px-4 py-3 border border-gray-200 max-w-xs">
<div className="text-sm text-gray-600">
<div className="flex items-center gap-2 mb-2">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<span className="font-medium">
{markers.length} of{" "}
{projects.filter((p) => p.coordinates).length} projects shown
</span>
</div>
{/* Status breakdown */} {/* Layer Control Panel */}
<div className="space-y-1 mb-2"> <div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg border border-gray-200 layer-panel-container">
{Object.entries(statusConfig).map(([status, config]) => { {/* Layer Control Header */}
const totalCount = projects.filter( <div className="px-4 py-3 border-b border-gray-200">
(p) => p.project_status === status && p.coordinates <button
).length; onClick={toggleLayerPanel}
const visibleCount = markers.filter((m) => { className="flex items-center justify-between w-full text-left layer-toggle-button"
const project = projects.find( title="Toggle Layer Controls"
(p) => >
p.coordinates && <div className="flex items-center gap-2">
p.coordinates <svg
.split(",") className="w-4 h-4 text-gray-600"
.map((c) => parseFloat(c.trim())) fill="none"
.toString() === m.position.toString() stroke="currentColor"
); viewBox="0 0 24 24"
return project && project.project_status === status; >
}).length; <path
strokeLinecap="round"
if (totalCount === 0) return null; strokeLinejoin="round"
strokeWidth={2}
return ( d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
<div key={status} className="flex items-center gap-2 text-xs"> />
<div </svg>
className="w-2 h-2 rounded-full" <span className="text-sm font-medium text-gray-700">
style={{ backgroundColor: config.color }} Map Layers
></div> </span>
<span <span className="text-xs text-gray-500 bg-gray-100 px-2 py-0.5 rounded-full">
className={ {1 + activeOverlays.length} active
statusFilters[status]
? "text-gray-600"
: "text-gray-400"
}
>
{config.shortLabel}:{" "}
{statusFilters[status] ? visibleCount : 0}/{totalCount}
</span>
</div>
);
})}
</div>
{projects.length > projects.filter((p) => p.coordinates).length && (
<div className="flex items-center gap-2 pt-2 border-t border-gray-200">
<div className="w-2 h-2 bg-amber-500 rounded-full"></div>
<span className="text-amber-600 text-xs">
{projects.length -
projects.filter((p) => p.coordinates).length}{" "}
missing coordinates
</span> </span>
</div> </div>
)} <svg
className={`w-4 h-4 text-gray-400 transition-transform duration-200 ${
showLayerPanel ? "rotate-180" : ""
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
</div> {/* Layer Control Content */}
<div className={`transition-all duration-300 ease-in-out ${
showLayerPanel ? "max-h-[70vh] opacity-100 overflow-visible" : "max-h-0 opacity-0 overflow-hidden"
}`}>
<div className="p-4 min-w-80 max-w-96 max-h-[60vh] overflow-y-auto">
{/* Base Layers Section */}
<div className="mb-4">
<h3 className="text-sm font-semibold text-gray-900 mb-3 flex items-center gap-2">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
Base Maps
</h3>
<div className="space-y-2">
{mapLayers.base.map((layer, index) => (
<label
key={index}
className="flex items-center gap-3 p-2 rounded hover:bg-gray-50 cursor-pointer transition-colors duration-200"
>
<input
type="radio"
name="baseLayer"
checked={activeBaseLayer === layer.name}
onChange={() => handleBaseLayerChange(layer.name)}
className="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 flex-1">
{layer.name}
</span>
</label>
))}
</div>
</div>
{/* Overlay Layers Section */}
{mapLayers.overlays && mapLayers.overlays.length > 0 && (
<div>
<h3 className="text-sm font-semibold text-gray-900 mb-3 flex items-center gap-2">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"
/>
</svg>
Overlay Layers
</h3> <div className="space-y-2">
{mapLayers.overlays.map((layer, index) => (
<label
key={index}
className="flex items-center gap-3 p-2 rounded hover:bg-gray-50 cursor-pointer transition-colors duration-200"
>
<input
type="checkbox"
checked={activeOverlays.includes(layer.name)}
onChange={() => toggleOverlay(layer.name)}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 flex-1">
{layer.name}
</span>
</label>
))}
</div>
</div>
)}
</div>
</div> </div>
</div>
{/* Status Filter Panel - Bottom Left */}
<div className="absolute bottom-4 left-4 z-[1000]">
<div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg px-4 py-3 border border-gray-200">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-700 mr-2">
Filters:
</span>
{/* Toggle All Button */}
<button
onClick={toggleAllFilters}
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-gray-100 hover:bg-gray-200 transition-colors duration-200 mr-2"
title="Toggle all filters"
>
<svg
className="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
<span className="text-gray-600">
{Object.values(statusFilters).every((v) => v)
? "Hide All"
: "Show All"}
</span>
</button>
{/* Individual Status Filters */}
{Object.entries(statusConfig).map(([status, config]) => {
const isActive = statusFilters[status];
const projectCount = projects.filter(
(p) => p.project_status === status && p.coordinates
).length;
return (
<button
key={status}
onClick={() => toggleStatusFilter(status)}
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-all duration-200 hover:bg-gray-100 ${
isActive ? "opacity-100 scale-100" : "opacity-40 scale-95"
}`}
title={`Toggle ${config.label} (${projectCount} projects)`}
>
<div
className={`w-3 h-3 rounded-full border-2 transition-all duration-200 ${
isActive ? "border-white shadow-sm" : "border-gray-300"
}`}
style={{
backgroundColor: isActive ? config.color : "#e5e7eb",
}}
></div>
<span
className={`transition-colors duration-200 ${
isActive ? "text-gray-700" : "text-gray-400"
}`}
>
{config.shortLabel}
</span>
<span
className={`ml-1 text-xs transition-colors duration-200 ${
isActive ? "text-gray-500" : "text-gray-300"
}`}
>
({projectCount})
</span>
</button>
);
})} </div>
</div>
</div> {/* Status Panel - Bottom Left */}
{markers.length > 0 && ( <div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg px-4 py-3 border border-gray-200">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-700 mr-2">
Filters:
</span>
{/* Toggle All Button */}
<button
onClick={toggleAllFilters}
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-gray-100 hover:bg-gray-200 transition-colors duration-200 mr-2"
title="Toggle all filters"
>
<svg
className="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
<span className="text-gray-600">
{Object.values(statusFilters).every((v) => v)
? "Hide All"
: "Show All"}
</span>
</button>
{/* Individual Status Filters */}
{Object.entries(statusConfig).map(([status, config]) => {
const isActive = statusFilters[status];
const projectCount = projects.filter(
(p) => p.project_status === status && p.coordinates
).length;
return (
<button
key={status}
onClick={() => toggleStatusFilter(status)}
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-all duration-200 hover:bg-gray-100 ${
isActive ? "opacity-100 scale-100" : "opacity-40 scale-95"
}`}
title={`Toggle ${config.label} (${projectCount} projects)`}
>
<div
className={`w-3 h-3 rounded-full border-2 transition-all duration-200 ${
isActive ? "border-white shadow-sm" : "border-gray-300"
}`}
style={{
backgroundColor: isActive ? config.color : "#e5e7eb",
}}
></div>
<span
className={`transition-colors duration-200 ${
isActive ? "text-gray-700" : "text-gray-400"
}`}
>
{config.shortLabel}
</span>
<span
className={`ml-1 text-xs transition-colors duration-200 ${
isActive ? "text-gray-500" : "text-gray-300"
}`}
>
({projectCount})
</span>
</button>
);
})}
</div> </div>
</div> </div>
)} )} {/* Full Screen Map */}
{/* Help Panel - Bottom Right */}
<div className="absolute bottom-4 right-4 z-[1000] bg-white/95 backdrop-blur-sm rounded-lg shadow-lg px-3 py-2 border border-gray-200">
<div className="text-xs text-gray-500">
Click markers for details Use 📚 to switch layers
</div>
</div>
{/* Full Screen Map */}
{markers.length === 0 ? ( {markers.length === 0 ? (
<div className="h-full w-full flex items-center justify-center bg-gray-100"> <div className="h-full w-full flex items-center justify-center bg-gray-100">
<div className="text-center max-w-md mx-auto p-8 bg-white rounded-lg shadow-lg"> <div className="text-center max-w-md mx-auto p-8 bg-white rounded-lg shadow-lg">
@@ -559,17 +757,18 @@ export default function ProjectsMapPage() {
</div> </div>
</div> </div>
</div> </div>
) : ( <div className="absolute inset-0"> ) : (
<div className="absolute inset-0">
<DynamicMap <DynamicMap
center={mapCenter} center={mapCenter}
zoom={mapZoom} zoom={mapZoom}
markers={markers} markers={markers}
showLayerControl={true} showLayerControl={false}
defaultLayer="Polish Geoportal Orthophoto" defaultLayer={activeBaseLayer}
activeOverlays={activeOverlays}
onViewChange={handleMapViewChange} onViewChange={handleMapViewChange}
/> />
</div> </div>
)} )} </div>
</div>
); );
} }

View File

@@ -8,7 +8,9 @@ import {
Popup, Popup,
LayersControl, LayersControl,
useMapEvents, useMapEvents,
useMap,
} from "react-leaflet"; } from "react-leaflet";
import L from "leaflet";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import { useEffect } from "react"; import { useEffect } from "react";
import { mapLayers } from "./mapLayers"; import { mapLayers } from "./mapLayers";
@@ -70,26 +72,55 @@ function MapEventHandler({ onViewChange }) {
return null; 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({ export default function EnhancedLeafletMap({
center, center,
zoom = 13, zoom = 13,
markers = [], markers = [],
showLayerControl = true, showLayerControl = true,
defaultLayer = "OpenStreetMap", defaultLayer = "OpenStreetMap",
activeOverlays = [],
onViewChange, onViewChange,
}) { }) {
useEffect(() => { useEffect(() => {
fixLeafletIcons(); fixLeafletIcons();
}, []); }, []); const { BaseLayer, Overlay } = LayersControl;
const { BaseLayer, Overlay } = LayersControl; return (
return (
<MapContainer <MapContainer
center={center} center={center}
zoom={zoom} zoom={zoom}
style={{ height: "100%", width: "100%" }} style={{ height: "100%", width: "100%" }}
scrollWheelZoom={true} scrollWheelZoom={true}
zoomControl={false}
> >
{/* Handle map view changes */} <CustomZoomHandler />
{onViewChange && <MapEventHandler onViewChange={onViewChange} />} {onViewChange && <MapEventHandler onViewChange={onViewChange} />}
{showLayerControl ? ( {showLayerControl ? (
@@ -135,13 +166,52 @@ export default function EnhancedLeafletMap({
)} )}
</Overlay> </Overlay>
))} ))}
</LayersControl> </LayersControl> ) : (
) : ( // Custom layer rendering when no layer control
// Default layer when no layer control <>
<TileLayer {/* Base Layer */}
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' {(() => {
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" 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 (
<WMSTileLayer
key={`custom-overlay-${index}`}
attribution={layer.attribution}
url={layer.url}
params={layer.params}
format={layer.params.format}
transparent={layer.params.transparent}
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) => ( {markers.map((marker, index) => (
<Marker <Marker