Cesium空间距离量测
最新在系统学习Cesium,之前学习了其绘制功能,那么在绘制的基础上如果把量测功能添加上是不是更完美?于是在学习他人代码的基础上,自己用Cesium + Vue3 实现一个距离量测功能。(看网上的这部分有些零散,就把自己md学习笔记汇总然后导进来。)
- 非贴地形距离测量
首先先获取两点的屏幕坐标,将其转为笛卡尔空间直角坐标。
-
可以使用 Cesium.Cartesian3.distance() 计算出两点之间的距离。一条线上的所有点,每2个点依次计算距离,汇总之后得到总长度。(不考虑地球的曲率,也不考虑地表的曲面)
-
还有一种方法是已知两个点的经纬度信息,创建了一个椭球体测地线对象(EllipsoidGeodesic),然后设置起始点和终点,利用其surfaceDistance属性获取这两个点之间的曲面距离。这个距离是沿着地球表面的最短路径距离,考虑了地球的曲率,通常用于测量地球上两个点之间的实际距离。其次,如果要更准确的计算,需要考虑这两个点的高度差,就需要用直角三角形求斜边方法来得到三维空间中两点的直线距离。
计算的代码如下
/* 空间两点距离计算函数 */
const getSpaceDistance = (positions: any) => {
let lengthAll = 0;
for (let i = 0; i < positions.length - 1; i++) {
// 1.将起点与终点位置信息从笛卡尔坐标形式转换为Cartographic形式
const point1cartographic = Cesium.Cartographic.fromCartesian(positions[i]);
const point2cartographic = Cesium.Cartographic.fromCartesian(positions[i + 1]);
// 2.设置测地线起点和终点,EllipsoidGeodesic中setEndPoints常与surfaceDistance搭配使用
const geodesic = new Cesium.EllipsoidGeodesic();
geodesic.setEndPoints(point1cartographic, point2cartographic);
// 3. 得到空间中点投影到地球表面的曲面距离
let s = geodesic.surfaceDistance; // surfaceDistance返回number 单位为m,带小数
// 4. 考虑两点的高度 利用直角三角形求斜边来求实际距离
s = Math.sqrt(Math.pow(s, 2) + Math.pow(point2cartographic.height - point1cartographic.height, 2));
// 5.每段距离求和
lengthAll += s
}
return lengthAll
}
代码结果如图:
- 贴地线以及考虑地形要素后的贴地距离。
这部分算法主要参考了这位博主大大的贴地形距离算法
Cesium提供了一个叫 sampleTerrainMostDetailed 获取每个插值点在地形表面上的采样点的height
- 由上,需要定义地形;开启地形深度检测,从而正确绘制;开启clampToGround属性绘制线,使其贴地;绘制点设置heightReference属性,使实体位置始终贴地。同时 使用
viewer.scene.pickPosition
取代viewer.camera.pickEllipsoid
因此在地形上移动时可以得到准确的坐标。
const viewer = new Cesium.Viewer('cesiumContainer', {
terrainProvider: await Cesium.createWorldTerrainAsync() // createWorldTerrainAsync是个异步函数
})
viewer.scene.globe.depthTestAgainstTerrain = true; //开启地形深度检测
clampToGround: true,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
-
根据两个点的坐标计算:
-
Cesium.Cartesian3.distance()计算出两点间直线距离
-
根据两点间直线距离来插,每1米插一下,插的越多越精准。对于插值,使用cesium 提供的·
Cesium.Math.lerp(start, end, t)
,在这里不附带起点终点各自的height,并且分别对每个点的经度以及纬度插值,最后生成需要的数据类型
-
-
根据接口异步返回的数据,就是每一个插值点所对应的经纬度高Cartographic对象组成的数组,调用距离计算函数,依次相加得到起点和终点对应的贴地距离。
两个函数的代码如下:
const getTerrainDistance = (position: Cartesian3[]) => {
return new Promise((resolve)=>{
let i = position.length - 2
let startCart3 = position[i] // 起点
let endCart3 = position[i + 1] // 终点
let linearDistance = Cesium.Cartesian3.distance(startCart3, endCart3)
// 插值数量,我这里根据两点间直线距离来插,每1米插一下,插的越多越精准
let splitNum = Math.floor(linearDistance)
// 拿到这个直线间每个点的经度纬度然后转为Cartographic的数组包括起点终点
const positions = []
let startCartographic = Cesium.Cartographic.fromCartesian(startCart3)
let endCartographic = Cesium.Cartographic.fromCartesian(endCart3)
// 不附带起点终点的地形而插值
let startDegrees = [startCartographic.longitude, startCartographic.latitude] // 经度纬度弧度制
let endDegrees = [endCartographic.longitude, endCartographic.latitude] // 经度纬度弧度制
positions.push(new Cesium.Cartographic(startDegrees[0], startDegrees[1]))
for (let i = 0; i < splitNum; i++) {
// 分别在经度纬度方向插值
let x = Cesium.Math.lerp(startDegrees[0], endDegrees[0], i / splitNum)
let y = Cesium.Math.lerp(startDegrees[1], endDegrees[1], i / splitNum)
positions.push(new Cesium.Cartographic(x, y))
}
// 地形细节采样:传入 目标地形 和 制图坐标插值组(不贴附地形意思就是height = 0) 获取 贴地形的制图坐标插值组,经度纬度高,不过经纬度是弧度制 再计算距离
Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, positions).then(cartographicArr => {
// console.log(cartographicArr)
addDistance(cartographicArr).then(distance=>{
// console.log(result)
resolve(distance)
})
})
})
}
/**
* @description 根据制图坐标计算距离
* @param {Array.<Cesium.Cartographic>} cartographicArr 制图坐标数组
* @returns {number} 距离值
*/
const addDistance = (cartographicArr:Cartographic[]) =>{
return new Promise((resolve)=>{
let terrainDistance = 0;
cartographicArr.map((item,index)=>{
if (index === cartographicArr.length - 1) return
let nextItem = cartographicArr[index+1]
let currentPosition = Cesium.Cartesian3.fromRadians(item.longitude, item.latitude, item.height)
let nextPosition = Cesium.Cartesian3.fromRadians(nextItem.longitude, nextItem.latitude, nextItem.height)
terrainDistance += Cesium.Cartesian3.distance(currentPosition,nextPosition)
})
// 异步完成后返回
resolve(terrainDistance)
})
}
结果如图:第一个是距离标签非动态变化。第二个则是可以随鼠标移动动态变化。
⚠️1. 异步问题:
在这里,距离的插值计算,以及最后插值点距离求和,都是一个异步函数。
使用语法:
const getTerrainDistance = () => {
return new Promise((resolve)=>{
...
resolve(result) // 使用 resolve 返回计算结果
})
// 如何拿到这个结果呢?
getTerrainDistance().then((result)=>{})
而且上面的代码里面还是一个嵌套的异步,逻辑相同。
- 绘制线的问题
定义一个绘制线的点的数组。
逻辑:
对于鼠标左击,判断如果长度是0,就加入点击的点,在外部需要再次把这点加进去,那么第一次点击,这个数组的长度就是2。并且两个值相等。如果在此时就调用计算距离,用 length - 2 得到的就是0,所以需要在下一次鼠标左击时,此时数组的长度变成3,鼠标移动只会动态修改第三个值,那么我们用 length - 3 从而拿到第一次和第二次点击的坐标可以计算距离,后面的以此类推。
代码如下:这样实现的也是点击后计算距离
if (polylinePoints.length >=3){
getTerrainDistance(polylinePoints).then((result:any) => {
console.log(result)
})
const getTerrainDistance = (position: Cartesian3[]) => {
return new Promise((resolve)=>{
let i = position.length - 3
let startCart3 = position[i] // 起点
let endCart3 = position[i + 1] // 终点
})
鼠标移动,动态的修改数组中的最后一个元素,根据鼠标移动变化而变化,所以如果在这里计算距离,可以实现动态距离的显示,但要注意,点击后移动数组的长度就是2,我们只需要 将 length - 2 就可以实现
代码如下:
// 鼠标移动
handler.setInputAction((movement: any) => {
const worldPoint = viewer.scene.pickPosition(movement.endPosition);
if (entity) {
polylinePoints.pop()
polylinePoints.push(worldPoint)
// 动态计算距离
getTerrainDistance(polylinePoints).then((result:any) => {
// console.log(result)
distance = result
viewer.entities.remove(labelEntity); // 标签动态变化
labelEntity = drawLabel(polylinePoints[polylinePoints.length - 1], result.toFixed(2) + "米");
})
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE)