Cesium 导航模块设计
js/cesium/navigation/ 是 3D 地图 iframe 内的 路线预览 + 实时导航 子系统。对外通过 createCesiumNavigation(viewer, options) 创建实例,由 js/cesium/index.js 在路线查询、换层、GPS 推送等场景下调用。
目录
- 设计目标
- 架构概览
- 两种工作模式
- 文件说明
- 对外 API 摘要
- 与上层集成
- 扩展与修改指引
- 目录结构
- 深度章节
- 1. 路径几何与显示路径构建
- 2. past / feature 路线切割
- 3. GPS 匹配与平滑插值
- 4. 路线材质与 flow 流动箭头
- 5. 导航跟车相机
- 6. 导航进度与多段续播
- 7. 路线数据预处理
设计目标
- 预览态:查询路线后,在当前楼层绘制多条备选路线(半透明灰/高亮蓝),自动
flyTo到合适视角。 - 导航态:开始实时导航后,GPS 点沿路线平滑插值移动,路线按进度切割为 已走过(past) / 未走过(feature),相机跟车。
- 多段路线:一条
routeNo可含多个segment(跨楼层);单段到达后通过positionUpdate通知父页面续播下一段(transitionSegment)。 - 可配置:图标、颜色、流动箭头、相机视角等通过
options/updateConfig调整,无需改业务逻辑。
架构概览
采用 工厂 + 共享 state/config 装配,模块间通过 createXxx({ viewer, state, ... }) 注入依赖。
createCesiumNavigation(viewer, options)│┌─────────────────────────┼─────────────────────────┐│ │ │constants.js state.js events.js(默认 config + (运行时可变 (on/off/emit)SMOOTH_CONFIG) 状态容器)│ │└────────────┬────────────┘│index.js(装配顺序)│┌──────────────────┼──────────────────┐│ │ │
route-materials route-display smooth-motion(材质/flow动画) (显示路径/切割) (GPS插值)│ │ │└────────┬─────────┴────────┬─────────┘│ │route-handlers nav-camera(预览绘制/flyTo) (跟车相机)│ │route-progress navigation-controller(进度/positionUpdate) (启停/GPS入口)│ │└────────┬─────────┘│api.js
数据分层
| 层级 | 文件 | 内容 | 生命周期 |
|---|---|---|---|
| 静态配置 | constants.js |
默认 options、SMOOTH_CONFIG |
创建时 merge;updateConfig 可改 |
| 运行时状态 | state.js |
路线、实体、state.smooth 等 |
init → 导航 → destroy |
| 视角配置 | nav-view-conf.js |
第一/第三人称、2D 俯视参数 | 作为 config 一部分 |
易混淆概念
| 名称 | 位置 | 含义 |
|---|---|---|
routeStyle.flow |
config |
路线 材质箭头流动(视觉) |
state.smooth |
state |
GPS 位置插值 运行时状态 |
SMOOTH_CONFIG |
constants |
插值/几何 算法常量 |
routeDisplay.positions |
state |
带圆角的 显示路径 采样点 |
routeData.points |
state |
当前 segment 原始节点 坐标 |
两种工作模式
预览态(isNavigating === false)
- 触发:
api.init({ routes, waypoints, currentFlId, routeNo, segmentIndex }) - 绘制当前楼层预览线、起终点,并
flyToCurrentFloorRoutes取景 updatePosition仅更新箭头 entity
详见 2. past / feature 路线切割 中的预览分支。
导航态(isNavigating === true)
- 触发:
api.startRealTimeNavigation() - 清除预览线,启用 past/feature 双色线与跟车相机
updatePosition→ GPS 匹配与平滑插值 → 路线切割与进度上报
详见 3. GPS 匹配与平滑插值、5. 导航跟车相机。
文件说明
| 文件 | 职责 |
|---|---|
index.js |
装配入口;refs 解决 navigation ↔ routeProgress 循环依赖 |
api.js |
对外 API;applySegmentContext 统一写入 state |
constants.js |
默认 options、SMOOTH_CONFIG、到达阈值 |
state.js |
createNavState() 运行时容器 |
nav-view-conf.js |
相机三种视角默认参数 |
floor-height.js |
从 store 读楼层高度 |
route-preprocess.js |
路线预处理 → §7 |
route-path-geometry.js |
路径纯几何 → §1 |
route-display.js |
显示路径、切割、距离 → §1、§2 |
route-materials.js |
材质与 flow → §4 |
route-handlers.js |
预览绘制、flyTo、initRoutePrimitives |
smooth-motion.js |
GPS 插值 → §3 |
nav-camera.js |
跟车相机 → §5 |
navigation-controller.js |
启停、handleNewPosition → §3 |
route-progress.js |
进度与续播 → §6 |
entities.js |
定位点、途径点、起终点、实点 |
events.js |
轻量事件总线 |
对外 API 摘要
| 方法 | 说明 |
|---|---|
init(payload) |
销毁旧态并进入预览 |
transitionSegment(payload) |
多段续播换层,保持 isNavigating |
startRealTimeNavigation() |
进入导航态 |
stopNavigation() |
停止导航,回到预览 |
updatePosition([lon, lat, alt]) |
预览更新箭头 / 导航推送 GPS |
updateRealPoint(...) |
可选 GPS 实点红点 |
updateConfig(partialOptions) |
热更新样式、flow、viewType |
destroy() |
释放资源 |
on / off |
订阅 positionUpdate 等事件 |
positionUpdate 字段与续播逻辑见 §6。
与上层集成
js/cesium/index.js:
resetPosition/changFloor→showRouteDetail→init或transitionSegment- postMessage
updatePosition→navSystem.updatePosition navSystem.on('positionUpdate', handleRoutePositionUpdate)→postMessage父页面
换层续播时模型 autoFly: false,避免与跟车相机冲突;预览取景由 flyToCurrentFloorRoutes 负责。
扩展与修改指引
| 需求 | 建议修改 |
|---|---|
| 线宽、到达阈值、插值速度 | constants.js |
| 默认颜色、箭头流动 | constants.js 或 updateConfig |
| 相机跟车 | nav-camera.js、nav-view-conf.js → §5 |
| GPS 匹配 / 插值 | navigation-controller.js、smooth-motion.js → §3 |
| 切割 / 预览绘制 | route-display.js、route-handlers.js → §2 |
| 进度 / route-over | route-progress.js → §6 |
新增模块时在 index.js 注册工厂;双向依赖用 refs 延迟引用。
目录结构
navigation/
├── README.md
├── index.js
├── api.js
├── constants.js
├── state.js
├── events.js
├── floor-height.js
├── nav-view-conf.js
├── route-preprocess.js
├── route-path-geometry.js
├── route-display.js
├── route-materials.js
├── route-handlers.js
├── smooth-motion.js
├── nav-camera.js
├── navigation-controller.js
├── route-progress.js
└── entities.js
深度章节
以下章节说明模块中最复杂、最常改动的逻辑。
1. 路径几何与显示路径构建
涉及文件:route-path-geometry.js、route-display.js(rebuildRouteDisplayPositions、nodesToDisplayPositions)
导航中有两套「路径」概念,不要混用:
| 名称 | 存储位置 | 来源 | 用途 |
|---|---|---|---|
| 控制点 / 节点路径 | routeData.points、smooth.routeCartesian |
接口节点 nodes[].pos |
GPS 路段匹配、passDistance 累加 |
| 显示路径 | routeDisplay.positions |
控制点 + 几何加工 | 绘制折线、past/feature 切割、插值沿路线走 |
1.1 控制点 → 显示路径
routeData.points(经纬度节点)→ rebuildRouteCartesian() → smooth.routeCartesian(Cartesian3)→ buildRouteDisplayWithLocalCorners(controlPoints)→ routeDisplay.positions(高密度采样点)
预览态 addBackgroundRoutePrimitive 也走 nodesToDisplayPositions → buildRouteDisplayWithLocalCorners,保证预览线与导航线几何一致。
1.2 buildRouteDisplayWithLocalCorners 策略
route-path-geometry.js 中的核心算法:
- 直线段:按
STRAIGHT_SAMPLE_STEP_M(默认 1m)线性插值,保持折线感。 - 折点(内角 < 170°):在顶点两侧各取
CORNER_BLEND_M(默认 1m)切点 A、B,用三次贝塞尔连接 A→B,绕过尖角。 - 近直线(内角 ≥ 170°):
skipCornerCut,直接连线,避免无意义圆角。
相关常量均在 SMOOTH_CONFIG 中。
1.3 其他几何工具
| 函数 | 作用 |
|---|---|
pointToSegmentInfoCartesian |
点到线段垂足、参数 t、距离 |
buildSmoothPathFromControlPoints |
Catmull-Rom 全曲线(插值 fallback) |
buildArcLengthTable / samplePathByNormalizedT |
弧长均匀采样,插值按路径长度而非直线 t |
pushUniqueCartesian |
去重相邻重合点(阈值 0.01m) |
1.4 进度在显示路径上的表示
findClosestOnRouteDisplay(point) 在 routeDisplay.positions 上找最近线段,返回:
index:采样段下标t:段内参数 [0,1]- 逻辑进度 =
index + t(用于切割与节流)
这与 routeData.points 上的 currentSegmentIndex(GPS 匹配用)是不同坐标系。
2. past / feature 路线切割
涉及文件:route-display.js、route-handlers.js、route-materials.js
导航态下路线拆成四条 primitive(描边 + 主线 × past/feature):
routePrimitives.pastBorder / past ← 已走过(灰色)
routePrimitives.featureBorder / feature ← 未走过(高亮色 + flow 箭头)
2.1 切割流程
GPS 插值点 cartesian→ findClosestOnRouteDisplay(cartesian)→ buildSplitFromRouteSample(index, t)past: positions[0..index] + 垂足future: 垂足 + positions[index+1..end]→ applyRouteStylePrimitives(past, future)→ 重建四条 Polyline Primitive
updateRouteProgressFromPoint 负责上述链路,并由 syncRouteStyleFromCartesian(smooth-motion)在插值每帧调用。
防抖动:
lastAppliedProgress:进度变化 < 0.002 时不重建 primitivesyncRouteStyleFromCartesian:120ms 内进度变化 < 0.003 时跳过(force可打破)
后退:allowBackward === true 时允许 past 回缩(needsRouteStyleResync 场景)。
2.2 预览态(无切割)
route-handlers.drawAllRoutesOnFloor:
- 遍历
state.routes,仅segment.flId === currentFlId - 每段
addBackgroundRoutePrimitive→ 写入backgroundRoutes - 当前
routeNo用 feature 材质高亮;备选线BACKGROUND_ROUTE_INACTIVE_ALPHA半透明 - 激活路线起终点 entity;
flyToCurrentFloorRoutes框选当前层节点包围球
导航开始时 clearBackgroundRoutePrimitives,预览线全部移除。
2.3 导航启动初始化
route-handlers.initRoutePrimitives:
rebuildRouteCartesian
→ rebuildRouteDisplayPositions
→ applyRouteStylePrimitives([], 全长 positions) // past 空,feature 为整段
3. GPS 匹配与平滑插值
涉及文件:navigation-controller.js、smooth-motion.js、constants.js(SMOOTH_CONFIG)
3.1 总览
GPS 不直接驱动 entity 位置,而是:
updatePosition→ handleNewPosition(匹配路线、算目标垂足)→ startSmoothMove(oldPoint, footOnRoute, waypoints)→ CallbackPositionProperty 每帧沿曲线移动→ syncRouteStyleFromCartesian(切割 past/feature)→ emitPositionUpdate(进度)
箭头 entity 与相机均绑定 getNavPositionProperty()。
3.2 GPS 路段匹配(handleNewPosition)
在 smooth.routeCartesian(节点折线)上:
- 对每个线段计算 GPS 到线段的距离与垂足
foot。 - 优先取距离 ≤
SEGMENT_MATCH_THRESHOLD(100m)的段;若无则取最近段。 - 路口消歧:
t ≥ 0.999时若下一段也更近,则adjustedSegmentIndex + 1;t ≤ 0.001时类似回退。 - 更新
currentSegmentIndex;若后退则needsRouteStyleResync = true。 - 若当前插值点与垂足距离 < 0.25m,忽略(防抖动)。
- 前进跨段:在
waypoints中插入跳过的节点,保证插值经过拐点。 startSmoothMove(oldPoint, foot, waypoints, { pauseStyleUpdate: isReverseMatch })。
3.3 插值曲线选取(startSmoothMove)
| 条件 | 曲线来源 |
|---|---|
| 前进且已有显示路径 | extractRouteDisplayPathBetween(from, to) 沿显示路径截取 |
后退或 pauseStyleUpdate |
buildSmoothPathFromControlPoints(Catmull-Rom) |
弧长表 smoothCurveArc + samplePathByNormalizedT 保证匀速沿路径运动。
时长:pathLength / NAV_MOVE_SPEED,夹在 [NAV_SMOOTH_DURATION_MIN, NAV_SMOOTH_DURATION_MAX]。
3.4 state.smooth 在插值帧中的职责
getNavPositionProperty 的 Callback 每帧:
- 按
t = elapsed / totalDuration采样位置 →currentPoint。 - 在
smoothRouteSegments上更新currentSegmentIndex(顶点吸附VERTEX_SNAP_EPS)。 - 前进:正常
syncRouteStyleFromCartesian。 - 后退:
smoothPauseRouteStyleUpdate,只emitPositionUpdate,不切割路线。 t >= 1:落到终点,强制同步样式,resetSmoothMotionState()。
3.5 flow vs smooth(再次区分)
routeStyle.flow:线上箭头纹理动画(route-materials),与 GPS 位移无关。state.smooth:标记点沿路线的运动状态(本章节)。SMOOTH_CONFIG:匹配阈值、插值速度、曲线采样等算法参数。
4. 路线材质与 flow 流动箭头
涉及文件:route-materials.js、constants.js(routeStyle.flow)
4.1 材质类型
| 材质集 | 用途 | 缓存变量 |
|---|---|---|
getRouteMaterials() |
导航 past/feature 双色线 | routeMaterials |
getInactivePreviewMaterials() |
预览备选半透明线 | inactivePreviewMaterials |
getPreviewRouteMaterials(isActive) |
预览当前/备选 | 组合上述 |
每条线 = border(描边纯色)+ line(chevron 箭头材质)。
4.2 flow 配置项
config.routeStyle.flow(默认见 constants.js):
| 字段 | 含义 |
|---|---|
animated |
feature 线是否流动 |
arrowSpeed |
流动速度(0 = 静止图案) |
arrowRepeat |
箭头重复密度 |
arrowArmWidth / arrowAngle / arrowStroke |
箭头形状 |
pastAnimated |
past 线是否单独动画 |
pastArrowRepeat |
past 线密度 |
updateConfig({ routeStyle: { flow } }) → applyFlowUniformsToRouteMaterials,无需重建全部材质。
4.3 动画驱动
ensureRouteAnimListener 注册 postRender:
mats.feature.uniforms.time = (performance.now() - routeAnimStartMs) * 0.001;
仅当 flow.animated 且存在 feature primitive 时更新。预览激活线与导航 feature 线共用该监听器。
5. 导航跟车相机
涉及文件:nav-camera.js、nav-view-conf.js
5.1 启用时机
navigation-controller.startRealTime → enableNavigationCamera:
- 清除
trackedEntity,lookAtTransform(IDENTITY) - 注册
postRender监听器 + 滚轮缩放 state.camera.isNavigationCameraMode = true
stop / destroy → disableNavigationCamera。
5.2 每帧跟车逻辑
positionProperty.getValue(currentTime) → 当前位置
positionProperty.getValue(time + 0.4s) → lookahead 位置(NAV_CAMERA_LOOKAHEAD_SEC)→ computeTravelHeading → targetHeading→ lerpAngle(smoothedHeading, targetHeading, 0.22)→ applyNavCameraForViewType(position, heading)
applyNavCameraForViewType 根据 resolveNavViewConf 计算相机偏移:
| viewType | 模式 | row 含义 | 滚轮缩放 |
|---|---|---|---|
| 1 | 第一人称 | 固定 row 偏移 |
— |
| 2 | 第三人称 | navCameraRange(默认来自 row) |
缩放 range |
| 3 | 2D 俯视 | column × navCameraColumnScale |
缩放 column |
lookAtOffsetDown:注视点沿法线向下偏移,避免盯头顶。
5.3 与预览 flyTo 的关系
| 场景 | 相机行为 |
|---|---|
预览 flyToCurrentFloorRoutes |
flyToBoundingSphere + getRouteFlyToHpr() |
导航中 updateConfig 改 viewType |
立即 applyNavCameraForViewType 或 flyToRouteDisplay |
换层续播 transitionSegment |
上层 changFloor 设 autoFly: false,跟车相机接管 |
getRouteFlyToHpr:全局 2D 视图俯仰 -90°;否则用当前 viewType 的 pitch。
6. 导航进度与多段续播
涉及文件:route-progress.js、route-display.js、api.js、index.js
6.1 positionUpdate 数据字段
route-progress.emitPositionUpdate 构造并派发(导航中节流 200ms,段到达立即):
| 字段 | 含义 |
|---|---|
routeNo / flId / segmentIndex |
当前路线与段 |
position |
[lon, lat, alt] |
passDistance |
沿节点折线累计已走距离(米) |
endDistance |
距段终点剩余距离;到达时为 0 |
nextPointDistance |
到下一节点距离 |
polylineLength |
段总长度 |
currentIndex / currentPointInfo / nextPointInfo |
节点索引与信息 |
hasNextSegment |
是否还有下一段 |
nextRouteInfo |
{ index, flId } 下一段信息(到达时) |
6.2 段到达判定
isRouteNavigationComplete 需同时满足:
- 距目的地节点 <
NAV_ROUTE_END_THRESHOLD_M(1m) - 在
routeData.points最后一段上且t ≥ 0.85
6.3 防重复 route-over
| 机制 | 位置 | 作用 |
|---|---|---|
segmentTransitionLock |
state |
到达有下一段时设为当前 segmentIndex,续播完成前忽略重复到达 |
emitPositionUpdate.cancel |
lodash throttle | 到达时取消 pending 节流,立即派发 |
handleRoutePositionUpdate |
index.js |
忽略迟到/重复的 endDistance === 0 事件 |
6.4 续播时序
段到达 → positionUpdate(endDistance: 0, hasNextSegment: true)→ index.js sendMessageToParent('route-over')→ 父页面换层 / 更新 routeData.currentRouteInfo→ changFloor({ changeFloor: true }) // autoFly: false→ showRouteDetail → navSystem.transitionSegmentapplySegmentContext(新 segmentIndex / flId)reset smooth 中间态navigation.startRealTime() // 保持 isNavigating
最后一段到达 → onRouteComplete → navigation.stop() → 回到预览态并重绘楼层路线。
6.5 一层多段轨迹的预览取景
同楼层多个 segment 时,flyToCurrentFloorRoutes 收集当前路线在该层 所有 segment 的节点 做包围球,而非只框第一段。详见 route-handlers.js。
7. 路线数据预处理
涉及文件:route-preprocess.js、floor-height.js
api.init / transitionSegment 在写入 state.routes 前调用 preprocessRoutes(routes, getHeight):
| 步骤 | 说明 |
|---|---|
| 补 Z | pos.length === 2 时 push(getFloorHeight(flId) + 0.2) |
| 枚举 | turn → TurnType,transit → TransitType |
| POI 展开 | node.posList 插入虚拟节点 nodeId_poi_{i} |
| 去重 | 相邻节点经纬度重合时,优先去掉 _poi_ 节点 |
| 目的地文案 | type == 1 且无 address 时填「目的地」 |
预处理后的结构:
routes[].segments[].nodes[].pos // [lon, lat, alt]
segmentIndex 与 currentFlId 由 api 入参决定,不在预处理中修改。
