博客 / 詳情

返回

高德地圖實現實時軌跡展示

Vue3 + 高德地圖(AMap) 實現平滑的實時軌跡回放與追蹤

前言

在物聯網、物流監控或安防調度系統中,實時展示設備(如無人機、車輛、手環)的移動軌跡是一個常見需求。如果僅僅是簡單的更新標記點位置,視覺上會出現“跳變”現象,體驗很不流暢。

本文將分享如何在 Vue 3 項目中,利用 高德地圖 (AMap) JS API 實現比較絲滑的實時軌效果。

核心痛點

  1. 平滑移動:點位更新時,Marker 需要從舊位置平滑過渡到新位置,而不是瞬間跳過去。
  2. 軌跡跟隨:隨着 Marker 的移動,軌跡線(Polyline)需要實時“生長”,緊跟在 Marker 後面。
  3. 增量更新:後端通常返回完整的歷史路徑或當前狀態,前端需要計算出“新增的路徑段”進行動畫播放。

實現邏輯解析

核心是利用高德地圖 API 的軌跡回放功能。雖然官方文檔提供了基礎的軌跡回放示例(參考:軌跡回放示例),但官方示例通常是一次性加載完整路徑(例如:先獲取完整經緯度數組,渲染出淺藍色背景線,再讓小車沿着路徑跑並畫出淺綠色軌跡)。

我們的業務場景與官方示例的主要區別在於:
我們的路徑數據是實時增量更新的。前端並沒有一開始就拿到完整的路徑,而是通過 WebSocket 或輪詢實時獲取後端返回的最新路徑數據。因此,我們需要自行設計邏輯,計算出每次更新的“增量片段”,並讓 Marker 平滑地走完這一段。

核心思路:

  1. 前後端數據約定

    • 理想情況下,後端最好直接返回“增量路徑”(即上一次位置到當前位置的座標集合)。
    • 但在實際項目中(比如本案例),後端接口返回的是當前時刻的完整累積路徑。因此,前端需要自行比對緩存的“上一次路徑”和“最新路徑”,計算出增量部分。
  2. 狀態管理 (緩存實例)

    • 使用 Map 數據結構來緩存每個設備(如無人機、手環)的 Marker(圖標)和 Polyline(軌跡線)實例。
    • 確保每個設備 ID 對應唯一的地圖實例,避免數據刷新時重複創建導致內存泄漏或閃爍。
  3. 計算增量路徑

    • 當新數據到達時,通過對比新舊路徑長度,截取出新增的路徑段
    • 這段新增路徑就是 Marker 接下來需要“平滑移動”的軌跡。
  4. 平滑動畫 (moveAlong)

    • 調用高德地圖的 marker.moveAlong() 方法,讓 Marker 沿着新增路徑平滑移動,而不是瞬間跳變。
  5. 實時繪製軌跡 (moving 事件)

    • 監聽 Marker 的 moving 事件。在移動過程中,實時更新軌跡線(Polyline)的路徑,從而實現“邊走邊畫”的效果。
    • 關鍵點:為什麼要在 moving 事件中更新總軌跡,而不是在動畫結束 (moveend) 後更新?
      • 這是為了防止數據推送頻率過快。如果等到動畫結束再更新,可能會出現“新的數據推送來了,但上一次動畫還沒結束,導致軌跡數據丟失或銜接不上”的問題。在 moving 過程中實時將 passedPath(已走過的路徑)拼接到總軌跡中,是最穩妥的方式。
  6. 動畫結束清理

    • 動畫結束時 (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 事件監聽,我們實現了一個高性能且視覺流暢的實時軌跡追蹤功能。這種方案特別適合無人機巡航、車輛實時定位等需要高頻更新位置的場景。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.