最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

高德地圖與Three.js?3D模型集成超詳細(xì)指南

 更新時(shí)間:2025年08月05日 09:37:11   作者:狗八先生~~  
Three.js是一個(gè)簡(jiǎn)化WebGL開發(fā)的JavaScript庫(kù),提供易用API創(chuàng)建3D圖形,這篇文章主要介紹了高德地圖與Three.js?3D模型集成的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下

1. 基礎(chǔ)概念與環(huán)境準(zhǔn)備

1.1 必要的庫(kù)與導(dǎo)入

在使用3D模型前,需要導(dǎo)入必要的庫(kù)和加載器:

import AMapLoader from "@amap/amap-jsapi-loader";
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';

說明

  • AMapLoader: 用于加載高德地圖API

  • OBJLoader: Three.js提供的OBJ模型加載器,用于加載3D模型的幾何數(shù)據(jù)

  • MTLLoader: Three.js提供的材質(zhì)加載器,用于加載模型的材質(zhì)和紋理

2. 地圖初始化與3D層配置

2.1 初始化高德地圖

async initMap() {
  try {
    this.AMaper = await AMapLoader.load({
      // 這里寫你的 web key
      key: "a94e7a66ed28a3d1d095868ad1bec1c3",
      plugins: [""],
    });
    this.map = new this.AMaper.Map("container", {
      viewMode: '3D',
      showBuildingBlock: false,
      center: [122.037275, 37.491067],
      pitch: 60,
      zoom: 17
    });
    // 初始化光線
    this.map.AmbientLight = new this.AMaper.Lights.AmbientLight([1, 1, 1], 1);
    this.map.DirectionLight = new this.AMaper.Lights.DirectionLight([1, 0, 1], [1, 1, 1], 1);
  } catch (e) {
    console.log(e);
  }
}

說明

  • 設(shè)置viewMode: '3D'啟用3D視圖

  • pitch: 60設(shè)置地圖的俯仰角度,使地圖呈現(xiàn)3D效果

  • 通過AmbientLightDirectionLight設(shè)置場(chǎng)景光照,對(duì)3D模型的顯示效果至關(guān)重要

3. 3D模型加載

3.1 創(chuàng)建3D層

