☰
ArcGIS for Android加载天地图瓦片:坐标系、URL模板与实战避坑指南
2026/10/15 4:16:48 网站建设 项目流程

1. 从底图焦虑说起:为什么加载天地图是移动端GIS的必修课

做ArcGIS for Android开发的朋友,大概率都经历过这样一个阶段:Demo跑通了,地图控件也显示出来了,但底图要么是灰蒙蒙一片,要么是ArcGIS默认的那套英文底图,放在国内项目里怎么看怎么别扭。更麻烦的是,很多甲方或者项目需求里明确要求使用天地图作为底图,原因也很直接——天地图是国内官方提供的权威地理信息公共服务,覆盖全、更新及时、中文标注友好,而且对国内开发者来说,它的服务稳定性和访问体验都相当不错。

但问题来了:ArcGIS for Android的官方文档里,底图加载的示例基本都是围绕ArcGIS自己的服务展开的,比如ArcGISTiledMapServiceLayer加载ArcGIS Server发布的切片服务,或者用WebTiledLayer加载一些标准的XYZ瓦片服务。天地图虽然也是标准的瓦片服务,但它的URL结构、坐标系、切片规则和ArcGIS默认的那套并不完全一样。直接套用官方示例,大概率会遇到瓦片错位、加载不出来、或者地图一片空白的情况。

这篇文章就是来解决这个问题的。我会从天地图的服务结构讲起,把坐标系、瓦片行列号、URL模板这些关键点拆开揉碎,然后一步步带你在ArcGIS for Android里把天地图加载出来。不管你是刚接触ArcGIS for Android的新手,还是已经做过几个项目但一直没搞明白瓦片加载原理的老手,这篇内容都能让你少走不少弯路。关键词就三个:ArcGIS for Android、天地图、瓦片加载。整篇内容会围绕这三个词展开,把背后的原理和实操细节都讲透。

2. 天地图瓦片服务的底层逻辑:坐标系与URL模板拆解

2.1 天地图到底提供了哪些瓦片服务

天地图的服务体系其实挺庞大的,但对移动端开发来说,最常用的就是几种基础底图:矢量底图、影像底图、地形底图,以及对应的中文标注图层。每种底图都提供了Web墨卡托投影(EPSG:3857)和经纬度投影(EPSG:4326)两种版本。这一点很关键,因为ArcGIS for Android里不同的图层类型对坐标系的支持是不一样的。

先看几个典型的服务地址结构。天地图的瓦片服务URL通常长这样:

http://t{0-7}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=你的密钥

这里有几个关键参数需要理解:

  • t{0-7}:这是天地图的服务器负载均衡机制,有8个子域名(t0到t7),轮询使用可以分散请求压力。
  • vec_w:vec表示矢量底图,_w表示Web墨卡托投影。如果是_c,那就是经纬度投影(EPSG:4326)。
  • LAYER=vec:图层名称,和URL路径里的vec对应。
  • TILEMATRIXSET=w:瓦片矩阵集,w代表Web墨卡托,c代表经纬度。
  • TILEMATRIX={z}:缩放级别。
  • TILEROW={y}:瓦片行号。
  • TILECOL={x}:瓦片列号。
  • tk=你的密钥:这是天地图的访问密钥,需要去天地图官网申请。

影像底图就是把vec换成img,地形底图换成ter,中文标注图层则是cva(矢量标注)、cia(影像标注)、cta(地形标注)。这些细节在后面的代码实现里会直接用到。

2.2 Web墨卡托与经纬度投影:选错了地图就歪了

坐标系这个问题,是很多新手最容易踩坑的地方。ArcGIS for Android里,ArcGISTiledMapServiceLayer默认期望的是ArcGIS自己的切片方案,而WebTiledLayer则更灵活,可以接受自定义的URL模板。但不管用哪种方式,坐标系必须和瓦片服务的坐标系匹配。

