AIGC标识 GeoLibre 技术架构与实现解析

1. 项目形态:一套 React 应用,四种宿主

GeoLibre 的代码库只维护一套 React 应用,却要同时运行在浏览器、桌面、移动端与 Jupyter 中:

宿主 构建方式 运行时 关键差异
Web Vite 静态构建 + nginx 浏览器 无本地文件对话框、无本地 MBTiles/本地栅格读取
桌面 Tauri v2(系统 WebView) Rust 命令桥 + WebView 完整本地 I/O,可拉起 Python sidecar
Android / iOS Tauri v2 mobile 系统 WebView 功能裁剪,商店沙盒限制
Jupyter anywidget(内嵌 dist-embed) Python 内核 ↔ JS 由 Python 驱动地图,双向同步
R HTML widget RStudio/Quarto/Shiny 交换 .geolibre.json 工程

选择 Tauri 而非 Electron,换来的是更小的安装体积与系统 WebView 的原生能力;代价是 WebView 差异被放大:Windows 是 WebView2(Chromium),macOS/iOS 是 WKWebView(JavaScriptCore),Linux 是 WebKitGTK。后者的合成器与 DMA-BUF 行为不同,仓库因此维护了 WEBKIT_DISABLE_DMABUF_RENDERER 一类环境变量开关,并记录了 WebKitGTK 上逐瓦片约 125 ms 的渲染成本。这类平台差异不是“配置问题”,而是跨端方案必须计入的工程成本。

2. 仓库结构:npm workspaces monorepo + 两个 Python 组件

apps/geolibre-desktop        React + Tauri 主应用(src/、src-tauri/、jupyterlite/、public/)
apps/geolibre-mcp-app        MCP 应用壳
packages/core                Zustand store、领域类型、.geolibre.json 工程 schema
packages/map                 MapLibre 生命周期、图层同步、Mapbox/Cesium/ArcGIS 引擎
packages/ui                  shadcn 风格 UI 原语
packages/processing          客户端算法注册表(矢量/栅格/WASM/OBIA/统计/DGGS)
packages/plugins             插件接口、内置插件、面板/菜单注册表
packages/embed               @geolibre/embed 类型化 iframe 客户端
packages/collab-core         协作协议与会话
backend/geolibre_server      本地 FastAPI sidecar(127.0.0.1:8765)
backend/geolibre_server_api  多用户项目/身份 API 参考实现(FastAPI + SQLAlchemy)
python/                      geolibre anywidget 包 + geolibre-mcp
workers/                     Cloudflare Workers:viewer/collab/tiles/ai-proxy
extensions/geolibre-chrome   Chrome 扩展

包边界是这套架构最重要的约束之一。@geolibre/core 持有全部领域类型与工程 schema,@geolibre/map 只负责渲染与同步,@geolibre/processing 只注册算法,@geolibre/plugins 定义插件契约。UI 层不直接操作地图 SDK,而是修改 store。这个约束在后面几节会反复出现。

3. 状态模型:store-driven 单向数据流

应用的状态收敛在一个 Zustand store 中,数据流被固定为七步:

  1. 数据入口:Add Data 菜单、Tauri 对话框、浏览器文件选择、拖拽、插件控件。
  2. 本地矢量转换:MapLibre 读不了的格式先由 DuckDB-WASM Spatial 转为 GeoJSON(INSTALL spatial; LOAD spatial; → ST_Read;GeoParquet 走 Parquet reader;zip Shapefile 先 shpjs,失败再交给 DuckDB;KMZ 客户端解压)。
  3. 写入 store:转换结果交给 addGeoJsonLayer;瓦片、服务、栅格、ArcGIS、MBTiles、插件图层则生成带 source 元数据的 GeoLibreLayer 记录。
  4. 引擎订阅:选中的 MapEngine 订阅 layers,MapLibre 走 MapController.syncLayers,另外三种引擎走各自适配器。
  5. 样式与面板改动:改的仍是图层状态,由 sync 落到 paint、可见性、不透明度、顺序与删除。
  6. 属性表选择:更新高亮要素源,可缩放地图到选中要素。
  7. 保存:projectFromStore 序列化为 .geolibre.json。