async loadModel() {
  this.isLoadingModels = true;
  try {
    this.object3Dlayer = new this.AMaper.Object3DLayer();
    this.map.add(this.object3Dlayer);
    // ...

說明

  • Object3DLayer是高德地圖提供的3D對(duì)象層,所有3D模型都需要添加到此層上

  • this.map.add(this.object3Dlayer)將3D層添加到地圖中

3.2 加載材質(zhì)和模型文件

    const materials = await new Promise((resolve, reject) => {
      new MTLLoader().load('http://localhost:9000/whgt/test.mtl', resolve, null, reject);
    });
?
    const event = await new Promise((resolve, reject) => {
      new OBJLoader().setMaterials(materials).load('http://localhost:9000/whgt/test.obj', resolve, null, reject);
    });
?
    this.vehicleBaseEvent = event;

說明

  • 首先加載MTL材質(zhì)文件,它定義了3D模型的外觀、顏色、反光等屬性

  • 然后加載OBJ模型文件,并將已加載的材質(zhì)應(yīng)用到模型上

  • 使用Promise包裝加載過程,實(shí)現(xiàn)異步加載

  • 將加載好的模型保存在vehicleBaseEvent中,以便后續(xù)使用

4. 創(chuàng)建3D網(wǎng)格(Mesh)

4.1 異步創(chuàng)建新的網(wǎng)格

async createMesh() {
  const materials = await new MTLLoader().loadAsync('http://localhost:9000/whgt/Electric_G_Power_Vehi_0411010527_texture.mtl');
  const event = await new OBJLoader().setMaterials(materials).loadAsync('http://localhost:9000/whgt/Electric_G_Power_Vehi_0411010527_texture.obj');
  return this.createMeshFromEvent(event);
}

說明

  • 這個(gè)函數(shù)用于創(chuàng)建新的3D網(wǎng)格,特別是用于創(chuàng)建車輛的3D模型

  • loadAsync方法是現(xiàn)代Promise寫法,簡(jiǎn)化了加載過程

4.2 從加載事件創(chuàng)建網(wǎng)格對(duì)象

createMeshFromEvent(event) {
  const meshes = event.children;
  const createdMeshes = [];
?
  // 第一步:計(jì)算所有網(wǎng)格的Y軸最大值(在反向坐標(biāo)系中,最大Y對(duì)應(yīng)模型底部)
  let maxY = -Infinity;
  for (let i = 0; i < meshes.length; i++) {
    const meshData = meshes[i];
    const vecticesF3 = meshData.geometry.attributes.position;
    const vectexCount = vecticesF3.count;
?
    for (let j = 0; j < vectexCount; j++) {
      const s = j * 3;
      // 在高德地圖坐標(biāo)系中Y軸是反向的
      const y = -vecticesF3.array[s + 1];
      if (y > maxY) {
        maxY = y;
      }
    }
  }
?
  // 計(jì)算Y軸偏移量,使底部對(duì)齊原點(diǎn)
  // 在反向坐標(biāo)系中,使用maxY作為偏移而不是minY
  const yOffset = -maxY;
?
  // 第二步:創(chuàng)建幾何體并應(yīng)用偏移
  for (let i = 0; i < meshes.length; i++) {
    const meshData = meshes[i];
    const vecticesF3 = meshData.geometry.attributes.position;
    const vecticesNormal3 = meshData.geometry.attributes.normal;
    const vecticesUV2 = meshData.geometry.attributes.uv;
    const vectexCount = vecticesF3.count;
?
    const mesh = new this.AMaper.Object3D.MeshAcceptLights();
    const geometry = mesh.geometry;
    const material = meshData.material[0] || meshData.material;
    const c = material.color;
    const opacity = material.opacity;
?
    for (let j = 0; j < vectexCount; j++) {
      const s = j * 3;
      geometry.vertices.push(
        vecticesF3.array[s],
        vecticesF3.array[s + 2],
        -vecticesF3.array[s + 1] + yOffset // 添加Y軸偏移量
      );
      if (vecticesNormal3) {
        geometry.vertexNormals.push(
          vecticesNormal3.array[s],
          vecticesNormal3.array[s + 2],
          -vecticesNormal3.array[s + 1]
        );
      }
      if (vecticesUV2) {
        geometry.vertexUVs.push(
          vecticesUV2.array[j * 2],
          1 - vecticesUV2.array[j * 2 + 1]
        );
      }
      geometry.vertexColors.push(c.r, c.g, c.b, opacity);
    }
?
    if (material.map) {
      mesh.textures.push(require('@/assets/3d/Electric_G_Power_Vehi_0411010527_texture.png'));
    }
    mesh.DEPTH_TEST = material.depthTest;
    mesh.transparent = opacity < 1;
    mesh.scale(200, 200, 200);
?
    createdMeshes.push(mesh);
  }
  return createdMeshes;
}

關(guān)鍵概念解析

  1. 網(wǎng)格結(jié)構(gòu)

    • 每個(gè)3D模型由多個(gè)網(wǎng)格組成,存儲(chǔ)在event.children

    • 每個(gè)網(wǎng)格包含幾何數(shù)據(jù)和材質(zhì)數(shù)據(jù)

  2. 坐標(biāo)系調(diào)整

    • Three.js和高德地圖使用不同的坐標(biāo)系

    • 代碼中計(jì)算Y軸最大值并應(yīng)用偏移,確保模型正確放置

  3. 網(wǎng)格數(shù)據(jù)處理

    • vecticesF3:頂點(diǎn)位置數(shù)據(jù)

    • vecticesNormal3:法線數(shù)據(jù),用于光照計(jì)算

    • vecticesUV2:UV貼圖坐標(biāo),用于紋理映射

  4. 創(chuàng)建高德地圖兼容的網(wǎng)格

    • 使用AMaper.Object3D.MeshAcceptLights()創(chuàng)建可接收光照的網(wǎng)格

    • 轉(zhuǎn)換并添加頂點(diǎn)、法線和UV數(shù)據(jù)

    • 設(shè)置材質(zhì)顏色和透明度

  5. 應(yīng)用紋理和縮放

    • 如果材質(zhì)有貼圖,添加紋理

    • 設(shè)置深度測(cè)試和透明度屬性

    • 應(yīng)用縮放(mesh.scale(200, 200, 200))使模型適合地圖比例

5. 模型管理與動(dòng)畫

5.1 模型位置與旋轉(zhuǎn)更新

// 添加動(dòng)畫循環(huán)方法
startAnimationLoop() {
  const animate = () => {
    this.updateVehicleAnimations();
    this.animationFrameId = requestAnimationFrame(animate);
  };
  this.animationFrameId = requestAnimationFrame(animate);
}
?
// 更新車輛動(dòng)畫狀態(tài)
updateVehicleAnimations() {
  const now = Date.now();
?
  Object.keys(this.vehiclePositions).forEach(vehicleId => {
    const positionData = this.vehiclePositions[vehicleId];
?
    // 如果沒有正在進(jìn)行的動(dòng)畫或者動(dòng)畫已經(jīng)完成,則跳過
    if (!positionData || !positionData.animating) {
      return;
    }
?
    // 計(jì)算動(dòng)畫進(jìn)度
    const elapsed = now - positionData.startTime;
    const duration = positionData.duration;
    let progress = Math.min(elapsed / duration, 1);
?
    // 如果動(dòng)畫已經(jīng)完成
    if (progress >= 1) {
      // 設(shè)置到最終位置
      const meshes = this.vehicleMeshes[vehicleId];
      if (meshes) {
        meshes.forEach(mesh => {
          mesh.position(new this.AMaper.LngLat(
            positionData.targetLng,
            positionData.targetLat,
            positionData.targetAlt
          ));
        });
      }
?
      // 標(biāo)記動(dòng)畫完成
      this.vehiclePositions[vehicleId].animating = false;
?
      // 更新警告標(biāo)記位置
      this.updateWarningMarkerPosition(vehicleId);
      return;
    }
?
    // 使用緩動(dòng)函數(shù)使動(dòng)畫更平滑
    progress = this.easeInOutQuad(progress);
?
    // 計(jì)算當(dāng)前位置
    const currentLng = positionData.startLng + (positionData.targetLng - positionData.startLng) * progress;
    const currentLat = positionData.startLat + (positionData.targetLat - positionData.startLat) * progress;
    const currentAlt = positionData.startAlt + (positionData.targetAlt - positionData.startAlt) * progress;
?
    // 更新車輛位置
    const meshes = this.vehicleMeshes[vehicleId];
    if (meshes) {
      meshes.forEach(mesh => {
        mesh.position(new this.AMaper.LngLat(currentLng, currentLat, currentAlt));
      });
    }
?
    // 同步更新警告標(biāo)記位置
    if (this.vehicleWarningMarkers[vehicleId]) {
      // 獲取車輛朝向
      const heading = this.vehicleHeadings[vehicleId] || 0;
?
      // 計(jì)算左上方的偏移量(根據(jù)車輛的朝向調(diào)整)
      const offsetDistance = 5;
      // 使用三角函數(shù)計(jì)算經(jīng)緯度偏移
      const headingRadians = heading * (Math.PI / 180);
      // 向左前方偏移 - 調(diào)整角度使其位于左上角
      const offsetAngle = headingRadians + Math.PI * 0.75; // 左前方45度
      const lngOffset = Math.cos(offsetAngle) * offsetDistance * 0.000008;
      const latOffset = Math.sin(offsetAngle) * offsetDistance * 0.000008;
?
      this.vehicleWarningMarkers[vehicleId].setPosition([
        currentLng + lngOffset,
        currentLat + latOffset,
        currentAlt
      ]);
    }
  });
}
?
// 緩動(dòng)函數(shù) - 使動(dòng)畫更平滑
easeInOutQuad(t) {
  return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
}

說明

  • startAnimationLoop:創(chuàng)建動(dòng)畫循環(huán),使用requestAnimationFrame實(shí)現(xiàn)高效渲染

  • updateVehicleAnimations:管理所有車輛模型的動(dòng)畫狀態(tài)和位置更新

  • 動(dòng)畫原理:計(jì)算當(dāng)前時(shí)間和起始時(shí)間的差值,得到動(dòng)畫進(jìn)度,再據(jù)此計(jì)算當(dāng)前位置

  • 使用緩動(dòng)函數(shù)(easeInOutQuad)使動(dòng)畫更加平滑自然,避免線性運(yùn)動(dòng)的機(jī)械感

5.2 模型加載與位置更新

// 修改 updateVehiclePositions 方法
async updateVehiclePositions() {
  // 步驟1:獲取當(dāng)前在線車輛ID列表
  const onlineVehicleIds = new Set(this.vehicles.map(v => v.id));
?
  // 步驟2:清理已離線的車輛模型
  Object.keys(this.vehicleMeshes).forEach(vehicleId => {
    if (!onlineVehicleIds.has(vehicleId)) {
      // 從地圖移除模型
      this.vehicleMeshes[vehicleId].forEach(mesh => {
        this.object3Dlayer.remove(mesh);
      });
      // 刪除緩存
      delete this.vehicleMeshes[vehicleId];
      // 刪除朝向記錄
      delete this.vehicleHeadings[vehicleId];
      // 刪除位置記錄
      delete this.vehiclePositions[vehicleId];
      // 刪除警告標(biāo)記
      if (this.vehicleWarningMarkers[vehicleId]) {
        this.map.remove(this.vehicleWarningMarkers[vehicleId]);
        delete this.vehicleWarningMarkers[vehicleId];
      }
    }
  });
?
  // 步驟3:更新或創(chuàng)建在線車輛模型
  for (const vehicle of this.vehicles) {
    if (!this.vehicleMeshes[vehicle.id]) {
      const meshes = await this.createMesh();
      // 確保每個(gè)子網(wǎng)格都有userData
      meshes.forEach(mesh => {
        mesh.userData = {
          ...mesh.userData,  // 保留原有數(shù)據(jù)
          vehicleId: vehicle.id
        };
        this.object3Dlayer.add(mesh);
      });
      this.vehicleMeshes[vehicle.id] = meshes;
?
      // 新創(chuàng)建的車輛模型,設(shè)置初始朝向
      if (!this.vehicleHeadings[vehicle.id]) {
        this.vehicleHeadings[vehicle.id] = vehicle.heading;
?
        // 首次加載時(shí)對(duì)所有網(wǎng)格應(yīng)用相同的初始旋轉(zhuǎn)
        meshes.forEach(mesh => {
          // 記錄初始旋轉(zhuǎn)角度到mesh的userData中
          mesh.userData.initialRotation = Math.PI + vehicle.heading + 180;
          mesh.rotateZ(mesh.userData.initialRotation);
        });
      }
?
      // 初始化位置數(shù)據(jù),首次不需要?jiǎng)赢?
      const position = new this.AMaper.LngLat(vehicle.longitude, vehicle.latitude, vehicle.altitude);
      meshes.forEach(mesh => {
        mesh.position(position);
      });
?
      this.vehiclePositions[vehicle.id] = {
        startLng: vehicle.longitude,
        startLat: vehicle.latitude,
        startAlt: vehicle.altitude,
        targetLng: vehicle.longitude,
        targetLat: vehicle.latitude,
        targetAlt: vehicle.altitude,
        startTime: Date.now(),
        duration: 1000, // 1秒動(dòng)畫
        animating: false
      };
?
      // 關(guān)鍵改動(dòng):立即檢查并創(chuàng)建警告標(biāo)記(不等待下一次更新)
      if (this.isVehicleWarning(vehicle)) {
        this.createWarningMarker(vehicle);
      }
?
      continue; // 跳過首次加載的車輛動(dòng)畫
    }

關(guān)鍵概念

  1. 模型管理生命周期

    • 對(duì)離線車輛進(jìn)行清理,釋放資源

    • 對(duì)新上線的車輛創(chuàng)建新模型

    • 對(duì)已有車輛更新位置和朝向

  2. userData存儲(chǔ)自定義數(shù)據(jù)

    • 使用mesh.userData存儲(chǔ)車輛ID和初始旋轉(zhuǎn)角度

    • 利用Three.js提供的這一機(jī)制,可以在模型中附加任意自定義數(shù)據(jù)

  3. 多層次對(duì)象管理

    • 使用多個(gè)對(duì)象存儲(chǔ)不同數(shù)據(jù):vehicleMeshes存儲(chǔ)模型,vehicleHeadings存儲(chǔ)朝向,vehiclePositions存儲(chǔ)位置

6. 模型交互與事件處理

6.1 模型點(diǎn)擊事件處理

// 修改點(diǎn)擊事件處理方法
async handleMapClick(e) {
  if (this.isLoadingModels) {
    console.warn('模型尚未加載完成');
    return;
  }
  try {
    const px = new this.AMaper.Pixel(e.pixel.x, e.pixel.y);
    const obj = this.map.getObject3DByContainerPos(px, [this.object3Dlayer], false);
?
    // 先移除之前的選中效果
    this.removeSelectionEffect();
?
    if (!obj || !obj.object) {
      console.log('未點(diǎn)擊到任何模型');
      return;
    }
?
    // 遍歷對(duì)象層級(jí)查找vehicleId
    let currentObj = obj.object;
    while (currentObj) {
      if (currentObj.userData?.vehicleId) {
        const vehicleId = currentObj.userData.vehicleId;
        const vehicle = this.vehicles.find(v => v.id === vehicleId);
?
        if (vehicle) {
          this.selectedVehicle = vehicle;
          this.isPopupVisible = true;
?
          // 添加選中效果
          this.addSelectionEffect(vehicleId);
?
          // 調(diào)用 getTrack 方法并傳遞 vehicleId
          const response = await this.getTrack({ vehicleId });
?
          // 移除之前的軌跡
          if (this.currentTrack) {
            this.map.remove(this.currentTrack);
            this.currentTrack = null;
          }
          
          // ...后續(xù)代碼省略
          
          return;
        } else {
          console.log(`未找到 ID 為 ${vehicleId} 的車輛信息`);
        }
      }
      currentObj = currentObj.parent;
    }
?
    console.log('點(diǎn)擊的模型未關(guān)聯(lián)車輛信息');
  } catch (error) {
    console.error('點(diǎn)擊處理發(fā)生錯(cuò)誤:', error);
  }
}

說明

  • getObject3DByContainerPos:高德地圖提供的射線拾取方法,用于識(shí)別用戶點(diǎn)擊的3D對(duì)象

  • 通過遍歷對(duì)象層級(jí)(currentObj = currentObj.parent)查找關(guān)聯(lián)的車輛ID

  • 使用選中效果和彈窗提供用戶反饋

  • 實(shí)現(xiàn)了3D模型的交互功能

6.2 鼠標(biāo)懸停交互

handleMapMove(e) {
  const px = new this.AMaper.Pixel(e.pixel.x, e.pixel.y);
  const obj = this.map.getObject3DByContainerPos(px, [this.object3Dlayer], false) || {};
?
  if (obj.object) {
    this.map.setDefaultCursor('pointer');
  } else {
    this.map.setDefaultCursor('default');
  }
}

說明

  • 當(dāng)鼠標(biāo)懸停在3D模型上時(shí),改變鼠標(biāo)指針樣式,提供用戶反饋

  • 使用與點(diǎn)擊相同的射線拾取技術(shù)

7. 選中效果與視覺反饋

7.1 添加選中效果

// 添加選中效果
addSelectionEffect(vehicleId) {
  // 先移除之前的效果
  this.removeSelectionEffect();

  // 獲取車輛模型位置
  const meshes = this.vehicleMeshes[vehicleId];
  if (!meshes || meshes.length === 0) return;

  const mesh = meshes[0];
  const position = mesh.position();

  // 創(chuàng)建擴(kuò)散效果
  const radius = 10; // 初始半徑(米)
  const maxRadius = 40; // 最大半徑(米)
  const ringCount = 4; // 同時(shí)顯示的環(huán)數(shù)
  const effects = [];

  for (let i = 0; i < ringCount; i++) {
    const circle = new this.AMaper.Circle({
      center: [position.lng, position.lat],
      radius: radius + (i * (maxRadius - radius) / ringCount),
      strokeColor: '#1890FF',
      strokeWeight: 3,
      strokeOpacity: 0.8 - (i * 0.2),
      fillColor: '#1890FF',
      fillOpacity: 0.4 - (i * 0.1),
      zIndex: 99 - i,
    });
    this.map.add(circle);
    effects.push(circle);
  }

  // 存儲(chǔ)效果對(duì)象和數(shù)據(jù)
  this.selectionEffect = {
    circles: effects,
    vehicleId: vehicleId,
    animation: {
      startRadius: radius,
      maxRadius: maxRadius,
      currentPhase: 0,
      ringCount: ringCount,
      lastUpdateTime: Date.now(),
      animationSpeed: 0.3 // 控制動(dòng)畫速度
    }
  };

  // 開始動(dòng)畫
  this.animateSelectionEffect();
}

說明

  • 使用高德地圖的Circle對(duì)象創(chuàng)建選中效果

  • 通過設(shè)置多個(gè)同心圓并控制其透明度和大小,實(shí)現(xiàn)視覺上的擴(kuò)散動(dòng)畫

  • 存儲(chǔ)選中效果的狀態(tài)信息,以便后續(xù)動(dòng)畫和清理

7.2 動(dòng)畫效果實(shí)現(xiàn)

// 更新選中效果動(dòng)畫 - 修復(fù)閃爍問題
animateSelectionEffect() {
  if (!this.selectionEffect) return;
?
  const effect = this.selectionEffect;
  const now = Date.now();
  const delta = (now - effect.animation.lastUpdateTime) / 1000; // 秒
  effect.animation.lastUpdateTime = now;
?
  // 更新動(dòng)畫階段
  effect.animation.currentPhase = (effect.animation.currentPhase + delta * effect.animation.animationSpeed) % 1;
?
  // 檢查車輛是否仍然存在
  const vehicleId = effect.vehicleId;
  const meshes = this.vehicleMeshes[vehicleId];
  if (!meshes || meshes.length === 0) {
    this.removeSelectionEffect();
    return;
  }
?
  // 獲取車輛當(dāng)前位置
  const mesh = meshes[0];
  const position = mesh.position();
?
  // 使用循環(huán)模式,讓圓環(huán)連續(xù)出現(xiàn)
  for (let i = 0; i < effect.animation.ringCount; i++) {
    // 計(jì)算每個(gè)環(huán)的相位偏移,確保均勻分布
    const phaseOffset = i / effect.animation.ringCount;
?
    // 添加偏移量到當(dāng)前相位
    let phase = (effect.animation.currentPhase + phaseOffset) % 1;
?
    // 計(jì)算當(dāng)前半徑
    const radiusDelta = effect.animation.maxRadius - effect.animation.startRadius;
    const currentRadius = effect.animation.startRadius + (phase * radiusDelta);
?
    // 計(jì)算不透明度 - 關(guān)鍵修改:使用平滑的不透明度曲線
    let opacity;
    if (phase < 0.1) {
      // 淡入階段 (0-10%)
      opacity = phase * 10 * 0.8; // 從0逐漸增加到0.8
    } else if (phase > 0.7) {
      // 淡出階段 (70-100%)
      const fadeOutPhase = (phase - 0.7) / 0.3; // 歸一化到0-1
      opacity = 0.8 * (1 - fadeOutPhase); // 從0.8逐漸減少到0
    } else {
      // 正常顯示階段 (10-70%)
      opacity = 0.8;
    }
?
    // 確保在相位接近1時(shí)完全透明,避免閃爍
    if (phase > 0.95) {
      opacity = 0;
    }
?
    // 更新圓的位置、大小和不透明度
    const circle = effect.circles[i];
    circle.setCenter([position.lng, position.lat]);
    circle.setRadius(currentRadius);
    circle.setOptions({
      strokeOpacity: opacity,
      fillOpacity: opacity / 2
    });
  }
?
  // 繼續(xù)動(dòng)畫循環(huán)
  requestAnimationFrame(() => this.animateSelectionEffect());
}

說明

  • 使用requestAnimationFrame實(shí)現(xiàn)高效的動(dòng)畫循環(huán)

  • 通過計(jì)算時(shí)間差來保證動(dòng)畫速度一致

  • 使用相位偏移讓多個(gè)圓環(huán)錯(cuò)開,形成連續(xù)的波浪效果

  • 實(shí)現(xiàn)了淡入淡出的不透明度變化,讓動(dòng)畫更加平滑自然

8. 高級(jí)3D效果與性能優(yōu)化

8.1 警告標(biāo)記與模型關(guān)聯(lián)

// 新增方法:創(chuàng)建警告標(biāo)記(簡(jiǎn)化DOM結(jié)構(gòu),提高渲染速度)
createWarningMarker(vehicle) {
  if (!vehicle || !this.vehicleMeshes[vehicle.id]) return;
?
  // 獲取車輛模型位置
  const meshes = this.vehicleMeshes[vehicle.id];
  if (meshes && meshes.length > 0) {
    const mesh = meshes[0];
    const position = mesh.position();
?
    // 獲取車輛朝向
    const heading = this.vehicleHeadings[vehicle.id] || 0;
?
    // 計(jì)算左上方的偏移量
    const offsetDistance = 5;
    const headingRadians = heading * (Math.PI / 180);
    const offsetAngle = headingRadians + Math.PI * 0.75;
    const lngOffset = Math.cos(offsetAngle) * offsetDistance * 0.000008;
    const latOffset = Math.sin(offsetAngle) * offsetDistance * 0.000008;
?
    // 創(chuàng)建警告標(biāo)記(恢復(fù)高度效果)
    const warningMarker = new this.AMaper.Marker({
      position: [
        position.lng + lngOffset,
        position.lat + latOffset,
        position.alt
      ],
      content: `
        <div style="
          width: 36px; 
          height: 36px; 
          background-color: #ff0000; 
          border-radius: 50%; 
          display: flex; 
          justify-content: center; 
          align-items: center; 
          color: white; 
          font-weight: bold; 
          font-size: 24px;
          box-shadow: 0 0 10px #ff0000, 0 0 20px rgba(255,0,0,0.5);
          border: 2px solid white;
          position: relative;
          margin-top: -60px; /* 通過CSS上移,造成高度效果 */
          transform: translateY(-30px);
          text-align: center;
          line-height: 36px;
        ">
          <span style="display: inline-block; position: relative; top: -1px;">!</span>
          <div style="
            position: absolute;
            bottom: -60px;
            left: 50%;
            width: 2px;
            height: 60px;
            background: linear-gradient(to bottom, rgba(255,0,0,0.8), rgba(255,0,0,0));
            transform: translateX(-50%);
          "></div>
        </div>
      `,
      offset: new this.AMaper.Pixel(-18, -18),
      zIndex: 999
    });
?
    this.map.add(warningMarker);
    this.vehicleWarningMarkers[vehicle.id] = warningMarker;
  }
}

說明

  • 結(jié)合了HTML+CSS和地圖標(biāo)記,實(shí)現(xiàn)了3D模型之上的警告指示

  • 計(jì)算偏移量時(shí)考慮了車輛的朝向,使標(biāo)記位置更加合理

  • 通過CSS實(shí)現(xiàn)了視覺上的高度效果和警告提示的漸變連接線

  • 設(shè)置了較高的zIndex確保警告標(biāo)記在最上層

9. 資源管理與優(yōu)化

9.1 清理資源與內(nèi)存管理

beforeDestroy() {
  clearInterval(this.syncVehicleDataInterval);
  if (this.stationMarkers) {
    this.stationMarkers.forEach(marker => marker.setMap(null));
  }
?
  // 清除動(dòng)畫幀
  if (this.animationFrameId) {
    cancelAnimationFrame(this.animationFrameId);
  }
?
  // 清理警告標(biāo)記
  Object.keys(this.vehicleWarningMarkers).forEach(vehicleId => {
    const warningMarker = this.vehicleWarningMarkers[vehicleId];
    if (warningMarker) {
      this.map.remove(warningMarker);
    }
  });
?
  // 清理連接線標(biāo)記
  if (this.vehicleLineMarkers) {
    Object.keys(this.vehicleLineMarkers).forEach(vehicleId => {
      const lineMarker = this.vehicleLineMarkers[vehicleId];
      if (lineMarker) {
        this.map.remove(lineMarker);
      }
    });
  }
}

說明

  • 在組件銷毀時(shí)清理所有資源,防止內(nèi)存泄漏

  • 取消動(dòng)畫幀請(qǐng)求,停止動(dòng)畫循環(huán)

  • 移除所有地圖標(biāo)記和3D對(duì)象

總結(jié)

本文檔詳細(xì)介紹了如何在高德地圖中集成和使用Three.js的3D模型,涵蓋了從模型加載、渲染、交互到動(dòng)畫的全過程。通過遵循這些實(shí)踐,可以實(shí)現(xiàn)高效且美觀的3D地圖交互效應(yīng)。

主要知識(shí)點(diǎn)包括:

  1. 模型加載與材質(zhì)應(yīng)用

  2. 坐標(biāo)系轉(zhuǎn)換與對(duì)齊

  3. 模型位置和旋轉(zhuǎn)更新

  4. 動(dòng)畫效果實(shí)現(xiàn)

  5. 交互事件處理

  6. 視覺反饋效果

  7. 性能優(yōu)化和資源管理

最后附上項(xiàng)目源碼:

<template>
  <div id="app">
    <div id="container"></div>
    <!-- 恢復(fù)定位按鈕 -->
    <button id="reset-camera-button" @click="resetCamera">恢復(fù)定位</button>
    <!-- 使用 Popup 組件 -->
    <LeftPopup :isPopupVisible="isPopupVisible" @close="handlePopupClose" @clear-selection="clearVehicleSelection"
      :vehicleInfo="selectedVehicle" @clear-right-selection="clearRightSelection" />
    <!-- 移除緊急制動(dòng)按鈕 -->
    <button id="use-vehicle-button" :class="{ disabled: !selectedVehicle }"
      :style="{ right: isRightPopupExpanded ? '30px' : '330px' }" @click="handleUseVehicle"
      :title="!selectedVehicle ? '當(dāng)前未選中車輛' : ''">
      派單
    </button>
    <!-- 監(jiān)控按鈕,只在選中車輛時(shí)顯示 -->
    <button id="monitor-button" v-if="selectedVehicle" @click="openMonitorPopup"
      :style="{ right: isRightPopupExpanded ? '30px' : '470px' }">
      監(jiān)控
    </button>
    <!-- 任務(wù)按鈕,僅當(dāng)車輛有訂單時(shí)顯示 -->
    <button id="task-button" v-if="selectedVehicle && hasActiveOrder" @click="handleTaskManage"
      :style="{ right: isRightPopupExpanded ? '30px' : '400px' }">
      任務(wù)
    </button>
    <!-- 使用監(jiān)控組件 -->
    <MonitorPopup :isPopupVisible="isMonitorPopupVisible" @close="closeMonitorPopup" :vehicleInfo="selectedVehicle" />
    <!-- 使用 RightPopup 組件 -->
    <RightPopup :isExpanded="isRightPopupExpanded" @toggle="toggleRightPopup" :vehicles="allVehicles"
      @select="handleVehicleSelect" :selectedVehicle="selectedVehicle" />
    <!-- 站點(diǎn)信息彈窗 -->
    <StationPopup :isPopupVisible="isStationPopupVisible" @close="handleStationPopupClose"
      :stationInfo="selectedStation" :position="stationPosition" />
    <!-- 用車彈窗 -->
    <UseVehiclePopup :isPopupVisible="isUseVehiclePopupVisible" @close="handleUseVehiclePopupClose"
      :vehicleInfo="selectedVehicle" />
    <!-- 任務(wù)管理彈窗 -->
    <TaskPopup :isPopupVisible="isTaskPopupVisible" @close="handleTaskPopupClose" :vehicleInfo="selectedVehicle" />
  </div>
</template>
<script>
import AMapLoader from "@amap/amap-jsapi-loader";
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';
import LeftPopup from '../../../components/PopupLeft';
import RightPopup from '../../../components/PopupRight';
import StationPopup from '../../../components/StationPopup';
import UseVehiclePopup from '../../../components/UseVehiclePopup';
import TaskPopup from '../../../components/TaskPopup';
import MonitorPopup from '../../../components/MonitorPopup';
import { listOnlineVehicles } from '@/api/ugvms/vehicles';
import { getTrack } from '@/api/ugvms/vehicles';
import { listStaion } from "@/api/ugvms/station";
import { orderByVehicleId } from "@/api/ugvms/order";

export default {
  name: "App",
  components: {
    LeftPopup,
    RightPopup,
    StationPopup,
    UseVehiclePopup,
    TaskPopup,
    MonitorPopup,
  },
  data() {
    return {
      map: null,
      AMaper: null,
      isPopupVisible: false,
      object3Dlayer: null,
      isRightPopupExpanded: false,
      vehicles: [], // 存儲(chǔ)車輛信息
      allVehicles: [], // 存儲(chǔ)所有車輛信息
      vehicleMeshes: {}, // 存儲(chǔ)每個(gè)車輛的 3D 模型
      vehicleAnimations: {}, // 存儲(chǔ)車輛動(dòng)畫狀態(tài)
      selectedVehicle: null,
      isLoadingModels: false,
      currentTrack: null, // 保存當(dāng)前顯示的軌跡對(duì)象
      currentTrackMarkers: null, // 保存軌跡起點(diǎn)和終點(diǎn)標(biāo)記
      stations: [], // 存儲(chǔ)站點(diǎn)信息
      selectedStation: null, // 當(dāng)前選中的站點(diǎn)信息
      isStationPopupVisible: false, // 控制站點(diǎn)信息彈窗的顯示狀態(tài)
      stationPosition: { x: 0, y: 0 },
      isUseVehiclePopupVisible: false, // 控制用車彈窗的顯示狀態(tài)
      isTaskPopupVisible: false, // 控制任務(wù)彈窗的顯示狀態(tài)
      hasActiveOrder: false, // 當(dāng)前選中車輛是否有活動(dòng)訂單
      vehicleHeadings: {}, // 存儲(chǔ)每個(gè)車輛的朝向歷史
      vehiclePositions: {}, // 存儲(chǔ)每個(gè)車輛的當(dāng)前位置和目標(biāo)位置
      animationFrameId: null, // 存儲(chǔ)動(dòng)畫幀ID
      selectionEffect: null, // 存儲(chǔ)選中效果對(duì)象
      isMonitorPopupVisible: false, // 監(jiān)控彈窗是否可見
      vehicleWarningMarkers: {}, // 存儲(chǔ)每個(gè)車輛的警告標(biāo)記
      vehicleLineMarkers: {}, // 存儲(chǔ)車輛警告標(biāo)記的連接線
    };
  },
  async mounted() {
    await this.initMap();
    this.loadModel();
    this.fetchVehicleData();
    this.fetchStationData();  // 新增:獲取站點(diǎn)數(shù)據(jù)
    console.log('Starting  vehicle data sync interval');
    this.syncVehicleDataInterval = setInterval(() => {
      // console.log(' 觸發(fā)車輛數(shù)據(jù)獲取,時(shí)間:', new Date());
      this.fetchVehicleData();
    }, 5000);

    // 添加鼠標(biāo)點(diǎn)擊事件監(jiān)聽器
    this.map.on('click', (e) => {
      this.handleMapClick(e);
    });

    // 添加鼠標(biāo)移入事件監(jiān)聽器
    this.map.on('mousemove', (e) => {
      this.handleMapMove(e);
    });

    // 調(diào)整高德地圖水印位置和樣式
    this.adjustMapLogo();

    // 啟動(dòng)動(dòng)畫循環(huán)
    this.startAnimationLoop();
  },
  beforeDestroy() {
    clearInterval(this.syncVehicleDataInterval);
    if (this.stationMarkers) {
      this.stationMarkers.forEach(marker => marker.setMap(null));
    }

    // 清除動(dòng)畫幀
    if (this.animationFrameId) {
      cancelAnimationFrame(this.animationFrameId);
    }

    // 清理警告標(biāo)記
    Object.keys(this.vehicleWarningMarkers).forEach(vehicleId => {
      const warningMarker = this.vehicleWarningMarkers[vehicleId];
      if (warningMarker) {
        this.map.remove(warningMarker);
      }
    });

    // 清理連接線標(biāo)記
    if (this.vehicleLineMarkers) {
      Object.keys(this.vehicleLineMarkers).forEach(vehicleId => {
        const lineMarker = this.vehicleLineMarkers[vehicleId];
        if (lineMarker) {
          this.map.remove(lineMarker);
        }
      });
    }
  },
  methods: {
    async initMap() {
      try {
        this.AMaper = await AMapLoader.load({
          // 這里寫你的 web key
          key: "a94e7a66ed28a3d1d095868ad1bec1c3",
          plugins: [""],
        });
        this.map = new this.AMaper.Map("container", {
          viewMode: '3D',
          showBuildingBlock: false,
          center: [122.037275, 37.491067],
          pitch: 60,
          zoom: 17
        });
        // 初始化光線
        this.map.AmbientLight = new this.AMaper.Lights.AmbientLight([1, 1, 1], 1);

        this.map.DirectionLight = new this.AMaper.Lights.DirectionLight([1, 0, 1], [1, 1, 1], 1);
      } catch (e) {
        console.log(e);
      }
    },
    async loadModel() {
      this.isLoadingModels = true;
      try {
        this.object3Dlayer = new this.AMaper.Object3DLayer();
        this.map.add(this.object3Dlayer);

        const materials = await new Promise((resolve, reject) => {
          new MTLLoader().load('http://localhost:9000/whgt/test.mtl', resolve, null, reject);
        });

        const event = await new Promise((resolve, reject) => {
          new OBJLoader().setMaterials(materials).load('http://localhost:9000/whgt/test.obj', resolve, null, reject);
        });

        this.vehicleBaseEvent = event;
      } finally {
        this.isLoadingModels = false;
      }
    },

    // 獲取站點(diǎn)數(shù)據(jù)并將其展示在地圖上
    async fetchStationData() {
      try {
        const response = await listStaion();
        if (response.code === 200) {
          this.stations = response.rows;
          this.displayStationsOnMap();
        } else {
          console.error('Failed to fetch station data:', response.msg);
        }
      } catch (error) {
        console.error('Failed to fetch station data:', error);
      }
    },
    displayStationsOnMap() {
      if (!this.map || !this.stations.length) return;

      // 清除之前的站點(diǎn)標(biāo)記
      if (this.stationMarkers) {
        this.stationMarkers.forEach(marker => marker.setMap(null));
      }

      this.stationMarkers = this.stations.map(station => {
        const iconSrc = this.getIconSrc(station.icon);
        const marker = new this.AMaper.Marker({
          position: [station.longitude, station.latitude, 0], // 設(shè)置 z 坐標(biāo)為 0
          icon: new this.AMaper.Icon({
            image: iconSrc,
            size: new this.AMaper.Size(32, 32), // 設(shè)置圖標(biāo)大小
            imageSize: new this.AMaper.Size(32, 32), // 設(shè)置圖片大小
          }),
          title: station.name,
        });

        marker.setMap(this.map);

        // 添加點(diǎn)擊事件
        marker.on('click', () => {
          this.handleStationClick(station);
        });

        return marker;
      });
    },
    handleStationClick(station) {
      // 將經(jīng)緯度轉(zhuǎn)換為容器坐標(biāo)
      const lnglat = new this.AMaper.LngLat(station.longitude, station.latitude);
      const pixel = this.map.lngLatToContainer(lnglat);

      // 獲取地圖容器位置
      const mapContainer = document.getElementById('container');
      const rect = mapContainer.getBoundingClientRect();

      // 計(jì)算頁(yè)面坐標(biāo)(考慮滾動(dòng)偏移)
      const x = pixel.x + rect.left - mapContainer.scrollLeft;
      const y = pixel.y + rect.top - mapContainer.scrollTop;

      this.selectedStation = station;
      this.stationPosition = { x, y };
      this.isStationPopupVisible = true;
    },
    getIconSrc(iconName) {
      switch (iconName) {
        case 'warehouse':
          return require('@/assets/drawable/amap_warehouse.png');
        case 'station':
          return require('@/assets/drawable/amap_station.png');
        case 'stop':
          return require('@/assets/drawable/amap_stop.png');
        default:
          return require('@/assets/drawable/amap_na.png'); // 可以根據(jù)需要設(shè)置默認(rèn)圖片
      }
    },

    // 監(jiān)聽左側(cè)彈窗中的關(guān)閉功能,取消右側(cè)彈窗中的選中選項(xiàng)
    clearVehicleSelection() {
      this.selectedVehicle = null;
      // 移除選中效果
      this.removeSelectionEffect();
      // 強(qiáng)制重新渲染右側(cè)組件(如果存在緩存問題)
      this.allVehicles = [...this.allVehicles];
    },

    // 處理站點(diǎn)信息彈窗的關(guān)閉事件
    handleStationPopupClose() {
      this.isStationPopupVisible = false; // 隱藏彈窗
      this.selectedStation = null; // 清空選中的站點(diǎn)信息
    },

    // 重置攝像機(jī)到初始位置
    resetCamera() {
      if (this.map) {
        // 重置攝像機(jī)到初始位置
        this.map.setCenter([122.037275, 37.491067]); // 初始中心點(diǎn)
        this.map.setZoom(17); // 初始縮放級(jí)別
        this.map.setPitch(60); // 初始俯仰角
        this.map.setRotation(0); // 重置地圖朝向?yàn)檎狈较?

        // 取消選中的車輛
        this.selectedVehicle = null;
        // 移除選中效果
        this.removeSelectionEffect();
        // 關(guān)閉左側(cè)彈窗
        this.isPopupVisible = false;

        // 隱藏軌跡
        if (this.currentTrack) {
          this.map.remove(this.currentTrack);
          this.currentTrack = null;
        }

        // 隱藏軌跡標(biāo)記
        if (this.currentTrackMarkers) {
          this.currentTrackMarkers.forEach(marker => {
            this.map.remove(marker);
          });
          this.currentTrackMarkers = null;
        }
      }
    },
    // 修改后的方法
    async createMesh() {
      const materials = await new MTLLoader().loadAsync('http://localhost:9000/whgt/Electric_G_Power_Vehi_0411010527_texture.mtl');
      const event = await new OBJLoader().setMaterials(materials).loadAsync('http://localhost:9000/whgt/Electric_G_Power_Vehi_0411010527_texture.obj');
      return this.createMeshFromEvent(event);
    },

    createMeshFromEvent(event) {
      const meshes = event.children;
      const createdMeshes = [];

      // 第一步:計(jì)算所有網(wǎng)格的Y軸最大值(在反向坐標(biāo)系中,最大Y對(duì)應(yīng)模型底部)
      let maxY = -Infinity;
      for (let i = 0; i < meshes.length; i++) {
        const meshData = meshes[i];
        const vecticesF3 = meshData.geometry.attributes.position;
        const vectexCount = vecticesF3.count;

        for (let j = 0; j < vectexCount; j++) {
          const s = j * 3;
          // 在高德地圖坐標(biāo)系中Y軸是反向的
          const y = -vecticesF3.array[s + 1];
          if (y > maxY) {
            maxY = y;
          }
        }
      }

      // 計(jì)算Y軸偏移量,使底部對(duì)齊原點(diǎn)
      // 在反向坐標(biāo)系中,使用maxY作為偏移而不是minY
      const yOffset = -maxY;

      // 第二步:創(chuàng)建幾何體并應(yīng)用偏移
      for (let i = 0; i < meshes.length; i++) {
        const meshData = meshes[i];
        const vecticesF3 = meshData.geometry.attributes.position;
        const vecticesNormal3 = meshData.geometry.attributes.normal;
        const vecticesUV2 = meshData.geometry.attributes.uv;
        const vectexCount = vecticesF3.count;

        const mesh = new this.AMaper.Object3D.MeshAcceptLights();
        const geometry = mesh.geometry;
        const material = meshData.material[0] || meshData.material;
        const c = material.color;
        const opacity = material.opacity;

        for (let j = 0; j < vectexCount; j++) {
          const s = j * 3;
          geometry.vertices.push(
            vecticesF3.array[s],
            vecticesF3.array[s + 2],
            -vecticesF3.array[s + 1] + yOffset // 添加Y軸偏移量
          );
          if (vecticesNormal3) {
            geometry.vertexNormals.push(
              vecticesNormal3.array[s],
              vecticesNormal3.array[s + 2],
              -vecticesNormal3.array[s + 1]
            );
          }
          if (vecticesUV2) {
            geometry.vertexUVs.push(
              vecticesUV2.array[j * 2],
              1 - vecticesUV2.array[j * 2 + 1]
            );
          }
          geometry.vertexColors.push(c.r, c.g, c.b, opacity);
        }

        if (material.map) {
          mesh.textures.push(require('@/assets/3d/Electric_G_Power_Vehi_0411010527_texture.png'));
        }
        mesh.DEPTH_TEST = material.depthTest;
        mesh.transparent = opacity < 1;
        mesh.scale(200, 200, 200);

        createdMeshes.push(mesh);
      }
      return createdMeshes;
    },

    // 添加動(dòng)畫循環(huán)方法
    startAnimationLoop() {
      const animate = () => {
        this.updateVehicleAnimations();
        this.animationFrameId = requestAnimationFrame(animate);
      };
      this.animationFrameId = requestAnimationFrame(animate);
    },

    // 更新車輛動(dòng)畫狀態(tài)
    updateVehicleAnimations() {
      const now = Date.now();

      Object.keys(this.vehiclePositions).forEach(vehicleId => {
        const positionData = this.vehiclePositions[vehicleId];

        // 如果沒有正在進(jìn)行的動(dòng)畫或者動(dòng)畫已經(jīng)完成,則跳過
        if (!positionData || !positionData.animating) {
          return;
        }

        // 計(jì)算動(dòng)畫進(jìn)度
        const elapsed = now - positionData.startTime;
        const duration = positionData.duration;
        let progress = Math.min(elapsed / duration, 1);

        // 如果動(dòng)畫已經(jīng)完成
        if (progress >= 1) {
          // 設(shè)置到最終位置
          const meshes = this.vehicleMeshes[vehicleId];
          if (meshes) {
            meshes.forEach(mesh => {
              mesh.position(new this.AMaper.LngLat(
                positionData.targetLng,
                positionData.targetLat,
                positionData.targetAlt
              ));
            });
          }

          // 標(biāo)記動(dòng)畫完成
          this.vehiclePositions[vehicleId].animating = false;

          // 更新警告標(biāo)記位置
          this.updateWarningMarkerPosition(vehicleId);
          return;
        }

        // 使用緩動(dòng)函數(shù)使動(dòng)畫更平滑
        progress = this.easeInOutQuad(progress);

        // 計(jì)算當(dāng)前位置
        const currentLng = positionData.startLng + (positionData.targetLng - positionData.startLng) * progress;
        const currentLat = positionData.startLat + (positionData.targetLat - positionData.startLat) * progress;
        const currentAlt = positionData.startAlt + (positionData.targetAlt - positionData.startAlt) * progress;

        // 更新車輛位置
        const meshes = this.vehicleMeshes[vehicleId];
        if (meshes) {
          meshes.forEach(mesh => {
            mesh.position(new this.AMaper.LngLat(currentLng, currentLat, currentAlt));
          });
        }

        // 同步更新警告標(biāo)記位置
        if (this.vehicleWarningMarkers[vehicleId]) {
          // 獲取車輛朝向
          const heading = this.vehicleHeadings[vehicleId] || 0;

          // 計(jì)算左上方的偏移量(根據(jù)車輛的朝向調(diào)整)
          const offsetDistance = 5;
          // 使用三角函數(shù)計(jì)算經(jīng)緯度偏移
          const headingRadians = heading * (Math.PI / 180);
          // 向左前方偏移 - 調(diào)整角度使其位于左上角
          const offsetAngle = headingRadians + Math.PI * 0.75; // 左前方45度
          const lngOffset = Math.cos(offsetAngle) * offsetDistance * 0.000008;
          const latOffset = Math.sin(offsetAngle) * offsetDistance * 0.000008;

          this.vehicleWarningMarkers[vehicleId].setPosition([
            currentLng + lngOffset,
            currentLat + latOffset,
            currentAlt
          ]);
        }
      });
    },

    // 緩動(dòng)函數(shù) - 使動(dòng)畫更平滑
    easeInOutQuad(t) {
      return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
    },

    // 修改 updateVehiclePositions 方法
    async updateVehiclePositions() {
      // 步驟1:獲取當(dāng)前在線車輛ID列表
      const onlineVehicleIds = new Set(this.vehicles.map(v => v.id));

      // 步驟2:清理已離線的車輛模型
      Object.keys(this.vehicleMeshes).forEach(vehicleId => {
        if (!onlineVehicleIds.has(vehicleId)) {
          // 從地圖移除模型
          this.vehicleMeshes[vehicleId].forEach(mesh => {
            this.object3Dlayer.remove(mesh);
          });
          // 刪除緩存
          delete this.vehicleMeshes[vehicleId];
          // 刪除朝向記錄
          delete this.vehicleHeadings[vehicleId];
          // 刪除位置記錄
          delete this.vehiclePositions[vehicleId];
          // 刪除警告標(biāo)記
          if (this.vehicleWarningMarkers[vehicleId]) {
            this.map.remove(this.vehicleWarningMarkers[vehicleId]);
            delete this.vehicleWarningMarkers[vehicleId];
          }
        }
      });

      // 步驟3:更新或創(chuàng)建在線車輛模型
      for (const vehicle of this.vehicles) {
        if (!this.vehicleMeshes[vehicle.id]) {
          const meshes = await this.createMesh();
          // 確保每個(gè)子網(wǎng)格都有userData
          meshes.forEach(mesh => {
            mesh.userData = {
              ...mesh.userData,  // 保留原有數(shù)據(jù)
              vehicleId: vehicle.id
            };
            this.object3Dlayer.add(mesh);
          });
          this.vehicleMeshes[vehicle.id] = meshes;

          // 新創(chuàng)建的車輛模型,設(shè)置初始朝向
          if (!this.vehicleHeadings[vehicle.id]) {
            this.vehicleHeadings[vehicle.id] = vehicle.heading;

            // 首次加載時(shí)對(duì)所有網(wǎng)格應(yīng)用相同的初始旋轉(zhuǎn)
            meshes.forEach(mesh => {
              // 記錄初始旋轉(zhuǎn)角度到mesh的userData中
              mesh.userData.initialRotation = Math.PI + vehicle.heading + 180;
              mesh.rotateZ(mesh.userData.initialRotation);
            });
          }

          // 初始化位置數(shù)據(jù),首次不需要?jiǎng)赢?
          const position = new this.AMaper.LngLat(vehicle.longitude, vehicle.latitude, vehicle.altitude);
          meshes.forEach(mesh => {
            mesh.position(position);
          });

          this.vehiclePositions[vehicle.id] = {
            startLng: vehicle.longitude,
            startLat: vehicle.latitude,
            startAlt: vehicle.altitude,
            targetLng: vehicle.longitude,
            targetLat: vehicle.latitude,
            targetAlt: vehicle.altitude,
            startTime: Date.now(),
            duration: 1000, // 1秒動(dòng)畫
            animating: false
          };

          // 關(guān)鍵改動(dòng):立即檢查并創(chuàng)建警告標(biāo)記(不等待下一次更新)
          if (this.isVehicleWarning(vehicle)) {
            this.createWarningMarker(vehicle);
          }

          continue; // 跳過首次加載的車輛動(dòng)畫
        }

        // 確保有車輛朝向記錄
        if (!this.vehicleHeadings[vehicle.id]) {
          this.vehicleHeadings[vehicle.id] = vehicle.heading;
        }

        // 計(jì)算需要旋轉(zhuǎn)的角度差
        const currentHeading = vehicle.heading;
        const prevHeading = this.vehicleHeadings[vehicle.id];
        const rotationDelta = currentHeading - prevHeading;

        // 獲取車輛當(dāng)前位置信息
        let positionData = this.vehiclePositions[vehicle.id];

        // 如果沒有位置信息,初始化它
        if (!positionData) {
          const meshes = this.vehicleMeshes[vehicle.id];
          if (meshes && meshes.length > 0) {
            const position = meshes[0].position();
            positionData = {
              startLng: position.lng,
              startLat: position.lat,
              startAlt: position.alt || 0,
              targetLng: vehicle.longitude,
              targetLat: vehicle.latitude,
              targetAlt: vehicle.altitude,
              startTime: Date.now(),
              duration: 1000, // 1秒動(dòng)畫
              animating: true
            };
            this.vehiclePositions[vehicle.id] = positionData;
          }
        } else {
          // 更新現(xiàn)有的位置信息,開始新的動(dòng)畫
          // 如果位置變化明顯,則開始動(dòng)畫
          const distanceThreshold = 0.0000001; // 可以根據(jù)需要調(diào)整閾值
          const significantChange =
            Math.abs(positionData.targetLng - vehicle.longitude) > distanceThreshold ||
            Math.abs(positionData.targetLat - vehicle.latitude) > distanceThreshold;

          if (significantChange) {
            // 如果當(dāng)前有動(dòng)畫正在進(jìn)行,使用當(dāng)前實(shí)際位置作為起點(diǎn)
            if (positionData.animating) {
              const meshes = this.vehicleMeshes[vehicle.id];
              if (meshes && meshes.length > 0) {
                const currentPos = meshes[0].position();
                positionData.startLng = currentPos.lng;
                positionData.startLat = currentPos.lat;
                positionData.startAlt = currentPos.alt || 0;
              }
            } else {
              // 否則使用上一個(gè)目標(biāo)位置作為起點(diǎn)
              positionData.startLng = positionData.targetLng;
              positionData.startLat = positionData.targetLat;
              positionData.startAlt = positionData.targetAlt;
            }

            // 設(shè)置新的目標(biāo)位置
            positionData.targetLng = vehicle.longitude;
            positionData.targetLat = vehicle.latitude;
            positionData.targetAlt = vehicle.altitude;
            positionData.startTime = Date.now();
            positionData.animating = true;
          }
        }

        // 只有當(dāng)朝向有明顯變化時(shí)才進(jìn)行旋轉(zhuǎn)
        if (Math.abs(rotationDelta) > 0.001) {
          // 更新旋轉(zhuǎn)
          const meshes = this.vehicleMeshes[vehicle.id];
          meshes.forEach(mesh => {
            mesh.userData.vehicleId = vehicle.id; // 確保ID正確
            // 計(jì)算實(shí)際需要的旋轉(zhuǎn)角度
            const targetRotation = Math.PI + currentHeading + 180;
            const currentRotation = mesh.userData.initialRotation || 0;
            const actualRotationDelta = targetRotation - currentRotation;
            mesh.rotateZ(actualRotationDelta);
            // 更新初始旋轉(zhuǎn)角度
            mesh.userData.initialRotation = targetRotation;
          });

          // 更新朝向記錄
          this.vehicleHeadings[vehicle.id] = currentHeading;
        }

        // 更新車輛警告標(biāo)記
        const isWarning = this.isVehicleWarning(vehicle);
        const hasWarning = !!this.vehicleWarningMarkers[vehicle.id];

        if (isWarning && !hasWarning) {
          // 新增警告情況:創(chuàng)建警告標(biāo)記
          this.createWarningMarker(vehicle);
        } else if (!isWarning && hasWarning) {
          // 警告解除:移除警告標(biāo)記
          this.map.remove(this.vehicleWarningMarkers[vehicle.id]);
          delete this.vehicleWarningMarkers[vehicle.id];

          // 同時(shí)移除連接線(如果存在)
          if (this.vehicleLineMarkers && this.vehicleLineMarkers[vehicle.id]) {
            this.map.remove(this.vehicleLineMarkers[vehicle.id]);
            delete this.vehicleLineMarkers[vehicle.id];
          }
        } else if (isWarning && hasWarning) {
          // 警告繼續(xù)存在:更新位置
          this.updateWarningMarkerPosition(vehicle.id);
        }
      }
    },

    // 新增方法:創(chuàng)建警告標(biāo)記(簡(jiǎn)化DOM結(jié)構(gòu),提高渲染速度)
    createWarningMarker(vehicle) {
      if (!vehicle || !this.vehicleMeshes[vehicle.id]) return;

      // 獲取車輛模型位置
      const meshes = this.vehicleMeshes[vehicle.id];
      if (meshes && meshes.length > 0) {
        const mesh = meshes[0];
        const position = mesh.position();

        // 獲取車輛朝向
        const heading = this.vehicleHeadings[vehicle.id] || 0;

        // 計(jì)算左上方的偏移量
        const offsetDistance = 5;
        const headingRadians = heading * (Math.PI / 180);
        const offsetAngle = headingRadians + Math.PI * 0.75;
        const lngOffset = Math.cos(offsetAngle) * offsetDistance * 0.000008;
        const latOffset = Math.sin(offsetAngle) * offsetDistance * 0.000008;

        // 創(chuàng)建警告標(biāo)記(恢復(fù)高度效果)
        const warningMarker = new this.AMaper.Marker({
          position: [
            position.lng + lngOffset,
            position.lat + latOffset,
            position.alt
          ],
          content: `
            <div style="
              width: 36px; 
              height: 36px; 
              background-color: #ff0000; 
              border-radius: 50%; 
              display: flex; 
              justify-content: center; 
              align-items: center; 
              color: white; 
              font-weight: bold; 
              font-size: 24px;
              box-shadow: 0 0 10px #ff0000, 0 0 20px rgba(255,0,0,0.5);
              border: 2px solid white;
              position: relative;
              margin-top: -60px; /* 通過CSS上移,造成高度效果 */
              transform: translateY(-30px);
              text-align: center;
              line-height: 36px;
            ">
              <span style="display: inline-block; position: relative; top: -1px;">!</span>
              <div style="
                position: absolute;
                bottom: -60px;
                left: 50%;
                width: 2px;
                height: 60px;
                background: linear-gradient(to bottom, rgba(255,0,0,0.8), rgba(255,0,0,0));
                transform: translateX(-50%);
              "></div>
            </div>
          `,
          offset: new this.AMaper.Pixel(-18, -18),
          zIndex: 999
        });

        this.map.add(warningMarker);
        this.vehicleWarningMarkers[vehicle.id] = warningMarker;
      }
    },
    async fetchVehicleData() {
      try {
        const response = await listOnlineVehicles();
        // 添加過濾條件:只保留 online 為 true 的車輛
        this.vehicles = response.data.filter(v => v.online === true);
        this.allVehicles = response.data.filter(v => v.online !== null);

        // 如果當(dāng)前有選中的車輛,更新 selectedVehicle 的數(shù)據(jù)
        if (this.selectedVehicle) {
          const latestVehicle = this.vehicles.find(v => v.id === this.selectedVehicle.id);
          if (latestVehicle) {
            this.selectedVehicle = latestVehicle; // 更新 selectedVehicle
            this.checkVehicleOrders(); // 刷新車輛訂單狀態(tài)
          } else {
            // 如果選中的車輛已離線,關(guān)閉彈窗并清除軌跡
            this.isPopupVisible = false;
            this.selectedVehicle = null;
            this.hasActiveOrder = false;

            // 清除軌跡和標(biāo)記
            if (this.currentTrack) {
              this.map.remove(this.currentTrack);
              this.currentTrack = null;
            }

            if (this.currentTrackMarkers) {
              this.currentTrackMarkers.forEach(marker => {
                this.map.remove(marker);
              });
              this.currentTrackMarkers = null;
            }
          }
        }

        this.updateVehiclePositions();
      } catch (error) {
        console.error('Failed to fetch onlineVehicle data:', error);
      }
    },
    // 修改點(diǎn)擊事件處理方法
    async handleMapClick(e) {
      if (this.isLoadingModels) {
        console.warn('模型尚未加載完成');
        return;
      }
      try {
        const px = new this.AMaper.Pixel(e.pixel.x, e.pixel.y);
        const obj = this.map.getObject3DByContainerPos(px, [this.object3Dlayer], false);

        // 先移除之前的選中效果
        this.removeSelectionEffect();

        if (!obj || !obj.object) {
          console.log('未點(diǎn)擊到任何模型');
          return;
        }

        // 遍歷對(duì)象層級(jí)查找vehicleId
        let currentObj = obj.object;
        while (currentObj) {
          if (currentObj.userData?.vehicleId) {
            const vehicleId = currentObj.userData.vehicleId;
            const vehicle = this.vehicles.find(v => v.id === vehicleId);

            if (vehicle) {
              this.selectedVehicle = vehicle;
              this.isPopupVisible = true;

              // 添加選中效果
              this.addSelectionEffect(vehicleId);

              // 調(diào)用 getTrack 方法并傳遞 vehicleId
              const response = await this.getTrack({ vehicleId });

              // 移除之前的軌跡
              if (this.currentTrack) {
                this.map.remove(this.currentTrack);
                this.currentTrack = null;
              }

              // 驗(yàn)證軌跡數(shù)據(jù)格式
              const path = response.data;
              if (!path || !Array.isArray(path) || path.length === 0) {
                console.warn('軌跡數(shù)據(jù)為空或格式不正確');
                return;
              }

              // 驗(yàn)證每個(gè)點(diǎn)是否有正確的經(jīng)緯度
              const validPath = path.filter(point =>
                Array.isArray(point) &&
                point.length === 2 &&
                typeof point[0] === 'number' &&
                typeof point[1] === 'number'
              );

              if (validPath.length === 0) {
                console.warn('沒有有效的軌跡點(diǎn)');
                return;
              }

              // 創(chuàng)建新的軌跡
              this.currentTrack = new this.AMaper.Polyline({
                path: validPath,
                strokeColor: "#0066FF",
                strokeWeight: 6,
                strokeOpacity: 0.8,
                lineJoin: 'round',
                lineCap: 'round',
                showDir: true,
                strokeStyle: 'dashed',
                strokeDasharray: [8, 4],
                outlineColor: '#FFFFFF',
                outlineWeight: 2,
                borderWeight: 3, // 添加邊框
                dirColor: '#ff6a00', // 使用亮橙色箭頭,更加明顯
                zIndex: 120 // 確保箭頭在其他元素上方
              });

              // 在軌跡上添加起點(diǎn)和終點(diǎn)標(biāo)記
              if (validPath.length > 0) {
                // 添加起點(diǎn)標(biāo)記
                const startMarker = new this.AMaper.Marker({
                  position: validPath[0],
                  content: '<div style="background-color: #1890FF; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white;"></div>',
                  offset: new this.AMaper.Pixel(-6, -6),
                  zIndex: 121
                });

                // 添加終點(diǎn)標(biāo)記
                const endMarker = new this.AMaper.Marker({
                  position: validPath[validPath.length - 1],
                  content: '<div style="background-color: #ff6a00; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white;"></div>',
                  offset: new this.AMaper.Pixel(-6, -6),
                  zIndex: 121
                });

                this.map.add([this.currentTrack, startMarker, endMarker]);

                // 存儲(chǔ)標(biāo)記,以便稍后移除
                this.currentTrackMarkers = [startMarker, endMarker];
              } else {
                this.map.add(this.currentTrack);
              }

              return;
            } else {
              console.log(`未找到 ID 為 ${vehicleId} 的車輛信息`);
            }
          }
          currentObj = currentObj.parent;
        }

        console.log('點(diǎn)擊的模型未關(guān)聯(lián)車輛信息');
      } catch (error) {
        console.error('點(diǎn)擊處理發(fā)生錯(cuò)誤:', error);
      }
    },
    handleMapMove(e) {
      const px = new this.AMaper.Pixel(e.pixel.x, e.pixel.y);
      const obj = this.map.getObject3DByContainerPos(px, [this.object3Dlayer], false) || {};

      if (obj.object) {
        this.map.setDefaultCursor('pointer');
      } else {
        this.map.setDefaultCursor('default');
      }
    },
    toggleRightPopup() {
      this.isRightPopupExpanded = !this.isRightPopupExpanded;
    },
    getTrack(vehicleId) {
      return getTrack(vehicleId);
    },
    handlePopupClose() {
      this.isPopupVisible = false;
      // 移除選中效果
      this.removeSelectionEffect();
      // 隱藏軌跡
      if (this.currentTrack) {
        this.map.remove(this.currentTrack);
        this.currentTrack = null;
      }
      // 隱藏軌跡標(biāo)記
      if (this.currentTrackMarkers) {
        this.currentTrackMarkers.forEach(marker => {
          this.map.remove(marker);
        });
        this.currentTrackMarkers = null;
      }
    },
    // 處理右側(cè)組件選中車輛事件
    async handleVehicleSelect(vehicle) {
      // 從 vehicles 數(shù)組中獲取最新的車輛數(shù)據(jù)
      const latestVehicle = this.vehicles.find(v => v.id === vehicle.id);
      if (!latestVehicle) {
        console.error('未找到對(duì)應(yīng)的車輛信息');
        return;
      }

      // 先移除之前的選中效果
      this.removeSelectionEffect();

      this.selectedVehicle = latestVehicle; // 使用最新的車輛數(shù)據(jù)
      this.isPopupVisible = true; // 彈出左側(cè)彈窗

      // 添加選中效果
      this.addSelectionEffect(latestVehicle.id);

      // 找到對(duì)應(yīng)的車輛模型
      const meshes = this.vehicleMeshes[latestVehicle.id];
      if (meshes && meshes.length > 0) {
        // 聚焦到該模型的位置
        const mesh = meshes[0]; // 取第一個(gè)子網(wǎng)格
        const position = mesh.position(); // 獲取模型的位置
        this.map.setCenter([position.lng, position.lat]); // 設(shè)置地圖中心點(diǎn)
        this.map.setZoom(19); // 調(diào)整縮放級(jí)別
        this.map.setPitch(60); // 調(diào)整俯仰角
      }

      // 更新車輛實(shí)時(shí)軌跡
      await this.updateVehicleTrack({ "vehicleId": latestVehicle.id });

      // 檢查車輛訂單狀態(tài)
      await this.checkVehicleOrders();
    },

    // 更新車輛軌跡
    async updateVehicleTrack(vehicleId) {
      try {
        // 調(diào)用 API 獲取軌跡數(shù)據(jù)
        const response = await this.getTrack(vehicleId);

        // 移除之前的軌跡
        if (this.currentTrack) {
          this.map.remove(this.currentTrack);
          this.currentTrack = null;
        }

        // 驗(yàn)證軌跡數(shù)據(jù)格式
        const path = response.data;
        if (!path || !Array.isArray(path) || path.length === 0) {
          console.warn('軌跡數(shù)據(jù)為空或格式不正確');
          return;
        }

        // 驗(yàn)證每個(gè)點(diǎn)是否有正確的經(jīng)緯度
        const validPath = path.filter(point =>
          Array.isArray(point) &&
          point.length === 2 &&
          typeof point[0] === 'number' &&
          typeof point[1] === 'number'
        );

        if (validPath.length === 0) {
          console.warn('沒有有效的軌跡點(diǎn)');
          return;
        }

        // 創(chuàng)建新的軌跡
        this.currentTrack = new this.AMaper.Polyline({
          path: validPath,
          strokeColor: "#0066FF",
          strokeWeight: 6,
          strokeOpacity: 0.8,
          lineJoin: 'round',
          lineCap: 'round',
          showDir: true,
          strokeStyle: 'dashed',
          strokeDasharray: [8, 4],
          outlineColor: '#FFFFFF',
          outlineWeight: 2,
          borderWeight: 3, // 添加邊框
          dirColor: '#ff6a00', // 使用亮橙色箭頭,更加明顯
          zIndex: 120 // 確保箭頭在其他元素上方
        });

        // 在軌跡上添加起點(diǎn)和終點(diǎn)標(biāo)記
        if (validPath.length > 0) {
          // 添加起點(diǎn)標(biāo)記
          const startMarker = new this.AMaper.Marker({
            position: validPath[0],
            content: '<div style="background-color: #1890FF; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white;"></div>',
            offset: new this.AMaper.Pixel(-6, -6),
            zIndex: 121
          });

          // 添加終點(diǎn)標(biāo)記
          const endMarker = new this.AMaper.Marker({
            position: validPath[validPath.length - 1],
            content: '<div style="background-color: #ff6a00; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white;"></div>',
            offset: new this.AMaper.Pixel(-6, -6),
            zIndex: 121
          });

          this.map.add([this.currentTrack, startMarker, endMarker]);

          // 存儲(chǔ)標(biāo)記,以便稍后移除
          this.currentTrackMarkers = [startMarker, endMarker];
        } else {
          this.map.add(this.currentTrack);
        }
      } catch (error) {
        console.error('Failed to update vehicle track:', error);
      }
    },

    clearRightSelection() {
      this.selectedVehicle = null; // 清除選中的車輛,從而清除右側(cè)組件的選擇狀態(tài)
    },

    handleUseVehicle() {
      if (!this.selectedVehicle) return;
      // 顯示用車彈窗
      this.isUseVehiclePopupVisible = true;
    },

    // 處理用車彈窗關(guān)閉事件
    handleUseVehiclePopupClose() {
      this.isUseVehiclePopupVisible = false;
      this.checkVehicleOrders(); // 刷新車輛訂單狀態(tài)
    },

    // 處理任務(wù)按鈕點(diǎn)擊事件
    handleTaskManage() {
      if (!this.selectedVehicle) return;
      this.isTaskPopupVisible = true;
    },

    // 處理任務(wù)彈窗關(guān)閉事件
    handleTaskPopupClose() {
      this.isTaskPopupVisible = false;
      this.checkVehicleOrders(); // 刷新車輛訂單狀態(tài)
    },

    // 檢查當(dāng)前選中車輛是否有活動(dòng)訂單
    async checkVehicleOrders() {
      if (!this.selectedVehicle) return;

      try {
        const response = await orderByVehicleId(this.selectedVehicle.id);
        if (response.code === 200 && response.data.length > 0) {
          // 查找是否有進(jìn)行中的訂單
          const activeOrder = response.data.find(order => order.status === 10);
          this.hasActiveOrder = !!activeOrder;
        } else {
          this.hasActiveOrder = false;
        }
      } catch (error) {
        console.error('獲取車輛訂單失敗:', error);
        this.hasActiveOrder = false;
      }
    },

    // 修改 adjustMapLogo 方法
    adjustMapLogo() {
      // 給地圖容器添加一個(gè)延時(shí),確保地圖元素已完全加載
      setTimeout(() => {
        // 查找高德地圖的logo元素
        const logoElements = document.querySelectorAll('.amap-logo,.amap-copyright');

        logoElements.forEach(element => {
          // 將水印移到左下角固定位置,便于用按鈕遮擋
          element.style.bottom = '10px';
          element.style.left = '10px';
          element.style.transform = 'scale(0.7)';
          element.style.opacity = '0.3';
          element.style.zIndex = '100';
        });

        // 調(diào)整恢復(fù)定位按鈕位置,使其遮擋水印
        const resetButton = document.getElementById('reset-camera-button');
        if (resetButton) {
          resetButton.style.left = '10px';
          resetButton.style.bottom = '10px';
        }
      }, 1000);
    },

    // 添加選中效果
    addSelectionEffect(vehicleId) {
      // 先移除之前的效果
      this.removeSelectionEffect();

      // 獲取車輛模型位置
      const meshes = this.vehicleMeshes[vehicleId];
      if (!meshes || meshes.length === 0) return;

      const mesh = meshes[0];
      const position = mesh.position();

      // 創(chuàng)建擴(kuò)散效果
      const radius = 10; // 初始半徑(米)
      const maxRadius = 40; // 最大半徑(米)
      const ringCount = 4; // 同時(shí)顯示的環(huán)數(shù)
      const effects = [];

      for (let i = 0; i < ringCount; i++) {
        const circle = new this.AMaper.Circle({
          center: [position.lng, position.lat],
          radius: radius + (i * (maxRadius - radius) / ringCount),
          strokeColor: '#1890FF',
          strokeWeight: 3,
          strokeOpacity: 0.8 - (i * 0.2),
          fillColor: '#1890FF',
          fillOpacity: 0.4 - (i * 0.1),
          zIndex: 99 - i,
        });
        this.map.add(circle);
        effects.push(circle);
      }

      // 存儲(chǔ)效果對(duì)象和數(shù)據(jù)
      this.selectionEffect = {
        circles: effects,
        vehicleId: vehicleId,
        animation: {
          startRadius: radius,
          maxRadius: maxRadius,
          currentPhase: 0,
          ringCount: ringCount,
          lastUpdateTime: Date.now(),
          animationSpeed: 0.3 // 控制動(dòng)畫速度
        }
      };

      // 開始動(dòng)畫
      this.animateSelectionEffect();
    },

    // 移除選中效果
    removeSelectionEffect() {
      if (this.selectionEffect) {
        // 移除所有圓形
        this.selectionEffect.circles.forEach(circle => {
          this.map.remove(circle);
        });
        this.selectionEffect = null;
      }
    },

    // 更新選中效果動(dòng)畫 - 修復(fù)閃爍問題
    animateSelectionEffect() {
      if (!this.selectionEffect) return;

      const effect = this.selectionEffect;
      const now = Date.now();
      const delta = (now - effect.animation.lastUpdateTime) / 1000; // 秒
      effect.animation.lastUpdateTime = now;

      // 更新動(dòng)畫階段
      effect.animation.currentPhase = (effect.animation.currentPhase + delta * effect.animation.animationSpeed) % 1;

      // 檢查車輛是否仍然存在
      const vehicleId = effect.vehicleId;
      const meshes = this.vehicleMeshes[vehicleId];
      if (!meshes || meshes.length === 0) {
        this.removeSelectionEffect();
        return;
      }

      // 獲取車輛當(dāng)前位置
      const mesh = meshes[0];
      const position = mesh.position();

      // 使用循環(huán)模式,讓圓環(huán)連續(xù)出現(xiàn)
      for (let i = 0; i < effect.animation.ringCount; i++) {
        // 計(jì)算每個(gè)環(huán)的相位偏移,確保均勻分布
        const phaseOffset = i / effect.animation.ringCount;

        // 添加偏移量到當(dāng)前相位
        let phase = (effect.animation.currentPhase + phaseOffset) % 1;

        // 計(jì)算當(dāng)前半徑
        const radiusDelta = effect.animation.maxRadius - effect.animation.startRadius;
        const currentRadius = effect.animation.startRadius + (phase * radiusDelta);

        // 計(jì)算不透明度 - 關(guān)鍵修改:使用平滑的不透明度曲線
        let opacity;
        if (phase < 0.1) {
          // 淡入階段 (0-10%)
          opacity = phase * 10 * 0.8; // 從0逐漸增加到0.8
        } else if (phase > 0.7) {
          // 淡出階段 (70-100%)
          const fadeOutPhase = (phase - 0.7) / 0.3; // 歸一化到0-1
          opacity = 0.8 * (1 - fadeOutPhase); // 從0.8逐漸減少到0
        } else {
          // 正常顯示階段 (10-70%)
          opacity = 0.8;
        }

        // 確保在相位接近1時(shí)完全透明,避免閃爍
        if (phase > 0.95) {
          opacity = 0;
        }

        // 更新圓的位置、大小和不透明度
        const circle = effect.circles[i];
        circle.setCenter([position.lng, position.lat]);
        circle.setRadius(currentRadius);
        circle.setOptions({
          strokeOpacity: opacity,
          fillOpacity: opacity / 2
        });
      }

      // 繼續(xù)動(dòng)畫循環(huán)
      requestAnimationFrame(() => this.animateSelectionEffect());
    },

    // 保留緩動(dòng)函數(shù)以備后用
    easeInOutSine(x) {
      return -(Math.cos(Math.PI * x) - 1) / 2;
    },

    // 新增監(jiān)控相關(guān)方法
    // 打開監(jiān)控彈窗
    openMonitorPopup() {
      if (this.selectedVehicle) {
        this.isMonitorPopupVisible = true;
      }
    },

    // 關(guān)閉監(jiān)控彈窗
    closeMonitorPopup() {
      this.isMonitorPopupVisible = false;
    },

    // 判斷車輛是否處于警告狀態(tài)
    isVehicleWarning(vehicle) {
      if (!vehicle) return false;

      // 檢查符合警告條件的情況
      return vehicle.drivingCmdControlMode === -1 ||
        vehicle.driveSystemError === 1 ||
        vehicle.driveSystemError === 2 ||
        vehicle.errorPowerBattery === 1 ||
        vehicle.errorMotor === 1 ||
        vehicle.errorDc === 1;
    },

    // 更新警告標(biāo)記位置
    updateWarningMarkerPosition(vehicleId) {
      const warningMarker = this.vehicleWarningMarkers[vehicleId];
      if (!warningMarker) return;

      const meshes = this.vehicleMeshes[vehicleId];
      if (meshes && meshes.length > 0) {
        const mesh = meshes[0];
        const position = mesh.position();

        // 獲取車輛朝向
        const heading = this.vehicleHeadings[vehicleId] || 0;

        // 計(jì)算左上方的偏移量(根據(jù)車輛的朝向調(diào)整)
        const offsetDistance = 5;
        // 使用三角函數(shù)計(jì)算經(jīng)緯度偏移,左上角相對(duì)于車輛頭部位置
        const headingRadians = heading * (Math.PI / 180);
        // 向左前方偏移 - 調(diào)整角度使其位于左上角
        const offsetAngle = headingRadians + Math.PI * 0.75; // 左前方45度
        const lngOffset = Math.cos(offsetAngle) * offsetDistance * 0.000008;
        const latOffset = Math.sin(offsetAngle) * offsetDistance * 0.000008;

        warningMarker.setPosition([
          position.lng + lngOffset,
          position.lat + latOffset,
          position.alt
        ]);
      }
    },

    // 更新或添加車輛警告標(biāo)記(保留此方法以兼容可能的其他調(diào)用)
    updateVehicleWarningMarker(vehicle) {
      if (!vehicle || !this.vehicleMeshes[vehicle.id]) return;

      const isWarning = this.isVehicleWarning(vehicle);
      const hasWarning = !!this.vehicleWarningMarkers[vehicle.id];

      // 簡(jiǎn)化判斷邏輯
      if (isWarning && !hasWarning) {
        // 需要警告但沒有標(biāo)記:創(chuàng)建新標(biāo)記
        this.createWarningMarker(vehicle);
      } else if (!isWarning && hasWarning) {
        // 不需要警告但有標(biāo)記:移除標(biāo)記
        this.map.remove(this.vehicleWarningMarkers[vehicle.id]);
        delete this.vehicleWarningMarkers[vehicle.id];

        // 同時(shí)移除連接線(如果存在)
        if (this.vehicleLineMarkers && this.vehicleLineMarkers[vehicle.id]) {
          this.map.remove(this.vehicleLineMarkers[vehicle.id]);
          delete this.vehicleLineMarkers[vehicle.id];
        }
      } else if (isWarning && hasWarning) {
        // 需要警告且已有標(biāo)記:更新位置
        this.updateWarningMarkerPosition(vehicle.id);
      }
    },
  },
};
</script>

<style>
#container {
  padding: 0px;
  margin: 0px;
  width: 100%;
  height: 90.8vh;
}

/* 在樣式部分添加信息卡片樣式 */
.vehicle-info-card {
  padding: 15px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}

/* 美化恢復(fù)定位按鈕 */
#reset-camera-button {
  position: absolute;
  bottom: 10px;
  left: 10px;
  z-index: 1001;
  /* 確保按鈕在水印上方 */
  padding: 10px 15px;
  background-color: rgba(52, 152, 219, 0.9);
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 100px;
  /* 確保按鈕足夠?qū)捯哉趽跛?*/
  min-height: 36px;
  /* 確保按鈕足夠高以遮擋水印 */
}

#reset-camera-button:hover {
  background-color: rgba(41, 128, 185, 1);
  transform: scale(1.05);
}

#reset-camera-button:before {
  content: "?";
  margin-right: 5px;
  font-size: 16px;
}

