找回密码
 立即注册
首页 业界区 业界 高德地图实现实时轨迹展示

高德地图实现实时轨迹展示

刘凤 前天 15:20
Vue3 + 高德地图(AMap) 实现平滑的实时轨迹回放与追踪

前言

在物联网、物流监控或安防调度系统中,实时展示设备(如无人机、车辆、手环)的移动轨迹是一个常见需求。如果仅仅是简单的更新标记点位置,视觉上会出现“跳变”现象,体验很不流畅。
本文将分享如何在 Vue 3 项目中,利用 高德地图 (AMap) JS API 实现比较丝滑的实时轨效果。
核心痛点


  • 平滑移动:点位更新时,Marker 需要从旧位置平滑过渡到新位置,而不是瞬间跳过去。
  • 轨迹跟随:随着 Marker 的移动,轨迹线(Polyline)需要实时“生长”,紧跟在 Marker 后面。
  • 增量更新:后端通常返回完整的历史路径或当前状态,前端需要计算出“新增的路径段”进行动画播放。
实现逻辑解析

核心是利用高德地图 API 的轨迹回放功能。虽然官方文档提供了基础的轨迹回放示例(参考:轨迹回放示例),但官方示例通常是一次性加载完整路径(例如:先获取完整经纬度数组,渲染出浅蓝色背景线,再让小车沿着路径跑并画出浅绿色轨迹)。
我们的业务场景与官方示例的主要区别在于:
我们的路径数据是实时增量更新的。前端并没有一开始就拿到完整的路径,而是通过 WebSocket 或轮询实时获取后端返回的最新路径数据。因此,我们需要自行设计逻辑,计算出每次更新的“增量片段”,并让 Marker 平滑地走完这一段。
核心思路:


  • 前后端数据约定

    • 理想情况下,后端最好直接返回“增量路径”(即上一次位置到当前位置的坐标集合)。
    • 但在实际项目中(比如本案例),后端接口返回的是当前时刻的完整累积路径。因此,前端需要自行比对缓存的“上一次路径”和“最新路径”,计算出增量部分。

  • 状态管理 (缓存实例)

    • 使用 Map 数据结构来缓存每个设备(如无人机、手环)的 Marker(图标)和 Polyline(轨迹线)实例。
    • 确保每个设备 ID 对应唯一的地图实例,避免数据刷新时重复创建导致内存泄漏或闪烁。

  • 计算增量路径

    • 当新数据到达时,通过对比新旧路径长度,截取出新增的路径段
    • 这段新增路径就是 Marker 接下来需要“平滑移动”的轨迹。

  • 平滑动画 (moveAlong)

    • 调用高德地图的 marker.moveAlong() 方法,让 Marker 沿着新增路径平滑移动,而不是瞬间跳变。

  • 实时绘制轨迹 (moving 事件)

    • 监听 Marker 的 moving 事件。在移动过程中,实时更新轨迹线(Polyline)的路径,从而实现“边走边画”的效果。
    • 关键点:为什么要在 moving 事件中更新总轨迹,而不是在动画结束 (moveend) 后更新?

      • 这是为了防止数据推送频率过快。如果等到动画结束再更新,可能会出现“新的数据推送来了,但上一次动画还没结束,导致轨迹数据丢失或衔接不上”的问题。在 moving 过程中实时将 passedPath(已走过的路径)拼接到总轨迹中,是最稳妥的方式。


  • 动画结束清理

    • 动画结束时 (moveend),清理临时绘制的辅助线,移除监听器,防止内存泄漏。

代码详解

1. 状态管理与初始化

我们使用 Map 来管理地图上的 Marker 和 Polyline 实例。
  1. // 存储 Marker 实例 (Key: 设备ID, Value: AMap.Marker)
  2. const uavMarkers = ref(new Map());
  3. // 存储轨迹线 Polyline 实例 (Key: 设备ID, Value: AMap.Polyline)
  4. const uavPaths = ref(new Map());
