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

资讯详情

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

WebGIS二三维一体化浏览:SuperMap iClient联动设计与实践

WebGIS二三维一体化浏览:SuperMap iClient联动设计与实践 简介面向GIS开发者的SuperMap iClient for 3D平面场景二三维一体化浏览资源包用于实现二维三维地图在同一平台的无缝切换帮助解决传统GIS中二三维视角分离、空间信息表达不直观的问题可应用在城市规划、环境分析、交通管理等专业场景。资源共18个文件压缩包约18.9MB主要包含js脚本、html演示页面、udd/udb空间数据文件以及docx说明文档分别覆盖前端调用、示例工程、地理数据和开发说明。已有445人学习适合有一定SuperMap基础、正在学习三维GIS开发的初中级工程师。通过学习该包可掌握数据处理与融合、三维建模、交互操作、性能优化如LOD、用户体验及跨平台兼容性等关键技术借助示例代码能快速搭建二三维一体化浏览原型。资源以SuperMap iClient经典示例为基础兼顾概念讲解和工程实践可直接复用其中代码片段有助提升GIS开发效率。1. 项目概述与需求定位最近在做一个WebGIS项目用户既想要传统二维地图的简洁交互又想要三维场景的直观体验。折腾了一圈最后落地用的是SuperMap iClient for 3D来做“平面场景二三维一体化浏览”也就是同一个业务界面里面左边二维地图、右边三维场景两边联动浏览、同步定位。这里把整个项目的设计思路、落地细节和踩坑过程完整梳理一遍。这类需求在GIS行业里太常见了。比如做智慧园区的时候运维侧需要二维平面图快速定位到具体楼栋再跳转到三维场景看楼宇外观、管线走向。如果做成两个独立的系统用户就得在两套界面之间来回切换操作成本很高。二三维一体化浏览的核心价值就是把二维地图的“轻”和三维场景的“重”融入同一个交互闭环二维负责宏观定位三维负责微观呈现两者通过坐标和视角联动无缝衔接。1.1 这个项目解决什么问题这里要明确所谓“二三维一体化”并不是简单地把一张二维地图和一个三维地球摆在一个页面里而是要做到以下几点视点同步二维地图移动中心点三维场景相机跟着飞到对应位置反过来旋转三维视角二维地图同步缩放到相同区域。图层关联二维矢量图层和三维模型图层共享同一套数据源选中某个地物时两边同时高亮。操作互补支持在二维图上框选范围三维场景直接保留该范围的三维要素再做空间量算、剖切等分析。1.2 技术选型为什么选SuperMap iClient for 3D市面上的三维WebGIS方案不少Cesium、LeafletThree.js、MapboxGLdeck.gl等都能做类似效果。选SuperMap iClient for 3D最主要的考虑是项目底层数据源是SuperMap iServer发布的服务iClient for 3D对自家的REST服务、S3M三维缓存格式支持是最完善的起码省掉了大量格式适配的工作量。另一方面iClient for 3D本身封装了Cesium在具备Cesium生态优势的同时提供了大量GIS能力比如多源数据叠加地形、影像、矢量、模型、点云、三维空间分析通视、剖面、淹没以及非常方便的Layer和Scene模型管理。这些能力如果用原生Cesium从头写工作量会大很多。如果你项目里的数据主要是ArcGIS或自研服务那完全可以选Cesium或MapboxGL但如果你已经跑在SuperMap体系里iClient for 3D基本是成本最低、见效最快的路线。2. 整体设计思路双向联动的核心机制二三维一体化的技术难点不在“画两个地图”而在于“两个视图如何基于同一套地理语义保持同步”。我把这个项目里的联动拆解成三个层面数据层、坐标层、视图层。2.1 数据组织方式二维地图加载的是iServer发布的Map服务通常是一组已经配好样式、切好缓存的地图瓦片三维场景加载的是iServer发布的三维服务数据源是S3M格式的缓存包括倾斜摄影模型、人工精模、BIM、点云等。两层数据在服务端共享同一个数据源但前端不能直接把二维瓦片叠加到三维地球上看除非你只做2.5D效果否则瓦片在三维视角下会拉伸变形、贴近地表。所以主体方案仍然是双容器渲染一个div承载二维SuperMap.Map另一个div承载三维Viewer。2.2 联动机制的工作流程核心思想是事件驱动加状态防抖。两个视图各自维护自己的交互事件当用户操作其中一个视图时触发事件回调在回调里计算出目标坐标和视角参数再调用另一个视图的API去执行飞行或平移。为了避免两个视图互相触发导致的死循环我给每次同步动作加了一个isSyncing标志位。同步过程中两个视图的change事件都会被暂时屏蔽等参数设置完成后再恢复监听。这一点不做的话大概率会看到相机来回抖动最后卡死。二维地图触发moveend ↓ 计算二维中心点经纬度 ↓ 根据zoom推算三维相机高度 ↓ 调用viewer.camera.flyTo ↓ 三维相机焦段变化触发camera.changed ↓ 反算二维中心点和zoom ↓ 调用map.setCenter同步二维地图流程不算复杂实际代码需要同时处理好事件防抖和坐标转换后面单独用一章来拆解。3. 核心细节解析坐标系同步与视点联动联动效果做得好不好核心在坐标转换和高度换算。这一步出错哪怕差一两个像素三维视角飞过去后中心点都会偏离目标地物。3.1 坐标转换的底层逻辑SuperMap二维地图默认的坐标系一般是“Web墨卡托”EPSG:3857或自定义高斯投影而三维场景里相机位置、经纬度计算都基于地理坐标系EPSG:4326。因此二维地图的center拿到的是投影坐标不能直接拿去驱动三维相机必须先转成经纬度。SuperMap的类库本身提供了LonLat对象但二维地图内部会使用Projection机制做投影管理。在设置二维中心点的时候可以传入经纬度SuperMap会基于地图当前投影自动转成投影坐标。反过来从二维地图拿到center后通过projection转换接口也可以取到经纬度。如果遇到自定义坐标系建议直接引入proj4js传入自定义投影参数做转换避免依赖iServer端的动态投影。3.2 视点同步算法二维地图的操作能力是“中心点缩放级别”三维相机的控制能力是“经纬度高度方向角”。要让两边能够互相映射我用了一个换算策略不追求严格的像素级等比而是保证“中心点一致、视觉比例和谐”。二维转三维时从map.getCenter()取出中心经纬度再根据zoom级别推算一个合适的相机高度。这个推算不要求绝对精确满足视觉比例即可我用的经验公式是height 20000 * 2^(12 - zoom)也就是zoom越大相机拉得越低、看得越细。实测下来zoom为10时高度约8000米zoom为16时高度约250米符合二维缩放尺度下的视觉习惯。三维转二维时从viewer.camera.positionCartographic取出相机经纬度直接把该经纬度setCenter到二维地图。缩放级别则按照相机高度反推zoom Math.round(12 - Math.log2(height / 20000))为什么不用真实分辨率去做严格换算因为二维地图和三维场景的屏幕可视范围、视野角度不同即使按像素级等比换算用户视觉上仍然会觉得有偏差。这里追求的是“中心点一致、大致比例一致”实践证明这种近似方案在交互上体验更好用户感受到的是流畅联动而不是机械对位。3.3 图层可见性控制一体化浏览还需要解决一个问题三维场景中加载了多层模型比如倾斜摄影、地下管线、BIM体块如果全部显示页面会卡到没法交互。我做了两个控制方案。第一是LOD控制。在Viewer的scene配置里开启LOD设置最大屏幕空间误差maxScreenSpaceError让远处模型自动抽稀近处模型精细显示。推荐值设在1.5到2之间太小会导致加载大量细节模型太大则远处模型会糊掉。第二是联动显隐。在二维地图的图层树上勾选某个专题图层三维场景对应模型图层也会同步显隐。这个实现的关键是找到二维图层与三维实体之间的唯一标识映射。我采用的是“服务地址图层名称”作为唯一键因为二维图层树和三维场景的图层列表都来自iServer元数据命名是一致的。监听二维图层树的勾选事件后按这个唯一键找到对应的Primitive或Entity集合统一设置show属性从而做到一套勾选逻辑同时控制两端的显示状态。4. 实操过程从零搭建二三维一体化页面下面把整个落地过程完整写一遍适用SuperMap iClient for 3D 10i及以上版本需要先部署好iServer并发布对应的二维地图服务和三维场景服务。4.1 环境准备与资源引入前端项目结构比较简单核心文件就三个一个HTML页面、一个业务JS、一个样式表。接入步骤如下到SuperMap官网下载iClient for 3D的发行包得到若干js/css文件。在页面里引入三个核心依赖Cesium.js、SuperMap.Web.B3D的库、业务主程序库。准备一个用来包裹三维场景的div需要显式设置宽高二维地图同样需要一个div。script srcpath/to/Cesium.js/script script srcpath/to/supermap-web-b3d.js/script link relstylesheet hrefpath/to/supermap-web-b3d.css注意iClient for 3D的库包体积比较大建议开启网站静态资源gzip压缩首屏加载能快不少。如果项目本身用Webpack/Vite打包也可以把库依赖放在externals里避免参与打包体积优化。我的实测经验是开启gzip后三维库的首屏加载时间能缩短一半以上。4.2 初始化二维地图与三维场景先看二维地图初始化这段逻辑和SuperMap iClient for JavaScript 2D的写法几乎一致// 二维地图容器 const map new SuperMap.Map(map2D, { controls: [new SuperMap.Control.ScaleLine()], projection: EPSG:3857 }); // 加载iServer发布的态势地图服务 const layer2D new SuperMap.Layer.TiledDynamicRESTLayer( BaseMap, http://127.0.0.1:8090/iserver/services/map-china/rest/maps/China, { transparent: true, cacheEnabled: true } ); layer2D.events.on({ layerInitialized: () map.addLayer(layer2D) });三维场景的初始化采用的是Scene方式// 三维场景容器 const viewer new SuperMap.Web.B3D.Viewer(map3D, { scene3D: { url: http://127.0.0.1:8090/iserver/services/3D-scene/rest/realspace, enableLighting: false }, controlOptions: { enableRotate: true, enableTranslate: true, enableZoom: true } });这里有个关键细节Viewer的初始化参数里scene3D.url指的是iServer三维服务地址不是SPB文件路径。如果你拿不到场景服务地址也可以直接用SceneManager从本地加载S3M缓存目录但那样就没有iServer的权限控制和动态调度能力了。生产环境建议用iServer地址开发调试时可以先用本地缓存快速验证。4.3 联动逻辑实现联动是项目最核心的代码我封装了一个双向同步类把坐标转换和事件防抖都收敛在里面。简化版代码如下class SyncManager { constructor(map, viewer) { this.map map; this.viewer viewer; this.isSyncing false; this.initEvents(); } initEvents() { // 二维移动结束后通知三维飞过去 this.map.events.register(moveend, this, this.onMapMoveEnd); // 三维相机变化时通知二维同步中心 this.viewer.camera.changed.addEventListener(this.onCameraChanged, this); } onMapMoveEnd() { if (this.isSyncing) return; this.isSyncing true; const center this.map.getCenter(); const lonLat this.toLonLat(center); const height this.calcHeightByZoom(this.map.getZoom()); this.viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees( lonLat.lon, lonLat.lat, height ), duration: 0.5 }); this.isSyncing false; } onCameraChanged() { if (this.isSyncing) return; this.isSyncing true; const carto this.viewer.camera.positionCartographic; const lon Cesium.Math.toDegrees(carto.longitude); const lat Cesium.Math.toDegrees(carto.latitude); const zoom this.calcZoomByHeight(carto.height); this.map.setCenter(new SuperMap.LonLat(lon, lat), zoom); this.isSyncing false; } toLonLat(center) { // 投影坐标转经纬度如果已经是经纬度直接返回 if (center.lon -180 || center.lon 180 || center.lat -90 || center.lat 90) { return this.map.projection.inverse(center); } return center; } calcHeightByZoom(zoom) { return 20000 * Math.pow(2, 12 - zoom); } calcZoomByHeight(height) { return Math.max(3, Math.round(12 - Math.log2(height / 20000))); } } const syncManager new SyncManager(map, viewer);写完后我实际测试下来flyTo的duration设0.5秒最合适太短会显得突兀太长用户会感觉迟钝。另外camera.changed是相机任何变化都会触发的高频事件如果项目里还需要做鼠标交互建议在同步前判断一下当前是否在飞行状态可以用viewer.scene.screenSpaceCameraController.enableInputs来判断或者直接用一个飞行标志位过滤中间态避免拖动过程中二维地图频繁刷新中心点。4.4 图层高亮联动的处理除了视点联动选中联动也很重要。我的做法是二维地图点击某个要素拿到要素属性里的唯一ID然后去三维场景里遍历Entity找到同ID的模型并设置高亮颜色。反向操作同理三维场景拾取模型后把ID回传给二维做闪烁。代码核心是两个事件绑定// 二维要素点击 map.events.register(featureSelected, this, (feature) { const id feature.attributes.ID; highlight3DEntity(viewer, id); }); // 三维场景点击拾取 viewer.screenSpaceEventHandler.setInputAction((movement) { const picked viewer.scene.pick(movement.position); if (Cesium.defined(picked)) { const id picked.id.properties.ID; highlight2DFeature(map, id); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);这里有几个坑一是三维拾取到的可能是S3M模型内部的子部件id未必是顶层要素的id需要先根据模型层级关系向上找到业务ID二是高亮材质如果直接修改Entity的color会破坏原始纹理更稳妥的做法是使用Cesium的colorBlendMode配合半透明白色叠加或者给模型单独添加一个高亮外壳体这样既能突出显示又不丢失原有贴图细节。5. 常见问题与排查技巧实录这个项目做完之后我在调试阶段踩了不少坑列几个最典型的方便后面接手的人少走弯路。现象可能原因解决方案三维场景一直白屏iServer服务地址错误或未开启跨域在浏览器控制台查看网络请求确认三维服务返回200iServer站点开启CORS配置二维地图空白只有底图背景色图层未初始化完成就addLayer监听layerInitialized事件再做addLayer或检查瓦片路径是否有token联动时三维中心点偏移明显二维地图用了自定义投影toLonLat未做投影转换引入proj4js使用自定义投影参数反算经纬度camera.changed频繁触发导致页面卡顿事件回调里做了过多的DOM操作使用requestAnimationFrame做节流只在每帧最后一次变化时同步三维模型高亮后纹理丢失直接改了entity.color改用colorBlendMode加半透明白色叠加保留原始纹理5.1 三维场景加载白屏排查这个问题八成出在服务端跨域或场景服务没配置好。我遇到过一次iServer部署在Linux服务器上端口默认8090前端页面用localhost访问WebSocket和HTTP请求都被浏览器拦截了。最终的解决办法是让前端使用和iServer同源的域名或者干脆把iServer的跨域开关打开并在代理层把3D服务的WebSocket也代理一遍。顺带一提如果页面是通过HTTPS访问的一定要确认iServer也是HTTPS否则混合内容会被浏览器直接阻止这一点在排查时很容易忽略。5.2 联动漂移问题的处理联动漂移的根因绝大多数是坐标系不统一。二维服务发布的是什么投影三维服务底层是什么坐标系两边的空间参考必须严格匹配。我在实施时踩过一次二维数据用的是CGCS2000高斯投影三维服务的地形是WGS84结果不管怎么调参数飞过去总是偏几百米。后来把二维地图的投影明确指定为EPSG:4490并在地图初始化的projection参数里做了配置问题立刻消失。这类问题在排查时不要只看页面代码先到iServer的服务元数据里确认两个服务的坐标系是否一致。5.3 高频事件与卡顿优化联动卡顿主要来自camera.changed。相机的每次微小移动都会触发该事件在浏览器里高频callback很容易让主线程满负荷。我的优化方案有两个层次第一层用requestAnimationFrame节流保证即使触发100次同步动作也只在浏览器渲染帧内执行一次第二层在用户拖拽三维场景时先不实时同步二维而是等用户松开鼠标后再同步一次。至于二维拖拽反过来用的是moveend事件本身只在拖拽结束时触发所以不需要额外节流。6. 从项目里总结的几条经验这个项目做完我印象最深的一点是二三维一体化浏览看着是个“呈现层”的活但实际上80%的工作都花在坐标系、事件机制和数据对应关系上。如果前期不从数据源头把空间参考统一好后面所有联动都是空中楼阁。给后续要做的朋友三点建议第一在项目启动阶段就确认二维和三维服务的坐标系是否一致不一致立刻做动态投影或数据转换别拖到联调阶段再处理第二联动逻辑一定要抽成独立模块不要散落在页面里否则后续加图层、加标绘、加卷帘功能时很容易互相干扰第三性能优化要前置像S3M缓存、LOD参数、实体数量控制这些在场景搭建时就要预估好数据规模不要等卡了再返工。再分享一个小技巧如果你需要在二维侧做空间查询比如框选一批楼栋然后把结果同步到三维显示可以在查询完成后把二维查询到的要素ID集合传给三维侧由三维侧按ID过滤生成临时实体而不是把二维的Geometry直接塞给三维渲染。这样做的好处是性能好、逻辑清晰也不容易出现坐标转换的精度损失。这个思路我在项目里反复验证过稳定可靠值得纳入你自己的项目工具箱。本文还有配套的精品资源点击获取
返回列表