空间计算基础

空间计算基础

这篇笔记面向低空领域的后端工程实践。它不是 GIS 软件操作手册,而是帮助后端工程师建立一套“空间数据怎么表达、怎么存储、怎么计算、怎么实时处理”的基础模型。

传统互联网后端里,我们经常处理的是用户、订单、支付、库存、权限这些业务对象;低空领域多了一类非常核心的对象:空间对象。

  • 一架无人机不是只有一个设备 ID,它还有位置、速度、航向、高度、姿态、时间。
  • 一条航线不是普通列表,它是一条带高度约束和时间约束的空间路径。
  • 一个禁飞区不是简单的业务状态,它可能是一个二维面,也可能是一个带高度上下限的三维空域。
  • 一次告警不是普通规则匹配,它常常要判断“当前点是否进入某个区域”“未来轨迹是否会穿越空域”“多架无人机是否会发生空间冲突”。

可以先用一句话理解空间计算:

空间计算就是把点、线、面、体、时间这些要素放到统一坐标参考下,判断它们之间的位置关系、距离关系、覆盖关系、相交关系和动态变化关系。

1. 从低空业务看空间计算

1.1 低空系统里的典型空间问题

低空业务里的空间问题通常不是单点查询,而是“空间 + 时间 + 规则”的组合。

场景 空间对象 典型问题 后端关注点
无人机实时监控 点、轨迹线 设备现在在哪里,是否偏航,是否失联 高频写入、低延迟查询、轨迹压缩
航线规划 线、缓冲区、空域 航线是否穿越禁飞区,是否经过风险区 拓扑判断、距离判断、批量校验
禁飞区告警 点、面、体 无人机是否进入禁飞区 点面关系、高度判断、实时规则
空域管理 多边形、棱柱体、时间窗 某段时间内空域是否被占用 空间索引、时间索引、冲突检测
起降点管理 点、服务半径 最近起降点在哪里,是否在覆盖范围内 近邻搜索、距离计算
风险评估 栅格、矢量、实时轨迹 某区域风险等级如何变化 栅格叠加、流式聚合
指挥调度 设备点、任务区、通信覆盖 如何就近派单、如何切换链路 空间查询、状态同步

低空后端需要同时关心两类问题:

  1. 静态空间关系:航线是否穿越禁飞区、起降点是否在服务区内、地块是否重叠。
  2. 动态空间状态:无人机实时位置是否进入告警区、未来 30 秒是否会冲突、某个网格里当前有多少飞行器。

1.2 一个后端视角的空间数据流

flowchart LR
    A["无人机 / 机巢 / 作业平台"] --> B["接入层<br/>MQTT / HTTP"]
    B --> C["消息队列<br/>Kafka / Pulsar"]
    C --> D["实时计算<br/>Flink / Spark Streaming"]
    D --> E["空间状态计算<br/>点面判断 / 网格聚合 / 轨迹预测"]
    E --> F["在线存储<br/>Redis Geo / Redis Hash / 内存索引"]
    E --> G["空间数据库<br/>PostgreSQL + PostGIS"]
    E --> H["时序库<br/>TimescaleDB / InfluxDB / ClickHouse"]
    F --> I["监控大屏 / 告警服务 / 调度系统"]
    G --> I
    H --> J["轨迹回放 / 统计分析 / 训练数据"]

这个链路里有一个很重要的分工:

  • 消息队列负责削峰、解耦和顺序消费。
  • 流处理引擎负责实时计算空间状态。
  • PostGIS负责复杂空间对象、空间索引和权威空间查询。
  • Redis Geo / 内存索引负责低延迟在线查询。
  • 时序数据库负责高频遥测数据的长期存储、压缩和回放。

不要指望一个数据库解决所有空间问题。低空系统里,通常需要“权威空间库 + 实时状态缓存 + 时序轨迹库 + 流处理”的组合。

2. 空间数据基础

2.1 点、线、面、体与时间

空间数据最基本的表达对象如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0维:点 Point
例:无人机当前位置、起降点、基站位置

1维:线 LineString
例:航线、航路、轨迹、道路

2维:面 Polygon / MultiPolygon
例:禁飞区、限制区、行政区、通信覆盖区

2.5维:二维面 + 高度区间
例:某禁飞区平面范围内,高度 0m-120m 禁飞

3维:体 Solid / Polyhedron / Mesh
例:复杂建筑物空间、真实空域体、三维障碍物

4维:空间 + 时间
例:某空域 10:00-10:30 被任务 A 占用

低空业务里最常见的不是完整 3D,而是 2.5D

1
2
3
4
二维平面范围:Polygon
高度下限:min_alt
高度上限:max_alt
时间窗口:start_time ~ end_time

例如一个禁飞区可以这样建模:

1
2
3
4
5
6
7
8
9
NoFlyZone
├── id: NFZ-001
├── geometry: Polygon / MultiPolygon
├── crs: EPSG:4326
├── min_alt_m: 0
├── max_alt_m: 120
├── alt_reference: AMSL / AGL / ellipsoid
├── start_time: 2026-05-26 10:00:00
└── end_time: 2026-05-26 18:00:00

这比一开始就上复杂三维体要容易落地得多,也更适合大多数监管、告警、航线校验场景。

2.2 坐标系到底解决什么问题

坐标系解决的是“一个数字坐标到底代表地球上的哪里”。

同样是:

1
2
3
lon = 116.397
lat = 39.908
height = 80

如果没有坐标参考,这三个数是不完整的。你还必须知道:

  • 经度纬度基于哪个地球椭球?
  • 高度是相对椭球、平均海平面,还是相对起飞点?
  • 坐标是 WGS84、CGCS2000,还是互联网地图使用的 GCJ-02 / BD-09?
  • 计算距离时,是在球面上算,还是投影到平面上算?

空间数据里常见的完整表达是:

1
坐标值 + 坐标参考系统 CRS + 高程基准 + 单位 + 轴顺序

其中 CRS 是 Coordinate Reference System,中文常说坐标参考系统。工程里经常用 EPSG 编码标识,例如:

  • EPSG:4326:WGS84 经纬度坐标。
  • EPSG:3857:Web Mercator,互联网地图瓦片常用。
  • EPSG:4490:CGCS2000 经纬度坐标。

2.3 地理坐标系与投影坐标系

空间坐标系可以先分成两大类:

类型 坐标形式 单位 适合做什么 不适合做什么
地理坐标系 经度、纬度 全球定位、数据交换、存储原始位置 直接做米级距离、面积、缓冲区
投影坐标系 x、y 米或其他线性单位 距离、面积、缓冲区、工程计算 跨大范围全球计算会有变形

地理坐标系把地球当作椭球来表示,经纬度是角度。投影坐标系把地球表面的一部分“摊平”到二维平面上,坐标单位通常是米。

简单理解:

1
2
地理坐标系:适合表达“在哪里”
投影坐标系:适合计算“多远、多大、缓冲多少米”

这也是很多空间计算 bug 的来源:拿经纬度直接做 buffer(100),你以为是 100 米,数据库可能理解成 100 度。

2.4 常用坐标系介绍

坐标系 常见 EPSG 坐标形式 典型用途 注意点
WGS84 EPSG:4326 经度、纬度 GPS、无人机 GNSS、GeoJSON 常见坐标 单位是度,GeoJSON 坐标顺序是 [lon, lat]
CGCS2000 EPSG:4490 经度、纬度 国内测绘、自然资源、政务空间数据 与 WGS84 很接近,但严肃测绘场景不能随意混用
Web Mercator EPSG:3857 x、y 米 Web 地图瓦片、在线地图底图 高纬度面积和距离变形明显,不适合严肃量测
UTM 多个分带 EPSG x、y 米 小范围工程测量、局部距离和面积计算 需要选择正确分带,跨带计算要小心
GCJ-02 无通用 EPSG 经度、纬度 国内互联网地图常见坐标 加偏坐标,不适合作为空间计算权威坐标
BD-09 无通用 EPSG 经度、纬度 百度地图生态 在 GCJ-02 基础上再次偏移
ECEF 常见为地心地固坐标 x、y、z 米 卫星导航、三维地球计算 后端业务查询中不如经纬度直观
ENU / NED 局部坐标 飞控、局部避障、短距离运动学 依赖局部原点,不能直接全局存储

几个工程结论:

  1. 设备上报原始 GNSS 坐标,通常优先存 WGS84 或明确的 CGCS2000。
  2. 用于地图展示时,可以转换为地图厂商要求的坐标。
  3. 用于空间计算时,必须统一坐标系。
  4. 用于距离、面积、缓冲区时,尽量转换到合适的投影坐标系,或者使用支持椭球距离的 geography 类型。
  5. GCJ-02 / BD-09 适合展示对齐互联网地图,不适合做权威计算底座。

2.5 坐标顺序:经纬度还是纬经度

坐标顺序是后端特别容易踩坑的地方。

常见习惯:

场景 顺序 示例
GeoJSON [longitude, latitude] [116.397, 39.908]
WKT x y,地理坐标中通常是 lon lat POINT(116.397 39.908)
很多地图 SDK 入参 可能是 lat, lon LatLng(39.908, 116.397)
数据库 geometry 点 x y ST_MakePoint(lon, lat)

