# 调研 —— 点位下设备的位置数据来源与三维呈现原语 **票:** [01 — 调研 —— 点位下设备的位置数据来源与三维呈现原语](../issues/01-research-device-position-data-and-rendering-primitives.md) **日期:** 2026-09-18 **范围:** 只查事实(能做什么、数据在哪),不给建议、不做设计。 **方法与只读声明:** 后端 `E:\pipe-ner-server` 全程**只读**阅读,未修改任何后端文件;按 `AGENTS.md` 未运行任何 build / dev / serve / 浏览器调试 / 截图命令。前端与 `node_modules/cesium` 亦只读。地图 `map.md` 的 `## Notes` 现状事实 1–8 未重复推导,只做独立复核与补充。 **路径简写(全文沿用):** - `SYS/` = `E:\pipe-ner-server\pipe-network-service\zksy-system\src\main\java\com\zksy\` - `ADMIN/` = `E:\pipe-ner-server\pipe-network-service\zksy-admin\src\main\java\com\zksy\` - `FE/` = `E:\City-Lifeline-Cockpit-TvT\src\modules\cockpit\` - `CES/` = `E:\City-Lifeline-Cockpit-TvT\node_modules\cesium\Source\Cesium.d.ts` - `ENG/` = `E:\City-Lifeline-Cockpit-TvT\node_modules\@cesium\engine\Source\` --- ## Bottom line 1. **不能一次拿到。** 后端 20 个 `/api/cockpit/v1/` 端点里,**没有任何一个**能在「某监测点下所有设备」这一粒度上同时给出经纬度 + 坐标校验 + 状态。缺口不在响应层包装,而在**数据模型**:设备一旦按点位归集,坐标就在 `CockpitMonitoringPointDeviceSnapshot` 上被丢掉了。唯一带完整设备坐标的来源(`/equipments/{deviceCode}/archive`)是逐台取。 2. **`deviceCodes` 已交付且可用,但前端零消费。** 它只在 `map-snapshot` 的 `monitoringPoints[].deviceCodes` 回传,**含离线设备**、**不受分页影响**、**无状态过滤**,空值是 `[]` 而非 `null`。前端 `FE/` 乃至整个 `src/` 里 `deviceCodes` **零引用**。 3. **点位与设备各有一份独立的经纬度,且后端没有任何相对位置信息。** `equipment_point_rel` 是纯关联表,无偏移/高度/朝向/井杆字段;全后端模块 grep 相关词 **0 命中**。「设备坐标是否等于点位坐标」是**数据问题,schema 不保证**。 4. **Cesium 1.145.0 上三者都能被单独拾取,只有 `PointPrimitive` 不能贴地。** `BillboardCollection` / `PointPrimitiveCollection` / `LabelCollection` 三者 GPU 侧都是批渲染(draw call 不随点数线性增长);Entity 的额外代价在 JS 对象与每帧 Visualizer update。Entity 的 `point.heightReference` 是**内部拿 billboard 冒充**实现的。 5. **仓库内没有任何设备级 glTF/GLB 模型资产。** 45 个 3D 文件全是建筑/车辆/船只/卫星/人体/道具/倾斜摄影瓦片;唯一「设备形状」的 glTF 只以 `oss.icegl.cn` 远程 URL 存在于插件 demo 代码里。设备点位目前完全依赖 8 个几何 SVG 形状 + `tokens.css` 色键烘焙成 billboard 图片。`SHAPES` 里**没有任何设备形状**。 --- ## 1. 数据来源:现有一张图相关端点里,哪一个能一次给出「某监测点下所有设备的经纬度 + 坐标校验 + 状态」 ### 1.1 端点全景(20 个只读端点) `ADMIN/web/controller/cockpit/` 下 grep `@GetMapping` 共 29 处、覆盖 20 个不同路径(部分控制器多端点)。契约手册的端点清单在 `docs/cockpit/README.md:27-61`,其中「一张图与详情」一节在 `:50-61`。 与设备坐标相关的候选只有 5 个(其余是概览/基础设施/预警处置,不含设备级坐标): | 端点 | 控制器位置 | 能否一次给出「点位下所有设备的经纬度 + 校验 + 状态」 | | --- | --- | --- | | `GET /api/cockpit/v1/monitoring-points/{code}` | `ADMIN/web/controller/cockpit/CockpitMonitoringPointDetailController.java:37` | **不能** —— 有设备清单 + 状态,**无任何经纬度** | | `GET /api/cockpit/v1/map-snapshot` | `ADMIN/.../CockpitMapSnapshotController.java:33` | **不能** —— 点位有坐标 + `deviceCodes`,但**没有设备明细**;带坐标的只有未关联设备 | | `GET /api/cockpit/v1/monitoring/distribution` | `ADMIN/.../CockpitMonitoringDistributionController.java:33` | **不能** —— 点位 marker 无设备清单,设备 marker 只含未关联设备 | | `GET /api/cockpit/v1/monitoring/situation` | `ADMIN/.../CockpitMonitoringSituationController.java:32` | **不能** —— `abnormalEquipment[]` 有设备坐标 + 校验 + 状态,但只含**异常**设备、且**无 `monitoringPointCode`** | | `GET /api/cockpit/v1/equipments/{deviceCode}/archive` | `ADMIN/.../CockpitEquipmentArchiveController.java:37` | **能拿到**,但**逐台一次**(N+1) | ### 1.2 逐端点源码核对 **① `/monitoring-points/{monitoringPointCode}` —— 缺经纬度,缺口在数据模型** - 响应类 `SYS/cockpit/point/CockpitMonitoringPointDetailResponse.java:7-19`,`devices` 字段声明在 `:17`,类型 `List`。 - 卡片类 `SYS/cockpit/point/CockpitMonitoringPointDeviceCard.java:5-16`,共 11 个字段:`deviceCode` `:6`、`deviceName` `:7`、`deviceTypeName` `:8`、`online` `:9`、`alarm` `:10`、`warning` `:11`、`workOrderInProgress` `:12`、`statusReasons` `:13`、`statusTime` `:14`、`deviceStatus` `:15`、`statusLabel` `:16`。**无 longitude / latitude / coordinateValidation。** - 卡片不是直接从数据源构造的:`SYS/cockpit/point/CockpitMonitoringPointStatusResolver.java:34-48` 的 `cards()` 从 `CockpitMonitoringPointDeviceSnapshot` 逐个映射,而快照类 `SYS/cockpit/point/CockpitMonitoringPointDeviceSnapshot.java:6-14` 同样只有那 9 个字段(无坐标)。→ **坐标在更上游的模型里就已经不存在了**,不只是响应层少写了一个 getter。 - 点级坐标校验**是有的**:`CockpitMonitoringPointDetailResponse.java:18` 的 `coordinateValidation`,由 `SYS/cockpit/point/CockpitMonitoringPointDetailService.java:43-44` 调 `CockpitCoordinateValidator.validate(detail.getLongitude(), detail.getLatitude(), detail.isCoordinateVerified())` 得到;输入来自 `SYS/cockpit/point/CockpitMonitoringPointDetail.java:9-11` 的 `longitude` / `latitude` / `coordinateVerified`。校验器在 `SYS/cockpit/foundation/CockpitCoordinateValidator.java:11-32`(产出 `state` / `reason` / `renderable` / `crs`,见 `SYS/cockpit/foundation/CockpitCoordinateValidation.java:3-19`)。 - **注意一个易踩的点**:该响应**不返回监测点自身的 longitude / latitude**(`CockpitMonitoringPointDetailResponse.java:8-19` 无这两个字段),只回校验结论。点位坐标得从 `map-snapshot` 的点位摘要拿。 - 演示页佐证:`docs/cockpit/monitoring-point-detail-demo.html:89-92` 的 `device()` 工厂只造 `deviceCode/deviceName/deviceTypeName/deviceStatus/statusLabel/statusReasons/statusTime`,`:111` 的 `coordinateValidation` 是**点级**的一个对象,不在任何 device 里。 - 适配器只有空实现:`SYS/cockpit/point/EmptyCockpitMonitoringPointDetailAdapter.java:8-10` 的 `load()` 直接 `return null`;接口 `SYS/cockpit/point/CockpitMonitoringPointDetailAdapter.java:3-4`。全后端 grep `CockpitMonitoringPointDetailAdapter` 只命中接口 + 空实现 + 两个测试的 mock → **真实数据适配器尚未落地**(与 `docs/cockpit/README.md:152` 把「数据库级联调」列为未执行项一致)。 **② `/map-snapshot` —— 点位有坐标、有 `deviceCodes`,但没有设备明细** - 服务 `SYS/cockpit/map/CockpitMapSnapshotService.java:75-102`:`monitoringPoints(...)` + `unassociatedEquipment(...)` 两条列表,`total = points.size() + equipment.size()`(`:79`),分页窗口横跨这一条总序列(`:80-87`)。 - 点位摘要 `SYS/cockpit/map/CockpitMonitoringPointSummary.java:10-21` 有 `longitude` `:19`、`latitude` `:20`、`coordinateValidation` `:21`,**但没有 `devices[]`**。 - 未关联设备摘要 `SYS/cockpit/map/CockpitUnassociatedEquipmentSummary.java:8-16` 有 `longitude` `:14`、`latitude` `:15`、`coordinateValidation` `:16`;但它**只含未关联设备** —— `CockpitMapSnapshotService.java:278-288` 用 `associatedDeviceCodes(snapshot)`(`:291-302`,取整份快照里所有点位下的 deviceCode)把已关联设备排除掉。 - 响应类 `SYS/cockpit/map/CockpitMapSnapshotResponse.java:15-18`:`monitoringPoints` / `unassociatedEquipment` / `coordinateValidation` / `mapFilter`。 - 演示页 `docs/cockpit/map-snapshot-demo.html:64-70` 与之一致:点位对象只有 `pointCode/pointName/region/industry/typeCode/typeName/pointStatus/statusReasons/deviceCodes/longitude/latitude/coordinateValidation`,**没有设备明细**。 **③ `/monitoring/distribution` —— 点位 marker 连设备清单都没有** - `MonitoringPointMarker` 在 `SYS/cockpit/monitoring/CockpitMonitoringDistributionResponse.java:274-311`:`pointCode/pointName/region/industry/deviceType/pointStatus/statusReasons/longitude/latitude/coordinateValidation` —— **无 `devices[]`、无 `deviceCodes`**。 - 构造处 `SYS/cockpit/monitoring/CockpitMonitoringDistributionService.java:293-306`;点位校验在 `:269-274`,不可渲染则计数并 `continue`(`:271-274`,`invalidCoordinateCount++`)。 - `UnassociatedEquipmentMarker` 在 `CockpitMonitoringDistributionResponse.java:313-350`,有 `longitude` `:321` / `latitude` `:322` / `coordinateValidation` `:323`;构造处 `CockpitMonitoringDistributionService.java:359-373`,校验在 `:333-338`。 - **已关联设备的坐标在快照模型里就没有**:`SYS/cockpit/monitoring/CockpitMonitoringDistributionSnapshot.java:107` 的 `SourceMonitoringPoint.devices` 类型是 `List`(又是那个无坐标的类),而 `SourceSmartDevice`(同文件 `:161-256`)**才**有 `longitude` `:166` / `latitude` `:167` / `coordinateVerified` `:168` 以及 online/alarm/warning/workOrderInProgress/statusReasons/statusTime。 - 类注释自己写明了这个契约取向:`CockpitMonitoringDistributionSnapshot.java:14-15` —— "Associated devices are described through their monitoring point; the point's typeCode is the shared smart-device type of its devices." **④ `/monitoring/situation` —— 有设备坐标,但只含异常设备、且无点位归属** - `SYS/cockpit/monitoring/CockpitMonitoringSituationResponse.java:183-232` 的 `AbnormalEquipment`:`deviceCode` `:184`、`deviceName` `:185`、`deviceType` `:186`、`industry` `:187`、`region` `:188`、`abnormalType` `:189`、`status` `:190`、`statusReasons` `:191`、`longitude` `:192`、`latitude` `:193`、`coordinateValidation` `:194`。 - 源模型 `SYS/cockpit/monitoring/CockpitMonitoringSituationSnapshot.java:93-167` 的 `SourceMetricDevice` 也有 `longitude` `:101` / `latitude` `:102` / `coordinateVerified` `:103`。 - 但列表名就是 `abnormalEquipment`(响应 `:21`),且**没有任何 `monitoringPointCode` 字段** → 无法按监测点归集。 **⑤ `/equipments/{deviceCode}/archive` —— 有坐标,逐台** - `SYS/cockpit/equipment/CockpitEquipmentArchive.java:26-27` 的 `getLongitude()` / `getLatitude()` 委托给快照 `SYS/cockpit/equipment/CockpitEquipmentArchiveSnapshot.java:16-18`(`longitude` / `latitude` / `equipmentLocation`)。 - 演示页 `docs/cockpit/equipment-archive-demo.html` 同端点的样例载荷一致。 ### 1.3 后端领域模型里设备到底有没有坐标 **有,但是在「已关联」这条路上被丢掉了。** 有坐标的三处快照源模型: - `SYS/cockpit/map/CockpitMapSnapshot.java:100-127` 的 `SourceSmartDevice`:`longitude` `:105`、`latitude` `:106`、`coordinateVerified` `:107`。 - `SYS/cockpit/monitoring/CockpitMonitoringDistributionSnapshot.java:161-256` 的 `SourceSmartDevice`:除坐标外**还带状态**(`online` `:169`、`alarm` `:170`、`warning` `:171`、`workOrderInProgress` `:172`、`statusReasons` `:173`、`statusTime` `:174`)。 - `SYS/cockpit/monitoring/CockpitMonitoringSituationSnapshot.java:93-167` 的 `SourceMetricDevice`:坐标 + 指标状态。 点位侧的快照源模型也都有坐标: - `SYS/cockpit/map/CockpitMapSnapshot.java:67-98` 的 `SourceMonitoringPoint`:`longitude` `:72`、`latitude` `:73`、`coordinateVerified` `:74`、`devices` `:75`。 - `CockpitMonitoringDistributionSnapshot.java:99-159` 的 `SourceMonitoringPoint`:`longitude` `:104`、`latitude` `:105`、`coordinateVerified` `:106`、`devices` `:107`。 **结构性问题(本票最核心的发现)**:两条 `SourceMonitoringPoint.devices`(`CockpitMapSnapshot.java:75`、`CockpitMonitoringDistributionSnapshot.java:107`)的类型都是 `List` —— 正是那个**没有经纬度**的类。也就是说:设备坐标挂在 `SourceSmartDevice` 上;一旦设备按点位归集,就换成 `CockpitMonitoringPointDeviceSnapshot`,**坐标在这一步被丢弃**。响应层的 `CockpitMonitoringPointDeviceCard` 只是继承了这个缺失。 数据库层:`SYS/base/domain/EquipmentBase.java:65-72` 有 `longitude` `:66` / `latitude` `:69` / `equipmentLocation` `:72`(`equipment_base` 表)。 ### 1.4 若不能,缺哪个字段、挂在哪个响应类上、要动的类名与端点 **缺的字段:** 设备级 `longitude` / `latitude` / `coordinateValidation`(或等价的 `coordinateVerified` 布尔)。 **要动的类(按依赖顺序,均为事实陈述的「改动面」,不是设计建议):** 1. `SYS/cockpit/point/CockpitMonitoringPointDeviceSnapshot.java` —— 加 `longitude` / `latitude` / `coordinateVerified`。 2. `SYS/cockpit/point/CockpitMonitoringPointDeviceCard.java` —— 加 `longitude` / `latitude` / `coordinateValidation`。 3. `SYS/cockpit/point/CockpitMonitoringPointStatusResolver.java:34-48` 的 `cards()` —— 透传新字段(它是唯一的卡片构造点)。 4. 若同时要覆盖 `/map-snapshot` 与 `/monitoring/distribution`:两处 `SourceMonitoringPoint.devices` 的**填充方**(适配器实现)也要给坐标;另有两处显式 `new CockpitMonitoringPointDeviceSnapshot(...)` 需要补参数 —— `SYS/cockpit/monitoring/CockpitMonitoringDistributionService.java:343-352` 与 `:469-473`(`effectiveDevice`)。 5. 校验语义可直接复用 `SYS/cockpit/foundation/CockpitCoordinateValidator.java:11-32`(与点位级同一套 state/reason/renderable/crs 词表)。 **端点:不需要新增。** `/monitoring-points/{monitoringPointCode}` 已经回 `devices[]`(`CockpitMonitoringPointDetailController.java:37`),补上字段后即为「一次拿全」。这正是票 09 预期要去推的那张后端工单。 **另外两个可用的补充信道(都不能替代坐标):** - 设备编码 → 点位编码的映射已交付:WebSocket `cockpitDeviceStatusDelta` 帧带 `monitoringPointCode`(`ADMIN/web/websocket/DeviceStatusRedisListener.java:127`、`:187-194`,`:209` 的 `monitoringPointCode(deviceCode)` 实现;口径见 `docs/cockpit/README.md:109` 的 `equipment_base.equipment_code → equipment_point_rel → monitor_point.point_code`)。**帧里只有状态,没有坐标。** `DEVICE_CODE`/`device_code` 别名见 `docs/cockpit/README.md:10`。 - `mapFilter.deviceCodes` 在 `map-snapshot` / `distribution` / `situation` / `events/*` / `overview/monitoring` 等端点回显(`docs/cockpit/README.md:69`),是**编码集合**,不含坐标。 ### 1.5 后端交付状态与既有说明(Gogs 侧) - 前端仓 `.scratch/cockpit-shell-panel-layout/handoff/backend-contract-gaps.md:14-15` 记录 **BT-1 = Gogs #59、BT-2 = Gogs #60,均已于 2026-09-18 交付并关闭**;`:64-87` 是 BT-1 的完整工单草案(其验收标准 `:82` 写明「同一 `snapshotAt` 下与 `/monitoring-points/{code}` 的 `devices[]` 一致」)。 - **#59 交付的是 `deviceCodes`(编码清单),不是坐标** —— 证据是后端 `CockpitMonitoringPointSummary.java:14` 只有 `List deviceCodes`,`:76-79` 的 `MapFilter` 亦然。故票 09 第 2 条设想的「是不是 #59 已经带了坐标而前端适配层漏读」**可以排除**:前端没有漏读,后端确实没给。 - 后端 `docs/cockpit/` 下 26 个 demo 页 + `README.md` 已通读相关部分;**没有任何一页展示设备级坐标**(`monitoring-point-detail-demo.html:89-92`、`map-snapshot-demo.html:64-70`、`equipment-archive-demo.html` 为准)。 - 前端仓 grep `Gogs|gogs` → **0 命中**(Gogs 链接只存在于前端仓的 `.scratch/` 手写文档里,见上)。 --- ## 2. `deviceCodes` 的语义 **字段位置:** `SYS/cockpit/map/CockpitMonitoringPointSummary.java:14`(`private final List deviceCodes`),getter `:45`。构造参数 `:24`。 **回传端点:只有一个。** `CockpitMonitoringPointSummary` 只被 `SYS/cockpit/map/CockpitMapSnapshotResponse.java:15` 序列化 → 只有 `GET /api/cockpit/v1/map-snapshot` 的 `monitoringPoints[].deviceCodes`。 - `/monitoring/distribution` 的 `MonitoringPointMarker`(`CockpitMonitoringDistributionResponse.java:274-311`)**没有**这个字段。 - 同名但不同物:`mapFilter.deviceCodes`(`CockpitMapSnapshotResponse.java:64`、`CockpitMonitoringDistributionResponse.java:232`、`CockpitMonitoringSituationResponse.java:250`、`CockpitCurrentEventsResponse.java:104`、`CockpitMultidimensionalAnalysisResponse.java:115`、`CockpitHistoryAnalysisResponse.java:154`、`CockpitHandlingEfficiencyResponse.java:134`、`CockpitMonitoringOverviewResponse.java:201`)是**另一层语义**(生效筛选范围回显),不要与逐点 `deviceCodes` 混为一谈。 **构造口径(唯一构造点):** `SYS/cockpit/map/CockpitMapSnapshotService.java:246-253` ``` 246: List deviceCodes = devices.stream() 247: .filter(Objects::nonNull) 248: .map(CockpitMonitoringPointDeviceSnapshot::getDeviceCode) 249: .filter(Objects::nonNull) 250: .map(String::trim) 251: .filter(code -> !code.isEmpty()) 252: .distinct() 253: .collect(Collectors.toList()); ``` 逐条回答票面 4 问: | 问题 | 结论 | 证据 | | --- | --- | --- | | 在哪些端点回传 | 只有 `GET /api/cockpit/v1/map-snapshot` 的 `monitoringPoints[].deviceCodes` | `CockpitMapSnapshotResponse.java:15`;`CockpitMonitoringDistributionResponse.java:274-311` 无此字段 | | 是否含离线设备 | **含。** 构造链上**没有任何 online / status 过滤** | `CockpitMapSnapshotService.java:246-253`(只滤 null/空/去重);设备来源是整份 `source.getDevices()`(`:242-244`) | | 是否受分页影响 | **不受。** `deviceCodes` 在生成 summary 时算完(`:246-253` → 构造 `:255-268`),分页只切 summary 对象(`:79-87`)。点位被切到哪一页,它都带**该点位全量**的 deviceCodes | `:79-87`、`:246-268` | | 是否受状态过滤影响 | **不受。** `map-snapshot` 连 `pointStatus` 入参都没有 | 入参签名 `CockpitMapSnapshotService.java:53-54`(`page/pageSize/snapshotAt/industry/region/deviceType`);`docs/cockpit/README.md:52` | | 受什么筛选影响 | 受 `industry` / `region` / `deviceType` 影响,且是**通过点位**受影响:`monitoringPoints()` 先过滤点位(`:229-236`),再对存活点位算 deviceCodes。点位被筛掉 → 它的 deviceCodes 一并消失 | `:229-236`、`:231-233`(`matchesFilters(industry, region, deviceType, source.getTypeCode(), source.getRegion(), types)`) | | 空值语义 | **空数组 `[]`,不是 `null`。** `source.getDevices() == null` 时先退化成 `Collections.emptyList()` | `:242-244`;演示页 `docs/cockpit/map-snapshot-demo.html:64` 与 `:70` 正是 `deviceCodes:[]` | | 顺序 | **源顺序去重,未排序。** 用的是 Stream `distinct()`(保持首次出现顺序) | `:252` | **`mapFilter.deviceCodes` 的独立口径(易混,值得记一笔):** - `CockpitMapSnapshotService.java:321-346`:由「存活点位的 `deviceCodes` ∪ `unassociatedEquipment` 的 `deviceCode`」组成,`sorted(...)` 升序(`:344-345`、`:348-352`);`:56-57` 的类注释与 `docs/cockpit/README.md:69` 都写「过滤后全量、分页前、升序」。 - **一处口径细节**:`unassociatedEquipment` 的排除集用的是 `associatedDeviceCodes(snapshot)`,即**未过滤的整份快照**(`:291-302`);而 `mapFilter` 只遍历**过滤后**的 points + equipment(`:327-343`)。所以被 `region`/`industry` 筛掉的点位,其关联设备的编码既不在 points 里、也不在 equipment 里 → 该编码不出现在 `mapFilter.deviceCodes`。这是既有实现的客观行为,本票只记录。 **前端消费现状:零。** `FE/` 与整个 `E:\City-Lifeline-Cockpit-TvT\src` grep `deviceCodes` → **0 命中**。`FE/domain/mapSummary.js:26-44` 的 `toMonitoringPoint()` 只读 `longitude/latitude/coordinateValidation/pointCode/pointName/pointStatus/statusReasons`,**不读 `deviceCodes`**;文件头 `:4-7` 列的后端形状注释里也没有它。→ 契约已交付但前端尚未接线(这正是票 05「设备点位图层的触发与生命周期」要处理的入口)。 --- ## 3. 设备与点位的空间关系 ### 3.1 两张表各有独立的经纬度列 | 表 | 实体类 | 坐标字段 | 位置描述 | 其它 | | --- | --- | --- | --- | --- | | `monitor_point`(关键监控点位表) | `SYS/base/domain/MonitorPoint.java`,`@TableName(value = "monitor_point", schema = "app_user")` `:23` | `longitude` `:46`、`latitude` `:49` | `location` 位置描述 `:43` | `pointId` `:28`、`pointCode` `:31`、`pointName` `:34`、`networkId` 关联管网设施ID `:37`、`pointType` `:40`、`installationDate` `:53`、`status` `:56`、`delFlag` `:77` | | `equipment_base`(设备基础信息表) | `SYS/base/domain/EquipmentBase.java`,`@TableName("equipment_base")` `:23` | `longitude` `:66`、`latitude` `:69` | `equipmentLocation` 设备位置描述 `:72` | `equipmentId` `:28`、`equipmentCode` `:31`、`equipmentName` `:34`、`equipmentTypeId` `:53`、`installTime` `:76`、`district` `:46`、等 | DDL 佐证:`pipe-network-service/sql/iot_equipment_management.sql:63-83`(`monitor_point`,`longitude DECIMAL(15,10)` `:70`、`latitude` `:71`、`location VARCHAR(512)` `:69`、`network_id` `:67`)。 ### 3.2 关联表是纯关联,没有任何相对位置信息 `SYS/base/domain/EquipmentPointRel.java`:`@TableName(value = "equipment_point_rel", schema = "app_user")` `:21`,字段只有 `relId` `:27`、`equipmentId` `:30`、`pointId` `:33`、`createTime` `:37`、`delFlag` `:41`。 DDL:`sql/iot_equipment_management.sql:88-97` —— `rel_id` / `equipment_id` / `point_id` / `create_time`,主键 + 两个索引,**无其它列**。 → **没有「同井 / 同杆 / 相对点位 / 安装高度 / 朝向 / 方位」这类信息。** **查证范围(明确「查不到」):** - 后端 `pipe-network-service/` 全模块 grep `同井|同杆|杆塔|relativePosition|offsetX|offsetY|mountHeight|installHeight|poleCode|wellCode` → **0 命中**。 - `pipe-network-service/sql/` 全目录 grep `offset|height|azimuth|heading|elevation|altitude|relative` → **0 命中**。 - 设备侧最接近「位置」的自由文本只有 `EquipmentBase.equipmentLocation` `:72`("设备位置描述")与 `MonitorPoint.location` `:43`("位置描述")——都是给人读的字符串,不是可计算的相对几何。 ### 3.3 「设备的经纬度与所属监测点经纬度是否同一个值」 **后端没有任何地方保证或校验它们相等**,因此这**是数据问题,不是契约问题**: - 两张表各存一份独立的 `longitude`/`latitude`(§3.1),没有触发器、没有服务端校验、没有外键约束把二者绑定。 - 后端只在一处显式声明了「关联设备的空间承载对象是它的监测点」这一**契约取向**:`SYS/cockpit/monitoring/CockpitMonitoringDistributionSnapshot.java:14-15` 的类注释 —— "Associated devices are described through their monitoring point." - 结构性推论(本票能给出的最确定的表述):**只要走「已关联设备」这条路,响应里根本取不到设备自己的坐标**(§1.3),所以「是否同一个值」在当前契约下**无法从任一 cockpit 端点观测到**。要回答它,必须先补齐 §1.4 的字段,或逐台调 `/equipments/{deviceCode}/archive` 与点位摘要做比对。 - 前端纪律与此互相印证:`CONTEXT.md` 的 `Avoid_` 禁止「人工坐标偏移 / 把聚合点当作真实监测点」(票 09 正文 `:20` 已引),即**不许前端自己造设备位置**。 --- ## 4. 呈现原语(Cesium 1.145.0) ### 4.1 版本与证据位置 - 实装版本 **`1.145.0`**:`E:\City-Lifeline-Cockpit-TvT\node_modules\cesium\package.json` 的 `version` 字段(另有 `node_modules/cesium/package.json` 里 `@cesium/engine: ^26.3.0` 的运行时实现包)。 - 随包 d.ts **只有一个**:`CES/` = `node_modules/cesium/Source/Cesium.d.ts`(51,152 行)。`node_modules/cesium/Build/Cesium/Cesium.d.ts` **不存在**(`Test-Path` = False),`package.json` 的 `types` 指向 `./Source/Cesium.d.ts`。 - **注意**:随包 d.ts 是**被删减过的**,全文 grep `@performance` → **0 命中**。原版 JSDoc 的 `Performance:` 段落只在官方 ref-doc 网页上。故本节的「开销」结论同时引用三处: 1. 随包 d.ts(API 存在性与签名,行号可复现); 2. 随包实现源码 `ENG/` = `node_modules/@cesium/engine/Source/`(批渲染与拾取的**实现机制**,行号可复现); 3. 官方 ref-doc(`Performance:` 段落的**语义**,给 URL)。 - 官方文档 URL(版本对应 1.145 源码行): - `https://cesium.com/learn/cesiumjs/ref-doc/Billboard.html` - `https://cesium.com/learn/cesiumjs/ref-doc/BillboardCollection.html` - `https://cesium.com/learn/cesiumjs/ref-doc/PointPrimitiveCollection.html` - `https://cesium.com/learn/cesiumjs/ref-doc/PointPrimitive.html` - `https://cesium.com/learn/cesiumjs/ref-doc/Label.html` / `LabelCollection.html` - `https://cesium.com/learn/cesiumjs/ref-doc/Scene.html#pick` ### 4.2 三者的批渲染与单点开销(**机制**可证,**数值**无官方口径) | | `Entity` + `billboard`/`label` graphics | `BillboardCollection` | `PointPrimitiveCollection` | | --- | --- | --- | --- | | 声明位置 | `CES:21784 export class Entity {`(文档 `:21778-21783`「aggregate multiple forms of visualization into a single high-level object」) | `CES:27414 export class BillboardCollection {` | `CES:42804 export class PointPrimitiveCollection {` | | 元素类 | 不是元素:`Entity.billboard` `CES:21836`、`label` `:21864`、`point` `:21889`、`properties` `:21905`、`id` `:21801`(**注意 `Entity.id` 是 `string`**) | `Billboard` `CES:27064` | `PointPrimitive` `CES:42667` | | GPU 侧实际渲染 | **同一批 collection**:`BillboardVisualizer`(`CES:19715-19721`「maps `Entity#billboard` to a `Billboard`」)、`LabelVisualizer`(`CES:23375-23382`)、`PointVisualizer`(`CES:24089-24095`);`DataSourceDisplay.defaultVisualizersCallback()` `CES:21295` 默认挂上三者 | 整集合**一次 instanced draw**:`ENG/Scene/BillboardCollection.js:2089 command.count = 6;`、`:2090 command.instanceCount = billboardsLength;`;instanced ⇒ 恒 1 个 VA(`ENG/Renderer/VertexArrayFacade.js:344`) | `ENG/Scene/PointPrimitiveCollection.js:1154 command.primitiveType = PrimitiveType.POINTS;`、`:496` 注释 `// 1 vertex per pointPrimitive`、**不传 indexBuffer** ⇒ 1 个 VA ⇒ 一次 draw | | draw call 数(默认混合模式) | 同上(取决于它落到哪个 collection) | `OPAQUE_AND_TRANSLUCENT` 时 **2 个命令**:`ENG/Scene/BillboardCollection.js:2063 const totalLength = opaqueAndTranslucent ? vaLength * 2 : vaLength;` | 同理按 blend pass 分:`ENG/Scene/PointPrimitiveCollection.js:1155-1156`(`Pass.OPAQUE` / `Pass.TRANSLUCENT`) | | 官方 `Performance:` 口径 | 无 Entity 级条目 | 见下 | 见下 | | 每元素更新开销 | 每 Entity 一套 JS graphics 对象 + 每帧 Visualizer update(`BillboardVisualizer.update` d.ts `:19728`、`LabelVisualizer.update` `:23389`、`PointVisualizer.update` `:24102`);标签还有字形生成/缓存(`ENG/Scene/LabelCollection.js:96 createGlyphCanvas`) | 逐 billboard 写入 **CPU→GPU traffic**,且**与改了几个属性无关**;改动多时「清空重建」可能更省 | `add`/`remove` 期望常数时间,但会**整块重写顶点缓冲(O(n) + CPU→GPU 开销)**;大批量应先 `add` 完再 `update` | | 可调的性能杠杆 | — | `blendOption`(全不透明或全半透明可提升**最多 2x**)d.ts `:27472`;同标识符的图片**自动共享纹理** d.ts `:27390-27391` | `blendOption`(同 2x)d.ts `:42852`;`add` 的官方建议见下 | | 附带事实 | `DataSourceDisplay.defaultVisualizersCallback()` `CES:21295` 默认挂上三个 Visualizer;`EntityCluster` `CES:21984` 负责懒建集合(`clusterBillboards` `:22017` / `clusterLabels` `:22021` / `clusterPoints` `:22025`) | `BillboardCollection` 构造器需要 `scene` 才能贴地或对地球做深度测试(`CES:27406`) | — | **出处原文(官方 ref-doc,逐字要点):** - `Billboard` 页 `Performance:` —— "Reading a property, e.g., `Billboard#show`, is constant time. Assigning to a property is constant time but results in CPU to GPU traffic when `BillboardCollection#update` is called. **The per-billboard traffic is the same regardless of how many properties were updated.** If most billboards in a collection need to be updated, it may be more efficient to clear the collection with `BillboardCollection#removeAll` and add new billboards instead of modifying each one."(`https://cesium.com/learn/cesiumjs/ref-doc/Billboard.html`) - `PointPrimitiveCollection` 页 `Performance:` —— "For best performance, prefer **a few collections, each with many points**, to many collections with only a few points each. Organize collections so that points with the same update frequency are in the same collection."(`https://cesium.com/learn/cesiumjs/ref-doc/PointPrimitiveCollection.html`) - `PointPrimitiveCollection#add` 页 `Performance:` —— "Calling `add` is expected constant time. However, the collection's vertex buffer **is rewritten - an `O(n)` operation that also incurs CPU to GPU overhead**. For best performance, add as many pointPrimitives as possible before calling `update`." - `PointPrimitiveCollection#remove` 页 —— 同理 O(n) 重写;官方明确「只想临时隐藏点时,用 `PointPrimitive#show` 比 remove + re-add 更省」。 - `LabelCollection` **自己没有绘制路径**,它建在两个 `BillboardCollection` 上:`ENG/Scene/LabelCollection.js:630`(`_backgroundBillboardCollection`)、`:643`(`_glyphBillboardCollection`)、`:274`(**每个字形一个 billboard**)、`:1049-1050`(两者各自 `update(frameState)`)。→ 文本标签的 GPU 成本 ≈ 字形数 × billboard,但仍落在 billboard 批里(`Label` 的 GPU 侧没有独立通道)。 **诚实标注(本票不编数):** Cesium 官方文档与随包源码给出的都是**复杂度与机制**(1 次 draw / O(n) 重写 / 逐元素 traffic 恒定),**没有**「N 个点多少毫秒」这类数值,随包 d.ts 里连 `@performance` 段落都被删了。故本票**不给**量级数字;量的判断属于票 02(特效开销预算)与本图 08(原型)的走查。 ### 4.3 能否单独拾取(不与监测点标记抢) **结论:三者都能逐元素拾取,且拾取互不干扰;但「谁被拾到」由遮挡关系与拾取矩形决定,必须显式处理重叠。** - 每个元素都有自己的 `id`,这就是拾取时回给你的东西: - `Billboard.id: any` `CES:27286`,option 文档 `CES:26994`「A user-defined object to return when the billboard is picked with `Scene#pick`」 - `Label.id: any` `CES:38568`(文档 `:38566`) - `PointPrimitive.id: any` `CES:42755`(文档 `:42753`「the user-defined value returned when the point is picked」) - `Entity.id: string` `CES:21801`(**类型不同**:Entity 路径下拾取回来的 `id` 是 Entity 本身,见下) - **一次 draw call 下仍能逐实例拾取**:pick color 是逐元素的 → `ENG/Scene/BillboardCollection.js:1096-1098`(batchTable 或顶点属性)、`ENG/Scene/PointPrimitiveCollection.js:556` + `:568 AttributeCompression.encodeRGB8(pickColor)` + `:1169 command.pickId = "v_pickColor"`。 - 拾取在**离屏 pick framebuffer** 里做,结果进 `Set` 去重:`ENG/Scene/PickFramebuffer.js:56-98`(`:61 const objects = new Set();`)→ **互不干扰**。 - API 签名与语义: - `Scene.pick(windowPosition, width?, height?): any | undefined` `CES:44947`;文档 `CES:44928-44930`「contains **the first (top) primitive** in the scene at a particular window coordinate」;默认拾取矩形 `3×3`(`CES:44943-44944`)。 - `Scene.drillPick(windowPosition, limit?, width?, height?): any[]` `CES:45054`;文档 `CES:45042-45045`「**all** primitives at a particular window coordinate ... ordered by their visual order in the scene (**front to back**)」。 - `Scene.pickAsync` `CES:44986`;`Scene.pickPosition` `CES:45040`。 - `pickedObject` 的形状固定为 `{ primitive, id, ... }`:`ENG/Renderer/Context.js:1679-1682` 的官方示例即 `context.createPickId({ primitive: this, id: this.id })`;渲染侧 `ENG/Scene/PickFramebuffer.js:78 const object = context.getObjectByPickColor(pickColor);`。 - Entity 路径下 `id` 就是 Entity:`ENG/DataSources/BillboardVisualizer.js:127-128`(`billboard = cluster.getBillboard(entity); billboard.id = entity;`)、`ENG/DataSources/LabelVisualizer.js:138`、`ENG/DataSources/PointVisualizer.js:148`。 **「抢不抢」的实际裁决点(仓库现状,这是票 06 要面对的既成事实):** `FE/services/cesiumEngine.js:404-431` 的 `pickPointMarkerAt()`: ``` 417: export function pickPointMarkerAt(viewer, windowX, windowY) { 418: if (!viewer || !viewer.scene || !viewer.canvas || typeof viewer.scene.pick !== 'function') return null 420: const rect = viewer.canvas.getBoundingClientRect() 421: const picked = viewer.scene.pick(new Cartesian2(windowX - rect.left, windowY - rect.top)) 422: const entity = picked && picked.id 423: const properties = entity && entity.properties 424: if (!properties || !properties.pointCode) return null ``` - 用的是 **`scene.pick`(单个,最上层)**,**没有传 width/height**(走默认 3×3),**没有用 `drillPick`**(整个 `src/` grep `drillPick` → 0 命中)。 - 判据是 **实体属性里有 `pointCode`**(`:424`),注释 `:408-409` 明说「只认点位标记数据源上的实体 —— 拾到地形、影像、3D Tiles 或空白一律回 `null`」。 - 因此:**一个只有 `deviceCode` 属性、没有 `pointCode` 的设备实体,在现有这条链路上会被判为 `null`(不抢)**;反过来说,**它也无法被现有链路拾到** —— 要支持设备拾取,就得扩充这条判据或另开一个入口(票 06 的范围)。 - 重叠时的行为是确定的:`scene.pick` 只回最上层那一个(`:44928-44930`),**同像素下的另一个拾不到**;要区分就必须换 `drillPick`。这是「同一监测点下多台设备坐标重叠」时必须先裁决的事实(对应本图 `## Not yet specified` 第 1 条)。 - 一个相关开关:`Scene#pickTranslucentDepth`(`ENG`/d.ts 中 `pickPosition` 的文档 `CES:45033`「Set `Scene#pickTranslucentDepth` to `true` to include the depth of translucent primitives; otherwise, this essentially picks through translucent primitives」)。该文档挂在 `pickPosition` 上,**不要外推到 `pick`**;但半透明特效与拾取的关系需要票 02/06 专门处理。 **仓库现状补充:** `src/` grep `BillboardCollection|PointPrimitiveCollection|LabelCollection` → **0 命中**。点位目前**只用** Entity + `billboard` graphic(`FE/services/cesiumEngine.js:319-362` 的 `syncPointMarkers()`,`:335-357` 的 `source.entities.add({...})`;数据源常量 `:267 POINT_MARKER_SOURCE`,`pointMarkerSource()` `:286-301`)。DOM 手势层在 `FE/services/pointPick.js`(`:12 CLICK_MAX_DRAG_PX = 4`、`:20 isClickGesture`、`:35 attachPointPick`、`:52 onPick`),只做「位移 ≤ 4px 算点选」判定并输出窗口坐标,不 preventDefault、不抢 pointer capture。接线在 `FE/services/mapService.js:380`(`attachPointPick(record.host, ({x,y}) => pickPointAt(x,y))`)与 `:796-806`(`:799` 调 `pickPointMarkerAt`)。 ### 4.4 能否贴地(`heightReference`) **支持矩阵(随包 d.ts,行号可复现):** | 目标 | `heightReference` | 证据 | | --- | --- | --- | | `Entity.billboard` → `BillboardGraphics` | ✅ | option `CES:19517`、属性 `:19617`、option 文档 `:19494` | | `Entity.label` → `LabelGraphics` | ✅ | option `:23196`、属性 `:23314` | | `Entity.point` → `PointGraphics` | ✅ **但见下方陷阱** | option `:24003`、属性 `:24036` | | `BillboardCollection` 的 `Billboard` | ✅ | option `CES:27027`、属性 `:27078`、option 文档 `:27002` | | `LabelCollection` 的 `Label` | ✅ | option `:38343`、属性 `:38376` | | `PointPrimitiveCollection` 的 `PointPrimitive` | ❌ **没有** | `PointPrimitive` 全类 `CES:42667-42755`:`show`/`position`/`scaleByDistance`/`translucencyByDistance`/`pixelSize`/`color`/`outlineColor`/`outlineWidth`/`distanceDisplayCondition` `:42746`/`disableDepthTestDistance` `:42751`/`id` `:42755`/`splitDirection` —— **无 `heightReference`** | **`HeightReference` 枚举** `CES:36770`(文档 `:36767-36769`「Represents the position relative to the terrain.」):`NONE = 0` `:36774`、`CLAMP_TO_GROUND = 1` `:36778`、`RELATIVE_TO_GROUND = 2` `:36782`、`CLAMP_TO_TERRAIN = 3` `:36786`、`RELATIVE_TO_TERRAIN = 4` `:36790`、`CLAMP_TO_3D_TILE = 5` `:36794`。 **实现侧交叉验证(`ENG/` 源码里 grep `heightReference` 的命中数):** `Scene/PointPrimitive.js` → **0**、`Scene/PointPrimitiveCollection.js` → **0**;而 `Scene/Billboard.js` → 19、`Scene/BillboardCollection.js` → 8、`Scene/Label.js` → 12、`Scene/LabelCollection.js` → 8。 **关键陷阱(Entity 的 `point.heightReference` 是「借 billboard 冒充」):** `ENG/DataSources/PointVisualizer.js:117-130` ``` 119: if (heightReference !== HeightReference.NONE && !defined(billboard)) { 120: if (defined(pointPrimitive)) { 121: returnPrimitive(item, entity, cluster); 122: pointPrimitive = undefined; 123: } 124: 125: billboard = cluster.getBillboard(entity); 126: billboard.id = entity; 127: billboard.image = undefined; ``` `:230 billboard.heightReference = heightReference;`;`:138-149` 在 `heightReference === NONE` 时才回落到真正的 `cluster.getPoint(entity)`。 → **想用 `PointPrimitiveCollection` 实现贴地是做不到的**(原语本身没有这个属性);Entity 的 `point` 一旦要贴地,引擎内部就把它换成一个**无图 billboard**。这个结论在随包实现源码与 `Build/CesiumUnminified/Cesium.js:197909-197927` 中一致。 **构造器要求(贴地/深度测试的前提):** `BillboardCollection` 的 `options.scene` 文档 `CES:27406`「Must be passed in for billboards that use the height reference property or will be depth tested against the globe」;`LabelCollection` 同款 `CES:38647`。`PointPrimitiveCollection` 的构造器 `CES:42805-42810` **只有 `modelMatrix` / `debugShowBoundingVolume` / `blendOption` / `show`,没有 `scene`** —— 与「不支持贴地」自洽。Entity 路径下这个 `scene` 由 `ENG/DataSources/EntityCluster.js:661-665`(`new CollectionConstructor({ scene: this._scene })`)保证。 **贴地相关的抗裁剪参数:** `BillboardCollection.coarseDepthTestDistance` `CES:27491` / `threePointDepthTestDistance` `:27503`;`LabelCollection.threePointDepthTestDistance` `CES:38737`;文档说明 `threePointDepthTestDistance` 管的是「billboard/label 的 `heightReference` 为 `CLAMP_TO_GROUND`/`CLAMP_TO_TERRAIN` 时,在相机距离内改用三点深度测试,保证任一点可见即整块可见」(`CES:27412-27413`、`:38653`)。`PointPrimitiveCollection` **没有**这两个参数。 ### 4.5 `disableDepthTestDistance` 与 `distanceDisplayCondition` 的可用性 | 目标 | `distanceDisplayCondition` | `disableDepthTestDistance` | | --- | --- | --- | | `BillboardGraphics` | ✅ option `CES:19528` / 属性 `:19691` | ✅ option `:19529` / 属性 `:19696` | | `LabelGraphics` | ✅ option `:23203` / 属性 `:23355` | ✅ option `:23204` / 属性 `:23360` | | `PointGraphics` | ✅ option `:24009` / 属性 `:24065` | ✅ option `:24010` / 属性 `:24070` | | `Billboard` | ✅ option `:27038` / 属性 `:27275` | ✅ option `:27039` / 属性 `:27282` | | `Label` | ✅ option `:38350` / 属性 `:38558` | ✅ option `:38351` / 属性 `:38564` | | `PointPrimitive` | ✅ 属性 `CES:42746` | ✅ 属性 `CES:42751` | | 集合级 | — | `BillboardCollection` 的 `coarseDepthTestDistance` `:27491` / `threePointDepthTestDistance` `:27503` | `PointPrimitive` 这两项**可用但不在 d.ts 的 `add()` option 模板里**(`add(options?: any)` `CES:42883`),实现里确实生效:`ENG/Scene/PointPrimitive.js:49-53`(`disableDepthTestDistance` 校验)、`:60-103`(`distanceDisplayCondition` 校验/克隆)、`:398-440`(两个 accessor)。 语义(`Billboard` 版,`CES:27277-27281`):「When set to `undefined` or `0`, the depth test is always applied. When set to `Number.POSITIVE_INFINITY`, the depth test is never applied.」`Label` `CES:38560-38563`、`PointPrimitive` `:42748-42751`、`BillboardGraphics` `:19693-19694` 同义。集合级 `coarseDepthTestDistance` 的文档 `CES:27482-27490` 说明它**只在 `disableDepthTestDistance` 允许深度测试时才生效**。 **仓库现状(既成事实,设备点位要与它一致或显式分歧):** `FE/services/cesiumEngine.js:335-357` 的 billboard 同时写了 ``` 344: heightReference: HeightReference.CLAMP_TO_GROUND, 346: disableDepthTestDistance: Number.POSITIVE_INFINITY, ``` (`:310-311` 注释说明动机:有 DEM,否则点位浮在山体上方或埋进去。)device 点位若要视觉一致,这两个取值就是现成的对照基准。 --- ## 5. 设备级资产清单 ### 5.1 glTF / GLB 等 3D 模型资产:**无设备级模型** 全仓 glob `**/*.{glb,gltf,fbx,obj,b3dm,i3dm,pnts,cmpt,3dtiles,ktx2,dae,drc}` 命中 45 个文件,**全部在 `public/plugins/**`**(`dist/plugins/**` 是构建镜像,`node_modules/draco3d/bunny.drc` 是依赖自带)。`src/`、`tools/`、`preview/`、`pluginMaker/`、`docs/`、`.scratch/` **零命中**;**`src/assets/` 目录不存在**(`Test-Path` = False)。 `public/` 下命中(路径 | 字节)——均为建筑/交通/星球/人体/道具: - `public/plugins/basic/htmls/model/model.gltf` | 550544 —— **扩展名骗人**:实为 GLB 二进制(头是 `glTF` magic + JSON chunk),含 `KHR_draco_mesh_compression`,generator `Khronos glTF Blender I/O v4.0.44`,节点名 `Bottom`;不是设备。 - `public/plugins/basic/materials/raytracedDiamond/diamond.glb` | 9960 - `public/plugins/digitalCity/model/{coneAnchorB 18132, spiral-middle 1460, tornado 4256}`、`digitalCity/volumetricFluidFire/demo-scene.glb` | 29448 - `public/plugins/digitalPark/innovationHubAr/model/InnovationHubAR.glb` | 3331380;`digitalPark/model/{arctic_tooltip 178284, laboratoryBuild 200616, vr_sculpture_park 155416}` - `public/plugins/earthSample/model/lowpolyPlanet/{airplane.gltf 21598, cloud.gltf 11376, planet.gltf 727753}` - `public/plugins/eCommerce/model/{box 52140, pack 81068}` - `public/plugins/floor/models/topoBase/baseModel{A,B,C,D,E,F,G,H,L}.glb` | 4132 – 95928 - `public/plugins/geokit/case-real-3/model/{chuan.glb 1806572(船), weixing.glb 360988(卫星)}` - `public/plugins/geokit/tiles/level_*_tile_*.b3dm` × 9 | 3333 – 190547 - `public/plugins/hunyuan3D/model/icegl-bm.glb` | 305548 - `public/plugins/industry4/model/lambo.glb` | 1230764(跑车) - `public/plugins/medical/model/brainStorm/brain.glb` | 408744 - `public/plugins/UIdemo/model/arrowWhite2.5d.glb` | 155056 - `public/plugins/visualArts/biineBee/models/bee.glb` | 1814280;`visualArts/model/{LeePerrySmith.glb 404976(人头), room-transformed.glb 428436}` - `public/plugins/water/model/staticWater.glb` | 58412 **唯一「设备形状」的 glTF 只以远程 URL 存在于插件 demo 代码,仓库无本地副本:** - `src/plugins/zoneFreeScene/pages/freeRefiningIndustry.vue:90,100,102,104,106,110,112,114` —— `gasTank.glb` / `oilHeater.glb` / `tower.glb` / `oilTank.glb` / `machine.glb` / `tank2.glb` / `gtUnit.glb` / `flaringDevice.glb`,全部 `https://oss.icegl.cn/p/modelServer/models/2低像素炼油/...` - `src/plugins/zoneFreeScene/pages/freeShipSea.vue:67,69,71,73,75,77` —— `灯塔B.glb` / `货轮B.glb` / `游轮A.glb` / `风电机B.glb` / `风电机A.glb` / `浮标A.glb`(同域名) 易误判项:`src/plugins/industry4/common/device.ts` + `components/device.vue` 名字里有 device,但读全文是 **UnrealBloom / LED 发光着色器 demo**,用的模型是 `lambo.glb`,不是设备资产。 ### 5.2 3D Tiles / city massing:与设备模型完全两条线 仓库内 tileset json 只有 2 个: 1. `public/plugins/geokit/tiles/tileset.json`(12913 B)—— 插件 demo 的倾斜摄影瓦片:`asset.version 1.0`、`gltfUpAxis: "Z"`、`geometricError 500.0`,配 9 个 `b3dm`。引用点:`src/plugins/geokit/pages/case-3dtiles.vue:27`、`src/plugins/simpleGIS/components/tilesCom.vue:44`、`src/plugins/simpleGIS/components/obliquePhoto.vue:41`。 2. `.scratch/cockpit-shell-panel-layout/verify/fixtures/tiles/tileset.json`(739 B)—— 走查夹具,**只有包围盒、无 content**,自述见 `:3-7`。 驾驶舱加载链(现状参考):`FE/domain/mapLayers.js:81-90`(`planTilesets()`)→ `FE/services/cesiumEngine.js:20-21`(import `Cesium3DTileset` / `Cesium3DTileStyle`)、`:204-223`(`applyTilesets()`,`:214 await Cesium3DTileset.fromUrl(...)`)、`:180-197`(`readTilesetDiagnostics()`)、`:235-255`(`applyViewModeLayers()` 用 `Cesium3DTileStyle` 调 alpha);入口注释 `FE/services/mapService.js:110` 与 `:896`。 **票 34 的 city massing 资产本体不在仓库**,是 repo 外生成物:生成器 `tools/city-massing/build-town.mjs`(`:19-20` 产物 `/{town.glb,tileset.json,meta.json}`;`:25-27` `GIS = 'D:/GIS/GIS'`、`DATA_DIR`;`:484` 写 `town.glb`;`:503-517` 写 `tileset.json`);回验 `tools/city-massing/verify-town.mjs`(`:29` 读 `town.glb`、`:149-150` 读 tileset `transform`);说明 `tools/city-massing/README.md:48-60`。运行时靠 `.env.local:11`(`{"prefix":"/gis/city","dir":"D:/GIS/GIS/cityMassing"}`)与 `public/cockpit/config.json:42-47` 的 `"tilesets":[{"url":"/gis/city/tileset.json"}]` 挂载(两者都被 `.gitignore` 排除)。票据 `.scratch/cockpit-shell-panel-layout/issues/34-lightweight-city-massing.md:23,46,73`。 → **仓库里 0 个 city massing 资产文件**,且它是「示意建筑体块」,与设备级模型无关。 ### 5.3 驾驶舱的 marker / legend 图标:**运行时以内联 SVG 生成,不是 canvas 绘制** - `FE/domain/icons.js:12-57` —— 内联 SVG 字符串图标集;渲染在 `FE/components/CockpitIcon.vue:8-18`,`:31 iconFor(props.name)`。 - `FE/domain/pointStatus.js:42-59` —— `SHAPES` 内联 SVG 片段(12×12 viewBox,仅 `currentColor`);`:217-223 markerSvg()` 打成 `data:image/svg+xml;charset=utf-8,...`。 - `FE/services/cesiumEngine.js:335-347` —— 作为 `billboard.image` 挂到 Cesium 实体,`:341-342 width: 18, height: 18`。 - `FE/services/mapService.js:639-651` —— `toneColors()` 从 `.cockpit-shell` 的 `getComputedStyle` 读 `--lv1..--lv4/--neutral`(`:647 styles.getPropertyValue('--'+tone)`;`:631-637` 有同值兜底色);`:670-676` 调 `syncPointMarkers(..., (shape,color) => markerSvg(shape,color,18))`。 **修正一处常见假设:** `FE/` 下 grep `OffscreenCanvas|drawImage|toDataURL|createElement('canvas')|devicePixelRatio` 在**绘制语义上 0 命中**(`canvas` 的命中全指 Cesium 自己的 `viewer.canvas`)。所以**没有离屏 canvas 光栅化**;是纯 SVG data-URL,尺寸由 `billboard.width/height = 18` 像素决定,与 `devicePixelRatio` 无关。 ### 5.4 静态图标文件(与设备无关) - `public/logo.png` | 33245、`src/images/icon.png` | 5586、`public/plugins/simpleGIS/image/icon.png` | 1440、`public/cockpit/aerial.webp` | 517078(`FE/components/MapCanvas.vue:12` 用作中央一张图的占位底图)。 - **全仓唯一「地图点位图标」现成样例**:`public/plugins/geokit/case-real-3/icons/satellite.svg` | 1124、`ship.svg` | 600(24×24,圆底 + 白色 glyph),被 `src/plugins/geokit/pages/case-real-3.vue:51-53` 的 `` 使用。**注意这是 three.js / Tres 的 `@icegl/geokit` 组件(实现只在 `node_modules`),不是 Cesium 链路**,只可作视觉参照。 - 无关 demo 资产:`public/plugins/industry4/icon/c1..c9.webp`(车漆皮肤按钮,`src/plugins/industry4/components/su7/UIcarSkin.vue:12,25`)、`industry4/preview/deviceLight*.png`(案例封面,`src/plugins/industry4/config.js:22-23`)、earthSample 地球贴图、`operationTool/preview/*.png` 中文预览图、各插件 `preview/*.png` 封面。 - **`alarm` / `status` / `equip` 命名的设备状态图标:查不到。**(图片名匹配 `icon|marker|device|point|equip|alarm|status|legend` 与 `点|标|图例`,范围 `public/`、`public/plugins/`、`src/`。) - `FE/` 递归搜索 `*.png|*.svg|*.jpg|*.webp|*.gif|*.ico` → **0 个图片文件**。驾驶舱点位不依赖任何图片图标。 ### 5.5 `FE/domain/icons.js` 的形状清单(全 67 行) 共 **6 个**图标,每项为 `{ viewBox, inner }`,`inner` 是**内联 SVG 标记字符串**,一律 `currentColor`(无任何色值),无 canvas、无外部图片文件、无 `devicePixelRatio` 逻辑: | 键名 | 行 | viewBox | 内容 | | --- | --- | --- | --- | | `brand` | `:14` | `0 0 32 32` | 同心环 + 十字轴线 | | `layers` | `:23` | `0 0 16 16` | 图层叠片 | | `points` | `:30` | `0 0 16 16` | 双圆环 + 实心点(**通用几何标记,非设备形状**) | | `event` | `:37` | `0 0 16 16` | 三角 + 感叹号 | | `resource` | `:44` | `0 0 16 16` | 库房 / 处置力量 | | `blank` | `:51` | `0 0 48 32` | 空框 + 折线(面板占位) | 导出:`ICON_NAMES` `:59`、`isKnownIcon` `:61`、`iconFor` `:65`。尺寸由调用方给:`FE/components/CockpitIcon.vue:27` 的 `size` 默认 `'1em'`(`:11-12` 绑到 `width`/`height`)。文件头 `:4-7` 自述「先用内联 SVG 字符串而不是图片文件……正式稿由设计公司交付」。 ### 5.6 `FE/domain/pointStatus.js` 的 `SHAPES`(全 223 行) 状态词表 `POINT_STATUS` `:27-34`(6 个,原样后端字符串):`ALARM` `:28`、`WARNING` `:29`、`WORK_ORDER` `:30`、`NORMAL` `:31`、`OFFLINE` `:32`、`EMPTY` `:33`。 **形状注册表 `SHAPES` `:42-59`,8 个键(verbatim):** | 形状名 | 行 | 定义 | | --- | --- | --- | | `triangle` | `:44` | 实心三角 | | `diamond` | `:46` | 实心菱形 | | `square` | `:48` | 实心方块(`rx="1"`) | | `circle` | `:50` | 实心圆 | | `ring` | `:52` | 空心圆环(`stroke-width="2"`) | | `hollowSquare` | `:54` | 空心方块 | | `hollowDiamond` | `:56` | 空心菱形 | | `cross` | `:58` | 十字 | 头部注释 `:37-40`:内联 SVG 片段、12×12 视框、只用 `currentColor`、**不含任何颜色值**;同一份片段被地图图例与地图标记共用。 **状态 → 视觉映射 `POINT_STATUS_VISUALS` `:78-85`:** | 状态 | tone | shape | label | priority | 行 | | --- | --- | --- | --- | --- | --- | | `ALARM` | `lv1` | `triangle` | 报警 | 1 | `:79` | | `WARNING` | `lv2` | `diamond` | 预警 | 2 | `:80` | | `WORK_ORDER` | `lv3` | `square` | 工单处置 | 3 | `:81` | | `NORMAL` | `lv4` | `circle` | 正常 | 4 | `:82` | | `OFFLINE` | `neutral` | `ring` | 离线 | 5 | `:83` | | `EMPTY` | `neutral` | `hollowSquare` | 无设备 | 6 | `:84` | | (`UNKNOWN_VISUAL`) | `neutral` | `hollowDiamond` | 状态未发布 | 7 | `:88` | | (`UNASSOCIATED_VISUAL`) | `neutral` | `cross` | 无关联设备 | 8 | `:91` | 其它:`DEFAULT_SHAPE = 'circle'` `:61`、`isKnownShape` `:63`、`shapeSvg` `:68`、`visualForStatus` `:94`、`pointVisual` `:105`、`planPointMarkers` `:121`、`summarizeMarkerStatuses` `:147`、`legendVisuals` `:157`、`pointLegend` `:172`、`legendMatchesMarkers` `:184`、`markerSvg` `:217`。`planPointMarkers()` 的 marker 对象带 `longitude` `:138` / `latitude` `:139`(`:101-102` 注释解释「这里不读坐标,能不能上图是 `domain/coordinate.js` 的事」)。 **是否设备形状?——否。** 8 个全部是**通用几何标记**(实心/空心 的圆-环-方-菱形 + 三角 + 十字),**没有** box / camera / valve / pump / 设备箱 之类的设备 glyph。连「无设备」(`EMPTY`)用的也是几何形状 `hollowSquare`(`:84`)。 ### 5.7 `FE/styles/` 目录里只有 1 个文件 `FE/styles/tokens.css`(2870 B,71 行)。点位 / 设备状态语义色(全部锚在 `.cockpit-shell` `:15`): | 自定义属性 | 行 | 值 | | --- | --- | --- | | `--lv1` | `:34` | `#ff4d5e` | | `--lv2` | `:35` | `#ffa63d` | | `--lv3` | `:36` | `#4fd8ff` | | `--lv4` | `:37` | `#35e39a` | | `--neutral` | `:38` | `#6d92ae` | `:31-33` 注释明确「语义色:事件四级与点位状态共用一套调色板(ADR 0006 / 0008)」。其它 token:`--u` `:17`、`--bg-0` `:20`、`--panel-top/-bot/-line/-line-hi` `:21-24`、`--inset` `:25`、`--cyan/--cyan-d` `:28-29`、`--text/--text-2/--muted` `:41-43`、`--fs-caption/--fs-body/--fs-title/--fs-heading` `:50-53`、`--space-1..3` `:56-58`、`--column-w` `:62`、`--z-map/--z-columns/--z-overlay` `:65-67`、`--shadow` `:70`。 配套:事件四级色键 `FE/domain/statusTones.js:14`(`TONES`)、`:19-24`(`LEVEL_TAGS`);`FE/components/StatusTag.vue:86-90` 用 `var(--lv1..--neutral)` 落到 `--tag-color`。 ### 5.8 `tools/` 递归只有 3 个文件:`tools/city-massing/build-town.mjs`(23899 B)、`tools/city-massing/verify-town.mjs`(9246 B)、`tools/city-massing/README.md`(4076 B)。 - `build-town.mjs` = **城市体块**模型生成器(`:19-20` 用法与产物、`:25-27` 读本机 `D:/GIS/GIS` 的 OSM buildings/roads/waterways + `resa.json` + `terrain-grid.json`、`:387-390` 写 glTF JSON、`:484 writeGlb(...'town.glb'...)`、`:503-517` 写 `tileset.json`)。 - `verify-town.mjs` = 回验脚本(不生成资产)。 - **没有任何生成 marker 图片或下载模型的脚本**:`tools/` 下 grep `marker|icon|\.png|\.svg|\.jpg|download|fetch\(` → **0 命中**。 ### 5.9 设备级模型资产的一句话裁决 > **No —— 仓库内不存在任何现成的设备级 glTF/GLB 模型资产。** 45 个 3D 文件的文件名与(小 JSON 及二进制头的)内容全部指向建筑、车辆、船只、卫星、星球、人体、道具与倾斜摄影瓦片;唯一「设备形状」的 glTF(`gasTank` / `oilTank` / `machine` / `tower` / `flaringDevice` / `风电机` / `灯塔` / `浮标`)只以 `https://oss.icegl.cn/...` **远程 URL** 写在 `src/plugins/zoneFreeScene/pages/*.vue` 的 demo 里,本地无副本。设备点位当前**完全依赖 SVG 形状**(`FE/domain/pointStatus.js:42-59` 的 8 个几何形状)+ `FE/styles/tokens.css:34-38` 的色键,烘焙成 `data:image/svg+xml` billboard 图片(`FE/domain/pointStatus.js:217-223`、`FE/services/cesiumEngine.js:335-347`)。驾驶舱内也**没有**任何 glTF 实体加载代码(`FE/` 内 `.glb|.gltf|GLTF|GLB|Model.` 仅 1 处注释命中 `FE/domain/mapLayers.js:73`)。 --- ## 6. 明确「查不到」的清单与查证范围 | 查不到的东西 | 查过的地方与模式 | | --- | --- | | 后端任何「一次给出点位下全部设备经纬度 + 校验 + 状态」的端点 | `ADMIN/web/controller/cockpit/` 全部 20 个 `@GetMapping`;`docs/cockpit/README.md:27-61` 的端点清单;`docs/cockpit/*.html` 26 个 demo 页(重点 `monitoring-point-detail-demo.html`、`map-snapshot-demo.html`、`equipment-archive-demo.html`) | | 后端设备级相对位置 / 同井同杆 / 安装高度 / 朝向 | `pipe-network-service/` 全模块 grep `同井\|同杆\|杆塔\|relativePosition\|offsetX\|offsetY\|mountHeight\|installHeight\|poleCode\|wellCode` → 0 命中;`pipe-network-service/sql/` 全目录 grep `offset\|height\|azimuth\|heading\|elevation\|altitude\|relative` → 0 命中 | | 后端把「设备坐标 == 点位坐标」写成契约的地方 | `docs/cockpit/README.md` 全文;`CockpitMonitoringDistributionSnapshot.java:9-19` 与 `CockpitMapSnapshot.java` 的类注释;`CockpitCoordinateValidator.java` 全文(只做单点范围校验,不做两点相等校验) | | `equipment_point_rel` 之外的设备-点位关系表 | 全后端 grep `equipment_point_rel\|monitor_point` → 只命中 1 个 SQL、1 个实体、1 个 mapper、1 个 WS 监听器与 1 个测试 | | Cesium 随包 d.ts 里的 `@performance` / 「一次 draw call」文字 | `CES` 全文 51,152 行 grep `@performance` → 0 命中;grep `one draw call` / `single draw` / `batched` / `draw call` → 只命中 3D Tiles 与 Provider 的无关批处理 | | `node_modules/cesium/Build/Cesium/Cesium.d.ts` | `Test-Path` = False;全仓 glob `**/Cesium.d.ts` 只命中 `node_modules/cesium/Source/Cesium.d.ts` | | `PointPrimitive.heightReference` | `CES:42667-42755` 全类无该属性;`ENG/Scene/PointPrimitive.js` 与 `PointPrimitiveCollection.js` 各 grep `heightReference` → 0 命中;官方 `PointPrimitiveCollection` / `PointPrimitive` ref-doc 页无该项 | | `PointPrimitiveCollection` 的 `scene` / `coarseDepthTestDistance` / `threePointDepthTestDistance` | 构造器 `CES:42805-42810` 只有 4 个 option | | Cesium 官方给出的「N 个点耗时多少 ms」数值 | 随包 d.ts(`@performance` 已删)、`ENG/` 实现源码、官方 ref-doc 三处都只有复杂度与机制,**无实测数值** | | 仓库内设备级 glTF/GLB | 全仓 glob 各扩展名 + 逐名判定 + 小 JSON/GLB 头内容核查(见 §5.1) | | 仓库内 city massing 资产文件 | 全仓 glob `.glb/.gltf/.b3dm` + 文本 grep `cityMassing\|city-massing` → 只有工具/文档/票据/配置引用,产物在 repo 外 `D:/GIS/GIS/cityMassing` | | `src/assets/` 目录 | `Test-Path` = False | | `FE/` 下的图片资产 | 递归 `*.png,*.svg,*.jpg,*.webp,*.gif,*.ico` → 0 个图片文件 | | 设备/状态专用图标(alarm / status / equip 形状) | 图片名匹配 `icon\|marker\|device\|point\|equip\|alarm\|status\|legend` 与 `点\|标\|图例`,范围 `public/`、`public/plugins/`、`src/` → 只有插件 demo 图 | | 离屏 canvas 光栅化 marker | `FE/` grep `OffscreenCanvas\|drawImage\|toDataURL\|createElement('canvas')\|devicePixelRatio` → 0 命中 | | 前端对 `deviceCodes` 的任何消费 | 全前端 `src/` grep `deviceCodes` → 0 命中 | | 前端对 `BillboardCollection` / `PointPrimitiveCollection` / `LabelCollection` / `drillPick` 的任何使用 | 全前端 `src/` grep 上述四项 → 0 命中 | **已知的未验证项(诚实标注):** 后端的真实数据适配器未落地(`SYS/cockpit/point/EmptyCockpitMonitoringPointDetailAdapter.java:8-10` 是空实现),因此「设备坐标与点位坐标实际差多少」「同一点位下多台设备坐标是否真的重叠」这类**数据事实**在本工作区无法观测,只能从 schema 与契约推断(§3.3)。按 `AGENTS.md`,本次未运行任何构建/调试命令,也未做浏览器走查。