这条链路解释了几个设计结果:

  • 四种渲染引擎、分屏 pane、撤销栈、工程保存共用同一份状态,切换引擎不需要重建工程;
  • 任何“直接 map.addLayer()”的写法都会绕开 sync,导致撤销、保存、分屏不同步,因此源码里明确禁止;
  • 插件拿到的 app API 只能在 store 语义内操作,无法越权改地图。

撤销栈的参数也体现了工程化取舍:historyCoalesceMs = 400(连续操作合并窗口)、maxHistoryFeatureCount = 500_000、以及按 200 字节/要素折算内存预算,避免大图层把历史记录撑爆。

4. 数据接入:分层解析,轻量优先

格式读取不是一棵“统一解析器”,而是按成本分层的:

类别 首选路径 兜底
GeoJSON / JSON 直接读取 —
Shapefile(zip) shpjs DuckDB ST_Read
GeoPackage / SQLite sql.js / 专用 reader DuckDB
KML / KMZ DOMParser + 客户端解压 —
FlatGeobuf / PMTiles / COG 专用 reader + HTTP Range —
GeoParquet Parquet reader(Arrow) DuckDB
Zarr / NetCDF / HDF 专用 reader + 维度选择 —
CAD / DXF / LandXML / Excel 专用 parser —
其它矢量 — DuckDB-WASM Spatial

DuckDB-WASM Spatial 在这里同时扮演两个角色:格式驱动与计算引擎。它把文件注册成临时表再查询,因此“导入”和“SQL 查询”共用同一条路径,不需要为每种格式写两套代码。

真正决定云原生可用性的不是前端库,而是两个网络条件:CORS 与 HTTP Range。COG、PMTiles、GeoParquet 都依赖分段读取,对象存储若不返回 Accept-Ranges 或不允许跨域,浏览器端就会退化为整文件下载甚至失败。这一点在自托管与私有桶场景里尤其明显。

规模阈值同样是分层的一部分:packages/core/src/types.ts 中的 LARGE_VECTOR_FEATURE_THRESHOLD = 50_000,超过该数量的本地矢量图层不再用一个内存中的 GeoJSON source,而是切换到客户端矢量瓦片(geojson-vt 协议)。同一阈值也约束派生几何、Cesium 点要素与 Sedona 结果集,避免一次性把几十万要素塞进主线程。

5. 渲染层:四种引擎的抽象与代价

渲染抽象围绕 MapEngine 与能力矩阵展开:每个引擎声明自己支持哪些数据源、投影、3D、控件与插件能力,UI 据此隐藏或禁用入口,而不是等运行时抛错。

引擎 投影 三维 加载成本 典型限制
MapLibre GL JS(默认) Web Mercator、部分平面 CRS globe/terrain 主包内 不支持任意椭球投影
Mapbox GL JS Web Mercator globe/terrain 懒加载,约 1.91 MB(gzip 532 KB) 需 token,许可条款不同
CesiumJS 椭球、行星 原生 3D 地球 约 4.7–4.8 MB,懒加载 插件控件需适配层
ArcGIS Maps SDK 多投影、3D 场景 2D/3D 视图 约 84 MB、18,810 个文件,走 CDN 依赖 Esri CDN 与 API key

多引擎不是“多引一个库”那么简单,成本体现在三处:

  1. 图层同步:同一份 GeoLibreLayer 要映射到不同 SDK 的 source/layer 语义,MapLibre 的 style spec 与 ArcGIS 的 renderer 差异很大,packages/map/src 里因此有 layer-sync.ts、cesium-layer-sync.ts、arcgis-layers.ts 等多套适配器。
  2. 控件宿主:MapLibre 控件在 Cesium/ArcGIS 上需要桥接层(cesium-control-host.ts、arcgis-control-host.ts),否则插件生态无法复用。
  3. 能力降级:几何编辑器(GeoEditor)的插件声明 engines: ["maplibre", "mapbox"],切到 Cesium/ArcGIS 时不会激活;这不是 bug,而是宿主 API 不兼容时的显式降级。

分屏(split panes)复用同一份状态,按地面分辨率同步相机,每个 pane 可以挂不同引擎。这也是 store-driven 设计最直接的收益。

6. 空间处理:四种执行后端

地图上的“Processing”并不是一个后端,而是四条执行路径的统一入口:

