摘要:看懂瓦片坐标与 URL 模板的关系,任何"花式瓦片服务"你都能 5 分钟接进来。
目录(TOC)
- 一、问题长什么样
- [二、根因:瓦片 URL 是一个"模板 + 坐标"的契约](#二、根因:瓦片 URL 是一个"模板 + 坐标"的契约 "#%E4%BA%8C%E6%A0%B9%E5%9B%A0%E7%93%A6%E7%89%87-url-%E6%98%AF%E4%B8%80%E4%B8%AA%E6%A8%A1%E6%9D%BF-%E5%9D%90%E6%A0%87%E7%9A%84%E5%A5%91%E7%BA%A6")
- [三、实现:接管 URL 拼接,并做好多域名](#三、实现:接管 URL 拼接,并做好多域名 "#%E4%B8%89%E5%AE%9E%E7%8E%B0%E6%8E%A5%E7%AE%A1-url-%E6%8B%BC%E6%8E%A5%E5%B9%B6%E5%81%9A%E5%A5%BD%E5%A4%9A%E5%9F%9F%E5%90%8D")
- [四、升华:一切瓦片服务都是"坐标到 URL 的映射"](#四、升华:一切瓦片服务都是"坐标到 URL 的映射" "#%E5%9B%9B%E5%8D%87%E5%8D%8E%E4%B8%80%E5%88%87%E7%93%A6%E7%89%87%E6%9C%8D%E5%8A%A1%E9%83%BD%E6%98%AF%E5%9D%90%E6%A0%87%E5%88%B0-url-%E7%9A%84%E6%98%A0%E5%B0%84")
- 五、结论
一、问题长什么样
需求来了:"把谷歌影像作为底图。"很多人用开源地图引擎,默认走内置的公共瓦片源,结果一搜代码,发现引擎并没有直接提供------于是上网抄一段"魔改版"的瓦片源,跑起来是能显示,但一旦放大缩到某些级别就花屏、或者被服务端限流。你反复检查,地址明明是对的,凭什么?
假设你也这样尝试过:
kotlin
// 自以为接入完成,但瓦片地址拼错了
val source = XYTileSource(
name = "GoogleImage",
minZoom = 0,
maxZoom = 22,
tileSizePixels = 256,
imageFilenameEnding = "",
baseUrl = "http://mts1.google.com/vt/lyrs=y&hl=x-local"
)
mapView.setTileSource(source)
一运行,第一屏是花的,或者干脆一片空白。
答案藏在引擎的一个细节里:XYTileSource 的基类会按自己的规则把瓦片坐标拼进 URL 尾部 ,而谷歌的服务要求的是 x/y/z 这种带名字的参数。引擎默认拼出来的东西,和谷歌要的不一样------所以你得接管 URL 拼接 。问题不在于"没有现成的",而在于绝大多数人没搞懂瓦片源的 URL 到底是怎么拼出来的。
二、根因:瓦片 URL 是一个"模板 + 坐标"的契约
地图引擎渲染底图时,会为屏幕上的每个瓦片计算一个 (x, y, zoom) 三元组------这就是瓦片坐标 :zoom 是缩放级别,x/y 是该级别下瓦片在网格里的行列号。引擎知道坐标,但它不知道你的服务商希望 URL 长什么样。
于是"瓦片源"这个抽象,本质上就是一件简单的事:
拿到
(x, y, zoom),拼出一个能拿到这张图的 URL。
内置的服务,引擎已经写好了拼接规则;自定义的服务,拼接规则由你接管。所以问题的根因不是"谷歌瓦片地址难找",而是"引擎没有为每一种服务内置拼接规则,你得自己实现这个'契约'"。
三、实现:接管 URL 拼接,并做好多域名
1. 重写 URL 拼接:把坐标按服务要求拼进去
关键在重写"取 URL"的方法,从瓦片索引里拆出 x/y/zoom,再按服务的参数名拼好:
kotlin
// 自定义瓦片源:把引擎算好的瓦片坐标,拼成服务端认识的 URL
class GoogleImageSource : XYTileSource(
name = "GoogleImage",
minZoom = 0,
maxZoom = 22,
tileSizePixels = 256,
imageFilenameEnding = "",
baseUrl = arrayOf("http://mts1.google.com/vt/lyrs=y&hl=x-local")
) {
override fun getTileURLString(tile: Long): String {
val x = MapTileIndex.getX(tile)
val y = MapTileIndex.getY(tile)
val zoom = MapTileIndex.getZoom(tile)
// 服务端要求 x/y/z 命名参数,与引擎默认拼法不同
return "$baseUrl&x=$x&y=$y&z=$zoom"
}
}
这里 baseUrl 和拼接是分离的:baseUrl 只描述"服务 + 样式",真正的坐标在拼接阶段动态补上。
2. 多域名轮询:规避服务端限流
在线瓦片服务通常提供多个镜像域名(如 mts1/2/3)。如果只写死一个域名,请求集中在单点,容易被限流。做法是把它们都塞进 base 地址列表:
kotlin
// 多域名:引擎会按某个策略轮询,避免单点被打爆
val source = XYTileSource(
name = "GoogleImage",
minZoom = 0,
maxZoom = 22,
tileSizePixels = 256,
imageFilenameEnding = "",
baseUrl = arrayOf(
"http://mts1.google.com/vt/lyrs=y&hl=x-local",
"http://mts2.google.com/vt/lyrs=y&hl=x-local",
"http://mts3.google.com/vt/lyrs=y&hl=x-local"
)
) {
override fun getTileURLString(tile: Long): String {
val x = MapTileIndex.getX(tile)
val y = MapTileIndex.getY(tile)
val zoom = MapTileIndex.getZoom(tile)
return "$baseUrl&x=$x&y=$y&z=$zoom"
}
}
注意:虽然拼接只用了"当前这一个域名"的 baseUrl,但把全部域名列进去,引擎在派发请求时会分散到这些域名上,这是在线瓦片接入的通用姿势。
3. 按图层名切换:影像 vs 矢量
同一个服务往往有"影像"和"矢量"两种样式,差异只是 URL 里的一个参数(lyrs=y 是影像、lyrs=m 是矢量)。可以定义两个瓦片源,用图层名做路由:
kotlin
// 图层名 -> 瓦片源 的路由表
fun tileSourceFor(layerName: String): TileSource = when (layerName) {
"影像底图" -> googleImageSource
"矢量底图" -> googleVectorSource
else -> defaultSource
}
// 切换底图:一行代码,但背后的瓦片调度器会整体换血
mapView.setTileSource(tileSourceFor(currentLayerName))
setTileSource 之所以重要,是因为它不止换了 URL 模板,还会触发瓦片调度器重新拉取当前视野的瓦片------所以"切换底图"天然自带刷新,你不用额外手动 invalidate。
四、升华:一切瓦片服务都是"坐标到 URL 的映射"
把谷歌这个例子抽象掉,你会得到一个通用结论:
任何在线瓦片服务,无论它是影像、矢量、还是某个私有协议,本质上都是给一个
(x, y, zoom),返回一个可下载图片的 URL。差别只在:参数叫什么、域名有几个、是否有 token、是否需要签名。
所以对接一个新服务,你只需要问四个问题:
- 最小/最大级别是多少?(决定缩放范围约束)
- 瓦片尺寸是多少?(常见 256)
- 域名有几个?(决定是否要多域名轮询)
- URL 参数叫什么、要不要 token / 签名?(决定拼接规则)
把这四件事问清楚,"接入任何瓦片源"就从"抄代码碰运气"变成"填空"。
五、结论
- 瓦片源的本质是**"
(x, y, zoom)→ 可下载 URL"的契约**,内置服务已实现,自定义服务要自己实现拼接。 - 自定义瓦片源要重写 URL 拼接方法,把引擎算好的瓦片坐标,按服务端要求的参数名拼好。
- 多域名轮询是规避在线瓦片限流的通用做法,把镜像域名都列进 base 地址即可。
- 影像 / 矢量等样式差异往往只是一个 URL 参数,用图层名路由表切换最干净。
- 对接任何新瓦片服务,只需确认级别范围 / 瓦片尺寸 / 域名数 / 参数与签名四件事。
完整可运行源码 :GitCode 仓库 · android_osmdroid_maplibre
你在实际项目中踩过哪些地图相关的坑?评论区聊聊。