在物联网、物流监控或安防调度系统中,实时展示设备(如无人机、车辆、手环)的移动轨迹是一个常见需求。如果仅仅是简单的更新标记点位置,视觉上会出现“跳变”现象,体验很不流畅。
本文将分享如何在 Vue 3 项目中,利用 高德地图 (AMap) JS API 实现比较丝滑的实时轨轨迹效果。
核心是利用高德地图 API 的轨迹回放功能。虽然官方文档提供了基础的轨迹回放示例(参考: 轨迹回放示例),但官方示例通常是一次性加载完整路径(例如:先获取完整经纬度数组,渲染出浅蓝色背景线,再让小车沿着路径跑并画出浅绿色轨迹)。
我们的业务场景与官方示例的主要区别在于:
我们的路径数据是
实时增量更新的。前端并没有一开始就拿到完整的路径,而是通过 WebSocket 或轮询实时获取后端返回的最新路径数据。因此,我们需要自行设计逻辑,计算出每次更新的“增量片段”,并让 Marker 平滑地走完这一段。
前后端数据约定:
状态管理 (缓存实例):
Map 数据结构来缓存每个设备(如无人机、手环)的
Marker(图标)和
Polyline(轨迹线)实例。计算增量路径:
平滑动画 (
moveAlong):
marker.moveAlong() 方法,让 Marker 沿着新增路径平滑移动,而不是瞬间跳变。
实时绘制轨迹 (
moving 事件):
moving 事件。在移动过程中,实时更新增量轨迹线(增量轨迹线需要新建临时轨迹线实例来表现,之前缓存过的轨迹线是照常展示)的路径,这样就实现了“边走边画”的效果。moving 事件中更新总轨迹,而不是在动画结束 (
moveend) 后更新?
moving 过程中实时将
passedPath(已走过的路径)拼接到总轨迹中然后缓存起来,作为下一次有新的增量路径时就从缓存的总轨迹中截取新增路径的基准数据,这个是比较稳妥的方式。动画结束清理:
moveend),清理临时绘制的辅助线,移除监听器,防止内存泄漏。我们使用
Map 来管理地图上的 Marker 和 Polyline 实例。
// 存储 Marker 实例 (Key: 设备ID, Value: AMap.Marker)const uavMarkers = ref(new Map());// 存储轨迹线 Polyline 实例 (Key: 设备ID, Value: AMap.Polyline)const uavPaths = ref(new Map());
refreshTempPoint这个函数负责处理单条设备数据的更新逻辑。
// 刷新设备点位与轨迹// item: 后端返回的设备数据对象// position: 当前最新的坐标点// type: 更新类型('init' 为初始化,其他为增量更新)const refreshTempPoint = async (item, position, type, marker, tempOverlay, pathOverlay) => { // 1. 清理上一轮的临时覆盖物(如临时路线、距离文本)
tempOverlay?.clearOverlays(); if (item.coordinatesLine) { const coordinatesLine = JSON.parse( tougao.firstfeed.com.cn item.coordinatesLine); // 解析后端返回的完整路径数组
// --- A. 初始化起点 Marker ---
let tempMarker = tempUavMarkers.value.get(item.id); if (!tempMarker) { // 如果是第一次出现,渲染起点
tempMarker = renderPoint(coordinatesLine[0], item, "", pathOverlay);
tempUavMarkers.value.set(item.id, tempMarker);
} // --- B. 获取或创建历史轨迹线 (Polyline) ---
let polyline = uavPaths.value.get(item.id); if (!polyline) {
polyline = 1qbdr.firstfeed.com.cn 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 && newPathSegment.length > 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[ spp.gov.cn.firstfeed.com.cn Math.ceil(coordinatesLine.length - 2)], `${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 = health.firstfeed.com.cn tempUavMarkers.value.get(item.id); if (tempMarker) {
tempMarker.setMap(null);
pathOverlay && pathOverlay.removeOverlay(tempMarker);
tempUavMarkers.value.delete(item.id);
} let polyline = uavPaths.value.get(item.id); if (polyline) {
polyline.setMap(null);
pathOverlay && pathOverlay.removeOverlay(polyline);
uavPaths.value.delete(item.id);
}
}
};
}
};
// 无人机和手环轨迹暂时const refreshAirMap = async (type, data) => { const res = await getUavElement(); // console.log("无人机数据", res.result);
res.result.map(async (item) => { // data.map(async (item) => {
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: `<div class="marker-label-container">
<div class="info-name">${item.name}</div>
<div class=" zhengfa.firstfeed.com.cn icon-placeholder"></div> ${batteryHtml(item)}
</div>`, 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
);
} // 轨迹部分,判断是否有轨迹
}
}); // });};
说明一下哦,我的项目中还需要实现其他功能像是手环电量展示,点击按钮可隐藏无人机轨迹,点击按钮可隐藏手环轨迹,无人机和手环轨迹起始点也需要展示一个点位图标,轨迹线上显示距离,还考虑了第一次进入项目,如无轨迹就只更新点位坐标等等这些,无关的轨迹展示逻辑的各位观众老爷略过就好,这篇文章主要是分享一下实时轨迹实现的逻辑,把轨迹相关逻辑抽出来重新写一份代码,我嫌麻烦嘻嘻。
1.前端第拿到点位轨迹数据
2.创建点位并缓存(如果已经创建过缓存中获取)
3.清除之前的缓存的临时轨迹(第一次渲染是没有临时轨迹的)
4.创建轨迹线 polyline并缓存(如果已经创建就缓存中获取)
5.根据缓存的轨迹线对比当前轨迹数据计算新增路径(如果没有缓存那当前轨迹数据就是新增路径)
6.创建临时轨迹实例通过
moveAlong渲染出增量路径轨迹(放在临时图层,每次渲染轨迹前清除,因为只需要用一次)
7.通过
moving监听临时轨迹走过的路径并添加到之前创建(缓存)的 polyline (说明: polyline 在有轨迹的时间段是一直展示的,现在 polyline 就会有和临时轨迹有部分是重叠的,重叠部分就是临时轨迹部分,等到下一次渲染轨迹,清除了临时轨迹就只展示 polyline,然后从 polyline 末端开始又一轮增量路径的轨迹)
8.轨迹移动结束
moveend,添加业务逻辑。
结合 增量路径计算
moveAlong 轨迹回放 以及
moving 事件监听,我们实现了一个高性能且视觉流畅的实时轨迹追踪功能。这种方案特别适合无人机巡航、车辆实时定位等需要高频更新位置的场景。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70048949/viewspace-3109423/ ,如需转载,请注明出处,否则将追究法律责任。