在地理信息系统(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
容器承载地图实例:
在组件挂载阶段初始化 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);
}
};
通过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);
}
}
通过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';
}
标记点管理
创建临时标记点并在绘制完成后清理:
// 创建标记点
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 的事件系统与动态属性实现标绘图形的实时更新,并通过分层数据管理实现固定区域与动态区域的分离。
扩展方向: