去年接了一个三维城市展示项目甲方指定了 ArcGIS API for JS 4.18底图还必须是天地图。我当时觉得“这不就是加一个图层的事”结果把天地图 URL塞进WMTSLayer再放到SceneView里跑了一下三维地球上一片黑控制台报了一串跨域错误。排查了两三天才发现问题不只是跨域还有切片矩阵、坐标系、图层类型这些环节都要对齐。后来我换了实现思路把天地图作为自定义瓦片图层接入才稳定跑起来。这篇文章就把我踩过的坑和最终能用的方案整理出来重点说清楚 ArcGIS JS 4.18 在SceneView里加载天地图服务的两种可行做法一是直接使用WMTSLayer二是继承BaseTileLayer自定义瓦片图层。这两种方案都能在实际项目里落地第二种更稳也更好控制出错点。如果你也正准备把天地图放进三维地球这篇文章应该能帮你少走几次弯路。1. 为什么三维场景加载天地图不能按二维思路来很多朋友一听到“加载天地图”第一反应就是用二维地图的加载方式新建一个TileLayer或者WMTSLayer加进Map里就完事。这套思路在MapView里确实能跑但换到SceneView之后往往会遇到“看得见二维三维里却一片空白”的尴尬情况。1.1 SceneView和MapView对切片底图的要求不一样MapView本质是一个平面地图容器底图切片属于标准的 Web Mercator 瓦片或者某个自定义投影瓦片它只需要在当前视口范围内把瓦片画出来就行。但SceneView是一个三维地球底图要作为纹理贴在球面上这就涉及到三维场景的切片调度机制它要求图层必须有完整、正确的tileInfo信息包括坐标系、切片原点、LOD 分辨率。如果这个信息缺失或者和天地图实际切片规则对不上三维引擎就没法计算哪一级瓦片对应哪个屏幕分辨率结果就是瓦片一直请求不到场景黑屏或者白屏。另外三维场景默认的地面底图坐标系是 Web Mercator也就是 EPSG:102100 Web 墨卡托这也是天地图_w系列服务使用的切片坐标系。如果你加载的是天地图_c系列也就是 CGCS2000 地理坐标系EPSG:4490的切片那就要额外处理坐标系转换和切片原点偏移否则瓦片位置会整体漂移。实践下来在SceneView里尽量用_w系列减少很多麻烦。1.2 四套加载方案最后我留下了两套为了找到能用的方案我前后试过四种加载方式方案原理实际效果直接把天地图 URL 塞给TileLayer让 TileLayer 按常规 ArcGIS 切片规则请求失败天地图不是标准 ArcGIS 切片服务URL 模板不匹配用WMTSLayer加载标准 WMTS 服务JS API 内置支持 WMTS部分场景可用但 Capabilities 中的 tk 参数容易丢后期 401用WebTileLayer加载${level}/${row}/${col}模板浏览器瓦片模板失败WebTileLayer 默认只支持 Web Mercator 全球标准切片天地图 WMTS 参数格式不完全一样继承BaseTileLayer自己实现 getTileUrl自定义瓦片请求地址稳定最终采用最终我留下的两套方案就是WMTSLayer和自定义BaseTileLayer。前者适合快速验证后者适合正式项目落地。后面我会把这两套方案的完整代码和配置逻辑都写出来。2. 动手之前先把Key和服务参数搞清楚天地图不像 ArcGIS Online 底图那样可以直接匿名访问服务请求必须带上tk参数也就是从天地图官网申请的 Key。这个 Key 的申请流程很常规但里面有几个细节容易导致“明明填了 Key还是报非法 Key”的怪问题。2.1 天地图Key申请与容易翻车的细节去天地图官网注册开发者账号在控制台里创建应用就能拿到 Key。创建应用时普通浏览器网页应用选择“浏览器端”类型应用名称随便填但要记住 Key 绑定的是某个域名/IP 还是“无限制”。如果是本地调试建议先选无限制或带localhost否则部署到服务器后域名不匹配也会报错。我踩过的坑是同一个 Key 用在服务端和浏览器端天地图控制台里 Key 的类型如果选得太严格浏览器端请求会被拒绝。常见的报错是{code:301001,msg:非法key}出现这个错不用急着怀疑代码先检查三件事Key 是否激活刚申请的 Key 偶尔有延迟。请求的域名是否在允许列表里。服务类型是否对应影像底图用img矢量底图用vec注记用cia/cva混用会导致鉴权异常。2.2 服务URL和图层名别记混了天地图服务分底图和注记层常用的有这几个图层名称说明对应的通用服务vec矢量底图vec_wcva矢量中文注记cva_wimg影像底图img_wcia影像中文注记cia_wter地形晕渲底图ter_w例如加载影像底图时WMTS GetTile 地址是这样的https://t0.tianditu.gov.cn/img_w/wmts?SERVICEWMTSREQUESTGetTileVERSION1.0.0LAYERimgSTYLEdefaultTILEMATRIXSETwFORMATtilesTILEMATRIX3TILEROW2TILECOL1tk你的key注意这里的TILEMATRIXSETw表示 Web Mercator 切片矩阵。如果你用的是_c系列服务这个值要改成c对应 CGCS2000 经纬度投影。在SceneView里我强烈建议直接用w。2.3 理解_w、_c两种切片矩阵天地图同时提供_w和_c两种切片矩阵二者本质区别是坐标系这意味着同一个地理范围对应的瓦片行列号完全不同。_w系列Web MercatorEPSG:102100全球切片方案和 ArcGIS Online、Google Maps 类似从左上角开始瓦片大小 256x256。_c系列CGCS2000 经纬度EPSG:4490切片原点在经纬度 (-180, 90)行列号按经纬度等间隔划分。如果你不小心把_w服务的图层挂在_c矩阵下或者反过来瓦片显示位置就会乱出现“地图拼接错位”的现象。在三维场景中SceneView对 Web Mercator 的支持最完善所以直接选用_w系列能用最少的代码得到预期效果。这也是后面所有示例代码都默认使用_w的原因。3. 先试最简单方案WMTSLayer加载如果你已经熟悉 ArcGIS JS API 的WMTSLayer那可以先用它加载天地图。这个方案的优点是代码少API 内置了 WMTS Capabilities 解析能力理论上只需要给一个服务地址就能工作。3.1 最小可运行代码在SceneView里用WMTSLayer加载天地图影像底图的示例代码require([ esri/Map, esri/views/SceneView, esri/layers/WMTSLayer, esri/layers/BaseTileLayer, // 后文会用 ], function (Map, SceneView, WMTSLayer, BaseTileLayer) { const tiandituImg new WMTSLayer({ url: https://t0.tianditu.gov.cn/img_w/wmts?tk你的key, activeLayer: { id: img_w, title: 天地图影像底图, layer: img, style: default, format: tiles, tileMatrixSet: w }, version: 1.0.0 }); const map new Map({ basemap: { baseLayers: [tiandituImg] }, ground: world-elevation }); const view new SceneView({ container: viewDiv, map: map, viewingMode: global, camera: { position: [116.39, 39.9, 10000000], heading: 0, tilt: 0 } }); });代码的重点是activeLayer对象它告诉WMTSLayer从 Capabilities 文档里读取哪个图层。这里的layer: img是天地图服务里的图层名tileMatrixSet: w是切片矩阵。如果activeLayer不设置API 可能因为找不到默认图层而什么都不显示。3.2 为什么有时WMTSLayer会静默失败按上面的代码我在本地能成功显示但部署到客户服务器后瓦片加载不出来了。打开 Network 面板发现请求天地图时出现了大量 401错误信息是Invalid/Expired token。排查后发现原因很憋屈WMTSLayer第一次会请求GetCapabilities这时 URL 里的tk参数是有效的但 Capabilities 返回的资源 URL 模板里没有带上tk。后续WMTSLayer按这个模板请求瓦片自然就丢失了 Key。这就解释了为什么它会出现“有时候能用换环境就挂”的诡异现象。要解决这个问题要么在WMTSLayer的构造函数里加customParameters: { tk: 你的key }要么换用下面自定义BaseTileLayer方案让每个瓦片请求都老老实实带tk参数。3.3 注记图层怎么叠上去天地图的影像底图默认不带地名和道路文字实际项目中通常要叠加一个注记层。在WMTSLayer方案里再定义一个注记WMTSLayer然后和底图一起放进baseLayers数组即可const tiandituCia new WMTSLayer({ url: https://t0.tianditu.gov.cn/cia_w/wmts?tk你的key, activeLayer: { id: cia_w, title: 天地图影像中文注记, layer: cia, style: default, format: tiles, tileMatrixSet: w } }); const map new Map({ basemap: { baseLayers: [tiandituImg, tiandituCia] } });注记层之所以能盖在影像底图上是因为它的瓦片 PNG 图片背景透明所以只需要调整数组顺序后添加的图层默认绘制在上层。如果你发现注记没显示多半是注记图层的layer参数写错了影像注记是cia矢量注记是cva。4. 通用方案继承BaseTileLayer自定义WMTS图层如果你受不了WMTSLayer的 Key 丢失问题或者需要更精细地控制请求参数我建议用自定义BaseTileLayer。这套方案把请求 URL 完全握在自己手里天地图的每个瓦片地址都是自己拼出来的排查问题也很直观。它也是我在正式项目里最终采用的方案。4.1 写一个TiandituLayer类在 ArcGIS JS API 4.18 中BaseTileLayer允许你继承并重写getTileUrl方法这是自定义瓦片图层的核心。所谓自定义瓦片图层就是让 ArcGIS JS 的切片调度引擎帮我们计算当前视口需要哪些行列号的瓦片然后把这个行列号交给我们的getTileUrl去拼真实请求地址。示例代码如下define([ esri/layers/BaseTileLayer, esri/request ], function (BaseTileLayer) { return BaseTileLayer.createSubclass({ properties: { tk: null, layerName: null, tileMatrixSet: null }, getTileUrl: function (level, row, col) { const tk this.tk || ; const layerName this.layerName || img; const tileMatrixSet this.tileMatrixSet || w; return this.url /wmts ?SERVICEWMTS REQUESTGetTile VERSION1.0.0 LAYER layerName STYLEdefault TILEMATRIXSET tileMatrixSet FORMATtiles TILEMATRIX level TILEROW row TILECOL col tk tk; } }); });这里我用了BaseTileLayer.createSubclass的方式这种写法在 4.18 里很常见。getTileUrl的入参level、row、col分别对应 WMTS 里的TILEMATRIX、TILEROW、TILECOL天地图的行列号和 ArcGIS JS 里的行列号方向一致所以可以直接映射。如果你用的是 ES Module 写法可以这样写import BaseTileLayer from arcgis/core/layers/BaseTileLayer; const TiandituLayer BaseTileLayer.createSubclass({ // 同上面的属性 });但 4.18 时代很多项目还在用dojo/require方式所以我在示例里保留了兼容性更好的写法。实际开发时根据自己的工程化环境选一种即可。4.2 生成天地图的LOD数组自定义BaseTileLayer必须配置tileInfo否则切片调度器不知道每一级的分辨率。天地图 Web Mercator 的切片规则和 ArcGIS Online 底图一致每一级的分辨率可以按下式计算function buildTiandituTileInfo() { const resolutions []; const scales []; for (let level 0; level 18; level) { const resolution 156543.03392804097 / Math.pow(2, level); resolutions.push(resolution); scales.push(resolution * 96 / 0.0254); } return { rows: 256, cols: 256, compression: JPEG, origin: { x: -20037508.342787, y: 20037508.342787 }, spatialReference: { wkid: 102100 }, lods: resolutions.map(function (res, index) { return { level: index, resolution: res, scale: scales[index] }; }) }; }这里有几个关键点需要解释rows和cols代表瓦片尺寸天地图是 256x256。origin是切片原点Web Mercator 全球范围是[-20037508.342787, -20037508.342787]到[20037508.342787, 20037508.342787]原点在左上角。resolution是每个像素代表的地图单位长度第 0 级全球一张 256x256 的瓦片分辨率就是401 * 2 / 256也就是 156543.0339。scale只和显示有关这里按标准屏幕 DPI 换算就行不要求像素对齐但最好填上。把这个tileInfo交给自定义图层const tileInfo buildTiandituTileInfo(); const tiandituLayer new TiandituLayer({ url: https://t0.tianditu.gov.cn, layerName: img, tileMatrixSet: w, tk: 你的key, tileInfo: tileInfo });这里url和getTileUrl里的拼接规则是配套的所以不要直接给url带上/wmts否则最后会拼出/wmts/wmts。4.3 集成进SceneView后的效果加载影像底图后我再叠加一个注记层代码会变成这样const tiandituImg new TiandituLayer({ url: https://t0.tianditu.gov.cn, layerName: img, tileMatrixSet: w, tk: 你的key, tileInfo: tileInfo }); const tiandituCia new TiandituLayer({ url: https://t0.tianditu.gov.cn, layerName: cia, tileMatrixSet: w, tk: 你的key, tileInfo: tileInfo }); const map new Map({ basemap: { baseLayers: [tiandituImg, tiandituCia] }, ground: world-elevation }); const view new SceneView({ container: viewDiv, map: map, viewingMode: global });运行后三维场景的地球表面会先显示天地图影像接着叠加中文注记。因为注记瓦片背景透明底图地名和影像融合得比较自然。相比WMTSLayer方案这套方案的好处是每个瓦片请求都带着tk不会中途丢参数。可以随意加customParameters比如不同投影矩阵。排错简单直接在 Network 面板看 URL 就能定位问题。请求地址完全可控也便于后续扩展做偏转、纠偏或缓存。这套方案还有一个附加价值如果项目要接其他 WMTS 服务比如国家地理信息公共服务平台的其它底图只需要改一下图层名和 URL 拼接规则TiandituLayer就能复用。5. 常见问题排查实录把天地图接入SceneView看上去不难但真跑起来问题千奇百怪。我把实际项目里遇到的典型问题整理成了一个速查表方便你直接按症状排查。问题现象可能原因解决方案三维场景黑屏、白屏没有设置 tileInfo或坐标系不匹配给自定义 TileLayer 设置 Web Mercator 的 tileInfo瓦片加载 301001 非法 key服务类型、域名、Key 类型不匹配检查天地图控制台 Key 配置确认请求的 layer 和 tk 对应瓦片加载后错位、拼接不上混用了_c和_w切片矩阵统一使用_w并设置tileMatrixSet: w注记层不显示透明 PNG 被当成普通瓦片压缩检查layer参数是否是cia/cva确认瓦片格式不变报错unable to complete operation. unable to perform query operation.图层请求失败服务跨域或参数异常清掉浏览器缓存用 Network 面板检查瓦片请求确认服务可访问WMTSLayer 请求大量 401Capabilities 返回的 URL 模板丢失 tk改用自定义 BaseTileLayer或在 WMTSLayer 上增加 customParameters5.1 三维场景黑屏、黑屏黑屏最常发生在“代码没报错地图场是黑的”的时候。这种情况第一时间想到的不是代码而是底图瓦片无法被SceneView渲染。三维地球的全球底图通常需要 Web Mercator 切片。如果你的自定义图层没配tileInfoSceneView就没有办法把瓦片映射到地球表面自然什么都不显示。解决方案就是前面提到的buildTiandituTileInfo。这里再提醒一句tileInfo.spatialReference.wkid一定要设置成102100不要写成 3857。虽然两者本质上都是 Web Mercator但 ArcGIS JS 在某些版本里对 102100 的支持更直接避免产生不必要的坐标转换。5.2 报错“unable to perform query operation”这个报错英文原文经常是unable to complete operation. unable to perform query operation.我第一次看到这个报错时很慌以为是图层加载逻辑写错了。后来发现这个错误并不是天地图 WMTS 服务本身的问题而是 ArcGIS JS 里的Map对象默认会去访问 Basemap 的图例和服务信息或者你的代码里对某个图层执行了查询操作但该图层没有查询能力。排查思路是看控制台完整堆栈定位到具体是哪个图层触发了 query。如果你没有手动调用查询检查默认的basemap是不是被替换了或Map构造时有没有带没用的图层。天地图 WMTS 是纯瓦片服务本身不支持query操作如果代码里不小心对该图层调了queryFeatures就会报这个错。我在项目里最终把底图设置成Map的basemap.baseLayers不再额外添加能查询的图层这个报错就没再出现。5.3 图层偏移、拉伸、位置不对如果你用的天地图服务是_c系列在SceneView里经常会出现瓦片“斜着铺”“偏移半屏”的情况。这是因为三维场景默认按 Web Mercator 来组织地球表面切片你在一个 Web Mercator 场景里强行塞入 CGCS2000 经纬度切片坐标系不匹配瓦片行列号解析就会偏差大。解决意见很简单全部换成_w系列。你要加载影像底图就请求img_w注记层请求cia_w服务路径里的img_w、cia_w要和TILEMATRIXSETw配套。如果确实有业务必须用_c系列比如要和 CGCS2000 的本地数据叠加那就需要额外做投影转换和切片矩阵偏移不建议在SceneView里直接用。5.4 天地图返回“非法key”301001天地图的 301001 错误绝大多数不是代码问题而是请求参数和服务权限没对齐。我整理了一个自查列表照着排查基本能解决检查tk参数是否正确复制是否带了多余空格。检查天地图控制台里 Key 对应的域名是否包含当前访问域名本地调试时建议配置localhost。检查服务类型img_w服务对应LAYERimgcia_w对应LAYERcia不能混用。检查请求地址是http还是https天地图官方接口同时支持但有些环境下会强制 HTTPS不要混用。如果刚申请 Key 不久等待 5 到 10 分钟再试偶尔有缓存延迟。还有一点天地图的 Key 申请时应用类型选“浏览器端”和“服务端”会生成不同的校验规则。浏览器端的 Key 对 Referer 校验服务端的 Key 对 IP 校验。你在SceneView里直接从前端发请求应该选“浏览器端”否则很容易被浏览器跨域策略拦掉或者被服务器校验拒绝。6. 最后分享一点实战经验如果你问我最终在正式项目里会选哪种方案我会毫不犹豫选自定义BaseTileLayer。虽然多写了一个类但你能完全掌控请求地址排查问题时不用猜WMTSLayer内部是怎么解析 Capabilities 的。尤其是将来要做瓦片缓存、图层透明、或者接多个来源的 WMTS 服务这套自定义类的扩展性会好很多。另外有个贴近项目的小技巧三维场景加载天地图时记得把注记层放在底图图层之后。如果你的场景里还有矢量建筑、标牌等三维要素可以把它们的图层放在注记层之上这样地名注记不会被建筑模型压住但又能保证底图的信息量。这个层级关系在Map里通过add的顺序就能控制操作起来很简单。最后再提一句ArcGIS JS 4.18 虽然不算最新版本但天地图服务协议是稳定的这套方案在后续版本里同样适用。只要把 WMTS 请求参数理解透不管 API 版本怎么升你都能快速把天地图“贴”到三维地球上。如果能灵活根据项目情况调整tileInfo和请求 URL这套思路还可以迁移到其他 WMTS 服务上不只是局限于天地图。