建议在后端对象里不要只写 xy,也不要在接口里随便写数组。低空业务里更推荐显式命名:

1
2
3
4
5
6
7
{
"longitude": 116.397,
"latitude": 39.908,
"altitude": 80.5,
"altitudeRef": "AMSL",
"crs": "EPSG:4326"
}

如果为了 GeoJSON 标准必须使用数组,也要在接口文档中明确:

1
2
3
4
{
"type": "Point",
"coordinates": [116.397, 39.908, 80.5]
}

这里的顺序是经度、纬度、高度。

3. 高程系统:高度不是一个简单数字

3.1 常见高度概念

低空业务里,高度比经纬度更容易混乱。因为“高度 120 米”并不是一个完整说法,必须继续追问:相对哪个基准面 120 米?

可以先把高度理解成“目标点到某个基准面的垂直距离”。不同高度概念的区别,本质就是基准面不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
                    无人机 / Aircraft
*
| AGL:离地高度,关心离当前地面多高
|
地面 / Terrain,DEM/DSM 描述这个面
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 地面海拔,例如 terrain AMSL = 30m
|
-----------------+-------------------------- 平均海平面 / 大地水准面
|
| 大地水准面起伏 N
|
........................................... 参考椭球面 / Ellipsoid

椭球高 h:无人机相对参考椭球面的高度
AMSL / 海拔 H:无人机相对平均海平面或大地水准面的高度
AGL:无人机相对当前位置地面的高度

几个最常见概念可以这样区分:

高度类型 英文常见说法 参考基准 含义 低空场景
椭球高 Ellipsoidal Height,常记作 h 参考椭球面 GNSS 数学模型下的高度,和真实海平面、地面都不是一回事 GPS/GNSS 原始解算、坐标转换底层数据
AMSL / 海拔高 / 正高 Above Mean Sea Level / Orthometric Height,常记作 H 平均海平面、大地水准面或具体高程基准 通常业务说“海拔多少米”指这一类高度 航空规则、地形图、空域高度、轨迹展示
AGL / 离地高 Above Ground Level 当前经纬度对应的地面或地物表面 飞行器距离脚下地面多高 低空限高、避障、贴地飞行、安全高度
相对起飞点高度 Relative to Takeoff Point 起飞点、机巢、遥控器设定的局部零点 起飞后相对起飞点升高了多少 飞控显示、任务执行、操作员视角
气压高度 Pressure Altitude / Barometric Altitude 标准气压面或校准后的气压基准 由气压计换算出的高度,稳定性和校准有关 飞控高度保持、融合定位
正常高 Normal Height 似大地水准面 国内测绘高程系统中常见的高程类型 严肃测绘、国家高程基准相关数据

这里要特别澄清“相对高度”这个词。它不是一个严格唯一的高程基准,而是一个泛称:只要高度是相对某个局部零点给出的,都可能被叫作相对高度。无人机 App 里常见的“相对高度”,多数是相对起飞点或机巢,不应直接等同于 AMSL。AGL 也是一种“相对地面”的高度,但它的基准是当前位置脚下的地面;AMSL 虽然字面上也是“above mean sea level”,但在工程语境里通常归为海拔/绝对高度,而不是飞控里说的“相对高度”。

三者关系可以这样记:

1
2
3
4
5
6
7
8
椭球高 h
= 相对参考椭球面

AMSL / 海拔 H
= 相对平均海平面或大地水准面

AGL / 离地高
= 相对当前位置地面

椭球高和 AMSL 的关系:

1
2
3
4
5
6
h = H + N
H = h - N

h:椭球高
H:AMSL / 海拔高 / 正高
N:大地水准面起伏,也就是大地水准面相对椭球面的高度差

这里的 N 不是常数,不同经纬度位置不同,需要依赖大地水准面模型,例如 EGM96、EGM2008 或国内具体高程基准转换模型。

大地水准面模型可以简单理解为:用一套模型描述“平均海平面如果延伸到陆地下方,会形成一个什么样的起伏面”。它不是纯手工测量出来的,也不是凭空算出来的,而是由卫星重力测量、地面重力测量、海洋测高、水准测量、GNSS 联测点等观测数据,再结合地球重力场理论和数学拟合计算得到的。工程上可以把它当成一个查询模型:

1
2
3
4
输入:经度、纬度
输出:该位置的大地水准面起伏 N

AMSL / 海拔 H = 椭球高 h - N

所以,GNSS 给出的椭球高要转成更接近业务习惯的海拔高度,就需要通过大地水准面模型查出当前位置的 N。不同模型、不同区域、不同精度等级会带来不同转换结果,严肃测绘或监管场景不能随意混用模型。

AMSL 和 AGL 的关系:

1
2
3
4
AGL = aircraft_AMSL - terrain_AMSL

aircraft_AMSL:无人机海拔
terrain_AMSL:当前位置地面海拔,通常来自 DEM/DSM/地形服务

如果无人机只有椭球高,也可以先转成 AMSL,再算 AGL:

1
2
aircraft_AMSL = aircraft_ellipsoid_height - geoid_undulation
AGL = aircraft_AMSL - terrain_AMSL

举个例子:

1
2
3
4
5
6
无人机 GNSS 椭球高 h = 165m
当地大地水准面起伏 N = 35m
当前位置地面海拔 terrain_AMSL = 30m

无人机 AMSL = h - N = 165 - 35 = 130m
无人机 AGL = 130 - 30 = 100m

所以同一架无人机在同一时刻,可以同时有:

1
2
3
4
椭球高:165m
AMSL:130m
AGL:100m
相对起飞点高度:如果起飞点海拔为 45m,且按起飞点归零,则可能是 85m

这些数值都可能“正确”,但它们描述的是相对不同基准面的高度。空间计算里真正危险的不是高度值本身,而是拿 AGL 的规则去比较 AMSL 的数据,或者拿相对起飞点高度去比较空域海拔高度

实际工程里还要考虑具体高程基准、模型版本、DEM 精度、地形变化、建筑物高度、传感器误差和区域差异,不能只拿公式硬算所有场景。低空系统最稳妥的做法是:原始高度照实保存,明确 altitude_ref,计算时再转换到规则要求的高度基准。

3.2 低空场景下该用哪种高度

场景 推荐高度表达 原因
原始遥测入库 保留设备原始高度字段,并记录高度基准 避免后续无法追溯
监管空域限高 通常需要明确 AMSL 或 AGL 空域规则必须知道高度相对什么
禁飞区高度范围 min_alt_m + max_alt_m + alt_reference 不同规则可能基准不同
飞控实时控制 常见相对高度、气压高度、融合高度 飞控侧更关注局部稳定性
地形避障 AGL 或 AMSL + DEM 地形高程 需要知道离地高度
轨迹回放展示 可展示相对高度,也可展示海拔 看业务用户习惯

低空系统建议至少保存以下字段:

1
2
3
4
5
6
7
longitude
latitude
altitude
altitude_ref -- ellipsoid / AMSL / AGL / relative_takeoff / pressure
height_source -- GNSS / barometer / fused / manual / terrain
takeoff_altitude -- 如果有相对起飞点高度,记录起飞点基准
terrain_altitude -- 如果计算过地形高程,可单独保存

不要只存一个 height 字段。字段名越模糊,后面的空间判断越容易出事故。

3.3 高度基准不一致会带来什么问题

举个例子:

1
2
3
4
5
6
禁飞区规则:0m - 120m AGL 禁飞
无人机上报:altitude = 130m AMSL
当地地面海拔:30m AMSL

无人机 AGL = 130 - 30 = 100m
结论:它实际在禁飞高度范围内

如果后端直接拿 130 <= 120 判断,就会误判为不在禁飞范围。

所以低空空间计算里,常见做法是:

  1. 入库保留原始高度和高度基准。
  2. 计算时转换到规则要求的高度基准。
  3. 对于 AGL 判断,需要叠加 DEM、DSM 或地形服务。
  4. 告警结果中记录转换过程,方便追溯。

4. 空间数据模型

4.1 矢量数据

矢量数据用点、线、面表达现实世界的离散对象。

类型 典型对象 低空场景
Point 无人机当前位置、起降点、通信基站
MultiPoint 多点 一组候选起降点
LineString 线 航线、历史轨迹
MultiLineString 多线 多段航线、道路网络
Polygon 禁飞区、任务区、行政边界
MultiPolygon 多面 多块分离的管控区域
GeometryCollection 几何集合 混合空间对象,不建议滥用

矢量数据适合表达边界清晰的对象,例如空域、围栏、航线。PostGIS、GeoJSON、KML 都主要围绕矢量模型展开。

4.2 栅格数据

栅格数据把空间切成规则像素或网格,每个格子有一个值。

1
2
3
4
5
6
7
+----+----+----+----+
| 12 | 13 | 15 | 18 |
+----+----+----+----+
| 10 | 11 | 14 | 16 |
+----+----+----+----+
| 8 | 9 | 12 | 15 |
+----+----+----+----+

典型栅格数据:

  • DEM:数字高程模型,表示地形高度,不包含建筑物、树木、电线塔等地表附着物。
  • DSM:数字表面模型,包含建筑物、树木等表面高度。
  • 影像瓦片:卫星影像、航拍影像。
  • 风场、降雨、温度、风险热力图。
  • 通信覆盖强度图。

