172 lines
8.4 KiB
Markdown
172 lines
8.4 KiB
Markdown
# Implement — 还原 V2X 实时路口信号灯与车辆展示
|
||
|
||
## 参照实现(只读)
|
||
|
||
`/Users/que01/Project/v2x_web/applications/dashboard/src/views/dashboard/components/Content/Map/HologramCross/`
|
||
|
||
改动前先读对应源文件,不要凭记忆写契约。
|
||
|
||
## 改动文件
|
||
|
||
| 文件 | 性质 |
|
||
| --- | --- |
|
||
| `scripts/lib/v2x-cesium-overlay.js` | 主要改动,L1–L4 分层重写 |
|
||
| `scripts/lib/cesium-preview.js` | 删除重复 `lampColorName`,调整 `setSignalState` 入参 |
|
||
| `scripts/test-v2x-cesium-overlay.js` | 扩充单测,覆盖 AC2–AC7 |
|
||
| `.trellis/tasks/.../prd.md` `design.md` | 如实现中发现偏差需回写 |
|
||
|
||
不改:`build-area.js`、`area-preview.js`、包产物生成逻辑。
|
||
|
||
## 执行顺序
|
||
|
||
每步结束跑一次 `npm run test:v2x-cesium-preview`,保持绿色再进下一步。
|
||
|
||
### 步骤 1 — L2 灯色字典 + 宽松解析(R2、R9)
|
||
|
||
1. 在 overlay 加 `LAMP_STATUS`(`11/21/22/23/31`)与 `lampColorName(status)`,未知返回 `"other"`。
|
||
2. 删除旧 `lampColor()` 中 `Number(status)===3 / ===2` 的判断。
|
||
3. 加 `parseLoosePayload(text)`:`JSON.parse` → 受限规范化重试 → `null`。
|
||
4. 心跳报文 `pong` 短路判定保留(与源项目一致)。
|
||
5. 二者导出到 `.utils`。
|
||
|
||
验证:`node scripts/test-v2x-cesium-overlay.js`
|
||
- `lampColorName(21)==="red"`, `(22)==="yellow"`, `(23)==="green"`, `(11)==="off"`, `(31)==="other"`
|
||
- **`lampColorName(2)!=="yellow"` 且 `lampColorName(3)!=="green"`**(防回归,AC2)
|
||
- `parseLoosePayload("{'a':1,}")` 解析成功;`parseLoosePayload("{bad")===null`(AC7)
|
||
|
||
**审查点 A**:灯色字典必须与 `HologramCross/components/utils.ts` 的
|
||
`lightStatusColorDict` 逐项一致,不得增删码值。
|
||
|
||
### 步骤 2 — L1 几何工具(支撑 R3)
|
||
|
||
1. 加 `getAngle(start, end)`:直接对齐源项目 `utils.ts` 的实现(返回 0–360)。
|
||
2. 加 `haversineMeters(a, b)`、`angleDeltaDegrees(a, b)`(后者处理 350° vs 10° 的环绕)。
|
||
3. 导出到 `.utils`。
|
||
|
||
验证:正北 `getAngle([0,0],[0,1])≈0`;正东 `≈90`;
|
||
`angleDeltaDegrees(350,10)===20`;`haversineMeters` 对已知两点误差 < 0.5m。
|
||
|
||
### 步骤 3 — 相位映射(R3、R4)
|
||
|
||
1. 实现 `buildPhaseSignalMap(links, nativeSignals, options)`,返回
|
||
`{byPhase, bound, unbound, diagnostics}`,算法见 `design.md` §4.2。
|
||
2. link 末两点**先 GCJ-02→WGS84 再算角度与距离**(原生信号是 WGS84)。
|
||
3. 一 link 匹配多 signal;`Map<phaseNo, Set<nodeKey>>` 并集累加。
|
||
4. 支持 `options.phaseSignalMap` 配置覆盖,命中则跳过几何匹配。
|
||
5. 默认 `maxDistanceMeters=30`、`maxHeadingDeltaDegrees=45`。
|
||
6. 导出到 `.utils`。
|
||
|
||
验证(AC3、AC4):
|
||
- 用 `outputs/fengshu-er-road/package/runtime/traffic-signals.json` 的真实 7 条信号做夹具,
|
||
配一组构造的 link(末点取自 `stopLongitude/stopLatitude` 反算 GCJ-02、
|
||
航向取自 `headingDegrees`),断言 `bound > 0`。
|
||
- 构造两条不同 link 共用同一 `phaseNo`,断言 `byPhase.get(phaseNo)` 含**两条 link 的全部 nodeKey**。
|
||
- 构造一条远离所有信号的 link,断言其相位进入 `unbound` 且带 `reason`。
|
||
- 配置覆盖生效:`phaseSignalMap` 指定的 phaseNo 取配置值。
|
||
|
||
**审查点 B**:这是全任务最易做错的一步。合入前必须用真实 `traffic-signals.json` 跑出
|
||
非零绑定数,若为 0 则说明容差或坐标方向反了,**不得靠放大容差蒙混过关**——先打印
|
||
`diagnostics` 的实际距离/角差,确认量级合理(距离应是米级而非千米级)。
|
||
|
||
### 步骤 4 — 车辆注册表(R7、R8)
|
||
|
||
1. 实现 `createVehicleRegistry({now})`,含 `ingest / sweep / list / clear`。
|
||
2. `interval` 取自推送体,`0` 或缺失回退 `500`。
|
||
3. `sweep`:`|now - timeStamp| >= interval*1.5` → `visible=false`。
|
||
4. 槽位复用:优先占用同 `model` 的不可见记录。
|
||
5. `modelNameFor(vehicle)`:OBU → dashboard 最终模板使用的 `11.glb`;target → `${type}${subType}.glb`。把 dashboard 车辆 GLB 复制到预览包。
|
||
6. `normalizeTargetVehicles` 补上源项目的 `type==1 && isNil(subType) → subType=1`。
|
||
7. 导出 `createVehicleRegistry`、`modelNameFor`。
|
||
|
||
验证(AC5):
|
||
- 注入一次推送 → `list()` 一条 `visible=true`。
|
||
- 假时钟推进 `interval*1.5` 后 `sweep()` → 该车 `visible=false`。
|
||
- 再注入同 key → 复用原槽位,`list().length` 不变。
|
||
- `clear()` 后为空。
|
||
- `modelNameFor({kind:"obu"})==="11.glb"`;`{kind:"target",type:1,subType:2}` → `"12.glb"`。
|
||
|
||
### 步骤 5 — WebSocket 连接层(R1、R6)
|
||
|
||
1. 实现 `createSocket({url, onOpen, onMessage, onClose, heartbeatMs, reconnect, socketFactory, setIntervalFn, setTimeoutFn})`。
|
||
2. 心跳 `{"heartBeat":"ping"}` / 30000ms。
|
||
3. 退避重连 `1s→2s→4s→8s` 封顶,**重连成功重放 `onOpen`**。
|
||
4. 主动 `close()` 不重连;`dispose()` 清所有计时器。
|
||
5. 三个 socket 的订阅帧按 `design.md` §4.3 的表格实现。
|
||
6. OBU bounds:`camera.computeViewRectangle()` → WGS84→GCJ-02 → `"lng,lat;…"` 四角,
|
||
`camera.moveEnd` 节流发送;不可用时发空串。
|
||
7. 导出 `createSocket`、`buildBoundsMessage`。
|
||
|
||
验证(AC1、AC6):
|
||
- 假 socket + 假时钟:推进 30s 断言收到一帧 `{"heartBeat":"ping"}`;推进 90s 断言三帧。
|
||
- 模拟 `onclose` 后推进退避时间,断言重新建连**且订阅帧被重放**。
|
||
- signal 首帧 `{"junctionId":"<code>"}`;target 无配置首帧 `{"deviceId":null}`,
|
||
有配置 `{"deviceId":"1,2"}`;obu 首帧为空串。
|
||
- `dispose()` 后推进时钟不再产生任何发送。
|
||
|
||
**审查点 C**:心跳报文与间隔必须逐字节对齐源项目
|
||
(`useObuCars.ts:28-31`、`useTargetCars.ts:27-30`、`CrossTrafficLights3D.vue:122-130`)。
|
||
|
||
### 步骤 6 — 接线与渲染适配(R4、R5、R10)
|
||
|
||
1. `loadLiveData()` 移除 `FlowTravelRatio/queryListWeek`,删 `state.metrics` 与面板 flow 文案。
|
||
2. 用 `buildPhaseSignalMap` 结果替换 `state.linkPhases`(`Map<phase, entity>` 整体删除)。
|
||
3. `updateSignalPhases` → 归一化为 `{nodeKeys, color, countDown}` 列表后调
|
||
`context.setSignalState(...)`,同时给 link polyline 上色(同相位多 link 全部上色)。
|
||
4. 车辆渲染改为消费 `registry`:`visible` 切 `entity.show`,位置用
|
||
`SampledPositionProperty` 按 `duration` 插值。
|
||
5. 面板状态行补:已绑定/未绑定相位数、缺失模型数、解析失败数。
|
||
6. `dispose()` 清 socket、计时器、实体、registry。
|
||
|
||
### 步骤 7 — `cesium-preview.js` 对接(R2、R5)
|
||
|
||
1. 删除 `cesium-preview.js` 中的 `lampColorName()`(与 overlay 重复且映射错误)。
|
||
2. `createLiveTrafficSignals().update(lamps)` 改签名为
|
||
`update(entries)`,`entries = [{nodeKeys:string[], color:"red"|"yellow"|"green"|"off"|"other", countDown:number}]`。
|
||
3. 按 `nodeKeys` 直接点亮 `TrafficSignalDynamic_<nodeKey>_<color>` 节点,
|
||
删除失效的 `signalByPhase` 回退逻辑。
|
||
4. `countDown` 驱动 `category === "countdown"` 资产显示。
|
||
5. `color === "off" | "other"` 时三色节点全灭。
|
||
|
||
验证:`npm run test:preview-assets`
|
||
|
||
### 步骤 8 — 全量校验
|
||
|
||
```bash
|
||
npm run test:v2x-cesium-preview
|
||
npm run test:v2x-preview-server
|
||
npm run test:preview-assets
|
||
npm run test:traffic-signals
|
||
npm run test:native-preview-traffic
|
||
npm run test:package-contract
|
||
```
|
||
|
||
AC10 校验:将区域配置 `v2xPreview.enabled` 置 `false` 重新生成,
|
||
确认生成页无 V2X 面板且 `package/` 下产物字节不变。
|
||
|
||
## 人工验收(AC9,需真实上游)
|
||
|
||
```bash
|
||
V2X_UPSTREAM=http://<host>:<port> \
|
||
npm run serve:v2x-preview -- --root outputs/fengshu-er-road --host 0.0.0.0 --port 7862
|
||
```
|
||
|
||
登录后逐项确认:
|
||
- [ ] 信号灯随相位变色,颜色与 dashboard 实时路口一致
|
||
- [ ] 倒计时数字走动
|
||
- [ ] 车辆出现、移动平滑、离开视野后消失(无幽灵车堆积)
|
||
- [ ] 连接持续 5 分钟以上不掉线
|
||
- [ ] 车辆与路口位置无明显偏移(坐标未二次转换)
|
||
- [ ] 面板绑定数、失败数符合预期
|
||
|
||
## 回滚点
|
||
|
||
- 步骤 7 独立可 revert(只动 `cesium-preview.js`)。
|
||
- 步骤 1–6 全在 overlay 单文件内,`git checkout scripts/lib/v2x-cesium-overlay.js` 即回到当前状态。
|
||
- `v2xPreview.enabled=false` 是运行时总开关。
|
||
|
||
## 不做(留待后续任务)
|
||
|
||
- AMap 底图 / 矢量瓦片图层
|
||
- `CrossDevices/Model3D.vue` 3D 杆件模型(设备维持点位)
|
||
- `useV2XEvents` 事件气泡
|