/** * 面板可定位对象 → 地图定位(票 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 } }