低空场景中,栅格数据常用于:

  • 判断无人机离地高度。
  • 计算地形遮挡和障碍物风险。
  • 生成区域风险热力图。
  • 做通信覆盖、气象影响、人口密度叠加分析。

4.3 矢量和栅格怎么配合

很多真实业务不是单一模型能解决的:

flowchart TD
    A["无人机实时点位<br/>Point"] --> D["空间风险判断"]
    B["禁飞区 / 空域<br/>Polygon"] --> D
    C["地形 / 建筑物 / 风场<br/>Raster"] --> D
    D --> E["是否告警 / 是否改航 / 是否限高"]

例如判断一架无人机是否低于安全离地高度:

  1. 遥测点给出经纬度和海拔高度。
  2. DEM 根据经纬度查出地面海拔。
  3. AGL = 无人机海拔 - 地面海拔
  4. 如果 AGL 小于安全阈值,则触发低高度风险。

5. 常见空间数据格式与协议

5.1 GeoJSON

GeoJSON可视化 | Powered by Mapbox
GeoJSON | Help | Mapbox

GeoJSON 是 Web 后端最常见的空间数据交换格式之一,基于 JSON,易读、易调试、易通过 HTTP API 传输。

GeoJSON 的基本结构可以分成三层:

1
2
3
4
5
6
7
8
Geometry
只表达几何形状,例如 Point、LineString、Polygon

Feature
geometry + properties,表达一个带业务属性的空间对象

FeatureCollection
多个 Feature 的集合,常用于接口返回和文件交换

5.1.1 常用空间类型

常用空间类型:

类型 含义 低空场景例子
Point 单个点 无人机当前位置、停机坪、机巢、基站
MultiPoint 多个点 一组候选起降点、一组巡检采样点
LineString 一条连续线 航线、历史轨迹、巡检线路
MultiLineString 多条线 多段备选航线、多条巡检线路
Polygon 一个面,可以有洞 禁飞区、任务区、电子围栏
MultiPolygon 多个面 多块不连续禁飞区、多个作业片区
GeometryCollection 多种几何混合集合 少用,容易让接口和入库逻辑复杂

一个点:

1
2
3
4
{
"type": "Point",
"coordinates": [116.397, 39.908]
}

一条线,适合表达航线或轨迹:

1
2
3
4
5
6
7
8
{
"type": "LineString",
"coordinates": [
[116.280, 39.880],
[116.350, 39.930],
[116.500, 39.970]
]
}

一个面,适合表达任务区、禁飞区、电子围栏:

1
2
3
4
5
6
7
8
9
10
11
12
{
"type": "Polygon",
"coordinates": [
[
[116.300, 39.900],
[116.420, 39.900],
[116.420, 40.000],
[116.300, 40.000],
[116.300, 39.900]
]
]
}

注意:Polygon 的环必须闭合,也就是第一个坐标点和最后一个坐标点一致。

Polygon 的坐标是三层数组:

1
2
3
4
5
6
7
Polygon
└── rings
├── outer ring
│ ├── [lon, lat]
│ ├── [lon, lat]
│ └── ...
└── inner ring,也就是洞,可选

带洞的面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"type": "Polygon",
"coordinates": [
[
[116.300, 39.900],
[116.500, 39.900],
[116.500, 40.100],
[116.300, 40.100],
[116.300, 39.900]
],
[
[116.360, 39.960],
[116.420, 39.960],
[116.420, 40.020],
[116.360, 40.020],
[116.360, 39.960]
]
]
}

这里第一组环是外边界,第二组环是洞。业务上可以理解为:“大区域内有一块排除区域”。

多个面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"type": "MultiPolygon",
"coordinates": [
[
[
[116.300, 39.900],
[116.350, 39.900],
[116.350, 39.950],
[116.300, 39.950],
[116.300, 39.900]
]
],
[
[
[116.450, 39.980],
[116.500, 39.980],
[116.500, 40.030],
[116.450, 40.030],
[116.450, 39.980]
]
]
]
}

一个带属性的要素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[116.30, 39.90],
[116.40, 39.90],
[116.40, 40.00],
[116.30, 40.00],
[116.30, 39.90]
]
]
},
"properties": {
"name": "示例禁飞区",
"minAlt": 0,
"maxAlt": 120
}
}

多个要素组成集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [116.397, 39.908]
},
"properties": {
"type": "drone",
"droneId": "DRONE-001",
"altitude": 95.2,
"altitudeRef": "AMSL"
}
},
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[116.280, 39.880],
[116.350, 39.930],
[116.500, 39.970]
]
},
"properties": {
"type": "route",
"routeId": "ROUTE-001",
"minAlt": 80,
"maxAlt": 110
}
}
]
}

5.1.2 GeoJSON 能表达带高度的点吗

可以。GeoJSON 资料里常见示例大多只有二维坐标:

1
2
3
4
{
"type": "Point",
"coordinates": [116.397, 39.908]
}

但 GeoJSON 的坐标位置并不只能有两个数字。一个位置可以带第三个值,工程上通常把它理解为高度:

1
2
3
4
{
"type": "Point",
"coordinates": [116.397, 39.908, 95.2]
}

顺序一般按下面理解:

1
2
[longitude, latitude, altitude]
[经度, 纬度, 高度]

因此,无人机实时点位可以这样表达:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [116.397, 39.908, 95.2]
},
"properties": {
"type": "drone",
"droneId": "DRONE-001",
"altitudeRef": "AMSL",
"heightSource": "GNSS"
}
}

这里要特别注意:GeoJSON 可以放第三个高度值,但它不会自动说明这个高度是 AMSLAGL、椭球高,还是相对起飞点高度。所以低空业务里不要只传 [lon, lat, alt],最好同时在 properties 里写清楚:

1
2
3
4
{
"altitudeRef": "AMSL",
"heightSource": "GNSS"
}

线也可以带高度,适合表达带高度的航线或轨迹:

1
2
3
4
5
6
7
8
{
"type": "LineString",
"coordinates": [
[116.280, 39.880, 80],
[116.350, 39.930, 100],
[116.500, 39.970, 110]
]
}

但是,GeoJSON 支持第三维坐标,不等于它能完整表达三维空域体。对于禁飞区、限高区、任务区这类低空对象,更推荐:

1
2
3
二维 Polygon 表达平面范围
properties.minAltM / maxAltM 表达高度范围
properties.altitudeRef 表达高度基准

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[116.300, 39.900],
[116.420, 39.900],
[116.420, 40.000],
[116.300, 40.000],
[116.300, 39.900]
]
]
},
"properties": {
"type": "no_fly_zone",
"minAltM": 0,
"maxAltM": 120,
"altitudeRef": "AGL"
}
}

前端兼容性也要分开看:

问题 结论
GeoJSON 能不能解析 [lon, lat, alt] 多数库可以解析,不会因为第三个值报错
二维地图会不会用高度渲染 很多二维地图只用前两个值,第三个值会被忽略
能不能只靠 GeoJSON 表达三维空域 不适合,标准 GeoJSON 没有真正的三维体模型
需要三维可视化怎么办 结合 Cesium、Mapbox 3D、deck.gl、Three.js、3D Tiles 等能力

所以,低空系统里可以用 GeoJSON 第三个坐标值表达无人机点位或轨迹高度;但空域规则、禁飞区高度范围,更建议放在 properties 中明确建模,后端计算时再按 altitudeRef 做统一转换。

5.1.3 GeoJSON 能表达圆吗

严格来说,GeoJSON 标准几何类型里没有 Circle。也就是说,下面这种写法不是标准 GeoJSON 几何:

1
2
3
4
5
{
"type": "Circle",
"coordinates": [116.397, 39.908],
"radius": 500
}

工程里表达“圆”通常有两种做法。

第一种是业务对象用“圆心 + 半径”表达:

1
2
3
4
5
6
7
8
9
10
11
12
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [116.397, 39.908]
},
"properties": {
"shape": "circle",
"radiusM": 500,
"name": "停机坪 500 米服务范围"
}
}

这种方式适合:

  • 后端规则配置。
  • 数据库存储业务规则。
  • 用 PostGIS ST_DWithin 做半径判断。
  • 用 Redis GEO 做附近查询。

它的优点是语义清楚、半径不会因为多边形顶点数而失真;缺点是标准 GeoJSON 渲染器不一定会自动把它画成圆,因为几何本身只是一个点,圆的含义藏在 properties.radiusM 里。

第二种是用 Polygon 近似表达圆:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[116.40285, 39.90800],
[116.40114, 39.91118],
[116.39700, 39.91249],
[116.39286, 39.91118],
[116.39115, 39.90800],
[116.39286, 39.90482],
[116.39700, 39.90351],
[116.40114, 39.90482],
[116.40285, 39.90800]
]
]
},
"properties": {
"shape": "circle_approx",
"center": [116.397, 39.908],
"radiusM": 500
}
}

这种方式适合:

  • 地图前端直接渲染。
  • 导出给只认识标准 GeoJSON 几何的系统。
  • 和其他多边形做相交、包含、裁剪等空间计算。