后端 运行位置 适用 代价
Client(Turf.js) 浏览器主线程/Worker 轻量矢量运算 精度受经纬度影响
Pyodide 浏览器 WebAssembly 需要 GeoPandas/Shapely 的投影感知运算 首次下载几十 MB 运行时
Sidecar(FastAPI) 本机 127.0.0.1:8765 栅格、格式转换、GeoPandas 增强 需安装可选 extras
Whitebox WASM 浏览器 WebAssembly Worker 1,000+ 地理处理工具 受浏览器内存与单线程约束

Whitebox 工具箱在菜单层列出 1,068 个工具条目、9 个分类,运行时目录快照包含 775 个带参数定义的工具(两者不是同一集合:菜单还包含锁定条目与仅分类展示项)。工具定义里的 param.kind 决定它在模型构建器里的角色:raster_in/vector_in 成为输入端口,raster_out/vector_out 成为输出端口,其余进入参数面板。

sidecar 是可选组件,通过 /sidecar 同源代理接入浏览器构建,路径被限制在 GEOLIBRE_CONVERSION_ROOTS(镜像内默认 /data)。这个限制不是可选项:一个能执行任意路径读写的本地 HTTP 服务,在浏览器同源代理下等于把文件系统暴露给页面。

模型构建器(Model Builder)把上述工具串成 DAG:packages/processing/src/model-graph.ts 用 Kahn 算法做拓扑排序,校验 10 类图问题(未知端口、重复输入、悬空边等),并支持导出等价 Python 脚本。它没有迭代器,因此“批量处理”需要靠 Batch 模式或导出脚本,这一点在实现上是明确的缺失,而不是隐藏功能。

7. SQL 工作区:三种引擎的取舍

引擎 实现 能力 限制
DuckDB Spatial DuckDB-WASM 读本地/远程文件、空间函数、Parquet 单浏览器线程,内存受限
PostGIS(PGlite) PostGIS WASM 真实 PostGIS 语义 运行时约 19 MB,单连接
Apache Sedona CereusDB WASM 分布式语义的本地近似 结果集上限 50,000 要素

DuckDB 路径的注册方式是“每次查询重建临时表”:把图层 FeatureCollection 写入内存文件系统,CREATE OR REPLACE TEMP TABLE ... AS SELECT * FROM ST_Read(...),查询结束即销毁。这样查询永远反映当前图层状态,代价是每次都要重新解析。结果表默认只渲染前 500 行,历史保留 25 条,避免大结果拖垮 UI。

8. 工程格式与序列化边界

.geolibre.json 是唯一事实源,顶层包含 version、layers、layerGroups、pluginState、legend、printLayout、storymap、widgets、bookmarks、interaction 等块。图层对象支持多种类型(矢量、栅格、瓦片、服务、3D、点云、插件图层等),样式、弹窗、连接、虚拟字段、快速过滤器都挂在图层上。

格式设计里有两个细节值得注意:

  • 凭据脱敏:工程文件只保存凭据引用,不保存明文密钥;分享前还有一层 share-readiness 检查。
  • 序列化上限:V8 的最大字符串长度约 536 MB,超过会抛 RangeError;JavaScriptCore 与 SpiderMonkey 的报错文案又不同。project-serialization-limits.ts 因此按错误文案匹配,并把“工程过大”引导到 PMTiles/FlatGeobuf,而不是让用户看到一个无上下文的失败。导入类文件(QGIS 工程、ArcGIS Pro 工程、插件工程)另有 25 MiB 的上限。

9. 插件体系:契约、生命周期与边界

插件接口 GeoLibrePlugin 描述 id、名称、版本、engines、activate/deactivate,以及可选的右侧栏面板、工具栏菜单、浮动面板、地图点击工具、助手工具等声明。激活流程由 plugin-manager.ts 统一编排:注册 → 能力检查 → 引擎匹配 → 建立作用域 API(scopeAppToPlugin)→ 调用 activate;失败时按相反顺序回滚。

外部插件的来源有三种:zip 包(校验 SHA-256)、plugin.json 清单 URL、以及构建时烘进 public/plugins/<id>/ 的 bundled drop-in。市场(marketplace)只是清单的分发层,安装仍走同一套校验与信任流程。插件不能绕过部署策略:部署策略里的 plugins 段与 capability(如 plugins:install)会在加载前裁决。

