authentik 地理数据包 @goauthentik/geo 解析:hexworld 底图瓦片归档的构建期生成器
【免费下载链接】authentikThe authentication glue you need.项目地址: https://gitcode.com/GitHub_Trending/au/authentik
本文以packages/geo包为主线,讲解 authentik 事件地图(events map)所依赖的 hexworld 六边形底图归档(PMTiles)是如何在构建期生成的。读完你将掌握:该包的定位与「构建期工具、不进入浏览器」的边界、它与运行时地图元素的契约设计、三档 H3 分辨率缩放带的原理、归档的重新生成流程,以及品牌级运行时覆盖(自定义瓦片服务器)的用法。
包的定位:构建期机器,零浏览器代码
@goauthentik/geo是一个纯构建期工具包,其职责是把 Natural Earth 矢量数据和 Protomaps planet 数据切片,转换为tiles/hexworld.pmtiles归档,再由web构建流程把该归档拷贝进前端产物。它不向浏览器发送任何运行时代码。
从 package.json 可以看到包的完整脚本面:
hexworld:build→node scripts/build-hexworld.ts:运行归档生成器;tiles:pull-hexworld→node scripts/pull-hexworld.ts:拉取/拷贝已发布的归档与字体到tiles/;test→vitest run:Node 环境单测;lint:types→ 对src、scripts、test三处执行tsc --noEmit类型检查。
运行环境要求 Node ≥ 24(engines与devEngines均做了约束)。运行时依赖仅两个:h3-js(H3 六边形网格计算)与pmtiles(读取 PMTiles 归档);@mapbox/vector-tile、pbf、@types/geojson等则用于解码 MVT 瓦片与处理 GeoJSON。
实际的地图元素与样式位于web/src/elements/maps/,与归档生成器分离——这是「构建期 vs 运行时」职责划分的直接体现。
与运行时地图的契约:三份共享文件
归档与绘制它的地图元素之间必须对齐三件事,因此这三件事由运行时元素拥有定义权,本包只导入、不重新声明:
| 契约项 | 拥有方 |
|---|---|
| Zoom → H3 分辨率分段(缩放带) | web/src/elements/maps/bands.ts |
| 标签种类与显示缩放级别 | web/src/elements/maps/labels.ts |
| 版权署名(attribution)字符串 | web/src/elements/maps/attribution.ts |
这三份文件刻意保持零依赖。原因正如 README 所解释的:如果从样式模块导入,会把 MapLibre 等重依赖拖进 Node 构建脚本。依赖方向是单向的——构建工具读取应用的契约,反向绝不成立。
以缩放带为例,bands.ts 定义了全局唯一的HEX_BANDS:
export const HEX_BANDS: readonly HexBand[] = [ { res: 3, minzoom: 0, maxzoom: 2 }, { res: 4, minzoom: 3, maxzoom: 6 }, { res: 5, minzoom: 7, maxzoom: 7 }, ];而署名串定义在 attribution.ts:
export const HEXWORLD_ATTRIBUTION = "© OpenStreetMap (labels) · Natural Earth";生成器会把同样的字符串盖进 PMTiles 元数据(metadata)里。值得注意的是,该文件注释特别强调「纯文本设计」:airgap 测试禁止样式内出现外部 URL,地图上仅需 ODbL 文本署名,真正的链接由文档页承载。
三档缩放带(Zoom bands)的几何测算
构建时归档使用三个 H3 分辨率。README 中给出的测量值通过 h3 的getHexagonEdgeLengthAvg与getHexagonAreaAvg计算得出——这里的「单元格宽度」指对顶点到对顶点的距离,约为边长的 2 倍:
| Zoom 范围 | H3 分辨率 | 边长 | 单元格宽度 | 单元格面积 |
|---|---|---|---|---|
| z0–z2 | 3 | ~69 km | ~138 km | ~12,393 km² |
| z3–z6 | 4 | ~26 km | ~52 km | ~1,770 km² |
| z7 | 5 | ~10 km | ~20 km | ~253 km² |
z7 是归档的maxzoom,MapLibre 在其之上进行 overzoom(超采样放大)。运行时通过bandForZoom将缩放级别钳制到 res-5 分段,保证事件在更高缩放级别下仍按 res-5 分箱,而不会掉落到最粗的分辨率——见 bands.ts 的实现注释:
export function bandForZoom(zoom: number): HexBand { // 越过 MAX_BAND_ZOOM 后地图展示的是该分段的 overzoom 瓦片, // 事件必须解析到该分辨率,而不是落到最粗的一档。 const z = Math.floor(Math.max(0, Math.min(MAX_BAND_ZOOM, zoom))); return HEX_BANDS.find((band) => z >= band.minzoom && z <= band.maxzoom) ?? HEX_BANDS[0]!; }关键约束:缩放带被烘焙进每一个已发布的归档。修改HEX_BANDS会使已有瓦片失效,因此它被固化在运行时契约中,任何变更都需要同步重建并重新发布归档。
已发布的归档内容
tiles/hexworld.pmtiles被提交进仓库(这是当前detail版本,约 22 MiB):全球完整的 res-3/res-4 网格,外加限定在人口密集区 detail 区域内的 res-5 覆盖层。
tiles/fonts/随归档一并发布 Latin Noto Sans 字形(Regular + Medium 两种字重,0-255 与 256-511 两个 Unicode 范围),采用 SIL Open Font License 1.1(许可文本见 tiles/fonts/OFL.txt)。归档与字形均提交入库,目的是让 web 构建保持密封性(hermetic)——克隆后无需联网。
web构建会把tiles/hexworld.pmtiles和tiles/fonts/拷贝到web/dist/assets/maps/。若构建时缺失任一文件,构建会显式失败,而不是静默产出坏掉的地图。
detail-zone:人口密集区的 res-5 覆盖逻辑
res-5 覆盖层不是全球铺开的,而是由 src/detail-zone.ts 计算出的「人口密集区」:
- 每个高于
minPop的locality标签,以其所在 res-4 单元格为中心,用gridDisk(ring)向外扩展一圈,所有圆环的并集构成baseCells(res-4 级); - 再对每个 res-4 单元格做七子展开(
cellToChildren)得到 res-5 的detailCells。
实现刻意把区域基准放在 res-4 而非 res-5:在 z7–z8 时,区域外由 res-4 单元格作为底填,区域内叠加 res-5 子单元,二者面积上是精确的 7 倍父子关系(即使顶点互相交错)。detailCellsForRes5负责把 res-5 陆地单元与区域求交,只输出区域内的 res-5 单元作为覆盖层。
生成器的输入与流水线
生成器入口为scripts/build-hexworld.ts(由 package.json 的hexworld:build指向)。前置条件:
- Node ≥ 24,且已安装本 workspace(
pnpm install); tippecanoe与go-pmtiles在PATH上;- 一份本地 PMTiles planet 数据,解出 z0–8(Protomaps planet 构建的约 1–3 GB 切片)。
构建命令:
# 仅预览 shell 流水线,不实际执行: pnpm run hexworld:build -- --dry-run --out tiles # 完整运行——同时产出两种尺寸的切片: pnpm run hexworld:build -- --dump ./planet-z8.pmtiles --out tiles # tiles/hexworld-detail.pmtiles ← res 3 + 4 + 分区 res 5(随包发布) # tiles/hexworld-plain.pmtiles ← 仅 res 3 + 4(更小、更粗糙)生成器首次运行时会下载的输入被钉死在特定版本,保证一年后重跑仍能产出同样的瓦片:
- Natural Earth 矢量数据:
nvkelso/natural-earth-vector@v5.1.2。其中ne_50m_land.geojson提供陆地多边形,ne_50m_admin_0_countries.geojson提供国家归属,州/省界来自ne_10m_admin_1_states_provinces.geojson(因为 50m 的 admin-1 数据集只覆盖 9 个国家); - Protomaps planet 构建:
20260521——标签层的来源。当前随包归档正是从该构建切出;若想收录新的地名,需对更新的构建重新生成。
流水线的源码级拆解
README 概括了流水线,而 packages/geo/src 下的模块给出了每个环节的具体算法:
陆地单元化(land fill):src/land.ts 的
landCells用 h3 的polygonToCellsExperimental(..., "containmentCenter", true)把陆地多边形转成 H3 单元。前置处理normalizePolygon做了两件事:把纬度钳制到 Web Mercator 上限 ±85.051129(超出即离图,h3 在极点附近也不稳定);对经度跨度超过 180° 的环(如南极洲的 -180..180 多边形)在 -60°、60° 两条经线上做 Sutherland–Hodgman 裁剪,切成三个子 180° 的条带。单元格多边形输出由 src/cells.ts 的cellPolygon完成,它把跨越反经线(antimeridian)的边界平移进连续的 0..360 范围,避免下游出图时产生横跨全球的细长碎片——test/cells.test.ts 里两个用例分别验证了「闭合环」与「不跨越反经线」。国家/区域归属(country assignment):src/countries.ts 先从 Natural Earth FeatureCollection 构建可检索索引(
buildCountryIndex/buildRegionIndex),每条记录预计算 bbox 用于快速排除,再对每个单元格质心做点-多边形包含判断(射线投射法,支持外环+内洞)。国家代码依次尝试ISO_A2/iso_a2、ISO_A3/iso_a3、ADM0_A3/adm0_a3;区域代码则依次尝试iso_3166_2/ISO_3166_2、adm1_code/ADM1_CODE、code_hasc。质心落在任何多边形之外的单元格(海洋或南极冰盖)不会出现在结果中。边界提取(borders):src/borders.ts 的
borderEdges遍历每个陆地单元及其 6 个邻居,只要相邻两单元在最强适用级别上不同,就沿共享 H3 边(cellsToDirectedEdge+directedEdgeToBoundary)产出边界要素:- 国家不同 →
level: 0(国家边界优先); - 国家相同但区域不同 →
level: 1(区域边界仅在两国代码一致时发出); - 邻居非陆地 → 在提供
land集合时发出level: 0海岸线边,保证每个国家都被水域完整围合。 每个无序单元对只会产生一条要素(按两个单元 ID 的字典序规范化去重),携带的level属性让运行时样式能从同一 source-layer 用不同线宽渲染国界与区界。aCell/bCell仅存在于内存要素中用于下游区域过滤,交给 tippecanoe 前会被剥掉以减小瓦片体积。
- 国家不同 →
标签归一化(labels):src/labels.ts 从 dump 的
places层读取标签。关键设计是国家/区域的显示缩放由 hexworld 自己决定:Protomaps 的min_zoom是为其高密度底图调校的(比如中国在 dump 里是min_zoom: 6,会埋没大国),所以COUNTRY_ZOOM_TIERS按人口分档重写国家标签的起始缩放——人口 ≥1 亿从 z0 显示、≥2000 万 z1、≥500 万 z2,其余 z3;区域标签则取运行时契约里的LABEL_MIN_ZOOM.region;本地地物保留 dump 的min_zoom。此外还有dedupePlaces(同一地物在 min_zoom 到 z8 的每张瓦片重复出现,按 kind+name+res-5 单元聚合成一点、保留最早 minZoom)与capLocalities(按人口排序截断本地地物数量,防止标签爆炸)。切片与合并:上述要素交给 tippecanoe 和 tile-join 产出 PMTiles。两种尺寸的切片总是成对生成,最终选哪种由人工把关——在 pmtiles 查看器里检查两者后再按发布预算决定。
重新生成并发布归档
AUTHENTIK_HEXWORLD_SOURCE=/path/to/hexworld-detail.pmtiles \ pnpm run tiles:pull-hexworld git add tiles/hexworld.pmtiles && git committiles:pull-hexworld由 scripts/pull-hexworld.ts 实现:若AUTHENTIK_HEXWORLD_SOURCE指向本地路径则直接拷贝(文件已就位时跳过),否则按默认 URL 拉取;随后从 Protomaps 的资源地址抓取Noto Sans Regular/Noto Sans Medium的 0-255、256-511 两个字形段到tiles/fonts/。
运行时覆盖:品牌级自定义瓦片服务器
归档不是事件地图的唯一数据源。一个品牌(Brand)可以把事件地图指向自己的瓦片服务器(System > Brands > Map tiles设置),这会完全绕过本归档——相关逻辑见 web/src/elements/maps/basemap-style.ts。这意味着 hexworld 归档是「开箱即用的默认底图」,而多租户/多品牌场景下可以通过品牌配置接入私有地图服务,为事件地理可视化提供自定义外观与数据源。
测试覆盖
本包的测试直接对应流水线的核心算法:
pnpm run test # Vitest,仅 Node 环境 pnpm run lint:types # tsc 检查 src、scripts、test覆盖范围包括陆地填充(land)、边界提取(borders)、国家归属(countries)、detail 区域(detail-zone)与标签归一化(labels)的数学逻辑,测试文件位于 packages/geo/test。地图元素自身的测试则与元素同住,位于web/test/unit/maps/与web/test/component/——这再次印证了「构建期工具测试与运行时组件测试」的分层边界。
【免费下载链接】authentikThe authentication glue you need.项目地址: https://gitcode.com/GitHub_Trending/au/authentik
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考