它的缺点是“圆”变成了“多边形近似圆”。顶点越多越接近圆,但数据越大;顶点越少越轻,但边界越粗糙。

低空后端建议这样取舍:

场景 推荐表达
规则存储 圆心 Point + radiusM
半径查询 ST_DWithin / Redis GEO,不必先生成圆面
地图展示 前端按圆心半径绘制,或后端生成 Polygon
与禁飞区、任务区做面关系计算 生成 Polygon 近似圆后再计算
需要严谨测距 使用 geography 或合适投影,不要直接在经纬度上按度数扩圆

例如“无人机是否进入停机坪 500 米服务范围”,后端更推荐:

1
2
3
4
5
6
7
SELECT id, name
FROM landing_pad
WHERE ST_DWithin(
geom::geography,
ST_SetSRID(ST_MakePoint(116.397, 39.908), 4326)::geography,
500
);

而不是先把停机坪生成一个 GeoJSON 圆形多边形,再做点面判断。

注意点:

  • 坐标顺序是 [longitude, latitude],不是 [latitude, longitude]
  • GeoJSON 默认使用 WGS84 经纬度表达。
  • 多边形外环和内环有方向约定,但不同库容错程度不同,入库前最好做几何修复。
  • Polygon 的环必须闭合,也就是第一个坐标点和最后一个坐标点一致。
  • GeoJSON 没有标准 Circle 类型;圆要么作为业务语义使用圆心 + 半径,要么转成多边形近似表达。
  • 大量轨迹点用 GeoJSON 直接传输会比较重,实时链路可使用 Protobuf、FlatBuffers 或自定义二进制协议。

5.2 WKT / WKB / EWKT

WKT 是 Well-Known Text,适合在 SQL、日志、调试和接口测试中表达几何对象。

1
2
3
POINT(116.397 39.908)
LINESTRING(116.397 39.908, 116.410 39.920)
POLYGON((116.30 39.90, 116.40 39.90, 116.40 40.00, 116.30 40.00, 116.30 39.90))

WKB 是 Well-Known Binary,是二进制表达,适合机器传输和存储。

EWKT 是 PostGIS 扩展形式,可以带 SRID:

1
SRID=4326;POINT(116.397 39.908)

工程上常见选择:

格式 优点 缺点 适合场景
WKT 易读、易复制、适合 SQL 体积较大 调试、脚本、后台管理
WKB 紧凑、解析快 不易读 系统间高效传输
EWKT 能携带 SRID 偏 PostGIS 生态 PostGIS 调试和导入

5.3 KML / KMZ

KML 常用于 Google Earth、三维地球、航线展示和空间标注交换。KMZ 本质上是压缩后的 KML 包,可以包含图标、贴图等资源。

KML 的特点:

  • XML 格式,可读性比 JSON 差一些,但生态成熟。
  • 支持点、线、面、样式、文件夹、时间等表达。
  • 很适合和三维地球类工具交互。
  • 文件体积大时解析成本较高。

低空场景里,KML/KMZ 常用于:

  • 导入导出航线。
  • 在三维地球上查看任务区、禁飞区、轨迹。
  • 和外部测绘、规划工具交换标注数据。

5.4 Shapefile、GeoPackage、GeoTIFF、MVT

格式 类型 说明 适合场景
Shapefile 矢量 老牌 GIS 文件格式,通常由多个文件组成 存量测绘数据交换
GeoPackage 矢量/栅格 基于 SQLite 的单文件空间数据包 离线包、移动端、数据交付
GeoTIFF 栅格 带地理参考信息的 TIFF DEM、影像、栅格分析
MVT 矢量瓦片 Mapbox Vector Tile,面向地图切片渲染 大规模 Web 地图展示
CSV + lon/lat 表格 简单点位数据交换 临时导入、运营配置
3D Tiles 三维 大规模三维模型和倾斜摄影数据 三维城市、建筑物、实景三维

后端系统不要只认 GeoJSON。实际项目里,经常会遇到测绘单位给 Shapefile、影像单位给 GeoTIFF、前端地图要 MVT、三维平台要 3D Tiles。

5.5 OGC 服务协议

OGC 是 Open Geospatial Consortium,很多 GIS 服务协议都来自这个生态。

协议 作用 类比理解
WMS 返回地图图片 服务端渲染好一张图给前端
WMTS 返回地图瓦片图片 按瓦片级别、行列号取图
WFS 返回矢量要素 查询空间对象数据
WCS 返回栅格覆盖数据 查询 DEM、影像等覆盖数据
CSW 元数据目录服务 查有哪些空间数据集

现代 Web 地图也大量使用瓦片服务:

1
2
/{z}/{x}/{y}.png       -- 栅格瓦片
/{z}/{x}/{y}.pbf -- 矢量瓦片 MVT

后端工程里可以这样分工:

  • 业务 API 返回任务、空域、设备状态。
  • GIS 服务返回底图、影像、专题图层。
  • 空间数据库负责权威空间查询。
  • 地图前端负责图层叠加和交互。

6. 空间数据库与存储选型

6.1 PostgreSQL + PostGIS

PostGIS 官方中文手册

PostGIS 是 PostgreSQL 的空间扩展。它让 PostgreSQL 能存储 geometry/geography,建立空间索引,并执行空间函数。

适合 PostGIS 的数据:

  • 禁飞区、限飞区、空域边界。
  • 起降点、机巢、基站、任务区。
  • 航线、历史轨迹、行政区划。
  • 需要事务一致性和复杂查询的空间业务数据。

不适合直接全部塞进 PostGIS 的数据:

  • 每秒几十万条以上的原始遥测点,尤其是只做回放和聚合的冷数据。
  • 极低延迟的实时位置热点查询。
  • 需要强压缩、降采样、保留策略的时序数据。

典型表设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CREATE EXTENSION IF NOT EXISTS postgis;

CREATE TABLE no_fly_zone (
id bigserial PRIMARY KEY,
name text NOT NULL,
geom geometry(MultiPolygon, 4326) NOT NULL,
min_alt_m numeric(10, 2),
max_alt_m numeric(10, 2),
alt_ref text NOT NULL DEFAULT 'AMSL',
valid_from timestamptz,
valid_to timestamptz,
properties jsonb DEFAULT '{}'::jsonb
);

CREATE INDEX idx_no_fly_zone_geom
ON no_fly_zone
USING gist (geom);

PostGIS 的核心能力:

  • 空间类型:geometrygeographyraster
  • 空间索引:GiST、SP-GiST、BRIN 等。
  • 空间关系:相交、包含、覆盖、邻接、距离。
  • 空间构造:缓冲区、凸包、包络框、简化。
  • 坐标转换:不同 SRID 之间转换。

6.2 geometry 与 geography

PostGIS 中最常用的是 geometrygeography

类型 计算模型 单位 优点 注意点
geometry 平面几何 取决于坐标系 功能丰富、性能好、适合投影坐标 经纬度下距离单位是度
geography 椭球地理 适合经纬度上的距离和面积 函数支持比 geometry 少,计算更重

常见选择:

  • 存储权威边界:geometry(MultiPolygon, 4326)
  • 小范围距离/缓冲:先 ST_Transform 到本地投影坐标,再 ST_Buffer / ST_DWithin
  • 全球或跨区域经纬度距离:考虑 geography

示例:

1
2
3
4
5
-- 经纬度 geometry 转 geography 后,以米为单位计算距离
SELECT ST_Distance(
ST_SetSRID(ST_MakePoint(116.397, 39.908), 4326)::geography,
ST_SetSRID(ST_MakePoint(116.410, 39.920), 4326)::geography
);

6.3 Redis Geo

Redis Geo 适合做内存型、实时、低延迟的附近查询。

常见命令:

1
2
3
4
GEOADD drone:live 116.397 39.908 drone-001
GEOADD drone:live 116.410 39.920 drone-002

GEOSEARCH drone:live FROMLONLAT 116.400 39.910 BYRADIUS 5 km WITHDIST

适合场景:

  • 查询某个点附近的无人机、机巢、起降点。
  • 实时大屏展示当前在线设备。
  • 调度系统快速找最近资源。

限制:

  • Redis Geo 主要处理经纬度点位,不适合复杂多边形拓扑判断。
  • 高度、速度、航向等属性需要配合 Hash、String 或其他结构保存。
  • 数据是内存热点状态,不应作为权威空间库。
  • 经纬度范围和精度要符合 Redis Geo 底层 geohash 编码约束。

常见组合:

1
2
3
drone:geo                 -- GEO,保存实时经纬度
drone:state:{drone_id} -- HASH,保存高度、速度、航向、电量、状态
drone:last:{drone_id} -- STRING/JSON,保存最新完整遥测

6.4 时序数据库 TSDB

无人机遥测是典型时序数据:

1
drone_id + timestamp + lon + lat + alt + speed + heading + battery + status

特点:

  • 写入频率高。
  • 数据按时间追加。
  • 查询常按设备和时间范围过滤。
  • 需要降采样、压缩、保留策略。
  • 轨迹回放、统计报表、异常分析都依赖历史数据。

常见选择:

