t32-diameter-fill.mjs 5.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. import assert from 'node:assert/strict'
  2. import { planPipeModels } from '../../../src/modules/cockpit/domain/pipeModels.js'
  3. import { toPipeNetwork } from '../../../src/modules/cockpit/domain/pipeNetwork.js'
  4. const pipe=(code,x1,x2,diameter,industry='GAS')=>({code,industry,diameter,positions:[[110+x1*.0001,28.44],[110+x2*.0001,28.44]]})
  5. const rows=[pipe('left',0,1,300),pipe('gap1',1,2,null),pipe('gap2',2,3,null),pipe('right',3,4,600)]
  6. const plan=planPipeModels({pipes:rows,components:[]})
  7. const a=plan.instances.find(row=>row.id==='pipe:gap1')
  8. const b=plan.instances.find(row=>row.id==='pipe:gap2')
  9. assert.ok(a&&b,'double-sided anchored gap produces models')
  10. assert.ok(Math.abs(a.diameterM-.375)<1e-8)
  11. assert.ok(Math.abs(b.diameterM-.525)<1e-8)
  12. assert.equal(a.diameterSource,'interpolated')
  13. assert.deepEqual(a.diameterAnchors.map(row=>row.code),['left','right'])
  14. assert.equal(rows[1].diameter,null,'do not overwrite raw business diameter')
  15. assert.deepEqual(plan.diameterSummary,{reported:2,interpolated:2,missing:0})
  16. console.log('PASS two-sided linear fill, traceable anchors, immutable raw diameter')
  17. const middle=(pipes,code)=>planPipeModels({pipes,components:[]}).instances.find(row=>row.id==='pipe:'+code&&row.diameterSource!=='visual-default')
  18. const weighted=[pipe('l',0,1,200),pipe('a',1,2,null),pipe('b',2,5,null),pipe('r',5,6,600)]
  19. assert.ok(Math.abs(middle(weighted,'a').diameterM-.25)<1e-8)
  20. assert.ok(Math.abs(middle(weighted,'b').diameterM-.45)<1e-8)
  21. const reversed=weighted.slice().reverse().map(row=>({...row,positions:row.positions.slice().reverse()}))
  22. assert.ok(Math.abs(middle(reversed,'a').diameterM-.25)<1e-8,'independent of input and path direction')
  23. assert.equal(middle([pipe('l',0,1,400),pipe('a',1,2,null),pipe('r',2,3,400)],'a').diameterM,.4)
  24. assert.equal(middle([pipe('l',0,1,400),pipe('a',1,2,null)],'a'),undefined,'one-sided does not fill')
  25. assert.equal(middle([pipe('a',0,1,null),pipe('b',1,2,null)],'a'),undefined,'no source means no invented diameter')
  26. assert.equal(middle([pipe('l',0,1,400),pipe('a',1,2,null),pipe('r',2,3,600,'WATER_SUPPLY')],'a'),undefined,'do not cross industries')
  27. assert.equal(middle([...rows,pipe('fork',2,8,800)],'gap1'),undefined,'stop before branch node')
  28. const almost=pipe('near',2,3,600);almost.positions[0][0]+=1e-10
  29. assert.equal(middle([pipe('l',0,1,300),pipe('gap',1,2,null),almost],'gap'),undefined,'do not snap nearly coincident endpoints')
  30. assert.equal(middle([pipe('l',0,1,300),pipe('bad',1,2,0),pipe('r',2,3,600)],'bad').diameterSource,'interpolated')
  31. console.log('PASS length weighting, reversed paths, equal anchors, no one-sided/branch/industry/nearby guessing')
  32. const payload={features:rows.map((row,i)=>({geometry:{type:'LineString',coordinates:row.positions},properties:{code:row.code,industry:'GAS',diameter:i===0?'300.0':row.diameter,coordinateValidation:{renderable:true}}}))}
  33. const adapted=toPipeNetwork(payload)
  34. assert.equal(adapted.pipes[1].diameter,null)
  35. assert.equal(adapted.pipes[0].diameter,300)
  36. assert.deepEqual(planPipeModels(adapted).diameterSummary,{reported:2,interpolated:2,missing:0})
  37. delete payload.features[0].properties.diameter
  38. assert.equal(toPipeNetwork(payload).pipes[0].diameter,null)
  39. console.log('PASS API raw diameter parsing preserves null/absent and feeds interpolation')
  40. const loop={...pipe('loop',1,1,null),positions:[[110.0001,28.44],[110.0008,28.44],[110.0001,28.44]]}
  41. assert.equal(middle([pipe('l',0,1,300),pipe('gap',1,2,null),pipe('r',2,3,600),loop],'gap'),undefined,'closed branch still counts toward junction degree')
  42. console.log('PASS closed side loop prevents ambiguous fill')
  43. for(const value of [true,false,[],[300],{},' ','no-diameter']) {
  44. payload.features[0].properties.diameter=value
  45. assert.equal(toPipeNetwork(payload).pipes[0].diameter,null,'reject non numeric diameter '+JSON.stringify(value))
  46. }
  47. console.log('PASS nonnumeric JSON cannot become reported anchors')
  48. assert.ok(Math.abs(middle(weighted.slice().reverse(),'a').diameterM-.25)<1e-8)
  49. assert.ok(Math.abs(middle(weighted.map(row=>({...row,positions:row.positions.slice().reverse()})),'a').diameterM-.25)<1e-8)
  50. const closed=[pipe('only-anchor',0,1,300),pipe('ca',1,2,null),pipe('cb',2,0,null)]
  51. assert.equal(middle(closed,'ca'),undefined,'same reported pipe reached from both sides is not two anchors')
  52. assert.equal(middle(closed,'cb'),undefined)
  53. const ring=[pipe('ra',0,1,300),pipe('rg1',1,2,null),pipe('rb',2,3,600),pipe('rg2',3,0,null)]
  54. assert.equal(middle(ring,'rg1').diameterM,.45)
  55. assert.equal(middle(ring,'rg2').diameterM,.45)
  56. const multi={...pipe('poly',1,3,null),positions:[[110.0001,28.44],[110.0002,28.44],[110.0003,28.44]]}
  57. assert.ok(Math.abs(middle([pipe('l',0,1,200),multi,pipe('b',3,4,null),pipe('r',4,5,500)],'poly').diameterM-.3)<1e-8)
  58. assert.equal(middle([pipe('l',0,1,-300),pipe('gap',1,2,null),pipe('r',2,3,600)],'gap'),undefined)
  59. assert.equal(middle([pipe('l',0,1,300,'UNKNOWN'),pipe('gap',1,2,null,'UNKNOWN'),pipe('r',2,3,600,'UNKNOWN')],'gap'),undefined)
  60. console.log('PASS independent reversals, one/two-anchor cycles, polyline lengths, negative and unknown-industry anchors')