#use-vehicle-button {
  position: fixed;
  bottom: 20px;
  right: 20px;
  z-index: 1000;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background-color: #4CAF50;
  color: white;
  border: none;
  font-size: 16px;
  cursor: pointer;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  justify-content: center;
}

#use-vehicle-button:hover:not(.disabled) {
  background-color: #45a049;
  transform: scale(1.05);
}

#use-vehicle-button.disabled {
  background-color: #95a5a6;
  cursor: not-allowed;
  opacity: 0.7;
}

/* 任務(wù)按鈕樣式 */
#task-button {
  position: fixed;
  bottom: 20px;
  right: 90px;
  /* 位于派單按鈕左側(cè) */
  z-index: 1000;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background-color: #3498db;
  color: white;
  border: none;
  font-size: 16px;
  cursor: pointer;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  justify-content: center;
}

#task-button:hover {
  background-color: #2980b9;
  transform: scale(1.05);
}

/* 調(diào)整高德地圖水印樣式 */
.amap-logo {
  opacity: 0.3 !important;
  transform: scale(0.7) !important;
}

.amap-copyright {
  opacity: 0.3 !important;
  transform: scale(0.7) !important;
}

/* 監(jiān)控按鈕樣式 */
#monitor-button {
  position: fixed;
  bottom: 20px;
  /* 位于任務(wù)按鈕左側(cè) */
  z-index: 1000;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background-color: #e67e22;
  color: white;
  border: none;
  font-size: 16px;
  cursor: pointer;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  justify-content: center;
}