复制代码
2. 核心处理函数 refreshTempPoint

这个函数负责处理单条设备数据的更新逻辑。
  1. // 刷新设备点位与轨迹
  2. // item: 后端返回的设备数据对象
  3. // position: 当前最新的坐标点
  4. // type: 更新类型('init' 为初始化,其他为增量更新)
  5. const refreshTempPoint = async (item, position, type, marker, tempOverlay, pathOverlay) => {
  6.   // 1. 清理上一轮的临时覆盖物(如临时路线、距离文本)
  7.   tempOverlay?.clearOverlays();
  8.   if (item.coordinatesLine) {
  9.     const coordinatesLine = JSON.parse(item.coordinatesLine); // 解析后端返回的完整路径数组
  10.     // --- A. 初始化起点 Marker ---
  11.     let tempMarker = tempUavMarkers.value.get(item.id);
  12.     if (!tempMarker) {
  13.       // 如果是第一次出现,渲染起点
  14.       tempMarker = renderPoint(coordinatesLine[0], item, "", pathOverlay);
  15.       tempUavMarkers.value.set(item.id, tempMarker);
  16.     }
  17.     // --- B. 获取或创建历史轨迹线 (Polyline) ---
  18.     let polyline = uavPaths.value.get(item.id);
  19.     if (!polyline) {
  20.       polyline = trajectoryLine(item, pathOverlay); // 创建新的线实例
  21.       uavPaths.value.set(item.id, polyline);
  22.     }
  23.     // 获取当前地图上已有的路径(缓存的旧路径)
  24.     const existingPath = polyline.getPath() || [];
  25.     if (type != "init") {
  26.       // --- C. 增量更新逻辑 ---
  27.       
  28.       // 1. 计算增量路径:从已有路径的最后一个点开始截取,直到最新路径的末尾
  29.       const newPathSegment = coordinatesLine.slice(
  30.         existingPath.length ? existingPath.length - 1 : 0
  31.       );
  32.       // 2. 创建一条临时的“隐形”线段,用于辅助计算或展示(视需求而定)
  33.       const newPolyline = trajectoryLine(item, tempOverlay);
  34.       // 3. 如果有新增路径,开始动画
  35.       if (newPathSegment && newPathSegment.length > 0) {
  36.         
  37.         // 监听移动过程
  38.         marker.on("moving", function (e) {
  39.           // e.passedPath 是 Marker 在当前动画片段中已经走过的路径
  40.           newPolyline.setPath(e.passedPath);
  41.          
  42.           // [关键] 实时将走过的路径拼接到历史总轨迹中
  43.           // 这样即使 WebSocket 推送频率很快,也能保证轨迹数据的连续性
  44.           polyline.setPath([...existingPath, ...e.passedPath]);
  45.         });
  46.         // 开始平滑移动
  47.         marker.moveAlong(newPathSegment, {
  48.           duration: 1000,    // 动画时长,需根据 WebSocket 推送频率调整
  49.           autoRotation: true, // 车头自动对准路径方向
  50.         });
  51.         // 监听移动结束
  52.         marker.on("moveend", function () {
  53.           // 动画结束,清理临时覆盖物
  54.           tempOverlay?.clearOverlays();
  55.          
  56.           // 更新距离文本等信息
  57.           if (item.distance) {
  58.             renderText(
  59.               coordinatesLine[Math.ceil(coordinatesLine.length - 2)],
  60.               `${item.distance}米`,
  61.               tempOverlay
  62.             );
  63.           }
  64.          
  65.           // 移除监听器,防止重复绑定
  66.           marker.off("moveend");
  67.         });
  68.       } else {
  69.         // 如果没有新增路径(位置没变),仅更新文字信息
  70.         if (item.distance) {
  71.           renderText(..., `${item.distance}米`, tempOverlay);
  72.         }
  73.       }
  74.     } else {
  75.       // --- D. 初始化逻辑 ---
  76.       // 如果是初始化加载,直接设置完整路径,不进行动画回放
  77.       if (item.distance) {
  78.          renderText(..., `${item.distance}米`, tempOverlay);
  79.       }
  80.       polyline.setPath(coordinatesLine);
  81.     }
  82.   } else {
  83.     // --- E. 无轨迹数据时的降级处理 ---
  84.     // 如果后端没有返回路径数据,直接跳变到最新位置
  85.     marker.setPosition(position);
  86.    
  87.     // 清理相关的轨迹实例和缓存
  88.     let tempMarker = tempUavMarkers.value.get(item.id);
  89.     if (tempMarker) {
  90.       tempMarker.setMap(null);
  91.       pathOverlay && pathOverlay.removeOverlay(tempMarker);
  92.       tempUavMarkers.value.delete(item.id);
  93.     }
  94.    
  95.     let polyline = uavPaths.value.get(item.id);
  96.     if (polyline) {
  97.       polyline.setMap(null);
  98.       pathOverlay && pathOverlay.removeOverlay(polyline);
  99.       uavPaths.value.delete(item.id);
  100.     }
  101.   }
  102. };
