import { createNeighborhood } from './pipeNeighborhood.js' import { pipeColorKey, pipeVirtualDepthM } from './pipeVisual.js' import { isAssociatedDeviceVisible } from './modeLayerPlan.js' function admitted(row) { return row.coordinate?.renderable === true && Number.isFinite(row.longitude) && Number.isFinite(row.latitude) } /** Nearest centreline position is a display slot only, not a facility relationship. */ function nearest(point, path) { const factor = Math.cos(point[1] * Math.PI / 180) * 111320 let best = null for (let i = 1; i < path.length; i += 1) { const a = [(path[i - 1][0] - point[0]) * factor, (path[i - 1][1] - point[1]) * 111320] const d = [(path[i][0] - path[i - 1][0]) * factor, (path[i][1] - path[i - 1][1]) * 111320] const length = d[0] ** 2 + d[1] ** 2 const t = length ? Math.max(0, Math.min(1, -(a[0] * d[0] + a[1] * d[1]) / length)) : 0 const distance = Math.hypot(a[0] + t * d[0], a[1] + t * d[1]) if (!best || distance < best.distance) best = { distance, position: path[i - 1].map((v, axis) => v + t * (path[i][axis] - v)) } } return best } function deviceSlot(code) { let hash = 2166136261 for (const character of String(code)) hash = Math.imul(hash ^ character.charCodeAt(0), 16777619) hash = Math.imul(hash ^ (hash >>> 16), 0x85ebca6b) return (hash ^ (hash >>> 13)) >>> 0 } export function planEnhancementDevices(plan, rows = [], { mode = 'overview', abnormalDevices = null, facilityRelations = null } = {}) { if (!plan.visible) return { devices: [], deviceList: [] } const corridor = createNeighborhood(plan.object.path, plan.radiusM || 50) const pipes = (plan.objects || [plan.object]).filter((o) => o.sourceKind === 'pipe') // 真实设施关联(`Map`)。 // 只有后端按**真实主键**解析出 deviceCode 的行会进来;当前库数据下通常为空, // 届时按下一条的「同业邻近示意」降级 —— 不按名字/编码猜关联。 const relations = facilityRelations instanceof Map ? facilityRelations : new Map() const devices = [] const deviceList = [] const seen = new Set() for (const row of [...rows].sort((a, b) => String(a.code).localeCompare(String(b.code)))) { if (!row.code || seen.has(row.code)) continue const abnormal = Array.isArray(abnormalDevices) ? abnormalDevices.find((d) => d.code === row.code) : null const detailMode = mode === 'infrastructure' ? 'overview' : mode if (!isAssociatedDeviceVisible(detailMode, abnormal?.status || row.status) || (mode === 'warningResponse' && !abnormal)) continue seen.add(row.code) // 关联优先级:真实设施关系(后端按主键解析)> 载荷自带 networkId(旧路径,当前数据里设备侧不存在)> 同业邻近示意。 // 只比较真实标识符,绝不拿展示编码去比 networkId。 const relation = relations.get(row.code) || null const linked = (relation && pipes.find((p) => p.networkId && p.networkId === relation.facilityCode)) || (row.networkId ? pipes.find((p) => p.networkId && p.networkId === row.networkId) : null) const position = [row.longitude, row.latitude] const valid = admitted(row) if (!linked && valid && !corridor.contains(position)) continue // 维护者 2026-10-22:无坐标/未准入且无真实关联的设备在详情视图中**直接隐藏**。 // 此前按「仅清单」保留,会把一堆无位置设备塞进设备列表、挤占有位置设备的可读性; // 详情视图是空间视图,无法定位的设备在这里没有意义(主图台账仍可查)。 if (!valid && !linked) continue const candidates = linked ? [linked] : pipes.filter((p) => p.industry === row.industry) let chosen = null for (const pipe of candidates) { const at = valid ? nearest(position, pipe.path) : { distance: 0, position: pipe.path[0] } if (at && (!chosen || at.distance < chosen.distance)) chosen = { ...at, pipe } } const slotKey = chosen ? `${chosen.pipe.code}|${chosen.pipe.piece ?? 0}` : row.code const slot = deviceSlot(`${slotKey}|${row.code}`) const colorKey = chosen?.pipe.colorKey || pipeColorKey(row) const device = { ...row, position: chosen?.position || position, colorKey, depthM: chosen?.pipe.depthM ?? pipeVirtualDepthM(colorKey), pipeCode: chosen?.pipe.code || '', pipePiece: chosen?.pipe.piece ?? 0, slot, pipeDisplayRadiusM: (chosen?.pipe.displayDiameterM || 0) / 2, placement: linked ? 'linked' : chosen ? 'nearby' : 'standalone', placementText: linked ? (relation ? '真实设施关联 · 位置示意' : '关联设备 · 示意安装') : chosen ? '邻近设备 · 示意安装' : '设备原位置 · 无同业管段', relationType: relation?.relationType || '', retained: row.retained === true } devices.push(device) deviceList.push(device) } const groups = new Map() for (const device of devices.filter((d) => d.pipeCode)) { const key = `${device.pipeCode}|${device.pipePiece}` const list = groups.get(key) || [] list.push(device) groups.set(key, list) } for (const list of groups.values()) { const path = pipes.find((p) => p.code === list[0].pipeCode && (p.piece ?? 0) === list[0].pipePiece).path 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])) const total = lengths.reduce((a, b) => a + b, 0) list.forEach((device) => { let remaining = total * (0.08 + 0.84 * device.slot / 0xffffffff) for (let i = 0; i < lengths.length; i += 1) { if (remaining <= lengths[i] || i === lengths.length - 1) { const t = lengths[i] ? Math.min(1, remaining / lengths[i]) : 0 device.position = path[i].map((v, axis) => v + (path[i + 1][axis] - v) * t) break } remaining -= lengths[i] } }) } for (const device of devices) { device.mountHeightM = device.pipeDisplayRadiusM || 0 for (const pipe of pipes) { const radius = (pipe.displayDiameterM || 0) / 2 const at = nearest(device.position, pipe.path) if (!at || !radius || at.distance > radius + 0.6) continue device.mountHeightM = Math.max(device.mountHeightM, device.depthM - pipe.depthM + radius) } } return { devices, deviceList } }