天地图的_w系列服务用的是Web墨卡托投影(EPSG:3857),这也是目前大多数在线地图服务采用的投影方式。它的好处是:全球范围的地图可以近似成一个正方形,瓦片切割规则简单,缩放级别从0到18级(天地图目前支持到18级),每一级的瓦片数量是4的n次方。具体来说,第0级是1张瓦片,第1级是2x2=4张,第2级是4x4=16张,以此类推。

而_c系列服务用的是经纬度投影(EPSG:4326),瓦片切割规则略有不同。在ArcGIS for Android里,如果你用WebTiledLayer加载_c系列的服务,需要确保地图控件的空间参考也设置为WGS84经纬度坐标系,否则会出现瓦片错位。

我个人的建议是:优先使用_w系列服务。原因有两个:一是Web墨卡托的瓦片规则更通用,和大多数在线地图服务一致,后续如果要切换其他底图,迁移成本更低;二是ArcGIS for Android的WebTiledLayer对Web墨卡托的支持更成熟,配置起来更省心。

2.3 瓦片行列号的计算规则:为什么你的地图总是错位

瓦片行列号的计算,是加载天地图时最容易出问题的地方。很多人直接把URL模板里的{x}、{y}、{z}替换成ArcGIS提供的行列号,结果发现地图要么偏移,要么瓦片顺序全乱。这里面的核心问题在于:不同地图服务的瓦片原点位置不一样。

ArcGIS的切片服务,瓦片原点通常在左上角(西北角),行号向下递增,列号向右递增。而天地图的瓦片服务,原点也是在左上角,但它的行列号计算方式和ArcGIS的WebTiledLayer默认行为可能存在差异。具体来说,WebTiledLayer在构造时,需要你提供一个URL模板,里面用{level}、{row}、{col}这样的占位符。ArcGIS会根据当前地图的范围和缩放级别,自动计算出对应的行列号,然后替换到URL里。

但这里有个细节:天地图的URL模板里用的是{z}、{y}、{x},而ArcGIS的WebTiledLayer用的是{level}、{row}、{col}。你需要做的是把天地图的参数名映射到ArcGIS的占位符上。比如:

http://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={level}&TILEROW={row}&TILECOL={col}&tk=你的密钥

这样ArcGIS在请求瓦片时,就会把{level}替换成当前缩放级别,{row}替换成行号,{col}替换成列号。但前提是,你的地图控件的空间参考和瓦片服务的空间参考必须一致。如果地图控件用的是Web墨卡托,而瓦片服务用的是经纬度,那行列号的计算就会完全错乱。

还有一个容易忽略的点:瓦片级别偏移。有些地图服务的缩放级别是从1开始计数的,而ArcGIS的WebTiledLayer默认从0开始。天地图的级别是从1到18,但ArcGIS的{level}占位符在0级时也会请求瓦片。如果你发现地图在最小级别时显示异常,可以尝试在URL模板里对级别做偏移处理,比如用{level}直接对应,或者在代码里做判断。

3. 在ArcGIS for Android中落地天地图加载的完整实操

3.1 环境准备与依赖配置

在开始写代码之前,先确认你的开发环境。ArcGIS for Android的SDK目前主要通过Maven仓库分发,你需要在项目的build.gradle文件里添加依赖。以ArcGIS Runtime SDK for Android 100.x版本为例:

dependencies { implementation 'com.esri.arcgisruntime:arcgis-android:100.15.0' }

如果你用的是更早的10.2.x版本,依赖方式会有所不同,通常需要手动下载SDK并导入arcgis-android-sdk库。不过现在新项目基本都推荐用100.x版本,API更现代,对WebTiledLayer的支持也更完善。

权限方面,需要在AndroidManifest.xml里添加网络访问权限:

<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

如果你还需要显示用户位置,再加定位权限。但加载天地图本身只需要网络权限就够了。