方案 特点 适合场景
TimescaleDB PostgreSQL 扩展,SQL 友好,可结合 PostGIS 既要时序又要空间 SQL
InfluxDB 专用时序库,写入和保留策略成熟 指标、遥测、监控
ClickHouse 列式分析强,压缩和聚合性能好 海量轨迹分析、离线统计
PostgreSQL 分区表 架构简单,生态统一 中小规模或早期阶段

一个务实的分层方案:

1
2
3
4
5
最新状态:Redis / 内存状态表
近实时明细:Kafka + Flink 状态
权威空间对象:PostgreSQL + PostGIS
历史轨迹:TimescaleDB / ClickHouse / 对象存储
离线分析:湖仓 / Spark / Flink Batch

7. PostGIS 常见函数

7.1 ST_Intersects:是否相交

ST_Intersects(A, B) 判断两个几何对象是否有任意空间交集。

适合问题:

  • 航线是否穿越禁飞区。
  • 任务区是否与管控区重叠。
  • 无人机缓冲区是否碰到障碍区。

示例:

1
2
3
4
5
6
7
-- 查询与某条航线相交的禁飞区
SELECT z.id, z.name
FROM no_fly_zone z
WHERE ST_Intersects(
z.geom,
ST_GeomFromText('LINESTRING(116.30 39.90, 116.50 40.00)', 4326)
);

注意:

  • ST_Intersects 包含边界接触,只要有交集就返回 true。
  • 大量查询时要确保 geom 有 GiST 索引。
  • 参与计算的几何对象 SRID 应一致。

7.2 ST_Contains:是否包含

ST_Contains(A, B) 判断 A 是否包含 B。

适合问题:

  • 禁飞区是否包含无人机当前位置。
  • 任务区是否包含某个起降点。
  • 行政区是否包含某个设施点。

示例:

1
2
3
4
5
6
7
-- 查询某个点落在哪些禁飞区内
SELECT z.id, z.name
FROM no_fly_zone z
WHERE ST_Contains(
z.geom,
ST_SetSRID(ST_MakePoint(116.397, 39.908), 4326)
);

注意一个细节:ST_Contains 对边界比较严格。如果点正好落在多边形边界上,结果可能不是你业务上想要的“在区域内”。很多业务更适合使用:

1
ST_Covers(zone.geom, point_geom)

ST_Covers 更接近“包含内部和边界”的业务语义。

7.3 ST_Buffer:缓冲区

ST_Buffer(geom, distance) 生成一个距离原几何对象一定范围的面。

适合问题:

  • 给航线生成安全走廊。
  • 给起降点生成服务半径。
  • 给障碍物生成避让区。

错误示例:

1
2
3
-- 如果 geom 是 EPSG:4326,经纬度单位是度,这里的 100 不是 100 米
SELECT ST_Buffer(geom, 100)
FROM route;

更合理的思路:

1
2
3
4
5
6
7
8
9
-- 示例:先转到一个以米为单位的投影坐标系,再做 100 米缓冲
SELECT ST_Transform(
ST_Buffer(
ST_Transform(geom, 3857),
100
),
4326
)
FROM route;

注意:EPSG:3857 虽然单位是米,但距离仍有变形。严肃工程计算应选择更适合当地的投影坐标系,例如合适的 UTM 分带或地方投影。

7.4 ST_DWithin:距离范围内

ST_DWithin(A, B, distance) 判断两个对象距离是否在给定范围内。

适合问题:

  • 查询某个点 5 公里内的起降点。
  • 判断无人机是否接近禁飞区边界。
  • 判断两架无人机是否过近。

示例:

1
2
3
4
5
6
7
8
-- geography 写法,距离单位是米
SELECT p.id, p.name
FROM landing_point p
WHERE ST_DWithin(
p.geom::geography,
ST_SetSRID(ST_MakePoint(116.397, 39.908), 4326)::geography,
5000
);

相比 ST_Distance(A, B) < 5000ST_DWithin 更容易利用空间索引和短路优化,是范围查询里更推荐的写法。

7.5 ST_Transform:坐标转换

ST_Transform(geom, srid) 把几何对象从当前坐标系转换到目标坐标系。

示例:

1
2
3
-- WGS84 经纬度转 Web Mercator
SELECT ST_Transform(geom, 3857)
FROM no_fly_zone;

注意:

  • ST_Transform 的前提是原始 geometry 已经有正确 SRID。
  • 如果只是给没有 SRID 的 geometry 打标签,应该用 ST_SetSRID
  • ST_SetSRID 不会改变坐标数值,ST_Transform 才会真正转换坐标。

对比:

1
2
3
4
5
6
7
8
-- 只是声明这个点属于 EPSG:4326
SELECT ST_SetSRID(ST_MakePoint(116.397, 39.908), 4326);

-- 把 EPSG:4326 的点转换到 EPSG:3857
SELECT ST_Transform(
ST_SetSRID(ST_MakePoint(116.397, 39.908), 4326),
3857
);

7.6 一个低空禁飞区查询示例

假设禁飞区是二维多边形,同时带高度上下限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 入参:
-- :lon = 无人机经度
-- :lat = 无人机纬度
-- :alt = 已转换到与规则一致的高度,例如 AMSL
-- :ts = 遥测时间

WITH p AS (
SELECT
ST_SetSRID(ST_MakePoint(:lon, :lat), 4326) AS geom,
:alt::numeric AS alt,
:ts::timestamptz AS ts
)
SELECT z.id, z.name
FROM no_fly_zone z, p
WHERE ST_Covers(z.geom, p.geom)
AND p.alt >= z.min_alt_m
AND p.alt <= z.max_alt_m
AND (z.valid_from IS NULL OR p.ts >= z.valid_from)
AND (z.valid_to IS NULL OR p.ts <= z.valid_to);

这里的本质是:

1
2
3
二维:点是否落在多边形内
高度:alt 是否落在 [min_alt, max_alt]
时间:ts 是否落在规则有效期内

这就是低空业务里非常常见的 2.5D 空间判断。

8. 数据接入与实时空间计算

8.1 高频遥测数据的特点

无人机遥测数据通常包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
drone_id
timestamp
longitude
latitude
altitude
altitude_ref
speed
heading
vertical_speed
roll / pitch / yaw
battery
link_status
mission_id

如果有 10 万架设备,每架每秒 1 条遥测,就是每秒 10 万条。如果每架每秒 5 条,就是每秒 50 万条。这个量级下,不能把每条数据都同步写入复杂空间数据库再计算告警。

实时链路通常要分层:

sequenceDiagram
    participant D as 无人机/机巢
    participant G as 接入网关(EMQX)
    participant K as Kafka
    participant F as Flink
    participant R as Redis
    participant P as PostGIS
    participant A as 告警服务

    D->>G: 遥测上报
    G->>K: 写入 telemetry topic
    F->>K: 消费遥测流
    F->>P: 加载/查询空域规则
    F->>F: 坐标归一化 + 网格定位 + 点面/高度判断
    F->>R: 更新最新状态和热点告警状态
    F->>A: 输出告警事件

8.2 接入层:高并发和低延迟

接入层要解决的是“设备如何稳定上来,消息如何可靠进入后端”。

常见协议选择:

协议 特点 适合场景
MQTT 轻量、发布订阅、适合弱网设备 遥测、状态、命令下发
HTTP 简单、生态成熟 低频上报、管理接口
WebSocket 双向长连接 大屏、地面站、实时控制界面
gRPC 强类型、性能好 服务间通信、平台内部接口
UDP / QUIC 低延迟 特定实时链路,需要自定义可靠性

遥测上报和控制指令通常要分开设计:

1
2
遥测上报:高频、可丢少量、重吞吐、重顺序
控制指令:低频、不能乱序、要确认、重低延迟和可靠性

不要把控制指令简单混在遥测流里。遥测链路可以追求吞吐,控制链路更关注确认、幂等、超时、重试和权限。

8.3 Kafka 在空间实时链路中的作用

Kafka 不做空间计算,它解决的是流数据进入计算系统前的缓冲和组织。

典型 topic:

1
2
3
4
5
6
telemetry.raw           -- 原始遥测
telemetry.normalized -- 坐标和字段归一化后的遥测
drone.state.updated -- 设备状态变更
spatial.alert -- 空间告警事件
command.downlink -- 控制指令
command.ack -- 指令确认

分区键选择很关键:

数据 推荐 key 原因
单机遥测 drone_id 保证同一无人机的消息有序
区域聚合 grid_id 方便同一空间网格聚合
任务事件 mission_id 保证同一任务事件顺序
告警事件 drone_idzone_id 看消费侧按设备还是按区域处理

如果要做轨迹判断,同一无人机的遥测顺序非常重要。通常应保证同一 drone_id 进入同一 Kafka 分区。

Flink 适合做有状态流计算。低空场景里常见计算包括:

  • 维护每架无人机最新状态。
  • 计算速度、航向、垂直速度异常。
  • 判断点是否进入禁飞区。
  • 判断航线是否偏离规划路径。
  • 按 H3/S2 网格聚合区域内设备数量。
  • 对告警做去重、抑制、升级。
  • 对未来轨迹做短期预测和冲突检测。

一个典型 Flink 处理流程:

