| | |
| | | import Taro from '@tarojs/taro'; |
| | | import { useCallback, useEffect, useRef, useState } from 'react'; |
| | | import { getNearbyVehicles } from '../../../../robotaxi/api/home'; |
| | | import { getDistance } from '../../../../utils/Robotaxi-public-method'; |
| | | import { HOME_MAP_ID } from '../constants'; |
| | | |
| | | type Deps = { |
| | | isOperatingArea: boolean; |
| | |
| | | const SETTLE_DELAY_MS = 500; // 地图停止后的静止判定时间(毫秒) |
| | | const MOVE_THRESHOLD_METERS = 500; // 触发新搜索的位移阈值(米) |
| | | const ANIMATION_DURATION_MS = 4500; // 车辆位移动画时长(毫秒) |
| | | const ANIMATION_FRAME_MS = 120; // 动画帧间隔(毫秒) |
| | | const VEHICLE_MARKER_ID_BASE = 900000000; // 车辆 marker id 起始段 |
| | | const VEHICLE_MARKER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/marker-car.png'; // 车辆 icon 地址 |
| | | |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 比较两批车辆集合是否存在新增或删除。 |
| | | */ |
| | | const hasVehicleCollectionChanged = ( |
| | | currentVehicleMap: Map<string, NearbyVehicleItem>, |
| | | nextVehicles: NearbyVehicleItem[] |
| | | ) => { |
| | | if (currentVehicleMap.size !== nextVehicles.length) { |
| | | return true; |
| | | } |
| | | return nextVehicles.some((vehicle) => !currentVehicleMap.has(vehicle.id)); |
| | | }; |
| | | |
| | | /** |
| | | * 规范化接口返回,过滤非法经纬度。 |
| | | */ |
| | | const normalizeVehicleList = (response: any): NearbyVehicleItem[] => { |
| | |
| | | const [vehicleMarkers, setVehicleMarkers] = useState<any[]>([]); // 车辆 marker 列表 |
| | | const pollTimerRef = useRef<any>(null); // 轮询定时器句柄 |
| | | const settleTimerRef = useRef<any>(null); // 停稳判定定时器句柄 |
| | | const animationTimerRef = useRef<any>(null); // 动画帧定时器句柄 |
| | | const animationSyncTimerRef = useRef<any>(null); // 动画结束后同步 React marker 的定时器 |
| | | const requestLockRef = useRef(false); // 请求并发锁,防止重入 |
| | | const latestSeqRef = useRef(0); // 请求序号,防止旧响应覆盖新状态 |
| | | const activeSearchCenterRef = useRef<NearbyCenter | null>(null); // 当前轮询中心点 |
| | | const lastSearchCenterRef = useRef<NearbyCenter | null>(null); // 上一次发请求的中心点 |
| | | const renderedVehicleMapRef = useRef<Map<string, NearbyVehicleItem>>(new Map()); // 当前渲染中的车辆 |
| | | const animationStateRef = useRef<{ startAt: number; endAt: number; tracks: VehicleTrackItem[] } | null>(null); // 当前动画轨迹状态 |
| | | const mapContextRef = useRef<any>(null); // 首页地图上下文,负责原生 marker 平移 |
| | | const fetchAndAnimateVehiclesRef = useRef<(center: NearbyCenter) => Promise<void>>(async () => {}); |
| | | const ensurePollingRef = useRef<() => void>(() => {}); |
| | | const activateCenterAndFetchRef = useRef<(center: NearbyCenter) => void>(() => {}); |
| | | const disposeVehiclesRef = useRef<() => void>(() => {}); |
| | | |
| | | /** |
| | | * 清理动画定时器。 |
| | | * 获取首页地图上下文。 |
| | | */ |
| | | const clearAnimationTimer = () => { |
| | | if (animationTimerRef.current) { |
| | | clearTimeout(animationTimerRef.current); |
| | | animationTimerRef.current = null; |
| | | const getMapContext = () => { |
| | | if (!mapContextRef.current) { |
| | | mapContextRef.current = Taro.createMapContext(HOME_MAP_ID); |
| | | } |
| | | animationStateRef.current = null; |
| | | return mapContextRef.current; |
| | | }; |
| | | |
| | | /** |
| | | * 清理动画同步定时器。 |
| | | */ |
| | | const clearAnimationSyncTimer = () => { |
| | | if (animationSyncTimerRef.current) { |
| | | clearTimeout(animationSyncTimerRef.current); |
| | | animationSyncTimerRef.current = null; |
| | | } |
| | | }; |
| | | |
| | | /** |
| | |
| | | const clearVehicles = () => { |
| | | renderedVehicleMapRef.current = new Map(); |
| | | setVehicleMarkers([]); |
| | | clearAnimationTimer(); |
| | | clearAnimationSyncTimer(); |
| | | }; |
| | | |
| | | /** |
| | |
| | | }, []); |
| | | |
| | | /** |
| | | * 基于插值轨迹推进车辆动画帧。 |
| | | * 使用原生地图能力平移已有车辆 marker。 |
| | | */ |
| | | const playAnimationFrame = () => { |
| | | const animationState = animationStateRef.current; |
| | | if (!animationState) return; |
| | | const now = Date.now(); |
| | | const progress = Math.max(0, Math.min(1, (now - animationState.startAt) / Math.max(1, animationState.endAt - animationState.startAt))); |
| | | const nextVehicleMap = new Map<string, NearbyVehicleItem>(); |
| | | animationState.tracks.forEach((track) => { |
| | | const lat = track.fromLat + (track.toLat - track.fromLat) * progress; |
| | | const lon = track.fromLon + (track.toLon - track.fromLon) * progress; |
| | | const direction = interpolateDirection(track.fromDirection, track.toDirection, progress); |
| | | nextVehicleMap.set(track.id, { |
| | | id: track.id, |
| | | latitude: lat, |
| | | longitude: lon, |
| | | direction |
| | | const translateVehicleMarkers = (tracks: VehicleTrackItem[]) => { |
| | | const mapContext = getMapContext(); |
| | | tracks.forEach((track) => { |
| | | const markerId = hashToMarkerId(track.id); |
| | | (mapContext as any).translateMarker?.({ |
| | | markerId, |
| | | destination: { |
| | | latitude: track.toLat, |
| | | longitude: track.toLon |
| | | }, |
| | | duration: ANIMATION_DURATION_MS, |
| | | autoRotate: false, |
| | | rotate: interpolateDirection(track.fromDirection, track.toDirection, 1) |
| | | }); |
| | | }); |
| | | renderedVehicleMapRef.current = nextVehicleMap; |
| | | setVehicleMarkers(buildVehicleMarkers(Array.from(nextVehicleMap.values()))); |
| | | if (progress >= 1) { |
| | | animationStateRef.current = null; |
| | | animationTimerRef.current = null; |
| | | return; |
| | | } |
| | | animationTimerRef.current = setTimeout(() => { |
| | | playAnimationFrame(); |
| | | }, ANIMATION_FRAME_MS); |
| | | }; |
| | | |
| | | /** |
| | |
| | | clearVehicles(); |
| | | return; |
| | | } |
| | | const now = Date.now(); |
| | | const currentVehicleMap = renderedVehicleMapRef.current; |
| | | const collectionChanged = hasVehicleCollectionChanged(currentVehicleMap, normalizedVehicles); |
| | | const nextVehicleMap = new Map<string, NearbyVehicleItem>( |
| | | normalizedVehicles.map((vehicle) => [vehicle.id, vehicle]) |
| | | ); |
| | | if (currentVehicleMap.size === 0 || collectionChanged) { |
| | | clearAnimationSyncTimer(); |
| | | renderedVehicleMapRef.current = nextVehicleMap; |
| | | setVehicleMarkers(buildVehicleMarkers(normalizedVehicles)); |
| | | return; |
| | | } |
| | | const tracks: VehicleTrackItem[] = normalizedVehicles.map((vehicle) => { |
| | | const currentVehicle = currentVehicleMap.get(vehicle.id); |
| | | return { |
| | |
| | | toDirection: vehicle.direction |
| | | }; |
| | | }); |
| | | clearAnimationTimer(); |
| | | animationStateRef.current = { |
| | | startAt: now, |
| | | endAt: now + ANIMATION_DURATION_MS, |
| | | tracks |
| | | }; |
| | | playAnimationFrame(); |
| | | renderedVehicleMapRef.current = nextVehicleMap; |
| | | translateVehicleMarkers(tracks); |
| | | clearAnimationSyncTimer(); |
| | | animationSyncTimerRef.current = setTimeout(() => { |
| | | if (seq !== latestSeqRef.current) { |
| | | return; |
| | | } |
| | | setVehicleMarkers(buildVehicleMarkers(Array.from(nextVehicleMap.values()))); |
| | | }, ANIMATION_DURATION_MS); |
| | | } catch (error) { |
| | | // 请求异常时保持当前车辆状态,避免闪断。 |
| | | } finally { |