"use client"; import type { Map as MapLibreMap, MapGeoJSONFeature, MapMouseEvent } from "maplibre-gl"; import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react"; import { createMeasurementFeatureCollection, emptyMeasurementFeatureCollection, ensureMeasurementLayers, setSelectedMeasurementPipeIds, setMeasurementSourceData } from "../map/measurement-layers"; import { getFeatureId } from "../map/feature-adapter"; import { getLastSegmentDistanceMeters, getLineDistanceMeters, getPolygonAreaSquareMeters, isSameCoordinate, type LngLatTuple } from "../map/geo"; export type WorkbenchMeasureMode = "distance" | "area" | "segment"; export type WorkbenchMeasureUnit = "m" | "km"; export type WorkbenchMeasurementResult = { label: string; value: string; unit: string; metrics: Array<{ label: string; value: string }>; }; type UseWorkbenchMeasurementOptions = { mapRef: RefObject; mapReady: boolean; mode: WorkbenchMeasureMode; unit: WorkbenchMeasureUnit; }; const SNAP_RADIUS_PIXELS = 14; const SNAP_LAYER_IDS = ["pipes-flow"]; type SelectedPipe = { id: string; lengthMeters?: number; }; export function useWorkbenchMeasurement({ mapRef, mapReady, mode, unit }: UseWorkbenchMeasurementOptions) { const [coordinates, setCoordinates] = useState([]); const [selectedPipes, setSelectedPipes] = useState([]); const [measuring, setMeasuring] = useState(false); const coordinatesRef = useRef(coordinates); const selectedPipesRef = useRef(selectedPipes); useEffect(() => { coordinatesRef.current = coordinates; }, [coordinates]); useEffect(() => { selectedPipesRef.current = selectedPipes; }, [selectedPipes]); useEffect(() => { const map = mapRef.current; if (!mapReady || !map) { return; } ensureMeasurementLayers(map); setMeasurementSourceData(map, emptyMeasurementFeatureCollection); }, [mapReady, mapRef]); const syncMeasurementSource = useCallback( (nextCoordinates: LngLatTuple[], nextMode = mode) => { const map = mapRef.current; if (!map) { return; } setMeasurementSourceData( map, nextMode !== "segment" && nextCoordinates.length > 0 ? createMeasurementFeatureCollection(nextCoordinates, nextMode) : emptyMeasurementFeatureCollection ); }, [mapRef, mode] ); useEffect(() => { syncMeasurementSource(coordinates, mode); }, [coordinates, mode, syncMeasurementSource]); useEffect(() => { const map = mapRef.current; if (!mapReady || !map) { return; } setSelectedMeasurementPipeIds( map, mode === "segment" ? selectedPipes.map((pipe) => pipe.id) : [] ); }, [mapReady, mapRef, mode, selectedPipes]); useEffect(() => { const map = mapRef.current; if (!mapReady || !map || !measuring) { return; } const activeMap = map; const canvas = map.getCanvas(); const previousCursor = canvas.style.cursor; canvas.style.cursor = "crosshair"; function handleClick(event: MapMouseEvent) { event.preventDefault(); if (mode === "segment") { const selectedPipe = getSelectedPipe(activeMap, event); if (selectedPipe) { setSelectedPipes((current) => addUniquePipe(current, selectedPipe)); } return; } const coordinate: LngLatTuple = [event.lngLat.lng, event.lngLat.lat]; setCoordinates((current) => { if (isSameCoordinate(current[current.length - 1], coordinate)) { return current; } return [...current, coordinate]; }); } map.on("click", handleClick); return () => { map.off("click", handleClick); canvas.style.cursor = previousCursor; }; }, [mapReady, mapRef, measuring, mode]); const clear = useCallback(() => { setCoordinates([]); setSelectedPipes([]); setMeasuring(false); syncMeasurementSource([]); }, [syncMeasurementSource]); const copyResult = useCallback(() => { if (!navigator.clipboard) { return; } void navigator.clipboard.writeText(getResultText(mode, unit, coordinatesRef.current, selectedPipesRef.current)); }, [mode, unit]); const result = useMemo(() => { const distanceMeters = getLineDistanceMeters(coordinates); const areaSquareMeters = mode === "area" ? getPolygonAreaSquareMeters(coordinates) : 0; const segmentCount = Math.max(coordinates.length - 1, 0); const lastSegmentMeters = getLastSegmentDistanceMeters(coordinates); const selectedPipeLengthMeters = selectedPipes.reduce((total, pipe) => total + (pipe.lengthMeters ?? 0), 0); if (mode === "area") { return { label: "当前面积", value: formatArea(areaSquareMeters, unit), unit: unit === "km" ? "km²" : "m²", metrics: [ { label: "顶点", value: String(coordinates.length) }, { label: "周长", value: `${formatDistance(distanceMeters, unit)} ${unit}` } ] }; } if (mode === "segment") { return { label: "资产长度", value: formatDistance(selectedPipeLengthMeters, unit), unit, metrics: [ { label: "管段", value: `${selectedPipes.length} 段` } ] }; } return { label: "当前长度", value: formatDistance(distanceMeters, unit), unit, metrics: [ { label: "段数", value: String(segmentCount) }, { label: "末段", value: `${formatDistance(lastSegmentMeters, unit)} ${unit}` } ] }; }, [coordinates, mode, selectedPipes, unit]); return { measuring, result, hasPoints: mode === "segment" ? selectedPipes.length > 0 : coordinates.length > 0, start: () => setMeasuring(true), stop: () => setMeasuring(false), clear, copyResult }; } function getResultText( mode: WorkbenchMeasureMode, unit: WorkbenchMeasureUnit, coordinates: LngLatTuple[], selectedPipes: SelectedPipe[] ) { const distanceMeters = getLineDistanceMeters(coordinates); if (mode === "area") { return `面积:${formatArea(getPolygonAreaSquareMeters(coordinates), unit)} ${unit === "km" ? "km²" : "m²"}`; } if (mode === "segment") { const lengthMeters = selectedPipes.reduce((total, pipe) => total + (pipe.lengthMeters ?? 0), 0); return `资产长度:${formatDistance(lengthMeters, unit)} ${unit}`; } return `长度:${formatDistance(distanceMeters, unit)} ${unit}`; } function getSelectedPipe(map: MapLibreMap, event: MapMouseEvent): SelectedPipe | null { const point = event.point; const features = map.queryRenderedFeatures( [ [point.x - SNAP_RADIUS_PIXELS, point.y - SNAP_RADIUS_PIXELS], [point.x + SNAP_RADIUS_PIXELS, point.y + SNAP_RADIUS_PIXELS] ], { layers: SNAP_LAYER_IDS } ); for (const feature of features) { const selectedPipe = toSelectedPipe(feature); if (selectedPipe) { return selectedPipe; } } return null; } function toSelectedPipe(feature: MapGeoJSONFeature): SelectedPipe | null { const id = getFeatureId(feature); if (!id || feature.layer.id !== "pipes-flow") { return null; } return { id, lengthMeters: getNumericProperty(feature.properties?.length) }; } function getNumericProperty(value: unknown) { const numberValue = Number(value); return Number.isFinite(numberValue) && numberValue > 0 ? numberValue : undefined; } function addUniquePipe(current: SelectedPipe[], next: SelectedPipe) { if (current.some((pipe) => pipe.id === next.id)) { return current; } return [...current, next]; } function formatDistance(valueMeters: number, unit: WorkbenchMeasureUnit) { const value = unit === "km" ? valueMeters / 1000 : valueMeters; return value.toLocaleString("zh-CN", { maximumFractionDigits: unit === "km" ? 2 : 1, minimumFractionDigits: 0 }); } function formatArea(valueSquareMeters: number, unit: WorkbenchMeasureUnit) { const value = unit === "km" ? valueSquareMeters / 1_000_000 : valueSquareMeters; return value.toLocaleString("zh-CN", { maximumFractionDigits: unit === "km" ? 3 : 1, minimumFractionDigits: 0 }); }