还有一个关键点:天地图的密钥申请。你需要去天地图官网注册账号,创建一个应用,拿到一个tk密钥。这个密钥是必须的,没有它,瓦片请求会返回403错误。密钥的申请流程不复杂,但要注意:每个密钥有每日请求配额限制,个人开发者一般够用,但如果项目访问量很大,可能需要申请企业密钥或者做缓存优化。

3.2 用WebTiledLayer构建天地图底图

WebTiledLayer是ArcGIS for Android里加载自定义瓦片服务的核心类。它的构造函数接受一个URL模板和一个可选的子域名列表。下面是一个加载天地图矢量底图的完整示例:

// 天地图矢量底图URL模板 String vecUrl = "http://t{subdomain}.tianditu.gov.cn/vec_w/wmts?" + "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0" + "&LAYER=vec&STYLE=default&TILEMATRIXSET=w" + "&FORMAT=tiles&TILEMATRIX={level}" + "&TILEROW={row}&TILECOL={col}&tk=你的密钥"; // 子域名列表,用于负载均衡 List<String> subDomains = Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7"); // 创建WebTiledLayer WebTiledLayer vecLayer = new WebTiledLayer(vecUrl, subDomains); // 创建地图并设置底图 ArcGISMap map = new ArcGISMap(); map.setBasemap(new Basemap(vecLayer)); // 将地图设置到MapView mapView.setMap(map);

这段代码看起来简单,但里面有几个细节值得展开说。

首先是{subdomain}占位符。WebTiledLayer的构造函数第二个参数是一个子域名列表,ArcGIS会自动轮询这些子域名,替换URL模板里的{subdomain}。天地图提供了t0到t7共8个子域名,全部用上可以分散请求压力,提升加载速度。

其次是URL模板里的参数顺序。天地图的WMTS服务对参数顺序其实不敏感,但为了可读性,建议按照SERVICE、REQUEST、VERSION、LAYER、STYLE、TILEMATRIXSET、FORMAT、TILEMATRIX、TILEROW、TILECOL、tk的顺序排列。这样后续排查问题时一眼就能看出哪个参数出了问题。

还有一个容易忽略的点:URL编码。天地图的URL里包含&符号,在Java字符串里不需要额外转义,但如果你把URL放在XML资源文件里,就需要用&amp;代替&。这个坑我在早期项目里踩过,当时把URL写在strings.xml里,结果请求一直失败,排查了半天才发现是XML转义的问题。

3.3 叠加中文标注图层:让地图看得懂

光有底图还不够,天地图的矢量底图本身是不带中文标注的,你需要额外加载标注图层。标注图层的URL和底图类似,只是把vec换成cva:

String cvaUrl = "http://t{subdomain}.tianditu.gov.cn/cva_w/wmts?" + "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0" + "&LAYER=cva&STYLE=default&TILEMATRIXSET=w" + "&FORMAT=tiles&TILEMATRIX={level}" + "&TILEROW={row}&TILECOL={col}&tk=你的密钥"; WebTiledLayer cvaLayer = new WebTiledLayer(cvaUrl, subDomains);

然后把这个标注图层叠加到地图上。在ArcGIS for Android 100.x里,可以通过ArcGISMapImageLayer或者FeatureLayer来叠加,但更简单的方式是使用GroupLayer或者直接把标注图层作为Basemap的一部分。不过Basemap的构造函数通常只接受一个Layer,所以更常见的做法是:

// 创建底图 Basemap basemap = new Basemap(vecLayer); // 创建地图 ArcGISMap map = new ArcGISMap(basemap); // 将标注图层添加到地图的操作图层列表 map.getOperationalLayers().add(cvaLayer);

这样标注图层就会叠加在底图之上。但要注意:标注图层的透明度和绘制顺序可能会影响显示效果。如果标注被底图遮挡,可以调整图层的Opacity属性,或者确保标注图层在操作图层列表中的顺序靠后。

另外,影像底图对应的标注图层是cia,地形底图对应的是cta。如果你用的是影像底图,记得把标注图层也换成对应的cia,否则标注和底图会对不上。

