前些天写了有关vue兼容高德1.4版本的3D地图,以及在地图中展示obj模型的相关要点,这几天发现高德1.4和2.0版本间互相切换的时候会有问题,于是给项目的地图版本版本升了级,现在全部使用高德2.0的script js api组件,目前看效果要比1.4更好看
本文将深入分析一个使用Vue.js、高德地图API和Three.js开发的无人车监控系统前端实现。该系统能够实时显示车辆位置、状态、轨迹,并提供派单、监控、任务管理等功能。
async initMap() {
try {
window.AMap = await AMapLoader.load({
key: process.env.VUE_APP_AMAP_KEY,
version: "2.0",
plugins: ['Map3D']
});
this.AMaper = window.AMap;
this.map = new this.AMaper.Map("container", {
viewMode: '3D',
showBuildingBlock: true,
rotation: 0,
center: [122.1234, 37.4567],
pitch: 60,
zoom: 17,
features: ['bg', 'road', 'building', 'point'],
mapStyle: 'amap://styles/normal'
});
要点:
这段代码使用AMapLoader.load方法异步加载高德地图API。这是一个Promise-based的加载方式,通过await等待加载完成。
关键技术点:
配置参数详解:
这里使用Promise结合事件监听和超时保护确保地图加载完成。通过同时设置事件监听和超时,无论哪种情况先发生都能继续执行后续代码,避免无限等待。
完善的错误捕获机制,当地图初始化失败时提供用户友好的错误提示,增强应用健壮性。
this.threeLayer = new ThreeLayer(this.map);
this.threeLayer.on('complete', () => {
const ambientLight = new THREE.AmbientLight('#ffffff', 1);
this.threeLayer.add(ambientLight);
const directionalLight = new THREE.DirectionalLight('#ffffff', 0.8);
directionalLight.position.set(0, 1, 0);
this.threeLayer.add(directionalLight);
resolve();
});
要点:
ThreeLayer是将Three.js与高德地图集成的核心组件,它创建一个覆盖在地图上的3D渲染层,允许在地图上放置Three.js对象。
光照系统详解:
使用Promise封装ThreeLayer的'complete'事件,确保在Three.js图层完全初始化后再执行后续代码,防止渲染错误。
ThreeLayer自动处理高德地图的经纬度坐标系统和Three.js的笛卡尔坐标系统之间的转换。当地图进行平移、缩放、旋转等操作时,ThreeLayer会自动调整Three.js场景,保持3D模型与地图位置的正确对应。
Three.js渲染需要较高的GPU性能,ThreeLayer实现了智能渲染策略:
const gltf = new ThreeGltf(this.threeLayer, {
url: this.vehicleGlbUrl,
position: [vehicle.longitude, vehicle.latitude],
height: 0,
scale: 5,
rotation: { x: 180, y: 0, z: 180 },
configLoader: (loader) => {
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/[email protected]/examples/js/libs/draco/');
loader.setDRACOLoader(dracoLoader);
},
onLoaded: (modelGroup) => {
const heading = vehicle.heading || 0;
modelGroup.rotation.set(
THREE.MathUtils.degToRad(90),
THREE.MathUtils.degToRad(-90 - heading),
THREE.MathUtils.degToRad(0)
);
gltf.modelGroup = modelGroup;
}
});
要点:
参数详解:
DRACO解码器的作用:
技术要点:
实现容错机制,当3D模型加载失败时自动降级使用简单2D标记,确保功能可用性,提升用户体验。
createVehicleLabelHTML(vehicle, isInOverlappingGroup, isFirstInGroup, groupSize, labelOffset, shouldShow) {
const isOffline = !vehicle.online;
const isWarning = this.isVehicleWarning(vehicle);
const labelContent = vehicle.no || vehicle.id.substring(0, 8);
const stackCountHtml = isInOverlappingGroup && isFirstInGroup && groupSize > 1 ?
`${groupSize}` : '';
let backgroundColor = 'rgba(0,0,0,0.7)'; // 默认黑色透明背景(离线)
let textColor = 'white'; // 默认文字颜色为白色
if (!isOffline) {
if (isWarning) {
backgroundColor = 'rgba(255,0,0,0.6)'; // 故障状态:红色透明背景
textColor = 'white'; // 故障状态:文字为白色
} else {
backgroundColor = 'rgba(0,255,0,0.5)'; // 正常在线:绿色透明背景
textColor = 'rgba(0,0,0,0.5)'; // 正常在线:文字为灰色,提高可读性
}
}
// ...返回HTML标签代码
}
要点:
状态判断详解:
驾驶控制模式异常(drivingCmdControlMode === -1)
驱动系统错误(driveSystemError为1或2)
电池故障(errorPowerBattery === 1)
电机故障(errorMotor === 1)
直流转换器故障(errorDc === 1)
关键技术点:
实现细节:
checkVehicleOverlapping() {
const allVehicles = [...this.vehicles];
const overlappingGroups = [];
if (allVehicles.length < 2) {
this.overlappingGroups = [];
return;
}
const positionGroups = {};
allVehicles.forEach(vehicle => {
if (!vehicle.longitude || !vehicle.latitude) return;
const key = `${Math.round(vehicle.longitude * 100000) / 100000},${Math.round(vehicle.latitude * 100000) / 100000}`;
if (!positionGroups[key]) {
positionGroups[key] = [];
}
positionGroups[key].push(vehicle.id);
});
Object.values(positionGroups).forEach(group => {
if (group.length > 1) {
overlappingGroups.push(group);
}
});
this.overlappingGroups = overlappingGroups;
}
要点:
算法详解:
辅助函数作用:
偏移计算逻辑:
交互控制要点:
交互设计细节:
updateLabelVisibility() {
this.checkVehicleOverlapping();
this.vehicles.forEach(vehicle => {
const labelElement = document.getElementById(`vehicle-label-${vehicle.id}`);
if (!labelElement) return;
const isInOverlappingGroup = this.isVehicleInOverlappingGroup(vehicle.id);
const groupIndex = this.getVehicleIndexInGroup(vehicle.id);
const isFirstInGroup = groupIndex === 0;
if (!isInOverlappingGroup || isFirstInGroup || this.showOfflineLabels) {
labelElement.style.opacity = '1';
labelElement.style.pointerEvents = 'auto';
if (isInOverlappingGroup && !isFirstInGroup && this.showOfflineLabels) {
const labelHeight = 20;
const verticalSpacing = 5;
labelElement.style.top = `${25 + groupIndex * (labelHeight + verticalSpacing)}px`;
} else {
labelElement.style.top = '25px';
}
} else {
labelElement.style.opacity = '0';
labelElement.style.pointerEvents = 'none';
}
if (isInOverlappingGroup && isFirstInGroup) {
labelElement.classList.add('has-dropdown');
} else {
labelElement.classList.remove('has-dropdown');
}
});
}
要点:
布局策略详解:
技术要点:
交互设计亮点:
addSelectionEffect(vehicleId) {
this.removeSelectionEffect();
const marker = this.vehicleGltfModels[vehicleId];
if (!marker) return;
// ...获取车辆位置
const radius = 5;
const maxRadius = 20;
const ringCount = 3;
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);
}
this.selectionEffect = {
circles: effects,
vehicleId: vehicleId,
animation: {
startRadius: radius,
maxRadius: maxRadius,
currentPhase: 0,
ringCount: ringCount,
lastUpdateTime: Date.now(),
animationSpeed: 0.3
}
};
}
要点:
选中效果技术详解:
兼容性处理策略:
资源管理考量:
animateSelectionEffect() {
if (!this.selectionEffect) return;
const effect = this.selectionEffect;
const now = Date.now();
const delta = (now - effect.animation.lastUpdateTime) / 1000;
effect.animation.lastUpdateTime = now;
effect.animation.currentPhase = (effect.animation.currentPhase + delta * effect.animation.animationSpeed) % 1;
// ...获取车辆位置
for (let i = 0; i < effect.animation.ringCount; i++) {
const phaseOffset = i / effect.animation.ringCount;
let phase = (effect.animation.currentPhase + phaseOffset) % 1;
const radiusDelta = effect.animation.maxRadius - effect.animation.startRadius;
const currentRadius = effect.animation.startRadius + (phase * radiusDelta);
let opacity;
if (phase < 0.1) {
opacity = phase * 10 * 0.8;
} else if (phase > 0.7) {
const fadeOutPhase = (phase - 0.7) / 0.3;
opacity = 0.8 * (1 - fadeOutPhase);
} else {
opacity = 0.8;
}
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
});
}
}
要点:
动画框架详解:
动画算法解析:
性能与体验优化点:
async updateVehicleTrack(params) {
try {
const response = await this.getTrack(params);
if (this.currentTrack) {
this.map.remove(this.currentTrack);
this.currentTrack = null;
}
// ...清理旧标记
const path = response.data;
if (!path || !Array.isArray(path) || path.length === 0) {
return;
}
// ...验证路径数据
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
});
// ...添加起点和终点标记
} catch (error) {
console.error('更新车辆轨迹失败:', error);
}
}
要点:
// 组件导入
import MonitorPopup from '../../../components/MonitorPopup';
import LeftPopup from '../../../components/PopupLeft';
import RightPopup from '../../../components/PopupRight';
import StationPopup from '../../../components/StationPopup';
import TaskPopup from '../../../components/TaskPopup';
import UseVehiclePopup from '../../../components/UseVehiclePopup';
// 组件注册
components: {
LeftPopup,
RightPopup,
StationPopup,
UseVehiclePopup,
TaskPopup,
MonitorPopup,
},
// 弹窗状态管理
data() {
return {
isPopupVisible: false,
isRightPopupExpanded: false,
isStationPopupVisible: false,
isUseVehiclePopupVisible: false,
isTaskPopupVisible: false,
isMonitorPopupVisible: false,
// ...其他状态
};
},
要点:
组件化设计优势:
状态管理策略:
交互流程设计:
async fetchStationData() {
try {
const response = await listStaion();
if (response && response.code === 200) {
this.stations = response.data || response.rows || [];
this.displayStationsOnMap();
}
} catch (error) {
console.error("获取站点数据异常:", error);
}
}
displayStationsOnMap() {
if (!this.map || !this.stations.length) return;
// ...清理旧标记
this.stationMarkers = this.stations.map(station => {
const iconSrc = this.getIconSrc(station.icon);
const marker = new this.AMaper.Marker({
position: [station.longitude, station.latitude],
icon: new this.AMaper.Icon({
image: iconSrc,
size: new this.AMaper.Size(32, 32),
imageSize: new this.AMaper.Size(32, 32),
}),
title: station.name,
});
this.map.add(marker);
marker.on('click', () => {
this.handleStationClick(station);
});
return marker;
});
}
要点:
数据处理特点:
实现技术点:
资源管理策略:
坐标转换精髓:
beforeDestroy() {
clearInterval(this.syncVehicleDataInterval);
// ...清理站点标记
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
// ...清理标记和连接线
if (this.vehicleGltfModels) {
Object.keys(this.vehicleGltfModels).forEach(vehicleId => {
const model = this.vehicleGltfModels[vehicleId];
if (model) {
try {
// ...清理标签和模型
if (model instanceof ThreeGltf) {
if (model.modelGroup) {
if (model.modelGroup.parent) {
model.modelGroup.parent.remove(model.modelGroup);
}
// 释放模型资源
if (model.modelGroup.traverse) {
model.modelGroup.traverse((child) => {
if (child.geometry) {
child.geometry.dispose();
}
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach(material => material.dispose());
} else {
child.material.dispose();
}
}
});
}
}
// ...销毁模型和移除引用
}
} catch (e) {
console.warn("移除车辆模型出错:", e);
}
}
});
}
// ...清理选中效果和地图实例
}
要点:
资源清理全面策略:
Three.js资源管理精要:
智能更新机制:
async fetchVehicleData() {
try {
const response = await listOnlineVehicles();
if (response && response.code === 200) {
const vehicles = response.data || [];
// ...处理数据
const vehicleChanged = this.vehicles.length !== processedVehicles.length ||
this.vehicles.some(oldV => {
const newV = processedVehicles.find(v => v.id === oldV.id);
return !newV ||
oldV.online !== newV.online ||
oldV.longitude !== newV.longitude ||
oldV.latitude !== newV.latitude;
});
this.vehicles = processedVehicles;
this.allVehicles = [...processedVehicles];
// ...根据变化决定是重建还是更新
this.$nextTick(() => {
this.updateLabelVisibility();
});
// ...更新选中车辆信息
}
} catch (error) {
console.error("获取车辆数据异常:", error);
}
},
mounted() {
// ...初始化地图
this.syncVehicleDataInterval = setInterval(() => {
this.fetchVehicleData();
}, 3000);
// ...其他初始化
},
要点:
实时更新设计:
数据处理精髓:
高效更新策略:
实时信息更新技术:
订单检查机制:
try {
// 尝试使用3D模型
// ...3D模型相关代码
} catch (err) {
console.error("3D模型加载失败,使用备选标记:", err);
const backupMarker = this.createSimpleMarker(vehicle);
this.vehicleGltfModels[vehicle.id] = backupMarker;
return backupMarker;
}
要点:当3D模型加载失败时,系统会自动降级使用简单的2D标记,确保基本功能可用。
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;
}
要点:通过多条件组合判断车辆故障状态,根据不同的硬件故障指标综合评估。
handleStationClick(station) {
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();
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;
}
要点:将地理坐标转换为屏幕坐标,实现弹窗精确定位。
为解决车辆密集区域标签重叠问题,系统实现了标签堆叠与悬停展开功能:
系统使用三种颜色区分车辆状态:
选中效果通过动态水波纹圆环提供明显视觉反馈。
重点关注Three.js资源释放,包括:
本项目成功地将高德地图API与Three.js 3D库无缝集成,实现了传统2D地图与3D模型的混合渲染。特别是ThreeLayer的使用,解决了地理坐标系与3D笛卡尔坐标系的转换问题,为车辆实时监控提供了更直观的可视化效果。
项目实现了创新的车辆标签系统,包括:
选中车辆时的水波纹动画效果展示了先进的前端可视化能力:
项目展现了专业的前端资源管理策略:
通过Vue组件化思想,项目实现了高度模块化的设计:
难点:Three.js默认使用笛卡尔坐标系,而地图使用经纬度坐标系,两者集成困难。
解决方案:
难点:地图上车辆密集区域的标签重叠严重影响可读性。
解决方案:
难点:频繁更新车辆位置和状态容易导致内存泄漏和性能问题。
解决方案:
难点:需要保持车辆位置、状态实时更新的同时维持良好的界面响应性。
解决方案:
这个无人车监控系统前端实现综合运用了Vue.js、高德地图API和Three.js,展示了Web前端在复杂可视化系统中的应用能力。特别是3D模型与地图的集成、车辆状态管理、重叠标签处理等功能,都体现了较高的技术水平和用户体验设计。
通过组件化设计和良好的资源管理,系统能够流畅展示大量车辆实时状态,并提供丰富的交互功能,为无人车调度管理提供了直观、高效的可视化界面。