第15章 - 矢量空间分析
矢量的”分析”和”显示”是两种完全不同的活。显示只要几何加上样式,分析要回答的是关系:谁在谁里面、谁离谁多远、两期数据在哪儿变了。GeoLibre 把这类问题收在 Processing → GeoLibre Toolbox → Vector 这一个对话框里——34 个工具,分七组,每个工具都有一个引擎选择器。
本章基于 GeoLibre 3.4。工具 id、参数 id、默认值与上限全部取自 packages/processing/src/vector-tools.ts、vector-tool-registry.ts、network-tools.ts、topology-tools.ts 与 statistics-tools.ts,并与 docs/user-guide/processing.md 的表格逐项核对。菜单与控件名保留英文原文。
15.1 两条实现路径与三个引擎
15.1.1 一个工具、三个引擎
同一个工具可以在三套代码上跑,对话框里叫 Engine:
| 引擎 | 实际执行者 | 安装要求 | 离线可用 | 处理投影 |
|---|---|---|---|---|
| Client (Turf.js) | 浏览器内的 Turf.js,吃图层的 GeoJSON | 无 | 是 | 不处理,直接在经纬度上算 |
| Sidecar (GeoPandas) | 本地 Python sidecar 的 GeoPandas + Shapely | 桌面版 + sidecar 的 vector extra |
是 | 会重投影后再算 |
| Python (Pyodide) | 浏览器里的 Pyodide,跑同一份 GeoPandas 代码 | 首次下载几十 MB 的 Python 运行时 | 首次需联网 | 会重投影后再算 |
三条里最实用的判断是:凡是”距离”、”面积”、”周长”这类有量纲的量,就用后两个引擎。Client 引擎在 EPSG:4326 的经纬度上做缓冲区,1 个单位不是 1 米,而是 1 度——纬度 40 度处 1 度经度只有约 85 km,缓冲出来的形状是个被压扁的椭圆,越往北越扁。
注意:sidecar 不可用(没装
vectorextra 或进程未启动)时不会静默回退:引擎下拉框保持 sidecar 并给出提示,Run 按钮被禁用。必须用侧车算的工具会提示改用 Pyodide,其余工具提示改用 Client——跑之前先确认引擎与提示,别假定”投影已经处理了”。
15.1.2 Pyodide 为什么值得单独提
Python (Pyodide) 是第三条路:同一份 GeoPandas/Shapely 代码,但完全跑在你的浏览器里。它解决了一个实际矛盾——Web 版没有 sidecar,但用户仍然需要投影感知的缓冲。
三件事要清楚:
- 首次运行的下载成本:Python 运行时几十 MB,从 CDN 懒加载。第一次必须有网。
- 之后复用:运行时预热后,后续运行不再下载。
- 结果与 Sidecar 一致:因为它跑的就是同一份 Python。
还有一个部署层面的取舍,官方文档写得很直白:默认从公共 jsDelivr CDN 加载,而 Pyodide 自己会再去加载 pyodide.asm.js 与 wasm——只对入口脚本做子资源完整性校验是不够的。生产或离线环境应该把 VITE_PYODIDE_INDEX_URL 指向自建的镜像。
15.1.3 250 000 这个数
Client 引擎里所有”两两比较”的工具都卡在同一个上限:
export const MAX_CLIENT_PAIRS = 250_000;
受影响的有 Intersection、Spatial join、Select by location。超限时工具不报错也不静默截断,而是打印:
Error: spatial join needs 1200000 comparisons (limit 250000); use the Sidecar engine for large layers
算一下就知道这个数多容易撞上:5000 个点 × 60 个多边形 = 30 万对,直接超。点对面做空间连接是最典型的撞墙场景。撞上了就换 Sidecar——它的 GeoPandas sjoin 有空间索引,不是朴素两两循环。
15.1.4 结果落在哪
| 工具类别 | 输出形态 |
|---|---|
| 几何、叠加、连接、选择 | 新地图图层(客户端是 GeoJSON,sidecar 结果同样加成图层) |
| Conversion 组 | 写文件,不加图层(见 第05章 数据添加与格式支持) |
每次运行都记进 Processing → History,条目里有 Re-run、Edit & re-run、Copy JSON、Copy Python。
15.2 工具全景
15.2.1 七组 34 个工具
| 分组 | 工具 |
|---|---|
| Geometry(17) | Buffer、Centroids、Convex hull、Dissolve、Bounding box、Simplify、Reproject、Explode、Aggregate by attribute、Smooth、Extract vertices、Points along geometry、Regular grid、Voronoi / Delaunay、Cell-site coverage、Decode polyline、Encode line to polyline |
| Overlay(4) | Clip、Intersection、Difference、Union |
| Join(2) | Spatial join、Attribute join |
| Select(3) | Select by value、Select by location、Random extract |
| Movement & time(3) | Trajectory speed、Detect stops、Space-time proximity |
| Data management(1) | Merge layers |
| Data quality(4) | Check validity、Fix geometries、Check topology rules、Fix topology |
同一份注册表还含 3 个 DGGS 工具(DGGS Generator / DGGS Binning / DGGS Compact),所以 VECTOR_TOOLS 一共 37 条。DGGS 属于离散全球网格体系的专题,本章不展开。
15.2.2 哪些工具挑输入几何
参数定义里带 geometryFilter 的工具会在图层下拉框里过滤——只列出符合条件的图层。选错了根本选不到。
| 工具 | 要求的输入几何 |
|---|---|
| Clip / Intersection / Difference / Union | 两个输入都必须是 polygon |
| Voronoi / Delaunay | 输入必须是 point |
| Isochrone / OD matrix / Sequential route | 必须都是 point |
| Aggregate by attribute | 输入必须是 polygon |
其余工具(Buffer、Centroids、Spatial join、Select by location、Merge layers 等)接受任意几何。
提示:Buffer 的输入不限几何,但
side选inside(内缩)时要求面输入。点或线没有内部可缩,工具会报错。
15.3 几何工具
15.3.1 Buffer:五个参数与五条校验
Buffer 是这个对话框里参数最全的工具,也最能说明”客户端与 Python 引擎必须给出同样的判定”这条设计原则。
| 参数 id | 面板标签 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
layer |
Input layer | layer | — | 必填 |
distance |
Distance | number | 1 | 必填,min: 0,步长 0.1 |
units |
Units | select | kilometers |
kilometers / meters / miles |
side |
Buffer side | select | outside |
Outside (grow) / Inside (shrink) / Both sides |
dissolve |
Dissolve result | boolean | false | 合并成一个要素并融掉重叠;属性会丢 |
校验顺序被刻意安排成”先单位、再方向、再 dissolve、最后距离”,源码注释解释了原因:
// Validate in the Python engine's order (`_buffer`: units, then side, then
// distance finiteness, then distance sign) so a call with several bad
// parameters at once gets the same first error from both engines, not just
// the same accept/reject verdict.
四种拒绝情形:
| 输入 | 日志 |
|---|---|
| 未知单位 | Error: unknown unit '...'; expected kilometers, meters, miles |
| 未知方向 | Error: unknown buffer side '...'; expected outside, inside, both |
dissolve 不是布尔 |
Error: buffer dissolve must be true or false |
| 距离不是有限数或为负 | Error: buffer distance must be a finite number |
警告:
distance为负会被拒绝,不会当作”内缩”。方向归属side,这是刻意的分工。程序化调用(Model Builder、历史记录重放、AI 助手)也走同一套校验——表单上的min: 0只约束表单,不约束调用方。
dissolve 的语义陷阱:勾上之后所有缓冲区融成一个无属性的要素。后续想按原要素属性分组统计就做不到了。要”按属性融合”,正确顺序是 Buffer(不勾 dissolve)→ 再跑 Dissolve(按字段)。
15.3.2 Centroids / Convex hull / Bounding box
三个”降维”工具,都只有一个输入参数:
| 工具 | id | 输入 | 输出 |
|---|---|---|---|
| Centroids | centroids |
任意几何 | 每个要素一个中心点 |
| Convex hull | convex-hull |
任意几何 | 整个图层一个凸包(不是每个要素一个) |
| Bounding box | bounding-box |
任意几何 | 整个图层一个矩形外包框 |
后两个的行为很多人第一次会误解:它们把所有要素一起包起来,产出一个要素。要”每个要素各自的外包框”,得先 Explode 或用 SQL 逐要素算。
15.3.3 Dissolve 与 Aggregate by attribute
| 工具 | 分组方式 | 统计量 | 输出属性 |
|---|---|---|---|
| Dissolve | 可选按一个字段分组 | 无 | 只保留分组字段 |
| Aggregate by attribute | 必填一个分组字段 | count / sum / mean / min / max / median | 分组字段 + 统计字段 |
Aggregate by attribute 的参数:
| 参数 id | 面板标签 | 必填 | 说明 |
|---|---|---|---|
layer |
Input layer | 是 | 只接受 polygon |
group_field |
Group by field | 是 | 分组字段 |
statistic |
Statistic | 否 | 默认 count |
stat_field |
Statistic field | 条件必填 | statistic 不是 count 时出现 |
stat_field 用 visibleWhen: { param: "statistic", notIn: ["count"] } 控制可见性——选 Count 时它隐藏,也就跳过了必填校验。这是全对话框里 visibleWhen 用得最典型的一处。
15.3.4 Simplify 与 Smooth:一对反向操作
| Simplify | Smooth | |
|---|---|---|
| 算法 | Douglas-Peucker | Chaikin |
| 顶点数 | 减少 | 增加 |
| 参数 | tolerance(单位是度,默认 0.01)、highQuality(默认 false) |
iterations(默认值来自工具定义) |
| 用途 | 减小文件、加快渲染 | 去掉折线的硬角、改善观感 |
| Z 值 | — | 保留 |
tolerance 的单位是度,这是个必须记住的坑。0.01 度在赤道约 1.1 km,在纬度 60 度处经度方向只有约 550 m——同一个容差在不同纬度简化掉的细节量不同。
注意:Simplify 之后不能拿去做面积或长度统计。Douglas-Peucker 会系统性缩小面积(凸角被切掉)。要统计就先统计,要出图才简化。
15.3.5 顶点级工具
| 工具 | id | 做什么 | 参数 |
|---|---|---|---|
| Explode | explode |
多部件几何拆成单部件,每部件一个要素,保留父要素属性 | layer |
| Extract vertices | extract-vertices |
每个顶点变成一个点要素,保留父要素属性 | layer |
| Points along geometry | points-along-geometry |
沿线(或面边界)按固定间距或固定数量布点 | layer、interval、units |
| Decode polyline | decode-polyline |
把编码折线字符串解成 LineString 图层 | layer、field、precision(5 或 6) |
| Encode line to polyline | encode-polyline |
把线几何编码成字符串写进新字段 | layer、precision、targetField |
Encode / Decode 这一对是给 Web 地图和路径 API 用的:Google 与 Valhalla 的折线编码能把一条几百个点的线压成几十个字符。precision 必须两边一致,5 和 6 混用会让整条线偏出去几十米。
15.3.6 Regular grid
生成渔网(fishnet),三个来源:
| 参数 id | 面板标签 | 类型 | 默认 |
|---|---|---|---|
source |
Extent source | select | viewport(Map viewport / Layer extent / Manual bounding box) |
layer |
Input layer | layer | 仅 source 为 layer 时可见 |
west / south / east / north |
West (min lon) 等 | number | 仅 source 为 bbox 时可见 |
cell_width |
Cell width (degrees) | number | 1,最小 0.0001 |
cell_height |
Cell height (degrees) | number | 留空则等于 cell_width |
cell_type |
Cell type | select | polygon(Rectangles / Points (cell centers)) |
硬上限 GRID_HARD_CAP = 1 000 000 个格子。cell_width 填 0.001 度(约 100 m)去铺一个省,轻松超百万——超了直接拒绝,不会把标签页拖死。
提示:宽高单位都是度。要用米,先确认地图坐标系;这个工具没有投影感知版本,格子永远是经纬度格网。
15.3.7 Voronoi / Delaunay
一个工具、两种产物:
| 参数 | 值 | 输出 |
|---|---|---|
type = voronoi |
默认 | 每个点一个多边形,裁剪到点的外包范围 |
type = delaunay |
— | 三角网 |
输入必须是点图层(geometryFilter: ["point"])。Voronoi 的”裁剪到外包范围”这一句很重要:不裁剪的 Voronoi 会无限外扩,图上看起来像一张失控的网。裁剪后它才是一个可用的”最近点服务区”近似。
注意:Voronoi 是欧氏距离的划分,不是路网距离。拿它当”最近门店”的近似可以,当”最近门店的可达范围”不行——中间隔着河或高速时结论完全相反。可达范围要用 15.7 的等时圈。
15.3.8 Cell-site coverage
从点站点生成天线扇区面,用于通信覆盖:
| 参数 | 说明 |
|---|---|
layer |
站点图层 |
azimuthField / azimuth |
方位角,可从字段读或填固定值 |
radiusField / radius |
半径,同上 |
beamwidthField / beamwidth |
波束宽度,同上 |
units |
长度单位 |
每一组都是”字段优先、固定值兜底”的结构——字段有值就用字段,字段为空就用固定值。这让你可以在同一批站点里混合使用逐站参数与统一默认值。
15.4 叠加分析:四个工具,三种属性策略
15.4.1 一张表看清区别
| 工具 | 几何结果 | 输入属性 | 叠加层属性 | 引擎调用 |
|---|---|---|---|---|
| Clip | 输入被叠加层裁掉外面的部分 | 保留 | 丢弃 | 每个输入要素与合并后的叠加几何求交 |
| Intersection | 两层的公共部分 | 保留 | 合并保留 | 逐对求交(输入数 × 叠加数) |
| Difference | 输入减去叠加层 | 保留 | 丢弃 | 每个输入要素与合并后的叠加几何求差 |
| Union | 两层合并成一个几何 | 不保留(两个引擎都不保留) | 不保留 | — |
这张表是本章最该记住的东西。Clip 和 Intersection 的差别不在几何,而在属性:Clip 只带输入层的字段,Intersection 把两边的字段合在一起。所以”我需要知道每个碎片属于哪个区”要用 Intersection,”我只要研究区内的地块”用 Clip 就够,还省一半字段。
15.4.2 参数与几何过滤
四个工具的输入参数结构一致:
| 工具 | 参数 |
|---|---|
| Clip | layer(Input layer)、overlay(Overlay (clip) layer) |
| Intersection | layer(Input layer)、overlay(Overlay layer) |
| Difference | layer(Input layer)、overlay(Overlay layer) |
| Union | layer(Input layer)、overlay(Overlay layer) |
四个参数都带 geometryFilter: ["polygon"],两层都必须是面。想用面裁线,得走 Select by location(within/intersects)或先在 SQL 里做。
15.4.3 Clip 与 Difference 的共用引擎
Clip 与 Difference 用的是同一个内部函数 overlay,差别只在传进去的算子:
function overlay(ctx, op, resultName, keepProperties) { ... }
// Clip: intersect(featureCollection([a, b]))
// Difference: difference(...)
关键在 keepProperties 分支:result.properties = keepProperties ? (feature.properties ?? {}) : {}。Clip 与 Difference 传 true,所以属性来自输入要素。这也解释了为什么叠加层的字段一定丢——合并后的几何根本对应不到任何单个叠加要素。
15.4.4 Intersection 的 1:N 语义
Intersection 不走上面那个共用引擎,因为它要保留两边属性:
for (const a of inputPolys) {
for (const b of overlayPolys) {
const piece = intersect(featureCollection([a, b]));
if (piece?.geometry) {
piece.properties = { ...(a.properties ?? {}), ...(b.properties ?? {}) };
results.push(piece);
}
}
}
两个后果:
- 一个输入要素可能产出多个碎片(如果它横跨多个叠加要素)。输出要素数可以大于输入要素数。
- 合并属性时叠加层的同名键会覆盖输入层的(展开顺序决定)。要防止字段打架,跑之前先把重名字段改名。
15.4.5 Union 为什么不保属性
Union 把两层并成一个整体几何,中间的分界线被抹掉。既然分界线没了,”这个碎片原来属于哪一层”这个问题就没有答案了——属性不保留是几何操作的必然结果,不是实现偷懒。需要按来源区分,用 Intersection 再补一层 Difference,或者直接用 Merge layers(它有一个 addSourceField)。
15.5 连接与选择
15.5.1 Spatial join
| 参数 id | 面板标签 | 类型 | 默认 |
|---|---|---|---|
layer |
Input layer | layer | — |
overlay |
Join layer | layer | — |
predicate |
Spatial relationship | select | intersects(Intersects / Within / Contains) |
how |
Join type | select | inner(Inner / Left) |
输入几何不限——这是它和四个叠加工具最大的区别,点、线、面都能连。
三条行为细节,全部来自源码:
- 一个输入要素匹配多个连接要素时,输出多个要素(与 GeoPandas
sjoin一致)。这就是”1:N 连接会让行数膨胀”。 - 同名字段冲突时输入层胜出:
{ ...(match.properties ?? {}), ...(feature.properties ?? {}) }——注意展开顺序,输入层在后面。源码注释特别指出 sidecar 的做法不同(它会给重名字段加_left/_right后缀)。所以同一个操作在 Client 与 Sidecar 引擎上,重名字段的处理不一样,跨引擎对比结果时要留意。 - Left join 会把连接层的所有字段补成 null,而不是干脆不出现。这样输出 schema 与匹配行一致,也和 GeoPandas 的 NaN 行为对齐。
15.5.2 Attribute join
纯表连接,不碰几何。参数比 Spatial join 多:
| 参数 id | 面板标签 | 必填 | 默认 |
|---|---|---|---|
layer |
Target layer | 是 | — |
overlay |
Join layer (table) | 是 | — |
target_field |
Target key field | 是 | — |
join_field |
Join key field | 是 | — |
how |
Join type | 否 | left |
fields |
Fields to bring over (optional) | 否 | 空(带全部非键字段) |
注意两个默认值与 Spatial join 相反:这里是 left join 为默认,且没有 predicate。
键的匹配规则是最容易出意外的地方,源码注释把边界列得很清楚:
| 情形 | 匹配结果 |
|---|---|
| 任一侧为 null / undefined / NaN / 空字符串 | 永不匹配(等价于 SQL/pandas 的 NaN 键) |
数值 5 与字符串 "5" |
匹配(键统一字符串化) |
"01001" 与 "1001" |
不匹配(保留前导零) |
| 多条连接行命中同一个键 | 第一条胜出(一对一) |
最后一条是很硬的限制:这是一对一连接,不是”一对多展开”。要一对多,用 Spatial join,或去 SQL 里写 JOIN。
fields 是一个逗号分隔的字段名列表,留空则带入除键字段外的所有连接字段。
15.5.3 Select by value
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
field |
field | — | 必填 |
operator |
select | eq |
见下表 |
value |
string | — | 除两个”空值判断”外必填 |
十个操作符:eq(=)、neq(≠)、gt(>)、gte(≥)、lt(<)、lte(≤)、contains(文本包含)、starts-with(文本前缀)、is-null(为空)、is-not-null(不为空)。
value 的可见性由 visibleWhen: { param: "operator", notIn: ["is-null", "is-not-null"] } 控制,所以选”为空”时不会要求你填一个值。
比较规则一句话:两边都能解析成数字时按数字比,否则按文本比。所以 "10" > 9 为真(数字比较),但 "10" > "9" 为假(字典序,因为有一位字符更小)。字段类型混杂时这条规则会让人抓狂——先用属性表看清字段类型。
15.5.4 Select by location
| 参数 | 可选值 |
|---|---|
layer |
待筛选图层 |
overlay |
筛选用的图层 |
predicate |
intersects / within / contains / disjoint(不相交) |
比 Spatial join 多一个 disjoint——它选的是”与筛选层完全没有交集”的要素。空筛选层时 disjoint 保留全部,其余三种一个不留,这个边界行为在源码注释里专门写明。
与 Spatial join 的关键区别:Select by location 只筛要素,不增字段。行数不变(除了被筛掉的),schema 不变。想加字段用 Spatial join。
15.5.5 Random extract
| 参数 | 默认 | 说明 |
|---|---|---|
method |
count |
Number of features / Percentage of features |
count |
10 | 超过输入要素数时自动截断 |
percentage |
10 | 0–100 |
用途是抽样检查与训练样本抽取。注意它没有随机种子参数——同一份数据跑两次会得到不同的子集。需要可复现的抽样,去 SQL 里用 ORDER BY random() LIMIT n(同样不可复现)或者自带种子字段。
15.6 坐标系、容差与结果偏差
15.6.1 三个容差参数,三种单位
这是本章最容易出错的地方——三个工具都有”容差”,但单位各不相同:
| 工具 | 参数 | 单位 | 默认 |
|---|---|---|---|
| Simplify | tolerance |
度 | 0.01 |
| Check topology rules / Fix topology | snapTolerance |
图层坐标单位(WGS84 时是度) | 0.0001 |
| Regular grid | cell_width / cell_height |
度 | 1 |
snapTolerance 的描述写得很明确:in layer coordinate units — degrees for WGS84。如果图层在投影坐标系里,0.0001 就是 0.0001 米——小到几乎不起作用,你得改成米制的合理值(例如 0.5 米的捕捉容差)。
警告:把 WGS84 图层的
snapTolerance0.0001 直接搬到投影坐标图层上,等于”不捕捉”。拓扑修复会看起来毫无效果。
15.6.2 距离:客户端与 GeoPandas 的分歧点
| 操作 | Client (Turf.js) | Sidecar / Pyodide (GeoPandas) |
|---|---|---|
| Buffer 距离 | 在经纬度上按度算(Turf 会做球面近似,但不是真投影) | 先重投影到本地 UTM 带(estimate_utm_crs)再算 |
| 面积 | Turf 用球面公式 | 投影后平面计算 |
| 结果差异 | 高纬度、大范围时显著 | 小范围时与真值接近 |
实践建议:凡是要交付的面积或距离数字,一律用 Sidecar 或 Pyodide。Client 引擎的价值在于”快速看到形状对不对”。
15.6.3 简化与拓扑的先后
顺序错了会做出”看起来对、其实碎了”的数据:
| 顺序 | 结果 |
|---|---|
| 先 Simplify 再拓扑检查 | 简化会制造新的自相交与缝隙,检查通过不代表原数据干净 |
| 先拓扑检查与修复,再 Simplify | 正确顺序 |
| Simplify 后再做叠加 | 边界不再严丝合缝,Intersection 会出现大量细碎碎片 |
15.6.4 投影选择速查
| 分析 | 推荐投影 |
|---|---|
| 缓冲区(米制距离) | 所在 UTM 带 / 地方平面坐标 |
| 面积统计 | Albers 等积 |
| 沿线布点(米制间距) | 等距投影或 UTM |
| 最近邻 / Voronoi | 尽量用等距投影,否则高纬度会偏向东西向 |
| 空间连接(拓扑关系) | 影响很小,但两层必须同 CRS |
最后一格是硬要求:空间连接与叠加的两个输入图层必须在同一个 CRS 里。GeoLibre 的图层通常统一显示在 Web Mercator,但工具读的是图层自身的 GeoJSON 坐标——两层 CRS 不一致时,几何根本对不上,结果会是”什么都没匹配上”,而不是报错。
15.7 网络分析
15.7.1 等时圈 / 服务区
| 参数 id | 面板标签 | 类型 | 默认 |
|---|---|---|---|
layer |
Origin points | layer(仅点) | — |
mode |
Travel mode | select | auto(Driving / Walking / Cycling) |
metric |
Metric | select | time(Travel time (minutes) / Distance (km)) |
contours |
Contours | string | "5,10,15" |
endpoint |
Routing server (Valhalla) | string | 空(运行时取全局配置) |
contours 是逗号分隔的数值:metric 为时间时单位是分钟,为距离时单位是公里。
三条硬上限:
| 常量 | 值 | 含义 |
|---|---|---|
MAX_ISOCHRONE_POINTS |
25 | 单次最多 25 个起点,超出只取前 25 个并打日志 |
MAX_ISOCHRONE_CONTOURS |
4 | Valhalla 单请求最多 4 个等值线 |
MAX_ROUTE_POINTS |
20 | 顺序路径最多 20 个途经点(公共 FOSSGIS 服务的 max_locations 就是 20) |
15.7.2 OD 成本矩阵
| 参数 | 说明 |
|---|---|
origins |
起点图层(仅点) |
destinations |
终点图层(仅点) |
mode |
出行方式 |
endpoint |
Valhalla 地址 |
输出是连接线,每条线带该 OD 对的行程时间与距离。
上限是 MAX_MATRIX_CELLS = 2500,即 起点数 × 终点数 ≤ 2500。超了报:Error: 60 × 60 = 3600 pairs exceeds the 2500-cell limit. Reduce the input sizes.
提示:2500 这个数决定了用法——它适合”20 个门店 × 100 个小区”,不适合”全城网格”。大规模可达性分析应该自己做路网分区,或者用等时圈叠加代替。
15.7.3 顺序路径
| 参数 | 说明 |
|---|---|
layer |
途经点图层(仅点) |
order_field |
决定顺序的字段 |
mode |
出行方式 |
endpoint |
Valhalla 地址 |
输出每条 leg 只有 leg_index、from_id、to_id、time_s、distance_km、mode,不带逐段导航指令;需要转向提示时用单独的 Navigation(turn-by-turn)工具。超过 20 个点会被截断并打日志——不是报错,是静默降级,留意日志。
15.7.4 谁在算这笔账
网络分析不在浏览器里算,它请求一个 Valhalla 服务器。默认是公共 FOSSGIS 实例(valhalla1.openstreetmap.de)。
两件事必须知道:
- 你提交的坐标会离开你的设备。 官方文档在右键快捷入口处明确提示了这一点,第一次使用会请求同意。
- 自托管可以改。 部署时可指向自己的 Valhalla 服务,见 第22章 服务端、自托管与部署运维。
15.7.5 右键快捷入口
| 菜单项 | 行为 |
|---|---|
| Buffer … here | 三个距离预设;预设值跟随 Settings 里的 Scale bar units:米制是 500 m / 1 km / 5 km,英制是 0.25 mi / 1 mi / 5 mi |
| Drive time from here | 5 / 10 / 15 分钟驾车等时圈 |
| Walk time from here | 同样三条等值线,步行 |
| Open in Processing… | 打开完整对话框,暴露预设没给的参数 |
同一套 Quick analysis 也出现在图层行菜单里,但对整层操作,只有四项:Buffer all features by …、Centroids、Convex hull、Bounding box。
15.8 空间统计
15.8.1 七个工具
Processing → GeoLibre Toolbox → Spatial Statistics:
| 工具 | 回答的问题 |
|---|---|
| Global Moran’s I | 这个数值属性在整层上是聚集、离散还是随机? |
| Local Moran’s I (LISA) | 逐个要素看:高高聚集、低低聚集、还是空间离群点? |
| Getis-Ord Gi* hotspots | 逐个要素看热点与冷点,带显著性等级 |
| Average nearest neighbor | 点格局比随机更聚集还是更分散? |
| Kernel density (heatmap) | 从点图层生成连续密度面 |
| Emerging Hot Spot (space-time cube) | 时空立方体上的热点趋势 |
| Composite score (suitability index) | 多个数值字段归一化加权成一个指数 |
15.8.2 Composite score 的六个决策
这是唯一一个”参数本身就是方法论”的工具。它的六个选择直接决定结果的含义:
| 决策 | 选项 | 影响 |
|---|---|---|
| 归一化方式 | min-max / z-score / rank / quantile | 决定原始值如何映射到可比尺度 |
| 相对权重 | 任意正数 | 界面上同时显示”占总权重的比例”,权重效果可读 |
| 好的一端 | 每字段各自指定 | 有的指标越大越好,有的越小越好 |
| 组合方式 | 加权算术平均 / 加权几何平均 | 算术平均允许一项强项补偿弱项;几何平均不允许 |
| 缺失值处理 | 丢弃该要素 / 在已有字段上重新归一化权重 | 无默认值,必须明确选 |
| 保留归一化分量 | 开 / 关 | 开启后每个字段的归一化值单独成列,评分可审计 |
输出刻度两个选项:0–100(默认,适合直接做图例)与 0–1(分数还要参与后续计算时用)。字段名由 Score field name 决定。
为什么缺失值没有默认值:官方文档写得很直接——两种做法都会产出看起来合理的图,但只有一种是你要的那个指数。丢弃会让某些区域成空洞,重归一化会让同一地区的评分随缺失组合变化。
关于 rank 与 quantile 归一化有一个细节:并列值共享秩(mid-rank 约定),所以相等的输入一定得到相等的分数。
提示:结果落地时已经按分数做了分级渲染。要复核,打开 Keep normalized components,然后在属性表里对几个要素手工算一遍加权和——如果对不上,多半是某个字段的”好的一端”设反了。
15.8.3 空间统计的适用范围
| 方法 | 前提假设 | 违反会怎样 |
|---|---|---|
| Global / Local Moran’s I | 空间权重矩阵合理;属性有意义的邻接关系 | 用不合适的邻接定义会得到”显著但无意义”的结果 |
| Getis-Ord Gi* | 同上 | 同上 |
| Average nearest neighbor | 研究区边界明确 | 边界不闭合时结论不可靠 |
| Kernel density | 带宽选择 | 带宽过小成噪声,过大抹平结构 |
| 时空立方体 | 时间切片等间隔 | 不等间隔会让趋势判断失真 |
15.9 数据质量与拓扑
15.9.1 两级质量检查
| 工具 | id | 查什么 | 输出 |
|---|---|---|---|
| Check validity | check-validity |
几何本身是否合法(自相交的环、洞跑到壳外…) | 在地图上标出问题要素 |
| Fix geometries | fix-geometries |
用 ST_MakeValid 修复 |
合法要素原样通过 |
| Check topology rules | check-topology-rules |
要素之间的拓扑规则 | 违规要素图层 + 按规则分类的计数 |
| Fix topology | fix-topology |
自动修复可修的违规 | 修复后的图层(或 dry run 报告) |
区别很重要:Check validity 看单个要素,Check topology rules 看要素之间。一块多边形可以自身完全合法,但它和邻居重叠了 3 米——只有后者能发现。
15.9.2 六条拓扑规则
| 规则 id | 面板标签 | 默认 |
|---|---|---|
line_must_not_self_intersect |
Lines must not self-intersect | 开 |
polygon_must_not_overlap |
Polygons must not overlap | 开 |
polygon_must_not_have_gaps |
Polygons must not have gaps | 开 |
line_must_not_have_dangles |
Lines must not have dangles | 关 |
point_must_be_covered_by_line |
Points must be covered by a line | 关 |
line_endpoints_must_snap_within_tolerance |
Line endpoints must connect within tolerance | 关 |
前三条默认开,后三条默认关,原因在源码注释里:连通性规则必须手动开启,否则任意图层上每一个自由端点、每一个孤立点都会被报成违规——一个完全正常的道路网会有成百上千个自由端。
snapTolerance 默认 0.0001,只对端点规则生效。
15.9.3 Fix topology 只能修三种
FIXABLE_TOPOLOGY_RULES 明确只有三条:
| 面板标签 | 规则 | 默认 |
|---|---|---|
| Snap nearly-connected line endpoints | line_endpoints_must_snap_within_tolerance |
开 |
| Repair dangles (snap or project onto nearby geometry) | line_must_not_have_dangles |
开 |
| Project points onto the nearest line | point_must_be_covered_by_line |
关 |
而 polygon_must_not_have_gaps 被引擎接受但从不产生任何修改,所以刻意不在 Fix topology 的选项里出现——它的注释写得很清楚:accepted by the engine but never produces changes, so it is not offered here。
还有一个 Preview only (dry run) 选项:只报告会做哪些修复,不产出修复后的图层。大范围修复前先 dry run 一次,否则你会在一个 10 万要素的图层上点下去然后等很久。
修复的原则是保守的:附近没有东西的自由端会被原样留下。它不会为了”修好”而把线拉到很远的地方。修复后要把结果和原图层叠一遍确认没有意外位移——日志会输出变更数量的尾部(最多 20 行)。
15.10 批处理与模型串联
| 需求 | 工具 | 说明 |
|---|---|---|
| 同一工具套多个图层 | Batch tools 的 Batch 模式 | 客户端引擎 |
| 多工具串成链并随项目保存 | Batch tools 的 Models 模式 | 客户端引擎 |
| 有分叉的可视化流程 | Model Builder | 见 第13章 模型构建器与自动化工作流 |
| 关系运算与聚合 | SQL Workspace | 见 第11章 SQL 工作区与 DuckDB 空间分析 |
| 循环与条件 | Python 控制台 / Notebook | 见 第19章 Python 控制台、Notebook 与 Jupyter 集成 |
Model Builder 的矢量工具节点取的就是本章这 37 个工具。意味着本章的任何工具都可以变成模型里的一个节点,且 type: "layer" 参数会自动变成输入端口(见第 13 章 13.5.2)。
15.11 精度、性能与结果校验
15.11.1 性能上限总表
| 限制 | 值 | 影响工具 |
|---|---|---|
MAX_CLIENT_PAIRS |
250 000 | Intersection、Spatial join、Select by location |
GRID_HARD_CAP |
1 000 000 格 | Regular grid |
MAX_ISOCHRONE_POINTS |
25 | Isochrone |
MAX_ISOCHRONE_CONTOURS |
4 | Isochrone |
MAX_MATRIX_CELLS |
2 500 | OD cost matrix |
MAX_ROUTE_POINTS |
20 | Sequential route |
15.11.2 七个校验动作
| 检查 | 怎么做 | 不通过说明 |
|---|---|---|
| 要素数是否合理 | 属性表底部计数,与输入对比 | 1:N 连接会让数变大,叠加会让数变成碎片 |
| 几何类型 | 图层信息 | Clip 出 MultiPolygon 很正常,出 GeometryCollection 说明有退化几何 |
| 面积总和 | 与输入面积比 | Difference 后应减小,Intersection 应小于两者中较小者 |
| 空几何 | Select by value 里用条件筛 null | 空几何会在导出时炸掉 |
| 字段完整性 | 属性表列头 | 同名字段被覆盖是 Spatial join 的已知行为 |
| 空间范围 | Zoom to layer | 结果跑到另一个大陆多半是 CRS 不一致 |
| 引擎一致性 | 同一操作分别用 Client 与 Sidecar 跑 | 差异过大说明单位或投影问题 |
15.11.3 五个常见错误
| 现象 | 原因 | 修法 |
|---|---|---|
| 缓冲区形状被压扁、越北越扁 | 用 Client 引擎在经纬度上按度缓冲 | 换 Sidecar / Pyodide |
| “needs N comparisons (limit 250000)” | 两两比较超限 | 换 Sidecar,或先聚合减少要素数 |
| 空间连接全都没匹配上 | 两层 CRS 不一致 | 统一 CRS 后重跑 |
| Intersection 产出大量微碎片 | 两层边界不严丝合缝 | 先做拓扑修复与捕捉 |
| 属性连接大量未匹配 | 一侧有前导零/数值与文本混用 | 统一键字段类型 |
15.12 两个完整案例
15.12.1 案例一:商圈覆盖分析
问题:某连锁有 12 家门店,想知道每家门店 15 分钟步行范围内覆盖了多少个居住小区、多少人口。
输入:stores(点,12 个)、communities(面,带 population 字段)。
步骤:
- 等时圈。Processing → GeoLibre Toolbox → Network → Isochrone / service area。
layer=storesmode= Walkingmetric= Travel time (minutes)contours=15- Run → 得到
Isochrones图层,12 个面。
注意:12 个起点在 25 的上限内,没问题。但如果门店超过 25 家,工具只会取前 25 个并打一行日志——它不是分批自动跑。
- 空间连接。Processing → GeoLibre Toolbox → Vector → Spatial join。
layer=communities(要保留小区属性)overlay= 等时圈图层predicate= Intersectshow= Inner(只要被覆盖的)- Engine = Sidecar (GeoPandas)(小区数 × 12 很容易撞 25 万上限)
提示:一个小区可能同时落在两个门店的等时圈里。Inner join 会为每个匹配产出一行,所以这是预期的重复,不是 bug——但它意味着你不能直接把
population求和,否则重叠区会被算两次。 - 去重。这一步是很多人漏掉的。用一个能区分门店的字段(等时圈图层会带来源点 id)分组:
- 若只需”被任意门店覆盖的小区”,用 Dissolve 或 SQL 的
SELECT DISTINCT按小区 id 去重。 - 若要”每个门店各覆盖多少”,则保留重复,按门店 id 分组求和。
- 若只需”被任意门店覆盖的小区”,用 Dissolve 或 SQL 的
- 统计。Processing → GeoLibre Toolbox → Vector → Aggregate by attribute:
group_field= 门店 id 字段statistic= Sumstat_field=population
或在 SQL Workspace 里:
SELECT store_id, count(DISTINCT community_id) AS communities, sum(population) AS population FROM joined GROUP BY store_id ORDER BY population DESC; - 验证。把去重后的小区总数与
communities总数对比。如果”被覆盖小区数”超过了总小区数,说明第 3 步没做。
这条案例的三个坑:
| 坑 | 表现 | 原因 |
|---|---|---|
| 人口被重复计 | 12 家门店覆盖人口之和远大于全城人口 | 重叠区在 1:N 连接里出现多次 |
| 步行等时圈奇形怪状 | 出现飞地、跨越河道 | Valhalla 用的是真实路网与人行桥,形状本来就不规则 |
| 小区只被部分覆盖 | 边界上的小区被整块计入 | 空间连接是按”是否相交”判断,不做面积加权 |
第三条要更精确的话,得走 Intersection(小区 ∩ 等时圈)再用 population × 面积占比 加权——但那需要先算面积,属于下一节的技能。
15.12.2 案例二:土地利用变化叠加
问题:有两期土地利用面(lu_2015、lu_2020),都带 code 字段。要产出”变化图斑”(2020 与 2015 类型不同的地方),并统计各转换类型的面积。
步骤:
-
统一 CRS。确认两期数据在同一投影坐标系(这是后续面积统计的前提)。不一致就先 Reproject(需要 Sidecar 或 Python 引擎)。
- 求公共区域。Intersection:
layer=lu_2020overlay=lu_2015- Engine = Sidecar(两期各几千个面,两两相乘轻松超 25 万)
输出每个碎片同时带两期的属性。注意同名字段
code会冲突——Client 引擎下输入层(2020)胜出,Sidecar 引擎下会加_left/_right后缀。跑之前先把字段改名为code_2020与code_2015,这是最省事的做法。 - 筛出变化。Select by value:
field=code_2020operator=neqvalue= … 这里有个问题:neq只能和一个固定值比,不能比两个字段。
两个字段互比需要走 SQL Workspace:
CREATE TABLE changed AS SELECT *, ST_Area(geometry) AS area_m2 FROM lu_overlay WHERE code_2020 <> code_2015;或者用 Edit → Select by Expression(按表达式选),再另存为新图层。
-
算变化方向的面积。按转换类型分组:
SELECT code_2015, code_2020, count(*) AS patches, sum(ST_Area(geometry)) AS area_m2 FROM changed GROUP BY code_2015, code_2020 ORDER BY area_m2 DESC; -
做变化图。把
changed建成图层,按code_2015 || '→' || code_2020做一个分类着色,或者只高亮”耕地转建设用地”这类重点转换。 - 验证:
- 变化图斑总面积应远小于研究区总面积(通常 5%–20%)。
- 各转换类型的面积之和应等于变化总面积(误差小于 1%)。
- 随机抽 10 个图斑,回到影像上目视核对。
这条案例的四个坑:
| 坑 | 表现 | 原因 |
|---|---|---|
| 同名字段互相覆盖 | 只剩一个 code |
Intersection 合并属性的展开顺序 |
| 出现大量 1 平方米的碎片 | 图斑数是预期的几十倍 | 两期边界不严丝合缝,Intersection 在缝隙处切出微片 |
| 面积对不上 | 分组求和与总数不等 | 混合了不同 CRS 或不同面积单位 |
| 变化图斑包含”伪变化” | 实际没变的地方被标成变化 | 两期分类体系不一致(三期分类 vs 二期分类) |
第二条的修法:先对两期数据做 Check topology rules(开 polygon_must_not_overlap 与 polygon_must_not_have_gaps),用 Fix topology 的 snapTolerance 把边界对齐;或者在 Intersection 之后用 SQL 过滤掉面积小于某个阈值的碎片。
警告:“伪变化”是土地利用变化分析最大的陷阱。两期数据的分类体系、最小成图单元、边界勾绘精度只要有一项不同,就会产生大量”技术性变化”。做统计之前先看两期的分类代码表——如果 2015 有 12 类而 2020 有 8 类,那么大部分”变化”其实是分类口径变了。
15.13 本章小结
- 矢量工具集中在 Processing → GeoLibre Toolbox → Vector,七组共 34 个(加上 3 个 DGGS 工具共 37 条注册表条目)。
- 三个引擎:Client (Turf.js) 不做投影、Sidecar (GeoPandas) 会重投影、Python (Pyodide) 在浏览器里跑同一份 GeoPandas 代码。凡有量纲的量用后两个。
- Sidecar 不可用时不会自动回退:提示后 Run 被禁用,需手动改选 Client 或 Pyodide。
- 250 000 是 Client 引擎两两比较的硬上限,影响 Intersection、Spatial join、Select by location。撞上就换 Sidecar。
- 四个叠加工具的区别主要在属性:Clip 保留输入属性、Intersection 合并两边属性、Difference 保留输入属性、Union 两边都不保留。
- Spatial join:1:N 会膨胀行数;同名字段输入层胜出(Client)而 Sidecar 加
_left/_right后缀;Left join 把连接字段补 null。 - Attribute join 是一对一(首行胜出),键的规则是”空值永不匹配、数值与数字字符串互通、保留前导零”。
- 三个”容差”单位不同:Simplify 的
tolerance是度,拓扑工具的snapTolerance是图层坐标单位,Regular grid 的格子是度。 - 网络分析靠外部 Valhalla 服务(默认公共 FOSSGIS 实例,坐标会离开设备)。上限:25 个起点、4 条等值线、2500 个 OD 格、20 个途经点。
- 网络分析的三条语义:起点只接受点图层;
contours用逗号分隔(时间=分钟,距离=公里);OD 输出是带时间与距离的连接线。 - Composite score 的六个决策里,缺失值处理没有默认值——必须明确选”丢弃”还是”重归一化”,两者产出的图都”看起来合理”但含义不同。
- 拓扑检查分两级:Check validity 看单个几何,Check topology rules 看要素之间。六条规则里前三条默认开,三条连通性规则默认关。
- Fix topology 只能修三种(端点捕捉、悬挂点、点投影到线);
polygon_must_not_have_gaps被接受但从不产生修改,所以没被提供。 - 顺序铁律:先拓扑修复,再简化与叠加。反过来会制造新的拓扑问题与大量碎片。
- 两个输入图层必须在同一 CRS。不一致时的表现是”一个都没匹配上”,而不是报错。
15.14 实践练习
-
三引擎对照:对同一个面图层做 1 公里缓冲区,分别用 Client (Turf.js)、Sidecar (GeoPandas)、Python (Pyodide) 各跑一次。把三个结果叠在一起,量出最大偏差出现在哪个纬度、哪个方向上,并用自己的话解释原因。
-
撞上限:造一个 800 点的点图层与一个 500 面的面图层,跑 Spatial join(Client 引擎),记录报错文本。再跑一次 Select by location(Client),看它是否也在同一处失败。最后换 Sidecar 重跑,记录耗时。
-
属性策略验证:用同一对图层分别跑 Clip、Intersection、Difference、Union,列出四次输出的字段列表。特别确认 Union 是否真的一个属性字段都不剩,并解释为什么这在几何上是必然的。
-
键匹配实验:造一个小表,键字段里混入
5(数值)、"5"(文本)、"05"、"01001"、空字符串、null。用 Attribute join 连到另一张只有"5"与"01001"的表上,记录哪些行匹配成功,并对照 15.5.2 的规则表验证。 -
容差单位陷阱:取一个 WGS84 的道路线图层和它的投影版本(例如 UTM),对两者都跑 Check topology rules(只开端点捕捉规则),
snapTolerance都用默认 0.0001。比较两次报出的违规数量,并算出要让投影版本得到相同捕捉效果,容差应该是多少。 -
完整商圈分析:用 15.12.1 的流程,但改成 Drive time 10 分钟,并且要求”每个小区只计入最近的那家门店”。写出你的去重规则(提示:需要先算小区中心到门店的距离,或者用 Voronoi 做最近门店划分,再与等时圈取交集),并说明这个规则在什么情况下会给出错误结论。
15.15 延伸阅读
- 处理工具箱的整体结构与两个工具箱的区别:见 第12章 地理处理工具箱
- 用 SQL 做连接、聚合与面积计算:见 第11章 SQL 工作区与 DuckDB 空间分析
- 坐标系统与投影的完整说明:见 第09章 坐标系统、投影与测量
- 把矢量流程固化成模型:见 第13章 模型构建器与自动化工作流
- 属性表、字段与筛选的日常操作:见 第10章 属性表、数据编辑与字段采集