GmsCore 地图样式资产体系:microG 定制 MapLibre/Mapbox 样式的源码、生成流程与实战配置
GmsCore 地图样式资产体系:microG 定制 MapLibre/Mapbox 样式的源码、生成流程与实战配置
导读
本文围绕 GmsCore 仓库中 artwork/styles/README.md 所描述的「microG 定制地图样式」资产体系展开,系统梳理 style-microg-normal-*、style-microg-satellite-*、style-stadia-outdoors.json 等样式文件的定位差异,并结合 play-services-maps 模块源码说明这些样式在应用中的真实加载与 API Key 注入机制,最后给出 sprite 与 PBF 字体资产的完整生成命令、编辑工具链及许可证约束。读完本文,你将掌握 microG 地图样式的文件家族划分、资产从源文件到 APK assets 的构建流水线,以及如何在 Maputnik 等工具中调试这些样式。
一、样式文件家族:七个 JSON 的定位与差异
artwork/styles/ 目录下共存放七个样式 JSON 文件,其分工在 README 中已有明确界定:
| 文件 | 定位说明 |
|---|---|
style-microg-normal-openmaptiles.json |
microG 自定义样式的纯 OpenMapTiles 模式版本,sources 指向 Stadia,但可替换为任何符合 schema 的供应商(如 MapTiler) |
style-microg-normal-stadia.json |
在 OpenMapTiles 基础上叠加 Stadia 自定义图层的变体 |
style-microg-normal-mapbox.json |
面向 Mapbox 的历史遗留、不再维护的变体 |
style-microg-satellite-stadia.json / style-microg-satellite-mapbox.json |
与 normal 相同,但额外带卫星影像图层(OpenMapTiles schema 本身未定义卫星图层,因此 satellite 版必然依赖特定供应商) |
style-stadia-outdoors.json |
与 Stadia Outdoors 样式完全一致,仅追加 microG 元数据 |
style-mapbox-outdoors-v12.json |
面向 Mapbox 的户外地形样式变体(同样属于历史维护路径) |
这些 JSON 都是 MapLibre 风格的 Style Spec v8 格式文件。以 style-microg-normal-openmaptiles.json 为例,文件开头即声明 "version": 8,其核心结构包含:
{
"version": 8,
"name": "Mountain View Full",
"center": [12.819420849458652, 50.03325860617235],
"sources": {
"openmaptiles": {
"type": "vector",
"url": "https://tiles.stadiamaps.com/data/openmaptiles.json"
}
},
"sprite": "asset://sprites",
"glyphs": "asset://{fontstack}/{range}.pbf",
"layers": [ ... ]
}
值得注意的细节:每个图层都携带 metadata 字段,其中 microg:gms-type-feature 与 microg:gms-type-element 是 microG 自定义的地图要素分类键(如 landscape.natural.landcover / geometry.fill),它们是运行时做样式覆盖(MapStyleOptions)的关键索引,详见后文源码分析。
二、Stadia 变体与 OpenMapTiles 变体的差异
2.1 Stadia 专属图层
对比 style-microg-normal-stadia.json 与纯 OpenMapTiles 版本,Stadia 变体额外引入了两类数据源:
"sources": {
"openmaptiles": {
"type": "vector",
"url": "https://tiles.stadiamaps.com/data/openmaptiles.json"
},
"terrain": {
"type": "raster-dem",
"tiles": ["https://tiles.stadiamaps.com/data/terrarium/{z}/{x}/{y}.png"],
"minzoom": 0, "maxzoom": 12, "tileSize": 256, "encoding": "terrarium"
},
"global": {
"type": "vector",
"tiles": ["https://tiles.stadiamaps.com/data/global_landcover_v1/{z}/{x}/{y}.pbf"],
"minzoom": 0, "maxzoom": 6
}
}
terrain:Terrarium 编码的栅格高程源(raster-dem),用于地形渲染;global:全球土地覆盖(landcover)矢量源,对应样式中的global_grass、global_water等以"maputnik:comment": "Stadia specific"标注的图层。
纯 OpenMapTiles 版本则只有单一 openmaptiles 源,因此更易于移植到 MapTiler 等其他 schema 兼容供应商。
2.2 卫星变体的结构
style-microg-satellite-stadia.json 在上述基础上叠加 satellite 栅格源,并保留版权声明:
"satellite": {
"attribution": "© CNES, Distribution Airbus DS, © Airbus DS, © PlanetObserver (Contains Copernicus Data)",
"type": "raster",
"tiles": ["https://tiles.stadiamaps.com/data/satellite/{z}/{x}/{y}.jpg"],
"minzoom": 0, "maxzoom": 24
}
对应图层为 "id": "satellite"、"type": "raster"、"source": "satellite"。由于 OpenMapTiles schema 不规定卫星图层,卫星版本只能绑定具体瓦片供应商。
三、源码侧印证:样式如何在 GmsCore 地图模块中加载
样式 JSON 并不是孤立的资产文件——它们在 play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Styles.kt 中被实际消费。核心逻辑如下:
val styleJson = JSONObject(
context.assets.open(
if (BuildConfig.STADIA_KEY.isNotEmpty()) when (mapType) {
GoogleMap.MAP_TYPE_SATELLITE, GoogleMap.MAP_TYPE_HYBRID -> "style-microg-satellite-stadia.json"
GoogleMap.MAP_TYPE_TERRAIN -> "style-stadia-outdoors.json"
else -> "style-microg-normal-stadia.json"
} else when (mapType) {
GoogleMap.MAP_TYPE_SATELLITE, GoogleMap.MAP_TYPE_HYBRID -> "style-microg-satellite-mapbox.json"
GoogleMap.MAP_TYPE_TERRAIN -> "style-mapbox-outdoors-v12.json"
else -> "style-microg-normal-mapbox.json"
}
).bufferedReader().readText()
)
这段代码给出了地图类型 → 样式文件的完整映射关系:
MAP_TYPE_NORMAL / NONE→ normal 变体;MAP_TYPE_SATELLITE / HYBRID→ satellite 变体;MAP_TYPE_TERRAIN→ 户外(outdoors)变体。
3.1 运行时 API Key 注入
是否使用 Stadia 变体由 BuildConfig.STADIA_KEY 决定。该字段在 play-services-maps/core/mapbox/build.gradle 中定义:
buildConfigField "String", "STADIA_KEY", "\"${localProperties.getProperty("stadia.key", System.getenv('STADIA_API_KEY') ?: "")}\""
即优先取 local.properties 中的 stadia.key,其次取环境变量 STADIA_API_KEY,两者都缺省则为空串(此时回退到 Mapbox 变体)。
当 Stadia Key 存在时,Styles.kt 会在加载后对 JSON 做二次处理:遍历 sources 下所有 url 与 tiles 数组,统一追加 ?api_key=... 查询参数,再交给 Style.Builder().fromJson(...) 渲染。
3.2 metadata 键在样式覆盖中的作用
README 提到的 microg:gms-type-feature / microg:gms-type-element 元数据,被用于实现 Google Maps 的 MapStyleOptions 风格化覆盖(Styles.kt):每个覆盖操作按 featureType 与 elementType 选择器匹配图层(matchesOperation 用「前缀匹配」规则),随后对图层的 paint 属性执行 hue / saturation / lightness / gamma / 颜色替换等变换(applyColorChanges),或将 visibility: "off" 的图层从 JSON 中移除(layerShouldBeRemoved)。这意味着你在 Google Maps API 层写的 MapStyleOptions 最终会落到这些微G样式图层的 paint 值上。
四、Sprite 与字体资产的构建流水线
样式 JSON 中 "sprite": "asset://sprites"、"glyphs": "asset://{fontstack}/{range}.pbf" 指向 APK assets 内的资源,它们由 sprite_sources/ 与 fonts/ 两个源目录生成。
4.1 sprite 生成(spreet)
sprite_sources/ 下的 33 个 SVG 文件是图标源,涵盖 POI(poi_generic*.svg、poi_grocery.svg、poi_restaurant.svg 等)和道路盾牌(shield_de-motorway_*.svg、shield_rectangle-*.svg、shield_us-interstate_*.svg)。使用 spreet 工具可生成 app assets 中的单个 sprite 文件:
$ spreet sprite_sources/ ../../play-services-maps-core-mapbox/src/main/assets/sprites
$ spreet --retina sprite_sources/ ../../play-services-maps-core-mapbox/src/main/assets/sprites@2x
--retina 参数生成 @2x 高分屏版本。生成结果是一对 sprites.json + sprites.png(以及 @2x 版本),对应仓库中的 play-services-maps/core/mapbox/src/main/assets/sprites.png 等产物。
注意:README 中的目标路径
../../play-services-maps-core-mapbox/src/main/assets/是历史模块名写法;在当前仓库中对应目录为play-services-maps/core/mapbox/src/main/assets/。
4.2 PBF 字体生成(build_pbf_glyphs)
fonts/ 目录存放 TTF 源字体(Roboto Bold / Medium / Regular、OpenSans Regular)与 combinations.json,后者定义了字体栈合并规则:
{"Roboto Regular": ["Roboto Regular", "OpenSans Regular"]}
含义:渲染 Roboto Regular 字体栈时,若该字体缺字形,则回退到 OpenSans Regular 补齐。README 特别说明:这样组合的目的是「Open Sans Regular 与 Roboto Regular 合并,确保没有字形缺失」。
使用 build_pbf_glyphs(来自 stadiamaps/sdf_font_tools)生成 PBF 字形瓦片:
$ build_pbf_glyphs --overwrite -c fonts/combinations.json fonts/ ../../play-services-maps-core-mapbox/src/main/assets
$ rm -r ../../play-services-maps-core-mapbox/src/main/assets/OpenSans\ Regular # 清理临时目录
--overwrite:覆盖已存在的输出;-c combinations.json:按字体栈合并规则生成;- 最后一步删除源字体残留的临时目录。
生成结果即 assets 下的 Roboto Regular/、Roboto Bold/、Roboto Medium/ 字体目录(每个目录内是 {range}.pbf 分块字形文件)。
4.3 字体符号链接的坑
README 记录了一个重要陷阱:assets 中存在一条符号链接,从字体栈规范名 Open Sans Regular,Arial Unicode MS Regular 指向 Roboto Regular。原因是 MapLibre 偶尔会尝试访问该字体名,若不存在会导致整块瓦片渲染失败。这也是 assets 目录中会出现 Open Sans Regular,Arial Unicode MS Regular/ 这样一个「目录名含逗号与空格」条目的原因。
4.4 完整部署命令序列
将样式、sprite、字体一次性部署到 app assets:
$ cp style-microg-{satellite,normal}-{mapbox,stadia}.json style-stadia-outdoors.json style-mapbox-outdoors-v12.json ../../play-services-maps-core-mapbox/src/main/assets/
$ spreet sprite_sources/ ../../play-services-maps-core-mapbox/src/main/assets/sprites
$ spreet --retina sprite_sources/ ../../play-services-maps-core-mapbox/src/main/assets/sprites@2x
$ build_pbf_glyphs --overwrite -c fonts/combinations.json fonts/ ../../play-services-maps-core-mapbox/src/main/assets
$ rm -r ../../play-services-maps-core-mapbox/src/main/assets/OpenSans\ Regular
第一条 cp 命令使用 bash 花括号展开,一次复制全部 7 个 JSON(style-microg-satellite-mapbox.json、style-microg-satellite-stadia.json、style-microg-normal-mapbox.json、style-microg-normal-stadia.json、style-stadia-outdoors.json、style-mapbox-outdoors-v12.json 以及按模式匹配的其余文件)。
五、编辑器与样式调试:Maputnik 与 Mapbox 托管源
README 推荐使用 Maputnik 作为样式可视化编辑器,并以 MapLibre Style Spec(含 expressions 文档)为语法参考,schema 基准则是 OpenMapTiles schema。
由于 app 内的样式使用 asset:// 协议引用 sprite 与字体,本地 JSON 无法直接在 Maputnik 中预览。README 给出的临时方案是将 sprite / glyphs 指向 Mapbox 托管的 microG 源:
"sprite": "mapbox://sprites/microg/cjui4020201oo1fmca7yuwbor/8fkcj5fgn4mftlzuak3guz1f9",
"glyphs": "mapbox://fonts/microg/{fontstack}/{range}.pbf",
改完预览后再换回 asset://sprites 与 asset://{fontstack}/{range}.pbf 部署到应用。
5.1 供应商相关文档
针对不同瓦片供应商,README 给出官方参考资料:
- Mapbox:mapbox-streets-v8 瓦片集参考
- Stadia Maps:自定义样式文档 与 瓦片集文档
5.2 本地替代方案
若不想依赖 Mapbox 托管,也可直接在本地以 Stadia 源打开 style-microg-normal-openmaptiles.json 进行预览(注意替换 sprite/glyphs 引用)。当前仓库的 assets 版本中,style-stadia-outdoors.json 直接使用远程资源:"glyphs": "https://tiles.stadiamaps.com/fonts/{fontstack}/{range}.pbf"、"sprite": "https://tiles.stadiamaps.com/styles/outdoors/sprite",可作为对比参考。
六、许可证与归属
microG 地图样式有明确的许可证链条,部署或二次分发时必须保留:
- normal / satellite 样式:基于 Mapbox Basic(Mapbox Open Styles 的一部分),样式代码 BSD 许可、虚拟要素与设计 CC BY 3.0;
- Stadia 变体:使用了 Stadia 对 OSM Bright 的变体片段(同样是 Mapbox Open Styles 的派生),样式代码 BSD 3-Clause、设计 CC BY 4.0;
- outdoor 样式:基于 Stadia Outdoors 样式,样式代码 BSD 3-Clause、设计 CC BY 4.0;
- 字体:Roboto 家族,Apache 2.0 许可;
- 派生记录:分别由 larma 与 /e/ foundation 完成派生。
完整条款见 artwork/styles/README.md 的 Legal 一节,仓库根目录另有 LICENSES(含 Apache-2.0、CC-BY-4.0、CC0-1.0 全文)可供核对。
七、小结
把 README 的说明与 play-services-maps 源码对照,可以得到完整的资产链路:
- 编辑:在 Maputnik 中调整
artwork/styles/*.json的图层与绘制属性,注意 Stadia 变体带terrain/global专属源,卫星变体带satellite栅格源; - 构建:用
spreet将sprite_sources/合成sprites.png/.json(含@2x),用build_pbf_glyphs配合fonts/combinations.json把 TTF 合成 PBF 字形瓦片,保留Open Sans Regular,Arial Unicode MS Regular符号链接; - 部署:7 个 JSON + sprite + 字体目录全部进入
play-services-maps/core/mapbox/src/main/assets/,由 Styles.kt 按地图类型与STADIA_KEY加载,并对MapStyleOptions覆盖与 API Key 注入做运行时处理。
这套体系的核心价值在于:microG 的 Play Services 地图实现不依赖任何闭源样式服务,样式与资产全部以开源许可存放于仓库内,开发者可以自由 fork、换源(MapTiler / Stadia / Mapbox)、调色并重新打包,实现完全自托管的 Google Maps 替代地图渲染管线。