第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 会把实时地图缩放到该比例并重新截图。

两条限制:

  1. 只在纸张(mm)页、且地图已截图之后可用;
  2. 如果地图的缩放范围到不了那个比例,合成器会用它能做到的最接近值,并明确告诉你。

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

三个必须知道的约束:

  1. 区块通过 Position 吸附到页面角,没有拖拽自由摆放;
  2. 没有自由文本框、图片/Logo 元素、内嵌小地图(inset map)、也没有版式网格;
  3. 地图本身的坐标网格(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 画一份手作图例:

  1. 填 Legend title;
  2. 每行给一个颜色与 Label,用 Add item 增加、叉号删除;
  3. 设 Position;
  4. 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 六个步骤

  1. 选 Coverage layer:驱动分页的矢量图层;
  2. 选 Coverage 方式:
    • One page per feature:逐个要素取景;
    • Pages along a line:沿每条线要素按 Segment length (km) 往前走(默认 20 km)。一个系列上限 5,000 页,撞上限就加大段长;
  3. 选 Page name field(默认是要素序号);
  4. 设 Page extent:Margin around feature(百分比),或在纸张页上用 Fixed scale;
  5. 可选:
    • Mask area outside current feature:把当前多边形之外压暗(仅”一要素一页”且图层是多边形);
    • Sort by / Order:页序(仅”一要素一页”);
    • Filter:限定参与分页的要素,例如 POP2000 > 100000 and ST = "CA";
  6. 预览里出现 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
标题、图例、比例尺、指北针、表格、图表、边框 可编辑文本与路径 位图
地图本身、基于图片的标记图标 内嵌为图像 位图

也就是说: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,一屏一页:

  1. 选择要包含的屏;
  2. 选纸张尺寸与方向;
  3. 填文档标题、副标题、署名、页脚(每页都印);
  4. 每章页显示捕获的地图视图、章配图(若有)与章节描述。

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。仍然可以分享,只是那些图层不会出现在分享地图里。要把它们带上:

  1. 把数据托管到网上:栅格转 COG,矢量转 PMTiles 或 GeoParquet,放到公开 HTTPS 服务器(GitHub、Hugging Face 等),再用 URL 加进地图(Add Data → Raster Layer 或 Vector Layer 填 URL)。瓦片服务、WMS、ArcGIS 服务等托管源只要允许跨源且不需登录,就能照原样用。
  2. 让小矢量随身走:用 Add Data → Vector Layer 加的本地矢量图层会自动内联进上传文件,所以永远不会出现在这个列表里。大矢量数据集仍建议托管,因为内联图层的每个要素在工程打开时都要被解析一遍。

注意:即使还没配置分享 token,这个对话框也会警告——因为手动把保存好的 .geolibre.json 传到 share.geolibre.app 会以同样的方式静默丢图层。

20.12.5 分享前的一张检查表

  1. 相机:把视图调到你希望别人第一眼看到的位置(分享的是当前视图);
  2. 底图:确认底图不依赖只有你有的 key;
  3. 图层来源:本地文件要么内联(小矢量),要么托管(COG/PMTiles/GeoParquet);
  4. 凭据:需要鉴权的服务要么改公开,要么在说明里告诉对方自带 key;
  5. 图例、控件与版本:图例、色带、卷帘都随工程走;内容改了要重新 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)。

两条前置条件要说清:

  1. 绘制本身只需要 project:edit 能力;
  2. 读回绘制要走 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&amp;maponly"
  title="GeoLibre map"
  width="100%"
  height="600"
  style="border: 0;"
  loading="lazy"
  allow="fullscreen; geolocation"
></iframe>

注意 & 在 HTML 属性里要写成 &amp;;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 也可以指向返回 GeoJSON FeatureCollection 或含多个 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 的报错。