3.4 地图控件的初始化与交互配置

地图加载出来之后,还需要对MapView做一些基本配置,让交互体验更顺畅。比如设置初始视野范围、缩放级别限制、旋转手势开关等:

// 设置初始视野范围(以北京为例) mapView.setViewpoint(new Viewpoint(39.9042, 116.4074, 10000)); // 设置缩放级别限制 mapView.setMinScale(1000000); mapView.setMaxScale(1000); // 禁用旋转手势(如果项目不需要) mapView.setRotationEnabled(false); // 启用缩放动画 mapView.setZoomAnimationEnabled(true);

这里有个经验之谈:初始视野范围不要设得太小。有些开发者喜欢把初始级别设到15级以上,结果地图加载时先显示一片空白,等瓦片慢慢加载出来才看到内容。更好的做法是把初始级别设在10到12级左右,让用户先看到大范围的地图,再逐步放大。这样瓦片加载的压力也小一些,体验更流畅。

还有一个细节:瓦片缓存。ArcGIS for Android的WebTiledLayer默认会使用内存缓存,但内存缓存的大小有限。如果你的应用需要频繁加载同一区域的地图,可以考虑启用磁盘缓存。不过磁盘缓存的配置相对复杂,需要自定义TileCache,这里先不展开,后续可以单独写一篇来讲。

4. 那些年我踩过的天地图加载坑:排查思路与解决方案

4.1 瓦片一片空白:从密钥到URL的逐项排查

地图加载不出来,是最常见的问题。表现通常是:MapView显示灰色网格,或者干脆一片空白,Logcat里可能有403、404或者超时错误。遇到这种情况,我一般会按照下面的顺序逐项排查:

第一步,检查密钥是否有效。天地图的密钥有有效期和配额限制。如果密钥过期或者配额用完,瓦片请求会返回403。你可以把URL复制到浏览器里直接访问,看看返回的是什么。如果返回的是错误信息,那基本就是密钥的问题。

第二步,检查URL模板的占位符是否正确。WebTiledLayer要求URL模板里必须包含{level}、{row}、{col}这三个占位符,缺一不可。如果你不小心写成了{z}、{y}、{x},ArcGIS无法识别,请求就不会带上正确的行列号,结果就是瓦片加载失败。

第三步,检查子域名列表是否为空。如果URL模板里用了{subdomain},但构造函数里没有传入子域名列表,ArcGIS会把{subdomain}替换成空字符串,导致URL变成http://t.tianditu.gov.cn/...,这个域名是不存在的。所以子域名列表必须传,而且不能是空列表。

第四步,检查网络权限和网络状态。这个看起来是基础问题,但确实有人忘记加INTERNET权限,或者测试设备没有联网。可以在代码里加一个简单的网络状态检查,确认设备当前有网络连接。

第五步,检查坐标系是否匹配。如果你用的是_w系列服务,但地图控件的空间参考被设置成了WGS84经纬度,瓦片行列号的计算就会错乱。确保MapView的空间参考和瓦片服务的投影一致。

4.2 瓦片错位与偏移:行列号计算的隐藏陷阱

瓦片错位是另一个让人头疼的问题。表现是:地图能加载出来,但瓦片之间的拼接有缝隙,或者整体偏移了一个固定的距离。这种情况通常和行列号的计算规则有关。

前面提到过,天地图的瓦片原点在左上角,行号向下递增,列号向右递增。但ArcGIS的WebTiledLayer在计算行列号时,可能会受到地图控件当前视野范围的影响。如果地图控件的初始视野范围设置得不合理,或者缩放级别和瓦片级别没有对齐,就会出现错位。

