
引言
Cesium是一款强大的开源3D地理信息可视化引擎,广泛应用于数字地球、地图可视化等领域。在Vue项目中集成Cesium可以快速构建高性能的地理信息应用。本文将详细介绍如何在Vue项目中实现交互式折线绘制功能,包括顶点添加、临时绘制、距离计算等核心功能,并为新手提供详细的代码注释和学习资源。
Cesium核心概念速览
在开始之前,我们先了解几个Cesium的核心概念:
- Viewer:Cesium的核心实例,用于创建和管理3D场景
- Entity:高层次的对象封装,用于创建和管理可视化对象(如点、线、面)
- Primitive:低层次的渲染对象,比Entity更高效,适合大量数据渲染
- Cartesian3:三维笛卡尔坐标,Cesium中表示位置的基本方式
- ScreenSpaceEventHandler:用于处理用户输入事件(如点击、鼠标移动)
环境准备
假设已完成Cesium 2D地图初始化,需要安装以下依赖:
npm install cesium lodash
核心功能实现
折线绘制的核心流程如下:
- 点击地图添加顶点
- 鼠标移动时更新临时折线
- 双击结束绘制并保存折线
- 右键取消绘制
代码解析
1. 数据属性定义
data() {
return {
viewer: null, // Cesium Viewer实例
isDrawing: false, // 是否处于绘制状态
currentPositions: [], // 当前折线的顶点坐标数组
tempPrimitive: null, // 临时折线Primitive对象
allPolylines: [], // 保存所有已绘制的折线
handler: null, // 屏幕空间事件处理器
isFirstClick: true, // 是否是首次点击(用于开始绘制)
vertexMarkers: [], // 顶点标记Entity数组
};
}
2. 初始化事件处理器
initDrawLine() {
// 创建屏幕空间事件处理器,用于监听用户在地图上的交互
this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.canvas);
// 移除默认的双击事件,避免与自定义双击结束绘制冲突
this.viewer.cesiumWidget.screenSpaceEventHandler.removeInputAction(
Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK
);
// 显示操作提示
this.showToast('点击添加顶点,双击结束绘制(至少需要3个顶点)');
// 绑定左键单击事件 - 添加折线顶点
this.handler.setInputAction((event) => {
// 禁用地图交互,防止绘制时误操作(如旋转、缩放地图)
this.disableMapInteraction();
// 将鼠标点击位置转换为地球表面坐标
const position = this.getPositionFromMouse(event.position);
if (!position) return;
// 检测与上一顶点的距离,避免过近的重复顶点
if (this.currentPositions.length > 0) {
const lastPosition = this.currentPositions[this.currentPositions.length - 1];
const distance = Cesium.Cartesian3.distance(position, lastPosition);
const DISTANCE_THRESHOLD = 1.0; // 距离阈值(米)
if (distance < DISTANCE_THRESHOLD) {
this.showToast(`点击位置与上一顶点距离过近(${distance.toFixed(2)}米),已忽略`);
this.enableMapInteraction(); // 重新启用地图交互
return;
}
}
// 首次点击时标记开始绘制
if (this.isFirstClick) {
this.isDrawing = true;
this.isFirstClick = false;
}
// 添加顶点坐标并显示标记
this.currentPositions.push(position);
this.addVertexMarker(position);
// 绘制临时折线(此时鼠标未移动,传入null)
this.drawTempLine(null);
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
// 鼠标移动 - 更新临时折线
this.handler.setInputAction((event) => {
if (!this.isDrawing || this.currentPositions.length === 0) return;
const position = this.getPositionFromMouse(event.endPosition);
if (position) {
this.throttledDrawTempLine(position); // 使用节流优化性能
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
// 左键双击 - 结束绘制
this.handler.setInputAction((event) => {
if (!this.isDrawing) return;
const position = this.getPositionFromMouse(event.position);
if (position) {
this.currentPositions.push(position);
this.addVertexMarker(position);
}
// 验证顶点数量,至少需要3个顶点才能形成闭合区域
if (this.currentPositions.length < 3) {
this.showToast('折线至少需要3个顶点,请继续添加');
return;
}
this.savePolyline(); // 保存折线
this.clearTempLine(); // 清除临时折线
this.resetDrawingState(); // 重置绘制状态
this.enableMapInteraction(); // 重新启用地图交互
}, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
// 右键单击 - 取消绘制
this.handler.setInputAction(() => {
if (this.isDrawing) {
this.clearTempLine();
this.resetDrawingState();
this.enableMapInteraction();
this.showToast('已取消绘制');
}
}, Cesium.ScreenSpaceEventType.RIGHT_DOWN);
3. 坐标转换
javascript
getPositionFromMouse(mousePosition) {
// 创建从相机到鼠标位置的射线
const ray = this.viewer.camera.getPickRay(mousePosition);
if (!ray) return null;
// 计算射线与地球表面的交点(获取地理坐标)
const position = this.viewer.scene.globe.pick(ray, this.viewer.scene);
if (!position) {
this.showToast('请在地球表面点击');
}
return position;
}
4. 临时折线绘制
javascript
drawTempLine(currentMousePosition) {
this.clearTempLine(); // 清除已有临时折线
if (this.currentPositions.length === 0 || !currentMousePosition) return;
// 创建包含已有顶点和当前鼠标位置的临时坐标数组
const tempPositions = [...this.currentPositions, currentMousePosition];
// 创建临时折线Primitive
this.tempPrimitive = new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: new Cesium.PolylineGeometry({
positions: tempPositions, // 折线顶点坐标
width: 5, // 折线宽度
vertexFormat: Cesium.PolylineMaterialAppearance.VERTEX_FORMAT,
}),
}),
appearance: new Cesium.PolylineMaterialAppearance({
material: Cesium.Material.fromType('Color', {
color: Cesium.Color.RED.withAlpha(0.8), // 临时折线为半透明红色
}),
}),
});
// 将临时折线添加到场景中
this.viewer.scene.primitives.add(this.tempPrimitive);
5. 折线保存与长度计算
javascript
savePolyline() {
// 创建最终折线Primitive
const polyline = new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: new Cesium.PolylineGeometry({
positions: this.currentPositions,
width: 5,
vertexFormat: Cesium.PolylineMaterialAppearance.VERTEX_FORMAT,
}),
}),
appearance: new Cesium.PolylineMaterialAppearance({
material: Cesium.Material.fromType('Color', {
color: Cesium.Color.BLUE.withAlpha(0.8), // 最终折线为半透明蓝色
}),
}),
});
// 添加到场景并保存引用
this.viewer.scene.primitives.add(polyline);
this.allPolylines.push(polyline);
// 计算并显示折线总长度
const totalLength = this.calculatePolylineLength(this.currentPositions);
this.showToast(
`折线绘制完成!顶点数: ${this.currentPositions.length}, 总长度: ${totalLength.toFixed(2)}米`
);
}
// 计算折线总长度
calculatePolylineLength(positions) {
let totalLength = 0;
// 遍历所有顶点,累加相邻顶点间的距离
for (let i = 0; i < positions.length - 1; i++) {
// 使用Cesium提供的Cartesian3距离计算方法,单位为米
totalLength += Cesium.Cartesian3.distance(
positions[i],
positions[i + 1]
);
}
return totalLength;
}
性能优化
- 节流处理:使用Lodash的throttle函数限制鼠标移动时的重绘频率
javascript
created() {
// 节流处理临时绘制,50ms内最多执行一次,优化性能
this.throttledDrawTempLine = throttle(this.drawTempLine, 50);
}
-
顶点去重:通过距离检测避免添加过近的重复顶点
-
资源销毁:组件销毁时清理Cesium资源,避免内存泄漏
javascript
beforeDestroy() {
if (this.viewer) this.viewer.destroy(); // 销毁Viewer实例
if (this.handler) this.handler.destroy(); // 销毁事件处理器
}
常见问题与调试
- 地图初始化失败:检查Cesium资源是否正确加载,确保API密钥有效
- 坐标获取不到:确保点击位置在地球表面,而非天空盒
- 折线不显示:检查坐标数组是否为空,材质颜色是否可见
- 性能问题 :使用
viewer.scene.debugShowFramesPerSecond = true
监控帧率
扩展功能建议
- 折线编辑:添加顶点拖拽、删除功能
- 样式自定义:允许用户修改折线颜色、宽度、材质
- 数据导出:将折线坐标导出为GeoJSON或其他格式
- 面积计算:对于闭合折线,添加面积计算功能
学习资源汇总
- Cesium官方文档 :Index - Cesium Documentation
- Cesium Sandcastle示例 :Cesium Sandcastle
- Vue-Cesium组件库 :A Vue 3 based component library of CesiumJS for developers | Vue for Cesium
- Lodash文档 :Lodash Documentation
- Cesium中文社区 :https://cesiumcn.org/
完整代码(带详细备注)
javascript
<template>
<div id="cesiumContainer" style="width: 100%; height: 100vh"></div>
</template>
<script>
// 导入地图初始化配置和工具函数
import initMap from '@/config/initMap.js'; // 地图初始化函数
import { mapConfig } from '@/config/mapConfig'; // 地图配置项(包含高德地图URL等)
import { throttle } from 'lodash'; // 导入节流函数用于性能优化
export default {
data() {
return {
viewer: null, // Cesium Viewer实例,地图的核心控制器
isDrawing: false, // 绘制状态标志:是否正在绘制折线
currentPositions: [], // 存储当前折线的顶点坐标数组(Cartesian3类型)
tempPrimitive: null, // 临时折线的Primitive对象,随鼠标移动更新
allPolylines: [], // 存储所有已完成绘制的折线对象
handler: null, // 屏幕空间事件处理器,用于监听鼠标交互
isFirstClick: true, // 首次点击标志:用于判断是否开始绘制
vertexMarkers: [], // 存储顶点标记的Entity对象数组
};
},
created() {
// 节流处理临时绘制函数,限制50ms内最多执行一次,优化鼠标移动时的性能
this.throttledDrawTempLine = throttle(this.drawTempLine, 50);
},
mounted() {
// 初始化Cesium地图,使用高德地图瓦片服务
// initMap参数:地图瓦片URL,是否开启3D模式(false表示2D)
this.viewer = initMap(mapConfig.gaode.url3, false);
// 初始化折线绘制功能
this.initDrawLine();
},
methods: {
/**
* 初始化折线绘制相关的事件处理器
* 绑定鼠标点击、移动、双击等事件,实现交互式绘制逻辑
*/
initDrawLine() {
// 创建屏幕空间事件处理器,监听canvas上的鼠标事件
this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.canvas);
// 移除Cesium默认的左键双击事件(默认是放大地图),避免与我们的双击结束绘制冲突
this.viewer.cesiumWidget.screenSpaceEventHandler.removeInputAction(
Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK
);
// 显示操作提示信息
this.showToast('点击添加顶点,双击结束绘制(至少需要3个顶点)');
// 绑定左键单击事件 - 添加折线顶点
this.handler.setInputAction((event) => {
// 绘制过程中禁用地图默认交互(旋转、缩放等),防止误操作
this.disableMapInteraction();
// 将鼠标点击位置转换为地球表面的地理坐标(Cartesian3)
const position = this.getPositionFromMouse(event.position);
if (!position) return; // 如果获取坐标失败,直接返回
// 重复顶点检测:如果不是第一个顶点,检查与上一个顶点的距离
if (this.currentPositions.length > 0) {
const lastPosition =
this.currentPositions[this.currentPositions.length - 1];
// 计算两点之间的直线距离(单位:米)
const distance = Cesium.Cartesian3.distance(position, lastPosition);
const DISTANCE_THRESHOLD = 1.0; // 距离阈值(米),可根据需求调整
// 如果距离小于阈值,忽略本次点击
if (distance < DISTANCE_THRESHOLD) {
this.showToast(
`点击位置与上一顶点距离过近(${distance.toFixed(2)}米),已忽略`
);
this.enableMapInteraction(); // 重新启用地图交互
return;
}
}
// 首次点击时,标记开始绘制状态
if (this.isFirstClick) {
this.isDrawing = true;
this.isFirstClick = false;
}
// 添加顶点坐标到数组,并在地图上显示顶点标记
this.currentPositions.push(position);
this.addVertexMarker(position);
// 绘制临时折线(此时鼠标未移动,传入null)
this.drawTempLine(null);
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
// 绑定鼠标移动事件 - 更新临时折线
this.handler.setInputAction((event) => {
// 只有在绘制状态且已有顶点时才更新临时折线
if (!this.isDrawing || this.currentPositions.length === 0) return;
// 获取鼠标当前位置对应的地理坐标
const position = this.getPositionFromMouse(event.endPosition);
if (position) {
// 使用节流后的方法更新临时折线
this.throttledDrawTempLine(position);
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
// 绑定左键双击事件 - 结束折线绘制
this.handler.setInputAction((event) => {
if (!this.isDrawing) return; // 非绘制状态不处理
// 获取双击位置的坐标并添加为最后一个顶点
const position = this.getPositionFromMouse(event.position);
if (position) {
this.currentPositions.push(position);
this.addVertexMarker(position);
}
// 验证顶点数量,至少需要3个顶点才能形成有效的折线
if (this.currentPositions.length < 3) {
this.showToast('折线至少需要3个顶点,请继续添加');
return;
}
// 保存最终折线、清理临时对象、重置状态、恢复地图交互
this.savePolyline();
this.clearTempLine();
this.resetDrawingState();
this.enableMapInteraction();
}, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
// 绑定右键单击事件 - 取消绘制
this.handler.setInputAction(() => {
if (this.isDrawing) {
this.clearTempLine(); // 清除临时折线
this.resetDrawingState(); // 重置绘制状态
this.enableMapInteraction(); // 恢复地图交互
this.showToast('已取消绘制');
}
}, Cesium.ScreenSpaceEventType.RIGHT_DOWN);
},
/**
* 将鼠标屏幕坐标转换为地球表面的地理坐标
* @param {Cesium.Cartesian2} mousePosition - 鼠标屏幕坐标
* @returns {Cesium.Cartesian3|null} 地球表面的地理坐标,失败时返回null
*/
getPositionFromMouse(mousePosition) {
// 创建从相机位置到鼠标位置的射线
const ray = this.viewer.camera.getPickRay(mousePosition);
if (!ray || !this.viewer.scene) return null;
// 计算射线与地球表面的交点(获取地理坐标)
const position = this.viewer.scene.globe.pick(ray, this.viewer.scene);
if (!position) {
this.showToast('请在地球表面点击'); // 如果点击了天空盒等非地球表面位置
}
return position;
},
/**
* 绘制临时折线(随鼠标移动更新)
* @param {Cesium.Cartesian3} currentMousePosition - 当前鼠标位置的地理坐标
*/
drawTempLine(currentMousePosition) {
this.clearTempLine(); // 先清除已有的临时折线
// 如果没有顶点或鼠标位置无效,不绘制
if (this.currentPositions.length === 0 || !currentMousePosition) return;
// 创建临时坐标数组:已有顶点 + 当前鼠标位置
const tempPositions = [...this.currentPositions, currentMousePosition];
// 创建临时折线Primitive
this.tempPrimitive = new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: new Cesium.PolylineGeometry({
positions: tempPositions, // 折线顶点坐标数组
width: 5, // 折线宽度(像素)
vertexFormat: Cesium.PolylineMaterialAppearance.VERTEX_FORMAT, // 指定顶点格式
}),
}),
appearance: new Cesium.PolylineMaterialAppearance({
material: Cesium.Material.fromType('Color', {
color: Cesium.Color.RED.withAlpha(0.8), // 临时折线为半透明红色
}),
}),
});
// 将临时折线添加到场景中显示
this.viewer.scene.primitives.add(this.tempPrimitive);
},
/**
* 清除临时折线
*/
clearTempLine() {
if (this.tempPrimitive) {
// 从场景中移除临时折线并释放资源
this.viewer.scene.primitives.remove(this.tempPrimitive);
this.tempPrimitive = null;
}
},
/**
* 保存最终绘制的折线
*/
savePolyline() {
// 创建最终折线Primitive
const polyline = new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: new Cesium.PolylineGeometry({
positions: this.currentPositions, // 使用当前所有顶点坐标
width: 5,
vertexFormat: Cesium.PolylineMaterialAppearance.VERTEX_FORMAT,
}),
}),
appearance: new Cesium.PolylineMaterialAppearance({
material: Cesium.Material.fromType('Color', {
color: Cesium.Color.BLUE.withAlpha(0.8), // 最终折线为半透明蓝色
}),
}),
});
// 添加到场景并保存引用
this.viewer.scene.primitives.add(polyline);
this.allPolylines.push(polyline);
// 计算并显示折线总长度
const totalLength = this.calculatePolylineLength(this.currentPositions);
this.showToast(
`折线绘制完成!顶点数: ${this.currentPositions.length},
总长度: ${totalLength.toFixed(2)}米`
);
},
/**
* 计算折线总长度
* @param {Cesium.Cartesian3[]} positions - 折线顶点坐标数组
* @returns {number} 折线总长度(米)
*/
calculatePolylineLength(positions) {
let totalLength = 0;
// 遍历所有相邻顶点对,累加距离
for (let i = 0; i < positions.length - 1; i++) {
totalLength += Cesium.Cartesian3.distance(
positions[i],
positions[i + 1]
);
}
return totalLength;
},
/**
* 在地图上添加顶点标记
* @param {Cesium.Cartesian3} position - 顶点坐标
*/
addVertexMarker(position) {
const marker = this.viewer.entities.add({
position: position,
point: {
pixelSize: 8, // 像素大小
color: Cesium.Color.YELLOW, // 黄色标记
outlineColor: Cesium.Color.BLACK, // 黑色轮廓
outlineWidth: 2, // 轮廓宽度
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, // 贴地显示
},
});
this.vertexMarkers.push(marker); // 保存标记引用,便于后续清理
},
/**
* 重置绘制状态
*/
resetDrawingState() {
this.isDrawing = false; // 退出绘制状态
this.isFirstClick = true; // 重置首次点击标志
this.currentPositions = []; // 清空顶点数组
// 如需保留顶点标记,注释掉下面两行
// this.vertexMarkers.forEach(marker => this.viewer.entities.remove(marker));
// this.vertexMarkers = [];
},
/**
* 显示临时提示信息
* @param {string} message - 提示文本
*/
showToast(message) {
const toast = document.createElement('div');
// 设置提示框样式
toast.style.cssText = `
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 8px 16px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
border-radius: 4px;
font-size: 14px;
z-index: 9999;
`;
toast.textContent = message;
document.body.appendChild(toast);
// 3秒后自动移除提示框
setTimeout(() => document.body.removeChild(toast), 3000);
},
/**
* 禁用地图交互(旋转、缩放等)
*/
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.enableRotate = true;
controller.enableZoom = true;
controller.enableTranslate = true;
controller.enableTilt = true;
controller.enableLook = true;
},
},
/**
* 组件销毁前清理资源
*/
beforeDestroy() {
if (this.viewer) this.viewer.destroy(); // 销毁Cesium Viewer实例
if (this.handler) this.handler.destroy(); // 销毁事件处理器
},
};
</script>
<style lang="scss" scoped>
#cesiumContainer {
width: 100%;
height: 100vh;
touch-action: none;
}
</style>
点点关注,有需求或问题可以在评论留言