mapLocate.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. /**
  2. * 面板可定位对象 → 地图定位(票 17;票 18/19 的台账与事件定位复用同一条接缝)。
  3. *
  4. * 为什么单独成一层:面板拿到的**只有点位编码**(后端这些聚合切片不返回经纬度,
  5. * 见 `infrastructure/emergency-support` 的 `pointCodes`;`operating-information` 的
  6. * `pointCodes` 实为设施编码、票 50 起不再喂本接缝),而地图上的坐标在 `mapStore`
  7. * 已加载的地图摘要里。所以"点面板里的对象联动地图"
  8. * 本质是一次**回联 + 判定**,不是"再取一次数":
  9. * 1. 从已加载的地图摘要里按编码找对象;
  10. * 2. 准入判据**只认后端 `renderable`**(票 07 的 `isRenderable()` —— 前端绝不重算经纬度);
  11. * 3. 找得到且可上图 → 交给地图服务移动相机(票 03 的 `setCameraFocus`)+ 打开两级详情(票 10)。
  12. *
  13. * 三种"点不动"必须分得开(否则值班人员不知道是自己点错了还是数据有问题):
  14. * · `missing` —— 该点位不在已加载的分页快照里(数据可能还没翻到,不是错误);
  15. * · `blocked` —— 点位在、但后端判定不可上图(票 07 的「无地图位置」,原因照实给);
  16. * · `unavailable` —— 引擎/地图实例还没就绪(稍后重试即可)。
  17. * 纯函数,可脱浏览器断言(见 verify/t17-infrastructure-panels.mjs)。
  18. */
  19. import { isRenderable, blockReason } from './coordinate.js'
  20. import { MAP_OBJECT_KIND } from './mapSummary.js'
  21. import { TERRAIN_LOCATE_REASON } from './pointLocate.js'
  22. /** 定位结果的四种情形(也是 `data-locate-result` 的取值,供走查断言)。 */
  23. export const LOCATE_RESULT = {
  24. OK: 'ok',
  25. MISSING: 'missing',
  26. BLOCKED: 'blocked',
  27. UNAVAILABLE: 'unavailable',
  28. }
  29. /** 给人读的失败文案(不泄漏接口词汇)。 */
  30. export const LOCATE_TEXT = {
  31. MISSING: '该点位不在已加载的一张图范围内',
  32. UNAVAILABLE: '地图尚未就绪,稍后重试',
  33. TERRAIN: '地形尚未就绪,定位待确认,请稍后重试',
  34. COORDINATE: '该对象坐标不可用,暂无法定位',
  35. }
  36. /**
  37. * 地形感知接缝(`locateMonitoringPoint`)回传的失败原因码 → 界面文案。
  38. * 票 53:三种失败(地图未就绪 / 坐标不可用 / 地形未就绪)各有说法,值班人员才分得清
  39. * 是自己点错、数据有问题还是稍后重试即可。认不出的原因码宁可回最笼统那一句,
  40. * 也不静默(沿用票 07「内部原因码不上界面」的立场:这里只出中文)。
  41. * @param {string} reason `domain/pointLocate.js` 的 `TERRAIN_LOCATE_REASON` 之一
  42. */
  43. export function locateFailureText(reason) {
  44. if (reason === TERRAIN_LOCATE_REASON.COORDINATE) return LOCATE_TEXT.COORDINATE
  45. if (reason === TERRAIN_LOCATE_REASON.MAP) return LOCATE_TEXT.UNAVAILABLE
  46. return LOCATE_TEXT.TERRAIN
  47. }
  48. /**
  49. * 从已加载的地图对象里找点位编码对应的对象。
  50. *
  51. * 监测点优先:同一个编码在两张表里理论上不会撞(编码空间独立),但真撞了要选**监测点** ——
  52. * CONTEXT.md 明确「一张图优先以监测点作为可点击地图对象」。
  53. * @param {Array} objects `mapStore.summary.objects`
  54. * @param {string} pointCode 点位编码
  55. */
  56. export function findMapObject(objects, pointCode) {
  57. const code = typeof pointCode === 'string' ? pointCode.trim() : ''
  58. if (!code) return null
  59. const list = Array.isArray(objects) ? objects : []
  60. const matched = list.filter((item) => item && item.code === code)
  61. if (!matched.length) return null
  62. return matched.find((item) => item.kind === MAP_OBJECT_KIND.MONITORING_POINT) || matched[0]
  63. }
  64. /**
  65. * 按**设备编码**回联它的宿主监测点(票 38 第二轮,维护者 2026-10-15 复验反馈第 2 条)。
  66. *
  67. * 关联设备(如真实数据里的 `21119740`)不以自己的编码上图:它是宿主点位标记的
  68. * `deviceCodes` 成员(设备点画在点位坐标上)。`findMapObject` 按对象编码找当然找不到——
  69. * 之前把它如实报成「不在已加载范围」,维护者的判语是「实际上该点位是已加载在图中的真实设备」。
  70. * 回联规则与 `domain/eventDetail.js` 的事件定位同一立场:先精确编码,再 `deviceCodes` 反查。
  71. */
  72. export function findDeviceOwner(objects, deviceCode) {
  73. const code = typeof deviceCode === 'string' ? deviceCode.trim() : ''
  74. if (!code) return null
  75. const list = Array.isArray(objects) ? objects : []
  76. return (
  77. list.find(
  78. (item) =>
  79. item &&
  80. item.kind === MAP_OBJECT_KIND.MONITORING_POINT &&
  81. Array.isArray(item.deviceCodes) &&
  82. item.deviceCodes.some((c) => String(c).trim() === code),
  83. ) || null
  84. )
  85. }
  86. /**
  87. * 规划一次定位(纯函数,不移动相机、不打开详情)。
  88. *
  89. * `mapReady` 由调用方给(引擎与相机控件是否就绪)——本层不 import service,
  90. * 保持「domain 不依赖 service」这条层次纪律(票 05 的 D4 守卫)。
  91. *
  92. * @param {object} params
  93. * @param {Array} params.objects 已加载的地图对象
  94. * @param {string} params.pointCode 目标点位编码
  95. * @param {boolean} [params.mapReady] 地图是否可以接受相机指令
  96. * @returns {{result: string, object: object|null, pointCode: string, reasonText: string}}
  97. */
  98. export function planLocate({ objects, pointCode, mapReady = true } = {}) {
  99. const code = typeof pointCode === 'string' ? pointCode.trim() : ''
  100. let found = findMapObject(objects, code)
  101. // 票 38 第二轮(维护者复验反馈第 2 条):**关联设备不以自身编码上图**(如真实数据里的
  102. // `21119740`「测试环境监测设备01」——它是宿主监测点 `deviceCodes` 的成员,画在宿主点位上)。
  103. // 直接按编码找不到对象时回联宿主监测点:定位 = 走宿主**点位的点击事件**(飞到目标 + 弹详情),
  104. // 而不是把「设备已加载在图中」误报成「该点位不在已加载范围」。
  105. // 回联规则与 `domain/eventDetail.js` 的事件定位同源(先精确编码,再 `deviceCodes` 反查)。
  106. let deviceCode = ''
  107. if (!found) {
  108. found = findDeviceOwner(objects, code)
  109. if (found) deviceCode = code
  110. }
  111. if (!found) {
  112. return { result: LOCATE_RESULT.MISSING, object: null, pointCode: code, deviceCode: '', reasonText: LOCATE_TEXT.MISSING }
  113. }
  114. if (!isRenderable(found)) {
  115. // 票 07:不上图 ≠ 不存在。原因用同一个词表(`blockReason`),界面照实说。
  116. return { result: LOCATE_RESULT.BLOCKED, object: found, pointCode: code, deviceCode, reasonText: blockReason(found).text }
  117. }
  118. if (!mapReady) {
  119. return { result: LOCATE_RESULT.UNAVAILABLE, object: found, pointCode: code, deviceCode, reasonText: LOCATE_TEXT.UNAVAILABLE }
  120. }
  121. return { result: LOCATE_RESULT.OK, object: found, pointCode: code, deviceCode, reasonText: '' }
  122. }
  123. /** 定位后的相机目标(只给地图服务,不在界面出现坐标数字)。 */
  124. export function locateCameraTarget(object) {
  125. const current = object || {}
  126. if (!Number.isFinite(current.longitude) || !Number.isFinite(current.latitude)) return null
  127. // 仅用于非监测点的旧中心换焦路径;完整点位定位由 mapService 采样真实地形。
  128. return { lon: current.longitude, lat: current.latitude, height: 0 }
  129. }