| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116 |
- 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, { facilityCode, facilityKind }>`)。
- // 只有后端按**真实主键**解析出 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 }
- }
|