feat: Add measurement tool with distance calculation and visual markers on the map
This commit is contained in:
@@ -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 (
|
||||
<Suspense fallback={<MapLoadingComponent t={t} />}>
|
||||
<DynamicMap {...props} />
|
||||
<DynamicMap
|
||||
{...props}
|
||||
isMeasuring={isMeasuring}
|
||||
measurementPoints={measurementPoints}
|
||||
onMeasurementClick={addMeasurementPoint}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
}, [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')}
|
||||
>
|
||||
<svg
|
||||
@@ -746,82 +815,6 @@ function ProjectsMapPageContent() {
|
||||
</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">
|
||||
{t('map.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} ${t('map.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">
|
||||
@@ -899,6 +892,58 @@ function ProjectsMapPageContent() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Measurement Display Panel */}
|
||||
{isMeasuring && (
|
||||
<div className="absolute bottom-20 left-4 z-[1000]">
|
||||
<div className="bg-white/95 backdrop-blur-sm rounded-lg shadow-lg px-4 py-3 border border-gray-200 min-w-64">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-sm font-semibold text-gray-900">
|
||||
{t('map.measureDistance')}
|
||||
</h3>
|
||||
<span className="text-xs text-gray-500">
|
||||
{measurementPoints.length} {t('map.points')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-700">
|
||||
{t('map.totalDistance')}:
|
||||
</span>
|
||||
<span className="text-sm font-mono font-semibold text-blue-600">
|
||||
{measurementDistance > 1000
|
||||
? `${(measurementDistance / 1000).toFixed(2)} km`
|
||||
: `${measurementDistance.toFixed(1)} m`
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{measurementPoints.length > 0 && (
|
||||
<div className="text-xs text-gray-600">
|
||||
{t('map.clickToAddPoints')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 pt-2 border-t border-gray-200">
|
||||
<button
|
||||
onClick={clearMeasurement}
|
||||
className="flex-1 px-3 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded transition-colors duration-200"
|
||||
disabled={measurementPoints.length === 0}
|
||||
>
|
||||
{t('map.clear')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentTool("move")}
|
||||
className="flex-1 px-3 py-1 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded transition-colors duration-200"
|
||||
>
|
||||
{t('map.finish')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}{" "}
|
||||
{/* Full Screen Map */}
|
||||
{markers.length === 0 ? (
|
||||
@@ -943,6 +988,9 @@ function ProjectsMapPageContent() {
|
||||
defaultLayer={activeBaseLayer}
|
||||
activeOverlays={activeOverlays}
|
||||
onViewChange={handleMapViewChange}
|
||||
isMeasuring={isMeasuring}
|
||||
measurementPoints={measurementPoints}
|
||||
onMeasurementClick={addMeasurementPoint}
|
||||
/>
|
||||
</div>
|
||||
)}{" "}
|
||||
|
||||
Reference in New Issue
Block a user