| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- /**
- * 面板可定位对象 → 地图定位(票 17;票 18/19 的台账与事件定位复用同一条接缝)。
- *
- * 为什么单独成一层:面板拿到的**只有点位编码**(后端这些聚合切片不返回经纬度,
- * 见 `infrastructure/emergency-support` 的 `pointCodes`;`operating-information` 的
- * `pointCodes` 实为设施编码、票 50 起不再喂本接缝),而地图上的坐标在 `mapStore`
- * 已加载的地图摘要里。所以"点面板里的对象联动地图"
- * 本质是一次**回联 + 判定**,不是"再取一次数":
- * 1. 从已加载的地图摘要里按编码找对象;
- * 2. 准入判据**只认后端 `renderable`**(票 07 的 `isRenderable()` —— 前端绝不重算经纬度);
- * 3. 找得到且可上图 → 交给地图服务移动相机(票 03 的 `setCameraFocus`)+ 打开两级详情(票 10)。
- *
- * 三种"点不动"必须分得开(否则值班人员不知道是自己点错了还是数据有问题):
- * · `missing` —— 该点位不在已加载的分页快照里(数据可能还没翻到,不是错误);
- * · `blocked` —— 点位在、但后端判定不可上图(票 07 的「无地图位置」,原因照实给);
- * · `unavailable` —— 引擎/地图实例还没就绪(稍后重试即可)。
- * 纯函数,可脱浏览器断言(见 verify/t17-infrastructure-panels.mjs)。
- */
- import { isRenderable, blockReason } from './coordinate.js'
- import { MAP_OBJECT_KIND } from './mapSummary.js'
- import { TERRAIN_LOCATE_REASON } from './pointLocate.js'
- /** 定位结果的四种情形(也是 `data-locate-result` 的取值,供走查断言)。 */
- export const LOCATE_RESULT = {
- OK: 'ok',
- MISSING: 'missing',
- BLOCKED: 'blocked',
- UNAVAILABLE: 'unavailable',
- }
- /** 给人读的失败文案(不泄漏接口词汇)。 */
- export const LOCATE_TEXT = {
- MISSING: '该点位不在已加载的一张图范围内',
- UNAVAILABLE: '地图尚未就绪,稍后重试',
- TERRAIN: '地形尚未就绪,定位待确认,请稍后重试',
- COORDINATE: '该对象坐标不可用,暂无法定位',
- }
- /**
- * 地形感知接缝(`locateMonitoringPoint`)回传的失败原因码 → 界面文案。
- * 票 53:三种失败(地图未就绪 / 坐标不可用 / 地形未就绪)各有说法,值班人员才分得清
- * 是自己点错、数据有问题还是稍后重试即可。认不出的原因码宁可回最笼统那一句,
- * 也不静默(沿用票 07「内部原因码不上界面」的立场:这里只出中文)。
- * @param {string} reason `domain/pointLocate.js` 的 `TERRAIN_LOCATE_REASON` 之一
- */
- export function locateFailureText(reason) {
- if (reason === TERRAIN_LOCATE_REASON.COORDINATE) return LOCATE_TEXT.COORDINATE
- if (reason === TERRAIN_LOCATE_REASON.MAP) return LOCATE_TEXT.UNAVAILABLE
- return LOCATE_TEXT.TERRAIN
- }
- /**
- * 从已加载的地图对象里找点位编码对应的对象。
- *
- * 监测点优先:同一个编码在两张表里理论上不会撞(编码空间独立),但真撞了要选**监测点** ——
- * CONTEXT.md 明确「一张图优先以监测点作为可点击地图对象」。
- * @param {Array} objects `mapStore.summary.objects`
- * @param {string} pointCode 点位编码
- */
- export function findMapObject(objects, pointCode) {
- const code = typeof pointCode === 'string' ? pointCode.trim() : ''
- if (!code) return null
- const list = Array.isArray(objects) ? objects : []
- const matched = list.filter((item) => item && item.code === code)
- if (!matched.length) return null
- return matched.find((item) => item.kind === MAP_OBJECT_KIND.MONITORING_POINT) || matched[0]
- }
- /**
- * 按**设备编码**回联它的宿主监测点(票 38 第二轮,维护者 2026-10-15 复验反馈第 2 条)。
- *
- * 关联设备(如真实数据里的 `21119740`)不以自己的编码上图:它是宿主点位标记的
- * `deviceCodes` 成员(设备点画在点位坐标上)。`findMapObject` 按对象编码找当然找不到——
- * 之前把它如实报成「不在已加载范围」,维护者的判语是「实际上该点位是已加载在图中的真实设备」。
- * 回联规则与 `domain/eventDetail.js` 的事件定位同一立场:先精确编码,再 `deviceCodes` 反查。
- */
- export function findDeviceOwner(objects, deviceCode) {
- const code = typeof deviceCode === 'string' ? deviceCode.trim() : ''
- if (!code) return null
- const list = Array.isArray(objects) ? objects : []
- return (
- list.find(
- (item) =>
- item &&
- item.kind === MAP_OBJECT_KIND.MONITORING_POINT &&
- Array.isArray(item.deviceCodes) &&
- item.deviceCodes.some((c) => String(c).trim() === code),
- ) || null
- )
- }
- /**
- * 规划一次定位(纯函数,不移动相机、不打开详情)。
- *
- * `mapReady` 由调用方给(引擎与相机控件是否就绪)——本层不 import service,
- * 保持「domain 不依赖 service」这条层次纪律(票 05 的 D4 守卫)。
- *
- * @param {object} params
- * @param {Array} params.objects 已加载的地图对象
- * @param {string} params.pointCode 目标点位编码
- * @param {boolean} [params.mapReady] 地图是否可以接受相机指令
- * @returns {{result: string, object: object|null, pointCode: string, reasonText: string}}
- */
- export function planLocate({ objects, pointCode, mapReady = true } = {}) {
- const code = typeof pointCode === 'string' ? pointCode.trim() : ''
- let found = findMapObject(objects, code)
- // 票 38 第二轮(维护者复验反馈第 2 条):**关联设备不以自身编码上图**(如真实数据里的
- // `21119740`「测试环境监测设备01」——它是宿主监测点 `deviceCodes` 的成员,画在宿主点位上)。
- // 直接按编码找不到对象时回联宿主监测点:定位 = 走宿主**点位的点击事件**(飞到目标 + 弹详情),
- // 而不是把「设备已加载在图中」误报成「该点位不在已加载范围」。
- // 回联规则与 `domain/eventDetail.js` 的事件定位同源(先精确编码,再 `deviceCodes` 反查)。
- let deviceCode = ''
- if (!found) {
- found = findDeviceOwner(objects, code)
- if (found) deviceCode = code
- }
- if (!found) {
- return { result: LOCATE_RESULT.MISSING, object: null, pointCode: code, deviceCode: '', reasonText: LOCATE_TEXT.MISSING }
- }
- if (!isRenderable(found)) {
- // 票 07:不上图 ≠ 不存在。原因用同一个词表(`blockReason`),界面照实说。
- return { result: LOCATE_RESULT.BLOCKED, object: found, pointCode: code, deviceCode, reasonText: blockReason(found).text }
- }
- if (!mapReady) {
- return { result: LOCATE_RESULT.UNAVAILABLE, object: found, pointCode: code, deviceCode, reasonText: LOCATE_TEXT.UNAVAILABLE }
- }
- return { result: LOCATE_RESULT.OK, object: found, pointCode: code, deviceCode, reasonText: '' }
- }
- /** 定位后的相机目标(只给地图服务,不在界面出现坐标数字)。 */
- export function locateCameraTarget(object) {
- const current = object || {}
- if (!Number.isFinite(current.longitude) || !Number.isFinite(current.latitude)) return null
- // 仅用于非监测点的旧中心换焦路径;完整点位定位由 mapService 采样真实地形。
- return { lon: current.longitude, lat: current.latitude, height: 0 }
- }
|