t24-pipe-layer.mjs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. /**
  2. * 票 24 离线回归:管线/设施几何图层(不联网)。
  3. *
  4. * 覆盖三件事:
  5. * 1. **契约**:后端 GeoJSON 形状 → 领域图层的转换([票 30](../issues/30-30.md) 交付物 1);
  6. * 2. **程序化构件分类**:样条管道(多段路径)/ 角管道(弯头)/ 阀门 / 支管管道(三通),
  7. * 以及未建模类型(调压等)**如实保留原文**而不是猜成已知类别;
  8. * 3. **坐标纪律**:未校验 / 零坐标 / 缺经度 一律**不得上图**,且必须带中文原因进「无地图位置」,
  9. * 既不被丢掉,也不被"挪一下"救回来。
  10. *
  11. * mock 样例在本脚本里现搭,不依赖 `mocks/contractMocks.js` 的注册时机,
  12. * 但形状必须与它一致(那才是浏览器里真正会吃到的载荷)。
  13. */
  14. import assert from 'node:assert/strict'
  15. import { toPipeNetwork, filterPipeNetwork, pipeWidth, PIPE_COMPONENT } from '../../../src/modules/cockpit/domain/pipeNetwork.js'
  16. const valid = { state: 'VALID', renderable: true, reason: null }
  17. const feature = (overrides) => ({
  18. kind: 'pipe',
  19. geometry: { type: 'LineString', coordinates: [[110.39, 28.44], [110.40, 28.45]] },
  20. properties: { id: 'P-1', industry: 'GAS', coordinateValidation: valid },
  21. ...overrides,
  22. })
  23. let failed = 0
  24. function check(name, action) {
  25. try {
  26. action()
  27. console.log(`PASS ${name}`)
  28. } catch (error) {
  29. failed += 1
  30. console.error(`FAIL ${name}: ${String(error.message).split('\n')[0]}`)
  31. }
  32. }
  33. const payload = {
  34. coverage: [
  35. { industry: 'GAS', geometryAvailable: true, pipeCount: 2, componentCount: 3 },
  36. { industry: 'WATER_SUPPLY', geometryAvailable: true, pipeCount: 1, componentCount: 0 },
  37. { industry: 'MANHOLE', geometryAvailable: false, pipeCount: 0, componentCount: 1 },
  38. ],
  39. features: [
  40. feature({}),
  41. // 多段路径 = 样条管道的输入(真实库多为两点,但契约必须支持多段)
  42. 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 } }),
  43. feature({ properties: { id: 'P-3', industry: 'WATER_SUPPLY', coordinateValidation: valid } }),
  44. { kind: 'component', geometry: { type: 'Point', coordinates: [110.395, 28.445] }, properties: { id: 'C-1', pointType: '阀门', networkId: 'P-1', industry: 'GAS', coordinateValidation: valid } },
  45. { kind: 'component', geometry: { type: 'Point', coordinates: [110.40, 28.44] }, properties: { id: 'C-2', pointType: '弯头', networkId: 'P-1', industry: 'GAS', coordinateValidation: valid } },
  46. { kind: 'component', geometry: { type: 'Point', coordinates: [110.39, 28.44] }, properties: { id: 'C-3', pointType: '三通', networkId: 'P-2', industry: 'GAS', coordinateValidation: valid } },
  47. { kind: 'component', geometry: { type: 'Point', coordinates: [110.41, 28.46] }, properties: { id: 'C-4', pointType: '调压', networkId: 'P-1', industry: 'GAS', coordinateValidation: valid } },
  48. ],
  49. }
  50. const network = toPipeNetwork(payload)
  51. check('B1 管段与构件分开归类,不混为一类对象', () => {
  52. assert.equal(network.pipes.length, 3)
  53. assert.equal(network.components.length, 4)
  54. assert.ok(network.pipes.every((row) => row.kind === undefined), '管段不带构件类别')
  55. })
  56. check('B2 多段路径原样保留(样条管道的输入不被压成两点)', () => {
  57. const spline = network.pipes.find((row) => row.code === 'P-2')
  58. assert.equal(spline.positions.length, 3)
  59. })
  60. check('B3 四类构件按后端真实属性分类', () => {
  61. const byId = Object.fromEntries(network.components.map((row) => [row.code, row.kind]))
  62. assert.equal(byId['C-1'], PIPE_COMPONENT.VALVE)
  63. assert.equal(byId['C-2'], PIPE_COMPONENT.ELBOW)
  64. assert.equal(byId['C-3'], PIPE_COMPONENT.BRANCH)
  65. assert.equal(byId['C-4'], PIPE_COMPONENT.OTHER)
  66. })
  67. check('B4 未建模的构件类型保留后端原文,不被猜成已知类别', () => {
  68. const regulator = network.components.find((row) => row.code === 'C-4')
  69. assert.equal(regulator.label, '调压')
  70. })
  71. check('B5 构件记录它属于哪条管线(不做拓扑推断)', () => {
  72. const valve = network.components.find((row) => row.code === 'C-1')
  73. assert.equal(valve.networkId, 'P-1')
  74. })
  75. check('B6 行业覆盖按后端声明转述,没点名的行业如实记"无几何"', () => {
  76. assert.equal(network.coverage.GAS.geometryAvailable, true)
  77. assert.equal(network.coverage.MANHOLE.geometryAvailable, false, '窨井无管线几何必须如实说无')
  78. assert.equal(network.coverage.DRAINAGE.geometryAvailable, false, '后端没点名排水 → 不假装有')
  79. assert.equal(network.coverage.DRAINAGE.pipeCount, 0)
  80. })
  81. check('C1 未校验坐标不得上图,且带中文原因', () => {
  82. const blocked = toPipeNetwork({ features: [feature({ properties: { id: 'X-1', industry: 'GAS', coordinateValidation: { state: 'UNVERIFIED', renderable: false, reason: 'UNVERIFIED_COORDINATE' } } })] })
  83. assert.equal(blocked.pipes.length, 0)
  84. assert.equal(blocked.blocked.length, 1)
  85. assert.equal(blocked.blocked[0].reasonText, '坐标未经校验')
  86. })
  87. check('C2 零坐标不得上图', () => {
  88. const blocked = toPipeNetwork({ features: [feature({ properties: { id: 'X-2', industry: 'GAS', coordinateValidation: { state: 'INVALID', renderable: false, reason: 'ZERO_COORDINATE' } } })] })
  89. assert.equal(blocked.pipes.length, 0)
  90. assert.equal(blocked.blocked[0].reasonText, '零坐标(经纬度均为 0)')
  91. })
  92. check('C3 认不出的原因回「无地图位置」,不把原因码甩给界面', () => {
  93. const blocked = toPipeNetwork({ features: [feature({ properties: { id: 'X-3', industry: 'GAS', coordinateValidation: { state: 'INVALID', renderable: false, reason: 'SOMETHING_NEW' } } })] })
  94. assert.equal(blocked.blocked[0].reasonText, '无地图位置')
  95. })
  96. check('C4 点数不足的管段不得渲染(不补点、不截断成一点)', () => {
  97. const blocked = toPipeNetwork({ features: [feature({ geometry: { type: 'LineString', coordinates: [[110.39, 28.44]] } })] })
  98. assert.equal(blocked.pipes.length, 0)
  99. assert.equal(blocked.blocked.length, 1)
  100. })
  101. check('C5 缺少校验结论一律拦下("没说不"不等于"可以画")', () => {
  102. const blocked = toPipeNetwork({ features: [feature({ properties: { id: 'X-4', industry: 'GAS' } })] })
  103. assert.equal(blocked.pipes.length, 0)
  104. assert.equal(blocked.blocked[0].reasonText, '坐标未校验')
  105. })
  106. check('D1 行业筛选只留该行业,图层过滤只此一处', () => {
  107. const gas = filterPipeNetwork(network, { industry: 'gas' })
  108. assert.ok(gas.pipes.every((row) => row.industry === 'GAS'))
  109. assert.ok(gas.components.every((row) => row.industry === 'GAS'))
  110. })
  111. check('D2 全域筛选不裁剪', () => {
  112. const all = filterPipeNetwork(network, { industry: 'all' })
  113. assert.equal(all.pipes.length, network.pipes.length)
  114. })
  115. check('D3 片区筛选如实标记为未作用到管线(管线无片区字段)', () => {
  116. assert.equal(filterPipeNetwork(network, { industry: 'all' }).districtApplied, false)
  117. })
  118. check('E1 管径分档映射线宽,缺管径不炸', () => {
  119. assert.equal(pipeWidth(null), 2)
  120. assert.equal(pipeWidth(200), 2)
  121. assert.equal(pipeWidth(400), 3)
  122. assert.equal(pipeWidth(800), 4)
  123. assert.equal(pipeWidth(1200), 6)
  124. })
  125. check('E2 空载荷不抛,退化为空图层', () => {
  126. const empty = toPipeNetwork(null)
  127. assert.deepEqual(empty.pipes, [])
  128. assert.deepEqual(empty.components, [])
  129. assert.equal(empty.coverage.GAS.geometryAvailable, false)
  130. })
  131. console.log(`T24 管线图层: ${failed ? 'FAILED' : 'passed'}`)
  132. process.exitCode = failed ? 1 : 0