我遇到过一次比较诡异的情况:地图在低缩放级别时正常,但放大到某个级别后,瓦片突然偏移了半个瓦片的位置。排查后发现,是因为天地图的瓦片级别和ArcGIS的缩放级别之间存在一个微小的偏移。天地图的级别是从1开始计数的,而ArcGIS的{level}占位符在0级时也会请求瓦片。虽然天地图在0级时也会返回瓦片(实际上是1级的瓦片),但行列号的计算可能会有偏差。

解决方法是:在URL模板里对级别做偏移处理。比如,如果你发现地图在某个级别开始错位,可以尝试把{level}替换成{level}加1或者减1,看看效果。不过更稳妥的做法是,确保地图控件的初始缩放级别和天地图的瓦片级别对齐。具体来说,可以在MapView初始化后,手动设置一个和天地图级别对应的缩放级别,而不是依赖ArcGIS的默认缩放级别。

4.3 加载速度慢与请求超时:子域名轮询与并发优化

天地图的瓦片服务在国内访问速度总体不错,但如果你的应用用户量较大,或者网络环境不太好,可能会遇到加载速度慢、请求超时的问题。这时候可以从几个方面优化:

子域名轮询是最基本的优化手段。天地图提供了t0到t7共8个子域名,全部用上可以显著提升并发加载能力。WebTiledLayer的构造函数支持传入子域名列表,ArcGIS会自动轮询这些子域名。但要注意:子域名列表的顺序和数量会影响轮询效果,建议把8个子域名全部传入,不要只传一两个。

瓦片预加载是另一个思路。ArcGIS for Android的MapView支持设置TileCache,可以在内存中缓存已加载的瓦片。如果用户经常查看同一区域的地图,预加载可以大幅减少重复请求。不过内存缓存的大小需要根据设备性能来调整,设得太大可能导致内存溢出。

请求超时设置也很关键。默认情况下,ArcGIS的瓦片请求超时时间可能比较短,在网络不稳定的情况下容易失败。可以通过自定义TileCache或者RequestConfiguration来调整超时时间。不过这个配置在100.x版本里需要通过ArcGISRuntimeEnvironment来设置,具体代码这里不展开,有需要的可以查阅官方文档。

还有一个经验:避免在低缩放级别加载过多瓦片。有些开发者喜欢把初始缩放级别设得很小(比如3级或4级),结果一次请求要加载几十张瓦片,网络压力很大。更好的做法是把初始级别设在10级左右,让用户先看到局部区域,再根据需要缩小范围。

5. 从能用到好用:天地图加载的进阶优化思路

5.1 多底图切换:矢量、影像、地形的动态切换

在实际项目里,用户往往需要在地图类型之间切换,比如从矢量底图切到影像底图,或者切到地形底图。实现这个功能的核心思路是:预先创建好各个底图的WebTiledLayer,然后在切换时替换Basemap。

// 预创建三种底图图层 WebTiledLayer vecLayer = createTiledLayer("vec"); WebTiledLayer imgLayer = createTiledLayer("img"); WebTiledLayer terLayer = createTiledLayer("ter"); // 切换底图的方法 public void switchBasemap(String type) { Basemap newBasemap; switch (type) { case "vec": newBasemap = new Basemap(vecLayer); break; case "img": newBasemap = new Basemap(imgLayer); break; case "ter": newBasemap = new Basemap(terLayer); break; default: newBasemap = new Basemap(vecLayer); } map.setBasemap(newBasemap); }

这里有个细节:切换底图时,标注图层也需要同步切换。矢量底图对应cva,影像底图对应cia,地形底图对应cta。如果只切换底图不切换标注,标注和底图会对不上,用户体验很差。

另外,频繁切换底图可能会导致内存占用上升,因为每个WebTiledLayer都会占用一定的内存缓存。如果设备内存紧张,可以在切换后手动清理不再使用的图层缓存。不过ArcGIS for Android的图层缓存管理机制比较自动化,一般情况下不需要手动干预。

5.2 离线缓存与性能平衡:移动端地图的取舍

