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(已走過的路徑)拼接到總軌跡中,是最穩妥的方式。
- 這是為了防止數據推送頻率過快。如果等到動畫結束再更新,可能會出現“新的數據推送來了,但上一次動畫還沒結束,導致軌跡數據丟失或銜接不上”的問題。在
- 監聽 Marker 的
-
動畫結束清理:
- 動畫結束時 (
moveend),清理臨時繪製的輔助線,移除監聽器,防止內存泄漏。
- 動畫結束時 (
代碼詳解
1. 狀態管理與初始化
我們使用 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());
2. 核心處理函數 refreshTempPoint
這個函數負責處理單條設備數據的更新邏輯。
// 刷新設備點位與軌跡
// item: 後端返回的設備數據對象
// position: 當前最新的座標點
// type: 更新類型('init' 為初始化,其他為增量更新)
const refreshTempPoint = async (item, position, type, marker, tempOverlay, pathOverlay) => {
// 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[0], 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 && 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[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 = 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="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
);
}
// 軌跡部分,判斷是否有軌跡
}
});
// });
};
説明一下哦,我的項目中還需要實現其他功能像是手環電量展示,點擊按鈕可隱藏無人機軌跡,點擊按鈕可隱藏手環軌跡,無人機和手環軌跡起始點也需要展示一個點位圖標,軌跡線上顯示距離,還考慮了第一次進入項目,如無軌跡就只更新點位座標等等這些,無關的軌跡展示邏輯的各位觀眾老爺略過就好,這篇文章主要是分享一下實時軌跡實現的邏輯,把軌跡相關邏輯抽出來重新寫一份代碼,我嫌麻煩嘻嘻。
總結
通過結合 增量路徑計算moveAlong 軌跡回放 以及 moving 事件監聽,我們實現了一個高性能且視覺流暢的實時軌跡追蹤功能。這種方案特別適合無人機巡航、車輛實時定位等需要高頻更新位置的場景。