pipeDiameter.js 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. /** Rendering-only estimates. Exact same-industry endpoints are a schematic adjacency, not surveyed topology. */
  2. const INDUSTRIES = new Set(['GAS', 'WATER_SUPPLY', 'DRAINAGE'])
  3. const positive = (value) => Number.isFinite(value) && value > 0
  4. function pathLength(positions) {
  5. let length = 0
  6. const rad = Math.PI / 180
  7. for (let i = 1; i < positions.length; i += 1) {
  8. const [lon1,lat1] = positions[i-1], [lon2,lat2] = positions[i]
  9. const h = Math.sin((lat2-lat1)*rad/2)**2 + Math.cos(lat1*rad)*Math.cos(lat2*rad)*Math.sin((lon2-lon1)*rad/2)**2
  10. length += 12742017.6 * Math.asin(Math.sqrt(Math.min(1,h)))
  11. }
  12. return length
  13. }
  14. /** Preserve reported values; fill only missing chains bounded by TWO distinct known pipes. */
  15. export function resolvePipeDiameters(pipes) {
  16. const resolved = new Map()
  17. const nodes = new Map(), edges = []
  18. for (const pipe of pipes) {
  19. if (positive(pipe.diameter)) resolved.set(pipe, { diameterMm:pipe.diameter, source:'reported', anchors:[] })
  20. if (!INDUSTRIES.has(pipe.industry) || !pipe.positions?.length) continue
  21. const key = (p) => JSON.stringify([pipe.industry,p[0],p[1]])
  22. const ends = [key(pipe.positions[0]),key(pipe.positions.at(-1))]
  23. const length = pathLength(pipe.positions)
  24. const edge = { pipe, ends, length }
  25. if (ends[0] !== ends[1] && positive(length)) edges.push(edge)
  26. // Closed/degenerate lines still occupy their endpoints and must block ambiguous propagation.
  27. for (const end of ends) {
  28. if (!nodes.has(end)) nodes.set(end,[])
  29. nodes.get(end).push(edge)
  30. }
  31. }
  32. const visited = new Set()
  33. for (const seed of edges) {
  34. if (positive(seed.pipe.diameter) || visited.has(seed)) continue
  35. const seen = new Set([seed])
  36. const walk = (node) => {
  37. let current = seed
  38. const missing = []
  39. while (nodes.get(node)?.length === 2) {
  40. const next = nodes.get(node).find(edge => edge !== current)
  41. if (positive(next.pipe.diameter)) return { missing, anchor:next }
  42. if (seen.has(next)) break
  43. seen.add(next)
  44. missing.push(next)
  45. node = next.ends[0] === node ? next.ends[1] : next.ends[0]
  46. current = next
  47. }
  48. return { missing, anchor:null }
  49. }
  50. const left = walk(seed.ends[0]), right = walk(seed.ends[1])
  51. const chain = [...left.missing.reverse(),seed,...right.missing]
  52. for (const edge of chain) visited.add(edge)
  53. if (!left.anchor || !right.anchor || left.anchor === right.anchor) continue
  54. const total = chain.reduce((sum,edge) => sum+edge.length,0)
  55. const a = left.anchor.pipe, b = right.anchor.pipe
  56. const anchors = [a,b].map(pipe => ({code:pipe.code,diameterMm:pipe.diameter})).sort((x,y)=>x.code.localeCompare(y.code))
  57. let distance = 0
  58. for (const edge of chain) {
  59. const fraction = (distance+edge.length/2)/total
  60. resolved.set(edge.pipe, { diameterMm:a.diameter+(b.diameter-a.diameter)*fraction, source:'interpolated', anchors })
  61. distance += edge.length
  62. }
  63. }
  64. return resolved
  65. }