fix: 修复场站围栏显隐逻辑与地图中心点同步问题
- 提取 `findMatchedPolygon` 为公共方法,统一围栏命中逻辑
- 地图区域变化时同步场站围栏显隐状态,确保与吸附后中心点一致
- 运营区页面仅展示运营区围栏,过滤场站围栏
- 修复收藏按钮点击事件冒泡问题
- 调整路线摘要弹窗样式
| | |
| | | getCityLocation: (cityName: string) => Promise<any>; |
| | | /** 地图数据刷新 */ |
| | | updateMapData: (lat: number, lng: number, cityCode?: string) => Promise<any>; |
| | | /** 同步场站围栏显隐 */ |
| | | syncStationFenceVisibility: (lat: number, lng: number, isInside: boolean) => void; |
| | | /** 城市就绪同步 */ |
| | | ensureSelectedCityReady: (lng: number, lat: number, skipCityEffect?: boolean, forceRefresh?: boolean) => Promise<any>; |
| | | /** 设置当前站点 */ |
| | |
| | | longitude: loc.longitude |
| | | }); |
| | | if (switchedCity?.code) { |
| | | deps.updateMapData(loc.latitude, loc.longitude, switchedCity.code); |
| | | const result = await deps.updateMapData(loc.latitude, loc.longitude, switchedCity.code); |
| | | deps.syncStationFenceVisibility(loc.latitude, loc.longitude, !!result?.isInside); |
| | | } |
| | | } catch (error) { |
| | | deps.skipNextLocationCenterRef.current = true; |
| | |
| | | deps.shouldIgnoreRegionChange.current = true; |
| | | const cachedCity = Taro.getStorageSync('selectedCity'); |
| | | if (cachedCity?.code) { |
| | | deps.updateMapData(startSite.latitude, startSite.longitude, cachedCity.code); |
| | | const result = await deps.updateMapData(startSite.latitude, startSite.longitude, cachedCity.code); |
| | | deps.syncStationFenceVisibility(startSite.latitude, startSite.longitude, !!result?.isInside); |
| | | } else { |
| | | const cityInfo = await deps.ensureSelectedCityReady(startSite.longitude, startSite.latitude, true); |
| | | if (cityInfo?.code) { |
| | | deps.updateMapData(startSite.latitude, startSite.longitude, cityInfo.code); |
| | | const result = await deps.updateMapData(startSite.latitude, startSite.longitude, cityInfo.code); |
| | | deps.syncStationFenceVisibility(startSite.latitude, startSite.longitude, !!result?.isInside); |
| | | } |
| | | } |
| | | return; |
| | |
| | | siteState: any; |
| | | cityState: any; |
| | | isLogin: boolean; |
| | | onLocationSettled: (lat: number, lng: number, isInside: boolean) => void; |
| | | }; |
| | | |
| | | /** |
| | | * 定位流程编排:不持有状态,仅组合 map/site/city 状态 hook 完成定位主链路。 |
| | | */ |
| | | export default function useLocationFlow({ mapState, siteState, cityState, isLogin }: Deps) { |
| | | export default function useLocationFlow({ mapState, siteState, cityState, isLogin, onLocationSettled }: Deps) { |
| | | const updateByStartSite = async () => { |
| | | const startSite = Taro.getStorageSync('startSite'); |
| | | if (!(startSite && startSite.latitude && startSite.longitude)) return false; |
| | |
| | | mapState.shouldIgnoreRegionChange.current = true; |
| | | const cachedCity = Taro.getStorageSync('selectedCity'); |
| | | if (cachedCity?.code) { |
| | | await mapState.updateMapData(startSite.latitude, startSite.longitude, cachedCity.code); |
| | | const result = await mapState.updateMapData(startSite.latitude, startSite.longitude, cachedCity.code); |
| | | onLocationSettled(startSite.latitude, startSite.longitude, !!result?.isInside); |
| | | } else { |
| | | const cityInfo = await cityState.ensureSelectedCityReady(startSite.longitude, startSite.latitude, true); |
| | | if (cityInfo?.code) { |
| | | await mapState.updateMapData(startSite.latitude, startSite.longitude, cityInfo.code); |
| | | const result = await mapState.updateMapData(startSite.latitude, startSite.longitude, cityInfo.code); |
| | | onLocationSettled(startSite.latitude, startSite.longitude, !!result?.isInside); |
| | | } |
| | | } |
| | | return true; |
| | |
| | | forceCityRefresh |
| | | ); |
| | | if (cityInfo?.code) { |
| | | const { newMarkers, sites } = await mapState.updateMapData(res.latitude, res.longitude, cityInfo.code); |
| | | const { isInside, newMarkers, sites } = await mapState.updateMapData(res.latitude, res.longitude, cityInfo.code); |
| | | const nearestCandidates = sites.length > 0 ? sites : newMarkers; |
| | | const { adsorbed, marker } = siteState.tryAdsorb(res.latitude, res.longitude, nearestCandidates); |
| | | const finalLat = adsorbed && marker ? marker.latitude : res.latitude; // 最终纬度 |
| | | const finalLng = adsorbed && marker ? marker.longitude : res.longitude; // 最终经度 |
| | | onLocationSettled(finalLat, finalLng, !!isInside); |
| | | if (adsorbed && updateCenter && marker) { |
| | | setTimeout(() => { |
| | | Taro.createMapContext('map').moveToLocation({ |
| | |
| | | } from '../../../../utils/Robotaxi-public-method'; |
| | | import { jumpTo } from '../../../../utils/jump'; |
| | | import { getGeofence } from '../../../../robotaxi/api/home'; |
| | | import { GeofenceType } from '../../../../robotaxi/constants/geofence'; |
| | | |
| | | type Deps = { |
| | | /** 是否未登录 */ |
| | |
| | | let nextPolygons = deps.polygons; |
| | | try { |
| | | const res: any = await getGeofence({ cityCode: deps.selectedCity.code }); |
| | | const geofenceList = filterRenderableGeofence(res); |
| | | const geofenceList = filterRenderableGeofence(res).filter((item) => Number(item?.type) === GeofenceType.OPERATING); // 查看运营区域时仅展示运营区 |
| | | if (geofenceList.length > 0) { |
| | | const { polygons: nextFormattedPolygons, fencePolylines: nextFencePolylines } = formatGeofenceByType(geofenceList); // 围栏格式统一复用公共实现 |
| | | nextPolygons = nextFormattedPolygons; |
| | |
| | | */ |
| | | export default function useRegionChange({ mapState, siteState, toOrigin, onRegionBegin, onRegionSettled }: Deps) { |
| | | /** |
| | | * 按最终落点回传地图结算结果,保证场站围栏与吸附后的中心点一致。 |
| | | */ |
| | | const settleRegionResult = (payload: { lat: number; lon: number; isInside: boolean }) => { |
| | | if (typeof onRegionSettled === 'function') { |
| | | onRegionSettled(payload); |
| | | } |
| | | }; |
| | | |
| | | /** |
| | | * 通过中心点主动触发吸附判断。 |
| | | */ |
| | | const queryPointerByCenter = async (lat: number, lng: number) => { |
| | |
| | | try { |
| | | const { isInside, newMarkers, sites } = await mapState.updateMapData(lat, lng); |
| | | if (!isInside) { |
| | | settleRegionResult({ lat, lon: lng, isInside: false }); |
| | | siteState.setBounce(false); |
| | | return; |
| | | } |
| | | const nearestCandidates = sites.length > 0 ? sites : newMarkers; |
| | | const { adsorbed } = siteState.tryAdsorb(lat, lng, nearestCandidates); |
| | | const { adsorbed, marker } = siteState.tryAdsorb(lat, lng, nearestCandidates); |
| | | const finalLat = adsorbed && marker ? marker.latitude : lat; // 最终纬度 |
| | | const finalLng = adsorbed && marker ? marker.longitude : lng; // 最终经度 |
| | | settleRegionResult({ lat: finalLat, lon: finalLng, isInside: true }); |
| | | setTimeout(async () => { |
| | | siteState.setBounce(false); |
| | | if (!adsorbed) { |
| | |
| | | } |
| | | try { |
| | | const { isInside, newMarkers, sites } = await mapState.updateMapData(lat, lng); |
| | | if (typeof onRegionSettled === 'function') { |
| | | onRegionSettled({ lat, lon: lng, isInside: !!isInside }); |
| | | if (!isInside) { |
| | | settleRegionResult({ lat, lon: lng, isInside: false }); |
| | | siteState.setBounce(false); |
| | | return; |
| | | } |
| | | const nearestCandidates = sites.length > 0 ? sites : newMarkers; |
| | | const { adsorbed, marker } = siteState.tryAdsorb(lat, lng, nearestCandidates); |
| | | const finalLat = adsorbed && marker ? marker.latitude : lat; // 最终纬度 |
| | | const finalLng = adsorbed && marker ? marker.longitude : lng; // 最终经度 |
| | | settleRegionResult({ lat: finalLat, lon: finalLng, isInside: true }); |
| | | if ((causedBy === 'gesture' || causedBy === 'drag' || wasReturningToOrigin) && marker) { |
| | | mapCtx.moveToLocation({ |
| | | latitude: marker.latitude, |
| | |
| | | import wsManager from '../../../utils/websocket'; |
| | | import { |
| | | filterRenderableGeofence, |
| | | findMatchedPolygon, |
| | | formatGeofenceByType, |
| | | getCityLocation, |
| | | mapOrderStatusToTripStatus, |
| | | safeParse, |
| | | getWeatherInfo |
| | | } from '../../../utils/Robotaxi-public-method'; |
| | |
| | | import { getOrderDistance } from '../../../robotaxi/api/select-map-site'; |
| | | import { checkOtp, toH5 } from '../../../utils/tools'; |
| | | import { subscribeOperationCouponGroup } from '../../../utils/subscribe'; |
| | | import { GeofenceType } from '../../../robotaxi/constants/geofence'; |
| | | |
| | | type HomeTripStatus = TripStatus | 0; |
| | | |
| | |
| | | const [isLogin, setIsLogin] = useState(false); // 是否登录 |
| | | const [overlayAnchorBottom, setOverlayAnchorBottom] = useState<number | null>(null); // 浮层初始化锚点(距底部) |
| | | const [temperatureUnit, setTemperatureUnit] = useState(DEFAULT_TEMPERATURE_UNIT); // 后台配置的温度单位 |
| | | const [operatingGeofenceList, setOperatingGeofenceList] = useState<any[]>([]); // 运营区围栏原始数据 |
| | | const [stationGeofenceList, setStationGeofenceList] = useState<any[]>([]); // 场站围栏原始数据 |
| | | const operatingGeofenceListRef = useRef<any[]>([]); // 运营区围栏最新缓存 |
| | | const stationGeofenceListRef = useRef<any[]>([]); // 场站围栏最新缓存 |
| | | const shouldShowOrderStatusCard = useMemo(() => tripStatus === 3 || Boolean(orderData?.id), [orderData?.id, tripStatus]); // 订单状态卡片展示时机 |
| | | const { |
| | | boxHeight, |
| | |
| | | |
| | | const displayTemperatureUnit = useMemo(() => normalizeTemperatureUnit(temperatureUnit), [temperatureUnit]); // 页面展示温度单位 |
| | | const displayTemperature = useMemo(() => convertTemperatureByUnit(weatherData?.temperature, displayTemperatureUnit), [weatherData?.temperature, displayTemperatureUnit]); // 页面展示温度值 |
| | | |
| | | /** |
| | | * 按当前命中的场站围栏刷新地图覆盖物,运营区始终常驻展示。 |
| | | */ |
| | | const applyVisibleGeofence = (matchedStationGeofence: any | null, nextOperatingGeofenceList?: any[]) => { |
| | | const operatingGeofenceSource = Array.isArray(nextOperatingGeofenceList) |
| | | ? nextOperatingGeofenceList |
| | | : operatingGeofenceListRef.current; |
| | | const { polygons: operatingPolygons } = formatGeofenceByType(operatingGeofenceSource); |
| | | const { polygons: stationPolygons, fencePolylines: stationFencePolylines } = formatGeofenceByType( |
| | | matchedStationGeofence ? [matchedStationGeofence] : [] |
| | | ); |
| | | setPolygons([...operatingPolygons, ...stationPolygons]); |
| | | setFencePolylines(stationFencePolylines); |
| | | }; |
| | | |
| | | /** |
| | | * 根据大头针中心点切换场站围栏显隐。 |
| | | */ |
| | | const syncStationFenceVisibility = (lat: number, lng: number, isInside: boolean) => { |
| | | const latestOperatingGeofenceList = operatingGeofenceListRef.current; |
| | | const latestStationGeofenceList = stationGeofenceListRef.current; |
| | | if (!Array.isArray(latestOperatingGeofenceList) || latestOperatingGeofenceList.length === 0) { |
| | | return; |
| | | } |
| | | if (!isInside || !Array.isArray(latestStationGeofenceList) || latestStationGeofenceList.length === 0) { |
| | | applyVisibleGeofence(null); |
| | | return; |
| | | } |
| | | const matchedStationGeofence = findMatchedPolygon( |
| | | { latitude: lat, longitude: lng }, |
| | | latestStationGeofenceList, |
| | | 0 |
| | | ); |
| | | applyVisibleGeofence(matchedStationGeofence); |
| | | }; |
| | | |
| | | /** |
| | | * 同步运营区围栏 ref,避免异步回调读到旧值。 |
| | | */ |
| | | useEffect(() => { |
| | | operatingGeofenceListRef.current = operatingGeofenceList; |
| | | }, [operatingGeofenceList]); |
| | | |
| | | /** |
| | | * 同步场站围栏 ref,保证初始化与吸附结算拿到最新数据。 |
| | | */ |
| | | useEffect(() => { |
| | | stationGeofenceListRef.current = stationGeofenceList; |
| | | }, [stationGeofenceList]); |
| | | |
| | | useEffect(() => { |
| | | let timer: any = null; |
| | |
| | | if (!selectedCity.code) return; |
| | | const allowMapMove = !isLocationAuthDenied; |
| | | getGeofence({ cityCode: selectedCity.code }).then((res: any) => { |
| | | |
| | | const geofenceList = filterRenderableGeofence(res); |
| | | const nextOperatingGeofenceList = geofenceList.filter((item) => Number(item?.type) === GeofenceType.OPERATING); // 运营区围栏集合 |
| | | const nextStationGeofenceList = geofenceList.filter((item) => Number(item?.type) === GeofenceType.STATION); // 场站围栏集合 |
| | | setOperatingGeofenceList(nextOperatingGeofenceList); |
| | | setStationGeofenceList(nextStationGeofenceList); |
| | | if (geofenceList.length > 0) { |
| | | const { polygons: newPolygons, fencePolylines: nextFencePolylines } = formatGeofenceByType(geofenceList); // 围栏面和场站虚线边框统一走公共格式化 |
| | | setPolygons(newPolygons); |
| | | setFencePolylines(nextFencePolylines); |
| | | const matchedStationGeofence = (typeof latitude === 'number' && typeof longitude === 'number') |
| | | ? findMatchedPolygon( |
| | | { latitude, longitude }, |
| | | nextStationGeofenceList, |
| | | 0 |
| | | ) |
| | | : null; |
| | | applyVisibleGeofence(matchedStationGeofence, nextOperatingGeofenceList); |
| | | } else { |
| | | setOperatingGeofenceList([]); |
| | | setStationGeofenceList([]); |
| | | setPolygons([]); |
| | | setFencePolylines([]); |
| | | if ((orderData || orderData?.id) && JSON.stringify(orderData) != '{}') return; |
| | |
| | | } |
| | | }).catch(err => { |
| | | console.error('获取电子围栏失败', err); |
| | | setOperatingGeofenceList([]); |
| | | setStationGeofenceList([]); |
| | | setPolygons([]); |
| | | setFencePolylines([]); |
| | | // 定位到城市中心 |
| | |
| | | mapState, |
| | | siteState, |
| | | cityState, |
| | | isLogin |
| | | isLogin, |
| | | onLocationSettled: syncStationFenceVisibility |
| | | }); // 定位流程编排 |
| | | |
| | | const handleClosePrivacy = () => { |
| | |
| | | shouldIgnoreRegionChange, |
| | | getCityLocation, |
| | | updateMapData, |
| | | syncStationFenceVisibility, |
| | | ensureSelectedCityReady, |
| | | setCurrentSiteFromStart: setCurrentSite, |
| | | firstEnterKey: FIRST_ENTER_KEY, |
| | |
| | | siteState, |
| | | toOrigin, |
| | | onRegionBegin: handleVehicleRegionBegin, |
| | | onRegionSettled: handleVehicleRegionSettled |
| | | onRegionSettled: (payload) => { |
| | | handleVehicleRegionSettled(payload); |
| | | syncStationFenceVisibility(payload.lat, payload.lon, payload.isInside); |
| | | } |
| | | }); // 地图 region-change 编排 |
| | | const { |
| | | ensureNetwork, |
| | |
| | | font-size: 20rpx; |
| | | font-weight: 500; |
| | | color: #FFFFFF; |
| | | line-height: 28rpx; |
| | | padding: 10rpx 12rpx 9rpx 14rpx; |
| | | line-height: 47rpx; |
| | | padding: 0 12rpx 0 14rpx; |
| | | } |
| | | |
| | | .route-summary-callout-arrow { |
| | |
| | | |
| | | import { getOpenCity } from '../../api/select-city'; |
| | | import { getGeofence } from '../../api/home'; |
| | | import { extractPointsFromPolygons, getDistance } from '../../../utils/Robotaxi-public-method'; |
| | | import { extractPointsFromPolygons, findMatchedPolygon, getDistance } from '../../../utils/Robotaxi-public-method'; |
| | | |
| | | export default function OperatingArea() { |
| | | const [longitude, setLongitude] = useState<number | null>(null); |
| | |
| | | setPolylines(updatePolylineStyle(geofenceList, polygonId)); |
| | | }; |
| | | |
| | | const projectPoint = (point: { latitude: number; longitude: number }, referenceLatitude: number) => { |
| | | const metersPerDegreeLat = 111320; |
| | | const metersPerDegreeLng = 111320 * Math.cos(referenceLatitude * Math.PI / 180); |
| | | return { |
| | | x: point.longitude * metersPerDegreeLng, |
| | | y: point.latitude * metersPerDegreeLat |
| | | }; |
| | | }; |
| | | |
| | | const getDistanceToSegment = ( |
| | | point: { latitude: number; longitude: number }, |
| | | start: { latitude: number; longitude: number }, |
| | | end: { latitude: number; longitude: number } |
| | | ) => { |
| | | const referenceLatitude = (point.latitude + start.latitude + end.latitude) / 3; |
| | | const pointXY = projectPoint(point, referenceLatitude); |
| | | const startXY = projectPoint(start, referenceLatitude); |
| | | const endXY = projectPoint(end, referenceLatitude); |
| | | const dx = endXY.x - startXY.x; |
| | | const dy = endXY.y - startXY.y; |
| | | const lengthSquared = dx * dx + dy * dy; |
| | | |
| | | if (lengthSquared === 0) { |
| | | return Math.sqrt((pointXY.x - startXY.x) * (pointXY.x - startXY.x) + (pointXY.y - startXY.y) * (pointXY.y - startXY.y)); |
| | | } |
| | | |
| | | let ratio = ((pointXY.x - startXY.x) * dx + (pointXY.y - startXY.y) * dy) / lengthSquared; |
| | | ratio = Math.max(0, Math.min(1, ratio)); |
| | | const closestX = startXY.x + ratio * dx; |
| | | const closestY = startXY.y + ratio * dy; |
| | | |
| | | return Math.sqrt((pointXY.x - closestX) * (pointXY.x - closestX) + (pointXY.y - closestY) * (pointXY.y - closestY)); |
| | | }; |
| | | |
| | | const isPointInPolygon = ( |
| | | point: { latitude: number; longitude: number }, |
| | | polygonPoints: Array<{ latitude: number; longitude: number }> |
| | | ) => { |
| | | if (!Array.isArray(polygonPoints) || polygonPoints.length < 3) { |
| | | return false; |
| | | } |
| | | |
| | | let isInside = false; |
| | | const pointX = point.longitude; |
| | | const pointY = point.latitude; |
| | | |
| | | for (let index = 0, lastIndex = polygonPoints.length - 1; index < polygonPoints.length; lastIndex = index, index += 1) { |
| | | const currentPoint = polygonPoints[index]; |
| | | const previousPoint = polygonPoints[lastIndex]; |
| | | const currentX = currentPoint.longitude; |
| | | const currentY = currentPoint.latitude; |
| | | const previousX = previousPoint.longitude; |
| | | const previousY = previousPoint.latitude; |
| | | |
| | | const isIntersected = ((currentY > pointY) !== (previousY > pointY)) |
| | | && (pointX < ((previousX - currentX) * (pointY - currentY)) / ((previousY - currentY) || Number.EPSILON) + currentX); |
| | | |
| | | if (isIntersected) { |
| | | isInside = !isInside; |
| | | } |
| | | } |
| | | |
| | | return isInside; |
| | | }; |
| | | |
| | | /** |
| | | * 根据点击点命中围栏,优先命中内部,其次吸附到最近边界。 |
| | | */ |
| | | const findNearestPolygonIdByTap = (latitudeValue: number, longitudeValue: number) => { |
| | | const tapPoint = { latitude: latitudeValue, longitude: longitudeValue }; |
| | | const insidePolygon = rawGeofenceData.find((item) => { |
| | | const polygonPoints = item.polygonPoints || []; |
| | | return isPointInPolygon(tapPoint, polygonPoints); |
| | | }); |
| | | |
| | | if (insidePolygon) { |
| | | return insidePolygon.mapPolygonId; |
| | | } |
| | | |
| | | let nearestPolygonId: number | null = null; |
| | | let nearestDistance = Number.POSITIVE_INFINITY; |
| | | |
| | | rawGeofenceData.forEach((item) => { |
| | | const polygonPoints = item.polygonPoints || []; |
| | | if (polygonPoints.length < 2) { |
| | | return; |
| | | } |
| | | |
| | | let minDistanceToBorder = Number.POSITIVE_INFINITY; |
| | | for (let index = 0; index < polygonPoints.length; index += 1) { |
| | | const start = polygonPoints[index]; |
| | | const end = polygonPoints[(index + 1) % polygonPoints.length]; |
| | | const distanceToSegment = getDistanceToSegment(tapPoint, start, end); |
| | | if (distanceToSegment < minDistanceToBorder) { |
| | | minDistanceToBorder = distanceToSegment; |
| | | } |
| | | } |
| | | |
| | | const distanceToFirstPoint = getDistance( |
| | | latitudeValue, |
| | | longitudeValue, |
| | | polygonPoints[0].latitude, |
| | | polygonPoints[0].longitude |
| | | ); |
| | | const matchDistance = Math.min(minDistanceToBorder, distanceToFirstPoint); |
| | | |
| | | if (matchDistance < nearestDistance) { |
| | | nearestDistance = matchDistance; |
| | | nearestPolygonId = item.mapPolygonId; |
| | | } |
| | | }); |
| | | |
| | | return nearestDistance <= 180 ? nearestPolygonId : null; |
| | | const matchedPolygon = findMatchedPolygon(tapPoint, rawGeofenceData, 180); |
| | | return matchedPolygon?.mapPolygonId ?? null; |
| | | }; |
| | | |
| | | const handleSelectCity = (cityCode: string) => { |
| | |
| | | </View> |
| | | {showDistance ? |
| | | <View className='collect-box'> |
| | | <Image onClick={handleCollect} className='collect-icon' src={isCollected ? starSrc : noStarSrc} /> |
| | | <Image onClick={(e) => { |
| | | e.stopPropagation(); |
| | | handleCollect(e); |
| | | }} className='collect-icon' src={isCollected ? starSrc : noStarSrc} /> |
| | | {renderHighlightedText(item.distance || '-', 'history-distance')} |
| | | </View> |
| | | : null} |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 统一标准化围栏点位,兼容 `{ lat, lng }` 和 `{ latitude, longitude }` 两种结构。 |
| | | */ |
| | | const normalizePolygonPoints = (points: any[] = []) => { |
| | | return points |
| | | .map((point) => ({ |
| | | latitude: Number(point?.latitude ?? point?.lat), |
| | | longitude: Number(point?.longitude ?? point?.lng) |
| | | })) |
| | | .filter((point) => Number.isFinite(point.latitude) && Number.isFinite(point.longitude)); |
| | | }; |
| | | |
| | | /** |
| | | * 将经纬度投影到近似平面,便于计算点到边的距离。 |
| | | */ |
| | | const projectPoint = (point: { latitude: number; longitude: number }, referenceLatitude: number) => { |
| | | const metersPerDegreeLat = 111320; |
| | | const metersPerDegreeLng = 111320 * Math.cos(referenceLatitude * Math.PI / 180); |
| | | return { |
| | | x: point.longitude * metersPerDegreeLng, |
| | | y: point.latitude * metersPerDegreeLat |
| | | }; |
| | | }; |
| | | |
| | | /** |
| | | * 计算点到线段的最短距离,单位为米。 |
| | | */ |
| | | export const getDistanceToSegment = ( |
| | | point: { latitude: number; longitude: number }, |
| | | start: { latitude: number; longitude: number }, |
| | | end: { latitude: number; longitude: number } |
| | | ) => { |
| | | const referenceLatitude = (point.latitude + start.latitude + end.latitude) / 3; |
| | | const pointXY = projectPoint(point, referenceLatitude); |
| | | const startXY = projectPoint(start, referenceLatitude); |
| | | const endXY = projectPoint(end, referenceLatitude); |
| | | const dx = endXY.x - startXY.x; |
| | | const dy = endXY.y - startXY.y; |
| | | const lengthSquared = dx * dx + dy * dy; |
| | | |
| | | if (lengthSquared === 0) { |
| | | return Math.sqrt((pointXY.x - startXY.x) * (pointXY.x - startXY.x) + (pointXY.y - startXY.y) * (pointXY.y - startXY.y)); |
| | | } |
| | | |
| | | let ratio = ((pointXY.x - startXY.x) * dx + (pointXY.y - startXY.y) * dy) / lengthSquared; |
| | | ratio = Math.max(0, Math.min(1, ratio)); |
| | | const closestX = startXY.x + ratio * dx; |
| | | const closestY = startXY.y + ratio * dy; |
| | | |
| | | return Math.sqrt((pointXY.x - closestX) * (pointXY.x - closestX) + (pointXY.y - closestY) * (pointXY.y - closestY)); |
| | | }; |
| | | |
| | | /** |
| | | * 判断点是否命中多边形内部。 |
| | | */ |
| | | export const isPointInPolygon = ( |
| | | point: { latitude: number; longitude: number }, |
| | | polygonPoints: Array<{ latitude: number; longitude: number }> |
| | | ) => { |
| | | if (!Array.isArray(polygonPoints) || polygonPoints.length < 3) { |
| | | return false; |
| | | } |
| | | |
| | | let isInside = false; |
| | | const pointX = point.longitude; |
| | | const pointY = point.latitude; |
| | | |
| | | for (let index = 0, lastIndex = polygonPoints.length - 1; index < polygonPoints.length; lastIndex = index, index += 1) { |
| | | const currentPoint = polygonPoints[index]; |
| | | const previousPoint = polygonPoints[lastIndex]; |
| | | const currentX = currentPoint.longitude; |
| | | const currentY = currentPoint.latitude; |
| | | const previousX = previousPoint.longitude; |
| | | const previousY = previousPoint.latitude; |
| | | |
| | | const isIntersected = ((currentY > pointY) !== (previousY > pointY)) |
| | | && (pointX < ((previousX - currentX) * (pointY - currentY)) / ((previousY - currentY) || Number.EPSILON) + currentX); |
| | | |
| | | if (isIntersected) { |
| | | isInside = !isInside; |
| | | } |
| | | } |
| | | |
| | | return isInside; |
| | | }; |
| | | |
| | | /** |
| | | * 查找当前点命中的围栏;未命中时可退化为最近围栏边界。 |
| | | */ |
| | | export const findMatchedPolygon = ( |
| | | point: { latitude: number; longitude: number }, |
| | | polygonList: any[] = [], |
| | | maxDistance = 180 |
| | | ) => { |
| | | if (!Array.isArray(polygonList) || polygonList.length === 0) { |
| | | return null; |
| | | } |
| | | |
| | | const insidePolygon = polygonList.find((item) => { |
| | | const polygonPoints = normalizePolygonPoints(item?.polygonPoints || item?.points || []); |
| | | return isPointInPolygon(point, polygonPoints); |
| | | }); |
| | | |
| | | if (insidePolygon) { |
| | | return insidePolygon; |
| | | } |
| | | |
| | | let nearestPolygon: any = null; |
| | | let nearestDistance = Number.POSITIVE_INFINITY; |
| | | |
| | | polygonList.forEach((item) => { |
| | | const polygonPoints = normalizePolygonPoints(item?.polygonPoints || item?.points || []); |
| | | if (polygonPoints.length < 2) { |
| | | return; |
| | | } |
| | | |
| | | let minDistanceToBorder = Number.POSITIVE_INFINITY; |
| | | for (let index = 0; index < polygonPoints.length; index += 1) { |
| | | const start = polygonPoints[index]; |
| | | const end = polygonPoints[(index + 1) % polygonPoints.length]; |
| | | const distanceToSegment = getDistanceToSegment(point, start, end); |
| | | if (distanceToSegment < minDistanceToBorder) { |
| | | minDistanceToBorder = distanceToSegment; |
| | | } |
| | | } |
| | | |
| | | const distanceToFirstPoint = getDistance( |
| | | point.latitude, |
| | | point.longitude, |
| | | polygonPoints[0].latitude, |
| | | polygonPoints[0].longitude |
| | | ); |
| | | const matchDistance = Math.min(minDistanceToBorder, distanceToFirstPoint); |
| | | |
| | | if (matchDistance < nearestDistance) { |
| | | nearestDistance = matchDistance; |
| | | nearestPolygon = item; |
| | | } |
| | | }); |
| | | |
| | | return nearestDistance <= maxDistance ? nearestPolygon : null; |
| | | }; |
| | | |
| | | /** |
| | | * 根据站点空间分布估算推荐缩放等级(站点越分散,缩放越小)。 |
| | | */ |
| | | export const getAutoScaleBySites = (sites: any[]) => { |