enhancementDevices.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import { createNeighborhood } from './pipeNeighborhood.js'
  2. import { pipeColorKey, pipeVirtualDepthM } from './pipeVisual.js'
  3. import { isAssociatedDeviceVisible } from './modeLayerPlan.js'
  4. function admitted(row) {
  5. return row.coordinate?.renderable === true && Number.isFinite(row.longitude) && Number.isFinite(row.latitude)
  6. }
  7. /** Nearest centreline position is a display slot only, not a facility relationship. */
  8. function nearest(point, path) {
  9. const factor = Math.cos(point[1] * Math.PI / 180) * 111320
  10. let best = null
  11. for (let i = 1; i < path.length; i += 1) {
  12. const a = [(path[i - 1][0] - point[0]) * factor, (path[i - 1][1] - point[1]) * 111320]
  13. const d = [(path[i][0] - path[i - 1][0]) * factor, (path[i][1] - path[i - 1][1]) * 111320]
  14. const length = d[0] ** 2 + d[1] ** 2
  15. const t = length ? Math.max(0, Math.min(1, -(a[0] * d[0] + a[1] * d[1]) / length)) : 0
  16. const distance = Math.hypot(a[0] + t * d[0], a[1] + t * d[1])
  17. if (!best || distance < best.distance) best = { distance, position: path[i - 1].map((v, axis) => v + t * (path[i][axis] - v)) }
  18. }
  19. return best
  20. }
  21. function deviceSlot(code) {
  22. let hash = 2166136261
  23. for (const character of String(code)) hash = Math.imul(hash ^ character.charCodeAt(0), 16777619)
  24. hash = Math.imul(hash ^ (hash >>> 16), 0x85ebca6b)
  25. return (hash ^ (hash >>> 13)) >>> 0
  26. }
  27. export function planEnhancementDevices(plan, rows = [], { mode = 'overview', abnormalDevices = null, facilityRelations = null } = {}) {
  28. if (!plan.visible) return { devices: [], deviceList: [] }
  29. const corridor = createNeighborhood(plan.object.path, plan.radiusM || 50)
  30. const pipes = (plan.objects || [plan.object]).filter((o) => o.sourceKind === 'pipe')
  31. // 真实设施关联(`Map<deviceCode, { facilityCode, facilityKind }>`)。
  32. // 只有后端按**真实主键**解析出 deviceCode 的行会进来;当前库数据下通常为空,
  33. // 届时按下一条的「同业邻近示意」降级 —— 不按名字/编码猜关联。
  34. const relations = facilityRelations instanceof Map ? facilityRelations : new Map()
  35. const devices = []
  36. const deviceList = []
  37. const seen = new Set()
  38. for (const row of [...rows].sort((a, b) => String(a.code).localeCompare(String(b.code)))) {
  39. if (!row.code || seen.has(row.code)) continue
  40. const abnormal = Array.isArray(abnormalDevices) ? abnormalDevices.find((d) => d.code === row.code) : null
  41. const detailMode = mode === 'infrastructure' ? 'overview' : mode
  42. if (!isAssociatedDeviceVisible(detailMode, abnormal?.status || row.status)
  43. || (mode === 'warningResponse' && !abnormal)) continue
  44. seen.add(row.code)
  45. // 关联优先级:真实设施关系(后端按主键解析)> 载荷自带 networkId(旧路径,当前数据里设备侧不存在)> 同业邻近示意。
  46. // 只比较真实标识符,绝不拿展示编码去比 networkId。
  47. const relation = relations.get(row.code) || null
  48. const linked = (relation && pipes.find((p) => p.networkId && p.networkId === relation.facilityCode))
  49. || (row.networkId ? pipes.find((p) => p.networkId && p.networkId === row.networkId) : null)
  50. const position = [row.longitude, row.latitude]
  51. const valid = admitted(row)
  52. if (!linked && valid && !corridor.contains(position)) continue
  53. // 维护者 2026-10-22:无坐标/未准入且无真实关联的设备在详情视图中**直接隐藏**。
  54. // 此前按「仅清单」保留,会把一堆无位置设备塞进设备列表、挤占有位置设备的可读性;
  55. // 详情视图是空间视图,无法定位的设备在这里没有意义(主图台账仍可查)。
  56. if (!valid && !linked) continue
  57. const candidates = linked ? [linked] : pipes.filter((p) => p.industry === row.industry)
  58. let chosen = null
  59. for (const pipe of candidates) {
  60. const at = valid ? nearest(position, pipe.path) : { distance: 0, position: pipe.path[0] }
  61. if (at && (!chosen || at.distance < chosen.distance)) chosen = { ...at, pipe }
  62. }
  63. const slotKey = chosen ? `${chosen.pipe.code}|${chosen.pipe.piece ?? 0}` : row.code
  64. const slot = deviceSlot(`${slotKey}|${row.code}`)
  65. const colorKey = chosen?.pipe.colorKey || pipeColorKey(row)
  66. const device = { ...row, position: chosen?.position || position, colorKey,
  67. depthM: chosen?.pipe.depthM ?? pipeVirtualDepthM(colorKey), pipeCode: chosen?.pipe.code || '',
  68. pipePiece: chosen?.pipe.piece ?? 0, slot,
  69. pipeDisplayRadiusM: (chosen?.pipe.displayDiameterM || 0) / 2,
  70. placement: linked ? 'linked' : chosen ? 'nearby' : 'standalone',
  71. placementText: linked
  72. ? (relation ? '真实设施关联 · 位置示意' : '关联设备 · 示意安装')
  73. : chosen ? '邻近设备 · 示意安装' : '设备原位置 · 无同业管段',
  74. relationType: relation?.relationType || '',
  75. retained: row.retained === true }
  76. devices.push(device)
  77. deviceList.push(device)
  78. }
  79. const groups = new Map()
  80. for (const device of devices.filter((d) => d.pipeCode)) {
  81. const key = `${device.pipeCode}|${device.pipePiece}`
  82. const list = groups.get(key) || []
  83. list.push(device)
  84. groups.set(key, list)
  85. }
  86. for (const list of groups.values()) {
  87. const path = pipes.find((p) => p.code === list[0].pipeCode && (p.piece ?? 0) === list[0].pipePiece).path
  88. const lengths = path.slice(1).map((p, i) => Math.hypot((p[0] - path[i][0]) * Math.cos(p[1] * Math.PI / 180), p[1] - path[i][1]))
  89. const total = lengths.reduce((a, b) => a + b, 0)
  90. list.forEach((device) => {
  91. let remaining = total * (0.08 + 0.84 * device.slot / 0xffffffff)
  92. for (let i = 0; i < lengths.length; i += 1) {
  93. if (remaining <= lengths[i] || i === lengths.length - 1) {
  94. const t = lengths[i] ? Math.min(1, remaining / lengths[i]) : 0
  95. device.position = path[i].map((v, axis) => v + (path[i + 1][axis] - v) * t)
  96. break
  97. }
  98. remaining -= lengths[i]
  99. }
  100. })
  101. }
  102. for (const device of devices) {
  103. device.mountHeightM = device.pipeDisplayRadiusM || 0
  104. for (const pipe of pipes) {
  105. const radius = (pipe.displayDiameterM || 0) / 2
  106. const at = nearest(device.position, pipe.path)
  107. if (!at || !radius || at.distance > radius + 0.6) continue
  108. device.mountHeightM = Math.max(device.mountHeightM, device.depthM - pipe.depthM + radius)
  109. }
  110. }
  111. return { devices, deviceList }
  112. }