谦虚中国人 發表於 2026-1-14 14:35:00

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

<h1 id="vue3--高德地图amap-实现平滑的实时轨迹展示">Vue3 + 高德地图(AMap) 实现平滑的实时轨迹展示</h1>
<h2 id="前言">前言</h2>
<p>在物联网、物流监控或安防调度系统中,实时展示设备(如无人机、车辆、手环)的移动轨迹是一个常见需求。如果仅仅是简单的更新标记点位置,视觉上会出现“跳变”现象,体验很不流畅。</p>
<p>本文将分享如何在 <strong>Vue 3</strong> 项目中,利用 <strong>高德地图 (AMap) JS API</strong> 实现比较丝滑的实时轨轨迹效果。</p>
<h2 id="核心痛点">核心痛点</h2>
<ol>
<li><strong>平滑移动</strong>:点位更新时,Marker 需要从旧位置平滑过渡到新位置,而不是瞬间跳过去。</li>
<li><strong>轨迹跟随</strong>:随着 Marker 的移动,轨迹线(Polyline)需要实时“生长”,紧跟在 Marker 后面。</li>
<li><strong>增量更新</strong>:后端通常返回完整的历史路径或当前状态,前端需要计算出“新增的路径段”进行动画播放。</li>
</ol>
<h2 id="实现逻辑解析">实现逻辑解析</h2>
<p>核心是利用高德地图 API 的轨迹回放功能。虽然官方文档提供了基础的轨迹回放示例(参考:轨迹回放示例),但官方示例通常是一次性加载完整路径(例如:先获取完整经纬度数组,渲染出浅蓝色背景线,再让小车沿着路径跑并画出浅绿色轨迹)。</p>
<p><strong>我们的业务场景与官方示例的主要区别在于:</strong><br>
我们的路径数据是<strong>实时增量更新</strong>的。前端并没有一开始就拿到完整的路径,而是通过 WebSocket 或轮询实时获取后端返回的最新路径数据。因此,我们需要自行设计逻辑,计算出每次更新的“增量片段”,并让 Marker 平滑地走完这一段。</p>
<h3 id="核心思路">核心思路:</h3>
<ol>
<li>
<p><strong>前后端数据约定</strong>:</p>
<ul>
<li>理想情况下,后端最好直接返回“增量路径”(即上一次位置到当前位置的坐标集合)。</li>
<li>但在实际项目中(比如本案例),后端接口返回的是<strong>当前时刻的完整累积路径</strong>。因此,前端需要自行比对缓存的“上一次路径”和“最新路径”,计算出增量部分。</li>
</ul>
</li>
<li>
<p><strong>状态管理 (缓存实例)</strong>:</p>
<ul>
<li>使用 <code>Map</code> 数据结构来缓存每个设备(如无人机、手环)的 <code>Marker</code>(图标)和 <code>Polyline</code>(轨迹线)实例。</li>
<li>确保每个设备 ID 对应唯一的地图实例,避免数据刷新时重复创建导致内存泄漏或闪烁。</li>
</ul>
</li>
<li>
<p><strong>计算增量路径</strong>:</p>
<ul>
<li>当新数据到达时,通过对比新旧路径长度,截取出<strong>新增的路径段</strong>。</li>
<li>这段新增路径就是 Marker 接下来需要“平滑移动”的轨迹。</li>
</ul>
</li>
<li>
<p><strong>平滑动画 (<code>moveAlong</code>)</strong>:</p>
<ul>
<li>调用高德地图的 <code>marker.moveAlong()</code> 方法,让 Marker 沿着新增路径平滑移动,而不是瞬间跳变。</li>
</ul>
</li>
<li>
<p><strong>实时绘制轨迹 (<code>moving</code> 事件)</strong>:</p>
<ul>
<li>监听 Marker 的 <code>moving</code> 事件。在移动过程中,实时更新增量轨迹线(增量轨迹线需要新建临时轨迹线实例来表现,之前缓存过的轨迹线是照常展示)的路径,这样就实现了“边走边画”的效果。</li>
<li><strong>关键点</strong>:为什么要在 <code>moving</code> 事件中更新总轨迹,而不是在动画结束 (<code>moveend</code>) 后更新?
<ul>
<li>这是为了防止数据推送频率过快。如果等到动画结束再更新,可能会出现“新的数据推送来了,但上一次动画还没结束,导致轨迹数据丢失或衔接不上”的问题。在 <code>moving</code> 过程中实时将 <code>passedPath</code>(已走过的路径)拼接到总轨迹中然后缓存起来,作为下一次有新的增量路径时就从缓存的总轨迹中截取新增路径的基准数据,这个是比较稳妥的方式。</li>
</ul>
</li>
<li>注意:每次进行轨迹渲染的时候要把之前的临时增量路径的轨迹线清除,还有之前的缓存的总轨迹线要展示,我是把临时增量路径的轨迹线放在自己定义的 tempOverlay 图层中,而把总轨迹线放在 pathOverlay 图层中。</li>
</ul>
</li>
<li>
<p><strong>动画结束清理</strong>:</p>
<ul>
<li>动画结束时 (<code>moveend</code>),清理临时绘制的辅助线,移除监听器,防止内存泄漏。</li>
</ul>
</li>
</ol>
<hr>
<h3 id="代码详解">代码详解</h3>
<h4 id="1-状态管理与初始化">1. 状态管理与初始化</h4>
<p>我们使用 <code>Map</code> 来管理地图上的 Marker 和 Polyline 实例。</p>
<pre><code class="language-javascript">// 存储 Marker 实例 (Key: 设备ID, Value: AMap.Marker)
const uavMarkers = ref(new Map());
// 存储轨迹线 Polyline 实例 (Key: 设备ID, Value: AMap.Polyline)
const uavPaths = ref(new Map());
</code></pre>
<h4 id="2-核心处理函数-refreshtemppoint">2. 核心处理函数 <code>refreshTempPoint</code></h4>
<p>这个函数负责处理单条设备数据的更新逻辑。</p>
<pre><code class="language-javascript">// 刷新设备点位与轨迹
// item: 后端返回的设备数据对象
// position: 当前最新的坐标点
// type: 更新类型('init' 为初始化,其他为增量更新)
const refreshTempPoint = async (item, position, type, marker, tempOverlay, pathOverlay) =&gt; {
// 1. 清理上一轮的临时覆盖物(如临时路线、距离文本)
tempOverlay?.clearOverlays();

if (item.coordinatesLine) {
    const coordinatesLine = JSON.parse(item.coordinatesLine); // 解析后端返回的完整路径数组

    // --- A. 初始化起点 Marker ---
    let tempMarker = tempUavMarkers.value.get(item.id);
    if (!tempMarker) {
      // 如果是第一次出现,渲染起点
      tempMarker = renderPoint(coordinatesLine, item, "", pathOverlay);
      tempUavMarkers.value.set(item.id, tempMarker);
    }

    // --- B. 获取或创建历史轨迹线 (Polyline) ---
    let polyline = uavPaths.value.get(item.id);
    if (!polyline) {
      polyline = trajectoryLine(item, pathOverlay); // 创建新的线实例
      uavPaths.value.set(item.id, polyline);
    }

    // 获取当前地图上已有的路径(缓存的旧路径)
    const existingPath = polyline.getPath() || [];

    if (type != "init") {
      // --- C. 增量更新逻辑 ---

      // 1. 计算增量路径:从已有路径的最后一个点开始截取,直到最新路径的末尾
      const newPathSegment = coordinatesLine.slice(
      existingPath.length ? existingPath.length - 1 : 0
      );

      // 2. 创建一条临时的”线段,用于展示增量路径
      const newPolyline = trajectoryLine(item, tempOverlay);

      // 3. 如果有新增路径,开始动画
      if (newPathSegment &amp;&amp; newPathSegment.length &gt; 0) {

      // 监听移动过程
      marker.on("moving", function (e) {
          // e.passedPath 是 Marker 在当前动画片段中已经走过的路径
          newPolyline.setPath(e.passedPath);

          // [关键] 实时将走过的路径拼接到历史总轨迹中
          // 这样即使 WebSocket 推送频率很快,也能保证轨迹数据的连续性
          polyline.setPath([...existingPath, ...e.passedPath]);
      });

      // 开始平滑移动
      marker.moveAlong(newPathSegment, {
          duration: 1000,    // 动画时长,需根据 WebSocket 推送频率调整
          autoRotation: true, // 车头自动对准路径方向
      });

      // 监听移动结束
      marker.on("moveend", function () {
          // 动画结束,清理临时覆盖物
          tempOverlay?.clearOverlays();

          // 更新距离文本等信息
          if (item.distance) {
            renderText(
            coordinatesLine,
            `${item.distance}米`,
            tempOverlay
            );
          }

          // 移除监听器,防止重复绑定
          marker.off("moveend");
      });
      } else {
      // 如果没有新增路径(位置没变),仅更新文字信息
      if (item.distance) {
          renderText(..., `${item.distance}米`, tempOverlay);
      }
      }

    } else {
      // --- D. 初始化逻辑 ---
      // 如果是初始化加载,直接设置完整路径,不进行动画回放
      if (item.distance) {
         renderText(..., `${item.distance}米`, tempOverlay);
      }
      polyline.setPath(coordinatesLine);
    }
} else {
    // --- E. 无轨迹数据时的降级处理 ---
    // 如果后端没有返回路径数据,直接跳变到最新位置
    marker.setPosition(position);

    // 清理相关的轨迹实例和缓存
    let tempMarker = tempUavMarkers.value.get(item.id);
    if (tempMarker) {
      tempMarker.setMap(null);
      pathOverlay &amp;&amp; pathOverlay.removeOverlay(tempMarker);
      tempUavMarkers.value.delete(item.id);
    }

    let polyline = uavPaths.value.get(item.id);
    if (polyline) {
      polyline.setMap(null);
      pathOverlay &amp;&amp; pathOverlay.removeOverlay(polyline);
      uavPaths.value.delete(item.id);
    }
}
};
</code></pre>
<p>}<br>
};</p>
<pre><code class="language-javascript">// 无人机和手环轨迹暂时
const refreshAirMap = async (type, data) =&gt; {
const res = await getUavElement();
// console.log("无人机数据", res.result);
res.result.map(async (item) =&gt; {
    // data.map(async (item) =&gt; {
    if (item.type == "1") {
      let position = JSON.parse(item.coordinates);
      // 获取或创建无人机标记
      let marker = uavMarkers.value.get(item.id);
      if (!marker) {
      // 创建无人机、手环点位。
      marker = renderPoint(position, item, "", overlayGroups.value);
      uavMarkers.value.set(item.id, marker);
      } else {
      if (item.elementType == "2") {
          // 已有点位且是手环点位就更新电量
          marker.setLabel({
            content: `&lt;div class="marker-label-container"&gt;
            &lt;div class="info-name"&gt;${item.name}&lt;/div&gt;
            &lt;div class="icon-placeholder"&gt;&lt;/div&gt;
            ${batteryHtml(item)}
            &lt;/div&gt;`,
            offset: new AMaps.value.Pixel(0, 0),
            direction: "center",
          });
      }
      }

      if (item.elementType == "1") {
      // 无人机轨迹
      refreshTempPoint(
          item,
          position,
          type,
          marker,
          pathOverlayGroups.value,
          pathsStartPointOverlayGroups.value
      );
      } else if (item.elementType == "2") {
      // 手环轨迹
      refreshTempPoint(
          item,
          position,
          type,
          marker,
          rescuePathsOverlayGroups.value,
          rescueOverlayGroups.value
      );
      }
      // 轨迹部分,判断是否有轨迹
    }
});
// });
};
</code></pre>
<p>说明一下哦,我的项目中还需要实现其他功能像是手环电量展示,点击按钮可隐藏无人机轨迹,点击按钮可隐藏手环轨迹,无人机和手环轨迹起始点也需要展示一个点位图标,轨迹线上显示距离,还考虑了第一次进入项目,如无轨迹就只更新点位坐标等等这些,无关的轨迹展示逻辑的各位观众老爷略过就好,这篇文章主要是分享一下实时轨迹实现的逻辑,把轨迹相关逻辑抽出来重新写一份代码,我嫌麻烦嘻嘻。</p>
<h3 id="完整执行逻辑">完整执行逻辑:</h3>
<p>1.前端第拿到点位轨迹数据<br>
2.创建点位并缓存(如果已经创建过缓存中获取)<br>
3.清除之前的缓存的临时轨迹(第一次渲染是没有临时轨迹的)<br>
4.创建轨迹线 polyline并缓存(如果已经创建就缓存中获取)<br>
5.根据缓存的轨迹线对比当前轨迹数据计算新增路径(如果没有缓存那当前轨迹数据就是新增路径)<br>
6.创建临时轨迹实例通过<code>moveAlong</code>渲染出增量路径轨迹(放在临时图层,每次渲染轨迹前清除,因为只需要用一次)<br>
7.通过<code>moving</code>监听临时轨迹走过的路径并添加到之前创建(缓存)的 polyline (说明: polyline 在有轨迹的时间段是一直展示的,现在 polyline 就会有和临时轨迹有部分是重叠的,重叠部分就是临时轨迹部分,等到下一次渲染轨迹,清除了临时轨迹就只展示 polyline,然后从 polyline 末端开始又一轮增量路径的轨迹)<br>
8.轨迹移动结束<code>moveend</code>,添加业务逻辑。</p>
<h2 id="总结">总结</h2>
<p>结合 增量路径计算<code>moveAlong</code> 轨迹回放 以及 <code>moving</code> 事件监听,我们实现了一个高性能且视觉流畅的实时轨迹追踪功能。这种方案特别适合无人机巡航、车辆实时定位等需要高频更新位置的场景。</p><br><br>
来源:https://www.cnblogs.com/lijinhuaboke/p/19457121
頁: [1]
查看完整版本: 高德地图实现实时轨迹展示