复制代码
}
};
  1. // 无人机和手环轨迹暂时
  2. const refreshAirMap = async (type, data) => {
  3.   const res = await getUavElement();
  4.   // console.log("无人机数据", res.result);
  5.   res.result.map(async (item) => {
  6.     // data.map(async (item) => {
  7.     if (item.type == "1") {
  8.       let position = JSON.parse(item.coordinates);
  9.       // 获取或创建无人机标记
  10.       let marker = uavMarkers.value.get(item.id);
  11.       if (!marker) {
  12.         // 创建无人机、手环点位。
  13.         marker = renderPoint(position, item, "", overlayGroups.value);
  14.         uavMarkers.value.set(item.id, marker);
  15.       }else{
  16.         
  17.         if (item.elementType == "2") {
  18.           // 已有点位且是手环点位就更新电量
  19.           marker.setLabel({
  20.             content: `
  21.               ${item.name}
  22.               
  23.               ${batteryHtml(item)}
  24.             `,
  25.             offset: new AMaps.value.Pixel(0, 0),
  26.             direction: "center",
  27.           });
  28.         }
  29.       }
  30.       if (item.elementType == "1") {
  31.         // 无人机轨迹
  32.         refreshTempPoint(
  33.           item,
  34.           position,
  35.           type,
  36.           marker,
  37.           pathOverlayGroups.value,
  38.           pathsStartPointOverlayGroups.value
  39.         );
  40.       } else if (item.elementType == "2") {
  41.         // 手环轨迹
  42.         refreshTempPoint(
  43.           item,
  44.           position,
  45.           type,
  46.           marker,
  47.           rescuePathsOverlayGroups.value,
  48.           rescueOverlayGroups.value
  49.         );
  50.       }
  51.       // 轨迹部分,判断是否有轨迹
  52.     }
  53.   });
  54.   // });
  55. };
复制代码
说明一下哦,我的项目中还需要实现其他功能像是手环电量展示,点击按钮可隐藏无人机轨迹,点击按钮可隐藏手环轨迹,无人机和手环轨迹起始点也需要展示一个点位图标,轨迹线上显示距离,还考虑了第一次进入项目,如无轨迹就只更新点位坐标等等这些,无关的轨迹展示逻辑的各位观众老爷略过就好,这篇文章主要是分享一下实时轨迹实现的逻辑,把轨迹相关逻辑抽出来重新写一份代码,我嫌麻烦嘻嘻。
总结

通过结合 增量路径计算moveAlong 轨迹回放 以及 moving 事件监听,我们实现了一个高性能且视觉流畅的实时轨迹追踪功能。这种方案特别适合无人机巡航、车辆实时定位等需要高频更新位置的场景。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册