#monitor-button:hover {
  background-color: #d35400;
  transform: scale(1.05);
}
</style>

到此這篇關(guān)于高德地圖與Three.js 3D模型集成超詳細(xì)指南的文章就介紹到這了,更多相關(guān)高德地圖與Three.js 3D模型集成內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • JS+CSS實(shí)現(xiàn)的簡(jiǎn)單折疊展開多級(jí)菜單效果

    JS+CSS實(shí)現(xiàn)的簡(jiǎn)單折疊展開多級(jí)菜單效果

    這篇文章主要介紹了JS+CSS實(shí)現(xiàn)的簡(jiǎn)單折疊展開多級(jí)菜單效果,涉及JavaScript頁(yè)面元素的遍歷及動(dòng)態(tài)操作技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-09-09
  • js字符串操作函數(shù)總結(jié)及使用方法示例

    js字符串操作函數(shù)總結(jié)及使用方法示例

    這篇文章主要介紹了js字符串操作函數(shù)toString、split、length、indexOf、lastIndexOf、replace、replaceAll、charAt、charCodeAt、contact、slice、substring、substr、match、exec、search、trim、includes
    2023-08-08
  • 使用js判斷TextBox控件值改變?nèi)缓蟪霭l(fā)事件

    使用js判斷TextBox控件值改變?nèi)缓蟪霭l(fā)事件

    這篇文章主要介紹了使用js判斷TextBox控件值改變?nèi)缓蟪霭l(fā)事件。需要的朋友可以過來參考下,希望對(duì)大家有所幫助
    2014-03-03
  • 小程序頁(yè)面間傳參的五種方式實(shí)例詳解

    小程序頁(yè)面間傳參的五種方式實(shí)例詳解

    頁(yè)面跳轉(zhuǎn),頁(yè)面之間傳遞參數(shù)在開發(fā)APP應(yīng)用的時(shí)候會(huì)經(jīng)常用到這樣的功能,下面這篇文章主要給大家介紹了關(guān)于小程序頁(yè)面間傳參的五種方式,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-08-08
  • 手動(dòng)實(shí)現(xiàn)js短信驗(yàn)證碼輸入框

    手動(dòng)實(shí)現(xiàn)js短信驗(yàn)證碼輸入框

    本文記錄一下自己手動(dòng)實(shí)現(xiàn)的一個(gè)前端常見的短信驗(yàn)證碼輸入組件,從需求到實(shí)現(xiàn)逐步優(yōu)化的過程。具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-06-06
  • 小程序自定義導(dǎo)航欄兼容適配所有機(jī)型(附完整案例)

    小程序自定義導(dǎo)航欄兼容適配所有機(jī)型(附完整案例)

    這篇文章主要介紹了小程序自定義導(dǎo)航欄兼容適配所有機(jī)型(附完整案例),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-04-04
  • javascript Canvas動(dòng)態(tài)粒子連線

    javascript Canvas動(dòng)態(tài)粒子連線

    這篇文章主要為大家詳細(xì)介紹了javascript Canvas動(dòng)態(tài)粒子連線,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-01-01
  • 不得不知的ES6小技巧

    不得不知的ES6小技巧

    ES6出來已經(jīng)有好幾年了,同時(shí)很多新特性可以被巧妙地運(yùn)用在項(xiàng)目中。我想要列下其中一些,希望它們對(duì)你有用
    2018-07-07
  • JS 在數(shù)組插入字符的實(shí)現(xiàn)代碼(可參考JavaScript splice() 方法)

    JS 在數(shù)組插入字符的實(shí)現(xiàn)代碼(可參考JavaScript splice() 方法)

    在數(shù)組插入字符,添加數(shù)組,刪除數(shù)組可以用slice自帶的方法。操作比較方便,這個(gè)代碼是作者通過push與shift方法實(shí)現(xiàn),只能是個(gè)思路,但不推薦這樣的方法。
    2009-12-12
  • 在Typescript中如何使用for...in詳解

    在Typescript中如何使用for...in詳解

    這篇文章主要給大家介紹了關(guān)于在Typescript中如何使用for...in的相關(guān)資料,以及TypeScript中使用for...in遍歷對(duì)象屬性會(huì)報(bào)錯(cuò)的解決辦法,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-03-03

最新評(píng)論

南宁市| 华亭县| 高邑县| 嘉黎县| 元谋县| 稷山县| 庄浪县| 若羌县| 招远市| 延川县| 西城区| 七台河市| 田东县| 白银市| 阿巴嘎旗| 诏安县| 炎陵县| 灵璧县| 鱼台县| 阳西县| 土默特右旗| 临夏市| 扎囊县| 广汉市| 获嘉县| 平原县| 黄浦区| 西畴县| 东源县| 岳普湖县| 衡南县| 买车| 台东县| 汉中市| 赤峰市| 昌吉市| 肥西县| 股票| 左贡县| 通许县| 格尔木市|