移动端地图应用绕不开离线缓存这个话题。天地图的瓦片服务虽然稳定,但在弱网环境或者无网环境下,地图就加载不出来了。如果项目有离线需求,可以考虑实现瓦片的本地缓存。

实现离线缓存的基本思路是:拦截WebTiledLayer的瓦片请求,先检查本地是否有缓存,如果有就直接读取本地文件,如果没有再发起网络请求,并在请求成功后把瓦片保存到本地。ArcGIS for Android提供了TileCache接口,可以自定义缓存逻辑。不过这个实现比较复杂,需要处理缓存过期、缓存大小限制、并发读写等问题。

对于大多数项目来说,更实用的做法是:利用ArcGIS的默认内存缓存,配合合理的缩放级别限制。比如,把最大缩放级别限制在16级,避免加载过多高精度瓦片;把初始缩放级别设在10到12级,减少首次加载的瓦片数量。这样可以在性能和体验之间找到一个平衡点。

还有一个技巧:预加载关键区域的瓦片。如果项目的地图范围是固定的(比如某个园区或城市),可以在应用启动时预先加载该区域的瓦片,这样用户打开地图时就能快速显示。ArcGIS for Android的MapView提供了setViewpointAsync方法,可以在后台异步加载指定范围的瓦片。

5.3 坐标系转换与叠加分析:天地图与其他图层的协同

在实际项目里,天地图往往不是单独使用的,而是作为底图,上面还要叠加业务图层,比如点位、路线、区域边界等。这时候坐标系转换就成了一个关键问题。

天地图的_w系列服务用的是Web墨卡托投影(EPSG:3857),而很多业务数据可能是WGS84经纬度(EPSG:4326)或者CGCS2000坐标系。如果直接把WGS84的数据叠加到Web墨卡托的底图上,位置会有偏移。ArcGIS for Android提供了GeometryEngine类,可以方便地进行坐标系转换:

// 将WGS84坐标转换为Web墨卡托坐标 Point wgs84Point = new Point(116.4074, 39.9042, SpatialReferences.getWgs84()); Point webMercatorPoint = (Point) GeometryEngine.project( wgs84Point, SpatialReferences.getWebMercator() );

反过来,如果需要把Web墨卡托的坐标转回WGS84,也是用同样的方法,只是目标空间参考换成SpatialReferences.getWgs84()。

这里有个经验:尽量在数据入库时就统一坐标系。如果业务数据本身就是Web墨卡托的,那叠加到天地图上就不需要转换,性能更好。如果数据是WGS84的,可以在数据加载时批量转换,而不是每次绘制时都转换。批量转换可以用GeometryEngine.project的批量版本,效率更高。

还有一个容易忽略的点:天地图的标注图层和底图图层的坐标系必须一致。如果你混用了_w和_c系列的服务,标注和底图就会错位。所以,要么全部用_w,要么全部用_c,不要混用。

6. 一些实战中的小体会

天地图加载这件事,说起来简单,但真正做起来,细节还是挺多的。我从最早用ArcGIS for Android 10.2的时候就开始折腾天地图,中间踩过的坑包括但不限于:密钥过期导致地图一片空白、URL模板占位符写错导致瓦片加载失败、坐标系不匹配导致标注偏移、子域名列表为空导致请求域名错误。每一个坑都花了不少时间排查,但也正是这些经历,让我对瓦片加载的底层机制有了更深的理解。

现在回头看,其实核心就三点:URL模板要写对、坐标系要匹配、密钥要有效。把这三点搞定,天地图加载基本就不会有大问题。剩下的优化,比如子域名轮询、瓦片缓存、多底图切换,都是在能用的基础上做好用的提升。

如果你正在做ArcGIS for Android的项目,需要加载天地图,希望这篇内容能帮你少走一些弯路。如果你已经做完了,但遇到了其他问题,也欢迎一起交流。地图开发这件事,坑是踩不完的,但每踩一个坑,对底层原理的理解就深一层,这大概也是做GIS开发的乐趣所在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询