管理端开发日记⑤:地图航线与无人机定位

配送订单进入飞行阶段后,纯文字状态已经不够。调度人员需要在地图上同时看到接驳点、船舶位置、规划航线和无人机当前位置。本篇记录这套“让数据真正飞起来”的过程。
1. 地图数据来自哪里
订单详情页会组合订单坐标、接驳点配置、航线航点和飞行任务:
| 地图对象 | 主要来源 |
|---|---|
| 起点 | 接驳点或机巢配置 |
| 终点 | 订单中的船舶经纬度 |
| 航线 | 航线任务返回的点集合 |
| 无人机 | 飞行任务当前进度或实时位置 |
地图组件只负责画,业务层先把不同接口的字段归一成 { latitude, longitude },避免渲染函数同时认识 lat、lng、waylineLatitude 等多套命名。
function normalizePoint(point = {}) { const latitude = Number(point.latitude ?? point.lat ?? point.y) const longitude = Number(point.longitude ?? point.lng ?? point.x) return Number.isFinite(latitude) && Number.isFinite(longitude) ? { latitude, longitude } : null}2. 初始化顺序
地图初始化依赖容器尺寸,航线又依赖详情数据,因此流程必须有顺序:
loadDetail ├─ 读取订单和飞行任务 ├─ 加载天地图脚本 └─ nextTick ├─ initMap ├─ renderBaseRoute └─ startDroneAnimation地图脚本只加载一次,实例也尽量复用。如果标签页切换回来重复创建地图,会出现覆盖物叠加、监听器重复和内存增长。
3. 航线绘制与端点标记
有效航点不少于两个时绘制折线,同时把第一个点和最后一个点标记为起点、终点:
function renderWayline(points) { const validPoints = points.map(normalizePoint).filter(Boolean) if (validPoints.length < 2) return renderBaseRoute()
routePolyline = new T.Polyline( validPoints.map(p => new T.LngLat(p.longitude, p.latitude)), { color: '#1769e8', weight: 6, opacity: 0.9 } ) map.addOverLay(routePolyline) addEndpointMarker(validPoints[0], 'start') addEndpointMarker(validPoints.at(-1), 'end')}当后端暂未返回航线时,页面退化为起点到终点的基础路线,而不是直接留下一块白地图。降级展示可以让调度人员至少确认配送方向。
4. 无人机位置不是 CSS 位移
无人机图标必须是地图覆盖物,它的位置由经纬度决定。若把一个普通 div 叠在地图上,再用像素计算移动,缩放和拖动地图后就会发生悬浮偏移。
droneMarker.setLngLat( new T.LngLat(point.longitude, point.latitude))图标锚点同样重要。无人机图片较大时,应让坐标落在机身中心或吊舱位置,而不是图片左上角。地图缩放不会改变地理坐标,但错误锚点会让视觉误差更加明显。
5. 按线段长度计算动画
如果每一段都用相同时间,短线段会慢、长线段会突然加速。页面先计算总距离,再按距离分配动画时间:
const segmentDuration = totalDuration * segmentDistance / totalDistance
function interpolate(start, end, progress) { return { longitude: start.longitude + (end.longitude - start.longitude) * progress, latitude: start.latitude + (end.latitude - start.latitude) * progress }}动画帧根据飞行开始时间和预计时长计算整体进度,再找到当前所在的线段。这使刷新页面后无人机不会从起点重新飞,而是恢复到合理位置。
6. 生命周期清理
地图页面离开时必须停止动画、移除覆盖物和事件:
function stopDroneAnimation() { if (animationFrameId) cancelAnimationFrame(animationFrameId) animationFrameId = 0}
onBeforeUnmount(() => { stopDroneAnimation() map?.clearOverLays?.() map = null})不清理的典型表现是:回到列表后 CPU 仍然占用,再次进入详情出现两架无人机同时移动。
7. 本篇小结
实时位置与演示动画要分开
地图若能获取无人机实时遥测,应直接使用最新坐标并标注更新时间。按起飞时间推算的动画只能作为缺少实时数据时的视觉补充,不能让调度人员误以为那是精确位置。页面数据模型因此保留 positionSource 和 lastUpdatedAt 的概念,便于在界面上解释来源。
航线刷新
任务变化时先移除旧 polyline、marker 和动画,再渲染新航线。只在原地图上不断 addOverLay 会让旧线路残留。适配视野也不是每一帧都执行,而是在首次加载、航线切换或用户主动点击“查看全程”时执行,否则地图会不停把用户拖回自动视角。
地图相关问题最终形成了一套排查顺序:先看坐标字段,再看经纬度顺序和坐标系,然后检查覆盖物类型与锚点,最后才调整图标尺寸。这个顺序比从 CSS 开始碰运气稳定得多。
地图功能最考验“坐标世界”和“页面世界”的边界。航点属于地理坐标,图标属于覆盖物,不能拿普通 CSS 去假装地图引擎。修好以后,无人机终于老老实实贴着航线飞,不再像一张贴在显示器上的贴纸。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





























