基于vue+Cesium实现交互式攻击箭头绘制

引言
在地理信息系统(GIS)和军事模拟领域,箭头绘制是一项基础且重要的功能。本文将介绍如何使用Cesium.js结合Vue框架实现交互式攻击箭头绘制功能,支持鼠标点击采集关键点、动态更新箭头形状、右键结束绘制等核心交互,并对实现过程中的关键技术点进行深入解析。
功能概述
本组件实现了以下核心功能:
- 地图初始化与基础配置
- 鼠标左键点击采集箭头关键点
- 鼠标移动实时更新箭头形状
- 右键点击结束绘制并输出结果
- 动态渲染箭头多边形与轮廓线
- 坐标转换与数据处理
- 资源释放与内存管理
核心代码解析
1. 项目结构与依赖引入
// 导入地图初始化函数
import initMap from '@/config/initMap.js';
// 导入地图配置项(包含高德地图服务地址等)
import { mapConfig } from '@/config/mapConfig';
// 导入算法工具库(包含攻击箭头绘制算法)
import xp from '@/utils/algorithm.js';
// 导入标记点图片资源
import boardimg from '@/assets/images/captain-01.png';2. 数据初始化
data() {return {viewer: null, // Cesium地图实例对象gatherPosition: [], // 采集的点坐标数组(Cartesian3格式)floatingPoint: null, // 鼠标移动时的动态浮动点实体drawHandler: null, // 屏幕空间事件处理器layerId: 'attackArrowLayer', // 自定义图层ID};
}3. 地图初始化
mounted() {// 初始化Cesium地图,使用高德地图服务this.viewer = initMap(mapConfig.gaode.url3, false);// 延迟1秒执行箭头采集函数,确保地图资源加载完成setTimeout(() => {this.gatherAttackArrow();}, 1000);
}4. 核心交互逻辑实现
4.1 事件绑定与交互流程
/*** 开始采集攻击箭头的坐标点* 绑定鼠标事件:左键点击采集点,鼠标移动更新浮动点,右键点击结束采集*/
gatherAttackArrow() {console.log('开始采集攻击箭头');this.gatherPosition = []; // 清空历史采集点数组this.floatingPoint = null; // 重置浮动点实体this.clearPlot(); // 清除已有绘制内容// 创建屏幕空间事件处理器,用于监听鼠标交互this.drawHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);// 左键点击事件 - 采集坐标点this.drawHandler.setInputAction((event) => {// 坐标点采集逻辑...}, Cesium.ScreenSpaceEventType.LEFT_CLICK);// 鼠标移动事件 - 更新浮动点位置this.drawHandler.setInputAction((event) => {// 浮动点更新逻辑...}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);// 右键点击事件 - 停止采集并处理结果this.drawHandler.setInputAction((movement) => {// 结束采集逻辑...}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
}4.2 关键点标记创建
/*** 创建地图上的关键点标记* @param {Cesium.Cartesian3} cartesian - 点的笛卡尔坐标* @param {number} oid - 点的唯一标识* @returns {Cesium.Entity} 创建的点实体对象*/
createPoint(cartesian, oid) {const point = this.viewer.entities.add({position: cartesian,billboard: {image: boardimg, // 使用导入的图片作为标记eyeOffset: new Cesium.Cartesian3(0, 0, -100), // 视线偏移heightReference: Cesium.HeightReference.NONE, // 不贴地scale: 1.0, // 缩放比例width: 32, // 固定宽度height: 32, // 固定高度},});// 自定义属性,用于后续识别和管理point.oid = oid;point.layerId = this.layerId;point.flag = 'keypoint';return point;
}5. 动态图形绘制
5.1 攻击箭头动态渲染
/*** 在地图上动态显示攻击箭头区域* 通过CallbackProperty实现多边形和轮廓线的动态更新*/
showRegion2Map() {// 定义多边形填充材质(黄色半透明)const material = Cesium.Color.fromCssColorString('#ff0').withAlpha(0.5);// 定义轮廓线材质(红色虚线)const outlineMaterial = new Cesium.PolylineDashMaterialProperty({dashLength: 16,color: Cesium.Color.fromCssColorString('#f00').withAlpha(0.7),});// 动态更新多边形层级(攻击箭头形状)const dynamicHierarchy = new Cesium.CallbackProperty(() => {if (this.gatherPosition.length > 1) {const lonLats = this.getLonLatArr(this.gatherPosition);// 使用算法库生成带尾翼的攻击箭头const doubleArrow = xp.algorithm.tailedAttackArrow(lonLats);const positions = doubleArrow.polygonalPoint;if (!positions || positions.length < 3) return null;positions.splice(positions.length - 2, 1); // 移除燕尾部分const pHierarchy = new Cesium.PolygonHierarchy(positions);pHierarchy.keyPoints = lonLats; // 存储原始关键点return pHierarchy;}return null;}, false);// 动态更新轮廓线坐标const outlineDynamicPositions = new Cesium.CallbackProperty(() => {// 轮廓线更新逻辑...}, false);// 添加多边形和轮廓线实体到地图const entity = this.viewer.entities.add({polygon: new Cesium.PolygonGraphics({hierarchy: dynamicHierarchy,material: material,show: true,}),polyline: new Cesium.PolylineGraphics({positions: outlineDynamicPositions,clampToGround: true,width: 2,material: outlineMaterial,show: true,}),});entity.layerId = this.layerId;entity.valueFlag = 'value';
}6. 坐标转换与数据处理
/*** 将笛卡尔坐标数组转换为经纬度数组* @param {Array<Cesium.Cartesian3>} positions - 笛卡尔坐标数组* @returns {Array<Array<number>>} 经纬度数组 [[lon, lat], ...]*/
getLonLatArr(positions) {const arr = [];for (let i = 0; i < positions.length; i++) {const p = this.getLonLat(positions[i]);if (p) {arr.push([p.lon, p.lat]);}}return arr;
}/*** 将笛卡尔坐标转换为经纬度坐标* @param {Cesium.Cartesian3} cartesian - 笛卡尔坐标* @returns {Object} 包含lon, lat, alt的经纬度对象*/
getLonLat(cartesian) {const cartographic = this.viewer.scene.globe.ellipsoid.cartesianToCartographic(cartesian);cartographic.height = this.viewer.scene.globe.getHeight(cartographic);return {lon: Cesium.Math.toDegrees(cartographic.longitude),lat: Cesium.Math.toDegrees(cartographic.latitude),alt: cartographic.height,};
}7. 资源释放与内存管理
beforeDestroy() {// 组件销毁时释放资源,避免内存泄漏if (this.viewer) {this.viewer.destroy(); // 销毁地图实例}if (this.drawHandler) {this.drawHandler.destroy(); // 销毁事件处理器}
}关键技术点解析
1. CallbackProperty动态更新机制
Cesium中的CallbackProperty是实现动态图形的核心,它允许我们定义一个回调函数,该函数会在每一帧渲染时被调用,返回最新的属性值。在攻击箭头绘制中,我们利用这一特性实现了箭头形状的实时更新:
const dynamicHierarchy = new Cesium.CallbackProperty(() => {// 根据当前采集的点动态生成箭头形状// ...
}, false);第二个参数false表示该属性不会引发视图更新,适合用于频繁更新的场景。
2. 事件处理器管理
Cesium的ScreenSpaceEventHandler用于处理用户交互事件,使用时需要注意:
- 使用后必须调用
destroy()方法释放资源 - 避免重复创建事件处理器导致内存泄漏
- 合理管理事件优先级和冲突
3. 坐标系统转换
Cesium中存在多种坐标系统,本项目中主要涉及:
- Cartesian3:三维笛卡尔坐标(世界坐标)
- Cartographic:地理坐标(弧度表示的经纬度)
- Degrees:角度表示的经纬度
通过ellipsoid.cartesianToCartographic和Math.toDegrees实现坐标转换。
4. 图层管理与实体标识
通过自定义属性(如layerId、flag)对Cesium实体进行分类管理,便于批量操作和清理:
// 清除当前图层上的所有绘制内容
clearPlot() {const entityList = this.viewer.entities.values;if (!entityList || entityList.length < 1) return;for (let i = 0; i < entityList.length; i++) {const entity = entityList[i];if (entity.layerId === this.layerId) {this.viewer.entities.remove(entity);i--; // 移除后索引调整}}
}使用方法
- 安装依赖:确保项目中已引入Cesium.js和Vue框架
- 配置地图服务:在
mapConfig.js中配置高德或其他地图服务地址 - 引入组件:在需要使用箭头绘制功能的页面引入该组件
- 初始化调用:通过
mounted钩子自动初始化或手动调用gatherAttackArrow方法
完整代码
<template><div id="cesiumContainer" style="width: 100%; height: 100vh"></div>
</template><script>
// 导入地图初始化函数
import initMap from '@/config/initMap.js';
// 导入地图配置项(包含高德地图服务地址等)
import { mapConfig } from '@/config/mapConfig';
// 导入算法工具库(包含攻击箭头绘制算法)
import xp from '@/utils/algorithm.js';
// 导入标记点图片资源
import boardimg from '@/assets/images/captain-01.png';export default {data() {return {viewer: null, // Cesium地图实例对象gatherPosition: [], // 采集的点坐标数组(存储Cartesian3格式坐标)floatingPoint: null, // 鼠标移动时的动态浮动点实体drawHandler: null, // 屏幕空间事件处理器(用于监听鼠标交互)layerId: 'attackArrowLayer', // 自定义图层ID,用于标识和管理绘制元素};},mounted() {// 初始化Cesium地图,使用高德地图服务this.viewer = initMap(mapConfig.gaode.url3, false);// 延迟1秒执行箭头采集函数,确保地图资源加载完成setTimeout(() => {this.gatherAttackArrow();}, 1000);},methods: {/*** 开始采集攻击箭头的坐标点* 绑定鼠标事件:左键点击采集点,鼠标移动更新浮动点,右键点击结束采集*/gatherAttackArrow() {console.log('开始采集攻击箭头');this.gatherPosition = []; // 清空历史采集点数组this.floatingPoint = null; // 重置浮动点实体this.clearPlot(); // 清除已有绘制内容// 创建屏幕空间事件处理器,用于监听鼠标交互this.drawHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);// 左键点击事件 - 采集坐标点this.drawHandler.setInputAction((event) => {const position = event.position; // 获取鼠标点击的屏幕坐标if (!Cesium.defined(position)) return;// 创建射线:从相机位置发射到点击的屏幕坐标const ray = this.viewer.camera.getPickRay(position);if (!Cesium.defined(ray)) return;// 计算射线与地球表面的交点(返回Cartesian3坐标)const cartesian = this.viewer.scene.globe.pick(ray, this.viewer.scene);if (!Cesium.defined(cartesian)) return;const num = this.gatherPosition.length;if (num === 0) {// 第一个点:添加到采集数组,创建浮动点,初始化区域显示this.gatherPosition.push(cartesian);this.floatingPoint = this.createPoint(cartesian, -1);this.showRegion2Map();}// 添加当前点到采集数组,并创建标记点实体this.gatherPosition.push(cartesian);const oid = this.gatherPosition.length - 2;this.createPoint(cartesian, oid);}, Cesium.ScreenSpaceEventType.LEFT_CLICK);// 鼠标移动事件 - 更新浮动点位置this.drawHandler.setInputAction((event) => {const position = event.endPosition; // 获取鼠标移动的屏幕坐标if (!Cesium.defined(position)) return;const ray = this.viewer.camera.getPickRay(position);if (!Cesium.defined(ray)) return;const cartesian = this.viewer.scene.globe.pick(ray, this.viewer.scene);if (!Cesium.defined(cartesian)) return;if (!this.floatingPoint) return;// 更新浮动点位置,并刷新采集数组的最后一个点this.floatingPoint.position.setValue(cartesian);this.gatherPosition.pop(); // 移除数组最后一个点(临时点)this.gatherPosition.push(cartesian); // 添加新的鼠标位置点}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);// 右键点击事件 - 停止采集并处理结果this.drawHandler.setInputAction((movement) => {if (this.gatherPosition.length < 2) return; // 至少需要2个有效点this.gatherPosition.pop(); // 移除最后一个临时点(浮动点)this.viewer.entities.remove(this.floatingPoint); // 移除浮动点实体// 销毁事件处理器,避免内存泄漏if (this.drawHandler) {this.drawHandler.destroy();this.drawHandler = null;}this.clearKeyPoint(); // 清除所有关键点标记this.getAttackArrowValue(); // 处理并输出采集的箭头数据}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);},/*** 创建地图上的关键点标记* @param {Cesium.Cartesian3} cartesian - 点的笛卡尔坐标* @param {number} oid - 点的唯一标识(用于区分不同点)* @returns {Cesium.Entity} 创建的点实体对象*/createPoint(cartesian, oid) {const point = this.viewer.entities.add({position: cartesian,billboard: {image: boardimg, // 使用导入的图片作为标记eyeOffset: new Cesium.Cartesian3(0, 0, -100), // 视线偏移(Z轴负值避免遮挡)heightReference: Cesium.HeightReference.NONE, // 不贴地,使用绝对高度scale: 1.0, // 缩放比例width: 32, // 固定宽度height: 32, // 固定高度},});point.oid = oid; // 自定义属性:点IDpoint.layerId = this.layerId; // 自定义属性:图层IDpoint.flag = 'keypoint'; // 自定义属性:标记为关键点return point;},/*** 在地图上动态显示攻击箭头区域* 通过CallbackProperty实现多边形和轮廓线的动态更新*/showRegion2Map() {// 定义多边形填充材质(黄色半透明)const material = Cesium.Color.fromCssColorString('#ff0').withAlpha(0.5);// 定义轮廓线材质(红色虚线)const outlineMaterial = new Cesium.PolylineDashMaterialProperty({dashLength: 16, // 虚线长度color: Cesium.Color.fromCssColorString('#f00').withAlpha(0.7), // 颜色及透明度});// 动态更新多边形层级(攻击箭头形状)const dynamicHierarchy = new Cesium.CallbackProperty(() => {if (this.gatherPosition.length > 1) {// 将笛卡尔坐标转换为经纬度数组const lonLats = this.getLonLatArr(this.gatherPosition);// 使用算法库生成带尾翼的攻击箭头const doubleArrow = xp.algorithm.tailedAttackArrow(lonLats);const positions = doubleArrow.polygonalPoint; // 获取箭头多边形顶点if (!positions || positions.length < 3) return null; // 至少需要3个点绘制多边形positions.splice(positions.length - 2, 1); // 移除燕尾部分(根据算法调整)// 创建多边形层级对象,并存储关键点信息const pHierarchy = new Cesium.PolygonHierarchy(positions);pHierarchy.keyPoints = lonLats; // 自定义属性:存储原始经纬度关键点return pHierarchy;}return null;}, false);// 动态更新轮廓线坐标const outlineDynamicPositions = new Cesium.CallbackProperty(() => {if (this.gatherPosition.length < 2) return null;const lonLats = this.getLonLatArr(this.gatherPosition);const doubleArrow = xp.algorithm.tailedAttackArrow(lonLats);const positions = doubleArrow.polygonalPoint;if (!positions || positions.length < 3) return null;positions.splice(positions.length - 2, 1); // 移除燕尾部分positions.push(positions[0]); // 闭合多边形(首尾点连接)return positions;}, false);// 添加多边形和轮廓线实体到地图const entity = this.viewer.entities.add({polygon: new Cesium.PolygonGraphics({hierarchy: dynamicHierarchy, // 动态层级material: material, // 填充材质show: true, // 是否显示}),polyline: new Cesium.PolylineGraphics({positions: outlineDynamicPositions, // 动态轮廓线坐标clampToGround: true, // 贴地显示width: 2, // 线宽material: outlineMaterial, // 线材质show: true, // 是否显示}),});entity.layerId = this.layerId; // 自定义属性:图层IDentity.valueFlag = 'value'; // 自定义属性:标记为有效数据实体},/*** 将笛卡尔坐标数组转换为经纬度数组* @param {Array<Cesium.Cartesian3>} positions - 笛卡尔坐标数组* @returns {Array<Array<number>>} 经纬度数组 [[lon, lat], ...]*/getLonLatArr(positions) {const arr = [];for (let i = 0; i < positions.length; i++) {const p = this.getLonLat(positions[i]);if (p) {arr.push([p.lon, p.lat]); // 存储格式:[经度, 纬度]}}return arr;},/*** 将笛卡尔坐标转换为经纬度坐标* @param {Cesium.Cartesian3} cartesian - 笛卡尔坐标* @returns {Object} 包含lon, lat, alt的经纬度对象*/getLonLat(cartesian) {// 将笛卡尔坐标转换为地理坐标(弧度)const cartographic =this.viewer.scene.globe.ellipsoid.cartesianToCartographic(cartesian);// 获取该点的地表高度cartographic.height = this.viewer.scene.globe.getHeight(cartographic);return {lon: Cesium.Math.toDegrees(cartographic.longitude), // 经度(度)lat: Cesium.Math.toDegrees(cartographic.latitude), // 纬度(度)alt: cartographic.height, // 高度(米)};},/*** 清除当前图层上的所有绘制内容*/clearPlot() {const entityList = this.viewer.entities.values;if (!entityList || entityList.length < 1) return;// 遍历所有实体,移除当前图层的元素for (let i = 0; i < entityList.length; i++) {const entity = entityList[i];if (entity.layerId === this.layerId) {this.viewer.entities.remove(entity);i--; // 移除后索引调整(避免漏删)}}},/*** 清除地图上所有关键点标记*/clearKeyPoint() {const entityList = this.viewer.entities.values;if (!entityList || entityList.length < 1) return;// 遍历所有实体,移除标记为关键点的元素for (let i = 0; i < entityList.length; i++) {const entity = entityList[i];if (entity.flag === 'keypoint') {this.viewer.entities.remove(entity);i--; // 移除后索引调整}}},/*** 获取采集的攻击箭头数据并输出* 转换为经纬度格式,便于后续处理*/getAttackArrowValue() {const entityList = this.viewer.entities.values;for (let i = 0; i < entityList.length; i++) {const entity = entityList[i];if (typeof entity.valueFlag !== 'undefined') {// 输出笛卡尔坐标点数组console.log('采集的攻击箭头(笛卡尔):',entity.polygon.hierarchy.getValue().positions);// 转换为经纬度坐标const dke = entity.polygon.hierarchy.getValue().positions;const objArr = [];for (let j = 0; j < dke.length; j++) {const cartesian3 = new Cesium.Cartesian3(dke[j].x,dke[j].y,dke[j].z);const cartographic =this.viewer.scene.globe.ellipsoid.cartesianToCartographic(cartesian3);objArr.push({lat: Cesium.Math.toDegrees(cartographic.latitude), // 纬度lng: Cesium.Math.toDegrees(cartographic.longitude), // 经度});}console.log('采集的攻击箭头(经纬度)', objArr);console.log('采集的攻击箭头关键点',entity.polygon.hierarchy.getValue().keyPoints);}}},},beforeDestroy() {// 组件销毁时释放资源,避免内存泄漏if (this.viewer) {this.viewer.destroy(); // 销毁地图实例}if (this.drawHandler) {this.drawHandler.destroy(); // 销毁事件处理器}},
};
</script><style lang="scss" scoped>
#cesiumContainer {width: 100%;height: 100vh;touch-action: none; // 禁止触摸操作的默认行为(如双击缩放)
}
</style>
总结与展望
本文实现了一个功能完整的交互式攻击箭头绘制组件,核心亮点包括:
- 流畅的鼠标交互体验
- 高效的动态图形渲染
- 完善的资源管理机制
- 清晰的代码结构和注释
未来可以进一步优化的方向:
- 添加箭头样式自定义功能(颜色、线宽、箭头大小等)
- 实现箭头的编辑和修改功能
- 增加撤销/重做功能
- 优化移动端交互体验
通过本文的实现,不仅掌握了Cesium的核心交互和绘图API,也学习了大型前端GIS应用中的性能优化和内存管理技巧,为构建更复杂的地理信息应用奠定了基础。
点点关注不迷路,代码中使用到的 algorithm.js与 plotUtil.js 有需要的留言。