From 0bb0b074291bde21d6c109cb8b23eeb428d2d446 Mon Sep 17 00:00:00 2001 From: RKWojs Date: Tue, 16 Sep 2025 16:41:08 +0200 Subject: [PATCH] feat: Add measurement tool with distance calculation and visual markers on the map --- src/app/projects/map/page.js | 210 ++++++++++++++++++++------------ src/components/ui/LeafletMap.js | 81 ++++++++++++ 2 files changed, 210 insertions(+), 81 deletions(-) diff --git a/src/app/projects/map/page.js b/src/app/projects/map/page.js index 70d5543..4fc5a46 100644 --- a/src/app/projects/map/page.js +++ b/src/app/projects/map/page.js @@ -43,18 +43,72 @@ function ProjectsMapPageContent() { const [showLayerPanel, setShowLayerPanel] = useState(true); const [currentTool, setCurrentTool] = useState("move"); // Current map tool + // Measurement tool state + const [isMeasuring, setIsMeasuring] = useState(false); + const [measurementPoints, setMeasurementPoints] = useState([]); + const [measurementDistance, setMeasurementDistance] = useState(0); + const [measurementLine, setMeasurementLine] = useState(null); + + // Measurement tool functions + const startMeasurement = () => { + setIsMeasuring(true); + setMeasurementPoints([]); + setMeasurementDistance(0); + setMeasurementLine(null); + }; + + const stopMeasurement = () => { + setIsMeasuring(false); + setMeasurementPoints([]); + setMeasurementDistance(0); + setMeasurementLine(null); + }; + + const addMeasurementPoint = (latlng) => { + const newPoints = [...measurementPoints, latlng]; + setMeasurementPoints(newPoints); + + if (newPoints.length > 1) { + // Calculate distance between last two points + const lastPoint = newPoints[newPoints.length - 2]; + const currentPoint = newPoints[newPoints.length - 1]; + + // Calculate distance using Haversine formula + const R = 6371; // Earth's radius in kilometers + const dLat = (currentPoint.lat - lastPoint.lat) * Math.PI / 180; + const dLon = (currentPoint.lng - lastPoint.lng) * Math.PI / 180; + const a = + Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(lastPoint.lat * Math.PI / 180) * Math.cos(currentPoint.lat * Math.PI / 180) * + Math.sin(dLon/2) * Math.sin(dLon/2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + const distance = R * c * 1000; // Convert to meters + + setMeasurementDistance(prev => prev + distance); + } + }; + + const clearMeasurement = () => { + setMeasurementPoints([]); + setMeasurementDistance(0); + setMeasurementLine(null); + }; + // Wrapper component for the map with proper loading state const MapWrapper = React.useMemo(() => { return function MapWrapperComponent(props) { return ( }> - + ); }; - }, [t]); - - // Status configuration with colors and labels + }, [t, isMeasuring, measurementPoints, addMeasurementPoint]); const statusConfig = { registered: { color: "#6B7280", @@ -237,6 +291,13 @@ function ProjectsMapPageContent() { }); }, [searchParams]); + // Stop measurement when switching away from measure tool + useEffect(() => { + if (currentTool !== "measure" && isMeasuring) { + stopMeasurement(); + } + }, [currentTool, isMeasuring]); + // Convert projects to map markers with filtering const markers = projects .filter((project) => project.coordinates) @@ -465,7 +526,15 @@ function ProjectsMapPageContent() { ? "bg-blue-100 text-blue-700" : "text-gray-700 hover:bg-gray-50" }`} - onClick={() => setCurrentTool("measure")} + onClick={() => { + if (currentTool === "measure") { + setCurrentTool("move"); + stopMeasurement(); + } else { + setCurrentTool("measure"); + startMeasurement(); + } + }} title={t('map.measureDistance')} > {" "} - {/* Status Filter Panel - Bottom Left */} -
-
-
- - {t('map.filters')} - - {/* Toggle All 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 ( - - ); - })}{" "} -
-
-
{" "} {/* Status Panel - Bottom Left */} {markers.length > 0 && (
@@ -899,6 +892,58 @@ function ProjectsMapPageContent() { })}
+ )} + + {/* Measurement Display Panel */} + {isMeasuring && ( +
+
+
+

+ {t('map.measureDistance')} +

+ + {measurementPoints.length} {t('map.points')} + +
+ +
+
+ + {t('map.totalDistance')}: + + + {measurementDistance > 1000 + ? `${(measurementDistance / 1000).toFixed(2)} km` + : `${measurementDistance.toFixed(1)} m` + } + +
+ + {measurementPoints.length > 0 && ( +
+ {t('map.clickToAddPoints')} +
+ )} + +
+ + +
+
+
+
)}{" "} {/* Full Screen Map */} {markers.length === 0 ? ( @@ -943,6 +988,9 @@ function ProjectsMapPageContent() { defaultLayer={activeBaseLayer} activeOverlays={activeOverlays} onViewChange={handleMapViewChange} + isMeasuring={isMeasuring} + measurementPoints={measurementPoints} + onMeasurementClick={addMeasurementPoint} /> )}{" "} diff --git a/src/components/ui/LeafletMap.js b/src/components/ui/LeafletMap.js index 917476f..eef9dd6 100644 --- a/src/components/ui/LeafletMap.js +++ b/src/components/ui/LeafletMap.js @@ -8,6 +8,7 @@ import { LayersControl, useMapEvents, useMap, + Polyline, } from "react-leaflet"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; @@ -84,6 +85,32 @@ const createColoredMarkerIcon = (color) => { } return null; }; +// Create numbered measurement marker icons +const createMeasurementMarkerIcon = (number) => { + if (typeof window !== "undefined") { + return new L.DivIcon({ + html: `
${number}
`, + className: 'custom-measurement-marker', + iconSize: [24, 24], + iconAnchor: [12, 12], + }); + } + return null; +}; + // Component to handle map events function MapEventHandler({ onViewChange }) { const map = useMapEvents({ @@ -106,6 +133,19 @@ function MapEventHandler({ onViewChange }) { return null; } +// Component to handle measurement events +function MeasurementHandler({ isMeasuring, onMeasurementClick, measurementPoints }) { + const map = useMapEvents({ + click: (e) => { + if (isMeasuring && onMeasurementClick) { + onMeasurementClick(e.latlng); + } + }, + }); + + return null; +} + // Custom zoom control component that handles external events function CustomZoomHandler() { const map = useMap(); @@ -143,6 +183,9 @@ export default function EnhancedLeafletMap({ activeOverlays = [], onViewChange, showOverlays = true, + isMeasuring = false, + measurementPoints = [], + onMeasurementClick, }) { useEffect(() => { fixLeafletIcons(); @@ -157,6 +200,11 @@ export default function EnhancedLeafletMap({ > {onViewChange && } + {isMeasuring && } {showLayerControl ? ( @@ -255,6 +303,39 @@ export default function EnhancedLeafletMap({ {marker.popup && {marker.popup}} ))} + + {/* Measurement elements */} + {isMeasuring && measurementPoints.length > 0 && ( + <> + {/* Measurement line */} + {measurementPoints.length > 1 && ( + + )} + + {/* Measurement point markers */} + {measurementPoints.map((point, index) => ( + + +
+ Point {index + 1}
+ Lat: {point.lat.toFixed(6)}
+ Lng: {point.lng.toFixed(6)} +
+
+
+ ))} + + )} ); }