| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352 |
- /**
- * 管线与设施点位图层(票 24)。
- *
- * 与监测点**不是同一类对象**,因此不共用 `domain/mapSummary.js` 的口径:
- * · 监测点是「承载设备的空间业务对象」,管线是**设施**(管网本身),二者不可混算;
- * · 契约形状也不同:这里是 GeoJSON 风格的 `features[]`,不是分页快照。
- *
- * 两条不可让步的纪律(沿用票 07 / 决策票 07):
- * 1. **准��只认后端 `coordinateValidation.renderable`**,本文件不重算经纬度范围,
- * 更不纠偏、不补默认坐标 —— 与 `isRenderable()` 同一立场;
- * 2. **不得臆造安装拓扑**。管段只有坐标、无正式端节点ID,
- * 所以角/支管构件只能来自后端给的真实构件属性(燃气 pointType)。
- * 经用户确认,完全重合的同业端点可仅用于无分叉链的管径示意插值,
- * 不据此生成角/支管构件、不把候选连接关系当工程拓扑。
- *
- * 行业覆盖如实呈现:哪个行业有几何、哪个没有,由后端 `coverage` 表达,
- * 前端只转述,不替后端猜(缺数据就说缺数据,不画空图层冒充"这里没有管线")。
- */
- import { COORDINATE_REASON_TEXT, NO_MAP_LOCATION_TEXT } from './coordinate.js'
- import { DISTRICT_CODES, INDUSTRY_CODES, districtQuery, industryQuery } from './filterOptions.js'
- /** 四个正式行业码(燃气/供水/排水/窨井)——`INDUSTRY_CODES` 的**值**才是后端码。 */
- const INDUSTRY_LIST = Object.freeze(Object.values(INDUSTRY_CODES))
- const INDUSTRY_SET = new Set(INDUSTRY_LIST)
- /** 四类程序化构件(维护者 2026-12 裁定)。样条管道是管段本身,不单列构件。 */
- export const PIPE_COMPONENT = {
- ELBOW: 'elbow',
- VALVE: 'valve',
- BRANCH: 'branch',
- OTHER: 'other',
- }
- /**
- * 后端构件类型 → 前端构件类别。
- *
- * 后端 `pointType` **原样透传**(票 30 实测为英文 `valve`;数据底座注释另有中文词表),
- * 两侧都认。白名单外一律落 `other` 并**保留原文**:界面要能说清"这是个什么构件",
- * 不能因为我们没建模就把调压/计量/阴极保护一律说成"其它"以外的东西,更不能猜。
- */
- const POINT_TYPE_COMPONENT = {
- 阀门: PIPE_COMPONENT.VALVE,
- 弯头: PIPE_COMPONENT.ELBOW,
- 三通: PIPE_COMPONENT.BRANCH,
- valve: PIPE_COMPONENT.VALVE,
- elbow: PIPE_COMPONENT.ELBOW,
- tee: PIPE_COMPONENT.BRANCH,
- cross: PIPE_COMPONENT.BRANCH,
- branch: PIPE_COMPONENT.BRANCH,
- }
- export const COMPONENT_TEXT = {
- [PIPE_COMPONENT.VALVE]: '阀门',
- [PIPE_COMPONENT.ELBOW]: '角管道',
- [PIPE_COMPONENT.BRANCH]: '支管管道',
- [PIPE_COMPONENT.OTHER]: '其它构件',
- }
- /**
- * 管径(mm) → 管线线宽(px)。
- *
- * 放 domain 而不是引擎层:它是**判定**,引擎只消费结果(与本仓「判定收进 domain」一致),
- * 这样它能脱 Cesium、脱离浏览器断言。
- * 分档而非线性——真实数据的管径跨度从几十到上千毫米,线性映射会把细管压成看不见、
- * 把粗管撑成整屏宽。缺管径回最细档(画出来总比不画诚实),但**不猜**一个"典型值"。
- */
- export function pipeWidth(diameter) {
- const value = Number(diameter)
- if (!Number.isFinite(value) || value <= 0) return 2
- if (value < 300) return 2
- if (value < 600) return 3
- if (value < 1000) return 4
- return 6
- }
- /** 分类:只认白名单自有键(继承键会漏出函数,与票 22 同一条纪律)。 */
- function componentOf(pointType) {
- const raw = typeof pointType === 'string' ? pointType.trim() : ''
- if (!raw) return { kind: PIPE_COMPONENT.OTHER, label: '' }
- if (Object.prototype.hasOwnProperty.call(POINT_TYPE_COMPONENT, raw)) {
- return { kind: POINT_TYPE_COMPONENT[raw], label: raw }
- }
- // 未建模的构件类型(调压/计量/阴极保护/其它):如实显示后端原文,不伪装成已知类别。
- return { kind: PIPE_COMPONENT.OTHER, label: raw }
- }
- /**
- * **管线/构件编码的片区前缀**(维护者 2026-10-23 更正)。
- *
- * 设施编码的组成规则是「**片区 + 行业 + 时间编码**」,例:TCRQ202609080001
- * = 太常(TC) + 燃气(RQ) + 202609080001;CNWS... = 城南 + 供水。所以片区的第一手来源
- * 就在编码前两位,而不是几何 properties 里的独立字段(那里**没有**片区字段)。
- *
- * 取值与 filterOptions.DISTRICT_CODES 的**后端片区编码**同值(south=0 / north=1 / taichang=2)。
- * 现网 6766 条几何里只出现过 TC 与 CN(实测:前缀与 name 里的片区词 100% 一致,
- * 4116/4116 与 2646/2646,零反例);**CB(城北)已由维护者 2026-10-23 确认**,**现网无该前缀数据**,
- * 现网还没有 CB 前缀的数据,所以选「城北」时管线为空 —— 那是编码里没有,不是没有管线。
- * 编码规则若变,改这一张表即可。
- */
- export const PIPE_DISTRICT_PREFIX = Object.freeze({
- CN: DISTRICT_CODES.south,
- CB: DISTRICT_CODES.north,
- TC: DISTRICT_CODES.taichang,
- })
- /**
- * 设施编码 → 片区编码(0/1/2);认不出回 null =**推不出**,不猜。
- *
- * 只认「两位片区字母 + 两位行业字母」这个正式形状(前四位必须都是字母):
- * 现网另有 GD-001(燃气阀门)、YL-YJ-001(窨井)这类**另一套编码**的构件,
- * 前两位不是片区码 —— 它们回 null,由筛选层如实计数,不硬塞进某个片区。
- */
- export function pipeDistrictOf(code) {
- const raw = typeof code === 'string' ? code.trim().toUpperCase() : ''
- if (!/^[A-Z]{2}[A-Z]{2}/.test(raw)) return null
- const prefix = raw.slice(0, 2)
- return Object.hasOwn(PIPE_DISTRICT_PREFIX, prefix) ? PIPE_DISTRICT_PREFIX[prefix] : null
- }
- /** 行业码 → 规范化后端码;认不出的回空串(不拿脏值去筛、不进统计)。 */
- function industryOf(code) {
- const raw = typeof code === 'string' ? code.trim() : ''
- return INDUSTRY_SET.has(raw) ? raw : ''
- }
- /** 后端 `coordinateValidation` → 准入结论 + 不上图原因(原因文案走票 07 词表)。 */
- function admission(properties) {
- const validation = (properties && properties.coordinateValidation) || null
- if (!validation) return { renderable: false, reasonCode: '', reasonText: '坐标未校验' }
- if (validation.renderable === true) return { renderable: true, reasonCode: '', reasonText: '' }
- const code = typeof validation.reason === 'string' ? validation.reason : ''
- return { renderable: false, reasonCode: code, reasonText: COORDINATE_REASON_TEXT[code] || NO_MAP_LOCATION_TEXT }
- }
- function isFiniteNumber(value) {
- return typeof value === 'number' && Number.isFinite(value)
- }
- function diameterOf(value) {
- if (typeof value !== 'number' && typeof value !== 'string') return null
- if (typeof value === 'string' && !value.trim()) return null
- const number = Number(value)
- return Number.isFinite(number) ? number : null
- }
- /** 管段几何:保留完整路径;任一顶点结构非法就整条拦下,不跳点重连。 */
- function pipePositions(geometry) {
- const raw = geometry && geometry.type === 'LineString' && Array.isArray(geometry.coordinates) ? geometry.coordinates : []
- if (raw.length < 2) return []
- const positions = []
- for (const pair of raw) {
- if (!Array.isArray(pair) || !isFiniteNumber(pair[0]) || !isFiniteNumber(pair[1])) return []
- positions.push([pair[0], pair[1]])
- }
- return positions
- }
- /** 构件几何:点 `[lon, lat]`。 */
- function componentPosition(geometry) {
- const raw = geometry && geometry.type === 'Point' && Array.isArray(geometry.coordinates) ? geometry.coordinates : null
- return raw && isFiniteNumber(raw[0]) && isFiniteNumber(raw[1]) ? [raw[0], raw[1]] : []
- }
- /**
- * 后端几何载荷 → 领域图层。
- *
- * @param {object} payload 真实契约见 [票 30](30-untitled.md)(端点 `GET /api/cockpit/v1/infrastructure/geometry`):
- * `{ type:'FeatureCollection', generatedAt, features:[Feature], coverage:[...] }`。
- * Feature 是标准 GeoJSON:`{ type:'Feature', id, geometry, properties }`;
- * **没有 `kind` 字段**——管段/构件按 `geometry.type`(LineString/Point)判别;
- * 非法坐标的 feature `geometry` 为 **null**(原始坐标保留在 `properties.sourceCoordinates`)。
- * @returns {{pipes:Array, components:Array, blocked:Array, coverage:object, counts:object}}
- * `pipes`/`components` 只含**后端已判定可上图**的对象;被拦下的进 `blocked`,
- * 带中文原因,供界面「无地图位置」清单如实显示。
- */
- export function toPipeNetwork(payload) {
- const features = Array.isArray(payload && payload.features) ? payload.features : []
- const pipes = []
- const components = []
- const blocked = []
- for (const feature of features) {
- const properties = (feature && feature.properties) || {}
- const industry = industryOf(properties.industry)
- // 管段/构件判别:契约没有 kind 字段,按 geometry 形状判。geometry 为 null(非法坐标)
- // 时按 properties.typeCode 归类——两端的 source(pipe_network_base / gas_pipe_point / manhole_base)
- // 各自只有一种形状,typeCode 是后端给的权威标识,不猜。
- const geometryType = feature && feature.geometry && feature.geometry.type
- const isComponent = geometryType === 'Point' || (!geometryType && String(properties.typeCode || '').includes('POINT'))
- const kind = isComponent ? 'component' : 'pipe'
- const id = String(properties.code || properties.sourceId || feature.id || '')
- const verdict = admission(properties)
- // 票 67 更正(维护者 2026-10-23):片区从**设施编码前缀**推(片区+行业+时间编码),
- // 几何 properties 里没有片区字段。推不出的回 null,交给筛选层如实计数。
- const district = pipeDistrictOf(id)
- if (!verdict.renderable) {
- blocked.push({
- kind,
- code: id,
- industry,
- district,
- name: String(properties.name || properties.pointType || id || ''),
- reasonCode: verdict.reasonCode,
- reasonText: verdict.reasonText,
- })
- continue
- }
- if (kind === 'pipe') {
- const positions = pipePositions(feature.geometry)
- if (positions.length < 2) {
- blocked.push({ kind, code: id, industry, district, name: String(properties.name || id || ''), reasonCode: '', reasonText: NO_MAP_LOCATION_TEXT })
- continue
- }
- pipes.push({
- code: id,
- district,
- sourceId: String(properties.sourceId ?? ''),
- networkId: String(properties.networkId ?? ''),
- status: typeof properties.status === 'string' ? properties.status : '',
- statusLabel: typeof properties.statusLabel === 'string' ? properties.statusLabel : '',
- name: String(properties.name || ''),
- remark: typeof properties.remark === 'string' ? properties.remark : '',
- networkLevel: properties.networkLevel ?? properties.network_level ?? 0,
- industry,
- material: String(properties.material ?? ''),
- diameter: diameterOf(properties.diameter),
- length: properties.length == null || String(properties.length).trim() === '' ? null
- : isFiniteNumber(Number(properties.length)) ? Number(properties.length) : null,
- positions,
- })
- continue
- }
- const position = componentPosition(feature.geometry)
- if (!position.length) {
- blocked.push({ kind, code: id, industry, district, name: String(properties.pointType || id || ''), reasonCode: '', reasonText: NO_MAP_LOCATION_TEXT })
- continue
- }
- const component = componentOf(properties.pointType)
- components.push({
- code: id,
- district,
- sourceId: String(properties.sourceId ?? ''),
- networkId: String(properties.networkId ?? ''),
- status: typeof properties.status === 'string' ? properties.status : '',
- statusLabel: typeof properties.statusLabel === 'string' ? properties.statusLabel : '',
- industry,
- kind: component.kind,
- label: component.label,
- name: String(properties.name || component.label || ''),
- position,
- })
- }
- return { pipes, components, blocked, coverage: coverageOf(payload, { pipes, components }), counts: countByIndustry({ pipes, components }) }
- }
- /** 行业覆盖:后端给了就照抄,没给就按实际画出来的数说 —— 两种情况都不替后端圆场。
- *
- * 真实契约(票 30)的 coverage 是**三态行**:`{industry, source, geometryType, state, reason, recordCount, geometryCount, renderableCount}`,
- * `state` ∈ `AVAILABLE`/`EMPTY`/`UNAVAILABLE`。同一行业可有**两行**(管线一行、构件一行),
- * 按行归并:任一行 AVAILABLE 即视为有几何;管段/构件计数各取各自行。 */
- function coverageOf(payload, drawn) {
- const declared = Array.isArray(payload && payload.coverage) ? payload.coverage : []
- const result = {}
- for (const row of declared) {
- const industry = industryOf(row && row.industry)
- if (!industry) continue
- const state = row && row.state
- const isPointRow = (row && row.geometryType) === 'Point'
- const slot = result[industry] || (result[industry] = { geometryAvailable: false, pipeCount: 0, componentCount: 0, gaps: [] })
- if (state === 'AVAILABLE') {
- slot.geometryAvailable = true
- if (isPointRow) slot.componentCount = finiteOr(row.renderableCount, row.geometryCount)
- else slot.pipeCount = finiteOr(row.renderableCount, row.geometryCount)
- } else if (state === 'UNAVAILABLE' || state === 'EMPTY') {
- // 缺口如实记录(如供水 NO_COMPONENT_SOURCE、窨井 NO_PIPELINE_SOURCE),
- // 供界面说明"这个行业为什么没有这一类几何"——不静默、不冒充。
- slot.gaps.push({ geometryType: row.geometryType || '', state: state || '', reason: row.reason || '' })
- }
- }
- if (!Object.keys(result).length) {
- for (const industry of INDUSTRY_LIST) result[industry] = { geometryAvailable: false, pipeCount: 0, componentCount: 0, gaps: [] }
- return result
- }
- // 后端没点名的行业:如实记"无几何",不让它从覆盖表里消失(消失=看起来像没这个行业)。
- for (const industry of INDUSTRY_LIST) {
- if (!result[industry]) result[industry] = { geometryAvailable: false, pipeCount: 0, componentCount: 0, gaps: [] }
- }
- return result
- }
- function finiteOr(...values) {
- for (const value of values) {
- const num = Number(value)
- if (Number.isFinite(num)) return num
- }
- return 0
- }
- function countByIndustry({ pipes, components }) {
- const counts = {}
- for (const industry of INDUSTRY_LIST) counts[industry] = { pipes: 0, components: 0 }
- for (const pipe of pipes) if (counts[pipe.industry]) counts[pipe.industry].pipes += 1
- for (const component of components) if (counts[component.industry]) counts[component.industry].components += 1
- return counts
- }
- /**
- * 按当前筛选裁剪(图层过滤只此一处,引擎与组件都不做第二次过滤)。
- *
- * 行业:前端键 → 后端码(`all`/空 = 不限)。
- *
- * **片区(票 67 更正,维护者 2026-10-23)**:几何 properties 里没有片区字段,但设施编码
- * 的组成规则是「片区 + 行业 + 时间编码」,所以片区可以从编码前缀**推出来**
- * (`pipeDistrictOf`)—— 这不是伪造筛选,是把编码里已有的信息用起来。三条纪律:
- * 1. 推不出片区的对象(现网 4 个:燃气阀门 + 窨井构件,用的是另一套编码)在**选具体片区时排除**,
- * 并如实计数(`districtUnresolvedCount`)—— 既不硬塞进某个片区,也不静默留在结果里;
- * 2. 选「未分区」时的语义就是**推不出片区的那批对象**(与后端 district=null 同一读法);
- * 3. `districtApplied` 只在真的按片区筛了时为 true(全域时 false),界面据此措辞。
- *
- * @param {object} network 图层(`toPipeNetwork()` 的结果)
- * @param {{industry?: string, scope?: string}} [filters] 全局条件(前端键)
- */
- export function filterPipeNetwork(network, filters = {}) {
- const current = network || { pipes: [], components: [], blocked: [] }
- const industry = industryQuery(filters.industry)
- const district = districtQuery(filters.scope)
- const keepIndustry = (row) => !industry || row.industry === industry
- // district 取值:'' = 全域|0/1/2 = 具体片区|'UNASSIGNED' = 推不出片区的那批。
- const unresolvedUnderDistrict = district !== '' && district !== 'UNASSIGNED'
- const keepDistrict = (row) => {
- if (district === '') return true
- if (district === 'UNASSIGNED') return row.district === null
- return row.district === district
- }
- const keep = (row) => keepIndustry(row) && keepDistrict(row)
- const unresolvedCount = (rows) => (unresolvedUnderDistrict
- ? rows.filter((row) => keepIndustry(row) && row.district === null).length
- : 0)
- return {
- pipes: current.pipes.filter(keep),
- components: current.components.filter(keep),
- blocked: current.blocked.filter(keep),
- coverage: current.coverage,
- counts: current.counts,
- districtApplied: district !== '',
- districtUnresolvedCount: unresolvedCount(current.pipes) + unresolvedCount(current.components),
- }
- }
|