基于 Cesium.js 的交互式绘图工具库
这是一个基于 Cesium.js 的交互式绘图工具库,包含两个主要类:PositionTransfer(坐标转换工具)和 DrawTool(绘图工具)。
1. PositionTransfer 类
功能:处理不同坐标系统间的转换,如经纬度、笛卡尔坐标、屏幕坐标等。
主要方法:
-
cartesian3ToLng(position)
将Cesium.Cartesian3
(三维笛卡尔坐标)转换为经纬度和高度(WGS84 坐标系)。 -
lngToCartesian3(position)
将经纬度对象{lng, lat, height}
转换为Cesium.Cartesian3
。 -
screenPositionToCartesian3(position)
将屏幕坐标(Cesium.Cartesian2
)转换为世界坐标(Cesium.Cartesian3
),考虑地形和 3D 模型。 -
generateCirclePoints(center, radius)
根据圆心和半径生成圆形边缘的经纬度点集合,用于绘制圆形。
2. DrawTool 类
功能:实现交互式绘制点、线、多边形、矩形、圆等几何图形,支持贴地或固定高度模式。
核心特性:
-
绘制模式
-
ClampToGround
: 图形贴附地形或 3D 模型表面。 -
None
: 固定高度(默认)。
-
-
支持的图形类型
-
点(Point)、折线(Polyline)、多边形(Polygon)、矩形(Rect)、圆(Circle)。
-
-
交互逻辑
-
左键单击:添加点或调整图形形状。
-
右键单击:结束绘制。
-
鼠标移动:实时预览未完成的图形。
-
关键方法:
-
active(drawType)
激活指定绘图类型,初始化事件监听和临时实体。 -
生成图形方法
-
generatePolyline()
: 动态更新折线顶点。 -
generateRect()
: 通过两点计算矩形四个角点。 -
generateCircle()
: 根据圆心和半径生成圆。 -
generatePolygon()
: 多边形自动闭合。
-
-
坐标拾取
getcartesian3FromScreen(px)
处理从屏幕坐标到世界坐标的转换,兼容地形和 3D 模型。 -
贴地处理
setClamp()
根据模式设置图形是否贴地,利用ClassificationType
实现。 -
事件触发
绘制结束时触发DrawEndEvent
,传递实体对象、坐标数据及图形类型。
3.使用示例
// 初始化 Viewer
const viewer = new Cesium.Viewer("cesiumContainer");
// 创建绘图工具实例
const drawTool = new DrawTool(viewer, { drawMode: "clampToGround" });
// 监听绘制完成事件
drawTool.DrawEndEvent.addEventListener((entity, positions, type, circlePoints) => {
console.log("绘制完成:", type, positions);
});
// 激活多边形绘制
drawTool.active(drawTool.DrawTypes.Polygon);
4.代码亮点
-
坐标系统无缝转换:支持复杂场景下的坐标精准拾取。
-
动态预览:利用
CallbackProperty
实现图形实时更新。 -
地形适配:通过
ClassificationType
确保图形贴合地形或模型表面。 -
事件驱动设计:便于扩展和集成到其他功能模块。
5.潜在改进
-
圆形精度:当前
getCirclePoint
假设地球为完美球体,可能在高纬度或大范围时产生误差,需改用测地线算法。 -
撤销/重做:可添加历史记录功能,支持操作回退。
-
UI 交互:增强绘制提示(如文字标签、撤销按钮)。
此工具适用于需要在地球表面交互式绘制标绘物的场景,如地理标注、测量、规划等。
6.具体实现
PositionTransfer.js
import * as Cesium from "cesium";
class PositionTransfer {
/**
* 坐标转换工具
* @param {Cesium.Viewer} viewer : viewer程序对象
*/
constructor(viewer) {
this.viewer = viewer;
}
/**
* 笛卡尔转经纬度
* @param {Cesium.Cartesian3} position : 笛卡尔三阶坐标
*/
cartesian3ToLng(position) {
// 获取当前椭球的坐标系统,其中包含了坐标转换工具
const ellipsoid = this.viewer.scene.globe.ellipsoid;
// 笛卡尔坐标转为弧度坐标
const cartoGraphic = ellipsoid.cartesianToCartographic(position);
// 将弧度坐标转为经纬度
const lng = Cesium.Math.toDegrees(cartoGraphic.longitude);
const lat = Cesium.Math.toDegrees(cartoGraphic.latitude);
const height = cartoGraphic.height;
return {
lng,
lat,
height,
};
}
/**
* 笛卡尔转经纬度(方式2)
* @param {Cesium.Cartesian3} position : 笛卡尔三阶坐标
*/
cartesian3ToDegreesHeight(position) {
let c = Cesium.Cartographic.fromCartesian(position);
return [
Cesium.Math.toDegrees(c.longitude),
Cesium.Math.toDegrees(c.latitude),
c.height,
];
}
/**
* 根据笛卡尔获取位置高度
* @param {Cesium.Cartesian3} position : 笛卡尔三阶坐标
*/
getPositionHeight(position) {
const cartographic = Cesium.Cartographic.fromCartesian(position);
return cartographic.height;
}
getCirclePoint(lon, lat, angle, radius) {
let dx = radius * Math.sin((angle * Math.PI) / 180.0);
let dy = radius * Math.cos((angle * Math.PI) / 180.0);
let ec = 6356725 + ((6378137 - 6356725) * (90.0 - lat)) / 90.0;
let ed = ec * Math.cos((lat * Math.PI) / 180);
let newLon = ((dx / ed + (lon * Math.PI) / 180.0) * 180.0) / Math.PI;
let newLat = ((dy / ec + (lat * Math.PI) / 180.0) * 180.0) / Math.PI;
return [newLon, newLat];
}
/**
* 获取圆的边缘坐标
* @param {Cesium.Cartesian3} center : 笛卡尔三阶坐标
* @param {Number} radius : 半径
*/
generateCirclePoints(center, radius) {
let points = [];
for (let i = 0; i < 360; i += 2) {
points.push(this.getCirclePoint(center[0], center[1], i, radius));
}
return points;
}
/**
* 经纬度转笛卡尔
* @param {Object} position : 经纬度 {lng,lat,height}
*/
lngToCartesian3(position) {
const { lng, lat, height } = position;
return new Cesium.Cartesian3.fromDegrees(lng, lat, height);
}
/**
* 经纬度转笛卡尔(批量)
* @param {Array<{lng,lat,height}>|Array<{lng,lat}>} positions : 经纬度坐标数组
* @param {Boolean} isHeight 是否包含了高度
*/
lngPositionsToCartesian3(positions, isHeight = false) {
const resArr = [];
if (Array.isArray(positions)) {
positions.forEach((position) => {
const { lng, lat, height } = position;
if (isHeight) {
resArr.push(lng, lat, height);
} else {
resArr.push(lng, lat);
}
});
// 根据是否带高度,返回对应的数据
return isHeight
? new Cesium.Cartesian3.fromDegreesArrayHeights(resArr)
: new Cesium.Cartesian3.fromDegreesArray(resArr);
}
}
/**
* 屏幕坐标和笛卡尔坐标转换
* @param {Cesium.Cartesian2} position : 屏幕坐标,笛卡尔2阶数据
*/
screenPositionToCartesian3(position) {
return this.viewer.scene.globe.pick(
this.viewer.camera.getPickRay(position),
this.viewer.scene
);
}
/**
* 世界坐标转屏幕坐标
* @param {Cesium.Cartesian3} position : 世界坐标
*/
cartesian3ToScreenPosition(position) {
return Cesium.SceneTransforms.wgs84ToWindowCoordinates(
this.viewer.scene,
position
);
}
// todo:世界坐标转为带地形高度的经纬度
}
export default PositionTransfer;
DrawTool.js
import * as Cesium from "cesium";
import PositionTransfer from "./PositionTrans";
class DrawTool {
/**
* 绘制工具
* @param {Cesium.Viewer} viewer : viewer程序对象
*/
constructor(viewer, options = {}) {
this.viewer = viewer;
// 开启深度检测
//this.viewer.scene.globe.depthTestAgainstTerrain = true;
viewer.scene.globe.depthTestAgainstTerrain = true;
// 设置线性深度
// viewer.scene.logarithmicDepthBuffer = false; // 禁用对数深度缓冲
// viewer.scene.globe.linearDepth = true; // 启用线性深度
this.positionTool=new PositionTransfer(viewer)
// 默认按照贴地模式绘制图元
this.drawMode = Cesium.defaultValue(options.drawMode, this.DrawModes.ClampToGround);
// 初始化handler,events
this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);
this.DrawEndEvent = new Cesium.Event(); //结束绘制事件
}
// 绘制模式,支持贴地和固定高度
DrawModes = {
ClampToGround: "clampToGround",
None: "none",
};
DrawTypes = {
Polyline: "Polyline",
Rect: "Rect",
Point: "Point",
Circle: "Circle",
Polygon: "Polygon",
};
// 激活工具,传入DrawType
active(drawType) {
// 如果在我们的绘制工具集合中,存在这个工具
if (Object.keys(this.DrawTypes).includes(drawType)) {
this.drawType = drawType;
// 最终的坐标
this.positions = [];
// 绘制过程中的坐标
this.curPositions = [];
// 每次点击有一个标点,需要存储实体
this.points = [];
// 注册鼠标事件
this.registerEvents();
//设置鼠标状态
this.viewer.enableCursorStyle = false;
this.viewer._element.style.cursor = "default";
this.createMarker(drawType);
} else {
return;
}
}
createMarker(drawType) {
this.marker = document.createElement("div");
this.marker.innerHTML = `左键点击绘制${drawType},右键结束绘制`;
this.marker.className = "marker-draw";
this.viewer.cesiumWidget.container.appendChild(this.marker);
}
destoryMarker() {
this.marker && this.viewer.cesiumWidget.container.removeChild(this.marker);
this.marker = null;
}
registerEvents() {
// 分别注册左键画点,右键结束画点,鼠标移动事件
this.leftClickEvent();
this.rightClickEvent();
this.mouseMoveEvent();
}
/**
* 屏幕坐标转笛卡尔,分别从模型,地形,地球表面进行讨论
*
* @param {Cesium.Cartesian2} px 屏幕坐标
*
* @return {Cesium.Cartesian3} Cartesian3 三维坐标
*/
getcartesian3FromScreen(px) {
if (this.viewer && px) {
// 使用pick获取当前射线穿透的第一个物体
const pick = this.viewer.scene.pick(px);
let isOn3dtiles = false;
let isOnTerrain = false;
// 最终得到的笛卡尔坐标
let cartesianRes = null;
if (pick && pick.primitive.isCesium3DTileset) {
isOn3dtiles = true;
}
// 判断是否加载了地形
let boolTerrain =
this.viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider;
// 如果没有点击到模型,并且加载了地形的话,走地形碰撞逻辑
if (!isOn3dtiles && !boolTerrain) {
const ray = this.viewer.scene.camera.getPickRay(px);
if (!ray) return null;
cartesianRes = this.viewer.scene.globe.pick(ray, this.viewer.scene);
isOnTerrain = true;
}
// 如果没有加载地形,也没有3dtiles,就是拾取到地球上的坐标
if (!isOn3dtiles && !isOnTerrain && boolTerrain) {
cartesianRes = this.viewer.scene.camera.pickEllipsoid(
px,
this.viewer.scene.globe.ellipsoid
);
}
// 3dtiles
if (isOn3dtiles) {
cartesianRes = this.viewer.scene.pickPosition(px);
}
// 如果有笛卡尔坐标,避免笛卡尔坐标的高度小于0
if (cartesianRes) {
let position = this.positionTool.cartesian3ToDegreesHeight(cartesianRes);
// 可以将坐标高度设置为>=0
// if(position[2]<0){
// }
return cartesianRes;
}
return false;
}
}
leftClickEvent() {
// 单机鼠标左键画点
this.handler.setInputAction((e) => {
// 屏幕坐标转为笛卡尔坐标,分三种情况
let position = this.getcartesian3FromScreen(e.position);
if (!position) return;
this.positions.push(position);
this.curPositions.push(position);
// 如果是第一个点,就开始根据drawType,绘制图案
if (this.positions.length === 1) {
this.startDraw();
this.setClamp()
}
// 如果是画线或者画点,每次点击左键,都在同一位置画一个点,绘制线的端点
if (
this.drawType === this.DrawTypes.Polyline ||
this.drawType === this.DrawTypes.Point
) {
this.generatePoint(position);
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}
// 鼠标移动事件
mouseMoveEvent() {
this.handler.setInputAction((e) => {
this.marker.style.left = e.endPosition.x + 20 + "px";
this.marker.style.top = e.endPosition.y - 20 + "px";
this.viewer._element.style.cursor = "default"; //由于鼠标移动时 Cesium会默认将鼠标样式修改为手柄 所以移动时手动设置回来
let position = this.getcartesian3FromScreen(e.endPosition);
if (!position || !this.drawEntity) return;
// tempPositions是每次鼠标移动时,我们得到的坐标,this.position是我们左键点击才能得到的坐标
this.curPositions = [...this.positions, position];
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
}
// 右键点击,结束绘制
rightClickEvent() {
this.handler.setInputAction(() => {
// 如果还没有开始绘制,直接结束绘制状态
if (!this.drawEntity) {
this.deactive();
return;
}
// 如果当前的坐标数量少于最小数量,直接结束
if (this.positions.length < this.minPositionCount) {
this.deactive();
return;
}
// 根据各种绘制类型,要重新给坐标赋值,吧callback变为constant
switch (this.drawType) {
case this.DrawTypes.Polyline:
this.drawEntity.polyline.positions = this.positions;
break;
case this.DrawTypes.Rect:
this.drawEntity.polygon.hierarchy = new Cesium.PolygonHierarchy(
this.getRectFourPoints()
);
this.drawEntity.polyline.positions = this.getRectFourPoints();
this.positions = this.getRectFourPoints();
this.positions.pop();
break;
case this.DrawTypes.Circle:
this.drawEntity.ellipse.semiMinorAxis = this.getAxis();
this.drawEntity.ellipse.semiMajorAxis = this.getAxis();
this.minPositionCount = 2;
break;
case this.DrawTypes.Polygon:
this.drawEntity.polygon.hierarchy = new Cesium.PolygonHierarchy(
this.positions.concat(this.positions[0])
);
this.drawEntity.polyline.positions = this.positions.concat(
this.positions[0]
);
break;
default:
break;
}
this.deactive();
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
}
startDraw() {
switch (this.drawType) {
case this.DrawTypes.Point:
// 对于点,直接结束绘制就完事了
this.drawEntity = this.generatePoint(this.positions[0]);
// 记录最小构成几何体的坐标数量,point:1,polyline:2,rect:2,circle:2,polygon:3
this.minPositionCount = 1;
break;
case this.DrawTypes.Polyline:
this.generatePolyline();
this.minPositionCount = 2;
break;
case this.DrawTypes.Rect:
this.generateRect();
this.minPositionCount = 2;
break;
case this.DrawTypes.Circle:
this.generateCircle();
this.minPositionCount = 2;
break;
case this.DrawTypes.Polygon:
this.generatePolygon();
this.minPositionCount = 3;
default:
break;
}
}
// 绘制点
generatePoint(position) {
const cartographic = Cesium.Cartographic.fromCartesian(position);
const point = this.viewer.entities.add({
Type: this.DrawTypes.Point,
position: position,
point: {
pixelSize: 14,
color: Cesium.Color.RED,
},
});
this.points.push(point);
return point;
}
// 绘制线,position使用callbackProperty
generatePolyline() {
this.drawEntity = this.viewer.entities.add({
Type: this.DrawTypes.Polyline,
polyline: {
positions: new Cesium.CallbackProperty((e) => {
return this.curPositions;
}, false),
width: 2,
material: new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.YELLOW,
})
},
});
}
// 绘制矩形,由polygon和polyline组成
generateRect() {
this.drawEntity = this.viewer.entities.add({
Type: this.DrawTypes.Rect,
polygon: {
hierarchy: new Cesium.CallbackProperty((e) => {
return new Cesium.PolygonHierarchy(this.getRectFourPoints());
}, false),
material: Cesium.Color.RED.withAlpha(0.6)
},
polyline: {
positions: new Cesium.CallbackProperty((e) => {
return this.getRectFourPoints();
}, false),
width: 1,
material: new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.YELLOW,
}),
depthFailMaterial: new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.YELLOW,
})
},
});
}
setClamp() {
// 设置贴地
if (this.drawMode === "clampToGround") {
if(this.drawEntity.ellipse){
this.drawEntity.ellipse.classificationType =
Cesium.ClassificationType.BOTH;
}
if(this.drawEntity.polygon){
this.drawEntity.polygon.classificationType =Cesium.ClassificationType.BOTH;
this.drawEntity.polygon.perPositionHeight=false
}
if(this.drawEntity.polyline){
this.drawEntity.polyline.classificationType =
Cesium.ClassificationType.BOTH;
this.drawEntity.polyline.clampToGround = true;
}
} else {
if(this.drawEntity.ellipse){
this.drawEntity.ellipse.height = this.positionTool.getPositionHeight(this.positions[0]);
}
if(this.drawEntity.polygon){
this.drawEntity.polygon.classificationType = undefined
this.drawEntity.polygon.perPositionHeight=true
}
if(this.drawEntity.polyline){
this.drawEntity.polyline.classificationType = undefined;
this.drawEntity.polyline.clampToGround = false;
}
}
}
// 获取矩形四个点
getRectFourPoints() {
let res = this.curPositions;
if (this.curPositions.length > 1) {
let p1 = this.curPositions[0];
let p2 = this.curPositions[1];
let c1 = Cesium.Cartographic.fromCartesian(p1);
let c2 = Cesium.Cartographic.fromCartesian(p2);
if (c1.height < 0) c1.height = 0;
if (c2.height < 0) c2.height = 0;
let lls = this.getRectanglePointsByTwoPoint(c1, c2);
// 坐标数组转为指定格式
let ars = [
lls[0][0],
lls[0][1],
c1.height,
lls[1][0],
lls[1][1],
c1.height,
lls[2][0],
lls[2][1],
c1.height,
lls[3][0],
lls[3][1],
c1.height,
lls[0][0],
lls[0][1],
c1.height,
];
res = Cesium.Cartesian3.fromDegreesArrayHeights(ars);
}
return res;
}
// 获取矩形四个点
getRectanglePointsByTwoPoint(c1, c2) {
//转为经纬度
let lngLat1 = [
Cesium.Math.toDegrees(c1.longitude),
Cesium.Math.toDegrees(c1.latitude),
];
let lngLat2 = [
Cesium.Math.toDegrees(c2.longitude),
Cesium.Math.toDegrees(c2.latitude),
];
let lngLat3 = [
Cesium.Math.toDegrees(c1.longitude),
Cesium.Math.toDegrees(c2.latitude),
];
let lngLat4 = [
Cesium.Math.toDegrees(c2.longitude),
Cesium.Math.toDegrees(c1.latitude),
];
return [lngLat1, lngLat3, lngLat2, lngLat4];
}
// 绘制多边形
generatePolygon() {
this.drawEntity = this.viewer.entities.add({
Type: this.DrawTypes.Polygon,
polygon: {
// 多边形坐标有首位闭合的特点
hierarchy: new Cesium.CallbackProperty((e) => {
return new Cesium.PolygonHierarchy(
this.curPositions.concat(this.curPositions[0])
);
}, false),
material: Cesium.Color.RED.withAlpha(0.6)
},
polyline: {
positions: new Cesium.CallbackProperty((e) => {
return this.curPositions.concat(this.curPositions[0]);
}, false),
width: 1,
material: new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.YELLOW,
}),
depthFailMaterial: new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.YELLOW,
})
},
});
}
// 绘制圆
generateCircle() {
this.drawEntity = this.viewer.entities.add({
position: this.positions[0],
ellipse: {
height: this.positionTool.getPositionHeight(this.positions[0]),
semiMinorAxis: new Cesium.CallbackProperty((e) => {
return this.getAxis();
}, false),
semiMajorAxis: new Cesium.CallbackProperty((e) => {
return this.getAxis();
}, false),
material: Cesium.Color.RED.withAlpha(0.6),
},
});
}
//圆半径
getAxis() {
if (this.curPositions.length>1) {
let p1 = this.curPositions[0];
let p2 = this.curPositions[this.curPositions.length - 1];
const axis = Cesium.Cartesian3.distance(p1, p2);
return axis;
}else{
return 0
}
}
// 结束绘制
deactive() {
// 对于圆,根据半径计算边缘点坐标并返回
let points = [];
if (this.drawType === this.DrawTypes.Circle) {
const radius = this.getAxis();
const positions = this.positionTool.cartesian3ToDegreesHeight(this.positions[0]);
points = this.positionTool.generateCirclePoints(positions, radius);
points = points.map((item) => {
const height = this.positionTool.getPositionHeight(this.positions[0]);
return Cesium.Cartesian3.fromDegrees(item[0], item[1], height);
});
}
// 提交绘制结束事件
this.DrawEndEvent.raiseEvent(
this.drawEntity,
this.positions,
this.drawType,
points
);
this.unRegisterEvents();
this.destoryMarker();
this.drawType = undefined;
this.drawEntity = undefined;
this.positions = [];
this.curPositions = [];
this.viewer._element.style.cursor = "pointer";
this.viewer.enableCursorStyle = true;
}
//解除鼠标事件
unRegisterEvents() {
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.RIGHT_CLICK);
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE);
this.handler.removeInputAction(
Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK
);
}
// 清除绘制的实体
removeAllDrawEnts() {
this.points &&
this.points.forEach((point) => {
this.viewer.entities.remove(point);
});
this.drawEntity && this.viewer.entities.remove(this.drawEntity);
this.points = [];
this.drawEntity = null;
}
removeListener(event) {
this.DrawEndEvent &&
this.DrawEndEvent.numberOfListeners &&
this.DrawEndEvent.removeEventListener(event);
}
}
export default DrawTool;
DrawToolUse.js
import * as Cesium from 'cesium'
import * as dat from 'dat.gui'
import DrawTool from '../lib/DrawTool'
const gui=new dat.GUI()
// Cesium Ion token
Cesium.Ion.defaultAccessToken='mytoken'
// viewer是整个三维场景的入口
const viewer=new Cesium.Viewer('cesiumContainer',{
// 隐藏默认显示的控件
// 时间轴控件
timeline:false,
// 动画控件
animation:false,
// 设置底图切换控件
baseLayerPicker:false,
// 复位按钮的控件
homeButton:false,
// 全屏按钮的控件
fullscreenButton:false,
// 导航功能的控件
geocoder:false,
// 隐藏二三维模式的切换控件
sceneModePicker:false,
scene3DOnly:true,
// 隐藏默认的导航按钮
navigationHelpButton:false,
// 时间是否流动
shouldAnimate:true,
imageryProvider:new Cesium.GridImageryProvider({
cells:1,
glowWidth:0,
color:Cesium.Color.WHITE.withAlpha(0.1),
backgroundColor:Cesium.Color.GRAY
})
})
const drawTool=new DrawTool(viewer)
gui.add({
fn(){
drawTool.active(drawTool.DrawTypes.Polyline)
drawTool.DrawEndEvent.addEventListener((ent,positions)=>{
console.log(positions);
})
}
},'fn').name('绘制线')
gui.add({
fn(){
drawTool.active(drawTool.DrawTypes.Polygon)
drawTool.DrawEndEvent.addEventListener((ent,positions)=>{
console.log(positions);
})
}
},'fn').name('绘制多边形')