Files
rfcp/frontend/src/components/map/MeasurementTool.tsx
2026-01-30 12:44:39 +02:00

120 lines
3.1 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { useMap, Polyline, Marker } from 'react-leaflet';
import L from 'leaflet';
interface MeasurementToolProps {
enabled: boolean;
onComplete?: (distanceKm: number) => void;
}
function haversineKm(
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number {
const R = 6371;
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
function totalDistance(pts: [number, number][]): number {
let total = 0;
for (let i = 1; i < pts.length; i++) {
total += haversineKm(pts[i - 1][0], pts[i - 1][1], pts[i][0], pts[i][1]);
}
return total;
}
const dotIcon = L.divIcon({
className: '',
iconSize: [10, 10],
iconAnchor: [5, 5],
html: '<div style="width:10px;height:10px;background:white;border:2px solid #333;border-radius:50%;"></div>',
});
export default function MeasurementTool({ enabled, onComplete }: MeasurementToolProps) {
const map = useMap();
const [points, setPoints] = useState<[number, number][]>([]);
const pointsRef = useRef(points);
pointsRef.current = points;
// Clear on disable
useEffect(() => {
if (!enabled) {
setPoints([]);
}
}, [enabled]);
// Click handler: add measurement point
useEffect(() => {
if (!enabled) return;
const handleClick = (e: L.LeafletMouseEvent) => {
setPoints((prev) => [...prev, [e.latlng.lat, e.latlng.lng]]);
};
const handleRightClick = (e: L.LeafletMouseEvent) => {
L.DomEvent.preventDefault(e.originalEvent);
const pts = pointsRef.current;
if (pts.length >= 2 && onComplete) {
onComplete(totalDistance(pts));
}
setPoints([]);
};
map.on('click', handleClick);
map.on('contextmenu', handleRightClick);
return () => {
map.off('click', handleClick);
map.off('contextmenu', handleRightClick);
};
}, [map, enabled, onComplete]);
if (!enabled || points.length === 0) return null;
const dist = totalDistance(points);
return (
<>
{points.length >= 2 && (
<Polyline
positions={points}
pathOptions={{ color: '#00ff00', weight: 3, dashArray: '10, 5' }}
/>
)}
{points.map((pos, idx) => (
<Marker key={idx} position={pos} icon={dotIcon} />
))}
{dist > 0 && (
<div
style={{
position: 'absolute',
top: '10px',
left: '50%',
transform: 'translateX(-50%)',
background: 'rgba(0,0,0,0.8)',
color: 'white',
padding: '6px 14px',
borderRadius: '6px',
zIndex: 2000,
pointerEvents: 'none',
fontSize: '13px',
fontWeight: 600,
letterSpacing: '0.3px',
}}
>
Distance: {dist.toFixed(2)} km ({(dist * 1000).toFixed(0)} m)
</div>
)}
</>
);
}