插件能做什么、不能做什么,由 app API 的边界决定:可以增删图层、读写样式、注册面板与菜单、发起 OAuth、保存凭据;但不能直接持有 MapLibre 实例,也不能绕过 store 修改工程。这个边界让插件在多引擎、分屏、撤销、保存四个维度上都不会破坏一致性。

10. AI、MCP 与脚本化

三条 AI 相关路径在实现上完全不同:

  • AI 助手:自然语言 → 工具调用 → 应用命令通道。所有操作都经过应用自己的命令层,因此可审计、可撤销;Provider 采用 BYOK,支持 Gemini、Anthropic、OpenAI、OpenRouter、Vercel AI Gateway、Amazon Bedrock、本地 Ollama 与任意 OpenAI 兼容端点。
  • MCP 服务器:geolibre-mcp 是 stdio 服务,无头地读写 .geolibre.json。project.py 负责构造片段(图层、插件状态),authoring.py 负责把片段应用到整份工程,server.py 是唯一导入 MCP SDK 的模块。工作区被限制在 GEOLIBRE_MCP_ROOTS 或 --root 内。
  • agent skill:一段 SKILL.md,教外部编码智能体“何时用哪条路、哪些规则容易踩坑”,不参与运行时。

Python 侧还有两个入口:应用内 Python Console 跑在 Pyodide 上,Notebook 面板在桌面端连本地 JupyterLab(relay 端口 8766),在 Web 端用 JupyterLite。geolibre Python 包则以 anywidget 形式内嵌完整应用,实现 Python 与地图的双向同步。这些路径最终都落到同一份工程格式或同一套命令上,这是它们能互相组合的原因。

11. 服务端拓扑:可选,而非必需

系统里有四个进程/服务,职责严格分离:

组件 默认地址 作用 是否必需
Python sidecar 127.0.0.1:8765 栅格/转换/GeoPandas 增强 可选
Server API :8000 多用户项目、身份、分享、OAuth2 可选
Collab relay :8787 WebSocket 实时协作 可选
Notebook relay 127.0.0.1:8766 桌面端 Jupyter 桥 可选

Server API 是 FastAPI + SQLAlchemy,默认 SQLite,可切 PostgreSQL;对象存储默认本地目录,也可用 S3 兼容存储。身份体系包括个人令牌(带 scope)、账号/会话、组织与组、OIDC 单点登录、SCIM 2.0、可信头代理登录,以及 OAuth 2.0 Authorization Code + PKCE。协作中继在托管环境用 Cloudflare Durable Object(每会话一个实例),自托管用 workers/collab-node 的 Node + ws 实现;协议是“整快照 + 增量补丁”,带回声抑制与撤销交互处理。

Docker Compose 把 Web(8080)、Server(8000)、Collab(8787)与 Postgres 串起来,Web 镜像同时承担静态资源与 /sidecar 反向代理。部署策略(deployment.json)提供六项能力开关:project:edit、data:add、processing:run、export:data、plugins:install、settings:manage,用于 kiosk、教室、只读查看等场景;UI Profiles 则是另一套更轻的界面裁剪,两者不互相替代。

12. 性能工程:阈值、懒加载与缓存

这套应用的性能策略基本可以概括为“用明确的数字决定实现路径”:

阈值 值 位置/作用
客户端矢量切片 50,000 要素 LARGE_VECTOR_FEATURE_THRESHOLD
撤销栈上限 500,000 要素 / 200 B 折算 history.ts
撤销合并窗口 400 ms historyCoalesceMs
启动包预算 3 MiB raw / 900 KiB gzip boot-budget.json
SQL 结果渲染 500 行 SQL 工作区
工程导入 25 MiB QGIS/ArcGIS/插件工程
Sedona 结果 50,000 要素 MAX_CEREUS_FEATURES
诊断日志 500 条 / 3000 字符 Diagnostics

启动包预算由 CI 守住,bundle:report 用于定位体积来源;Cesium、Mapbox、ArcGIS 都按需懒加载,代价是首次切换引擎有明显等待。属性表用 @tanstack/react-virtual 只渲染可视行;PWA 用 Workbox 做三层缓存(应用壳、CDN 运行时、瓦片/离线区域)。Linux 上的 WebKitGTK 渲染成本另有一套环境变量与降级策略,属于平台特例而不是通用优化。

