/** * 票 24 离线回归:管线/设施几何图层(不联网)。 * * 覆盖三件事: * 1. **契约**:后端 GeoJSON 形状 → 领域图层的转换([票 30](../issues/30-30.md) 交付物 1); * 2. **程序化构件分类**:样条管道(多段路径)/ 角管道(弯头)/ 阀门 / 支管管道(三通), * 以及未建模类型(调压等)**如实保留原文**而不是猜成已知类别; * 3. **坐标纪律**:未校验 / 零坐标 / 缺经度 一律**不得上图**,且必须带中文原因进「无地图位置」, * 既不被丢掉,也不被"挪一下"救回来。 * * mock 样例在本脚本里现搭,不依赖 `mocks/contractMocks.js` 的注册时机, * 但形状必须与它一致(那才是浏览器里真正会吃到的载荷)。 */ import assert from 'node:assert/strict' import { toPipeNetwork, filterPipeNetwork, pipeWidth, PIPE_COMPONENT } from '../../../src/modules/cockpit/domain/pipeNetwork.js' const valid = { state: 'VALID', renderable: true, reason: null } const feature = (overrides) => ({ kind: 'pipe', geometry: { type: 'LineString', coordinates: [[110.39, 28.44], [110.40, 28.45]] }, properties: { id: 'P-1', industry: 'GAS', coordinateValidation: valid }, ...overrides, }) let failed = 0 function check(name, action) { try { action() console.log(`PASS ${name}`) } catch (error) { failed += 1 console.error(`FAIL ${name}: ${String(error.message).split('\n')[0]}`) } } const payload = { coverage: [ { industry: 'GAS', geometryAvailable: true, pipeCount: 2, componentCount: 3 }, { industry: 'WATER_SUPPLY', geometryAvailable: true, pipeCount: 1, componentCount: 0 }, { industry: 'MANHOLE', geometryAvailable: false, pipeCount: 0, componentCount: 1 }, ], features: [ feature({}), // 多段路径 = 样条管道的输入(真实库多为两点,但契约必须支持多段) feature({ geometry: { type: 'LineString', coordinates: [[110.39, 28.44], [110.395, 28.445], [110.40, 28.44]] }, properties: { id: 'P-2', industry: 'GAS', diameter: 400, coordinateValidation: valid } }), feature({ properties: { id: 'P-3', industry: 'WATER_SUPPLY', coordinateValidation: valid } }), { kind: 'component', geometry: { type: 'Point', coordinates: [110.395, 28.445] }, properties: { id: 'C-1', pointType: '阀门', networkId: 'P-1', industry: 'GAS', coordinateValidation: valid } }, { kind: 'component', geometry: { type: 'Point', coordinates: [110.40, 28.44] }, properties: { id: 'C-2', pointType: '弯头', networkId: 'P-1', industry: 'GAS', coordinateValidation: valid } }, { kind: 'component', geometry: { type: 'Point', coordinates: [110.39, 28.44] }, properties: { id: 'C-3', pointType: '三通', networkId: 'P-2', industry: 'GAS', coordinateValidation: valid } }, { kind: 'component', geometry: { type: 'Point', coordinates: [110.41, 28.46] }, properties: { id: 'C-4', pointType: '调压', networkId: 'P-1', industry: 'GAS', coordinateValidation: valid } }, ], } const network = toPipeNetwork(payload) check('B1 管段与构件分开归类,不混为一类对象', () => { assert.equal(network.pipes.length, 3) assert.equal(network.components.length, 4) assert.ok(network.pipes.every((row) => row.kind === undefined), '管段不带构件类别') }) check('B2 多段路径原样保留(样条管道的输入不被压成两点)', () => { const spline = network.pipes.find((row) => row.code === 'P-2') assert.equal(spline.positions.length, 3) }) check('B3 四类构件按后端真实属性分类', () => { const byId = Object.fromEntries(network.components.map((row) => [row.code, row.kind])) assert.equal(byId['C-1'], PIPE_COMPONENT.VALVE) assert.equal(byId['C-2'], PIPE_COMPONENT.ELBOW) assert.equal(byId['C-3'], PIPE_COMPONENT.BRANCH) assert.equal(byId['C-4'], PIPE_COMPONENT.OTHER) }) check('B4 未建模的构件类型保留后端原文,不被猜成已知类别', () => { const regulator = network.components.find((row) => row.code === 'C-4') assert.equal(regulator.label, '调压') }) check('B5 构件记录它属于哪条管线(不做拓扑推断)', () => { const valve = network.components.find((row) => row.code === 'C-1') assert.equal(valve.networkId, 'P-1') }) check('B6 行业覆盖按后端声明转述,没点名的行业如实记"无几何"', () => { assert.equal(network.coverage.GAS.geometryAvailable, true) assert.equal(network.coverage.MANHOLE.geometryAvailable, false, '窨井无管线几何必须如实说无') assert.equal(network.coverage.DRAINAGE.geometryAvailable, false, '后端没点名排水 → 不假装有') assert.equal(network.coverage.DRAINAGE.pipeCount, 0) }) check('C1 未校验坐标不得上图,且带中文原因', () => { const blocked = toPipeNetwork({ features: [feature({ properties: { id: 'X-1', industry: 'GAS', coordinateValidation: { state: 'UNVERIFIED', renderable: false, reason: 'UNVERIFIED_COORDINATE' } } })] }) assert.equal(blocked.pipes.length, 0) assert.equal(blocked.blocked.length, 1) assert.equal(blocked.blocked[0].reasonText, '坐标未经校验') }) check('C2 零坐标不得上图', () => { const blocked = toPipeNetwork({ features: [feature({ properties: { id: 'X-2', industry: 'GAS', coordinateValidation: { state: 'INVALID', renderable: false, reason: 'ZERO_COORDINATE' } } })] }) assert.equal(blocked.pipes.length, 0) assert.equal(blocked.blocked[0].reasonText, '零坐标(经纬度均为 0)') }) check('C3 认不出的原因回「无地图位置」,不把原因码甩给界面', () => { const blocked = toPipeNetwork({ features: [feature({ properties: { id: 'X-3', industry: 'GAS', coordinateValidation: { state: 'INVALID', renderable: false, reason: 'SOMETHING_NEW' } } })] }) assert.equal(blocked.blocked[0].reasonText, '无地图位置') }) check('C4 点数不足的管段不得渲染(不补点、不截断成一点)', () => { const blocked = toPipeNetwork({ features: [feature({ geometry: { type: 'LineString', coordinates: [[110.39, 28.44]] } })] }) assert.equal(blocked.pipes.length, 0) assert.equal(blocked.blocked.length, 1) }) check('C5 缺少校验结论一律拦下("没说不"不等于"可以画")', () => { const blocked = toPipeNetwork({ features: [feature({ properties: { id: 'X-4', industry: 'GAS' } })] }) assert.equal(blocked.pipes.length, 0) assert.equal(blocked.blocked[0].reasonText, '坐标未校验') }) check('D1 行业筛选只留该行业,图层过滤只此一处', () => { const gas = filterPipeNetwork(network, { industry: 'gas' }) assert.ok(gas.pipes.every((row) => row.industry === 'GAS')) assert.ok(gas.components.every((row) => row.industry === 'GAS')) }) check('D2 全域筛选不裁剪', () => { const all = filterPipeNetwork(network, { industry: 'all' }) assert.equal(all.pipes.length, network.pipes.length) }) check('D3 片区筛选如实标记为未作用到管线(管线无片区字段)', () => { assert.equal(filterPipeNetwork(network, { industry: 'all' }).districtApplied, false) }) check('E1 管径分档映射线宽,缺管径不炸', () => { assert.equal(pipeWidth(null), 2) assert.equal(pipeWidth(200), 2) assert.equal(pipeWidth(400), 3) assert.equal(pipeWidth(800), 4) assert.equal(pipeWidth(1200), 6) }) check('E2 空载荷不抛,退化为空图层', () => { const empty = toPipeNetwork(null) assert.deepEqual(empty.pipes, []) assert.deepEqual(empty.components, []) assert.equal(empty.coverage.GAS.geometryAvailable, false) }) console.log(`T24 管线图层: ${failed ? 'FAILED' : 'passed'}`) process.exitCode = failed ? 1 : 0