ARTICLE DETAIL

资讯详情

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

ArcGIS JS SceneView三维场景加载天地图WMTS服务完整实践

ArcGIS JS SceneView三维场景加载天地图WMTS服务完整实践 如果你做过WebGIS开发大概率遇到过这样一个需求要在三维地球场景里展示国内数据底图却想用大家都熟悉的天地图。标题里那行字——“基于ArcGIS JS 4.18 API在三维场景SceneView下加载天地图服务”——就是我这次要分享的完整记录。先说结论ArcGIS JS API 4.18配合SceneView用WebTileLayer封装天地图WMTS服务在自定义tileInfo和坐标系匹配的前提下完全可以稳定跑起来。这篇文章会把Key申请、环境搭建、原理拆解、完整代码、报错排查一条龙讲清楚。不管你是刚接触ArcGIS JS的新人还是被三维底图折腾过的老手都可以直接对着参考。1. 开工前的三件事Key申请、API引入与版本选择1.1 天地图Key申请过程中的三个坑天地图的Key是访问瓦片服务的门票没Key或者Key配置错误页面永远只给你一片空白。申请入口是天地图官网的控制台注册开发者账号后创建新应用平台会生成一个类似字符串的tk参数。这里有几个坑需要注意。第一个坑应用类型别选错。天地图控制台创建应用时会要求选择应用类型常见的有浏览器端、服务端、移动端等。浏览器端类型一般会要求填写域名白名单也就是IP或Referer白名单。如果你申请的服务端Key却放在前端JS里请求服务端Key有独立调用配额但在浏览器里暴露存在安全隐患而且有些情况下会被跨域策略拦下来。实际项目我建议选浏览器端并把本地开发地址和线上域名都填进白名单开发时用localhost或者127.0.0.1就不会被拦截。第二个坑服务权限要勾选完整。创建应用时平台会列出矢量底图、影像底图、矢量注记、影像注记、地形晕渲等服务你当前应用只勾选了部分服务那么未勾选的服务即便Key正确也会返回“非法请求”或者不返回瓦片。我遇到过最典型的情况是代码里加载地形晕渲图但后台只勾选了矢量底图结果地形图层怎么都不出图。建议把常用的几项全部勾上反正按调用量计费权限开齐了做事方便。第三个坑Key写错位置。天地图WMTS请求的tk参数要放在URL最后例如tk你的key。如果拼接时多了空格、多了换行或者从控制台复制时把末尾不可见字符带进去请求会报code: 301001 / 非法key。这个错误在热搜里也经常出现大多数人不是Key真非法而是复制粘贴时出了问题。建议先在浏览器地址栏里手动访问一个瓦片URL确认能返回图片后再粘贴进代码。1.2 4.18的引入方式CDN还是本地化部署ArcGIS JS API 4.18的引入方式有两种官方CDN和本地化部署。官方CDN地址是https://js.arcgis.com/4.18/引入CSS和JS只需要两行代码对网络环境要求不高团队内部测试完全够用。但做正式项目时我更推荐本地化部署。原因很简单第一4.18的CDN官方一直保留但每次加载都需要从国外服务器拉取如果用户网络不稳定首次打开页面会非常慢第二本地部署可以把esri相关资源全部放到自己的静态服务器上与业务代码同源既方便做缓存策略也避免外部依赖出现不可控状况。ArcGIS官方实际上允许将API文件下载到本地使用只是需要保持目录结构完整。本地部署的具体做法是到ArcGIS官网下载ArcGIS Maps SDK for JavaScript 4.18的ZIP包解压后放到项目的arcgis_js_api目录下然后修改init.js和dojo.js等核心文件里的baseUrl把默认的https://js.arcgis.com/4.18/改成你自己的域名路径。这个步骤比较繁琐而且改错一个地方整个API都启动不了。如果你只是学习或者做内部工具直接CDN省事得多本文接下来的代码都基于CDN方式方便读者直接复制运行。1.3 为什么SceneView场景必须用_w后缀瓦片这是整个方案里最容易踩坑、也最容易被忽略的一步。天地图服务根据坐标系分为两套瓦片一套是CGCS2000地理坐标系瓦片地址里用_c后缀例如vec_c另一套是Web墨卡托投影坐标系瓦片地址里用_w后缀例如vec_w。ArcGIS JS API的MapView二维场景默认使用Web墨卡托但你也可以通过spatialReference指定为CGCS2000或其他坐标系来加载_c瓦片。而SceneView三维场景的情况完全不一样它的全球场景默认工作空间就是Web墨卡托3857即使是GlobalSceneView也基本围绕3857和4326展开。在三维场景里加载_c后缀的天地图瓦片会出现图层整体偏移、瓦片拼接不上、甚至完全加载不出来的问题。所以我处理SceneView加载天地图时一律使用_w后缀的瓦片地址。天地图的Web墨卡托切片方案与ArcGIS Online的全球切片方案完全一致原点位于左上角坐标为(-20037508.342787, 20037508.342787)瓦片大小256乘256初始级别为0级全球为一张瓦片。这个特性直接决定了后面tileInfo的配置方式。2. 加载原理拆解WebTileLayer与tileInfo到底在做什么2.1 WebTileLayer不是TileLayer别混用ArcGIS JS里有两个名字很相似的图层类型TileLayer和WebTileLayer。很多初学者会把它们当成同一个东西这是加载天地图失败的重灾区。TileLayer是ArcGIS原生的瓦片图层默认用来加载ArcGIS Server发布的缓存服务结构里带有lodInfos、tileInfo等完整的元数据描述格式相对固定。WebTileLayer则是专门用来加载OGC标准WMTS、XYZ等通用瓦片服务的图层它不要求服务端返回ArcGIS专用的元数据只要求你提供一个urlTemplate然后API按照{level}、{row}、{col}这些占位符去拼接瓦片地址。天地图虽然走的是WMTS协议但服务的元数据结构和ArcGIS原生TileLayer并不完全兼容。你当然可以通过TileLayer配合urlTemplate的方式构造一个图层但实操下来无论是参数拼接还是tileInfo匹配都比WebTileLayer麻烦。用WebTileLayer是社区里最稳定、最普遍的方案代码量也最少。WebTileLayer配合天地图WMTS地址时URL模板大概长这样const tdtUrl https://tile{s}.tianditu.gov.cn/vec_w/wmts?servicewmtsrequestGetTileversion1.0.0LAYERvectileMatrixSetwstyledefaultformattiletileMatrix{level}tileRow{row}tileCol{col}tk你的key;这里s是子域名序号level、row、col分别对应WMTS里的TileMatrix、TileRow、TileCol。天地图一共开放了tile0到tile7八个子域名加一层subDomains配置就能让浏览器并发请求不同域名加载速度会有肉眼可见的提升。2.2 自定义tileInfo的LOD计算推导WebTileLayer如果只给urlTemplate不指定tileInfoArcGIS会按默认的WGS84切片方案去理解每个level对应的比例尺而天地图Web墨卡托瓦片的level定义是完全另一套结果就是请求的瓦片行列号对不上。天地图会把请求返回成错误图片或者直接不返回页面上出现错位的瓦片。解决办法是手动构造天地图Web墨卡托对应的tileInfo核心是LOD数组。天地图的Web墨卡托切片方案与Google、ArcGIS Online一致0级是全世界一张256乘256的瓦片之后每放大一级行列数翻一倍。因此可以按照下面的公式生成function createTdtLods() { const lods []; const originX -20037508.342787; const originY 20037508.342787; const resolution0 156543.03392800014; const scale0 591657527.591555; for (let level 0; level 18; level) { lods.push({ level: level, resolution: resolution0 / Math.pow(2, level), scale: scale0 / Math.pow(2, level) }); } return lods; }其中resolution0是0级瓦片的分辨率单位为米/像素也就是全球地图周长40075016.685578米除以256像素再取整相关的结果。每一级的resolution等于上一级除以2因为每一级瓦片行列数翻倍每个像素代表的实际距离减半。scale0则是0级对应的比例尺分母。有了LOD数组再把tileInfo的origin设置为左上角(-20037508.342787, 20037508.342787)设置spatialReference为{wkid: 3857}一个完整匹配天地图Web墨卡托方案的tileInfo就组装完成了。这样ArcGIS请求瓦片时行列号计算就会跟天地图服务端的行列号严格对齐。2.3 天地图与ArcGIS切片方案的兼容性验证在把代码放到SceneView之前我习惯先在浏览器里手动验证一下天地图瓦片URL是否正确。验证方式很简单把{s}、{level}、{row}、{col}全部替换成固定数字比如请求https://tile0.tianditu.gov.cn/vec_w/wmts?servicewmtsrequestGetTileversion1.0.0LAYERvectileMatrixSetwstyledefaultformattiletileMatrix3tileRow1tileCol3tk你的key。如果返回一张正常的PNG图片说明Key、图层名、坐标集都正确。这样验证还有一个好处可以直观确认天地图的行列号规则是不是和ArcGIS LOD完全匹配。0级一张瓦片时tileMatrix0tileRow0tileCol01级时全球被切成4张瓦片tileMatrix1tileRow和tileCol范围是0到1。这套规则和ArcGIS Online的切片方案是同一套确认无误后再进代码能省掉大量调试时间。3. 完整实操从空白场景到天地图三维底图3.1 基础HTML骨架与模块引用现在正式写代码。首先搭建一个最简单的HTML页面引入ArcGIS JS 4.18的核心CSS与JS文件。因为是三维场景需要引入的样式和模块和二维略有区别但入口是一致的。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleSceneView加载天地图/title link relstylesheet hrefhttps://js.arcgis.com/4.18/esri/themes/dark/main.css script srchttps://js.arcgis.com/4.18//script style html, body, #viewDiv { margin: 0; padding: 0; width: 100%; height: 100%; } /style /head body div idviewDiv/div script require([ esri/Map, esri/views/SceneView, esri/layers/WebTileLayer, esri/Basemap ], function(Map, SceneView, WebTileLayer, Basemap) { // 核心逻辑写在这里 }); /script /body /html注意require是ArcGIS JS 4.x一直沿用的AMD模块加载方式所有API模块都必须在这个回调里使用。4.18版本还不是ESM模块写法不能直接用import这点跟5.x版本不同。如果你在4.18里尝试用import { Map } from arcgis/core/Map控制台会直接报错因为这是5.x的加载方式。3.2 加载矢量底图中文注记并设为场景底图三维场景里的“底图”本质上是一个Basemap对象而Basemap又由baseLayers数组组成。天地图矢量底图需要两个图层配合矢量图层vec_w提供道路、河流、行政边界等基础要素注记图层cva_w提供中文地名、道路名、POI文字标注。把这两个图层都放进baseLayers底图效果才完整。const tdtKey 你的天地图Key; function createTdtLayer(layerName, subDomains) { return new WebTileLayer({ urlTemplate: https://tile{s}.tianditu.gov.cn/ layerName _w/wmts?servicewmtsrequestGetTileversion1.0.0LAYER layerName tileMatrixSetwstyledefaultformattiletileMatrix{level}tileRow{row}tileCol{col}tk tdtKey, subDomains: subDomains || [0, 1, 2, 3, 4, 5, 6, 7], tileInfo: createTdtInfo() }); } function createTdtInfo() { const lods []; const resolution0 156543.03392800014; const scale0 591657527.591555; for (let level 0; level 18; level) { lods.push({ level: level, resolution: resolution0 / Math.pow(2, level), scale: scale0 / Math.pow(2, level) }); } return { rows: 256, cols: 256, origin: { x: -20037508.342787, y: 20037508.342787 }, spatialReference: { wkid: 3857 }, lods: lods }; } const vecLayer createTdtLayer(vec); const cvaLayer createTdtLayer(cva); const map new Map({ basemap: new Basemap({ baseLayers: [vecLayer, cvaLayer] }) }); const view new SceneView({ container: viewDiv, map: map, viewingMode: global, camera: { position: { x: 104.06, y: 30.55, z: 1000000 }, tilt: 0 } });这里有两个细节需要说明。第一createTdtLayer函数里的layerName参数直接决定了请求哪一套天地图图层vec是矢量底图cva是矢量注记切到img和cia就是影像底图加影像注记。第二camera的position我设置成了成都上空经度104.06、纬度30.55、高度1000000米这样打开页面就能直接看到三维地球并定位到成都。把Basemap设置到Map上以后SceneView的默认底图会被完全替换不再显示ArcGIS自带的全球底图。此时你看到的是一张完全以天地图为底的三维地球缩放、旋转、倾斜都正常底图和三维地球的叠加完全由ArcGIS内部处理。3.3 图层叠加、透明度与相机定位扩展底图就绪后最常见的需求是在上面叠加业务图层。比如在天地图底图上增加一个点代表某个城市的地标位置。SceneView里添加GraphicsLayer并添加Graphic即可。const graphicsLayer new GraphicsLayer(); map.add(graphicsLayer); const point { type: point, longitude: 104.06, latitude: 30.55 }; const symbol { type: point-3d, symbolLayers: [{ type: object, resource: { primitive: sphere }, material: { color: [255, 0, 0, 0.8] }, size: 50000 }] }; const graphic new Graphic({ geometry: point, symbol: symbol }); graphicsLayer.add(graphic);point-3d符号会渲染一个三维球体size: 50000表示球体的半径是5万米。这个尺寸放在城市尺度非常显眼用来做地标标注效果很好。相机视角还可以通过view.goTo做动画定位例如一句view.goTo({ position: { x: 104.06, y: 30.55, z: 200000 }, heading: 0, tilt: 45 })就能让视角从上空缓缓飞到500公里高度并以45度俯角观察场景。天地图各图层的透明度也可以通过opacity属性控制。比如想让矢量底图淡出、影像底图叠上来可以设置vecLayer.opacity 0.5影像底图imgLayer.opacity 1。这个操作在做多底图对比、业务数据叠加展示时非常实用。切换天地图影像底图和地形晕渲也不需要重新创建Basemap把basemap.baseLayers里的图层替换掉就行const imgLayer createTdtLayer(img); const ciaLayer createTdtLayer(cia); map.basemap.baseLayers.removeAll(); map.basemap.baseLayers.addMany([imgLayer, ciaLayer]);这段代码执行后底图会立刻从矢量切换到影像注记也跟着切成影像注记整个过程SceneView不会重新加载流畅度很高。4. 报错排查与性能优化经验总结4.1 高频报错速查表与处理方式在反复调试天地图加载的过程中我整理了一份高频报错速查表基本都是真实项目里撞见过的照着排查比瞎猜效率高得多。报错现象可能原因处理方法页面上没有任何瓦片控制台401天地图Key未配置白名单或Key非法检查控制台应用类型和域名白名单用浏览器直接访问瓦片URL验证Keycode: 301001 / 非法keyKey复制粘贴错误、tk参数拼错、服务权限未勾选重新复制Key确认URL末尾tk参数格式检查控制台已勾选服务瓦片全部加载出来但位置错乱用了_c后缀瓦片或tileInfo设置错误改用_w后缀瓦片检查tileInfo的origin、spatialReference、lods与天地图一致控制台输出unable to complete operation. unable to perform query operation.服务请求被跨域拦截或者Key配额耗尽确认天地图Key开启了对应域名白名单若配额耗尽去控制台查看调用量部分瓦片灰色或空白某些层级请求失败多为层级超过18级将tileInfo的lods最大级别限制在18天地图大部分免费瓦片服务最高到18级图层加载慢缩放卡顿默认并发域名少瓦片请求排队配置subDomains为0到7利用多个子域名并发下载重点说一下unable to complete operation. unable to perform query operation.这个报错。它的字面意思是ArcGIS内部某个查询操作失败了但实际原因往往是网络请求被浏览器拦截或者是服务端返回了非预期内容。很多人看到这个报错会去查ArcGIS代码逻辑其实应该先打开网络面板看天地图瓦片请求的状态码是不是429或者403。如果是429说明短时间内请求量过大触发了限流把SceneView的相机层级放大速度调慢或者增加瓦片缓存即可缓解。4.2 瓦片错位、灰白块与模糊三维场景特有的坑三维场景加载瓦片底图比二维更容易出现视觉问题因为SceneView的相机是可以倾斜和旋转的。最典型的坑有三个。第一个是瓦片错位。相机倾斜后远处的地平线附近会请求大量低层级瓦片如果tileInfo的lods和天地图服务端不匹配这些瓦片就会以错误的位置贴到地球表面看起来像地面裂开了。排查办法回到二维MapView里先测试同一个tileInfo二维正常了再切到三维能快速区分是图层问题还是视角问题。第二个是灰白块。天地图服务免费版有一定QPS限制快速缩放时瓦片请求并发猛增部分瓦片请求失败后ArcGIS不会自动重试屏幕上就会留下灰白色方块。处理办法有两个手动调用layer.refresh()强制刷新或者在view的watcher里监听stationary状态当地图停止运动后遍历图层执行一次刷新。第三个是近地面模糊。SceneView拉到很近时天地图最高18级瓦片已经用尽继续放大就会越来越模糊。这是瓦片服务本身的限制不是代码Bug。如果业务必须看到更精细的底图有两个思路接入天地图更高层级服务或者叠加本地高精度瓦片数据。多数WebGIS项目到16到17级已经够用18级属于兜底。4.3 性能优化心得从4.18到新版本的兼容性性能优化是另一个绕不开的话题。SceneView本质上是个WebGL应用瓦片底图会以纹理形式上传GPU瓦片数量过多会直接导致显存暴涨。我在实际项目中总结了几条有效的优化手段。第一限制最大层级。天地图免费服务最高到18级把tileInfo.lods精确到18级就够了不要写一个超长的lods数组否则SceneView在城市级别会自动去请求不存在的19级、20级瓦片白白增加HTTP请求。把maxScale绑定到对应级别相机拉近时ArcGIS不会再请求超出范围的瓦片。第二合理设置view.constraints。在SceneView上设置constraints里的altitude最大最小值避免相机飞到离地面太近的位置从根源上防止超高精度瓦片的海量请求。比如将最小高度限制在1000米左右既能看清城市轮廓又不会触发过高负载。第三使用view.updating状态提示。在加载大量图层时给页面加一个loading遮罩监听view.updating属性更新结束后再隐藏遮罩。这样用户不会误以为页面卡死。第四如果项目现在还停留在4.18建议在时间允许时评估升级。4.18是2020年的版本后续4.x版本在WebTileLayer的缓存策略、GPU内存管理、SceneView渲染性能上都有不少改进。不过升级不是必须的如果你的代码稳定运行、功能已满足需求4.18继续用也没有问题本文的原理和代码在新版本里同样适用只需要把CDN版本号改成新版本或者换成ESM模块引入方式。我个人在实际项目里最深刻的体会是加载天地图这类第三方瓦片服务真正的难点从来不在ArcGIS API本身而在于坐标系、切片方案、服务权限这三件事的匹配。把这三件事理顺了代码反而是最简单的那一步。最后再分享一个小技巧把本文的createTdtLayer和createTdtInfo封装成一个独立的工具文件放到项目公共模块里以后从二维切到三维、从矢量切到影像只需要一行调用省得每次重写。这个工具函数我已经在多个项目里复用稳定性非常高你也可以直接拿去改造。
返回列表