flowchart TD
    A["Kafka: telemetry.raw"] --> B["解析与校验"]
    B --> C["坐标归一化<br/>CRS / 高度基准 / 时间"]
    C --> D["异常过滤<br/>漂移点 / 重复点 / 延迟点"]
    D --> E["空间索引匹配<br/>网格候选区 / R-tree / 广播规则"]
    E --> F["空间关系判断<br/>点面 / 距离 / 高度 / 时间"]
    F --> G["状态机<br/>正常 -> 预警 -> 告警 -> 恢复"]
    G --> H["输出告警事件"]
    G --> I["更新 Redis 最新状态"]
    G --> J["写入时序库"]

是否一定要上 Flink,要看计算复杂度和团队维护能力。Flink 的价值不是“能消费 Kafka”,Go 服务同样可以订阅 Kafka、计算告警、写 Redis/DB、再推送到目标 topic。Flink 真正解决的是有状态流计算、窗口统计、乱序数据、checkpoint 容错、规则流广播、任务失败恢复和大规模并行扩展这些问题。

如果团队规模较小,没有专门人力维护 Flink、YARN/Kubernetes 作业、checkpoint、状态后端和作业发布体系,早期可以优先用 Go 业务代码实现:

1
2
3
4
5
6
7
Kafka telemetry topic
-> Go consumer group
-> 解析遥测
-> 内存/Redis/PostGIS 查询规则
-> 判断电子围栏、越界、接近风险
-> 写 Redis / TSDB / PostgreSQL
-> 推送 spatial.alert topic

适合直接用 Go 的情况:

  • 数据量还在业务服务可承受范围内。
  • 逻辑主要是“消费一条、判断一条、写出一条”。
  • 状态较简单,例如只维护每架无人机的最新位置和最近一次告警状态。
  • 乱序、迟到、重放后的精确恢复要求不高。
  • 团队更熟悉 Go、Kafka、Redis、PostGIS,不想过早引入大数据运维成本。

更适合引入 Flink 的情况:

  • 每秒遥测量达到几万、几十万,Go 消费者扩容和状态分片开始变复杂。
  • 需要按无人机、任务、H3/S2 网格维护长期状态。
  • 需要事件时间、watermark、窗口聚合,例如每 10 秒统计每个网格内无人机数量。
  • 需要动态规则流和遥测流关联,例如禁飞区变更后实时广播到所有计算节点。
  • 需要 checkpoint/savepoint,任务失败后能从一致状态恢复,尽量不漏告警、不重复告警。

简单说:早期业务闭环优先 Go,复杂流式状态和高吞吐场景再上 Flink。不要为了“架构完整”提前引入一套团队维护不了的大数据体系。

Flink 里处理空间规则时,常见有两种方式:

  1. 广播规则表:禁飞区、空域规则不大时,把规则广播到各个 task,本地内存计算。
  2. 网格候选过滤:先把无人机点映射到 H3/S2 网格,只拿相关网格的候选空域,再做精确几何判断。

如果每条遥测都去 PostGIS 查一次,吞吐和延迟很容易崩。正确思路通常是:

1
2
3
4
PostGIS 保存权威规则
Flink 定期加载或订阅规则变更
Flink 内部做候选过滤和实时判断
PostGIS 用于管理、回溯、离线校验

8.5 告警不是布尔判断,而是状态机

空间告警不能只做:

1
2
if in_zone:
send_alert()

否则无人机在边界附近抖动时,会不停产生告警风暴。

更合理的状态机:

stateDiagram-v2
    [*] --> Normal
    Normal --> Warning: 接近边界 / 预计进入
    Warning --> Alarm: 已进入禁飞区
    Alarm --> Recovering: 离开禁飞区
    Recovering --> Normal: 连续安全 N 秒
    Warning --> Normal: 风险解除
    Alarm --> Alarm: 持续在区内,周期性续报

工程上常用技巧:

  • 边界外扩一层预警缓冲区。
  • 告警恢复需要连续安全一段时间。
  • 同一设备同一区域做去重。
  • 告警事件记录进入时间、离开时间、最大侵入深度、最高风险等级。
  • 对延迟数据和乱序数据单独处理,避免旧数据覆盖新状态。

9. 空间网格化:把连续空间切成可计算单元

9.1 为什么需要空间网格

经纬度是连续空间,直接处理很灵活,但在高并发实时系统里不够“工程友好”。空间网格化就是把地球表面切成一系列有编号的格子。

1
2
3
4
5
连续坐标:
lon=116.397, lat=39.908

网格化:
grid_id = 8a2a1072b59ffff

网格化能解决很多后端问题:

  • 把空间位置变成字符串或整数 ID,方便分区、索引、聚合。
  • 快速统计某个区域当前有多少无人机。
  • 把点面精确判断前置为候选过滤。
  • 做热力图、密度图、风险图。
  • 支持 Kafka 按网格分区、Redis 按网格缓存。
  • 降低空间 join 的复杂度。

一个常见模式:

1
点位 -> 计算 grid_id -> 查询 grid_id 对应候选空域 -> 再做精确几何判断

不要把网格判断当作最终几何判断。网格通常用于加速和聚合,最终边界附近仍要回到精确几何。

9.2 H3 简介

H3 是 Uber 开源的全球离散网格系统,主要使用六边形网格,并包含少量五边形单元来铺满球面。

H3 的特点:

  • 多层级分辨率,从粗到细。
  • 大多数格子是六边形,邻接关系比较均匀。
  • 每个格子有唯一 ID。
  • 支持经纬度转 H3、H3 转边界、多级父子单元、邻居查询。
  • 很适合做空间聚合、热力图、附近扩散、候选区域过滤。

H3 层级可以粗略理解为:

1
2
resolution 越小:格子越大,适合城市/区域级聚合
resolution 越大:格子越小,适合街区/局部精细判断

低空场景应用:

场景 H3 用法
实时密度统计 每条遥测映射到 H3,按 grid_id 聚合数量
风险热力图 每个格子维护风险分、无人机数、告警数
候选禁飞区过滤 禁飞区预先 polyfill 成 H3 集合
调度分片 按 H3 网格分配计算任务
附近搜索 查询当前格子及 k-ring 邻居

H3 候选过滤流程:

flowchart TD
    A["禁飞区 Polygon"] --> B["H3 Polyfill"]
    B --> C["zone_id -> h3_cells"]
    D["无人机点位"] --> E["lat/lon -> h3_cell"]
    E --> F["按 h3_cell 找候选 zone"]
    F --> G["PostGIS / JTS 精确点面判断"]
    G --> H["输出是否进入"]

这个流程可以显著减少精确几何判断次数。

9.3 S2 简介

S2 是 Google 开源的球面几何库,把地球投影到立方体六个面上,再递归切分成层级单元。

S2 的特点:

  • 基于球面几何,天然适合全球范围。
  • 单元是近似四边形。
  • 层级结构清晰,支持 CellId 编码。
  • 支持区域覆盖、邻居、层级父子关系。
  • 在 Google 生态和一些全球空间索引场景中使用广泛。

H3 和 S2 的粗略对比:

维度 H3 S2
单元形状 主要六边形,少量五边形 近似四边形
邻接均匀性 六边形邻接更自然 四边形层级更规整
层级结构 分辨率层级 Cell level 层级
常见用途 聚合、热力图、网格运营 全球索引、区域覆盖、搜索
工程体验 网格聚合直观 层级编码和覆盖能力强

选择建议:

  • 做空间热力、密度、运营网格,H3 通常更直观。
  • 做全球球面几何索引、层级范围覆盖,S2 很有优势。
  • 系统内部一旦选定一种网格,不要轻易混用;确实要混用时,边界处要以精确几何为准。

9.4 北斗网格相关补充

北斗网格通常指依托北斗定位、地理网格编码、位置服务等体系,把空间位置编码成层级网格,用于位置标识、区域管理、应急救援、城市治理等场景。它更偏向国内位置服务、编码标准和行业应用体系。

可以把它与 H3/S2 做一个概念区分:

体系 更像什么 主要关注
H3 工程友好的全球六边形网格库 聚合、索引、邻接、空间分析
S2 球面几何与层级 Cell 编码库 全球索引、区域覆盖、球面计算
北斗网格 面向位置服务和治理应用的网格编码体系 位置标识、区域管理、行业标准对接

如果低空项目需要和政务、应急、城市治理平台对接,可能会遇到北斗网格、行政网格、城市治理网格等概念。工程上要问清楚:

  • 网格编码规则是什么?
  • 网格层级和精度是多少?
  • 坐标基准是什么?
  • 是否需要与 H3/S2 做映射?
  • 边界处按网格归属还是按精确几何归属?

9.5 网格化的坑

问题 说明 建议
边界误差 多边形边界附近,网格只能近似覆盖 候选过滤后做精确几何判断
分辨率选择 格子太大误报多,格子太小索引量大 按业务半径和吞吐压测选择
多层级一致性 父子格聚合可能出现重复或遗漏 明确聚合层级和去重规则
高度缺失 H3/S2 主要处理地表二维网格 高度要单独建模
时间缺失 网格只表达空间,不表达时间 结合时间窗口或状态 TTL

低空场景不要把 H3/S2 当成“三维空域模型”。它们解决的是二维地表空间索引,高度和时间需要业务模型补上。

10. 空间计算核心问题

10.1 空间关系:相交、包含、覆盖、距离

低空里最常用的空间关系可以记成四类:

关系 含义 例子
相交 两个对象有共同部分 航线穿越禁飞区
包含 一个对象在另一个对象内部 无人机点在任务区内
覆盖 包含内部和边界 点在禁飞区内或边界上
距离 两个对象相隔多远 距离禁飞区边界还有 200 米

航线和空域相交:

1
Route LineString ∩ Airspace Polygon != empty

无人机进入禁飞区:

1
2
3
Point in Polygon
AND altitude in [min_alt, max_alt]
AND timestamp in [start_time, end_time]

接近风险区:

1
distance(Point, Polygon) <= threshold

10.2 航线与空域相交判断

航线通常是折线:

1
P1 -> P2 -> P3 -> P4

空域通常是多边形加高度范围:

1
Polygon + min_alt + max_alt

二维平面判断:

flowchart TD
    A["航线 LineString"] --> C["二维相交判断"]
    B["空域 Polygon"] --> C
    C --> D{ST_Intersects?}
    D -->|否| E["不穿越该空域"]
    D -->|是| F["继续判断高度区间"]
    F --> G{航线高度范围与空域高度范围是否重叠?}
    G -->|否| H["平面穿越但高度不冲突"]
    G -->|是| I["航线与空域冲突"]

高度区间判断很简单:

1
2
route_max_alt >= zone_min_alt
AND route_min_alt <= zone_max_alt

这表示两个高度区间有重叠。

如果航线每个航点高度不同,可以按线段判断:

1
2
3
4
Segment(P1, P2)
├── 二维线段是否与 Polygon 相交
├── 线段高度范围是否与空域高度范围重叠
└── 如果需要更精确,计算相交位置处的插值高度

线性插值示意:

1
2
3
4
5
P1 alt = 80m
P2 alt = 140m

如果相交点在 P1 -> P2 的 25% 位置:
intersect_alt = 80 + (140 - 80) * 0.25 = 95m

10.3 无人机进入禁飞区检测

最常见的实时检测算法是 2.5D:

1
2
3
4
5
6
7
8
9
10
11
12
输入:
drone point = lon, lat, alt, timestamp
zone = polygon, min_alt, max_alt, valid_time

步骤:
1. 坐标统一到同一 CRS
2. 高度统一到同一高度基准
3. 用空间索引或网格找候选禁飞区
4. 点面判断:point in polygon
5. 高度判断:alt in [min_alt, max_alt]
6. 时间判断:timestamp in valid_time
7. 告警状态机去重和恢复判断

伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function checkNoFlyZone(point):
normalizedPoint = normalizeCrsAndAltitude(point)
gridId = h3FromPoint(normalizedPoint.lon, normalizedPoint.lat)
candidateZones = zoneIndex.getByGrid(gridId)

for zone in candidateZones:
if not pointInPolygon(normalizedPoint, zone.polygon):
continue

if not altitudeOverlap(normalizedPoint.alt, zone.minAlt, zone.maxAlt):
continue

if not timeInRange(normalizedPoint.timestamp, zone.validFrom, zone.validTo):
continue

emitAlert(point.droneId, zone.id)

这里有两个层次:

  • 网格、包围盒、空间索引用于减少候选数量。
  • 精确点面判断用于给出最终结论。

10.4 点在多边形内的算法

底层算法常见有射线法和环绕数法。

射线法的直觉是:从点向右发出一条无限长射线,数它和多边形边界相交的次数。

1
2
相交次数为奇数:点在多边形内
相交次数为偶数:点在多边形外

示意:

1
2
3
4
5
6
7
8
9
    +----------------+
| |
| P -----> | 射线与边界相交 1 次,P 在内部
| |
+----------------+

P ---------> +----------------+
| | 射线与边界相交 0 次,P 在外部
+----------------+

工程上通常不自己手写点面算法,而是使用成熟库:

  • 数据库:PostGIS。
  • Java:JTS / GeoTools。
  • Go:orb、go-geom、s2、h3-go 等生态库。
  • Python:Shapely / GeoPandas。

自己实现时要特别小心:

  • 点在边界上算不算内部?
  • 多边形是否闭合?
  • 是否有洞?
  • 多边形自相交怎么办?
  • 浮点误差如何处理?
  • 经纬度跨 180 度经线怎么办?

10.5 2D 平面 + 高度算法

2D + 高度,也就是常说的 2.5D,是低空系统最常见、最务实的建模方式。

模型:

1
空间对象 = 二维几何 + 高度范围 + 时间范围

禁飞区:

1
Polygon(lon, lat) + [min_alt, max_alt]

航线:

1
LineString(lon, lat) + 每个航点高度

检测逻辑:

1
二维关系成立 AND 高度区间重叠 AND 时间窗口重叠

优点:

  • 容易和 PostGIS 结合。
  • 容易用 H3/S2 做候选过滤。
  • 性能好,适合实时流处理。
  • 对大多数禁飞区、限高区、任务区、航线校验足够。
  • 数据库模型简单,运维成本低。

缺点:

  • 不能精确表达倾斜面、复杂三维建筑物、洞穴式空间。
  • 对曲面、非垂直边界、复杂障碍物表达能力弱。
  • 如果空域边界本身随高度变化,需要拆成多个高度层或多个多边形。

适用场景:

  • 禁飞区进入检测。
  • 航线与管控区冲突检测。
  • 低空任务区授权。
  • 常规限高规则。
  • 实时告警和大屏状态。

10.6 原生 3D 空间算法

原生 3D 不是“二维多边形加高度”,而是直接处理三维几何体,例如:

1
2
3
4
5
6
7
PointZ
LineStringZ
PolyhedralSurface
TIN
Mesh
Solid
3D Tiles

一个三维空域可以看成一个体:

1
2
3
4
5
6
7
8
9
    top surface
+-----------+
/ /|
+-----------+ |
| | |
| airspace | |
| |/
+-----------+
bottom surface

3D 算法要判断的是:

  • 点是否在三维体内。
  • 三维线段是否穿过三维体。
  • 两个三维体是否相交。
  • 三维体之间的最小距离。
  • 三维模型与航线是否碰撞。

优点:

  • 能表达复杂建筑、桥梁、塔吊、山体、真实空域体。
  • 对城市低空、贴地飞行、复杂障碍物更准确。
  • 适合三维可视化和数字孪生。

缺点:

  • 数据采集和清洗成本高。
  • 几何算法复杂,计算开销大。
  • 数据格式和数据库支持没有 2D 那么统一。
  • 实时高并发计算难度更高。
  • 很多业务规则仍然来自二维边界和高度范围,强行 3D 化反而增加复杂度。

选择建议:

场景 推荐模型
常规禁飞区、限高区、任务区 2D + 高度
航线审批、实时告警 2D + 高度 + 时间
城市复杂障碍物避让 局部 3D
三维数字孪生展示 3D Tiles / Mesh
飞控近距离避障 局部坐标系 + 传感器融合 + 3D
大规模在线监管 2D + 高度为主,3D 作为局部增强

一个务实路线是:

1
2
3
4
第一阶段:2D Polygon + 高度上下限
第二阶段:叠加 DEM/DSM,支持 AGL 判断
第三阶段:重点区域引入 3D Tiles / Mesh / 障碍物体
第四阶段:局部高风险区域做原生 3D 碰撞检测

11. 低空空间计算的工程架构

11.1 权威规则与实时状态分离

低空系统中,空间规则和实时状态的变化频率完全不同。

1
2
3
4
5
6
7
8
9
10
11
空间规则:
禁飞区、限飞区、行政区、起降点、航线模板
变化频率低
需要审核、版本、追溯
适合 PostGIS

实时状态:
无人机位置、速度、电量、链路状态、告警状态
变化频率高
需要低延迟
适合 Redis / Flink State / TSDB

架构上不要把它们混在一张“空间状态大表”里。更推荐:

flowchart TD
    A["PostGIS<br/>权威空间规则"] --> B["规则发布"]
    B --> C["Kafka: spatial.rule.changed"]
    C --> D["Flink Broadcast State"]
    E["Kafka: telemetry.raw"] --> F["Flink Keyed State"]
    D --> G["实时空间判断"]
    F --> G
    G --> H["Redis 最新状态"]
    G --> I["Kafka: spatial.alert"]
    G --> J["TSDB 历史轨迹"]

11.2 空间索引分层

高并发空间计算通常要多层过滤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
第 1 层:时间过滤
规则是否在当前时间有效

第 2 层:网格过滤
当前点所在 H3/S2 网格有哪些候选区

第 3 层:包围盒过滤
点是否落在候选多边形 bbox 内

第 4 层:精确几何判断
点面关系、线面关系、距离关系

第 5 层:高度过滤
高度基准转换后是否重叠

PostGIS 里 GiST 索引本身也会先做包围盒过滤,再做精确判断。流式系统里也可以借鉴这个思想,先便宜过滤,再昂贵计算。

11.3 数据质量:空间计算前必须先清洗

空间计算特别依赖数据质量。常见问题:

问题 表现 处理建议
坐标反了 无人机飞到国外或海里 入库校验经纬度范围和业务范围
坐标系混用 地图上整体偏移几百米 明确 CRS,统一转换
高度基准不明 限高判断错误 强制保存 altitude_ref
时间乱序 状态回退、告警反复 按设备维护事件时间和水位线
GPS 漂移 点位瞬间跳变 速度阈值、加速度阈值、平滑过滤
多边形非法 空间函数报错或结果异常 入库前 ST_IsValid / ST_MakeValid
边界语义不清 边界点是否告警争议 业务规则明确 covers/contains

PostGIS 常用检查:

1
2
3
SELECT id, ST_IsValid(geom), ST_IsValidReason(geom)
FROM no_fly_zone
WHERE NOT ST_IsValid(geom);

修复示例:

1
2
3
UPDATE no_fly_zone
SET geom = ST_MakeValid(geom)
WHERE NOT ST_IsValid(geom);

注意:自动修复可能改变几何形状,生产数据应记录修复前后差异并人工复核关键区域。

12. 一个端到端示例:实时禁飞区告警

12.1 数据准备

权威规则表:

1
2
3
4
5
6
7
8
9
10
no_fly_zone
├── id
├── name
├── geom: MultiPolygon, SRID=4326
├── min_alt_m
├── max_alt_m
├── alt_ref
├── valid_from
├── valid_to
└── version

规则索引表:

1
2
3
4
5
6
no_fly_zone_grid
├── zone_id
├── grid_type: H3
├── grid_resolution
├── grid_id
└── version

实时状态:

1
2
3
4
5
6
7
8
9
10
11
drone_live_state
├── drone_id
├── lon
├── lat
├── alt
├── alt_ref
├── speed
├── heading
├── timestamp
├── h3_cell
└── status

12.2 处理流程

flowchart TD
    A["遥测点"] --> B["字段校验"]
    B --> C["坐标系归一化"]
    C --> D["高度基准归一化"]
    D --> E["计算 H3 Cell"]
    E --> F["查询候选禁飞区"]
    F --> G["点面精确判断"]
    G --> H["高度范围判断"]
    H --> I["时间有效期判断"]
    I --> J["告警状态机"]
    J --> K["输出告警 / 更新状态"]

候选过滤:

1
2
3
4
5
6
如果 zone_count = 100000
直接点面判断:每条遥测最多判断 100000 个多边形

使用网格过滤:
当前 h3_cell -> 候选 zone 可能只有几十个
再做精确判断

这就是空间网格在实时系统里的核心价值:把“全量空间匹配”变成“候选集匹配”。

12.3 告警事件设计

告警事件不要只发一句“进入禁飞区”。建议包含足够追溯信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"eventId": "evt-20260526-0001",
"eventType": "NO_FLY_ZONE_INTRUSION",
"droneId": "drone-001",
"zoneId": "NFZ-001",
"level": "CRITICAL",
"position": {
"longitude": 116.397,
"latitude": 39.908,
"altitude": 95.2,
"altitudeRef": "AMSL",
"crs": "EPSG:4326"
},
"grid": {
"type": "H3",
"resolution": 10,
"id": "8a2a1072b59ffff"
},
"ruleVersion": 18,
"detectedAt": "2026-05-26T10:12:31+08:00",
"evidence": {
"spatialRelation": "COVERS",
"altitudeRange": [0, 120],
"distanceToBoundaryM": 0
}
}

这样做的好处:

  • 告警可以复盘。
  • 规则版本可追溯。
  • 坐标和高度基准明确。
  • 后续可以做告警聚合和统计。

13. 工程实践清单

13.1 建模清单

  • 每个空间字段必须明确 SRID。
  • 每个高度字段必须明确高度基准。
  • 空域、禁飞区、任务区建议使用 MultiPolygon,避免单面和多面混用。
  • 航线要保存航点序列、线几何、每个航点高度和时间约束。
  • 空间规则要有版本号,便于实时计算系统热更新。
  • 边界语义要明确:边界上算进入还是不算进入。
  • 所有关键几何入库前要做合法性检查。

13.2 计算清单

  • 不要在经纬度 geometry 上直接按“米”做 buffer 和 distance。
  • 大范围计算优先考虑 geography 或球面算法。
  • 小范围工程计算优先选择合适投影坐标系。
  • 实时系统先做候选过滤,再做精确计算。
  • 点面判断、线面判断尽量使用成熟库或 PostGIS。
  • 告警判断要做状态机,不要每条命中都发告警。
  • 高度判断要先统一高度基准。
  • 对乱序和延迟遥测要有处理策略。

13.3 存储清单

  • PostGIS 存权威空间规则和需要复杂查询的业务空间对象。
  • Redis Geo 存热点点位和附近查询,不存复杂空间关系。
  • TSDB / ClickHouse 存高频历史轨迹。
  • Kafka 保存实时流,按 drone_idgrid_id 合理分区。
  • 对象存储保存大体积影像、GeoTIFF、3D Tiles、离线数据包。
  • 规则更新要通过事件发布给实时计算系统,而不是让实时链路频繁查库。

13.4 接口清单

空间接口建议显式表达:

1
2
3
4
5
6
7
8
9
{
"geometry": {
"type": "Point",
"coordinates": [116.397, 39.908, 95.2]
},
"crs": "EPSG:4326",
"altitudeRef": "AMSL",
"timestamp": "2026-05-26T10:12:31+08:00"
}

不要让调用方猜:

  • 坐标顺序。
  • 坐标系。
  • 高度基准。
  • 单位。
  • 时间时区。

14. 面试和方案评审中的表达方式

14.1 如何解释 GIS 与后端的关系

可以这样说:

GIS 对后端来说,不只是地图展示,而是一套空间数据建模、索引、查询和计算能力。地图只是可视化层,真正的工程难点在于坐标系统一、空间关系判断、实时状态计算、海量轨迹存储和规则可追溯。

14.2 如何解释 PostGIS 的价值

可以这样说:

PostGIS 让 PostgreSQL 具备空间数据库能力,可以直接存储点线面,建立空间索引,并用 SQL 判断相交、包含、距离、缓冲区、坐标转换等关系。对低空系统来说,禁飞区、任务区、航线、起降点这些权威空间对象很适合放在 PostGIS 里。

14.3 如何解释 H3/S2 的价值

可以这样说:

H3/S2 把连续地理空间离散成层级网格。它们不是替代精确几何计算,而是用于索引、聚合和候选过滤。比如无人机实时进入禁飞区检测时,可以先通过网格找到候选禁飞区,再做精确点面判断,这样能显著降低实时计算成本。

14.4 如何解释 2.5D 与 3D 的取舍

可以这样说:

低空系统大多数监管和告警规则可以用二维多边形加高度上下限表达,也就是 2.5D。它性能好、数据模型简单、容易和 PostGIS、H3、Flink 结合。原生 3D 更适合复杂建筑物、三维障碍物、数字孪生和局部避障,但数据和计算成本都更高。工程上通常先用 2.5D 打底,再在重点区域引入 3D 能力。

15. 后续学习路径

这篇是基础入门,后续可以沿着下面路径继续深入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
1. 空间数据基础
├── 坐标系 / 投影 / 高程
├── 矢量 / 栅格 / 三维数据
└── GeoJSON / WKT / KML / GeoTIFF

2. 空间数据库
├── PostgreSQL + PostGIS
├── 空间索引 GiST / SP-GiST
├── 常见空间函数
└── 空间 SQL 优化

3. 实时空间计算
├── Kafka 遥测流
├── Flink 状态计算
├── H3/S2 网格化
└── 告警状态机

4. 低空业务算法
├── 航线与空域冲突
├── 禁飞区进入检测
├── 轨迹预测和碰撞风险
└── 空域容量和调度

5. 三维与数字孪生
├── DEM / DSM
├── 3D Tiles
├── 建筑物和障碍物模型
└── 局部 3D 碰撞检测

对于低空后端工程师,最建议先把下面几个能力打牢:

  1. 能说清楚 WGS84、CGCS2000、Web Mercator、GCJ-02 的区别和使用边界。
  2. 能区分椭球高、海拔高、AGL、AMSL,并知道高度基准不一致会导致误判。
  3. 能用 PostGIS 完成点面、线面、距离、缓冲区、坐标转换查询。
  4. 能设计“PostGIS + Kafka + Flink + Redis + TSDB”的实时空间计算链路。
  5. 能解释 H3/S2 为什么用于候选过滤和聚合,而不是替代精确几何。
  6. 能根据业务选择 2.5D 还是原生 3D,而不是一上来就堆复杂模型。

16. 参考资料

  • PostGIS 官方文档:空间类型、空间索引、空间函数。
  • PostgreSQL 官方文档:扩展机制、索引、分区、JSON、事务。
  • GeoJSON 标准:RFC 7946。
  • OGC 标准:WKT、WKB、KML、WMS、WFS、WMTS 等。
  • Uber H3 文档:全球六边形层级网格系统。
  • Google S2 Geometry:球面几何与层级单元。
  • Redis 官方文档:Geo 命令。
  • Flink 官方文档:状态、窗口、事件时间、广播状态。
  • Kafka 官方文档:topic、partition、consumer group、消息语义。
  • 地理空间索引:Geohash,Google S2与Uber H3 - 知乎

空间计算基础
https://www.albeltbo.com/posts/7b186e50.html
作者
AlbeltBo
发布于
2026年7月14日
许可协议