当前位置: 首页 > news >正文

5、Vue中使用Cesium实现交互式折线绘制详解

引言

Cesium是一款强大的开源3D地理信息可视化引擎,广泛应用于数字地球、地图可视化等领域。在Vue项目中集成Cesium可以快速构建高性能的地理信息应用。本文将详细介绍如何在Vue项目中实现交互式折线绘制功能,包括顶点添加、临时绘制、距离计算等核心功能,并为新手提供详细的代码注释和学习资源。

Cesium核心概念速览

在开始之前,我们先了解几个Cesium的核心概念:

  • Viewer:Cesium的核心实例,用于创建和管理3D场景
  • Entity:高层次的对象封装,用于创建和管理可视化对象(如点、线、面)
  • Primitive:低层次的渲染对象,比Entity更高效,适合大量数据渲染
  • Cartesian3:三维笛卡尔坐标,Cesium中表示位置的基本方式
  • ScreenSpaceEventHandler:用于处理用户输入事件(如点击、鼠标移动)

环境准备

假设已完成Cesium 2D地图初始化,需要安装以下依赖:

npm install cesium lodash

核心功能实现

折线绘制的核心流程如下:

  1. 点击地图添加顶点
  2. 鼠标移动时更新临时折线
  3. 双击结束绘制并保存折线
  4. 右键取消绘制

代码解析

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. 坐标转换

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. 临时折线绘制

drawTempLine(currentMousePosition) {this.clearTempLine(); // 清除已有临时折线if (this.currentPositions.length === 0 || !currentMousePosition) return;// 创建包含已有顶点和当前鼠标位置的临时坐标数组const tempPositions = [...this.currentPositions, currentMousePosition];// 创建临时折线Primitivethis.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. 折线保存与长度计算

savePolyline() {// 创建最终折线Primitiveconst 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;
}

性能优化

  1. 节流处理:使用Lodash的throttle函数限制鼠标移动时的重绘频率
created() {// 节流处理临时绘制,50ms内最多执行一次,优化性能this.throttledDrawTempLine = throttle(this.drawTempLine, 50);
}
  1. 顶点去重:通过距离检测避免添加过近的重复顶点

  2. 资源销毁:组件销毁时清理Cesium资源,避免内存泄漏

beforeDestroy() {if (this.viewer) this.viewer.destroy(); // 销毁Viewer实例if (this.handler) this.handler.destroy(); // 销毁事件处理器
}

常见问题与调试

  1. 地图初始化失败:检查Cesium资源是否正确加载,确保API密钥有效
  2. 坐标获取不到:确保点击位置在地球表面,而非天空盒
  3. 折线不显示:检查坐标数组是否为空,材质颜色是否可见
  4. 性能问题:使用viewer.scene.debugShowFramesPerSecond = true监控帧率

扩展功能建议

  1. 折线编辑:添加顶点拖拽、删除功能
  2. 样式自定义:允许用户修改折线颜色、宽度、材质
  3. 数据导出:将折线坐标导出为GeoJSON或其他格式
  4. 面积计算:对于闭合折线,添加面积计算功能

学习资源汇总

  1. Cesium官方文档:Index - Cesium Documentation
  2. Cesium Sandcastle示例:Cesium Sandcastle
  3. Vue-Cesium组件库:A Vue 3 based component library of CesiumJS for developers | Vue for Cesium
  4. Lodash文档:Lodash Documentation
  5. Cesium中文社区:https://cesiumcn.org/

完整代码(带详细备注)

<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];// 创建临时折线Primitivethis.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() {// 创建最终折线Primitiveconst 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>

点点关注,有需求或问题可以在评论留言

http://www.dtcms.com/a/272975.html

相关文章:

  • 电脑被突然重启后,再每次打开excel文件,都会记录之前的位置窗口大小,第一次无法全屏显示。
  • imx6ul Qt运行qml报错This plugin does not support createPlatformOpenGLContext!
  • 无人机抗风模块运行与技术难点分析
  • Flowable22变量监听器---------------持续更新中
  • OneFileLLM:一键聚合多源信息流
  • 股指期货交割交易日到期没平仓盈亏以哪个价格计算?
  • RP2040使用存储系统
  • 2025年7月10日泛财经要闻精选
  • ACPU正式启动全球化布局,重构AI时代的中心化算力基础施设
  • 基于cornerstone3D的dicom影像浏览器 第三十二章 文件夹做pacs服务端,fake-pacs-server
  • 专题 数字(Number)基础
  • pytorch深度学习-Lenet-Minist
  • (LeetCode 每日一题) 3440. 重新安排会议得到最多空余时间 II (贪心)
  • RabbitMQ消息队列——三个核心特性
  • LeetCode 1652. 拆炸弹
  • AI时代的接口调试与文档生成:Apipost 与 Apifox 的表现对比
  • Leetcode刷题营第十九题:对链表进行插入排序
  • Python 网络爬虫中 robots 协议使用的常见问题及解决方法
  • 图解 BFS 路径搜索:LeetCode1971
  • 芯片I/O脚先于电源脚上电会导致Latch-up(闩锁效应)吗?
  • Logback日志框架配置实战指南
  • 5种使用USB数据线将文件从安卓设备传输到电脑的方法
  • 【JavaScript 函数、闭包与 this 绑定机制深度解析】
  • 【C语言】指针笔试题2
  • 模块三:现代C++工程实践(4篇)第二篇《性能调优:Profile驱动优化与汇编级分析》
  • FlashAttention 快速安装指南(避免长时间编译)
  • QT网络通信底层实现详解:UDP/TCP实战指南
  • Centos 7下使用C++使用Rdkafka库实现生产者消费者
  • 【LeetCode 热题 100】19. 删除链表的倒数第 N 个结点——双指针+哨兵
  • 学习 Flutter (一)