cesium-multi-target-framework 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +474 -0
- package/dist/assets/renderWorker-118e8f3b.js +1 -0
- package/dist/cesium-multi-target-framework.js +4605 -0
- package/dist/cesium-multi-target-framework.umd.cjs +101 -0
- package/dist/cluster/QuadCluster.d.ts +66 -0
- package/dist/cluster/clusterClient.d.ts +44 -0
- package/dist/cluster/clusterWorker.d.ts +1 -0
- package/dist/config/types.d.ts +266 -0
- package/dist/core/EventEmitter.d.ts +9 -0
- package/dist/core/MultiTargetFramework.d.ts +184 -0
- package/dist/core/MultiTargetScene.d.ts +224 -0
- package/dist/data/types.d.ts +67 -0
- package/dist/events/bus.d.ts +336 -0
- package/dist/index.d.ts +22 -0
- package/dist/render/AirGroundReferenceRenderer.d.ts +20 -0
- package/dist/render/InstancedGltfBatch.d.ts +37 -0
- package/dist/render/InstancedSymbolRenderer.d.ts +65 -0
- package/dist/render/LowModelInstancedRenderer.d.ts +39 -0
- package/dist/render/ModelPoolRenderer.d.ts +43 -0
- package/dist/render/PointCloudRenderer.d.ts +135 -0
- package/dist/render/SelectionOverlayRenderer.d.ts +37 -0
- package/dist/render/targetVisualMetrics.d.ts +9 -0
- package/dist/site/SiteLayer.d.ts +43 -0
- package/dist/site/SiteLowModelRenderer.d.ts +12 -0
- package/dist/site/SiteSpatialIndex.d.ts +10 -0
- package/dist/site/types.d.ts +72 -0
- package/dist/track/TrackHoverPicker.d.ts +26 -0
- package/dist/track/TrackManager.d.ts +42 -0
- package/dist/track/TrackRenderer.d.ts +29 -0
- package/dist/track/types.d.ts +27 -0
- package/dist/worker/protocol.d.ts +182 -0
- package/dist/worker/renderWorker.d.ts +1 -0
- package/doc//345/244/232/347/233/256/346/240/207/344/274/230/345/214/226/344/270/216/351/242/204/346/265/213/346/270/262/346/237/223/350/257/264/346/230/216.md +186 -0
- package/doc//345/244/232/347/273/204/344/273/266/344/272/213/344/273/266/346/226/271/346/241/210.md +410 -0
- package/doc//345/257/271/345/244/226/346/216/245/345/217/243/350/257/264/346/230/216.md +519 -0
- package/doc//345/267/245/344/275/234/350/256/241/345/210/222.md +59 -0
- package/doc//346/270/262/346/237/223/344/270/232/345/212/241/351/200/273/350/276/221.md +202 -0
- package/doc//347/253/231/347/202/271/346/270/262/346/237/223/344/270/216/345/217/263/351/224/256/350/217/234/345/215/225/345/256/236/347/216/260/350/257/264/346/230/216.md +49 -0
- package/doc//350/247/206/345/217/243/351/251/261/345/212/250/346/225/260/346/215/256/346/265/201/347/250/213/344/277/256/346/224/271/350/256/241/345/210/222.md +69 -0
- package/doc//351/241/271/347/233/256/350/257/264/346/230/216/344/271/246.md +729 -0
- package/package.json +51 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# cesiumScene 多目标优化、预测与渲染说明
|
|
2
|
+
|
|
3
|
+
> 梳理对象:`低空基线/baseFrontend/src/components/cesiumScene`
|
|
4
|
+
> 关注点:海量目标如何不卡、目标位置如何预测、预测结果如何平滑渲染。
|
|
5
|
+
|
|
6
|
+
## 一、整体分层(数据怎么变成画面)
|
|
7
|
+
|
|
8
|
+
整条链路是单向流动,每一层只做一件事:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
业务数据(JSON / WebSocket)
|
|
12
|
+
│ 全量 setData / 增量 applyIncremental
|
|
13
|
+
▼
|
|
14
|
+
DataManager 差分:算出 新增/更新/删除,并预处理坐标
|
|
15
|
+
│ dirty 事件 (onPointsDirty / onPathsDirty / onAreasDirty)
|
|
16
|
+
▼
|
|
17
|
+
NodeManager 维护每个目标的"逻辑态":位置、航向、速度、历史点、预测点、交互态
|
|
18
|
+
│ 把节点投影成 RenderItem(渲染协议对象)
|
|
19
|
+
▼
|
|
20
|
+
InstanceManager 归一化预制体,转交渲染层
|
|
21
|
+
│
|
|
22
|
+
▼
|
|
23
|
+
RenderManager 按 renderKind 分发:model / polyline / point
|
|
24
|
+
│
|
|
25
|
+
▼
|
|
26
|
+
ModelRenderer 等 真正画出来:实例化合批 + 动画对象池
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
关键分界:
|
|
30
|
+
|
|
31
|
+
- **逻辑位(logicalPosition)**:服务端给的权威位置,代表"目标真实在哪"。
|
|
32
|
+
- **渲染位(renderPosition)**:当前这一帧实际画在屏幕上的位置,由预测+插值算出来,代表"现在画在哪"。
|
|
33
|
+
|
|
34
|
+
两者分开,是整个预测与平滑渲染的设计基石。
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 二、多目标性能优化
|
|
39
|
+
|
|
40
|
+
海量目标(数千架无人机/船舶)同时渲染的瓶颈在于 Cesium 单模型的 draw call。这里用了五个手段叠加。
|
|
41
|
+
|
|
42
|
+
### 1. 实例化合批(默认主路径)
|
|
43
|
+
|
|
44
|
+
同类型 + 同模型文件的目标,被归到同一个"桶"(bucket),整桶用一个 `ModelInstanceCollection`(MIC)渲染。
|
|
45
|
+
|
|
46
|
+
- 分桶规则:`类型|模型URI`,例如 `uav|/model/model_wrj.glb` 一个桶,船舶圆柱体另一个桶。
|
|
47
|
+
- 一桶内不管有多少目标,本质上是一次合批提交,draw call 大幅下降。
|
|
48
|
+
- 桶有 `dirty` 标记,只有增删目标或显隐变化才触发整桶重建;单纯位置移动只改矩阵,不重建。
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
桶(bucket)
|
|
52
|
+
├─ items: Map<id, {matrix, scale, show}> 每个目标一条
|
|
53
|
+
└─ collection: ModelInstanceCollection 整桶一次渲染
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. 动画对象池(近处增强路径)
|
|
57
|
+
|
|
58
|
+
合批模型不能各自播放动画。所以另设一个**固定大小的动画模型池**(默认 38 个槽位),只给"看得最清楚"的少数目标用带动画的独立模型。
|
|
59
|
+
|
|
60
|
+
工作方式:
|
|
61
|
+
|
|
62
|
+
- 每 200ms 评估一次(`evalIntervalMs`),从所有无人机里筛出候选,按优先级排序取 `topN`。
|
|
63
|
+
- 命中 topN 的目标:从池里借一个槽位,绑定独立动画模型,**同时隐藏它在合批里的实例**(避免重影)。
|
|
64
|
+
- 退出 topN 的目标:归还槽位,恢复合批实例显示。
|
|
65
|
+
- 同一时刻一个目标只走一种承载,可理解为"近处高表现、远处高性能"。
|
|
66
|
+
|
|
67
|
+
候选筛选条件(层层过滤):
|
|
68
|
+
|
|
69
|
+
| 维度 | 作用 |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| 相机高度区间 | 超出范围直接整体跳过 |
|
|
72
|
+
| 距离 / maxDistance | 太远不进池 |
|
|
73
|
+
| 朝向 dot | 在相机背后的剔除 |
|
|
74
|
+
| 屏幕坐标 | 不在画布内的剔除 |
|
|
75
|
+
| 视锥 + 包围球 | 不在视野内的剔除 |
|
|
76
|
+
| 近距放宽 | 很近的目标放宽侧向/边缘判定,避免"明明在眼前却没动画" |
|
|
77
|
+
|
|
78
|
+
排序优先级:先屏幕内 → 再离相机最近 → 再靠近屏幕中心 → 再正对朝向。
|
|
79
|
+
|
|
80
|
+
### 3. 选中/悬停优先抢占
|
|
81
|
+
|
|
82
|
+
被点击或悬停的目标,通过 `setUavAnimationPriorityIds` 插队到候选队列最前面,并跳过常规过滤,保证"我点中的目标一定在动"。还有**近距锁定回差**(enter 200m / exit 300m),避免目标在阈值附近反复进出池导致动画闪烁。
|
|
83
|
+
|
|
84
|
+
### 4. 相机高度分级(逻辑缩放 + 总开关)
|
|
85
|
+
|
|
86
|
+
`NodeManager` 监听相机变化(合并到每帧最多一次 rAF):
|
|
87
|
+
|
|
88
|
+
- 按相机离地高度算一个**缩放乘子**,下发给模型、选中环、投影圆面、地面点,让目标在不同视高下保持合适的视觉大小。最终乘子 = 高度倍率 × 角度倍率(顶视=1,平视=3)。
|
|
89
|
+
- 相机高于 `masterHideHeight`(当前默认 **150km**)时,隐藏模型内容(点模式仍保留),高空不渲染海量模型。
|
|
90
|
+
|
|
91
|
+
### 5. 帧批处理栅栏(commitFrame)
|
|
92
|
+
|
|
93
|
+
所有增删改先收集,最后调用一次 `commitFrame()` 统一落帧,避免每条消息都触发重建。`commitFrame` 内部顺序:先 tick 动画池绑定切换,再重建 dirty 的桶。
|
|
94
|
+
|
|
95
|
+
> 兜底:MIC 创建失败时自动 `_fallbackToSingleModels()` 退化为单模型,保证可用性。
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 三、预测逻辑(目标下一刻在哪)
|
|
100
|
+
|
|
101
|
+
当前实现采用 worker 侧预测状态机。每个目标按 id 维护一份 `PredictionMotionState`:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
运动状态(per target)
|
|
105
|
+
├─ base : 最新权威经纬度、高度、航向、水平速度、垂直速度
|
|
106
|
+
├─ renderStart : 新数据到达时,上一段预测在当前时刻的实际渲染位置
|
|
107
|
+
├─ fit : 航线拟合秒数,默认 2 秒
|
|
108
|
+
└─ predict : 预测窗口秒数,默认 10 秒;窗口后继续匀速外推
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
状态只在目标数据新增或更新时重建。重建前先用旧状态计算“此刻应该画在哪”,再把这个位置作为新航线的拟合起点,因此不会在新权威包到达时跳到权威点或远端预测点。
|
|
112
|
+
|
|
113
|
+
### 预测启用条件
|
|
114
|
+
|
|
115
|
+
预测只发生在 worker 输出渲染包时,满足以下条件才会计算预测坐标:
|
|
116
|
+
|
|
117
|
+
- 目标位于 buffered viewport 内,即视口内 + 缓冲区。
|
|
118
|
+
- 当前是单目标,`count === 1`;聚合簇不预测。
|
|
119
|
+
- 类型配置开启 `predictMove`。
|
|
120
|
+
- 相机高度处于低模 LOD 高度区间内:`cameraHeight <= pointAbove`。
|
|
121
|
+
- 相机高度不高于类型级 `predictMinCameraHeight`。
|
|
122
|
+
|
|
123
|
+
类型级 `predictSeconds` / `predictFitSeconds` 优先于全局配置;未配置时使用全局默认 `10` 和 `2`。
|
|
124
|
+
|
|
125
|
+
### 预测点怎么算
|
|
126
|
+
|
|
127
|
+
- 前 `predictFitSeconds` 秒:从 `renderStart` 用 easeInOutCubic 插值到“新权威点沿最新速度和航向走 fit 秒”的位置,航向同步按最短角插值。
|
|
128
|
+
- `predictFitSeconds` 后:以最新权威点为基准,按 `speedH`、`speedV`、`heading` 匀速外推。
|
|
129
|
+
- 超过 `predictSeconds` 后不停止,继续沿最新航线外推,避免数据中断时目标停顿。
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 四、渲染逻辑(预测怎么平滑地画出来)
|
|
134
|
+
|
|
135
|
+
worker 直接把预测后的 `lon/lat/height/heading` 写入 `RenderPacket`,并打上 `FLAG_PREDICT_MOVE`。渲染层不再自行外推,不再维护 anchor/correction/blend 状态。
|
|
136
|
+
|
|
137
|
+
renderer 只做一层很短的模型位置平滑:
|
|
138
|
+
|
|
139
|
+
- 预测目标 `moveDurationMs = 80ms`,避免渲染缓动和 worker 拟合叠加成明显滞后。
|
|
140
|
+
- 低模预测目标会从合批低模中拆出来用独立模型渲染,保证每帧位置可以平滑更新。
|
|
141
|
+
- 强制高模显示的目标在低模 LOD 高度内同样消费 worker 预测坐标。
|
|
142
|
+
- `predictMove: false` 的目标继续使用原 `smoothMoveDurationMs` 平滑行为。
|
|
143
|
+
|
|
144
|
+
### 高度显示曲线
|
|
145
|
+
|
|
146
|
+
真实高度低的目标贴地不好看,写入渲染高度时统一过一条曲线 `scaleHeightForDisplay`:真实越低显示倍率越高,但显示高度始终随真实高度单调递增。**逻辑位仍保留原始米值**,业务读真实高度用 `logicalPosition.height`。
|
|
147
|
+
|
|
148
|
+
### 另一条插值入口
|
|
149
|
+
|
|
150
|
+
`applyPointRenderSample` 是给"外部已经算好样本"的场景用的(如回放、`TargetPositionInterpolator` 每帧 postRender 批量回调),只更新渲染位与矩阵,不动逻辑位。`TargetPositionInterpolator` 走的是经纬度线性插值 + 航向 slerp,按固定时长(默认 500ms)从当前姿态过渡到新航迹。
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 五、关键数据流闭环(运行时)
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
业务数据 / mock 数据到达
|
|
158
|
+
→ MultiTargetScene stage-packed / incremental / realtime-upsert
|
|
159
|
+
→ renderWorker 更新 Store,并按目标 id 更新 PredictionMotionState
|
|
160
|
+
→ viewport 请求到达:
|
|
161
|
+
buffered viewport 选目标 → 单目标预测 → 写 RenderPacket
|
|
162
|
+
→ renderer 消费 RenderPacket:
|
|
163
|
+
点云/低模/高模按 worker 输出坐标渲染
|
|
164
|
+
→ 若上一包包含预测目标:
|
|
165
|
+
MultiTargetScene 在 rAF 中按 updateFps 继续请求 viewport,驱动预测坐标连续刷新
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- 预测只在 worker 内部维护;公开的 `RenderPacket` 坐标协议不变。
|
|
169
|
+
- renderer 不再做预测外推;预测目标直接采样 worker 每帧输出的位置,2 秒拟合完全由 worker 状态机负责。
|
|
170
|
+
- 目标删除、全量替换、实时删除会同步清理预测状态。
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 六、调参速查
|
|
175
|
+
|
|
176
|
+
| 参数 | 位置 | 含义 |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| `predictSeconds=10` | worker/config | 预测窗口秒数;窗口后仍持续外推 |
|
|
179
|
+
| `predictFitSeconds=2` | worker/config | 新权威数据到达后的航线拟合秒数 |
|
|
180
|
+
| `predictMinCameraHeight` | type config | 类型级预测最大相机高度 |
|
|
181
|
+
| `PREDICT_MOVE_DURATION_MS=80` | ModelPoolRenderer | 预测目标的 renderer 短平滑时长 |
|
|
182
|
+
| `masterHideHeight=150000` | SceneConfig | 高空隐藏模型阈值 |
|
|
183
|
+
| `poolSize=38 / topN=38` | ModelRenderer | 动画池容量与同时动画上限 |
|
|
184
|
+
| `evalIntervalMs=200` | ModelRenderer | 动画池评估间隔 |
|
|
185
|
+
| `nearLockEnter/Exit=200/300` | ModelRenderer | 近距锁定回差 |
|
|
186
|
+
| `UPDATE_FPS` | CesiumDemo | 帧循环频率 |
|
package/doc//345/244/232/347/273/204/344/273/266/344/272/213/344/273/266/346/226/271/346/241/210.md
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# 多组件事件方案
|
|
2
|
+
|
|
3
|
+
本文档定义一套面向业务组件的全局通信方案,用于地图、列表、详情面板、轨迹面板、告警面板等组件之间互相调用能力和同步状态。
|
|
4
|
+
|
|
5
|
+
核心目标:
|
|
6
|
+
|
|
7
|
+
- 每个组件使用唯一命名空间注册能力,例如 `cesium-map`、`target-table`、`alarm-panel`。
|
|
8
|
+
- 组件之间通过统一总线通信,避免互相直接持有实例。
|
|
9
|
+
- 区分“让某个组件做事”的命令调用,以及“某个状态发生变化”的事件广播。
|
|
10
|
+
- 所有注册都能注销,方便组件卸载、页面切换和热更新。
|
|
11
|
+
|
|
12
|
+
## 一、基本概念
|
|
13
|
+
|
|
14
|
+
### 命名空间
|
|
15
|
+
|
|
16
|
+
`namespace` 是组件在全局通信系统中的唯一名字。
|
|
17
|
+
|
|
18
|
+
推荐命名:
|
|
19
|
+
|
|
20
|
+
| 组件 | namespace |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| Cesium 多目标地图 | `cesium-map` |
|
|
23
|
+
| 目标列表 | `target-table` |
|
|
24
|
+
| 目标详情 | `target-detail` |
|
|
25
|
+
| 轨迹面板 | `track-panel` |
|
|
26
|
+
| 告警面板 | `alarm-panel` |
|
|
27
|
+
|
|
28
|
+
约束:
|
|
29
|
+
|
|
30
|
+
- 一个 namespace 同一时间只允许一个 owner 注册命令。
|
|
31
|
+
- 事件名也建议带 namespace,例如 `cesium-map:selectionChanged`。
|
|
32
|
+
- namespace 使用短横线命名,不使用中文、不使用空格。
|
|
33
|
+
|
|
34
|
+
### 命令调用
|
|
35
|
+
|
|
36
|
+
命令调用表示“点名让某个组件执行一个动作”,通常有返回值或失败原因。
|
|
37
|
+
|
|
38
|
+
例如:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
await componentBus.call("cesium-map", "selectTarget", {
|
|
42
|
+
targetId: "ship-001",
|
|
43
|
+
clearBefore: true,
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
含义:
|
|
48
|
+
|
|
49
|
+
> 找到命名空间为 `cesium-map` 的组件,调用它注册的 `selectTarget` 动作,并传入参数。
|
|
50
|
+
|
|
51
|
+
适合命令调用的动作:
|
|
52
|
+
|
|
53
|
+
- 选中目标
|
|
54
|
+
- 定位目标
|
|
55
|
+
- 设置目标颜色
|
|
56
|
+
- 隐藏/显示目标
|
|
57
|
+
- 打开轨迹
|
|
58
|
+
- 清空选择
|
|
59
|
+
|
|
60
|
+
### 事件广播
|
|
61
|
+
|
|
62
|
+
事件广播表示“某个事情发生了”,所有感兴趣的组件都可以监听。
|
|
63
|
+
|
|
64
|
+
例如:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
componentBus.emit("cesium-map:selectionChanged", {
|
|
68
|
+
targets,
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
适合事件广播的状态:
|
|
73
|
+
|
|
74
|
+
- 地图选中目标变化
|
|
75
|
+
- 鼠标悬停目标变化
|
|
76
|
+
- 目标被点击
|
|
77
|
+
- 右键菜单触发
|
|
78
|
+
- 轨迹点 hover
|
|
79
|
+
- 统计指标变化
|
|
80
|
+
|
|
81
|
+
## 二、推荐依赖
|
|
82
|
+
|
|
83
|
+
推荐使用 `mitt` 作为事件发布订阅底座,再自行封装命名空间命令协议。
|
|
84
|
+
|
|
85
|
+
理由:
|
|
86
|
+
|
|
87
|
+
- `mitt` 很小,API 简单,只有 `on/off/emit`。
|
|
88
|
+
- 支持 TypeScript。
|
|
89
|
+
- 不绑定框架,可在 Vue、React、原生 JS 中使用。
|
|
90
|
+
- 我们需要的 `namespace + action + params + return` 不属于普通事件库职责,自己薄封一层更清楚。
|
|
91
|
+
|
|
92
|
+
不建议第一版直接使用 `rxjs`。除非后续需要复杂的数据流操作,例如节流、合并、重放、状态流、跨多个数据源组合。
|
|
93
|
+
|
|
94
|
+
## 三、总线 API 设计
|
|
95
|
+
|
|
96
|
+
建议总线暴露四类方法:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
export interface ComponentBus {
|
|
100
|
+
register(namespace: string, actions: ComponentActionMap): DisposeFn;
|
|
101
|
+
call<Result = unknown>(namespace: string, action: string, params?: unknown): Promise<Result>;
|
|
102
|
+
|
|
103
|
+
on<Payload = unknown>(event: string, handler: ComponentEventHandler<Payload>): DisposeFn;
|
|
104
|
+
emit<Payload = unknown>(event: string, payload?: Payload): void;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type DisposeFn = () => void;
|
|
108
|
+
|
|
109
|
+
export type ComponentActionMap = Record<
|
|
110
|
+
string,
|
|
111
|
+
(params?: unknown, ctx?: ComponentActionContext) => unknown | Promise<unknown>
|
|
112
|
+
>;
|
|
113
|
+
|
|
114
|
+
export interface ComponentActionContext {
|
|
115
|
+
namespace: string;
|
|
116
|
+
action: string;
|
|
117
|
+
requestId: string;
|
|
118
|
+
at: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type ComponentEventHandler<Payload = unknown> = (payload: Payload) => void;
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 为什么叫 `call`
|
|
125
|
+
|
|
126
|
+
`call(namespace, action, params)` 比 `emit` 更适合命令:
|
|
127
|
+
|
|
128
|
+
- 它会找到明确的目标组件。
|
|
129
|
+
- 它可以返回结果。
|
|
130
|
+
- 它可以抛出错误。
|
|
131
|
+
- 它表达的是“调用能力”,不是“广播事件”。
|
|
132
|
+
|
|
133
|
+
如果团队更喜欢,也可以命名为 `invoke` 或 `execute`。本文统一使用 `call`。
|
|
134
|
+
|
|
135
|
+
## 四、最小实现示例
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import mitt from "mitt";
|
|
139
|
+
|
|
140
|
+
type DisposeFn = () => void;
|
|
141
|
+
type ActionHandler = (params?: unknown, ctx?: ActionContext) => unknown | Promise<unknown>;
|
|
142
|
+
type ActionMap = Record<string, ActionHandler>;
|
|
143
|
+
|
|
144
|
+
interface ActionContext {
|
|
145
|
+
namespace: string;
|
|
146
|
+
action: string;
|
|
147
|
+
requestId: string;
|
|
148
|
+
at: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const emitter = mitt<Record<string, unknown>>();
|
|
152
|
+
const registry = new Map<string, ActionMap>();
|
|
153
|
+
|
|
154
|
+
export const componentBus = {
|
|
155
|
+
register(namespace: string, actions: ActionMap): DisposeFn {
|
|
156
|
+
if (!namespace) throw new Error("component namespace is required.");
|
|
157
|
+
if (registry.has(namespace)) {
|
|
158
|
+
throw new Error(`component namespace already registered: ${namespace}`);
|
|
159
|
+
}
|
|
160
|
+
registry.set(namespace, actions);
|
|
161
|
+
return () => {
|
|
162
|
+
const current = registry.get(namespace);
|
|
163
|
+
if (current === actions) registry.delete(namespace);
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
async call<Result = unknown>(namespace: string, action: string, params?: unknown): Promise<Result> {
|
|
168
|
+
const actions = registry.get(namespace);
|
|
169
|
+
if (!actions) throw new Error(`component namespace not found: ${namespace}`);
|
|
170
|
+
|
|
171
|
+
const handler = actions[action];
|
|
172
|
+
if (!handler) throw new Error(`component action not found: ${namespace}.${action}`);
|
|
173
|
+
|
|
174
|
+
const ctx: ActionContext = {
|
|
175
|
+
namespace,
|
|
176
|
+
action,
|
|
177
|
+
requestId: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
178
|
+
at: Date.now(),
|
|
179
|
+
};
|
|
180
|
+
return await handler(params, ctx) as Result;
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
on<Payload = unknown>(event: string, handler: (payload: Payload) => void): DisposeFn {
|
|
184
|
+
emitter.on(event, handler as (payload: unknown) => void);
|
|
185
|
+
return () => emitter.off(event, handler as (payload: unknown) => void);
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
emit<Payload = unknown>(event: string, payload?: Payload): void {
|
|
189
|
+
emitter.emit(event, payload);
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## 五、`cesium-map` 第一版命令
|
|
195
|
+
|
|
196
|
+
Cesium 多目标框架初始化后注册命名空间:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
const disposeCesiumMap = componentBus.register("cesium-map", {
|
|
200
|
+
selectTarget: async (params) => {
|
|
201
|
+
const p = params as { targetId: string; clearBefore?: boolean };
|
|
202
|
+
return framework.selectTarget(p.targetId, { clearBefore: p.clearBefore });
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
selectTargets: async (params) => {
|
|
206
|
+
const p = params as { targetIds: string[]; clearBefore?: boolean };
|
|
207
|
+
return framework.selectTargets(p.targetIds, { clearBefore: p.clearBefore });
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
locateTarget: async (params) => {
|
|
211
|
+
const p = params as { targetId: string; options?: LocateTargetOptions };
|
|
212
|
+
return framework.locateTarget(p.targetId, p.options);
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
clearSelection: () => {
|
|
216
|
+
framework.clearSelection();
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
setTargetColor: (params) => {
|
|
220
|
+
const p = params as { targetId: string | string[]; color: string; clearBefore?: boolean };
|
|
221
|
+
framework.setTargetColor(p.targetId, p.color, { clearBefore: p.clearBefore });
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
setTargetGlow: (params) => {
|
|
225
|
+
const p = params as { targetId: string | string[]; enabled: boolean; color?: string; clearBefore?: boolean };
|
|
226
|
+
framework.setTargetGlow(p.targetId, p.enabled, p.color, { clearBefore: p.clearBefore });
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
hideTarget: (params) => {
|
|
230
|
+
const p = params as { targetId: string | string[]; clearBefore?: boolean };
|
|
231
|
+
framework.hideTarget(p.targetId, { clearBefore: p.clearBefore });
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
showTarget: (params) => {
|
|
235
|
+
const p = params as { targetId: string | string[]; clearBefore?: boolean };
|
|
236
|
+
framework.showTarget(p.targetId, { clearBefore: p.clearBefore });
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
boxTarget: (params) => {
|
|
240
|
+
const p = params as { targetId: string | string[]; enabled?: boolean; clearBefore?: boolean };
|
|
241
|
+
framework.boxTarget(p.targetId, p.enabled, { clearBefore: p.clearBefore });
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
unboxTarget: (params) => {
|
|
245
|
+
const p = params as { targetId: string | string[]; clearBefore?: boolean };
|
|
246
|
+
framework.unboxTarget(p.targetId, { clearBefore: p.clearBefore });
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
setTracks: (params) => {
|
|
250
|
+
const p = params as { tracks: TrackRecord[] };
|
|
251
|
+
framework.tracks.setTracks(p.tracks);
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
showTracks: (params) => {
|
|
255
|
+
const p = params as { targetIds: string[] };
|
|
256
|
+
framework.tracks.showTracks(p.targetIds);
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
hideTracks: (params) => {
|
|
260
|
+
const p = params as { targetIds: string[] };
|
|
261
|
+
framework.tracks.hideTracks(p.targetIds);
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
clearTracks: () => {
|
|
265
|
+
framework.tracks.clearTracks();
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
调用方示例:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
await componentBus.call("cesium-map", "selectTarget", {
|
|
274
|
+
targetId: "ship-001",
|
|
275
|
+
clearBefore: true,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await componentBus.call("cesium-map", "locateTarget", {
|
|
279
|
+
targetId: "ship-001",
|
|
280
|
+
options: { range: 12000, duration: 0.8 },
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## 六、`cesium-map` 第一版事件
|
|
285
|
+
|
|
286
|
+
Cesium 组件把内部事件转成全局事件:
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
const disposers = [
|
|
290
|
+
framework.scene.on("pick", (target) => {
|
|
291
|
+
componentBus.emit("cesium-map:targetPicked", { target });
|
|
292
|
+
}),
|
|
293
|
+
|
|
294
|
+
framework.scene.on("hover", (target) => {
|
|
295
|
+
componentBus.emit("cesium-map:targetHovered", { target });
|
|
296
|
+
}),
|
|
297
|
+
|
|
298
|
+
framework.scene.on("selection", (targets) => {
|
|
299
|
+
componentBus.emit("cesium-map:selectionChanged", { targets });
|
|
300
|
+
}),
|
|
301
|
+
|
|
302
|
+
framework.scene.on("contextmenu", (context) => {
|
|
303
|
+
componentBus.emit("cesium-map:contextMenu", { context });
|
|
304
|
+
}),
|
|
305
|
+
|
|
306
|
+
framework.tracks.on("hover", (hover) => {
|
|
307
|
+
componentBus.emit("cesium-map:trackHover", { hover });
|
|
308
|
+
}),
|
|
309
|
+
];
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
其他组件监听:
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
const dispose = componentBus.on("cesium-map:selectionChanged", (payload) => {
|
|
316
|
+
const p = payload as { targets: TargetSnapshot[] };
|
|
317
|
+
// 更新详情面板或列表选中态
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
推荐事件清单:
|
|
322
|
+
|
|
323
|
+
| 事件 | 说明 |
|
|
324
|
+
| --- | --- |
|
|
325
|
+
| `cesium-map:targetPicked` | 地图点击目标变化 |
|
|
326
|
+
| `cesium-map:targetHovered` | 地图 hover 目标变化 |
|
|
327
|
+
| `cesium-map:selectionChanged` | 地图选中集合变化 |
|
|
328
|
+
| `cesium-map:contextMenu` | 地图右键上下文 |
|
|
329
|
+
| `cesium-map:trackHover` | 轨迹点 hover |
|
|
330
|
+
| `cesium-map:statsChanged` | 地图统计变化,后续可按需增加 |
|
|
331
|
+
|
|
332
|
+
## 七、参数规范
|
|
333
|
+
|
|
334
|
+
参数统一使用对象,不使用 JSON 字符串。
|
|
335
|
+
|
|
336
|
+
推荐:
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
componentBus.call("cesium-map", "selectTarget", {
|
|
340
|
+
targetId: "ship-001",
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
不推荐:
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
componentBus.call("cesium-map", "selectTarget", "{\"targetId\":\"ship-001\"}");
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
原因:
|
|
351
|
+
|
|
352
|
+
- 对象能保留类型。
|
|
353
|
+
- 不需要反复 `JSON.parse`。
|
|
354
|
+
- 后续可以接 TypeScript 类型和运行时校验。
|
|
355
|
+
- 错误更早暴露。
|
|
356
|
+
|
|
357
|
+
## 八、错误处理
|
|
358
|
+
|
|
359
|
+
命令调用建议直接抛错,由调用方决定是否提示用户。
|
|
360
|
+
|
|
361
|
+
常见错误:
|
|
362
|
+
|
|
363
|
+
| 错误 | 说明 |
|
|
364
|
+
| --- | --- |
|
|
365
|
+
| namespace 未注册 | 目标组件还没加载或已卸载 |
|
|
366
|
+
| action 未注册 | 调用了不存在的动作 |
|
|
367
|
+
| 参数错误 | 缺少 id、颜色格式错误等 |
|
|
368
|
+
| 业务失败 | 目标不存在、定位失败等 |
|
|
369
|
+
|
|
370
|
+
调用方示例:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
try {
|
|
374
|
+
await componentBus.call("cesium-map", "locateTarget", { targetId });
|
|
375
|
+
} catch (error) {
|
|
376
|
+
console.warn("[target-table] locate target failed", error);
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## 九、生命周期
|
|
381
|
+
|
|
382
|
+
所有注册和监听都必须在组件卸载时注销。
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
const disposers = [
|
|
386
|
+
componentBus.register("target-table", actions),
|
|
387
|
+
componentBus.on("cesium-map:selectionChanged", handleSelectionChanged),
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
function dispose() {
|
|
391
|
+
for (const fn of disposers) fn();
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
注意:
|
|
396
|
+
|
|
397
|
+
- 地图销毁时要注销 `cesium-map` 命令。
|
|
398
|
+
- 页面切换时要注销本页面所有事件监听。
|
|
399
|
+
- 同一个 namespace 重复注册应直接报错,避免多个 owner 抢同一组命令。
|
|
400
|
+
|
|
401
|
+
## 十、后续增强
|
|
402
|
+
|
|
403
|
+
第一版先保持轻量,后续可逐步加入:
|
|
404
|
+
|
|
405
|
+
- TypeScript 泛型协议:为 `cesium-map.selectTarget` 绑定参数和返回值类型。
|
|
406
|
+
- 运行时参数校验:例如接入 `zod` 或手写轻量校验。
|
|
407
|
+
- 调用日志:记录 `namespace/action/requestId/duration/error`。
|
|
408
|
+
- 权限控制:限制某些组件不能调用危险命令。
|
|
409
|
+
- 异步超时:防止某个命令 Promise 长时间不返回。
|
|
410
|
+
- wildcard 事件:例如监听 `cesium-map:*` 做统一调试。
|