十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Cesium三维量测插件实现:坐标转换与距离面积高度计算

Cesium三维量测插件实现:坐标转换与距离面积高度计算 简介基于Cesium的三维量测插件源码包面向WebGIS前端开发与三维可视化应用工程师可在Cesium场景中快速实现距离、面积、高度等基础量测功能。压缩包共731个文件约7.08MB以126个JavaScript核心代码、145个PNG与76个GIF演示图、35个CSS样式、29个JSON配置及多类型静态资源组成同时附带SVG图标、字体文件、HTML示例页面、TypeScript声明与文档便于直接参考调用。插件使用方式简洁引入Cesium.js后再加载cesium-measure.js即可接入现有项目降低三维测量功能开发门槛。资源还包含Cesium的gltf模型与wasm等支撑文件源码与说明文档分层存放适合有一定Cesium基础、希望扩展三维编辑能力的开发者研究实现思路。目前已有852人学习下载是快速集成与二次开发的高性价比参考。1. 三维量测插件的核心定位让 Cesium 场景从看得见到量得出在智慧城市、水利水电、应急指挥这类三维 GIS 项目里模型是建出来了场景也能自由漫游但业务侧最常问的一句话是这个施工区到底有多大塔吊到高压线的水平距离几米基坑开挖深度和设计值差多少。Cesium 引擎擅长渲染三维地球却不直接提供点两下出距离的现成能力。基于 Cesium 的基础三维量测插件就是把鼠标点击、世界坐标拾取、空间换算、距离面积高差计算、图形与标注绘制这五件事封装成一套可复用的交互组件外部只需调用 start、clear、destroy 就能获得完整的量测语义。它适合三维平台前端开发、GIS 应用研发工程师拿去直接集成或裁剪扩写也适合团队在启动新项目时少重复写一遍 ScreenSpaceEventHandler 绑定的样板代码。下文从坐标系换算讲起逐步展开一个可运行、可扩展的插件实现。2. 量测能算准的前提Cesium 坐标系换算与投影边界2.1 三种坐标形态与一条转换链路Cesium 的量测插件表面上是在画线实质上是在维护一条坐标换算管道屏幕坐标{ x, y }进入事件回调后先转成世界坐标Cartesian3再转成地理坐标Cartographic最后才能被人类读懂并参与距离计算。Cartesian3是 ECEF 地心固定坐标系下的空间点单位为米Cartographic包含经度、纬度、高度但经度和纬度都是弧度制屏幕坐标则是像素值。三者之间转换并不复杂可插件里最容易出错的恰恰是弧度与角度的混用。// 屏幕坐标 - 世界坐标优先取真实几何表面兜底取椭球面 function pickPosition(viewer, windowPosition) { const scene viewer.scene; if (scene.pickPositionSupported) { const cartesian scene.pickPosition(windowPosition); if (cartesian) return cartesian; } // 深度拾取不可用时用射线与椭球体求交 const ray viewer.camera.getPickRay(windowPosition); return scene.globe.pick(ray, scene); } // 世界坐标 - 经纬度对象Cartesian3 与 Cartographic 互转 function cartesianToLonLat(cartesian, ellipsoid) { const carto ellipsoid.cartesianToCartographic(cartesian); return { lng: Cesium.Math.toDegrees(carto.longitude), lat: Cesium.Math.toDegrees(carto.latitude), height: carto.height, carto, }; }第一段pickPosition先尝试scene.pickPosition它是基于深度缓冲区反算真实场景表面坐标的地形、3D Tiles 和模型都能取到贴合表面的交点当场景为空球或深度拾取被禁用时再退回到globe.pick用射线与 WGS84 椭球求交。第二段把Cartesian3转成普通对象的经纬度表示height是从椭球面起算的椭球高不是海拔高。Cesium.Math.toDegrees是弧度转角度显示经纬度时不可省略。2.2 屏幕拾取的两条路径与参数差异在 Cesium 里做屏幕拾取最常见的有三条路径它们的返回结果和适用条件差别很大拾取方式返回几何前提条件典型场景scene.pickPosition深度缓冲区中真实可见的几何表面点pickPositionSupported为真地形与模型量测默认推荐camera.pickEllipsoid射线与参考椭球的数学交点无需特殊配置空球、白模底图时兜底scene.globe.pick射线与地形网格的交点需要先用getPickRay生成射线无模型但有地形时其中camera.pickEllipsoid完全不感知地表起伏遇到山地会明显量短scene.pickPosition受depthTestAgainstTerrain影响较大开启该选项后地形遮挡关系才准确否则点可能穿透山体取到背面的坐标。量测插件对外暴露一个pickMode参数默认设为auto内部按深度拾取 → globe 拾取 → 椭球拾取的顺序逐个降级。这套降级逻辑在大多数业务里都能得到合理结果有高精度模型时贴合模型模型缺省时至少有椭球交点兜底不至于让点击事件返回空坐标。2.3 投影、椭球与量测精度之间的关系Cesium 场景默认的参考椭球是 WGS84国内项目多以 CGCS2000 作为地理基准两者椭球参数差异在毫米到厘米级三维量测层面基本不产生可见偏差。真正影响精度的是距离公式的选择。Cesium.Cartesian3.distance(a, b)算的是穿过地壳的三维欧氏直线距离Cesium.EllipsoidGeodesic计算的是沿椭球面从起点到终点的测地线最短距离适合表达地表距离。举个例子北京到上海的Cartesian3.distance约 1067 公里而沿椭球面的测地距离约 1084 公里相差约 1.5%。这个差异在逐点量测时不能忽略所以插件里的量距功能应暴露distanceMode: euclidean | geodesic由上层调用方按业务口径选择。海拔差大的山区两类公式差异更大因为高度直接进入Cartesian3的 z 分量。另一个高频问题是加载 EPSG:3857 Web 墨卡托数据源如果不做坐标过滤就把 3857 的东西向平面坐标当作经纬度喂给Cartographic点位就会飘到错误区域。量测插件启动时若项目里接了 3857 切片务必在数据入口用Cesium.Math.toRadians对平面坐标做一次转换过滤再进入拾取链路。3. 可复用的 Cesium 基础三维量测插件类骨架与生命周期3.1 插件类的职责边界与字段设计量测插件应该遵循一个原则只管输入—状态—计算—输出不把相机控制、图层加载等功能揽进自己怀里。一个最小的插件类只需维护四类状态鼠标事件处理器、量测顶点数组、量测模式枚举、临时预览点。下面是一个完整的类骨架也是整个插件的核心容器。export default class MeasureTool { constructor(viewer, { mode distance } {}) { this._viewer viewer; this._scene viewer.scene; this._mode mode; // distance | area | height this._positions []; this._handler null; this._active false; // 用独立数据源管理量测实体避免污染业务实体 this._dataSource new Cesium.CustomDataSource(measure-tool); viewer.dataSources.add(this._dataSource); } get isActive() { return this._active; } start() { if (this._active) return; this._active true; this._bindHandler(); } stop() { this._active false; this._unbindHandler(); } clear() { this._positions.length 0; this._dataSource.entities.removeAll(); } destroy() { this.stop(); this.clear(); this._viewer.dataSources.remove(this._dataSource, true); this._handler null; } }viewer.dataSources.add(this._dataSource)是最值得说明的一行量测产生的所有折线、多边形、点、标注都归属于这个独立数据源clear()时removeAll()只动派生数据不影响业务图层。如果直接把量测实体塞进viewer.entities不仅清理要遍历大量实体业务侧的地物隐藏、样式联动还会被误伤。字段以_开头是社区惯例表示内部状态不直接改isActive提供只读访问避免外部直接篡改_active导致事件与状态不同步。3.2 事件绑定单双击与预览更新的节奏量测交互最常用的事件是三件套左键单击加点、鼠标移动预览、左键双击结束。下面是一次完整的绑定实现。_bindHandler() { if (this._handler) return; this._handler new Cesium.ScreenSpaceEventHandler(this._scene.canvas); this._handler.setInputAction((movement) { const cartesian pickPosition(this._viewer, movement.position); if (!cartesian) return; this._positions.push(cartesian); this._renderMeasure(); }, Cesium.ScreenSpaceEventType.LEFT_CLICK); this._handler.setInputAction((movement) { if (this._positions.length 0) return; this._previewPoint pickPosition(this._viewer, movement.endPosition); this._renderMeasure(); }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); this._handler.setInputAction(() { this._finishMeasure(); }, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK); // 右键清除本次量测遇到业务右键菜单冲突时可直接移除 this._handler.setInputAction(() { this.clear(); }, Cesium.ScreenSpaceEventType.RIGHT_CLICK); }事件绑定里有一个容易踩的坑在 Cesium 中LEFT_DOUBLE_CLICK触发前浏览器事件流会先派发两次LEFT_CLICK导致顶点数组末尾混入两个重复点。处理方式是双击回调里先判断尾两个点的屏幕像素距离小于 2 像素就把重复点剔除再进入结果计算。右键清除在原型阶段非常有用接入成熟项目时若与业务右键菜单冲突直接把RIGHT_CLICK分支换成自定义cancel事件即可。3.3 生命周期与工具栏集成量测插件的交互状态可以用一个小状态机管理idle → measuring → finished → idle。start()进入measuring_finishMeasure()进入finishedclear()回到idle。工具栏按钮的高亮与置灰切换完全依赖isActive这个只读入口UI 层不需要感知_positions的内部细节。方法行为对比表方法激活状态顶点数据事件绑定start()进入 measuring保留已有顶点绑定stop()回到 idle保留已有顶点解绑clear()不变清空顶点与实体不变destroy()释放实例清空全部销毁 handler如果量测插件和鹰眼视图放在同一个工具栏里需要留意事件顺序鹰眼通常监听viewer.clock.onTick做小地图同步量测插件监听的是画布鼠标事件两者互不干扰。但若是台风路径量测这类需要把结果同步到鹰眼小地图的场景就应在_renderMeasure之外额外抛出一个onMeasureChange(positions)回调由鹰眼组件自行消费。常见做法是构造函数的options里传入onMeasureChange函数量测过程中每推入一个顶点就调用一次。4. Cesium 量测插件中距离、面积、高度的实现细节与参数取舍4.1 距离量测动态折线与测地距离距离量测是插件里最先实现的模式。用户在场景里逐点点击插件实时更新一条折线并累加各段距离。核心的渲染与计算逻辑放在_renderMeasure里_renderMeasure() { const positions this._positions.slice(); if (this._previewPoint) positions.push(this._previewPoint); if (positions.length 2) return; // 每帧先清空量测数据源避免实体堆积 this._dataSource.entities.removeAll(); // 折线实体把已确认顶点和预览点连接成一条红线 this._dataSource.entities.add({ polyline: { positions, width: 3, material: Cesium.Color.RED, }, }); // 沿椭球面累加各段测地距离 const ellipsoid this._scene.globe.ellipsoid; let total 0; for (let i 0; i positions.length - 1; i) { const start ellipsoid.cartesianToCartographic(positions[i]); const end ellipsoid.cartesianToCartographic(positions[i 1]); total new Cesium.EllipsoidGeodesic(start, end).surfaceDistance; } // 把累计距离标注到最后一个点附近 this._dataSource.entities.add({ position: positions[positions.length - 1], label: { text: total.toFixed(2) m, font: 14px sans-serif, pixelOffset: new Cesium.Cartesian2(10, -20), }, }); }positions由已确认顶点加预览点组成每帧先removeAll()再重建渲染开销可控远低于实体不断堆积导致的卡顿。EllipsoidGeodesic.surfaceDistance返回的是沿椭球面的最短路径长度逐段累加比首尾直接算一次更符合三维场景里逐步点击的操作直觉。如果业务需要的是两栋楼之间的直线跨距把surfaceDistance替换成Cesium.Cartesian3.distance(positions[i], positions[i1])即可两种公式只差一个属性名但语义完全不同。4.2 面积量测多边形顶点与投影面积换算面积量测用到polygon几何体Cesium 在渲染层完成多边形三角化插件只需要回填顶点并计算面积。计算球面多边形面积通常采用球面近似算法// 球面多边形面积对经纬度边界做球面投影累加 function computePolygonArea(positions, ellipsoid) { const cartographics positions.map((p) ellipsoid.cartesianToCartographic(p) ); let area 0; for (let i 0; i cartographics.length; i) { const p1 cartographics[i]; const p2 cartographics[(i 1) % cartographics.length]; // 每段边界的面积贡献经度差 × 纬度正弦和 area (p2.longitude - p1.longitude) * (2 Math.sin(p1.latitude) Math.sin(p2.latitude)); } return Math.abs(area * 6378137.0 * 6378137.0 / 2); }该公式把每个边界段映射到球面投影后累加假设参考球半径为 6378137 米在几千平方公里的范围内误差可忽略。渲染侧把this._positions放入polygon的hierarchy同时设置perPositionHeight: true保留每个顶点的 Z 值这样多边形在三维地形上是贴皮的而不是悬在半空。面积结果的展示可加单位换算超过 100 万平方米时显示为平方千米避免长数字刷屏。4.3 高度量测椭球高、海拔高与相对高差的区分scene.pickPosition返回的Cartesian3转换出的Cartographic.height是椭球高不是海拔高。对大多数业务场景用户关心的是两点之间的相对高差而不是绝对海拔。相对高差的计算非常直接_finishHeightMeasure() { if (this._positions.length 2) return; const ellipsoid this._scene.globe.ellipsoid; const c1 ellipsoid.cartesianToCartographic(this._positions[0]); const c2 ellipsoid.cartesianToCartographic(this._positions[1]); // 相对高差 两者椭球高之差 const diff c2.height - c1.height; this._dataSource.entities.add({ position: this._positions[1], label: { text: 高差 ${Math.abs(diff).toFixed(2)} m, font: 14px sans-serif, pixelOffset: new Cesium.Cartesian2(10, -20), }, }); }diff为正表示终点高于起点为负则相反。若业务需要真实海拔不能只读Cartesian3转出的椭球高而要结合Cesium.sampleTerrainMostDetailed对地形高度做采样或接入大地水准面模型做改正。很多新手在山区项目里量高差偏差几十米原因就是把椭球高当成了海拔高这个口径问题在需求评审阶段就应先对齐。4.4 量测结果的多段管理与一键清除三种模式整合进同一个插件对象时保持clear()幂等是关键无论用户当前处于measuring还是finished调用clear()都只清量测数据而保留插件实例。前面的骨架里用_dataSource.entities.removeAll()做到了这一点。更精细的做法是维护一个_measurements[]数组每次完成一个量测块就push一条记录clear()遍历删除并清空数组这样能在 UI 上实现撤销上一次量测的操作。量测模式的数值口径差异需要在使用文档里写明量测模式几何对象数值口径关键设置距离polyline测地距离或欧氏距离distanceMode面积polygon球面投影近似面积perPositionHeight高度polyline label椭球高差海拔需sampleTerrain5. Cesium 量测插件的精度校验与三个高频排错技巧5.1 用已知地理数据反向量测误差插件写完不要直接接业务先做一轮精度校验。选两个已知经纬度坐标的点比如同一经度上纬度相差 1 度的两点理论距离约 111.2 公里。在场景里量一次看结果与理论值偏差是否小于 0.1%。校验步骤建议按下面的顺序走在两个已知经纬度点之间量测距离确认测地线结果与理论值偏差小于 0.1%在带地形的场景里量同一条山脊线分别用pickPosition和pickEllipsoid各测一次两者差值就是地形起伏引入的误差用 Cesium 自带的绘制矩形工具先框出一个规则的已知范围再用面积量测验算回推结果确认面积公式和顶点顺序没有颠倒。完成这三步后再去接业务数据后续排查会省很多时间。记得把校验用的经纬度点和理论值写进插件的单元测试防止后续重构时把公式改坏。5.2 三个高频排错场景与应对排错一量测点悬在半空或贴不到模型表面。先检查scene.pickPositionSupported是否为 true再确认没有在viewer初始化之前调用pickPosition。Cesium 的相机与场景在初始化完成后才可拾取常见于在viewer构造后立即启动量测插件的场景稍等一帧或监听scene.postRender再启动即可。排错二双击结束时多出一个顶点。这是LEFT_CLICK先于LEFT_DOUBLE_CLICK触发的典型表现。有效方案是双击回调里把_positions末尾两个点中距离小于 5 像素的重复点剔除再进入结果计算。这个阈值要定义在插件配置项里因为不同屏幕像素比下眯点手感不同。排错三3D 地球在连续量测旋转时出现卡顿甚至崩溃。多数原因是量测实体持续堆积_renderMeasure每帧removeAll再add量测块数量增长后渲染队列快速膨胀。解决思路是限制单次量测的最大顶点数把量测结果从Entity体系沉淀到静态PrimitiveEntity 适合交互Primitive 适合渲染转换时机选在_finishMeasure完成那一刻。提示如果发现距离结果与真实测量差 10% 以上优先检查拾取方式是否退回成了椭球拾取而不是先怀疑公式写错。最后补充一个在连续量测场景里很实用的性能细节量测标注超过几十个时把每帧的removeAll加add改为相机静止后再重建——监听scene.postRender判断相机位置和朝向是否发生变化未变化就不重绘只有用户停止旋转后才一次性提交结果帧耗时能稳定控制在个位数毫秒级别量测点位再多也不至于拖垮渲染管线。本文还有配套的精品资源点击获取
返回列表