第20章 - 制图输出、故事地图与分享嵌入
一张地图做完之后,真正的工作才开始:它要以什么形式离开 GeoLibre?
GeoLibre 给了四条完全不同的出口,各自解决一种”交付”问题:
| 出口 | 入口 | 产物 | 适合 |
|---|---|---|---|
| Print Layout | Project → Print Layout… | PNG / PDF / SVG,可出地图系列 | 报告插图、打印海报、成册图集 |
| 故事地图 | Project → Story Map… | 可交互叙事页 / PDF 讲义 / 独立 HTML | 汇报、教学、公众传播 |
| 分享 | Project → Share… | 一个公开的 .geolibre.json URL |
让别人在浏览器里继续探索 |
| 嵌入 | URL 参数 / @geolibre/embed / postMessage |
iframe 里的实时地图 | 嵌进自己的网站、系统、仪表盘 |
本章按这四条展开,最后补上实时协作与独立 HTML 导出这两个相关能力。
说明:本章基于 GeoLibre 3.4。菜单项、参数名、令牌名、事件名均保留英文原文,叙述用中文。事实来源为仓库
docs/user-guide/print-layout.md、docs/user-guide/storymaps.md、docs/user-guide/embedding.md、docs/user-guide/projects.md、docs/tutorials/sharing-embedding.md、docs/collaboration.md、packages/embed/src/index.ts。
20.1 Print Layout 入门
20.1.1 打开与界面
Project → Print Layout… 打开版式合成器。左边是控件,右边是实时预览。
| 区域 | 内容 |
|---|---|
| 左栏 | 标题、页面、地图框与比例、地图元素、图例编辑器、Atlas |
| 右侧 | Preview:由当前地图的一次截图构建 |
| 中间分隔条 / 对话框角柄 | 拖动可给预览腾空间 |
预览不是矢量重绘,而是”把地图画布抓下来”再排版。所以:
- 平移、缩放、改样式之后,要按 Recapture map 才会更新预览;
- 版式本身(标题、图例、比例尺、表格)始终按输出分辨率重绘,不会糊。
20.1.2 保存
版式随工程保存,重开合成器会带回同一页、同一组元素与 Atlas 设置。一个工程只有一个版式。
提示:需要两套版式(例如 A4 竖版报告图 + A3 横版海报)时,实际做法是复制工程文件再改版式,而不是在同一工程里切换。
20.2 标题与页面
20.2.1 标题
| 控件 | 说明 |
|---|---|
| Title | 默认取工程名 |
| Subtitle | 副标题 |
| Title placement | Outside frame(在地图上方)或 Inside frame(压在地图上) |
| Alignment | Left / Center / Right |
20.2.2 页面尺寸
| 分组 | 预设 |
|---|---|
| Paper | A4(210 × 297 mm)、A3(297 × 420 mm)、Letter(8.5 × 11 in)、Legal(8.5 × 14 in)、Tabloid(11 × 17 in) |
| Screen / digital | Full HD(1920 × 1080 px)、HD(1280 × 720 px)、4K UHD(3840 × 2160 px)、Square(1080 × 1080 px) |
- Custom… 接受 Width 与 Height,单位 px 或 mm;
- Orientation 在 Portrait / Landscape 之间切换(自定义尺寸不提供);
- Margin:Normal、Narrow、None (borderless)。
20.3 地图框、比例尺与打印范围
20.3.1 地图框
| 控件 | 作用 |
|---|---|
| Map background | 地图没有数据处的底色 |
| Map frame color | 地图外框颜色 |
| Map frame width | 外框宽度;填 0 即无框 |
20.3.2 比例尺(1:N)
可以直接输入比例,也可以点 Presets… 从 1:500 到 1:100,000 里选。应用后 GeoLibre 会把实时地图缩放到该比例并重新截图。
两条限制:
- 只在纸张(mm)页、且地图已截图之后可用;
- 如果地图的缩放范围到不了那个比例,合成器会用它能做到的最接近值,并明确告诉你。
20.3.3 打印范围
| 操作 | 说明 |
|---|---|
| Draw extent on map | 在地图上拖一个框来指定打印区域;按住 Shift 让它匹配页面比例 |
| Use active viewport | 用当前视口 |
| Clip to custom extent | 裁到自定义框 |
| Clear extent | 清除 |
注意:裁到自定义范围时,比例尺输入被禁用——范围本身已经决定了比例。这是”范围优先”还是”比例优先”的互斥,不是 bug。
20.4 地图元素清单
在 Map elements 下勾选要放到页上的元素:
| 元素 | 说明 |
|---|---|
| Title / Subtitle | 见 20.2.1 |
| Legend | 由可见图层生成,见 20.5 |
| Scale bar / North arrow | Group north arrow & scale bar 可让两者成组摆放 |
| Date | 导出日期 |
| Include GeoLibre attribution | 加一行 “Created with GeoLibre” |
| Footer text | 页脚自由文本 |
| Page border | 配 Border color 与 Border width |
| Info block (title block) | 工程式标题栏:Author、Project No.、CRS、Revision 与比例 |
| Colorbar | 连续色标:Colormap、Min value、Max value、Label、Orientation、Position、Length |
| Custom legend | 手工图例条目,见 20.5.2 |
| Attribute table | 图层记录表,见 20.6 |
| Chart | 条形/饼图/折线,见 20.6 |
三个必须知道的约束:
- 区块通过 Position 吸附到页面角,没有拖拽自由摆放;
- 没有自由文本框、图片/Logo 元素、内嵌小地图(inset map)、也没有版式网格;
- 地图本身的坐标网格(Controls → Gridlines)会随地图一起被截进去,并且地图会被适配到”边缘标注不被裁掉”。
提示:需要 Logo 或自由排版时,导 SVG 进 Inkscape/Illustrator 继续加(见 20.9)——这是官方推荐的组合,而不是在合成器里硬凑。
20.5 图例编辑器与自定义图例
20.5.1 图例编辑器
勾上 Legend 后,编辑器为每个可见图层(及其分级)列一条。每条可以:
- 改标签(改的是页面上显示的名字);
- 用眼睛按钮显示/隐藏;
- 上下移动。
其它控件:Legend title 设标题,Group classes by layer 把分级嵌到图层下,重置按钮回到自动图例。
注意:编辑器改的是工程里的图例——也就是 Controls → Legend 显示的那一个,两边同步。条目来自图层;要加”不是图层的条目”,用自定义图例。
20.5.2 自定义图例
Custom legend 画一份手作图例:
- 填 Legend title;
- 每行给一个颜色与 Label,用 Add item 增加、叉号删除;
- 设 Position;
- Import from dictionary 用 JSON 批量替换,例如
{"Forest": "#228b22", "Water": "#1e90ff"}。
20.6 数据块:属性表与图表
两者都读要素已加载在应用里的矢量图层(GeoJSON 或其它本地矢量图层)。
20.6.1 属性表
| 参数 | 说明 |
|---|---|
| Layer | 数据源 |
| Heading | 表头文字 |
| Columns | 要显示的列 |
| Sort by / Order | 排序字段与升降序 |
| Max rows | 最多 50 行 |
| Adjust number of rows to fit page | 让行数自动适配页面高度 |
20.6.2 图表
| 参数 | 说明 |
|---|---|
| Chart type | Bar / Pie / Line |
| Category field | 分类字段 |
| Aggregate | Count / Sum / Mean(后两者需要 Value field) |
20.6.3 页面范围过滤
两者都有 Page extent filter:
- All features:全部;
- 只取完全落在页面范围内;
- 只取与页面范围相交的。
在 Atlas 里还可以只显示当前图集要素(见 20.7)。
20.7 Atlas:地图系列
勾 Generate a multi-page map series,版式就变成”按一系列范围重复”的模板。
警告:Atlas 需要平面地图,在 Cesium 球或 ArcGIS 三维场景下不可用。
20.7.1 六个步骤
- 选 Coverage layer:驱动分页的矢量图层;
- 选 Coverage 方式:
- One page per feature:逐个要素取景;
- Pages along a line:沿每条线要素按 Segment length (km) 往前走(默认 20 km)。一个系列上限 5,000 页,撞上限就加大段长;
- 选 Page name field(默认是要素序号);
- 设 Page extent:Margin around feature(百分比),或在纸张页上用 Fixed scale;
- 可选:
- Mask area outside current feature:把当前多边形之外压暗(仅”一要素一页”且图层是多边形);
- Sort by / Order:页序(仅”一要素一页”);
- Filter:限定参与分页的要素,例如
POP2000 > 100000 and ST = "CA";
- 预览里出现 Page X of Y 与前后翻页按钮,翻页会移动实时地图到该页要素。
Filter 语法:比较运算符 =、!=、>、>=、<、<=、contains,用 and 连接。
20.7.2 图集令牌
可用在标题、副标题、页脚,以及 Page file name (ZIP export):
| 令牌 | 值 |
|---|---|
{atlas.name} |
页名 |
{atlas.pagenumber} |
页码 |
{atlas.total} |
总页数 |
{atlas.attr:FIELD} |
当前要素的 FIELD 值 |
沿一条线分页时,另有 km_start、km_end、segment、segments 可用。默认页文件名是 {atlas.pagenumber}-{atlas.name}。
提示:把
{atlas.attr:NAME}写进页脚的”第 {atlas.pagenumber} / {atlas.total} 页 —— {atlas.name}”,再配合 ZIP 导出的{atlas.pagenumber}-{atlas.name},一套图集的文件名与页脚就都自解释了。
20.8 导出:PNG、PDF、SVG 与分辨率
20.8.1 四个按钮
| 按钮 | 输出 | Atlas 下的变化 |
|---|---|---|
| Export PNG | 页面栅格图 | 变成 Export PNG ZIP:每页一个 PNG 打包 |
| Export PDF | 所选页面尺寸的 PDF | 变成 Export PDF (N pages):一个多页 PDF |
| Export SVG | 页面的 SVG | Atlas 下不可用 |
| Copy to Clipboard | 页面作为 PNG 进剪贴板 | — |
文件按标题命名。纸张页 150 DPI;屏幕预设与像素自定义按精确像素。Atlas 导出会逐页报告进度,对话框到全部完成才关闭。
20.8.2 什么矢量,什么栅格
| 内容 | SVG | |
|---|---|---|
| 标题、图例、比例尺、指北针、表格、图表、边框 | 可编辑文本与路径 | 位图 |
| 地图本身、基于图片的标记图标 | 内嵌为图像 | 位图 |
也就是说:PDF 是”整页 150 DPI 的图像放在 PDF 页上”,不是矢量内容;要进 Inkscape/Illustrator 继续编辑,选 SVG。
20.8.3 让地图更清晰
地图是从屏幕上的地图抓的,所以细节上限取决于地图在你屏幕上占多大,与页面尺寸无关。做大尺寸打印前:
所以做大尺寸打印前:最大化窗口(或换高 DPI 屏),再按 Recapture map。文字与其它版式元素本来就在输出分辨率下绘制,不受这条限制。
注意:View → Color vision preview 不影响导出——版式永远按地图真实配色导出。
20.9 故事地图:设置
Project → Story Map 打开构建器。故事保存在 .geolibre.json 里面,所以随工程走。
提示:故事为空时点 Load sample story 会填一个”五城世界之旅”。先点 Present 滚一遍看效果,再改章节——比对着空表单想象快得多。
20.9.1 全局设置
| 设置 | 作用 |
|---|---|
| Title / Subtitle / Byline / Footer | 演示页的头尾信息;Footer 接受内联 HTML(链接与署名) |
| Panel theme | 章节面板的明/暗主题 |
| Show markers | 在每个章节中心放一个标记,可选颜色 |
| Inset minimap | 在指定角显示一个小总览图 |
| Hide chapter list | 演示时移除导航面板(适合一气读完的故事) |
| Enable start view | 第一章之前加一个开场屏,带标题块;背景可选 Empty screen、Black screen、Global view、Preview of first chapter |
| Enable closing slide | 最后一章之后加收尾屏,用于鸣谢或结语;背景选项同上,另有 Hold on last chapter 把相机留在故事结束处 |
20.10 故事地图:章节与图层效果
20.10.1 一章有什么
点 Add chapter 会把当前地图视图捕获为新章节。每章:
| 字段 | 说明 |
|---|---|
| Title | 章标题 |
| Description | 描述,允许内联 HTML |
| Image URL | 可选配图 |
| Panel alignment | left、center、right、full |
| Map animation | flyTo、easeTo、jumpTo |
| Hide panel | 保留地图转场但隐藏文字(纯地图节拍) |
| Rotate camera | 转场稳定后缓慢旋转视角 |
辅助操作:Set to current view 在平移/缩放/倾斜之后重捕相机;地图钉按钮在编辑时飞到某章;箭头排序;垃圾桶删除。
20.10.2 图层效果
在 On enter 与 On exit 下添加”图层淡入淡出”:选一个工程图层、目标不透明度、(可选)过渡时长。这是”随着故事推进揭示/隐藏数据”的机制。
20.10.3 导入导出章节
| 操作 | 说明 |
|---|---|
| Export JSON / Import JSON | 整个故事(设置加章节);一份完整的 .geolibre.json 也能被导入 |
| Export CSV / Import CSV | 一章一行(表格友好);导入 CSV 会替换章节但保留当前故事设置 |
CSV 列:id, title, description, image, alignment, hidden, lng, lat, zoom, pitch, bearing, mapAnimation, rotateAnimation, onChapterEnter, onChapterExit。
- 列按名字匹配,顺序自由,只写你用的;
lng/lat设章节中心,缺的id在导入时生成; onChapterEnter/onChapterExit里是 JSON,手写时可留空。
20.11 故事地图:演示、PDF 讲义与 HTML 导出
20.11.1 演示
点 Present 在实时地图上开始滚动驱动的演示。滚动切换章节,Esc 或 Exit 回到编辑。
- 左侧导航面板列出全部章节,点任意条目直接跳转;用 Exit 旁的列表按钮开关;
- 地图控件(导航、全屏、球面开关、图层控制)在演示中仍可用;
- 每张章节卡可移动、可缩放:拖标题栏移动(双击复位),拖右下角缩放——这样可以把卡片挪开去看底下的地图。
20.11.2 PDF 讲义
Handout (PDF) 把故事导出成多页 PDF,一屏一页:
- 选择要包含的屏;
- 选纸张尺寸与方向;
- 填文档标题、副标题、署名、页脚(每页都印);
- 每章页显示捕获的地图视图、章配图(若有)与章节描述。
Clickable location markers 会在每章位置画一个标记(尖端落在章节坐标上)。在 PDF 阅读器里点标记,会用 Google Maps 打开该坐标——读者可以直接导航到那个地点。标记取故事的 Marker color;当故事启用了 Show markers 时该选项默认开启。
20.11.3 HTML 导出
Export HTML 保存一个自包含的 .html,脱离 GeoLibre 复现整个故事。章节效果引用到的内存 GeoJSON 图层会被内联,所以导出后的行为与站内预览一致。它是一份可以放在任何地方的静态文件(GitHub Pages、Netlify、S3、任意 web 服务器)。
注意:故事编辑与站内演示都在本地跑,但底图样式与章节图片仍从它们的 URL 抓取;独立 HTML 导出会从 CDN 加载 MapLibre 与 scrollama。要”完全离线”的交付,得自己托管这两者,或改用 Print Layout 出静态图。
20.12 分享:账号、快照与就绪检查
20.12.1 分享做了什么
Project → Share… 把当前工程上传到 share.geolibre.app,返回一个公开 URL,可以发给任何人或在实时 viewer 里打开。上传的文件与本地保存的是同一个 .geolibre.json,所以打开链接的人看到相同的图层、样式与地图视图。
要理解它的边界,先记住一句官方定性:GeoLibre 的工程分享是基于快照的(上传到 share.geolibre.app)。
| 含义 | 后果 |
|---|---|
| 上传的是那一刻的工程文件 | 之后再改本地工程,链接不会跟着变;要让收件人看到新版,就再分享一次,得到新 URL |
| 服务器只存工程文件 | 你电脑上的数据文件一个都不上传 |
| 拿到 URL 的人都能打开 | 分享链接是公开可访问的;凭据在出口被剥掉 |
20.12.2 连接账号(三种方式)
| 构建 | 做法 |
|---|---|
| 网页版 | 在 Share 对话框点 Sign in(或 Settings → Environment Variables → Share.GeoLibre account);弹出 share.geolibre.app 的同意页,批准即连上,登录保持到浏览器会话结束;会话过期会提供一键重新登录 |
| GeoLibre Desktop | 同样点 Sign in;同意页在系统浏览器里打开并回到应用;登录存进系统钥匙串,跨重启有效。登录后 Settings → Environment Variables 还能查看与吊销会话 |
| 移动端、notebook 嵌入,以及任何构建的兜底 | 在 share.geolibre.app/settings 的 Settings → API tokens 创建个人 token,粘进 Settings → Environment Variables → Share.GeoLibre API token |
20.12.3 分享就绪检查
工程文件主要是引用,所以一个工程可以上传成功、却在收件人那里什么都画不出来。Share 对话框打开时会检查工程指向的数据源,列出收件人加载不了的那些,并给原因与处理建议:
| 问题类别 | 含义 | 处理 |
|---|---|---|
| 使用了分享时会被移除的凭据 | token/API key 在上传时被剥离,收件人拿到的是没有密钥的 URL | 让服务公开,或让收件人自带 key |
| 浏览器取不到这个主机 | 主机没发跨源(CORS)头,或没有应答 | 这类图层在桌面版仍能用(不受浏览器 CORS 约束),但在浏览器 viewer 里是空的 |
| 服务返回未找到 | 签名的 URL 过期,或文件被移动 | 换新 URL |
| 指向私有或本地网络地址 | 内网服务只对同网段的人解析 | 若收件人恰好在同一内网,这样分享是正常的 |
这套检查在浏览器里、不带你的凭据运行,所以它看到的就是收件人看到的。它从不会阻止上传——把内网地图分享给内网同事是正常需求,列表是用来告知的。
20.12.4 分享本地数据
工程持有的是引用,share.geolibre.app 只存工程文件本身。所以:
- 从本地 GeoTIFF、GeoPackage、Shapefile 或其它磁盘文件加的图层,你自己打开没问题,别人打开什么都没有(收件人在浏览器里,读不到你的磁盘);
- 查询型图层(PostGIS、DuckDB SQL 图层、sidecar 结果)如果没有 URL,同理;
- 位于私有网络地址(
localhost、内网服务器)的图层不一样:它对同网段的同事可能可加载,所以留在上面那张”较软”的就绪列表里,而不是这里的警告里。
发现这类图层时,对话框把它们列在 N layers will be missing from the shared map 下,按钮变成 Share anyway。仍然可以分享,只是那些图层不会出现在分享地图里。要把它们带上:
- 把数据托管到网上:栅格转 COG,矢量转 PMTiles 或 GeoParquet,放到公开 HTTPS 服务器(GitHub、Hugging Face 等),再用 URL 加进地图(Add Data → Raster Layer 或 Vector Layer 填 URL)。瓦片服务、WMS、ArcGIS 服务等托管源只要允许跨源且不需登录,就能照原样用。
- 让小矢量随身走:用 Add Data → Vector Layer 加的本地矢量图层会自动内联进上传文件,所以永远不会出现在这个列表里。大矢量数据集仍建议托管,因为内联图层的每个要素在工程打开时都要被解析一遍。
注意:即使还没配置分享 token,这个对话框也会警告——因为手动把保存好的
.geolibre.json传到share.geolibre.app会以同样的方式静默丢图层。
20.12.5 分享前的一张检查表
- 相机:把视图调到你希望别人第一眼看到的位置(分享的是当前视图);
- 底图:确认底图不依赖只有你有的 key;
- 图层来源:本地文件要么内联(小矢量),要么托管(COG/PMTiles/GeoParquet);
- 凭据:需要鉴权的服务要么改公开,要么在说明里告诉对方自带 key;
- 图例、控件与版本:图例、色带、卷帘都随工程走;内容改了要重新 Share——旧 URL 仍是旧快照。
20.13 嵌入:URL 参数全表
浏览器构建可以嵌进任何网页,并通过查询参数配置。这是把”分享的工程”变成”网站里一张聚焦的实时地图”的方式。
先用 url 打开一个公开工程:
https://web.geolibre.app/?url=https://share.geolibre.app/giswqs/3d-tiles.geolibre.json
完整的参数表:
| 参数 | 示例 | 说明 |
|---|---|---|
url |
url=https://share.geolibre.app/you/project.geolibre.json |
从公开 URL 加载 .geolibre.json 工程 |
loading |
loading=true |
在 document 元素上暴露截图就绪状态;接受裸标志、true、1、yes、on;默认关闭(见 20.19) |
data |
data=https://assets.geolibre.app/data/places.geojson |
加载公开 GeoJSON、GeoParquet、PMTiles、COG,或含多个 GeoJSON 的 ZIP / REST 响应 |
style |
style=https://assets.geolibre.app/data/sample.style.json |
给 data 加载的数据套 GeoLibre/MapLibre 矢量样式或栅格样式 JSON |
dataType |
dataType=lidar |
标记路径没有扩展名的 data URL 是 LiDAR 点云;与 data 按位置配对 |
stac |
stac=https://earth-search.aws.element84.com/v1/collections/naip |
打开已连接到某个 STAC 目录/API/collection 的 STAC Catalogs 浏览器;给 collection URL 时加载即搜索,足迹作为图层 |
layout |
layout=viewer |
viewer 是只读外壳(Layers、View、Controls、底图、搜索/识别、Help 与图层的快捷筛选),创作 UI 隐藏;compact 是图标式的完整应用;embed 与 iframe 是别名 |
draw |
layout=viewer&draw |
让几何编辑器在 layout=viewer 下运行,宿主可提供绘制而不放开其余创作外壳;裸标志或 true、1、yes、on 开启;只在 viewer 布局生效,且需要 project:edit 部署能力 |
toolbar |
toolbar=none |
隐藏顶部工具栏但保留面板与状态栏;icons 为图标按钮;icon、icon-only 是别名;hidden、hide、off 是 none 的别名 |
panels |
panels=collapsed |
让 Layers 与 Style 收成图标条;none 隐藏所有面板;hidden、hide、off 是别名 |
hidePanels |
hidePanels=true |
隐藏这些面板的另一种写法 |
maponly |
maponly |
隐藏所有外壳(工具栏、面板、状态栏),只剩地图;裸标志或 true、1、yes、on 开启 |
welcome |
welcome=0 |
隐藏首次使用向导;接受 0、false、off、no;url= 或 data= 深链本身已自动抑制它 |
theme |
theme=dark |
设初始配色主题,覆盖系统偏好;接受 dark 或 light;应用内开关之后仍可用 |
settingsUrl |
settingsUrl=https://example.com/desktop-settings.json |
在首帧之前加载共享呈现设置;支持 language、layout、theme(强调色)、uiProfile;只影响本页,不替换本地已保存设置;Settings → Interface → Export… 导出的文件可直接用;settingUrl 是别名 |
tool |
tool=adaptive_filter |
按 id 打开 Processing(Whitebox 工具箱)对话框并预选该工具;未知 id 只打开对话框不预选 |
plugin |
plugin=swipe |
激活一个或多个内置插件,效果同从 Plugins 菜单点选 |
参数可以叠加。一个”窄、无壳、暗色”的分享工程:
https://web.geolibre.app/?url=https://share.geolibre.app/you/project.geolibre.json&maponly&theme=dark
20.13.1 私有工程与数据
url= 与 data= 由浏览器带同源凭据抓取,所以受会话 cookie 保护的工程或数据,只有当 GeoLibre 从同一来源提供时才加载得动。把登录保护的 URL 交给 web.geolibre.app 上的托管 viewer 会失败,因为 cookie 不会跨源发送。请自托管应用,或改用签名的、会过期的 URL。
20.14 嵌入:外观、只读语义与绘制
20.14.1 三种外壳
| 参数 | 剩下的东西 | 什么时候用 |
|---|---|---|
layout=viewer |
Layers、View、Controls、底图、搜索/识别、Help;创作 UI 全隐藏 | 读者应能开关图层、探索数据,但不能改 |
layout=compact |
完整创作工具栏,但更紧凑 | 空间小、仍要编辑 |
maponly |
只有地图 | 地图是文章的插图,不需要交互外壳 |
panels=none |
保留工具栏,隐藏侧/底面板 | 只要工具按钮 |
toolbar=none |
保留面板与状态栏,隐藏顶栏 | 自定义外壳 |
20.14.2 layout=viewer 到底禁了什么
只读不是”藏几个按钮”,而是成体系的:
- 图层列表镜像创作态的 Layers 面板(连文件夹一起),组名照搬;带快捷筛选的图层会在其行下列出筛选器,所以地图仍可被”提问”;
- Controls 菜单属于 viewer 外壳,只有两项写工程的功能被去掉(Field Collection 与 GPS Tracking);Record Tour 与 Record Video 只读地图,保留;
- 键盘也受限:工程快捷键(Ctrl/Cmd+N、+O、+S)与命令面板(Ctrl/Cmd+K)随菜单一起消失,而 View 快捷键(
[、]、n、u、r)保留; - 把文件拖到地图上不会导入任何东西;
- 会写工程的插件(几何编辑器、Annotations、GeoAgent)不能处于活动状态,即使工程文件保存时它们是开着的。
所以一个嵌入不能被一次按键、一次拖拽或一个工程文件”带进创作态”。显示类插件(图层控制、底图、时间滑块、图例与色带组件)继续工作,工程看起来与保存时一致。
提示:这也是嵌入可以放心交给外部页面的原因——它的只读性由代码保证,不是靠”把按钮藏起来”。
20.14.3 在 viewer 里允许绘制
默认 layout=viewer 下几何编辑器不能跑。加上 draw,viewer 会放它进来,其余仍是只读:没有菜单、没有 Style 面板、没有拖拽导入,Annotations 与 GeoAgent 仍被挡住。与 plugin=geo-editor 组合可以在加载时打开绘制工具栏:
https://geolibre.example.org/?url=https://share.geolibre.app/you/project.geolibre.json&layout=viewer&draw&plugin=geo-editor
用户画的东西落在 Sketches 图层里。宿主页用 getDrawnFeatures() 读回,并通过 featuresChanged 事件得知变化(见 20.17)。
两条前置条件要说清:
- 绘制本身只需要
project:edit能力; - 读回绘制要走 embed API,所以还需要:你页面的来源在该部署的嵌入允许列表里,且部署授予
export:data能力。
公开的 web.geolibre.app 没有允许列表,所以上面那个例子用的是自托管占位域名;在公开站点上,工具栏能画,但宿主页读不回来。
工程保存时几何编辑器是活动的,则它会照常打开工具栏(无论有没有 plugin=geo-editor)。viewer 里工具栏的 Open/Save 文件按钮被隐藏,Identify 也不提供 Edit geometry / Edit attributes,所以工具栏只画、只改本次会话里画的东西。
draw 不会给出该部署本来不给的东西:一个收紧 project:edit 的部署会忽略它,viewer 保持完全只读。
20.15 深度链接:工具与插件
20.15.1 tool=:直接把某个工具打开到”可运行”
tool=<id> 打开 Processing(Whitebox 工具箱)对话框并预选一个工具。额外的查询参数会按该工具自己的参数名预填表单,所以链接可以”到了就能跑”:
https://web.geolibre.app/?tool=extract_cog_subset&url=https%3A%2F%2Fdata.source.coop%2Fgiswqs%2Fopengeos%2Fdem.tif&bbox_crs=4326
当 tool= 存在时,应用处于 tool 模式:url 指的是工具输入(这里是待裁剪的 COG),而不是要加载的工程,工程加载器让位。上面那些应用/嵌入参数(theme、layout、panels、maponly、locale 等)保持原有含义,绝不传给工具。
预选与预填只对与 Processing 菜单匹配的 id 生效:菜单里没有的 id 仍会打开对话框,但不预选、不应用参数;菜单里有但当前引擎不暴露的 id(浏览器里的 WASM、桌面版的 Python sidecar)同样不预选。工具 id 与 Processing 菜单一致。
20.15.2 plugin=:打开时就带着插件
https://web.geolibre.app/?plugin=swipe
- 用插件 id(
maplibre-gl-time-slider)或短名(去掉maplibre-gl-、maplibre-、geolibre-前缀,即time-slider); - 短名大小写不敏感;
- 多个用逗号分隔,或重复该参数:
?plugin=graticule,h3-grid; - 与
url组合时,插件在分享工程加载完之后打开,叠在工程自己开启的插件之上; - 官方插件市场的插件也能用其注册表 id(
?plugin=openrndt-geolibre)。未安装的兼容条目会提示 Trust and load;确认信任不绕过部署的插件策略,被拒的插件不会加载或激活; - 只读的
layout=viewer里,已安装的市场插件仍会打开(但跳过提示),未安装的永远不会被安装; - 未知名字忽略;插件不支持当前渲染器时不激活;
- Directions 与反向地理编码会把你点的位置发给公共服务器,所以只能从菜单打开,且要先看一次性提示;
- 绘制与编辑类插件在
layout=viewer下保持关闭,唯一例外是链接里带了draw的几何编辑器。
20.16 把地图放进页面
20.16.1 最小 iframe
<iframe
src="https://web.geolibre.app/?url=https://share.geolibre.app/you/project.geolibre.json&maponly"
title="GeoLibre map"
width="100%"
height="600"
style="border: 0;"
loading="lazy"
allow="fullscreen; geolocation"
></iframe>
注意 & 在 HTML 属性里要写成 &;allow 里给 fullscreen 与 geolocation,否则全屏与定位按钮会被浏览器拦下。
20.16.2 “Open in GeoLibre” 徽章
在数据仓库的 README、数据目录、论文补充材料里,一枚 “Open in GeoLibre” 徽章能把数据变成一键打开的地图。徽章图片直接用 shields.io 的 Open%20in-GeoLibre-green.svg,外面套一个指向深链的链接:
https://img.shields.io/badge/Open%20in-GeoLibre-green.svg
https://web.geolibre.app/?data=https://assets.geolibre.app/data/places.geojson
打开分享工程而不是单个文件,就把 data= 换成 url=。同一枚徽章在 reStructuredText 里写成 .. image:: 指令、在 HTML 里写成 <a> 包 <img>,目标 URL 不变。
可以继续加参数:&style= 带样式、&theme=dark、&maponly。
注意:徽章常驻在 README 里,而 GitHub、PyPI、文档站各自对相对链接的改写方式不同,所以目标 URL 一定要写成绝对地址。编码规则不变:数据 URL 自带查询串时,整个值仍要
encodeURIComponent,因为&会在浏览器看到之前就被 GeoLibre 当成自己的分隔符。
20.17 postMessage 协议(v2)
URL 参数只在加载时配置一次。要和活的嵌入持续对话(跳到用户刚点的记录、高亮它、打开某个处理工具),并听取用户在地图里的动作,用 embed 的 postMessage API。
20.17.1 消息形状
两个方向的每条消息都带版本:
{ "v": 2, "type": "setView", "payload": { "center": [-95.7, 37.1], "zoom": 5 } }
应用发出的消息还带 "source": "geolibre",方便你从页面上其它 postMessage 流量里筛出来。协议 v2 是当前版本;GeoLibre 继续接受 v1 请求信封,并对 v1 宿主回 v1 事件。
20.17.2 宿主 → GeoLibre
| Type | Payload | 效果 |
|---|---|---|
loadProject |
{ url } |
加载 .geolibre.json 而不重载 iframe |
setView |
{ bbox } 或 { center, zoom, bearing, pitch, duration } |
适配范围,或把相机飞到给出的属性 |
highlightFeature |
{ layerId, featureId \| featureIds \| filter, fit } |
选中并高亮要素;filter 按属性匹配;fit 缩放到它们 |
openTool |
{ id, params } |
打开处理对话框并预填 params;?tool= 的运行时孪生 |
setLayerVisibility |
{ layerId, visible } |
显示/隐藏工程图层 |
listLayers |
{} |
图层摘要放在应答的 result 里 |
setFilter |
{ layerId, expression } |
应用 MapLibre 过滤表达式;传 null 清除 |
getViewport |
{} |
当前相机与范围放在 result |
setRenderer |
{ renderer } |
切换渲染器 |
getRenderer |
{} |
活动渲染器放在 result |
addLayer |
{ spec } |
运行时加一个工程格式的图层规格 |
addData |
{ url, styleUrl?, fit? } |
不重载 iframe 加载 GeoJSON/API、ZIP、GeoParquet、PMTiles 或 COG |
exportImage |
{} |
已渲染地图作为 PNG data URL 放在 result |
getLayerFeatures |
{ layerId } |
图层 GeoJSON 要素数组放在 result |
getDrawnFeatures |
{} |
所有 Sketches 图层的要素放在 result |
细节:
- 只给
{ layerId }调highlightFeature是清除高亮;一个指名了要素(或 filter)但没匹配到的请求会被拒绝而不是当作清除,避免打错 id 时静默清掉用户的选择; - 高亮从工程里读要素,所以只对以 GeoJSON 保存的矢量图层有效,不适用于要素只存在瓦片源里的图层;
setFilter会先经 MapLibre style spec 编译再存;addLayer要求地图真能读的源:url、非空tiles,或——对geojson、deckgl-viz这两种从内联要素绘制的图层——内联的geojson。两种情况都在ack里报错,而不是”报告成功却什么都不画”;addLayer拒绝源上的javascript:、vbscript:、data:、file:、blob:URL;pmtiles://这类自定义协议没问题;addData与?data=用同一套格式探测、CORS 要求、安全限制与可选的 Mapbox/MapLibre 样式导入;默认适配到新数据,传fit: false保留相机;应答返回所有创建的图层 id(含 ZIP 里解出的每一层)。
给任意消息加 requestId,应用就会回一个 ack 报告成功与否。
20.17.3 GeoLibre → 宿主
| Type | Payload | 何时触发 |
|---|---|---|
ready |
{ version } |
应用挂载完成、开始监听 |
ack |
{ requestId, ok, error, result } |
带 requestId 的消息被应用(或被拒) |
projectLoaded |
{ url, name, layerIds } |
工程加载完成,无论谁发起 |
selectionChanged |
{ layerId, featureIds } |
用户(或你的 highlightFeature)改变了选择 |
rendererchange |
{ renderer } |
渲染器变了 |
viewChanged |
{ bbox, center, zoom, bearing, pitch } |
相机移动(节流到约每秒四次) |
toolCompleted |
{ id, name, status, engine, durationMs, outputLayerNames } |
一次处理运行结束,无论成败 |
serverFileWritten |
{ path, toolId } |
基于文件的工具写出结果(转换与栅格工具) |
featuresChanged |
{ layerId, featureCount, removed? } |
图层的要素被替换或移除(去抖,见下) |
20.17.4 读回要素
addLayer 把要素推进地图,getLayerFeatures 与 getDrawnFeatures 把它们读出来,所以宿主能保存用户画/改的东西。它们与插件 API 的只读查询、Python 组件的 get_features / get_drawn_features 对应。
| 方面 | 规则 |
|---|---|
| 参数 | getLayerFeatures 取 { layerId }(非空字符串);getDrawnFeatures 不带参数,返回所有 Sketches 图层合并的要素;没画过就是空数组,不是错误 |
| 结果 | WGS84 GeoJSON 要素数组;id 原样返回(字符串还是字符串,数字还是数字),便于 join;是一份副本,改它不影响地图;它反映应用持有的内容——本身是截断子集(如 SQL 或 Iceberg 查询结果)的图层,给的就是那个子集 |
| 绘制 | Sketches 图层里的要素带绘制编辑器的记账属性(如 __gm_shape);圆与文字返回为 Point 要素,这些属性是原始形状的线索 |
| 错误 | 未知 layerId 被拒绝(No layer with id "…");没有内存要素的图层(栅格、瓦片、PMTiles、远程矢量源)返回空数组——所以空数组的含义是”没有内存要素”,光看 listLayers 的 type 分不出这两种情况;被移除的图层与未知的一样被拒 |
| 能力 | 需要 export:data(数据要离开应用)。被拒时抛 Missing export:data capability,featuresChanged 也不会发 |
featuresChanged 告诉你什么时候去读。它在一个图层的内存要素集合被替换时触发:绘制(包括第一次画、那时 Sketches 图层才被创建)、编辑、属性编辑、刷新、迟到的加载。它可能在没有内容变化时也触发,且不会为整份工程加载而发(那用 projectLoaded)。它对宿主自己用 addLayer / addData 创建的图层也会触发,所以别把每个事件都当新图层写回去,否则会循环。
- 一个原本有要素的图层被移除时,立刻发
{ layerId, featureCount: 0, removed: true }; - 图层还在但失去内存要素时,报
featureCount: 0(没有removed); - 事件按图层去抖:一次拖顶点会在一连串变化停止后 250 ms 发出一个事件;
- 在渲染器交接前 250 ms 内做的编辑可能被并入新基线而不上报。
map.on("featuresChanged", async ({ layerId, featureCount, removed }) => {
if (removed) {
removeFromBackend(layerId); // 图层没了,getLayerFeatures 会被拒
return;
}
console.log(`${layerId} 现在有 ${featureCount} 个要素`);
const features = await map.getLayerFeatures(layerId);
saveToBackend(layerId, features);
});
const drawings = await map.getDrawnFeatures(); // 所有 Sketches 图层合并
20.18 @geolibre/embed 类型化客户端与宿主页
20.18.1 安装与连接
@geolibre/embed 是一个零依赖 ESM 包,每次 GeoLibre 发布都发到 npm,版本与 app 对齐:
npm install @geolibre/embed
connect(iframe, options) 返回一个 promise,在应用发来 ready 时解析,所以不用自己写握手。它还会给每条命令打 requestId,并用对应 ack 结算该命令的 promise——这就是多条命令可以同时在飞而不串答案的原因。
import { connect } from "@geolibre/embed";
const map = await connect(iframe, {
origin: "https://web.geolibre.app", // 承载 iframe 的精确来源
timeoutMs: 15_000, // 等 ready,默认 15 秒
requestTimeoutMs: 15_000, // 等每次 ack,默认 15 秒
});
origin 必填且必须是 http(s) 来源:它既是每条出站消息的目标,也是入站消息的过滤器,所以别的 frame 或来源发来的消息一律忽略。传应用的来源,不是你自己页面的。
20.18.2 方法表
| 方法 | 解析为 | 说明 |
|---|---|---|
loadProject(url) |
void |
不重载 iframe 换工程 |
setView(target) |
void |
{ bbox },或 { center, zoom, bearing, pitch, duration } 里的任意项 |
highlightFeature({ layerId, … }) |
void |
featureId、featureIds 或 filter;fit: true 缩放到匹配项 |
openTool(id, params?) |
void |
?tool= 的运行时孪生 |
setLayerVisibility(layerId, visible) |
void |
显示/隐藏工程图层 |
listLayers() |
LayerSummary[] |
每层 { id, name, type, visible, opacity } |
setFilter(layerId, expression) |
void |
MapLibre 过滤表达式,null 清除 |
setRenderer(renderer) |
void |
见下 |
getRenderer() |
活动渲染器 | "maplibre"、"mapbox"、"cesium"、"arcgis" |
getViewport() |
Viewport |
{ bbox, center, zoom, bearing, pitch } |
addLayer(spec) |
新图层 id |
接受工程格式的图层规格 |
addData(url, options?) |
新图层 id 数组 |
像 ?data= 一样加载远程数据;options 是 { styleUrl, fit } |
exportImage() |
PNG data: URL |
当前渲染结果 |
getLayerFeatures(layerId) |
Feature[] |
图层要素 |
getDrawnFeatures() |
Feature[] |
用户的绘制(所有 Sketches 图层) |
on(event, listener) |
取消订阅函数 | 不是 promise;事件见 20.17.3 |
disconnect() |
不是 promise | 移除监听并拒绝所有在飞的请求 |
每条命令都是”失败就 reject”,绝不会假成功:带 ok: false 的 ack 会用应用自己的错误消息 reject;requestTimeoutMs 内没有回音也会 reject。connect 自身在 timeoutMs 内没等到 ready 就 reject——最常见的原因是部署没有把你的来源列进允许列表,或 origin 与 iframe 不匹配。
拆 iframe 时(React effect 清理、路由切换)要调 disconnect():它停止 message 监听并拒绝所有在飞命令,避免 promise 挂到页面寿命结束。
20.18.3 启用运行时 API
API 默认关闭。主配置是 deployment.json 里的 sharing.embedOrigins:
{
"version": 1,
"sharing": {
"embedOrigins": ["https://portal.example.com", "https://erp.example.com"]
}
}
Docker 用环境变量覆盖:
docker run --rm -p 8080:80 \
-e GEOLIBRE_EMBED_ORIGINS="https://portal.example.com,https://erp.example.com" \
ghcr.io/opengeos/geolibre:latest
静态构建时,旧的 VITE_GEOLIBRE_EMBED_ORIGINS 作为后备仍被尊重。策略里的来源是精确的 scheme://host[:port],JSON schema 不接受尾随路径;环境变量输入保留原有的尾随路径归一化。
允许列表双向生效:来自未列出来源的消息被忽略,应用发出的每条消息也只投给已列出的来源(配 * 时,在宿主首条消息识别出自己之前出站消息发给 *——这也是”请把来源写清楚”的又一个理由)。
设定允许列表还会把 ?embed=1 的工程/脚本桥(Python 包用的那套)收窄到同一组来源。要额外加固,可以在反向代理加 Content-Security-Policy: frame-ancestors <你的来源>,彻底禁止别的站点框住应用。
允许列表决定谁能发命令;要收窄有哪些命令,用运行时策略里的 部署能力:被拒的命令会以 Missing <capability> capability 明确报错,而不是静默无效。
| 命令 | 需要的能力 |
|---|---|
loadProject |
project:edit |
addLayer、addData |
data:add |
openTool |
processing:run |
exportImage、getLayerFeatures、getDrawnFeatures |
export:data |
setView、highlight、图层可见性 |
无需(永远可用) |
20.18.4 一个宿主页(不依赖 npm)
<iframe
id="map"
src="https://gis.example.com/?url=https://erp.example.com/fields.geolibre.json&maponly"
title="GeoLibre map"
width="100%"
height="600"
style="border: 0"
></iframe>
<script>
const frame = document.getElementById("map");
const APP_ORIGIN = "https://gis.example.com";
const send = (type, payload) =>
frame.contentWindow.postMessage({ v: 2, type, payload }, APP_ORIGIN);
window.addEventListener("message", (event) => {
if (event.origin !== APP_ORIGIN) return;
const message = event.data;
if (message?.source !== "geolibre" || message.v !== 2) return;
if (message.type === "ready") {
// 这时才可以开始发命令
} else if (message.type === "selectionChanged") {
showRecordFor(message.payload.featureIds[0]);
}
});
// 点你自己 UI 里的一条记录:飞过去并高亮,不重载
function focusField(field) {
send("setView", { bbox: field.bbox });
send("highlightFeature", {
layerId: "fields",
filter: { parcel_id: field.id },
fit: true,
});
}
</script>
发之前先等 ready:在应用挂载之前到达的消息不会排队。把 ready 当作幂等事件,因为应用每次重新挂载(frame 内导航、开发热重载)都会重发它。
提示:
setRenderer只确认选择,新画布是异步挂载的——等下一个ready事件再发相机或截图命令;exportImage()支持全部四种渲染器。
20.19 远程数据、STAC 与截图就绪
20.19.1 data= 能打开什么
公开 GeoJSON、GeoParquet、PMTiles、COG、LiDAR 点云(LAS、LAZ、COPC,或 EPT 的 ept.json),以及含一个或多个 .geojson/.json FeatureCollection 的 ZIP。ZIP 里每个 GeoJSON 变成独立图层。可选 style URL 给矢量数据套 Mapbox/MapLibre 样式 JSON:
https://web.geolibre.app/?data=https://assets.geolibre.app/data/places.geojson&style=https://assets.geolibre.app/data/sample.style.json
重复 data 可在同一张图上加载多份独立数据;按同样顺序重复 style 逐份配色,前面的用空 style= 占位:
https://web.geolibre.app/?data=https://example.com/roads.geojson&data=https://example.com/dem.tif&style=&style=https://example.com/dem.style.json
GeoLibre 会加载每一项,并只按它们存储范围的并集适配一次地图。样式只作用于同位置的 data。
20.19.2 REST、ZIP 与 ArcGIS 式服务的细节
data也可以指向返回 GeoJSONFeatureCollection或含多个 GeoJSON 的 ZIP 的 REST 端点;ZIP 响应靠Content-Type/Content-Disposition或 ZIP 文件签名识别,端点不必以.zip结尾。端点自带查询参数时才需要百分号编码。- GeoParquet 走 DuckDB 矢量读取器;PMTiles 用 HTTP range 流式读取。矢量 PMTiles 的样式在 source layer 被发现后应用;栅格 PMTiles 可以加载,但不接受 MapLibre 矢量样式。
- 远程服务器必须允许浏览器跨源请求(CORS);COG、GeoParquet、PMTiles 与 COPC 服务器还应支持 HTTP 字节范围请求。
20.19.3 栅格样式 JSON
对 COG,style 可以指向一个栅格样式 JSON 对象,支持字段:mode(single、rgb、index)、1 基的 bands、rescale 区间、colormap、reversed、nodata、opacity、gamma、stretch(linear、log、sqrt),以及归一化差值 index 预设:
{
"mode": "single",
"bands": [1],
"rescale": [[0, 1000]],
"colormap": "viridis",
"reversed": false,
"nodata": "auto",
"opacity": 0.85,
"gamma": 1,
"stretch": "linear"
}
把它托管成 dem.style.json 后两个 URL 一起传。
提示:这份 JSON 不用手写。打开矢量图层的 Layer actions → Styles → Export GeoLibre URL style,导出的
.geolibre.style.json只有符号化、没有要素数据,且渲染图层的source已经设成原 GeoJSON 的文件名主干。ZIP 多文件时,按每个图层各导一份,把layers与sources合并成一个文档。
20.19.4 STAC 目录深链
stac= 打开已连接的 STAC Catalogs 浏览器,接受静态目录、STAC API 根,或 API 里的一个 collection:
https://web.geolibre.app/?stac=https://earth-search.aws.element84.com/v1/collections/naip
collection URL(…/collections/{id})会通过它的 root 链接找到所属 API、选中该 collection 并立即搜索:地图适配 collection 范围,匹配项的足迹加成 STAC search footprints 图层,之后可从结果列表把某项资产(COG、GeoJSON、GeoParquet、PMTiles、Zarr)加成图层。给目录或 API 根则只连接,搜索留给你。
STAC URL 自带查询串时用 encodeURIComponent 编码。服务器必须允许跨源。目录面板是侧栏,所以 stac 在 layout=viewer 下无效(viewer 不能加图层),且会被 maponly 或 panels=none 隐藏。
20.19.5 截图就绪信号
给工程链接加 &loading=true 可以在不往截图里加可见浮层的前提下拿到机器可读的就绪信号:
https://web.geolibre.app/?url=https://share.geolibre.app/giswqs/national-land-cover-database-nlcd.geolibre.json&maponly&loading=true
<html> 元素暴露三个属性:
| 属性 | 值 |
|---|---|
data-geolibre-load-state |
loading、ready 或 error |
data-geolibre-load-pending |
待完成图层名(或初始化工作)的 JSON 数组 |
data-geolibre-load-errors |
失败消息的 JSON 数组 |
ready 的完整含义:工程/数据 URL 已加载、可见图层已挂载、其当前视口瓦片已加载、相机已停止、浏览器字体已加载。这些条件要在动画帧之间持续满足 500 ms。改变视图或图层会把信号打回 loading。隐藏图层(含隐藏组)、完全透明图层、超出缩放范围的图层不阻塞就绪。它不会下载整个数据集或视口之外的瓦片。
支持范围:原生 MapLibre 图层、栅格控件的 COG 图层、deck.gl 可视化图层(含共享的 deck.gl 渲染器)。没有就绪探针的自定义渲染器——Cesium、LiDAR、Zarr、splats、video——会明确报错而不是假定就绪。地图或瓦片能自愈的失败不阻塞 ready;永不自愈的失败会与超时一起上报,任何 120 秒内没结束的加载也一样。截图前先看 errors;embed API 的 ready 事件与浏览器的 networkidle 都不能证明渲染就绪。
Playwright 例子:
await page.setViewportSize({ width: 1600, height: 1000 });
await page.goto(projectLink + "&maponly&loading=true");
await page.waitForFunction(
() => ["ready", "error"].includes(document.documentElement.dataset.geolibreLoadState),
undefined,
{ timeout: 150_000 },
);
const result = await page.evaluate(() => ({
state: document.documentElement.dataset.geolibreLoadState,
errors: JSON.parse(document.documentElement.dataset.geolibreLoadErrors || "[]"),
}));
if (result.state !== "ready") throw new Error(result.errors.join("; "));
await page.screenshot({ path: "map.png" });
20.20 实时协作
Project → Collaborate… 启动或加入一个实时会话:多人同时编辑同一工程,带在线光标、聊天与逐人权限。
警告:该功能状态是实验性 MVP,并且默认关闭——除非构建配置了
VITE_GEOLIBRE_COLLAB_URL。
20.20.1 同步什么
| 内容 | 说明 |
|---|---|
| 工程状态 | 图层、图层组、样式、底图、地图视图(相机),以整份工程快照广播 |
| 在线状态(presence) | 每人的实时光标位置、视口矩形,加名字与颜色;临时,从不持久化 |
| 聊天 | 短文本,可附一个地图坐标;给迟到者转发有界的近期历史;从不写进工程 |
| 权限 | 主持人对每个参与者的”只读/可编辑”覆盖,在参与者列表上以 editOverride 广播 |
| 评论 | 地图评论的增/回复/解决/删除,以 comment-mutation 帧转发,应用到各端 store |
20.20.2 架构与取舍
不是 P2P 网格,而是一个集中式中继(Cloudflare Durable Object):它持有最新快照,所以迟到者立刻被引导;并把每条消息分发给其它 socket。
不用 CRDT/WebRTC 的理由很实际:store 本来就是唯一事实源,serializeProject/parseProject 已产出校验过的规范线上格式,协作适配器只是换成 WebSocket;而整快照 + 后写覆盖的一致性显而易见——中继最后看到的快照赢。按变更合并要引入逐字段时钟,CRDT(Yjs/Automerge)还会带来可观的 bundle 与第二个事实源。
20.20.3 权限与身份
- 匿名加入:主持人起会话并分享代码/链接,加入者取一个显示名与颜色;
- 会话模式:view-only(访客只能看与显示在线状态,其快照推送在服务端被拒)或 co-edit(拿到链接的人都能改);
- 逐人覆盖:主持人可把某个访客钉为只读/可编辑。有效编辑权在客户端与中继上按同一规则计算:主持人永远可编辑;否则覆盖优先;否则看会话模式;
- 主持人 token(只返回给创建者)把守
set-mode与set-participant-mode,所以访客无法自行提权; - 可选登录身份:加入者可带
identityToken,中继会先验证其 HMAC-SHA256 签名才填写participant.identity。身份默认关闭,除非部署方设置COLLAB_IDENTITY_SECRET;未设置时requireIdentity无法开启,GET /health与每个welcome都报identitySupported: false。
20.20.4 聊天
chat { text, coordinate? } 在服务端校验(非空、去空白、上限 2000 字符;坐标走与 presence 相同的有限数清洗器)后分发给包括发送者在内的所有参与者,所以中继的顺序是唯一事实源(客户端按回显渲染,不做乐观更新)。每条消息带服务端分配的 id 与 ts。中继保留有界、持久的历史(最近 50 条),迟到者通过 welcome.chat[] 看到近期对话。
聊天对所有会话成员开放(包括只读访客);只有工程编辑受限。一条消息可以附上发送者当前地图中心作为可点击坐标,点它会把收件人的地图移过去。聊天常驻在画布上的状态徽章里,所以边看图边聊;它是临时的,从不写进工程文件。
20.20.5 限制(必须知道)
| 限制 | 说明 |
|---|---|
| 撤销互扰 | 远端快照经 loadProject 应用,而它最后会 clearHistory():这能让远端编辑不进本地撤销栈,但也意味着协作者的编辑会清空你的撤销历史 |
| 封禁不是安全边界 | block-participant 记录”参与者键”(user:<userId>、invite:<token> 或 anon:<clientId>)。匿名访客重连就能回来,因为每次加入都拿到新的 clientId;要可执行的封禁,必须要求邀请或登录身份 |
| 快照大小 | 超过 10 MB(默认)的快照被拒,报 too-large;这个上限低于 Cloudflare 单条 WebSocket 消息 32 MiB 的天花板 |
| 仅桌面中继可用 | 网页版(JupyterLite)没有可挂的服务器;协作需要中继 |
| 评论与权限是 MVP 增量 | 协议里已有,但行为以当前实现为准 |
20.20.6 部署开关
| 场景 | 做法 |
|---|---|
| 构建态 | 设 VITE_GEOLIBRE_COLLAB_URL 为中继基址(如 wss://collab.geolibre.app,本地 wrangler dev 用 ws://127.0.0.1:8787)。未设置时 hook 惰性、所有协作 UI 隐藏,生产构建可以把这个功能留成暗的 |
| Docker 运行时 | -e GEOLIBRE_COLLAB_URL=…;入口会校验它、写进 geolibre-runtime-config.js,resolveCollabBaseUrl() 优先用它。不是 wss://(或回环上的 ws://)会让容器启动失败,而不是静默留暗 |
| 桌面 CSP | Tauri 的 connect-src 必须列上 wss 主机(现有的 https: 指令不授权 wss:);换中继要改 apps/geolibre-desktop/src-tauri/tauri.conf.json 并重建 |
| 自托管中继 | workers/collab-node 是可自托管的 Node/SQLite 中继,在根 docker-compose.yml 里是 geolibre-collab 服务,数据库挂在 /data/collab.sqlite;可调 COLLAB_MAX_SNAPSHOT_BYTES(compose 默认 1,000,000)与 COLLAB_IDLE_TTL_MS(默认 2 小时) |
20.21 独立 HTML 导出
除了故事地图的 Export HTML,工程本身也能整体导出:
Project → Export → Export as HTML… 把整个工程写成一个独立 HTML 文件,无需服务器即可离线运行。放在任何地方托管,或直接从磁盘打开。
三个相关出口要分清:
| 出口 | 产物 | 依赖 | 适合 |
|---|---|---|---|
| Project → Export → Export as HTML… | 单文件 HTML,含工程 | 离线可跑;远程图层仍需网络 | 交付一个”双击就开”的地图 |
| 故事地图 → Export HTML | 单文件故事页 | 从 CDN 加载 MapLibre 与 scrollama;底图与章图仍远程 | 滚动叙事 |
MCP export_html |
单文件 HTML,内嵌托管 viewer 并注入工程 | 需要能访问 app_url |
由 AI 客户端生成交付物 |
注意:MCP 导出的页面把工程 POST 给
app_url指定的来源(默认托管 viewer),凭据已剥离但工程其余内容会过去——见 第18章 的信任边界说明。Project → Export 这条本地导出没有这个问题。
20.22 本章小结
- 四条出口,四种交付:Print Layout 出静态成品,故事地图出叙事,分享出可探索的链接,嵌入出网站里的实时地图。
- Print Layout 是”截图 + 排版”:改完地图要 Recapture map;纸张页 150 DPI、像素页按精确像素;PDF 是位图,SVG 才是矢量的。Atlas 用四个令牌(
{atlas.name}、{atlas.pagenumber}、{atlas.total}、{atlas.attr:FIELD})与一个 Filter 语法(=、!=、>、>=、<、<=、contains加and)。 - 故事地图存在工程里:设置、章节、图层效果都进
.geolibre.json;章节可以 CSV 批量编辑;HTML 导出会内联章节效果引用的内存 GeoJSON。 - 分享是快照:上传那一刻的工程文件,之后要新版就再分享;本地数据不会上传,对话框会列出”哪些图层在别人那里是空的”。嵌入则靠参数起步(
url/data/layout/maponly)、靠postMessage持续。两条信任边界要记牢:sharing.embedOrigins决定谁能发命令,部署能力决定有哪些命令;export_html的app_url决定工程会发给谁。 - 截图就绪要显式开启:
loading=true给出data-geolibre-load-state,且ready只保证”当前视口”就绪,不是整个数据集。 - 协作是实验性的:整快照后写覆盖、远端编辑会清空本地撤销历史、匿名封禁可绕过。
20.23 实践练习
练习 1(Print Layout 三种输出) 用一张含图例与比例尺的地图,做一份 A4 横版版式:标题放 Inside frame、加比例尺与指北针、加 Info block。分别导出 PNG、PDF、SVG,用 Inkscape 打开 SVG,确认标题、图例、表格是可编辑文本,而地图是内嵌图像。解释为什么 PDF 不能这样编辑。
练习 2(Atlas 图集)
选一个省级面图层做 One page per feature 图集:页脚写 “第 {atlas.pagenumber} / {atlas.total} 页 —— {atlas.name}”,加 Mask area outside current feature,并导出 PNG ZIP。检查文件名是否遵循 {atlas.pagenumber}-{atlas.name},再解释为什么裁到自定义范围时比例尺输入会被禁用。
练习 3(故事地图) 用 Load sample story 起步,改成你自己的 5 章故事:每章设不同的 Panel alignment,加一次 On enter 图层淡入,设 Enable start view 为 Preview of first chapter。导出 CSV 检查列名,再导出 Handout (PDF) 与 Export HTML,对比两者在没有 GeoLibre 的环境里如何打开。
练习 4(分享的就绪检查) 建一个含”一个本地 Shapefile 图层 + 一个托管 COG + 一个公开 GeoJSON”的工程,打开 Share 对话框,记录它列出哪些问题、按钮为什么变成 Share anyway。然后按建议把 Shapefile 转成 PMTiles 并用 URL 加回来,再看一次就绪列表。
练习 5(嵌入参数矩阵)
用同一个分享工程做四个 iframe:maponly、layout=viewer、toolbar=none、panels=collapsed&theme=dark。记录每个版本里”图层能否开关、能否拖入文件、Ctrl/Cmd+K 是否可用、图层组名是否显示”。说明 layout=viewer 的只读性为什么不能靠按键绕过。
练习 6(运行时 API 与截图)
在自托管部署上配置 sharing.embedOrigins(或 Docker 的 GEOLIBRE_EMBED_ORIGINS),用 @geolibre/embed 写一个宿主页:connect、setView、highlightFeature、监听 selectionChanged。再用 Playwright 配合 loading=true 截一张”就绪后”的图,并故意把 origin 写错一次,记录 connect 的报错。