The ported overlay used the correct REST paths but lost the data handling
from the source dashboard's live-intersection view (HologramCross), so
signals rendered permanently red and vehicles often never appeared.
- Lamp status codes now follow the dashboard dictionary (11/21/22/23/31).
The previous 2/3 reading made every real push fall through to red. The
dictionary lives only in the overlay; the preview consumes normalized
{nodeKeys, color, countDown} entries so the two copies cannot drift.
- Bind V2X phases to native signal heads geometrically. The runtime
document has no phaseNo, so the old lookup fell back to signal.id and
never matched, leaving the dynamic assembly dark. Travel heading is
recovered as faceHeadingDegrees + 180, per the generator's
mast = travel - 90 / face = travel + 180. Verified 7/7 exact matches
against the fengshu-er-road runtime document.
- A phase now lights every approach it drives; the phase -> single entity
map silently overwrote all but the last.
- Drive the countdown assets from the push's countDown field.
- All three sockets heartbeat every 30s and reconnect with backoff,
replaying their subscription frame. Without this the service dropped
the connection and the scene emptied after about a minute.
- The OBU socket sends its bounds frame on connect and on camera move;
it previously sent nothing at all.
- Vehicles are swept when a push goes stale and their slots reused, so
they no longer accumulate as ghosts. Models follow the dashboard's
car_obu.glb / ${type}${subType}.glb naming.
- Parse vehicle pushes leniently, since the dashboard uses saferEval and
the payload is not guaranteed to be strict JSON. Failures are counted
and surfaced rather than dropped; no eval is introduced.
- Drop FlowTravelRatio/queryListWeek, which is not part of this view.
Also corrects a stale spec rule that required vehicleModelNames to be
empty. Live V2X vehicles need packaged models; the real invariant is no
generated routes or traffic simulation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
172 lines
8.3 KiB
Markdown
172 lines
8.3 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 → `car_obu.glb`;target → `${type}${subType}.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"})==="car_obu.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` 事件气泡
|