authentik 地理数据包 @goauthentik/geo 解析:hexworld 底图瓦片归档的构建期生成器
2026/9/11 17:53:38 网站建设 项目流程

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:buildnode scripts/build-hexworld.ts:运行归档生成器;
  • tiles:pull-hexworldnode scripts/pull-hexworld.ts:拉取/拷贝已发布的归档与字体到tiles/
  • testvitest run:Node 环境单测;
  • lint:types→ 对srcscriptstest三处执行tsc --noEmit类型检查。

运行环境要求 Node ≥ 24(enginesdevEngines均做了约束)。运行时依赖仅两个:h3-js(H3 六边形网格计算)与pmtiles(读取 PMTiles 归档);@mapbox/vector-tilepbf@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 的getHexagonEdgeLengthAvggetHexagonAreaAvg计算得出——这里的「单元格宽度」指对顶点到对顶点的距离,约为边长的 2 倍:

Zoom 范围H3 分辨率边长单元格宽度单元格面积
z0–z23~69 km~138 km~12,393 km²
z3–z64~26 km~52 km~1,770 km²
z75~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.pmtilestiles/fonts/拷贝到web/dist/assets/maps/。若构建时缺失任一文件,构建会显式失败,而不是静默产出坏掉的地图。

detail-zone:人口密集区的 res-5 覆盖逻辑

res-5 覆盖层不是全球铺开的,而是由 src/detail-zone.ts 计算出的「人口密集区」:

  • 每个高于minPoplocality标签,以其所在 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);
  • tippecanoego-pmtilesPATH上;
  • 一份本地 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 下的模块给出了每个环节的具体算法:

  1. 陆地单元化(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 里两个用例分别验证了「闭合环」与「不跨越反经线」。

  2. 国家/区域归属(country assignment):src/countries.ts 先从 Natural Earth FeatureCollection 构建可检索索引(buildCountryIndex/buildRegionIndex),每条记录预计算 bbox 用于快速排除,再对每个单元格质心做点-多边形包含判断(射线投射法,支持外环+内洞)。国家代码依次尝试ISO_A2/iso_a2ISO_A3/iso_a3ADM0_A3/adm0_a3;区域代码则依次尝试iso_3166_2/ISO_3166_2adm1_code/ADM1_CODEcode_hasc。质心落在任何多边形之外的单元格(海洋或南极冰盖)不会出现在结果中。

  3. 边界提取(borders):src/borders.ts 的borderEdges遍历每个陆地单元及其 6 个邻居,只要相邻两单元在最强适用级别上不同,就沿共享 H3 边(cellsToDirectedEdge+directedEdgeToBoundary)产出边界要素:

    • 国家不同 →level: 0(国家边界优先);
    • 国家相同但区域不同 →level: 1(区域边界仅在两国代码一致时发出);
    • 邻居非陆地 → 在提供land集合时发出level: 0海岸线边,保证每个国家都被水域完整围合。 每个无序单元对只会产生一条要素(按两个单元 ID 的字典序规范化去重),携带的level属性让运行时样式能从同一 source-layer 用不同线宽渲染国界与区界。aCell/bCell仅存在于内存要素中用于下游区域过滤,交给 tippecanoe 前会被剥掉以减小瓦片体积。
  4. 标签归一化(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(按人口排序截断本地地物数量,防止标签爆炸)。

  5. 切片与合并:上述要素交给 tippecanoe 和 tile-join 产出 PMTiles。两种尺寸的切片总是成对生成,最终选哪种由人工把关——在 pmtiles 查看器里检查两者后再按发布预算决定。

重新生成并发布归档

AUTHENTIK_HEXWORLD_SOURCE=/path/to/hexworld-detail.pmtiles \ pnpm run tiles:pull-hexworld git add tiles/hexworld.pmtiles && git commit

tiles: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),仅供参考

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

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

立即咨询