@mytec: iter6 ready for test
This commit is contained in:
119
frontend/src/components/map/MeasurementTool.tsx
Normal file
119
frontend/src/components/map/MeasurementTool.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user