高德地圖與Three.js?3D模型集成超詳細(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: 用于加載高德地圖APIOBJLoader: 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效果通過
AmbientLight和DirectionLight設(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)鍵概念解析:
網(wǎng)格結(jié)構(gòu):
每個(gè)3D模型由多個(gè)網(wǎng)格組成,存儲(chǔ)在
event.children中每個(gè)網(wǎng)格包含幾何數(shù)據(jù)和材質(zhì)數(shù)據(jù)
坐標(biāo)系調(diào)整:
Three.js和高德地圖使用不同的坐標(biāo)系
代碼中計(jì)算Y軸最大值并應(yīng)用偏移,確保模型正確放置
網(wǎng)格數(shù)據(jù)處理:
vecticesF3:頂點(diǎn)位置數(shù)據(jù)vecticesNormal3:法線數(shù)據(jù),用于光照計(jì)算vecticesUV2:UV貼圖坐標(biāo),用于紋理映射
創(chuàng)建高德地圖兼容的網(wǎng)格:
使用
AMaper.Object3D.MeshAcceptLights()創(chuàng)建可接收光照的網(wǎng)格轉(zhuǎn)換并添加頂點(diǎn)、法線和UV數(shù)據(jù)
設(shè)置材質(zhì)顏色和透明度
應(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)鍵概念:
模型管理生命周期:
對(duì)離線車輛進(jìn)行清理,釋放資源
對(duì)新上線的車輛創(chuàng)建新模型
對(duì)已有車輛更新位置和朝向
userData存儲(chǔ)自定義數(shù)據(jù):
使用
mesh.userData存儲(chǔ)車輛ID和初始旋轉(zhuǎn)角度利用Three.js提供的這一機(jī)制,可以在模型中附加任意自定義數(shù)據(jù)
多層次對(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í)間差來(lái)保證動(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)包括:
模型加載與材質(zhì)應(yīng)用
坐標(biāo)系轉(zhuǎn)換與對(duì)齊
模型位置和旋轉(zhuǎn)更新
動(dòng)畫效果實(shí)現(xiàn)
交互事件處理
視覺反饋效果
性能優(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í)菜單效果,涉及JavaScript頁(yè)面元素的遍歷及動(dòng)態(tài)操作技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09
使用js判斷TextBox控件值改變?nèi)缓蟪霭l(fā)事件
這篇文章主要介紹了使用js判斷TextBox控件值改變?nèi)缓蟪霭l(fā)事件。需要的朋友可以過來(lái)參考下,希望對(duì)大家有所幫助2014-03-03
手動(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ī)型(附完整案例),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04
javascript Canvas動(dòng)態(tài)粒子連線
這篇文章主要為大家詳細(xì)介紹了javascript Canvas動(dòng)態(tài)粒子連線,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-01-01
JS 在數(shù)組插入字符的實(shí)現(xiàn)代碼(可參考JavaScript splice() 方法)
在數(shù)組插入字符,添加數(shù)組,刪除數(shù)組可以用slice自帶的方法。操作比較方便,這個(gè)代碼是作者通過push與shift方法實(shí)現(xiàn),只能是個(gè)思路,但不推薦這樣的方法。2009-12-12

