fix: restore live V2X signal and vehicle fidelity in Cesium preview
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>
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
{"file": ".trellis/spec/preview/index.md", "reason": "校验改动未违反预览层约束:零依赖、无构建步骤、浏览器原生语法、IIFE。"}
|
||||
{"file": "docs/v2x-cesium-preview.md", "reason": "校验文档与实现一致:接口清单、坐标契约、配置项、回滚说明。"}
|
||||
{"file": ".trellis/tasks/08-24-v2x-realtime-cross-fidelity/prd.md", "reason": "AC1-AC10 是验收依据,逐条核对。"}
|
||||
{"file": ".trellis/tasks/08-24-v2x-realtime-cross-fidelity/design.md", "reason": "核对灯色字典、相位映射算法、心跳与订阅帧是否与源项目逐项对齐。"}
|
||||
233
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/design.md
Normal file
233
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/design.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# Design — 还原 V2X 实时路口信号灯与车辆展示
|
||||
|
||||
## 1. 现状与根因
|
||||
|
||||
`scripts/lib/v2x-cesium-overlay.js`(418 行,单个 IIFE)承担了登录、REST、三个 WS、坐标转换、
|
||||
实体渲染的全部职责。REST 路径与登录契约与源项目一致,失真集中在数据处理与渲染层。
|
||||
|
||||
逐项根因(对应 PRD 的 R1–R10):
|
||||
|
||||
| # | 现象 | 根因位置 |
|
||||
| --- | --- | --- |
|
||||
| R1 | 连接几十秒后静默中断 | `openSocket()` 无心跳、无重连 |
|
||||
| R2 | 灯永远红 | `lampColor()` 判 `2`/`3`;真实码是 `21/22/23` |
|
||||
| R3 | 原生信号灯模型从不亮 | `signalByPhase` 回退 `signal.id`,与 `phaseNo` 永不相等 |
|
||||
| R4 | 同相位只亮一条进口道 | `state.linkPhases` 是 `Map<phase, entity>`,后写覆盖 |
|
||||
| R5 | 无倒计时 | `updateSignalPhases()` 丢弃 `countDown` |
|
||||
| R6 | OBU 可能一辆车都没有 | `connectObuSocket()` 的 `onOpen` 传 `null`,未发订阅帧 |
|
||||
| R7 | 幽灵车堆积 | `state.vehicles` 只增不删,未用推送 `interval` |
|
||||
| R8 | 车型错配 | 按数组下标取模型,非 `${type}${subType}` 语义 |
|
||||
| R9 | 推送可能整条被丢 | `JSON.parse` + 静默 `catch`,源项目用 `saferEval` |
|
||||
| R10 | 多余请求 | `FlowTravelRatio/queryListWeek` 不属实时路口 |
|
||||
|
||||
## 2. 边界与不变量
|
||||
|
||||
保持不变:
|
||||
- overlay 仍是**单文件零依赖 IIFE**,由 `build-area.js:561` 原样拷入产物。不新增文件,避免改动
|
||||
`area-preview.js` 的 HTML 装配与包契约。
|
||||
- 纯逻辑继续通过 `createV2xCesiumOverlay.utils` 导出,供 `scripts/test-v2x-cesium-overlay.js`
|
||||
的 `vm.runInNewContext` 沙箱直接测试(无 DOM、无 Cesium)。
|
||||
- 坐标契约不变:GCJ-02 → WGS84 只在建实体前转一次。
|
||||
- 令牌只进 `sessionStorage`,页面始终从登录门进入。
|
||||
|
||||
新增的可测试纯函数一律**不触碰 `window` / `Cesium` / `document`**,副作用留在薄薄的适配层。
|
||||
|
||||
## 3. 模块内部分层
|
||||
|
||||
文件内按四层组织,自下而上:
|
||||
|
||||
```
|
||||
L1 纯工具 gcj02ToWgs84 / joinUrl / md5 / toWebSocketUrl / getAngle / haversine
|
||||
L2 纯解析 lampColorName / parseLoosePayload / normalizeObuVehicle /
|
||||
normalizeTargetVehicles / normalizeSignalLamps
|
||||
L3 纯状态机 createVehicleRegistry / createPhaseBinding / buildPhaseSignalMap
|
||||
L4 副作用层 createSocket(心跳/重连) / 实体渲染 / UI 面板
|
||||
```
|
||||
|
||||
L1–L3 全部导出到 `.utils`,测试只打 L1–L3。
|
||||
|
||||
## 4. 关键设计决策
|
||||
|
||||
### 4.1 灯色状态字典(R2)
|
||||
|
||||
直接采用源项目 `HologramCross/components/utils.ts` 的字典,语义化返回而非直接返回颜色,
|
||||
使 overlay 与 `cesium-preview.js` 共用同一套判定:
|
||||
|
||||
```
|
||||
11 -> "off" 灭灯
|
||||
21 -> "red"
|
||||
22 -> "yellow"
|
||||
23 -> "green"
|
||||
31 -> "other"
|
||||
其他 -> "other" // 不再落到 red
|
||||
```
|
||||
|
||||
`lampColorName(status)` 放在 overlay 的 L2 并导出。`cesium-preview.js` 中重复的
|
||||
`lampColorName()` 删除,改为消费 overlay 通过 `setSignalState` 回调传入的**已归一化**灯态
|
||||
(`{nodeKeys, color, countDown}`),避免两处字典漂移。
|
||||
|
||||
> 取舍:也可以把字典抽到共享文件让两边 require,但 overlay 必须是浏览器端零依赖 IIFE,
|
||||
> `cesium-preview.js` 同样是浏览器端脚本,两者无模块系统。让数据流单向(overlay → preview)
|
||||
> 比共享常量更简单,也消除了双字典。
|
||||
|
||||
### 4.2 相位 → 原生信号灯映射(R3,本任务最难的一处)
|
||||
|
||||
`traffic-signals.json` 的信号灯项只有 `phaseGroup: 0`,**不含 V2X `phaseNo`**,两侧没有公共 ID。
|
||||
唯一可靠的连接是**几何**。
|
||||
|
||||
源项目的做法给了线索:`CrossTrafficLights3D.vue:96-104` 对每条 link 取 `geom.coordinates` 的
|
||||
最后两点,末点即停止线位置,两点连线的方位角即进口道航向。原生信号灯项恰好也有
|
||||
`stopLongitude/stopLatitude` 与 `headingDegrees`。
|
||||
|
||||
因此定义纯函数:
|
||||
|
||||
```
|
||||
buildPhaseSignalMap(links, nativeSignals, options) -> {
|
||||
byPhase: Map<phaseNo, string[]>, // nodeKey 列表
|
||||
bound: number,
|
||||
unbound: Array<{phaseNo, reason}>,
|
||||
diagnostics: Array<{linkId, matchedNodeKeys, distanceMeters, headingDeltaDegrees}>
|
||||
}
|
||||
```
|
||||
|
||||
算法:
|
||||
1. 对每条 link:解析 `geom`,取末两点,**先 GCJ-02 → WGS84**,再算 `stopPoint` 与
|
||||
`approachHeading = getAngle(prePoint, lastPoint)`。
|
||||
2. 对每个原生 signal:取 `(stopLongitude, stopLatitude)` 与**行车方向**。
|
||||
|
||||
行车方向的语义由生成侧 `scripts/lib/traffic-signals.js` 定死,不是猜的:
|
||||
- `:54` `headingDegrees = atan2(axis)`,而 `axis = roadAxis(stopLineCenter → intersectionCenter)`,
|
||||
即**停止线指向路口中心的行车方向**。
|
||||
- `:82` `mast_heading_deg = heading - 90`
|
||||
- `:83` `face_heading_deg = heading + 180`
|
||||
|
||||
运行时 `traffic-signals.json` 中 `headingDegrees === mastHeadingDegrees`(实测 7/7 相等,
|
||||
如 `150.658`),即存的是 **mast 值**;`faceHeadingDegrees` 比它小 90(`60.658`)。因此:
|
||||
|
||||
```
|
||||
travelHeading = (faceHeadingDegrees + 180) % 360 // 首选
|
||||
= (mastHeadingDegrees + 90) % 360 // face 缺失时的等价回退
|
||||
```
|
||||
|
||||
实测对全部 7 个信号自洽。
|
||||
3. 候选条件:`haversine(stopPoint, signalStop) <= maxDistanceMeters`
|
||||
且 `angleDelta(approachHeading, travelHeading) <= maxHeadingDeltaDegrees`。
|
||||
4. 一条 link **可匹配多个** signal(同一进口道的多个灯头),满足 R4 的一半。
|
||||
5. link 的 `phaseList` 中每个 `phase` → 该 link 匹配到的全部 nodeKey,**并集累加**
|
||||
(`Map<phaseNo, Set<nodeKey>>`),满足 R4 的另一半(同相位多进口道)。
|
||||
6. 默认容差:`maxDistanceMeters = 30`、`maxHeadingDeltaDegrees = 45`。
|
||||
7. 未匹配上的相位进入 `unbound`,面板显示「N 已绑定 / M 未绑定」。
|
||||
|
||||
**实现期修正**:设计初稿认为朝向语义不可判定,打算用「三字段 ± 180 的候选集,任一命中即可」。
|
||||
实测该方案在凤树二路路口产生**严重误匹配** —— 一条 link 同时命中 3 条不同 `approachId` 的进口道
|
||||
(距离 1.57 / 13.01 / 17.56 m,角差 3.8 / 7.8 / 5.4°),因为 ±180 × 三字段几乎放行任意 90° 朝向。
|
||||
改为上述单一确定式后,7 条 link 对 7 个信号达成 **7/7 精确一对一**,误匹配为 0。
|
||||
候选集方案已废弃。
|
||||
|
||||
|
||||
**配置覆盖**:`v2xPreview.phaseSignalMap = { "<phaseNo>": ["<nodeKey>", …] }` 存在时,
|
||||
该 phaseNo 直接用配置值,跳过几何匹配。这是几何匹配失败时的逃生舱,也让映射可人工固化。
|
||||
|
||||
> 取舍:不用 `approachId` / `sourceWayId` 匹配,因为那是 OSM 侧标识,V2X link 不携带;
|
||||
> 不用最近邻唯一指派(匈牙利算法),因为一进口道多灯头是正常情况,强制一对一会漏灯。
|
||||
> 容差可配是因为不同路口的停止线标注精度差异较大。
|
||||
|
||||
### 4.3 WebSocket 连接层(R1、R6)
|
||||
|
||||
新增 `createSocket({url, onOpen, onMessage, onClose, heartbeatMs, reconnect})` 副作用封装:
|
||||
|
||||
- 心跳:`setInterval(() => send('{"heartBeat":"ping"}'), 30000)`,与源项目一致。
|
||||
- 重连:指数退避 `1s, 2s, 4s, 8s`,上限 `8s`,**重连成功后重新执行 `onOpen`**,
|
||||
确保订阅帧(`junctionId` / `deviceId` / `bounds`)被重放。这是源项目 `onConnected` 的语义。
|
||||
- `close()` 主动关闭时不触发重连;`dispose()` 清心跳与退避计时器。
|
||||
- 计时器通过参数注入(`setIntervalFn` / `setTimeoutFn`),使重连与心跳逻辑可在测试中用假时钟驱动。
|
||||
|
||||
订阅帧内容严格对齐源项目:
|
||||
|
||||
| socket | 连接后首帧 | 来源 |
|
||||
| --- | --- | --- |
|
||||
| signal | `{"junctionId":"<crossCode>"}` | `CrossTrafficLights3D.vue:129` |
|
||||
| obu | `bounds \|\| ""`(dashboard 传 `null` → 空串) | `useObuCars.ts:31` |
|
||||
| target | `{"deviceId":"<ids>"}` 或 `{"deviceId":null}` | `useTargetCars.ts:50-53` |
|
||||
|
||||
OBU 的 bounds:源项目取 AMap 视野四角。本项目用 Cesium `camera.computeViewRectangle()`
|
||||
换算成同格式 `lng,lat;lng,lat;lng,lat;lng,lat`(**WGS84 → GCJ-02 反向转换**,因为服务端按 GCJ-02
|
||||
过滤),在 `camera.moveEnd` 时节流发送 `{"bounds": "<…>"}`。视野不可用时退回空串,等价于不过滤。
|
||||
|
||||
> 取舍:Cesium 是倾斜视角,`computeViewRectangle()` 在极端俯仰下可能返回 `undefined`;
|
||||
> 此时退回空串而非跳过发送,保证仍能收到全量推送——宁可多收也不要空场景。
|
||||
|
||||
### 4.4 车辆注册表(R7、R8)
|
||||
|
||||
纯状态机 `createVehicleRegistry({now})`:
|
||||
|
||||
```
|
||||
registry.ingest(vehicles, interval) -> {added, updated, hidden}
|
||||
registry.sweep(nowMs) -> hiddenKeys
|
||||
registry.list() -> VehicleRecord[]
|
||||
```
|
||||
|
||||
- 复刻源项目:`interval = Number(payload.interval) === 0 ? 500 : Number(payload.interval)`。
|
||||
- `sweep()`:`Math.abs(now - timeStamp) >= interval * 1.5` 时 `visible = false`。
|
||||
- 槽位复用:新车优先占用同模型的 `visible === false` 记录(源项目 `useTargetCars.ts:110`),
|
||||
减少 Cesium 实体的增删抖动。
|
||||
- `now` 注入,测试用假时钟推进。
|
||||
- socket `onClose` 时 `registry.clear()`,对齐源项目 `onDisconnected: cars.value = []`。
|
||||
|
||||
渲染适配层据 `visible` 切 `entity.show`,**不删实体**,与槽位复用配套。
|
||||
|
||||
模型选择(R8):`modelNameFor(vehicle)` 返回 `car_obu.glb` 或 `${type}${subType}.glb`;
|
||||
在包内 `vehicleModelNames` 中按文件名匹配,命中不了则回退 `vehicleModelNames[0]` 并累加
|
||||
`missingModels` 计数供面板展示。
|
||||
|
||||
平滑:用 `Cesium.SampledPositionProperty` 按 `duration` 插值,替代当前的直接赋值跳变。
|
||||
|
||||
### 4.5 宽松解析(R9)
|
||||
|
||||
源项目用 `saferEval`。浏览器端零依赖不能引入该库,也不应引入 `eval`。改为
|
||||
`parseLoosePayload(text)`:先 `JSON.parse`;失败则做一次受限规范化(单引号→双引号、
|
||||
裸键补引号、去尾逗号、`NaN`/`Infinity` → `null`)后重试;仍失败返回 `null` 并
|
||||
`state.parseFailures += 1`,面板显示失败计数(R9 要求可见)。
|
||||
|
||||
> 取舍:不用 `new Function` 还原 `saferEval` 的完整语义——那等于在预览页开一个任意代码执行面。
|
||||
> 受限规范化覆盖实际会遇到的非严格 JSON 形态,且失败可观测,比静默丢弃安全得多。
|
||||
|
||||
### 4.6 移除范围外请求(R10)
|
||||
|
||||
`loadLiveData()` 的 `Promise.allSettled` 去掉 `FlowTravelRatio/queryListWeek`,
|
||||
`state.metrics` 及面板中的 `flow metrics` 文案一并移除。
|
||||
|
||||
## 5. 数据流
|
||||
|
||||
```
|
||||
登录 → resolveCrossCode()
|
||||
→ queryCrossLinkInfo / queryPoles / findDeviceByCrossCode / crossDeviceConfig
|
||||
→ buildPhaseSignalMap(links, nativeSignals) [纯]
|
||||
→ connectLiveSockets()
|
||||
signal → normalizeSignalLamps → lampColorName → setSignalState({nodeKeys,color,countDown})
|
||||
→ cesium-preview 点亮原生灯头 + 倒计时
|
||||
obu → parseLoosePayload → normalizeObuVehicle ┐
|
||||
target → parseLoosePayload → normalizeTargetVehicles ┴→ registry.ingest → 渲染适配
|
||||
registry.sweep → entity.show
|
||||
```
|
||||
|
||||
## 6. 兼容性与回滚
|
||||
|
||||
- `v2xPreview.enabled=false` 时 `createV2xCesiumOverlay()` 仍返回 `null`,生成页不含面板,
|
||||
包产物字节不变(AC10)。
|
||||
- 原生信号灯在**无 V2X 灯态时**的行为保持现状(`hideUnconfirmedSignalAssets` 语义不变),
|
||||
只有收到实时灯态才点亮,不会退回模拟相位。
|
||||
- 回滚点:本任务对 `cesium-preview.js` 的改动限于删除重复的 `lampColorName` 与调整
|
||||
`setSignalState` 入参形状,可单独 revert 而不影响 overlay。
|
||||
- 新增 `phaseSignalMap`、`maxDistanceMeters`、`maxHeadingDeltaDegrees` 均为可选配置,
|
||||
缺省即当前默认行为。
|
||||
|
||||
## 7. 风险
|
||||
|
||||
| 风险 | 影响 | 缓解 |
|
||||
| --- | --- | --- |
|
||||
| 几何匹配在实际路口全部落空 | 灯仍不亮 | 面板显性报「0 已绑定」;`phaseSignalMap` 配置覆盖兜底 |
|
||||
| `computeViewRectangle()` 返回 undefined | OBU 过滤异常 | 退回空串=不过滤 |
|
||||
| 宽松解析未覆盖真实非法形态 | 丢消息 | `parseFailures` 计数可见,便于按真实样本补规则 |
|
||||
| 服务端心跳格式与源项目不符 | 仍被断开 | 心跳报文与间隔取自源项目源码,AC1 用模拟服务端验证 |
|
||||
| 真实推送 `interval` 缺失 | 车辆过早隐藏 | 缺失时回退 500ms,与源项目一致 |
|
||||
@@ -0,0 +1,4 @@
|
||||
{"file": ".trellis/spec/preview/index.md", "reason": "预览层契约:无构建步骤、零依赖 IIFE、参数经 window.OSM_ASSET_PREVIEW_CONFIG 注入。overlay 与 cesium-preview.js 的所有改动都受这些约束。"}
|
||||
{"file": ".trellis/spec/pipeline/index.md", "reason": "确认 build-area.js 如何把 v2x-cesium-overlay.js 拷入产物,避免误增文件破坏包契约。"}
|
||||
{"file": ".trellis/spec/config/index.md", "reason": "新增可选配置 v2xPreview.phaseSignalMap / maxDistanceMeters / maxHeadingDeltaDegrees 需符合区域配置约定。"}
|
||||
{"file": "docs/v2x-cesium-preview.md", "reason": "现有 V2X 预览文档:配置项、坐标契约、代理要求、回滚方式。移除 FlowTravelRatio 与新增配置后需同步更新。"}
|
||||
171
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/implement.md
Normal file
171
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/implement.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# 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` 事件气泡
|
||||
99
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/prd.md
Normal file
99
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/prd.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# 还原 V2X 实时路口信号灯与车辆展示
|
||||
|
||||
## Goal
|
||||
|
||||
把 `v2x_web` dashboard「实时路口」(`HologramCross`) 的实时数据行为完整搬到本项目的 Cesium 预览中,使
|
||||
`outputs/<area>/…-cesium-preview.html` 在登录后能持续、正确地显示 **信号灯相位灯色与倒计时** 和
|
||||
**OBU / 目标识别车辆**。
|
||||
|
||||
当前实现(Codex 移植)接口地址是对的,但数据处理与渲染层基本失真,实际效果是:灯永远红、车可能一辆都不出现、
|
||||
出现后也不消失、连接几十秒后被服务端断开。本任务修复这条链路。
|
||||
|
||||
## Source of Truth
|
||||
|
||||
参照实现(只读,不修改):
|
||||
`/Users/que01/Project/v2x_web/applications/dashboard/src/views/dashboard/components/Content/Map/HologramCross/`
|
||||
|
||||
- `components/CrossTrafficLights3D.vue` — 信号灯:link 相位、WS 订阅、灯色/倒计时
|
||||
- `components/utils.ts` — 灯色状态字典、航向角计算
|
||||
- `components/CrossCars/hooks/useObuCars.ts` — OBU 车辆
|
||||
- `components/CrossCars/hooks/useTargetCars.ts` — 目标识别车辆
|
||||
- `components/CrossCars/type.ts` — 车辆数据结构
|
||||
|
||||
## Requirements
|
||||
|
||||
### R1 WebSocket 连接层对齐
|
||||
- 三个 socket(`signal` / `obuPosition` / `targetPosition`)均按源项目发送心跳
|
||||
`{"heartBeat":"ping"}`,间隔 30000ms。
|
||||
- 断线自动重连,重连后重新发送各自的订阅消息(signal 发 `junctionId`,target 发 `deviceId`,obu 发 bounds)。
|
||||
- 页面 `visibilitychange` 回到可见时行为不得导致重复连接堆积;`dispose()` 必须清掉心跳与重连计时器。
|
||||
|
||||
### R2 信号灯灯色映射修正
|
||||
- 采用源项目 `utils.ts` 的状态字典:`11=灭灯 21=红 22=黄 23=绿 31=其他`。
|
||||
- 现有 `lampColor()`(overlay)与 `lampColorName()`(`cesium-preview.js`)中基于 `2`/`3` 的判断必须移除。
|
||||
- 未知状态码按「其他/灭灯」处理,不得静默落到红色。
|
||||
|
||||
### R3 相位到原生信号灯的映射
|
||||
- `traffic-signals.json` 中不存在 V2X `phaseNo`(只有 `phaseGroup: 0`),当前回退到 `signal.id` 导致永不匹配。
|
||||
必须建立一条真实可用的 V2X `phaseNo` → 原生信号灯头的映射。
|
||||
- 映射结果必须可诊断:面板要能显示「N 个相位已绑定 / M 个未绑定」。
|
||||
- 允许区域配置提供显式覆盖映射。
|
||||
|
||||
### R4 一个相位可点亮多条进口道
|
||||
- 同一 `phaseNo` 对应的所有 link / 信号灯头都要被点亮,当前 `Map<phase, entity>` 的覆盖行为必须消除。
|
||||
|
||||
### R5 倒计时
|
||||
- 使用推送中的 `countDown` 字段(源项目 `phaseValue['time'] = signal.countDown`),驱动预览中已有的
|
||||
countdown 资产或等效显示。
|
||||
|
||||
### R6 车辆订阅正确建立
|
||||
- OBU socket 连接后必须发送订阅消息(源项目 `send(bounds || '')`),相机视野变化时发送
|
||||
`{"bounds": "<lng,lat;…>"}`。
|
||||
- target socket 连接后发送 `{"deviceId": "<ids joined by comma>"}`,无配置时发送 `{"deviceId": null}`。
|
||||
|
||||
### R7 车辆生命周期
|
||||
- 按源项目实现超时隐藏:超过 `interval * 1.5` 未更新的车辆隐藏并可复用槽位;`interval` 取自推送体,
|
||||
`0` 时回退 `500`。
|
||||
- socket 断开时清空车辆列表。
|
||||
- 车辆位置更新使用推送间隔做平滑插值,避免逐帧跳变。
|
||||
|
||||
### R8 车辆模型命名对齐
|
||||
- OBU 固定使用 `car_obu.glb` 语义的模型槽;目标识别车辆使用 `${type}${subType}.glb` 语义的模型槽。
|
||||
- 包内缺少对应模型时回退到默认车模型,并在面板提示,不得静默显示错误车型。
|
||||
|
||||
### R9 推送体解析健壮性
|
||||
- 源项目对两个车辆 socket 使用 `saferEval` 而非 `JSON.parse`,说明载荷不保证严格 JSON。
|
||||
- 解析必须容忍非严格 JSON,且解析失败要计数并在面板可见,不得整条静默丢弃。
|
||||
|
||||
### R10 范围纠正
|
||||
- `/facilities/api/FlowTravelRatio/queryListWeek`(周流量比)不属于「实时路口」,从实时加载链路移除。
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- 不移植 AMap 底图 / 矢量瓦片图层(`linkVectorHdMapTileUrl`)。
|
||||
- 不移植 `CrossDevices/Model3D.vue` 的 3D 杆件模型;设备维持点位显示(可后续独立任务)。
|
||||
- 不移植 `useV2XEvents` 的 V2X 事件气泡。
|
||||
- 不改变原生包产物(GLB / metadata / traffic-signals.json 的生成逻辑)。
|
||||
|
||||
## Constraints
|
||||
|
||||
- 不得提交上游地址、账号、令牌、AMap key;`v2xPreview` 配置保持非敏感。
|
||||
- 令牌只存 `sessionStorage`,页面始终从登录门进入。
|
||||
- 坐标契约不变:V2X 数据 GCJ-02,进 Cesium 前转一次 WGS84;原生数据不得二次转换。
|
||||
- `v2xPreview.enabled` 默认 `false`,关闭时生成页不含 V2X 面板。
|
||||
- 生成的预览为静态页,无构建步骤,overlay 保持零依赖 IIFE。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] AC1 三个 socket 均可见 30s 心跳;模拟服务端在 60s+ 后连接仍存活。
|
||||
- [ ] AC2 单测覆盖灯色映射:`21→红 22→黄 23→绿 11→灭 31→其他`,且 `2`/`3` 不再被当作黄/绿。
|
||||
- [ ] AC3 单测覆盖相位映射:给定 V2X link 相位与 `traffic-signals.json`,输出的绑定数 > 0,
|
||||
未绑定项被明确列出。
|
||||
- [ ] AC4 单测覆盖同相位多进口道:一个 `phaseNo` 对应 2 条 link 时,两条都被点亮。
|
||||
- [ ] AC5 单测覆盖车辆超时:注入两次推送并推进时间超过 `interval*1.5`,车辆转为不可见。
|
||||
- [ ] AC6 单测覆盖 OBU/target 订阅消息:连接后发出的首帧内容与源项目一致。
|
||||
- [ ] AC7 单测覆盖非严格 JSON 推送体能被解析,且失败计数可读。
|
||||
- [ ] AC8 `npm run test:v2x-cesium-preview`、`test:v2x-preview-server`、`test:preview-assets` 全绿。
|
||||
- [ ] AC9 真机(`npm run serve:v2x-preview` 接真实上游)登录后可见:信号灯随相位变色并显示倒计时、
|
||||
车辆出现并移动、离开后消失。此项由开发者人工确认。
|
||||
- [ ] AC10 `v2xPreview.enabled=false` 重新生成后,页面不含 V2X 面板,包产物字节不变。
|
||||
26
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/task.json
Normal file
26
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/task.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "v2x-realtime-cross-fidelity",
|
||||
"name": "v2x-realtime-cross-fidelity",
|
||||
"title": "还原 V2X 实时路口信号灯与车辆展示",
|
||||
"description": "",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-24",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
Reference in New Issue
Block a user