/** Facility selection uses entity category+primary key, never monitoring point codes. */ import { facilityStatusTone, entityIdentity } from './entityDetail.js' import { COORDINATE_REASON_TEXT, NO_MAP_LOCATION_TEXT } from './coordinate.js' import { filterFacilityPoints, FACILITY_DETAIL_CATEGORIES } from './facilityPoints.js' export function facilityIdentity(input) { const identity = entityIdentity(input, 'facility') return { ...identity, valid: identity.valid && identity.kind === 'facility' && FACILITY_DETAIL_CATEGORIES.includes(identity.category) } } const coordinatesOf = value => value && Number.isFinite(value.longitude) && Number.isFinite(value.latitude) ? [value.longitude, value.latitude] : null /** Resolve only current identity evidence; pipeline location is the along-line geometric midpoint of its validated polyline — never an endpoint, never invented. */ /** * 管线定位点 = 沿真实折线的**长度中点**(维护者 2026-10-30 15:57:不许再取起点/终点)。 * 每段按大圆近似计长(与 pipeDiameter 同一公式),半程落在哪一段就在段内线性插值; * 顶点不足或全长为零(重合点)时回那个真实顶点 —— 仍是几何证据,不是编造。 */ function polylineMidpoint(positions) { const points = (Array.isArray(positions) ? positions : []) .filter((point) => Array.isArray(point) && Number.isFinite(point[0]) && Number.isFinite(point[1])) if (!points.length) return null if (points.length === 1) return points[0] const rad = Math.PI / 180 const segmentLength = (a, b) => { const h = Math.sin((b[1] - a[1]) * rad / 2) ** 2 + Math.cos(a[1] * rad) * Math.cos(b[1] * rad) * Math.sin((b[0] - a[0]) * rad / 2) ** 2 return 12742017.6 * Math.asin(Math.sqrt(Math.min(1, h))) } const lengths = [] let total = 0 for (let i = 1; i < points.length; i += 1) { const length = segmentLength(points[i - 1], points[i]) lengths.push(length) total += length } if (!(total > 0)) return points[0] let walked = 0 for (let i = 0; i < lengths.length; i += 1) { const length = lengths[i] if (walked + length >= total / 2) { const t = length > 0 ? (total / 2 - walked) / length : 0 return [ points[i][0] + (points[i + 1][0] - points[i][0]) * t, points[i][1] + (points[i + 1][1] - points[i][1]) * t, ] } walked += length } return points[points.length - 1] } export function resolveFacilityPoint({ identity: input, points, detail, network } = {}) { const identity = facilityIdentity(input) if (!identity.valid) return null const row = (points || []).find(item => item.id === identity.id && item.type === identity.category) const current = detail && detail.kind === 'facility' && detail.id === identity.id && detail.category === identity.category ? detail : null const raw = current?.raw || {} let source = row || {} let position = null let placement = 'pending' let reasonText = '' let locationHint = '' if (identity.category === 'PIPE_NETWORK') { const matches = (network?.pipes || []).filter(pipe => String(pipe.sourceId ?? '') === identity.id || String(pipe.networkId ?? '') === identity.id) const byCode = matches.length ? matches : (network?.pipes || []).filter(pipe => raw.code && pipe.code === raw.code) const pipe = byCode.length === 1 ? byCode[0] : null if (pipe) { source = pipe position = polylineMidpoint(pipe.positions) placement = position ? 'drawn' : 'no-location' locationHint = position ? '定位到管段几何中点(沿管线长度取中点,非起止点)' : '' } else if (network) { const blocked = (network.blocked || []).find(item => raw.code && item.code === raw.code) placement = blocked ? 'no-location' : 'pending' reasonText = blocked?.reasonText || '当前地图尚未取到该管段的可定位几何' } } else if (current && Object.prototype.hasOwnProperty.call(raw, 'coordinates')) { position = coordinatesOf(current.coordinates) placement = position ? 'drawn' : 'no-location' reasonText = position ? '' : '设施档案未登记坐标' // 工程设施不是旧四类设施点契约;新定位必须有后端明确的可渲染结论。 if (position && identity.category === 'ENGINEERING_FACILITY' && raw.coordinateValidation?.renderable !== true) { position = null placement = raw.coordinateValidation?.renderable === false ? 'no-location' : 'pending' reasonText = placement === 'pending' ? '设施坐标尚未获得地图可渲染校验' : (COORDINATE_REASON_TEXT[raw.coordinateValidation.reason] || NO_MAP_LOCATION_TEXT) } } else if (row) { position = coordinatesOf(row) placement = row.placement reasonText = row.reasonText || '' } const status = raw.status ?? row?.status ?? '' return { key: 'facility-point|' + identity.category + '|' + identity.id, id: identity.id, type: identity.category, detailCategory: identity.category, code: String(raw.code || source.code || identity.id), name: current?.name || source.name || String(raw.name || identity.id), status: String(status), tone: facilityStatusTone(status), industry: source.industry || raw.industry || '', district: source.district ?? raw.district ?? null, placement, longitude: position?.[0] ?? null, latitude: position?.[1] ?? null, reasonText, locationHint, } } /** Selected ledger-only entities get one temporary marker, under the SAME filters; global legend counts stay unchanged. */ export function mergeFacilitySelectionPoints(base, selected, filters = {}) { if (!selected || selected.placement !== 'drawn') return base if (!filterFacilityPoints([selected], filters.types || [], filters).length) return base const list = Array.isArray(base) ? base : [] const index = list.findIndex(row => row.key === selected.key) if (index < 0) return [...list, selected] return list.map((row, i) => i === index ? selected : row) }