尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Cesium三维GIS线段绘制:DrawHandler原理、实现与超图集成实战

Cesium三维GIS线段绘制:DrawHandler原理、实现与超图集成实战 1. 项目概述从“画线”到空间数据交互的基石在三维地理信息系统的世界里绘制一条线段听起来是再基础不过的操作。但如果你深入过WebGL三维引擎的开发尤其是像Cesium这样处理全球级地理数据的平台你就会明白这个“画线”动作背后远不止是调用一个drawLine函数那么简单。它涉及到鼠标事件与三维球面坐标的精确转换、几何实体的动态创建与样式管理、绘制状态的流畅切换以及最终如何将用户交互的草图转化为可供空间分析的结构化数据。今天我们就以“超图与Cesium结合使用Cesium.DrawHandler绘制线段”这个具体场景为切入点深挖一遍三维标绘功能从零到一的实现逻辑、核心API的运用心法以及那些官方文档里不会明说但实际开发中一定会踩到的“坑”。这个功能的核心价值在于它为任何基于Cesium的三维GIS应用提供了最基础的空间数据采集能力。无论是规划一条管线、划定一个区域范围还是进行简单的距离量算线段绘制都是第一步。超图作为成熟的GIS平台其JavaScript客户端库对Cesium的原生绘制能力进行了封装和增强Cesium.DrawHandler尽管在较新版本的Cesium中其设计思想已被其他方式继承正是这一封装体系的典型代表。理解它不仅能完成功能更能透彻理解三维交互绘制的通用范式。2. 核心思路与方案选型为何是DrawHandler在Cesium的生态中实现绘制功能有多种路径。新手可能会直接想到监听鼠标事件然后手动创建Cesium.Polyline实体。这当然可行但代码会迅速变得臃肿你需要自己处理鼠标按下、移动、抬起的整个状态机处理坐标拾取scene.pickPosition还是scene.globe.pick处理实体临时预览与最终确认的区分还要考虑撤销、重做等交互。这显然不是高效的做法。Cesium.DrawHandler以及其后续演进的出现就是为了封装这一整套复杂的交互逻辑。它是一个“管理器”或“控制器”将绘制行为抽象为一种模式DrawMode。当你激活Cesium.DrawMode.Line模式时DrawHandler便接管了相关的鼠标和键盘事件内部维护着绘制状态并适时地抛出各种事件如drawStart,drawMove,drawEnd来通知你的应用程序。你的代码只需要监听这些事件并在合适的时机通常是drawEnd获取最终生成的几何数据然后将其转换为正式的、带样式的Cesium.Entity添加到数据源中。选择DrawHandler的核心理由有三点关注点分离它将底层的、繁琐的交互事件处理与上层的业务逻辑如数据保存、样式配置清晰地分离开。开发者只需关心“绘制完成后得到什么数据”以及“如何展示”而不用陷入“如何绘制”的泥潭。状态管理内聚它内部管理了绘制的生命周期开始、进行中、结束、临时图形的显示与销毁避免了开发者自己管理这些临时状态可能带来的内存泄漏或显示错误。与超图生态兼容在超图的产品体系中DrawHandler是连接Cesium原生能力与超图GIS服务如iServer发布的空间分析服务的桥梁之一。通过它绘制的几何对象可以更方便地传递给超图的各类服务进行处理。需要注意的是在Cesium的后续版本中大约1.46之后官方更推荐使用Cesium.ScreenSpaceEventHandler结合Cesium.Entity或Cesium.Primitive的CallbackProperty来实现动态绘制这提供了更灵活的底层控制。但DrawHandler的设计思想——模式化、事件驱动——依然是核心。许多项目特别是结合超图平台的项目仍在使用或参考这套模式进行封装因为它足够清晰、稳定且与GIS工作流契合度高。3. 环境准备与关键API深度解析在开始写代码之前我们需要确保环境就绪并彻底理解几个关键对象。3.1 基础环境搭建假设你已经有一个运行着Cesium Viewer的HTML页面。你需要引入Cesium和超图的客户端库。典型的引入顺序和关键配置如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 title超图Cesium线段绘制示例/title !-- 引入Cesium -- script srchttps://cesium.com/downloads/cesiumjs/releases/1.XX/Build/Cesium/Cesium.js/script link hrefhttps://cesium.com/downloads/cesiumjs/releases/1.XX/Build/Cesium/Widgets/widgets.css relstylesheet !-- 引入超图Cesium客户端库 -- script srchttp://localhost:8090/iserver/client/cesium/Cesium.js/script /head body div idcesiumContainer/div script // 初始化Cesium Viewer关闭默认的地球旋转、动画控件等让界面更专注于绘制 Cesium.Ion.defaultAccessToken 你的Ion Token; // 如果需要Cesium Ion资源 var viewer new Cesium.Viewer(cesiumContainer, { animation: false, timeline: false, baseLayerPicker: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, fullscreenButton: true }); // 后续代码将在这里编写 /script /body /html注意超图的客户端库可能会覆盖或扩展原生的Cesium对象因此务必按官方文档建议的顺序引入。上述示例中第二个script标签引入的超图库其路径需根据你的超图iServer实际部署地址进行调整。3.2 核心APICesium.DrawHandler与Cesium.DrawModeCesium.DrawHandler构造函数通常接受两个参数一个Cesium Viewer实例以及一个可选的配置对象。它的核心作用是创建一个绘制管理器。var drawHandler new Cesium.DrawHandler(viewer, { clampToGround: true, // 是否贴地绘制。对于线段true表示线段节点会吸附到地形表面false则在空间直线绘制。 showPoints: true, // 绘制时是否显示节点小点 pointSymbol: Cesium.Color.RED, // 节点样式 polylineSymbol: Cesium.Color.YELLOW.withAlpha(0.7) // 临时线段的样式 });Cesium.DrawMode是一个枚举定义了绘制模式。对于我们今天的主题就是Cesium.DrawMode.Line。其他常用模式还有Polygon面、Point点等。激活绘制模式的方法是drawHandler.activate(Cesium.DrawMode.Line);一旦激活DrawHandler就会开始监听鼠标事件进入绘制状态。此时鼠标在球面上点击就会开始定义线段的节点。3.3 事件循环理解绘制的生命周期DrawHandler通过事件与我们通信。理解这些事件的触发时机是正确使用它的关键。drawStart: 当用户点击第一个点绘制行为正式开始前触发。此时可以做一些初始化工作比如清空上一个临时图形。drawMove: 当用户移动鼠标在点击第一个点后双击结束前时不断触发。回调函数会传入当前鼠标位置对应的世界坐标。这个事件用于更新临时线段的视觉反馈。drawEnd: 当用户双击结束绘制时触发。这是最重要的一个事件。回调函数会传入最终生成的所有节点的坐标数组Cartesian3数组。我们拿到这个坐标数组就可以创建正式的Cesium.Entity了。drawStop: 当绘制被主动取消如调用drawHandler.deactivate()时触发。mouseOut: 当鼠标移出Cesium容器时触发可用于暂停绘制反馈。我们的核心逻辑将围绕监听drawEnd事件展开。4. 完整实现步骤与代码逐行解读让我们一步步实现一个完整的、带撤销功能的线段绘制工具。4.1 初始化与全局状态管理首先我们定义一些全局状态。// 在viewer初始化之后 var activeDrawHandler null; // 当前活动的绘制处理器 var drawnEntities []; // 存储所有已绘制完成的线段实体用于撤销等操作 var temporaryEntity null; // 指向当前正在绘制的临时实体4.2 激活线段绘制模式我们需要一个UI按钮这里用HTML按钮示例来触发绘制。button iddrawLineBtn绘制线段/button button idclearAllBtn清除全部/button button idundoBtn撤销上一条/buttondocument.getElementById(drawLineBtn).addEventListener(click, function() { // 如果已有激活的绘制先停用它 if (activeDrawHandler) { activeDrawHandler.deactivate(); } // 创建新的DrawHandler实例 activeDrawHandler new Cesium.DrawHandler(viewer, { clampToGround: true, // 根据需求选择是否贴地。地形起伏大时贴地线段会随地形弯曲。 showPoints: true, pointSymbol: Cesium.Color.RED, polylineSymbol: Cesium.Color.CYAN.withAlpha(0.5) // 临时线用半透明青色 }); // 监听drawEnd事件 activeDrawHandler.drawEvt.addEventListener(function(result) { // result.object.positions 就是绘制结束时所有点的坐标数组 var positions result.object.positions; // 检查是否至少有两个点构成一条线段 if (positions.length 2) { console.warn(线段至少需要两个点。); return; } // 创建正式的、带样式的线段实体 var lineEntity viewer.entities.add({ name: 绘制线段_ new Date().getTime(), polyline: { positions: positions, width: 5, // 线宽 material: Cesium.Color.BLUE, // 最终颜色设为蓝色 clampToGround: true // 与绘制时设置保持一致 } }); // 将实体存入历史数组 drawnEntities.push(lineEntity); // 绘制结束后自动停止当前的绘制模式避免误操作。 activeDrawHandler.deactivate(); activeDrawHandler null; // 清除临时实体引用 temporaryEntity null; console.log(线段绘制完成坐标点, positions); }); // 监听drawMove事件用于高亮显示临时线段可选DrawHandler已内置 // 但我们可以利用它来做一些自定义效果比如实时显示线段长度 activeDrawHandler.drawMoveEvt.addEventListener(function(movement) { // movement.endPosition 是当前鼠标位置 // 这里可以计算并实时更新UI显示长度代码略 }); // 最后激活绘制模式 activeDrawHandler.activate(Cesium.DrawMode.Line); console.log(线段绘制模式已激活请在地球上点击绘制双击结束。); });4.3 实现撤销与清除功能有了drawnEntities数组实现撤销和清除就很简单了。// 撤销上一条线段 document.getElementById(undoBtn).addEventListener(click, function() { if (drawnEntities.length 0) { var lastEntity drawnEntities.pop(); // 从数组取出最后一项 viewer.entities.remove(lastEntity); // 从场景中移除 console.log(已撤销上一条线段。); } else { console.log(没有可撤销的线段。); } }); // 清除所有已绘制的线段 document.getElementById(clearAllBtn).addEventListener(click, function() { // 遍历数组移除所有实体 drawnEntities.forEach(function(entity) { viewer.entities.remove(entity); }); // 清空数组 drawnEntities.length 0; console.log(已清除所有线段。); });4.4 样式定制与高级特性基础的绘制完成了但一个健壮的工具还需要更多细节。1. 动态样式与分类显示我们可以根据线段属性如类型、状态设置不同样式。假设我们从drawEnd事件拿到数据后还要从服务器获取该线段的属性。// 在drawEnd事件监听器内创建实体时使用更复杂的material var lineMaterial new Cesium.PolylineOutlineMaterialProperty({ color: Cesium.Color.fromCssColorString(#FF6B6B), // 主线颜色 outlineWidth: 2, outlineColor: Cesium.Color.BLACK }); var lineEntity viewer.entities.add({ polyline: { positions: positions, width: 8, material: lineMaterial, clampToGround: true }, // 可以附加自定义属性用于后续查询或分类 properties: { type: pipeline, status: planned, length: calculateGeodesicLength(positions) // 计算地理长度 } });2. 地理长度计算计算线段在地球椭球体上的实际长度这是一个很常见的需求。function calculateGeodesicLength(positions) { var length 0; for (var i 0; i positions.length - 1; i) { var geodesic new Cesium.EllipsoidGeodesic(); // 将笛卡尔坐标转换为经纬度弧度 var cartographic1 Cesium.Cartographic.fromCartesian(positions[i]); var cartographic2 Cesium.Cartographic.fromCartesian(positions[i1]); geodesic.setEndPoints(cartographic1, cartographic2); length geodesic.surfaceDistance; } // 转换为公里 return length / 1000; }3. 贴地ClampToGround的注意事项当clampToGround: true时Cesium会尝试将线段的每个节点贴到地形或3D Tiles表面。这会产生两个关键影响性能贴地计算比空间直线更耗性能尤其是在复杂地形和大量线段时。精度与显示当地形细节不够如全球地形或节点间距很大时贴地线段可能看起来是“穿透”地形的因为它是采样有限的地形高度点进行连接。对于精度要求高的场景需要更密集的节点或使用Cesium.SampledPositionProperty进行插值。5. 常见问题、性能优化与避坑指南在实际项目中仅仅实现功能是不够的稳定性和性能同样重要。下面是我在多个项目中总结的经验和教训。5.1 内存泄漏与对象销毁问题反复激活/停用DrawHandler或者不正确地管理实体会导致内存占用不断上升。解决方案单一实例管理像上面示例一样全局只维护一个activeDrawHandler引用。在激活新的绘制前务必调用旧实例的deactivate()方法并可以将旧引用设为null以便垃圾回收。销毁DrawHandlerDrawHandler提供了destroy()方法。如果你确定某个绘制器不再使用应调用drawHandler.destroy()来移除其所有事件监听器。实体清理从viewer.entities中remove实体时如果该实体有复杂的CallbackProperty或自定义材质也需要确保这些资源被正确释放。简单的颜色材质通常会被Cesium自动管理。5.2 绘制交互的体验优化问题1用户双击结束绘制时很容易因为手抖产生一个非常近的冗余点。优化在drawEnd事件中对positions数组进行预处理过滤掉距离过近的连续点。function simplifyPositions(positions, minDistance 1.0) { var simplified []; if (positions.length 0) return simplified; simplified.push(positions[0]); for (var i 1; i positions.length; i) { var dist Cesium.Cartesian3.distance(simplified[simplified.length - 1], positions[i]); if (dist minDistance) { simplified.push(positions[i]); } } return simplified; } // 在drawEnd中使用 var simplifiedPositions simplifyPositions(positions, 5.0); // 忽略5米内的点问题2在移动端双击操作不便捷。优化可以提供额外的UI控制按钮如“完成绘制”、“取消绘制”通过编程方式触发结束。DrawHandler本身可以通过drawHandler.stopDrawing()来手动结束当前绘制并触发drawEnd。5.3 与超图服务的集成绘制好的线段最终往往要保存到超图iServer发布的GIS服务中。几何对象转换DrawHandler给出的positions是Cesium.Cartesian3数组。要传递给超图服务通常需要转换为GeoJSON格式或超图自定义的几何结构如SuperMap.Geometry.LineString。// 将Cartesian3数组转换为经纬度数组 var degreesArray []; positions.forEach(function(cartesian) { var cartographic Cesium.Cartographic.fromCartesian(cartesian); var lon Cesium.Math.toDegrees(cartographic.longitude); var lat Cesium.Math.toDegrees(cartographic.latitude); var height cartographic.height; // 注意高度 degreesArray.push([lon, lat, height]); }); // 构建一个简单的GeoJSON LineString var geoJsonFeature { type: Feature, geometry: { type: LineString, coordinates: degreesArray // GeoJSON是[lon, lat, height]顺序 }, properties: { // 你的属性 } };调用编辑服务使用超图客户端库的SuperMap.REST.EditFeaturesService将GeoJSON数据发送到iServer完成要素的添加。5.4 在复杂场景下的性能考量当场景中有成千上万条线段时直接使用viewer.entities添加可能会影响性能。使用Primitive API对于静态的、大量重复的线段考虑使用更低级别的Cesium.Primitive或Cesium.GroundPrimitive集合。它们由WebGL直接管理渲染效率远高于Entity API。但缺点是失去了Entity的易用性和动态属性绑定能力。数据源聚合将所有线段放在一个Cesium.CustomDataSource或Cesium.GeoJsonDataSource中便于统一管理和控制显隐。细节层次LOD对于超长线段或密集线段可以实现LOD在视角拉远时显示简化版本或聚合表示拉近时再显示细节。6. 扩展思考从绘制到分析的工作流一条线段绘制出来仅仅是开始。在一个完整的三维GIS应用中它应该能触发后续的工作流属性录入绘制结束后弹出一个表单让用户输入该线段的属性如管线材质、管径、所属项目等。空间分析将线段几何发送给iServer进行缓冲区分析、叠加分析、通视分析等。例如绘制一条拟建道路立即分析其沿途的拆迁范围。实时量算在drawMove事件中实时计算并显示已绘制部分的长度和方位角。拓扑检查与已有的其他管线实体进行拓扑关系检查如是否交叉、间距是否合规。历史版本管理通过drawnEntities数组和撤销栈可以实现简单的绘图会话历史管理。更复杂的版本管理需要与后端数据库结合。实现这些扩展功能核心在于将drawEnd事件中获取的几何数据与你的业务逻辑、后端服务紧密结合起来。Cesium.DrawHandler提供了一个稳定可靠的输入接口而真正的价值在于你如何处理这些输入的空间数据。通过以上从原理到实现再到优化和扩展的完整梳理你应该对在“超图Cesium”环境下实现线段绘制功能有了透彻的理解。记住工具是死的工作流是活的。深刻理解每个API背后的设计意图和潜在成本才能构建出既流畅又稳健的三维地理信息应用。
返回列表