osmdroid 地图实战 03|谷歌影像的 URL,为什么不能 setTileSource 了事?

摘要:看懂瓦片坐标与 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、是否需要签名。

所以对接一个新服务,你只需要问四个问题:

  1. 最小/最大级别是多少?(决定缩放范围约束)
  2. 瓦片尺寸是多少?(常见 256)
  3. 域名有几个?(决定是否要多域名轮询)
  4. URL 参数叫什么、要不要 token / 签名?(决定拼接规则)

把这四件事问清楚,"接入任何瓦片源"就从"抄代码碰运气"变成"填空"。

五、结论

  • 瓦片源的本质是**"(x, y, zoom) → 可下载 URL"的契约**,内置服务已实现,自定义服务要自己实现拼接。
  • 自定义瓦片源要重写 URL 拼接方法,把引擎算好的瓦片坐标,按服务端要求的参数名拼好。
  • 多域名轮询是规避在线瓦片限流的通用做法,把镜像域名都列进 base 地址即可。
  • 影像 / 矢量等样式差异往往只是一个 URL 参数,用图层名路由表切换最干净。
  • 对接任何新瓦片服务,只需确认级别范围 / 瓦片尺寸 / 域名数 / 参数与签名四件事。

完整可运行源码GitCode 仓库 · android_osmdroid_maplibre

你在实际项目中踩过哪些地图相关的坑?评论区聊聊。

相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen1 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone1 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui