三维 GIS:电子围栏功能实现(Cesium+Turf + 规则引擎)
三维 GIS:电子围栏功能实现(Cesium+Turf + 规则引擎)
本文以 GIS Gallery 项目中的“电子围栏”为例,梳理一个可运行的三维电子围栏实现:Cesium 负责三维可视化与时间轴驱动,Turf.js 负责所有空间计算,独立规则引擎负责告警判定与去重,三者通过统一的数据流打通。s
本专栏对应工程仓库:
关键代码参考:
- 页面装配:[ElectronicFenceDemo.vue]
- Cesium 主视图:[Main3DView.vue]
- 空间计算工具:[geo.js]
- 轨迹采样/时间化:[trackGenerator.js]
- 规则引擎:[RuleEngine.js]
数据文件(由后端静态目录对外暴露,前端以/basic-data/...访问):
- 围栏:
src/main/resources/static/basic-data/xibuanquan.json - 轨迹:
src/main/resources/static/basic-data/walking_trajectory.json
项目效果展示
下面用几张关键截图把整体效果“串”起来:进入页面后视角会自动对准园区围栏,围栏边界以能量护盾形式拉伸抬升,人员沿多条轨迹同步移动;当目标临近边界或越界时,边界出现局部高亮与告警点位,同时右侧告警列表实时刷新;底部时间轴支持拖拽回放,在任意时间点复盘人员位置与告警分布。
第 1 章 项目背景与整体架构
1.1 电子围栏业务需求、行业应用
“电子围栏”的核心目标是:给定一块空间区域(围栏),对目标(人/车/设备)的位置变化进行实时判定,并在触发约束时形成可追溯的事件记录(告警)。
常见行业场景:
- 安防:人员越界、重点区域闯入、夜间禁入
- 车辆监管:车辆禁行区、路线偏离、进出场站
- 工业/园区:危险区域隔离、施工围挡、资产防盗
- 运营分析:到访统计、驻留时长、路径热力
在本 Demo 中,围绕“人员在园区围栏内外的进出”形成两类告警:
- 时间违规:非规定时间进出入围栏
- 空间违规:离开围栏边界(或者离开允许区域:围栏缓冲区 + 偏移阈值)
1.2 二维围栏 vs 三维围栏差异
二维围栏通常将目标位置视为平面点(经纬度),围栏是 Polygon/MultiPolygon;三维围栏在工程实现上主要差异在于“表达与交互”,而不是空间数学本身:
- 展示差异:三维相机(heading/pitch/roll)影响观看体验,需要“初始化视角/重置视角”
- 渲染差异:围栏、轨迹、点位需贴地(clampToGround)并保证深度测试/遮挡可控
- 时间维度:Cesium 原生 clock 驱动动画,适合“回放 + 拖拽时间轴”的时空分析
本 Demo 的空间判定仍以地表二维几何为主:点是否在面内、点到边界距离、缓冲区等,这些由 Turf.js 统一提供。
1.3 整体技术架构图:前端可视化 + 空间计算 + 规则调度
1.4 技术栈选型原因分析
- Cesium:三维地球 + 原生时间轴(Clock)与插值(SampledPositionProperty),适配“实时态势 + 回放”
- Turf.js:覆盖围栏判断、缓冲区、距离计算等常见 GIS 几何能力,API 简洁、工程成本低
- 自研规则引擎:将“空间/时间判定与去重”从渲染逻辑中剥离,避免后续规则扩展时对 UI 造成侵入
第 2 章 技术前置知识
2.1 Cesium 核心概念:实体、图元、坐标系统、事件监听
- Viewer:场景容器,内部包含
scene、camera、clock等核心对象 - Entity:面向业务对象的抽象(人员、轨迹、告警点),可直接绑定位置属性与样式
- DataSource:
GeoJsonDataSource:加载 GeoJSON 并自动生成实体(围栏、缓冲区)CustomDataSource:自行组织实体集合(轨迹线、人员点位)
- 坐标:
- WGS84 经纬度:业务侧主要使用 lon/lat
- Cesium 世界坐标:
Cartesian3.fromDegrees(lon, lat, height)转换为地心坐标
- 事件监听:
viewer.clock.onTick.addEventListener(fn):每个 tick 执行一次,用于“按当前时间取位置 + 规则判定”
2.2 Turf.js 空间计算常用 API:面、点、相交、包含、缓冲区
本 Demo 主要用到的 Turf API:
- 点面关系:
booleanPointInPolygon(point, polygon) - 缓冲区:
buffer(feature, meters, {units:'meters'}) - 点到边界距离:
pointToLineDistance(point, polygonToLine(polygon), {units:'meters'}) - 沿线采样:
length(line)+along(line, distance)(按统一步长补点)
对应封装在:[geo.js]、[trackGenerator.js]。
2.3 规则引擎基础:规则定义、条件判断、动作执行、表达式语法
在“电子围栏”里,规则引擎可以简化为三类职责:
- 规则输入:当前时刻、目标 ID、目标位置、围栏/允许区、配置项
- 规则判定:基于
inFence / inAllowed与时间窗计算事件(IN/OUT、TIME/SPACE) - 动作输出:生成告警事件(列表入队、地图打点等),并提供去重/节流能力
本 Demo 的规则引擎为“轻量状态机”:
- 为每个目标维护上一帧状态:
lastStateByPersonId - 通过前后两帧比较派生事件:
fenceEvent / allowedEvent - 通过 key+时间窗口实现告警去重:
shouldEmit(key, nowMs)
实现位置:[RuleEngine.js]。
第 3 章 开发环境搭建与初始化
3.1 前端工程与依赖
本模块在frontend目录下,典型启动方式(以项目 README 为准):
cdfrontendnpminpmrun dev关键依赖(见frontend/package.json):
cesium+vite-plugin-cesium:在 Vite 下正确打包 Cesium 资源@turf/turf:空间计算vue:组件化页面
3.2 页面与模块入口
页面装配与首次加载逻辑位于:[ElectronicFenceDemo.vue]:
onMounted():初始化引擎、设置播放参数、拉取围栏与轨迹数据、设置回放起始时间- Layout:TopBar / LeftPanel / Main3DView / RightPanel / BottomTimeline / FloatingWidgets 六区布局
3.3 Cesium Viewer 初始化与底图
Cesium 创建在:[Main3DView.vue] 的onMounted()内:
- 使用
UrlTemplateImageryProvider挂载 ArcGIS World Imagery 瓦片 - 在 Viewer 初始化时通过
baseLayer注入底图(避免后续图层管理混乱)
初始化视角与“视角重置”共用同一份参数(见“第 4 章”与“第 8 章”)。
第 4 章 Cesium 三维电子围栏绘制模块
4.1 围栏 GeoJSON 加载与合并
围栏数据拉取与预处理在 store 中完成:[fenceStore.js]:
refreshFence():fetch/basic-data/xibuanquan.json,并调用mergeFenceFeatures()处理多面合并
合并逻辑在:[geo.js:mergeFenceFeatures]:
- 逐个 union,尽量将 FeatureCollection 合成一个统一的围栏 Feature
4.2 Cesium 绘制:透明填充 + 高亮边框
围栏绘制在:[Main3DView.vue:loadFence]:
GeoJsonDataSource.load(...)fill: Color.TRANSPARENT:内部透明strokeWidth: 3:边框高亮clampToGround: true:贴地
4.2.1 电子围栏“拉伸护盾”渲染(核心:Wall + 动态高度 + 自定义材质)
在三维场景中,只画一个贴地的围栏边线往往不够“直观”。本 Demo 在围栏边界上额外构建了一个“能量护盾”效果:沿围栏边界生成 Wall(垂直面),并在页面进入时从地面逐渐“抬升”,形成“围栏拉伸”的视觉反馈。
实现入口在:[Main3DView.vue:buildFenceVisuals]。
(1) 从围栏面提取边界环(ring)
围栏数据最终会合并为一个 Polygon/MultiPolygon Feature。要在 Cesium 里做 Wall,需要一条“边界线”的点列,因此第一步是从面中提取环坐标:
- 使用 [geo.js:extractFenceRings] 把 Polygon/MultiPolygon 的 rings 提取为
[[lon,lat], ...]点列 - 同时做闭合处理(首尾点一致),确保 Wall/Polyline 不出现断裂
(2) 将经纬度点列转换为 Cesium 的 wall positions
Cesium 的wall.positions是Cartesian3[],本 Demo直接把ring扁平化后用Cartesian3.fromDegreesArray转换:
degrees = ring.flat()得到[lon1,lat1, lon2,lat2, ...]positions = Cartesian3.fromDegreesArray(degrees)
注意这里 positions 没带 height,因为“拉伸高度”交给minimumHeights/maximumHeights控制。
(3) Wall 的“拉伸高度”计算方式:每个顶点一组 min/max height
Cesium 的 Wall 支持按顶点指定高度数组:
minimumHeights[i]:该顶点对应的底部高度maximumHeights[i]:该顶点对应的顶部高度
本 Demo 的做法是:
minimumHeights = Array(ringLength).fill(0):底部固定贴地maximumHeights使用CallbackProperty动态返回Array(ringLength).fill(h),其中h = getShieldHeight()
这样每次渲染帧都会重新计算当前“护盾高度”,达到持续抬升的动画效果。高度函数见:[Main3DView.vue:getShieldHeight],核心是一个带缓动的插值:
progress = clamp((now - shieldRaisedAt) / raiseDurationMs, 0..1)eased = 1 - (1 - progress)^3(cubic ease out)height = targetHeight * eased
因此,护盾从 0m 平滑抬升到targetHeight(例如 66m),并在raiseDurationMs(例如 2200ms)内完成。
(4) 自定义材质:用 GLSL fabric 做“扫描流光”
Wall 的材质不是简单纯色,而是自定义的动态 shader(扫描/栅格/能量感):
- 材质注册:[Main3DView.vue:registerAnimatedMaterials]
Material._materialCache.addMaterial(type, { fabric: { uniforms, source }})source是 Cesium Material 的 GLSL 片段逻辑
- 动态 uniforms 注入:[Main3DView.vue:AnimatedMaterialProperty]
getValue()每帧更新time = (performance.now() - startedAt) / 1000- shader 内用
time * speed驱动fract/sin/smoothstep,形成扫描与闪烁
对应到围栏护盾,Wall 使用的是FenceEnergyShieldMaterial。
(5) 同步的边界流光:Polyline + 自定义 Trail 材质
除了 Wall,本 Demo 还在围栏边界上叠加了两条 polyline:
edge-core:细的 glow polyline(增强轮廓)edge-trail:较粗的 trail polyline(沿边界跑动的能量流)
两者和 Wall 共用同一份positions(沿 ring 边界),并且 polyline 设置clampToGround: true,保证贴地观感稳定。
(6) “局部高亮”是如何定位到围栏边界的(与拉伸护盾配套)
当人员接近边界或触发告警时,Demo 会在“离人员最近的围栏边界段”上叠加一段黄色脉冲流光,用于强调危险边界位置:
- 边界段计算:[geo.js:getNearestFenceSegment]
turf.polygonToLine(fence)把面转线turf.nearestPointOnLine(line, point)找最近点及其在边界线上的里程turf.lineSliceAlong以最近点为中心切出一段固定长度的边界线段
- 渲染位置更新:
updateFenceHighlight(...)内把该线段转Cartesian3.fromDegreesArray,并用自定义FencePulseMaterial渲染为动态脉冲
4.3 缓冲区与允许区:从“视觉层”扩展到“判定层”
围栏系统通常区分两类区域:
- 围栏本体:越界立即告警(fence out)
- 允许区(围栏 + buffer + offset):用于“误差容忍/缓冲”,可视为更宽松的判定边界
本 Demo 中:
createBufferArea(fence, bufferMeters):用于渲染缓冲区(橙色透明面)createAllowedArea(fence, bufferMeters, offsetMeters):用于规则判定(允许区不一定渲染)
实现位置:[geo.js]。
4.4 初始化视角:让业务“开箱即用”
三维围栏 Demo 的体验高度依赖相机视角:
- 进入页面:自动对准园区围栏(无需手动寻找)
- 重置视角:一键回到初始最佳视角
本 Demo 将初始化视角固化为INITIAL_CAMERA_VIEW(经纬度 + 高度 + heading/pitch/roll),在围栏加载后直接setView定位。
第 5 章 实时目标轨迹模拟与点位推送
5.1 多人轨迹数据结构
轨迹文件为 GeoJSON FeatureCollection,每个 Feature 表示一个人的轨迹 LineString:
geometry.type = LineStringproperties.person_name:人员名称
5.2 统一步长补点:沿线插值采样
为了“即使原始点很少也能平滑行走”,采用“沿线按步长采样”:
- 将 LineString 视为一条线
- 计算总长度
turf.length - 以
stepMeters为步长进行turf.along采样,得到等距点 - 末尾补齐终点(避免采样误差导致终点丢失)
实现位置:[trackGenerator.js:sampleAlongLine]。
5.3 时间化轨迹:为每个采样点赋予时间戳
Cesium 的时序插值依赖时间轴,因此需要把“几何点序列”转为“带时间的点序列”:
- 对每个采样点生成
{t, lon, lat, alt} t以随机 dt 累加,形成“真实感”移动
实现位置:[trackGenerator.js:generateTimedTrackFromLineStringFeature]。
5.4 Cesium 实体:SampledPositionProperty + 轨迹线
在 Cesium 侧,人员点位使用SampledPositionProperty:
addSample(time, position)添加样本forward/backwardExtrapolationType = HOLD:边界时间也能保持位置,避免回放端点抖动
实现位置:[Main3DView.vue:buildPersonEntity]。
轨迹线使用 polyline(只渲染,不参与判定),并与人员点位共用同一 CustomDataSource。
5.5 行走方向图标:避免终点“突然回头”
人员图标用左右行走 SVG(/roam-left.svg、/roam-right.svg),方向计算遵循“优先看上一帧”的原则:
- 优先比较当前点与前一秒位置(prev)
- prev 取不到才比较下一秒(next)
- 都取不到就保持上一方向
实现位置:[Main3DView.vue:evaluateOnTick]。
第 6 章 Turf.js 空间几何检测(核心算法)
6.1 点是否在围栏内:booleanPointInPolygon
判定入口:[geo.js:isPointInPolygon]:
- 输入:
(lon, lat, polygonFeature) - 输出:
true/false
在规则引擎中会同时计算:
inFence:是否在围栏内inAllowed:是否在允许区内(围栏 + buffer + offset)
6.2 缓冲区与允许区:buffer
缓冲区/允许区均由 Turf 的buffer()实现,单位统一为 meters。
这一步的工程意义:
- UI 侧可直接渲染 buffer 面,形成“安全边界”的可视化表达
- 规则侧用 allowed 面容纳 GPS 漂移、定位误差、采样误差
6.3 点到围栏边界距离:pointToLineDistance
告警经常需要“离边界多远”的指标,用于:
- 告警严重程度分级
- 进入/离开的趋势判断
- “临近边界”提示(未实现但可扩展)
实现位置:[geo.js:distanceToFenceMeters]。
6.4 易错点:围栏 OUT 与 允许区 OUT 的语义区别
实际项目里最常见的问题之一是:把“离开围栏”与“离开允许区”混在一起。
inFence=false && inAllowed=true:已经越过围栏边界,但仍在允许区内(误差容忍范围内)- 如果只在
allowedEvent=OUT才告警,会导致“出围栏但不报警”的体验问题
因此本 Demo 的空间告警分成两条:
fenceEvent=OUT:离开围栏边界(立即告警)allowedEvent=OUT:离开允许区(更严重的告警,通常用于“远离围栏很多”)
实现位置:[RuleEngine.js]。
第 7 章 规则引擎设计与业务规则配置
7.1 输入输出与模块边界
规则引擎的输入(ctx)包含:
nowMs:当前时间(来自 Cesium clock)personId/personNamelon/latfenceFeature:围栏 FeatureallowedFeature:允许区 Feature(围栏+buffer+offset)pathMotion:路径整体趋向(可选)
输出是一个 violations 数组,每项包含:
type:TIME / SPACEmotion:IN / OUT(或空)reasondistanceMeters(可选)
7.2 状态机:用“前一帧 vs 当前帧”计算事件
状态存储在lastStateByPersonId中:
inFence:上一帧是否在围栏inAllowed:上一帧是否在允许区
对比即可得到事件:
- fenceEvent:IN / OUT / null
- allowedEvent:IN / OUT / null
对应实现:[RuleEngine.js:evaluate]。
7.3 规则配置:时间窗、偏移、缓冲、告警开关
配置集中存放在 store:
accessTime.start/end:准入时间窗(例如 09:00-18:00)offsetMeters:偏移容忍bufferMeters:缓冲半径alarmEnabled:全局告警开关
对应位置:[fenceStore.js:config]。
7.4 去重策略:避免告警风暴
由于 Cesium tick 高频触发,必须提供去重窗口:
- 以
${personId}-${ruleKey}形成 key - 在
800ms内重复触发则丢弃
对应实现:[RuleEngine.js:shouldEmit]。
第 8 章 三端联动:可视化 + 空间计算 + 规则引擎打通
8.1 数据流从页面到渲染
以“进入页面”流程为例:
- [ElectronicFenceDemo.vue]
onMounted拉取围栏与轨迹 - store 更新
fenceGeoJson / tracks - [Main3DView.vue] watch 到变化,重新加载围栏/轨迹实体
- Cesium clock tick 时,取每个人当前位置,交给 RuleEngine 判定
- violations 写入
fenceStore.alarms,同时在 Cesium 中打点渲染告警
8.2 Tick 驱动:以 Cesium Clock 为“唯一时间源”
Tick 逻辑在:[Main3DView.vue:evaluateOnTick]:
JulianDate.toDate(clock.currentTime)转为 ms- 遍历 tracks,为每个 entity 取
position.getValue(clock.currentTime) - 将 lon/lat 与 fence/allowed 交给
engine.evaluate(ctx) - 对返回 violations 调用
pushAlarm(...)
8.3 告警列表与地图标识同步
告警入队在:[Main3DView.vue:pushAlarm]:
fenceStore.alarms.unshift(...):列表头插,时间倒序alarmDs.entities.add(...):地图点位标识
清空逻辑(列表清空时同步清除地图点位):
- 监听
fenceStore.alarms.length,为 0 时alarmDs.entities.removeAll()
8.4 初始化视角与“视角重置”
初始化视角与重置视角应一致,否则用户会觉得“重置按钮不可预期”。
- 初始化:围栏加载完成后
setCamera(INITIAL_CAMERA_VIEW) - 重置:
flyHome()优先回到INITIAL_CAMERA_VIEW
视角参数定义于 store,便于未来做“保存/加载视角配置”或“多预设视角”扩展。
8.5 控制台拾取视角:用于快速迭代体验
Main3DView 在window.__efCesium上暴露了工具:
window.__efCesium.dumpCamera():输出当前位置与可复制 snippetwindow.__efCesium.setCamera(view):用destination + orientation设置视角
这套工具适合在“调视角/调 pitch/range”阶段快速迭代,确定后再固化到INITIAL_CAMERA_VIEW。
以上内容覆盖了一个三维电子围栏 Demo 的“可运行最小闭环”:数据加载 → 轨迹时间化 → Cesium 时序渲染 → Turf 空间判定 → 规则引擎告警 → UI 列表与地图标注同步。后续若要增强业务深度,可从“告警分级、驻留/临近边界、路径偏离、区域多围栏、多视角预设、告警处理闭环”等方向继续扩展。
