|
|
@@ -0,0 +1,65 @@
|
|
|
+import assert from 'node:assert/strict'
|
|
|
+import { planPipeModels } from '../../../src/modules/cockpit/domain/pipeModels.js'
|
|
|
+import { toPipeNetwork } from '../../../src/modules/cockpit/domain/pipeNetwork.js'
|
|
|
+const pipe=(code,x1,x2,diameter,industry='GAS')=>({code,industry,diameter,positions:[[110+x1*.0001,28.44],[110+x2*.0001,28.44]]})
|
|
|
+const rows=[pipe('left',0,1,300),pipe('gap1',1,2,null),pipe('gap2',2,3,null),pipe('right',3,4,600)]
|
|
|
+const plan=planPipeModels({pipes:rows,components:[]})
|
|
|
+const a=plan.instances.find(row=>row.id==='pipe:gap1')
|
|
|
+const b=plan.instances.find(row=>row.id==='pipe:gap2')
|
|
|
+assert.ok(a&&b,'double-sided anchored gap produces models')
|
|
|
+assert.ok(Math.abs(a.diameterM-.375)<1e-8)
|
|
|
+assert.ok(Math.abs(b.diameterM-.525)<1e-8)
|
|
|
+assert.equal(a.diameterSource,'interpolated')
|
|
|
+assert.deepEqual(a.diameterAnchors.map(row=>row.code),['left','right'])
|
|
|
+assert.equal(rows[1].diameter,null,'do not overwrite raw business diameter')
|
|
|
+assert.deepEqual(plan.diameterSummary,{reported:2,interpolated:2,missing:0})
|
|
|
+console.log('PASS two-sided linear fill, traceable anchors, immutable raw diameter')
|
|
|
+
|
|
|
+const middle=(pipes,code)=>planPipeModels({pipes,components:[]}).instances.find(row=>row.id==='pipe:'+code)
|
|
|
+const weighted=[pipe('l',0,1,200),pipe('a',1,2,null),pipe('b',2,5,null),pipe('r',5,6,600)]
|
|
|
+assert.ok(Math.abs(middle(weighted,'a').diameterM-.25)<1e-8)
|
|
|
+assert.ok(Math.abs(middle(weighted,'b').diameterM-.45)<1e-8)
|
|
|
+const reversed=weighted.slice().reverse().map(row=>({...row,positions:row.positions.slice().reverse()}))
|
|
|
+assert.ok(Math.abs(middle(reversed,'a').diameterM-.25)<1e-8,'independent of input and path direction')
|
|
|
+assert.equal(middle([pipe('l',0,1,400),pipe('a',1,2,null),pipe('r',2,3,400)],'a').diameterM,.4)
|
|
|
+assert.equal(middle([pipe('l',0,1,400),pipe('a',1,2,null)],'a'),undefined,'one-sided does not fill')
|
|
|
+assert.equal(middle([pipe('a',0,1,null),pipe('b',1,2,null)],'a'),undefined,'no source means no invented diameter')
|
|
|
+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')
|
|
|
+assert.equal(middle([...rows,pipe('fork',2,8,800)],'gap1'),undefined,'stop before branch node')
|
|
|
+const almost=pipe('near',2,3,600);almost.positions[0][0]+=1e-10
|
|
|
+assert.equal(middle([pipe('l',0,1,300),pipe('gap',1,2,null),almost],'gap'),undefined,'do not snap nearly coincident endpoints')
|
|
|
+assert.equal(middle([pipe('l',0,1,300),pipe('bad',1,2,0),pipe('r',2,3,600)],'bad').diameterSource,'interpolated')
|
|
|
+console.log('PASS length weighting, reversed paths, equal anchors, no one-sided/branch/industry/nearby guessing')
|
|
|
+
|
|
|
+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}}}))}
|
|
|
+const adapted=toPipeNetwork(payload)
|
|
|
+assert.equal(adapted.pipes[1].diameter,null)
|
|
|
+assert.equal(adapted.pipes[0].diameter,300)
|
|
|
+assert.deepEqual(planPipeModels(adapted).diameterSummary,{reported:2,interpolated:2,missing:0})
|
|
|
+delete payload.features[0].properties.diameter
|
|
|
+assert.equal(toPipeNetwork(payload).pipes[0].diameter,null)
|
|
|
+console.log('PASS API raw diameter parsing preserves null/absent and feeds interpolation')
|
|
|
+
|
|
|
+const loop={...pipe('loop',1,1,null),positions:[[110.0001,28.44],[110.0008,28.44],[110.0001,28.44]]}
|
|
|
+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')
|
|
|
+console.log('PASS closed side loop prevents ambiguous fill')
|
|
|
+
|
|
|
+for(const value of [true,false,[],[300],{},' ','no-diameter']) {
|
|
|
+ payload.features[0].properties.diameter=value
|
|
|
+ assert.equal(toPipeNetwork(payload).pipes[0].diameter,null,'reject non numeric diameter '+JSON.stringify(value))
|
|
|
+}
|
|
|
+console.log('PASS nonnumeric JSON cannot become reported anchors')
|
|
|
+
|
|
|
+assert.ok(Math.abs(middle(weighted.slice().reverse(),'a').diameterM-.25)<1e-8)
|
|
|
+assert.ok(Math.abs(middle(weighted.map(row=>({...row,positions:row.positions.slice().reverse()})),'a').diameterM-.25)<1e-8)
|
|
|
+const closed=[pipe('only-anchor',0,1,300),pipe('ca',1,2,null),pipe('cb',2,0,null)]
|
|
|
+assert.equal(middle(closed,'ca'),undefined,'same reported pipe reached from both sides is not two anchors')
|
|
|
+assert.equal(middle(closed,'cb'),undefined)
|
|
|
+const ring=[pipe('ra',0,1,300),pipe('rg1',1,2,null),pipe('rb',2,3,600),pipe('rg2',3,0,null)]
|
|
|
+assert.equal(middle(ring,'rg1').diameterM,.45)
|
|
|
+assert.equal(middle(ring,'rg2').diameterM,.45)
|
|
|
+const multi={...pipe('poly',1,3,null),positions:[[110.0001,28.44],[110.0002,28.44],[110.0003,28.44]]}
|
|
|
+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)
|
|
|
+assert.equal(middle([pipe('l',0,1,-300),pipe('gap',1,2,null),pipe('r',2,3,600)],'gap'),undefined)
|
|
|
+assert.equal(middle([pipe('l',0,1,300,'UNKNOWN'),pipe('gap',1,2,null,'UNKNOWN'),pipe('r',2,3,600,'UNKNOWN')],'gap'),undefined)
|
|
|
+console.log('PASS independent reversals, one/two-anchor cycles, polyline lengths, negative and unknown-industry anchors')
|