feat: Implement layer control and custom zoom functionality in ProjectsMapPage and LeafletMap components
This commit is contained in:
@@ -5,6 +5,7 @@ import Link from "next/link";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import Button from "@/components/ui/Button";
|
||||
import { mapLayers } from "@/components/ui/mapLayers";
|
||||
|
||||
// Dynamically import the map component to avoid SSR issues
|
||||
const DynamicMap = dynamic(() => import("@/components/ui/LeafletMap"), {
|
||||
@@ -29,6 +30,9 @@ export default function ProjectsMapPage() {
|
||||
in_progress_construction: 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
|
||||
const statusConfig = {
|
||||
@@ -68,6 +72,7 @@ export default function ProjectsMapPage() {
|
||||
);
|
||||
setStatusFilters(newState);
|
||||
};
|
||||
|
||||
// Toggle status filter
|
||||
const toggleStatusFilter = (status) => {
|
||||
setStatusFilters((prev) => ({
|
||||
@@ -75,6 +80,26 @@ export default function ProjectsMapPage() {
|
||||
[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)
|
||||
const updateURL = (center, zoom) => {
|
||||
const params = new URLSearchParams();
|
||||
@@ -96,7 +121,9 @@ export default function ProjectsMapPage() {
|
||||
window.mapUpdateTimeout = setTimeout(() => {
|
||||
updateURL(center, zoom);
|
||||
}, 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(() => {
|
||||
// Check for URL parameters for coordinates and zoom
|
||||
const lat = searchParams.get('lat');
|
||||
@@ -127,6 +154,7 @@ export default function ProjectsMapPage() {
|
||||
// Prevent scrolling on body
|
||||
document.body.style.overflow = "hidden";
|
||||
document.documentElement.style.overflow = "hidden";
|
||||
|
||||
// Cleanup when leaving page
|
||||
return () => {
|
||||
if (nav) {
|
||||
@@ -141,6 +169,7 @@ export default function ProjectsMapPage() {
|
||||
}
|
||||
};
|
||||
}, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/projects")
|
||||
.then((res) => res.json())
|
||||
@@ -182,6 +211,7 @@ export default function ProjectsMapPage() {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [searchParams]);
|
||||
|
||||
// Convert projects to map markers with filtering
|
||||
const markers = projects
|
||||
.filter((project) => project.coordinates)
|
||||
@@ -258,7 +288,7 @@ export default function ProjectsMapPage() {
|
||||
{project.plot}
|
||||
</div>
|
||||
)}
|
||||
</div>{" "}
|
||||
</div>
|
||||
{project.project_status && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-700">Status:</span>
|
||||
@@ -303,6 +333,7 @@ export default function ProjectsMapPage() {
|
||||
};
|
||||
})
|
||||
.filter((marker) => marker !== null);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-gray-50 flex items-center justify-center">
|
||||
@@ -317,11 +348,12 @@ export default function ProjectsMapPage() {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<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">
|
||||
<div className="flex gap-3">
|
||||
<div
|
||||
className="fixed inset-0 bg-gray-50 overflow-hidden"
|
||||
>
|
||||
{/* Floating Header - Left Side */}
|
||||
<div className="absolute top-4 left-4 z-[1000]">
|
||||
{/* Title Box */}
|
||||
<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-3">
|
||||
<h1 className="text-lg font-semibold text-gray-900">
|
||||
@@ -330,10 +362,293 @@ export default function ProjectsMapPage() {
|
||||
<div className="text-sm text-gray-600">
|
||||
{markers.length} of {projects.length} projects with coordinates
|
||||
</div>
|
||||
</div> </div>
|
||||
</div>
|
||||
|
||||
{/* Zoom Controls - Below Title */}
|
||||
<div className="absolute top-20 left-4 z-[1000]">
|
||||
<div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg border border-gray-200 flex flex-col">
|
||||
<button
|
||||
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={() => {
|
||||
// This will be handled by the map component
|
||||
const event = new CustomEvent('mapZoomIn');
|
||||
window.dispatchEvent(event);
|
||||
}}
|
||||
title="Zoom In"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
className="px-3 py-2 hover:bg-gray-50 transition-colors duration-200 text-gray-700 font-medium text-lg"
|
||||
onClick={() => {
|
||||
// This will be handled by the map component
|
||||
const event = new CustomEvent('mapZoomOut');
|
||||
window.dispatchEvent(event);
|
||||
}}
|
||||
title="Zoom Out"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Status Filter Panel */}
|
||||
|
||||
{/* 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">
|
||||
<Link href="/projects">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-white/95 backdrop-blur-sm border-gray-200 shadow-lg hover:bg-white"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16M4 10h16M4 14h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
List View
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/projects/new">
|
||||
<Button variant="primary" size="sm" className="shadow-lg">
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Add Project
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Layer Control Panel */}
|
||||
<div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg border border-gray-200 layer-panel-container">
|
||||
{/* Layer Control Header */}
|
||||
<div className="px-4 py-3 border-b border-gray-200">
|
||||
<button
|
||||
onClick={toggleLayerPanel}
|
||||
className="flex items-center justify-between w-full text-left layer-toggle-button"
|
||||
title="Toggle Layer Controls"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Map Layers
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-0.5 rounded-full">
|
||||
{1 + activeOverlays.length} active
|
||||
</span>
|
||||
</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:
|
||||
@@ -408,124 +723,7 @@ export default function ProjectsMapPage() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Link href="/projects">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-white/95 backdrop-blur-sm border-gray-200 shadow-lg hover:bg-white"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16M4 10h16M4 14h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
List View
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/projects/new">
|
||||
<Button variant="primary" size="sm" className="shadow-lg">
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Add Project
|
||||
</Button>
|
||||
</Link>
|
||||
</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 */}
|
||||
<div className="space-y-1 mb-2">
|
||||
{Object.entries(statusConfig).map(([status, config]) => {
|
||||
const totalCount = projects.filter(
|
||||
(p) => p.project_status === status && p.coordinates
|
||||
).length;
|
||||
const visibleCount = markers.filter((m) => {
|
||||
const project = projects.find(
|
||||
(p) =>
|
||||
p.coordinates &&
|
||||
p.coordinates
|
||||
.split(",")
|
||||
.map((c) => parseFloat(c.trim()))
|
||||
.toString() === m.position.toString()
|
||||
);
|
||||
return project && project.project_status === status;
|
||||
}).length;
|
||||
|
||||
if (totalCount === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={status} className="flex items-center gap-2 text-xs">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: config.color }}
|
||||
></div>
|
||||
<span
|
||||
className={
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 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 */}
|
||||
)} {/* Full Screen Map */}
|
||||
{markers.length === 0 ? (
|
||||
<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">
|
||||
@@ -559,17 +757,18 @@ export default function ProjectsMapPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : ( <div className="absolute inset-0">
|
||||
) : (
|
||||
<div className="absolute inset-0">
|
||||
<DynamicMap
|
||||
center={mapCenter}
|
||||
zoom={mapZoom}
|
||||
markers={markers}
|
||||
showLayerControl={true}
|
||||
defaultLayer="Polish Geoportal Orthophoto"
|
||||
showLayerControl={false}
|
||||
defaultLayer={activeBaseLayer}
|
||||
activeOverlays={activeOverlays}
|
||||
onViewChange={handleMapViewChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)} </div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
Popup,
|
||||
LayersControl,
|
||||
useMapEvents,
|
||||
useMap,
|
||||
} from "react-leaflet";
|
||||
import L from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { useEffect } from "react";
|
||||
import { mapLayers } from "./mapLayers";
|
||||
@@ -70,26 +72,55 @@ function MapEventHandler({ onViewChange }) {
|
||||
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;
|
||||
}, []); const { BaseLayer, Overlay } = LayersControl;
|
||||
return (
|
||||
<MapContainer
|
||||
center={center}
|
||||
zoom={zoom}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
scrollWheelZoom={true}
|
||||
zoomControl={false}
|
||||
>
|
||||
{/* Handle map view changes */}
|
||||
<CustomZoomHandler />
|
||||
{onViewChange && <MapEventHandler onViewChange={onViewChange} />}
|
||||
|
||||
{showLayerControl ? (
|
||||
@@ -135,13 +166,52 @@ export default function EnhancedLeafletMap({
|
||||
)}
|
||||
</Overlay>
|
||||
))}
|
||||
</LayersControl>
|
||||
) : (
|
||||
// Default layer when no layer control
|
||||
</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='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
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) => (
|
||||
<Marker
|
||||
|
||||
Reference in New Issue
Block a user