13. 安全与隐私的边界

“本地优先”在实现上是一组约束,而不是一句承诺:

  • 数据默认不出浏览器:分析、SQL、WASM 工具都在客户端执行;只有主动添加远程 URL 或分享工程时才发生网络上传。
  • CSP 双处同步:桌面 tauri.conf.json 与 Web docker/nginx.conf 各自维护脚本白名单,tests/tauri-csp.test.ts 会重新推导并校验二者一致,避免只改一边。
  • 凭据不落工程:工程文件只存凭据引用;凭据本身走系统钥匙串或本地加密存储。
  • 本地服务收敛:sidecar 绑定回环地址,浏览器构建通过同源 /sidecar 代理并限制路径;MCP 限制根目录;协作中继的权限与身份是显式模型。
  • 部署能力闸门:能力开关在路由与 UI 两侧同时生效,只读查看模式不会因为前端绕过而获得写权限。

14. 工程质量门

GeoLibre 的 CI 不是“跑一遍测试”,而是一组带棘轮的约束:

  • 前端:node --test 测试套件 + 覆盖率地板 78% lines / 78% branches / 63% functions;
  • 后端:pytest + 覆盖率地板 55%;
  • 端到端:Playwright 分 core/features 两类项目;
  • 静态检查:ESLint 告警上限 242、TypeScript 全量构建、cargo check 校验 Tauri crate;
  • 棘轮:未测模块基线、测试类型错误数、lint 告警数只允许下降;
  • 生成物一致性:Whitebox 菜单目录、i18n 工具元数据、插件文档、示例画廊都必须与源码同步重新生成,否则 CI 失败。

国际化使用 react-i18next,en.json 是唯一源,类型由 i18next.d.ts 约束;RTL 语言要求使用逻辑方向工具类(ms-/me-/ps-/pe-),物理方向类由自定义 lint 规则拦截。发布侧覆盖 Web、Windows、macOS(含沙盒版)、Linux 六种包格式、Android、iOS、Jupyter wheel、R 包与 Chrome 扩展,各平台的裁剪在构建期决定,而不是运行时探测。

15. 局限与适用边界

从源码可以明确读出的限制:

  1. 桌面专属能力:本地文件对话框、本地 MBTiles、本地栅格读取、工程直接落盘依赖 Tauri,Web 端不可用。
  2. 引擎能力不对等:MapLibre 只支持 Web Mercator 类投影;GeoEditor 只在 MapLibre/Mapbox 激活;部分控件在 Cesium/ArcGIS 上需要适配层。
  3. 浏览器资源上限:WASM 工具与 DuckDB 共享浏览器内存,多 GB 数据仍应交给桌面 GIS 或服务端。
  4. 云原生依赖网络条件:没有 CORS 与 Range 支持的对象存储,COG/PMTiles/GeoParquet 的“按需读取”优势无法成立。
  5. 模型构建器无迭代器:批量任务要靠 Batch 或导出的 Python 脚本。
  6. 不是企业级地理数据库:版本化编辑、复制、企业级事务不在范围内。
  7. 移动端与商店沙盒:部分能力(如 sidecar、Earth Engine 登录)在移动端或 Mac App Store 构建中被移除。

这些限制并不影响它作为“浏览器优先的轻量工作台”的定位,但决定了它不适合承担重服务端计算或企业级数据治理。

16. 小结

GeoLibre 的架构可以概括为三层:可序列化的单一状态源(Zustand + .geolibre.json)、按能力抽象的多渲染后端(MapLibre/Mapbox/Cesium/ArcGIS)、按成本分层的执行后端(Turf → Pyodide → sidecar → WASM)。它的工程价值不在于功能数量,而在于把“跨端、多引擎、可撤销、可分享”这四个约束同时放进了一套状态模型里,并用阈值、能力矩阵和 CI 棘轮把边界固定下来。


项目地址:https://github.com/opengeos/GeoLibre

教程地址:https://znlgis.github.io/gis/tutorial/GeoLibre/

posted @ 2026-10-21 11:24  我才是银古  阅读(43)  评论(0)    收藏  举报