
📋 文章目录
引言
Cesium作为一款强大的3D地理信息可视化库,在WebGIS开发中有着广泛的应用。本文将详细介绍如何在Vue框架下基于Cesium实现交互式多边形绘制与编辑功能,包括颜色自定义、透明度调整以及面积实时计算等实用功能,并解决开发过程中遇到的各种常见问题。
无论是GIS应用、智慧城市还是工程测量系统,多边形绘制都是核心功能之一。通过本文的实现方案,你将能够快速集成专业的多边形编辑工具到自己的Cesium项目中,并掌握Cesium事件处理和状态管理的最佳实践。
功能概述
本文实现的多边形工具具有以下特性:
✅ 交互式绘制 :左键点击添加顶点,右键结束绘制 ✅ 顶点编辑 :拖拽顶点实时调整多边形形状 ✅ 样式自定义 :支持颜色选择和透明度调整 ✅ 面积计算 :自动计算并显示多边形面积(平方公里) ✅ 交互控制:绘制/编辑时禁用地图默认交互,完成后恢复
环境准备
在开始之前,请确保你的项目中已安装以下依赖:
# 安装Cesium
npm install cesium --save
# 安装Turf.js用于面积计算
npm install @turf/turf --save
核心实现步骤
1. 地图初始化
首先,我们需要初始化Cesium地图实例。在Vue组件的mounted钩子中完成地图的加载:
import initMap from '@/config/initMap.js';
import { mapConfig } from '@/config/mapConfig';
export default {
data() {
return {
viewer: null,
// 绘制状态管理
isDrawing: false,
isEditing: false,
currentPolygon: null,
polygonPoints: [],
dynamicPoints: [],
vertexEntities: [],
vertexHandlers: [], // 顶点事件处理器数组
handler: null,
polygonColor: '#0000FF', // 默认蓝色
polygonAlpha: 0.5, // 默认透明度
areaLabel: null // 面积标签实体
};
},
mounted() {
// 初始化地图
this.viewer = initMap(mapConfig.gaode.url3, false);
// 初始化绘制处理器
this.initDrawHandler();
}
}
2. 多边形绘制
多边形绘制是通过监听鼠标事件实现的,主要分为三个阶段:
- 左键点击:添加顶点并更新多边形
- 鼠标移动:动态显示多边形轮廓
- 右键点击:结束绘制并创建最终多边形
核心代码实现:
// 开始绘制多边形
startDrawPolygon() {
if (this.isEditing) this.stopEditPolygon();
this.isDrawing = true;
this.polygonPoints = [];
this.dynamicPoints = [];
this.disableMapInteraction();
// 左键点击添加顶点
this.handler.setInputAction(this.handleLeftClick, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// 鼠标移动更新动态轮廓
this.handler.setInputAction(this.handleMouseMove, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
// 右键结束绘制
this.handler.setInputAction(this.handleRightClick, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
// 创建动态多边形实体
this.createDynamicPolygon();
},
// 创建动态多边形(绘制过程中的临时多边形)
createDynamicPolygon() {
// 移除已存在的动态多边形
if (this.currentPolygon) {
this.viewer.entities.remove(this.currentPolygon);
}
this.currentPolygon = this.viewer.entities.add({
polygon: {
hierarchy: new Cesium.CallbackProperty(() => {
return new Cesium.PolygonHierarchy(this.dynamicPoints);
}, false),
material: Cesium.Color.RED.withAlpha(0.3),
outline: true,
outlineColor: Cesium.Color.RED
}
});
}
3. 顶点编辑功能
编辑功能允许用户通过拖拽顶点来调整多边形形状,实现思路是为每个顶点创建可交互的点实体,并监听其拖拽事件:
// 为每个顶点添加拖拽事件
addVertexDragHandler(vertexEntity, index) {
const vertexHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);
// 将处理器存储到数组,以便后续销毁
this.vertexHandlers.push(vertexHandler);
vertexHandler.setInputAction((event) => {
const pick = this.viewer.scene.pick(event.position);
if (Cesium.defined(pick) && pick.id === vertexEntity) {
// 开始拖拽
const moveHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);
moveHandler.setInputAction((moveEvent) => {
const newCartesian = this.getCartesianFromMouse(moveEvent.endPosition);
if (newCartesian) {
// 更新顶点位置
this.polygonPoints[index] = newCartesian;
vertexEntity.position.setValue(newCartesian);
// 更新多边形
this.currentPolygon.polygon.hierarchy.setValue(
new Cesium.PolygonHierarchy(this.polygonPoints)
);
// 更新面积显示
this.calculateAndShowArea();
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
// 结束拖拽
vertexHandler.setInputAction(() => {
moveHandler.destroy();
}, Cesium.ScreenSpaceEventType.LEFT_UP);
}
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
}
4. 颜色与透明度自定义
通过添加颜色选择器和透明度滑块,允许用户自定义多边形样式:
// 更新多边形样式
updatePolygonStyle() {
if (!this.currentPolygon) return;
try {
const color = Cesium.Color.fromCssColorString(this.polygonColor)
.withAlpha(this.polygonAlpha);
this.currentPolygon.polygon.material = color;
} catch (e) {
console.error('颜色解析错误:', e);
// 使用默认颜色作为fallback
this.currentPolygon.polygon.material = Cesium.Color.BLUE.withAlpha(0.5);
}
}
5. 面积计算与显示
使用Turf.js库计算多边形面积,并在多边形中心显示面积标签:
// 计算并显示多边形面积
calculateAndShowArea() {
try {
if (this.polygonPoints.length < 3) return;
// 将笛卡尔坐标转换为经纬度
const coordinates = this.polygonPoints.map(cartesian => {
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
return [
Cesium.Math.toDegrees(cartographic.longitude),
Cesium.Math.toDegrees(cartographic.latitude)
];
});
// 闭合多边形
coordinates.push(coordinates[0]);
// 使用Turf.js计算面积
const polygon = turf.polygon([coordinates]);
const areaSquareMeters = turf.area(polygon);
const areaSquareKm = areaSquareMeters / 1000000;
const areaText = `面积: ${areaSquareKm.toFixed(4)} 平方公里`;
// 显示面积标签
this.showAreaLabel(areaText);
} catch (e) {
console.error('面积计算错误:', e);
this.showAreaLabel('面积计算失败');
}
},
// 在多边形中心显示面积标签
showAreaLabel(text) {
// 移除旧标签
if (this.areaLabel) {
this.viewer.entities.remove(this.areaLabel);
}
// 计算多边形中心点
const center = this.calculatePolygonCenter();
// 创建新标签
this.areaLabel = this.viewer.entities.add({
position: center,
label: {
text: text,
font: '16px sans-serif',
fillColor: Cesium.Color.YELLOW,
backgroundColor: Cesium.Color.BLACK.withAlpha(0.7),
padding: new Cesium.Cartesian2(12, 8),
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
verticalOrigin: Cesium.VerticalOrigin.CENTER,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
outline: true,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 1
}
});
}
常见问题解决方案
在开发过程中,我们可能会遇到以下问题:
1. 多边形颜色显示异常
问题:设置颜色后多边形显示为白色或不生效。
解决方案:
-
确保颜色值是有效的CSS颜色字符串
-
检查透明度值是否为数字类型
-
添加错误处理和默认颜色 fallback
// 安全的颜色设置方法
try {
const color = Cesium.Color.fromCssColorString(this.polygonColor)
.withAlpha(Number(this.polygonAlpha) || 0.5); // 确保alpha为数字
this.currentPolygon.polygon.material = color;
} catch (e) {
console.error('颜色解析错误:', e);
// 使用默认颜色作为fallback
this.currentPolygon.polygon.material = Cesium.Color.BLUE.withAlpha(0.5);
}
2. 面积标签不可见
问题:面积标签不显示或被地形遮挡。
解决方案:
-
提升标签高度,避免被地形遮挡
-
使用醒目颜色和背景提高可见性
-
设置
disableDepthTestDistance
确保标签始终显示在最前面// 计算中心点时提升高度
calculatePolygonCenter() {
// ... 经纬度计算逻辑 ...// 将中心点转换回笛卡尔坐标,并提升高度 return Cesium.Cartesian3.fromDegrees(centerLon, centerLat, 50); // 提升50米高度
}
3. 控制台alpha类型错误
问题 :Expected alpha to be typeof number, actual typeof was string
解决方案:
-
确保透明度值为数字类型
-
在Vue中使用
.number
修饰符或手动转换类型
4. 地图交互无法恢复
问题:结束绘制或编辑后,地图依然不能缩放或移动。
解决方案:
- 增强交互恢复方法,确保所有控制参数被正确设置
- 管理所有事件处理器的生命周期,确保完全销毁
- 在所有退出路径调用交互恢复方法
// 恢复地图交互(增强版)
enableMapInteraction() {
const controller = this.viewer.scene.screenSpaceCameraController;
// 确保所有输入被启用
controller.enableInputs = true;
// 恢复所有相机控制
controller.enableRotate = true;
controller.enableZoom = true;
controller.enableTranslate = true;
controller.enableTilt = true;
controller.enableLook = true;
console.log("地图交互已完全恢复");
},
// 停止编辑多边形(修复版)
stopEditPolygon() {
this.isEditing = false;
// 销毁所有顶点拖拽事件处理器
this.vertexHandlers.forEach(handler => {
if (handler && !handler.isDestroyed()) {
handler.destroy();
}
});
this.vertexHandlers = []; // 清空处理器数组
this.enableMapInteraction();
}
完整代码
Vue组件代码
<template>
<div id="cesiumContainer" style="width: 100%; height: 100vh">
<div class="polygon-controls">
<label>多边形颜色:</label>
<input type="color" v-model="polygonColor" @input="updatePolygonStyle" />
<label>透明度:</label>
<input
type="range"
min="0"
max="1"
step="0.1"
v-model.number="polygonAlpha"
@input="updatePolygonStyle"
/>
</div>
<div class="">结束绘制</div>
</div>
</template>
<script>
import initMap from '@/config/initMap.js';
import { mapConfig } from '@/config/mapConfig';
// import * as Cesium from 'cesium';
import * as turf from '@turf/turf';
export default {
data() {
return {
viewer: null,
// 绘制状态管理
isDrawing: false,
isEditing: false,
currentPolygon: null,
polygonPoints: [],
dynamicPoints: [],
vertexEntities: [],
handler: null,
polygonColor: '#0000FF', // 默认蓝色
polygonAlpha: 0.5, // 默认透明度
areaLabel: null, // 面积标签实体
vertexHandlers: [], // 新增:存储顶点拖拽事件处理器
};
},
mounted() {
this.viewer = initMap(mapConfig.gaode.url3, false);
this.initDrawHandler();
},
methods: {
// 初始化绘制事件处理器
initDrawHandler() {
this.handler = new Cesium.ScreenSpaceEventHandler(
this.viewer.scene.canvas
);
this.startDrawPolygon();
},
// 开始绘制多边形
startDrawPolygon() {
console.log('开始绘制多边形');
if (this.isEditing) this.stopEditPolygon();
this.isDrawing = true;
this.polygonPoints = [];
this.dynamicPoints = [];
this.disableMapInteraction();
// 左键点击添加顶点
this.handler.setInputAction(
this.handleLeftClick,
Cesium.ScreenSpaceEventType.LEFT_CLICK
);
// 鼠标移动更新动态轮廓
this.handler.setInputAction(
this.handleMouseMove,
Cesium.ScreenSpaceEventType.MOUSE_MOVE
);
// 右键结束绘制
this.handler.setInputAction(
this.handleRightClick,
Cesium.ScreenSpaceEventType.RIGHT_CLICK
);
// 创建动态多边形实体
this.createDynamicPolygon();
},
// 处理左键点击添加顶点
handleLeftClick(event) {
const cartesian = this.getCartesianFromMouse(event.position);
if (!cartesian) return;
this.polygonPoints.push(cartesian);
this.dynamicPoints.push(cartesian);
// 添加顶点标记
this.addVertexMarker(cartesian);
},
// 处理鼠标移动更新轮廓
handleMouseMove(event) {
if (this.polygonPoints.length === 0 || !this.isDrawing) return;
const cartesian = this.getCartesianFromMouse(event.endPosition);
if (!cartesian) return;
// 更新动态点
if (this.dynamicPoints.length > this.polygonPoints.length) {
this.dynamicPoints.pop();
}
this.dynamicPoints.push(cartesian);
},
// 处理右键结束绘制
handleRightClick() {
if (this.polygonPoints.length < 3) {
alert('至少需要3个顶点才能形成多边形');
this.clearDrawing();
return;
}
// 移除动态点
this.dynamicPoints.pop();
this.isDrawing = false;
// 保存最终多边形
this.saveFinalPolygon();
// 清除临时事件
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE);
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.RIGHT_CLICK);
// 开启编辑模式
this.startEditPolygon();
// 确保交互已恢复(双重保险)
setTimeout(() => {
this.enableMapInteraction();
}, 100);
},
// 开始编辑多边形
startEditPolygon() {
this.isEditing = true;
this.disableMapInteraction();
// 为每个顶点添加拖拽事件
this.vertexEntities.forEach((vertex, index) => {
this.addVertexDragHandler(vertex, index);
});
},
// 添加顶点拖拽事件
addVertexDragHandler(vertexEntity, index) {
const vertexHandler = new Cesium.ScreenSpaceEventHandler(
this.viewer.scene.canvas
);
// 将处理器存储到数组,以便后续销毁
this.vertexHandlers.push(vertexHandler);
vertexHandler.setInputAction((event) => {
const pick = this.viewer.scene.pick(event.position);
if (Cesium.defined(pick) && pick.id === vertexEntity) {
// 开始拖拽
const moveHandler = new Cesium.ScreenSpaceEventHandler(
this.viewer.scene.canvas
);
moveHandler.setInputAction((moveEvent) => {
const newCartesian = this.getCartesianFromMouse(
moveEvent.endPosition
);
if (newCartesian) {
// 更新顶点位置
this.polygonPoints[index] = newCartesian;
vertexEntity.position.setValue(newCartesian);
// 更新多边形
this.currentPolygon.polygon.hierarchy.setValue(
new Cesium.PolygonHierarchy(this.polygonPoints)
);
// 更新面积显示
this.calculateAndShowArea();
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
// 结束拖拽
vertexHandler.setInputAction(() => {
moveHandler.destroy();
}, Cesium.ScreenSpaceEventType.LEFT_UP);
}
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
},
// 停止编辑多边形
stopEditPolygon() {
this.isEditing = false;
// 销毁所有顶点拖拽事件处理器
this.vertexHandlers.forEach((handler) => {
if (handler && !handler.isDestroyed()) {
handler.destroy();
}
});
this.vertexHandlers = []; // 清空处理器数组
this.enableMapInteraction();
},
// 创建动态多边形
createDynamicPolygon() {
// 移除已存在的动态多边形
if (this.currentPolygon) {
this.viewer.entities.remove(this.currentPolygon);
}
this.currentPolygon = this.viewer.entities.add({
polygon: {
hierarchy: new Cesium.CallbackProperty(() => {
return new Cesium.PolygonHierarchy(this.dynamicPoints);
}, false),
material: Cesium.Color.RED.withAlpha(0.3), // 绘制过程中使用红色
outline: true,
outlineColor: Cesium.Color.RED,
},
});
},
// 保存最终多边形(修复重复定义问题)
saveFinalPolygon() {
// 移除动态多边形
this.viewer.entities.remove(this.currentPolygon);
try {
// 验证颜色和透明度
console.log(
'当前颜色值:',
this.polygonColor,
'透明度:',
this.polygonAlpha
);
const color = Cesium.Color.fromCssColorString(
this.polygonColor
).withAlpha(Number(this.polygonAlpha) || 0.5);
// 创建最终多边形,应用自定义颜色和透明度
this.currentPolygon = this.viewer.entities.add({
polygon: {
hierarchy: new Cesium.PolygonHierarchy(this.polygonPoints),
material: color,
outline: true,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
},
});
// 计算并显示面积
this.calculateAndShowArea();
} catch (e) {
console.error('创建多边形失败:', e);
// 使用默认颜色作为fallback
this.currentPolygon = this.viewer.entities.add({
polygon: {
hierarchy: new Cesium.PolygonHierarchy(this.polygonPoints),
material: Cesium.Color.BLUE.withAlpha(0.5),
outline: true,
outlineColor: Cesium.Color.BLACK,
},
});
}
},
// 添加顶点标记
addVertexMarker(position) {
const vertexEntity = this.viewer.entities.add({
position: position,
point: {
pixelSize: 10,
color: Cesium.Color.YELLOW,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
});
this.vertexEntities.push(vertexEntity);
},
// 从鼠标位置获取笛卡尔坐标
getCartesianFromMouse(position) {
const ray = this.viewer.camera.getPickRay(position);
if (!ray) return null;
return this.viewer.scene.globe.pick(ray, this.viewer.scene);
},
// 清除绘制状态
clearDrawing() {
this.isDrawing = false;
this.polygonPoints = [];
this.dynamicPoints = [];
if (this.currentPolygon) {
this.viewer.entities.remove(this.currentPolygon);
}
this.vertexEntities.forEach((vertex) =>
this.viewer.entities.remove(vertex)
);
this.vertexEntities = [];
if (this.areaLabel) {
this.viewer.entities.remove(this.areaLabel);
this.areaLabel = null;
}
this.enableMapInteraction();
},
// 更新多边形样式
updatePolygonStyle() {
if (!this.currentPolygon) return;
try {
const color = Cesium.Color.fromCssColorString(
this.polygonColor
).withAlpha(Number(this.polygonAlpha) || 0.5);
this.currentPolygon.polygon.material = color;
console.log('样式更新成功:', color);
} catch (e) {
console.error('颜色解析错误:', e);
this.currentPolygon.polygon.material = Cesium.Color.BLUE.withAlpha(0.5);
}
},
// 计算并显示多边形面积
calculateAndShowArea() {
try {
if (this.polygonPoints.length < 3) return;
// 将笛卡尔坐标转换为经纬度
const coordinates = this.polygonPoints.map((cartesian) => {
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
return [
Cesium.Math.toDegrees(cartographic.longitude),
Cesium.Math.toDegrees(cartographic.latitude),
];
});
// 确保多边形闭合
if (
coordinates.length > 0 &&
!(
coordinates[0][0] === coordinates[coordinates.length - 1][0] &&
coordinates[0][1] === coordinates[coordinates.length - 1][1]
)
) {
coordinates.push([...coordinates[0]]);
}
// 使用Turf.js计算面积
const polygon = turf.polygon([coordinates]);
const areaSquareMeters = turf.area(polygon);
const areaSquareKm = areaSquareMeters / 1000000;
const areaText = `面积: ${areaSquareKm.toFixed(4)} 平方公里`;
console.log('计算面积:', areaText);
// 显示面积标签
this.showAreaLabel(areaText);
} catch (e) {
console.error('面积计算错误:', e);
this.showAreaLabel('面积计算失败');
}
},
// 在多边形中心显示面积标签(优化版本)
showAreaLabel(text) {
// 移除旧标签
if (this.areaLabel) {
this.viewer.entities.remove(this.areaLabel);
}
// 计算多边形中心点(优化算法)
const center = this.calculatePolygonCenter();
// 创建新标签,增强可见性
this.areaLabel = this.viewer.entities.add({
position: center,
label: {
text: text,
font: '16px sans-serif',
fillColor: Cesium.Color.YELLOW, // 使用醒目颜色
backgroundColor: Cesium.Color.BLACK.withAlpha(0.8), // 增强对比度
padding: new Cesium.Cartesian2(12, 8),
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
verticalOrigin: Cesium.VerticalOrigin.CENTER,
disableDepthTestDistance: Number.POSITIVE_INFINITY, // 始终显示在最前面
pixelOffset: new Cesium.Cartesian2(0, 0),
outline: true,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 1,
},
});
},
// 计算多边形中心点(更可靠的方法)
calculatePolygonCenter() {
if (this.polygonPoints.length === 0) return Cesium.Cartesian3.ZERO;
// 计算经纬度平均值
let totalLon = 0,
totalLat = 0;
const cartographics = this.polygonPoints.map((cartesian) =>
Cesium.Cartographic.fromCartesian(cartesian)
);
cartographics.forEach((cartographic) => {
totalLon += Cesium.Math.toDegrees(cartographic.longitude);
totalLat += Cesium.Math.toDegrees(cartographic.latitude);
});
const centerLon = totalLon / cartographics.length;
const centerLat = totalLat / cartographics.length;
// 将中心点转换回笛卡尔坐标,并提升高度避免被地形遮挡
return Cesium.Cartesian3.fromDegrees(centerLon, centerLat, 50);
},
// 禁用地图交互
disableMapInteraction() {
const controller = this.viewer.scene.screenSpaceCameraController;
controller.enableRotate = false;
controller.enableZoom = false;
controller.enableTranslate = false;
controller.enableTilt = false;
controller.enableLook = false;
},
// 恢复地图交互
enableMapInteraction() {
const controller = this.viewer.scene.screenSpaceCameraController;
// 确保所有输入被启用
controller.enableInputs = true;
// 恢复所有相机控制
controller.enableRotate = true;
controller.enableZoom = true;
controller.enableTranslate = true;
controller.enableTilt = true;
controller.enableLook = true;
// 重置鼠标事件处理
if (this.handler) {
this.handler.destroy();
this.handler = new Cesium.ScreenSpaceEventHandler(
this.viewer.scene.canvas
);
}
console.log('地图交互已完全恢复');
},
},
beforeDestroy() {
if (this.viewer) {
this.viewer.destroy();
}
if (this.handler) {
this.handler.destroy();
}
// 移除控制面板
const controlPanel = document.querySelector('.polygon-controls');
if (controlPanel) {
controlPanel.remove();
}
},
};
</script>
CSS样式
<style lang="scss" scoped>
#cesiumContainer {
width: 100%;
height: 100vh;
touch-action: none;
.polygon-controls {
position: absolute;
top: 20px;
right: 20px;
z-index: 1000;
background: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.polygon-controls label {
display: block;
margin: 10px 0 5px;
font-weight: bold;
}
.polygon-controls input {
width: 100%;
margin-bottom: 10px;
}
}
</style>
总结与扩展
本文详细介绍了如何基于Cesium和Vue实现交互式多边形绘制与编辑功能,包括核心功能实现、样式自定义和面积计算等关键技术点。特别解决了开发过程中常见的颜色显示异常、标签不可见、类型错误和交互无法恢复等问题。
通过事件监听、动态属性更新和地理空间计算,我们构建了一个功能完善的多边形编辑工具。重点强调了事件处理器生命周期管理和状态控制的最佳实践,这些经验对于开发复杂Cesium交互功能具有普遍参考价值。
功能扩展方向
- 添加删除功能:允许用户删除多边形或单个顶点
- 支持多个多边形:管理多个多边形图层
- 导入导出:支持GeoJSON格式导入导出
- 测量工具:添加距离测量、角度测量等功能
- 样式库:预设多种多边形样式供选择
- 撤销/重做:实现操作历史记录功能
希望本文能帮助你快速掌握Cesium多边形绘制技术,如果有任何问题或建议,欢迎在评论区留言讨论!
原创不易,转载请注明出处
如果本文对你有帮助,别忘了点赞和关注哦!