基于 vue+Cesium 实现军事标绘之钳击箭头绘制实战
效果图
在地理信息系统(GIS)开发中,军事标绘是一个重要的应用场景,其中箭头类标绘(如攻击箭头、钳击箭头)是常用的战术符号。本文将基于 Cesium 引擎,详细讲解如何实现可交互的钳击箭头绘制功能,支持动态跟随鼠标调整、固定部分标绘区域及自动清理临时标记等特性。
一、技术背景与实现目标
Cesium 简介
Cesium 是一款开源的 3D 地理信息引擎,支持高精度全球地形、影像加载及矢量数据可视化,广泛应用于数字地球、军事仿真等领域。其强大的空间分析能力和实时渲染特性,使其成为军事标绘的理想选择。
实现目标
本文将实现一个交互式钳击箭头绘制工具,具备以下功能:
- 鼠标点击确定标绘关键点,支持动态调整
- 第三次点击后固定右侧箭头,左侧保持跟随鼠标移动
- 绘制完成后自动隐藏标记点,仅保留最终标绘图形
- 支持标绘数据的提取与格式化输出
二、环境准备
依赖引入
实现该功能需要以下核心依赖:
// 导入Cesium地图初始化工具
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';
基础组件结构
我们将创建一个 Vue 组件,通过cesium-container
容器承载地图实例:
<template><div id="cesiumContainer" class="cesium-container"></div>
</template><style scoped>
.cesium-container {width: 100%;height: 100vh;overflow: hidden;
}
</style>
三、核心实现详解
3.1 地图初始化与数据准备
在组件挂载阶段初始化 Cesium 实例,并设置绘制延迟以确保资源加载完成:
export default {name: 'CesiumMap',data() {return {viewer: null, // Cesium核心实例drawHandler: null, // 屏幕事件处理器layerId: 'pincerArrowLayer', // 标绘图层ID(用于实体管理)fixedPoints: [], // 已固定的关键点坐标currentPoint: null, // 当前鼠标位置(浮动点)pointEntities: [] // 标记点实体集合(用于清理)};},mounted() {// 初始化地图(使用高德底图)this.viewer = initMap(mapConfig.gaode.url3, false);// 延迟5秒开始绘制,确保地图加载完成setTimeout(() => {this.addPincerArrow();}, 5000);}
};
3.2 交互事件处理
通过Cesium.ScreenSpaceEventHandler
监听鼠标事件,实现关键点采集与动态调整:
methods: {addPincerArrow() {// 初始化数据与清理历史标绘this.fixedPoints = [];this.currentPoint = null;this.pointEntities = [];this.clearPlot();// 创建箭头实体(动态更新)this.showRegion2Map();// 初始化事件处理器this.drawHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);// 左键点击:添加固定点this.drawHandler.setInputAction((event) => {// 屏幕坐标转地图坐标(笛卡尔坐标系)const position = event.position;const ray = this.viewer.camera.getPickRay(position);const cartesian = this.viewer.scene.globe.pick(ray, this.viewer.scene);if (!Cesium.defined(cartesian)) return;// 限制最大点数为5个if (this.fixedPoints.length >= 5) return;// 添加固定点并创建标记this.fixedPoints.push(cartesian);const pointEntity = this.createPoint(cartesian, this.fixedPoints.length - 1);this.pointEntities.push(pointEntity);// 初始化第一个浮动点if (this.fixedPoints.length === 1) {this.currentPoint = cartesian.clone();}// 达到5个点时完成绘制if (this.fixedPoints.length === 5) {this.cleanupDrawing(true); // 清理资源并隐藏标记this.getPincerArrowValue(); // 提取标绘数据}}, Cesium.ScreenSpaceEventType.LEFT_CLICK);// 鼠标移动:更新浮动点this.drawHandler.setInputAction((event) => {// 仅在未完成绘制时更新if (this.fixedPoints.length === 0 || this.fixedPoints.length >= 5) return;// 计算当前鼠标的地图坐标const position = event.endPosition;const ray = this.viewer.camera.getPickRay(position);const cartesian = this.viewer.scene.globe.pick(ray, this.viewer.scene);if (Cesium.defined(cartesian)) {this.currentPoint = cartesian;this.viewer.scene.requestRender(); // 触发重绘}}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);}
}
3.3 动态箭头绘制核心逻辑
通过Cesium.CallbackProperty
实现箭头的实时更新,区分固定区域与动态区域:
showRegion2Map() {// 定义箭头样式(填充色与轮廓)const fillMaterial = 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(() => {// 至少需要3个点才能绘制箭头if (this.fixedPoints.length < 3) return null;try {// 构建点集:前3个固定点 + 动态点let positions;if (this.fixedPoints.length >= 3) {// 前3个点固定,后续点动态更新const fixedPart = this.fixedPoints.slice(0, 3);let floatingPart = this.fixedPoints.slice(3);// 添加当前鼠标位置(浮动点)if (this.currentPoint && this.fixedPoints.length < 5) {floatingPart.push(this.currentPoint);}positions = [...fixedPart, ...floatingPart];}// 坐标转换:笛卡尔坐标转经纬度const lonLats = this.getLonLatArr(positions);this.removeDuplicate(lonLats); // 去重处理// 调用算法生成箭头多边形const doubleArrow = xp.algorithm.doubleArrow(lonLats);if (!doubleArrow || !doubleArrow.polygonalPoint) return null;// 返回多边形层级数据const pHierarchy = new Cesium.PolygonHierarchy(doubleArrow.polygonalPoint);pHierarchy.keyPoints = lonLats;return pHierarchy;} catch (err) {console.error('箭头计算失败:', err);return null;}}, false);// 创建箭头实体(填充+轮廓)const entity = this.viewer.entities.add({polygon: new Cesium.PolygonGraphics({hierarchy: dynamicHierarchy,material: fillMaterial,show: true}),polyline: new Cesium.PolylineGraphics({positions: new Cesium.CallbackProperty(() => {// 与多边形逻辑类似,生成轮廓线// ...(省略与dynamicHierarchy类似的轮廓计算逻辑)}, false),clampToGround: true,width: 2,material: outlineMaterial})});entity.layerId = this.layerId;entity.valueFlag = 'value';
}
3.4 辅助功能实现
标记点管理
创建临时标记点并在绘制完成后清理:
// 创建标记点
createPoint(cartesian, oid) {return this.viewer.entities.add({position: cartesian,billboard: {image: boardimg,scale: 1.0,width: 32,height: 32},oid, // 自定义属性:点编号layerId: this.layerId,flag: 'keypoint' // 标记点类型});
}// 清理资源
cleanupDrawing(isComplete) {// 销毁事件处理器if (this.drawHandler) {this.drawHandler.destroy();this.drawHandler = null;}// 绘制完成后移除所有标记点if (isComplete) {this.pointEntities.forEach(entity => {this.viewer.entities.remove(entity);});}
}
坐标转换与数据提取
将 Cesium 内部坐标转换为通用经纬度格式:
// 笛卡尔坐标转经纬度
getLonLat(cartesian) {const cartographic = this.viewer.scene.globe.ellipsoid.cartesianToCartographic(cartesian);return {lon: Cesium.Math.toDegrees(cartographic.longitude),lat: Cesium.Math.toDegrees(cartographic.latitude)};
}// 提取标绘数据
getPincerArrowValue() {const entityList = this.viewer.entities.values;for (const entity of entityList) {if (entity.valueFlag === 'value') {const hierarchy = entity.polygon.hierarchy.getValue();if (!hierarchy || !hierarchy.positions) continue;// 转换为经纬度数组const coordinates = hierarchy.positions.map(pos => {const cartographic = this.viewer.scene.globe.ellipsoid.cartesianToCartographic(pos);return {lat: Cesium.Math.toDegrees(cartographic.latitude),lng: Cesium.Math.toDegrees(cartographic.longitude)};});console.log('钳击箭头数据:', coordinates);console.log('关键点:', hierarchy.keyPoints);}}
}
四、关键技术点解析
动态更新机制
通过Cesium.CallbackProperty
实现图形实时刷新,该接口会在每一帧渲染前重新计算属性值,确保箭头随鼠标动态变化。坐标系统转换
Cesium 内部使用笛卡尔坐标系(Cartesian3
),需通过ellipsoid.cartesianToCartographic
转换为经纬度坐标,便于实际应用。事件管理
使用ScreenSpaceEventHandler
处理鼠标交互,注意在组件销毁前调用destroy()
方法释放资源,避免内存泄漏。分层设计
通过layerId
和flag
属性对实体进行分类管理,便于批量清理和查询。
五、总结与扩展
本文实现了一个具备动态交互能力的军事标绘工具,核心是通过 Cesium 的事件系统与动态属性实现标绘图形的实时更新,并通过分层数据管理实现固定区域与动态区域的分离。
扩展方向:
- 支持更多标绘符号(如进攻箭头、集结地等)
- 添加标绘编辑功能(移动、删除关键点)
- 实现标绘数据的持久化存储与加载
- 优化大场景下的渲染性能
六、完整代码(需要导入算法的请留言)
<template><!-- Cesium地图容器,占满整个视口 --><div id="cesiumContainer" class="cesium-container"></div>
</template><script>
// 导入地图初始化函数,用于创建Cesium Viewer实例
import initMap from '@/config/initMap.js';
// 导入地图配置,包含底图服务URL等信息
import { mapConfig } from '@/config/mapConfig';
// 导入自定义算法库,提供军事标绘相关算法(如箭头生成)
import xp from '@/utils/algorithm.js';
// 导入标记点图片,用于显示地图上的关键点
import boardimg from '@/assets/images/captain-01.png';export default {name: 'CesiumMap',data() {return {viewer: null, // Cesium Viewer实例gatherPosition: [], // 采集的点坐标(已弃用,使用fixedPoints)floatingPoint: null, // 浮动点实体(跟随鼠标移动)drawHandler: null, // 屏幕空间事件处理器layerId: 'pincerArrowLayer', // 图层ID,用于管理实体fixedPoints: [], // 已固定的点坐标currentPoint: null, // 当前浮动点坐标pointEntities: [], // 点实体数组,用于后续清理};},mounted() {// 组件挂载后初始化地图this.viewer = initMap(mapConfig.gaode.url3, false);// 延迟5秒后开始箭头绘制,确保地图完全加载setTimeout(() => {this.addPincerArrow();}, 5000);},methods: {// 开始绘制钳击箭头addPincerArrow() {console.log('开始绘制钳击箭头');// 初始化数据this.fixedPoints = []; // 存储用户点击确定的固定点this.currentPoint = null; // 当前浮动点(随鼠标移动)this.pointEntities = []; // 存储所有点实体this.clearPlot(); // 清除之前的绘制内容// 创建箭头实体(动态更新)this.showRegion2Map();// 创建屏幕空间事件处理器,监听鼠标操作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;const cartesian = this.viewer.scene.globe.pick(ray, this.viewer.scene);if (!Cesium.defined(cartesian)) return;// 限制最多5个点if (this.fixedPoints.length >= 5) return;// 添加新固定点并创建可视化实体this.fixedPoints.push(cartesian);const pointEntity = this.createPoint(cartesian,this.fixedPoints.length - 1);this.pointEntities.push(pointEntity);// 初始化浮动点if (this.fixedPoints.length === 1) {this.currentPoint = cartesian.clone();}// 达到5个点时完成绘制if (this.fixedPoints.length === 5) {this.cleanupDrawing(true); // 清理资源并移除临时点this.getPincerArrowValue(); // 获取最终箭头数据}}, Cesium.ScreenSpaceEventType.LEFT_CLICK);// 鼠标移动事件:更新浮动点位置this.drawHandler.setInputAction((event) => {const position = event.endPosition;if (!Cesium.defined(position)) return;const ray = this.viewer.camera.getPickRay(position);const cartesian = this.viewer.scene.globe.pick(ray, this.viewer.scene);if (!Cesium.defined(cartesian)) return;// 更新浮动点并触发地图重绘if (this.fixedPoints.length > 0 && this.fixedPoints.length < 5) {this.currentPoint = cartesian;this.viewer.scene.requestRender();}}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);},// 清理绘制资源cleanupDrawing(isComplete = false) {// 销毁事件处理器if (this.drawHandler) {this.drawHandler.destroy();this.drawHandler = null;}// 如果绘制完成,移除所有点标记if (isComplete && this.pointEntities.length > 0) {this.pointEntities.forEach((entity) => {this.viewer.entities.remove(entity);});this.pointEntities = [];}},// 创建箭头图形实体showRegion2Map() {// 定义箭头样式const fillMaterial =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(() => {// 至少需要3个点才能绘制箭头if (this.fixedPoints.length < 3) return null;try {// 构建计算用的点数组:固定点 + 浮动点let positions;// 当有3个或更多固定点时,前3个点固定,后续点可浮动if (this.fixedPoints.length >= 3) {const fixedPart = this.fixedPoints.slice(0, 3);let floatingPart = this.fixedPoints.slice(3);// 如果有浮动点且未达到最大点数,添加到浮动部分if (this.currentPoint && this.fixedPoints.length < 5) {floatingPart.push(this.currentPoint);}positions = [...fixedPart, ...floatingPart];} else {// 少于3个点时,全部点都可能浮动positions = [...this.fixedPoints];if (this.currentPoint && positions.length > 0) {positions[positions.length - 1] = this.currentPoint;}}// 转换为经纬度数组并去重let lonLats = this.getLonLatArr(positions);this.removeDuplicate(lonLats);// 使用算法库生成箭头多边形const doubleArrow = xp.algorithm.doubleArrow(lonLats);// 验证并返回多边形层级if (!doubleArrow ||!doubleArrow.polygonalPoint ||doubleArrow.polygonalPoint.length < 3) {console.log('算法返回无效数据:', doubleArrow);return null;}const polygonPositions = doubleArrow.polygonalPoint;const pHierarchy = new Cesium.PolygonHierarchy(polygonPositions);pHierarchy.keyPoints = lonLats;return pHierarchy;} catch (err) {console.error('计算箭头多边形时出错:', err);return null;}}, false);// 动态计算多边形轮廓const outlineDynamicPositions = new Cesium.CallbackProperty(() => {// 逻辑与上面类似,用于生成箭头轮廓线if (this.fixedPoints.length < 3) return null;try {let positions;if (this.fixedPoints.length >= 3) {const fixedPart = this.fixedPoints.slice(0, 3);let floatingPart = this.fixedPoints.slice(3);if (this.currentPoint && this.fixedPoints.length < 5) {floatingPart.push(this.currentPoint);}positions = [...fixedPart, ...floatingPart];} else {positions = [...this.fixedPoints];if (this.currentPoint && positions.length > 0) {positions[positions.length - 1] = this.currentPoint;}}let lonLats = this.getLonLatArr(positions);this.removeDuplicate(lonLats);const doubleArrow = xp.algorithm.doubleArrow(lonLats);if (!doubleArrow ||!doubleArrow.polygonalPoint ||doubleArrow.polygonalPoint.length < 2) {console.log('算法返回无效轮廓数据:', doubleArrow);return null;}// 闭合多边形轮廓const outlinePositions = doubleArrow.polygonalPoint.slice();outlinePositions.push(outlinePositions[0]);return outlinePositions;} catch (err) {console.error('计算箭头轮廓时出错:', err);return null;}}, false);// 创建箭头实体const entity = this.viewer.entities.add({polygon: new Cesium.PolygonGraphics({hierarchy: dynamicHierarchy,material: fillMaterial,show: true,}),polyline: new Cesium.PolylineGraphics({positions: outlineDynamicPositions,clampToGround: true,width: 2,material: outlineMaterial,show: true,}),});// 标记实体所属图层和类型entity.layerId = this.layerId;entity.valueFlag = 'value';},// 创建地图上的标记点createPoint(cartesian, oid) {const point = this.viewer.entities.add({position: cartesian,billboard: {image: boardimg, // 标记点图片eyeOffset: new Cesium.Cartesian3(0, 0, -500), // 视角偏移heightReference: Cesium.HeightReference.NONE, // 高度参考scale: 1.0, // 缩放比例width: 32, // 固定宽度height: 32, // 固定高度},});// 添加自定义属性以便后续识别和管理point.oid = oid;point.layerId = this.layerId;point.flag = 'keypoint';return point;},// 数组去重(移除相邻重复点)removeDuplicate(lonLats) {if (!lonLats || lonLats.length < 2) return;for (let i = 1; i < lonLats.length; i++) {const p1 = lonLats[i - 1];const p2 = lonLats[i];if (p2[0] === p1[0] && p2[1] === p1[1]) {lonLats.splice(i, 1);i--;}}},// 将笛卡尔坐标数组转换为经纬度数组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;},// 将笛卡尔坐标转换为经纬度对象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--;}}},// 获取最终绘制的箭头数据getPincerArrowValue() {const entityList = this.viewer.entities.values;for (let i = 0; i < entityList.length; i++) {const entity = entityList[i];// 查找箭头实体if (typeof entity.valueFlag !== 'undefined') {// 获取多边形层级数据const hierarchy = entity.polygon.hierarchy.getValue();if (!hierarchy || !hierarchy.positions) {console.log('警告: 多边形层级或位置数据不存在');continue;}// 将笛卡尔坐标转换为经纬度格式const dke = hierarchy.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),});}// 输出关键点数据if (hierarchy.keyPoints) {console.log('采集的钳击箭头关键点', hierarchy.keyPoints);} else {console.log('警告: 关键点数据不存在');}}}},},beforeDestroy() {// 组件销毁前清理资源,防止内存泄漏if (this.drawHandler) {this.drawHandler.destroy();this.drawHandler = null;}if (this.viewer) {this.viewer.destroy();this.viewer = null;}},
};
</script><style scoped>
.cesium-container {width: 100%;height: 100vh;overflow: hidden;
}
</style>