Pipe slope, cover and structure levels
64-bitLength, slope and least cover of each pipe; rim and sump of each structure; and the list of rule problems.
slope = (inv_up − inv_dn) ÷ L; crown(t) = inv_up + (inv_dn − inv_up)·t + D; cover = min over samples (ground − crown); sump = lowest invert − sump depth
Sample positions are k ÷ n along the pipe from whole numbers. Diameters in inches or millimeters are divided by the exact 12 or 1000. Comparisons with the rules allow an absolute 1e-9. The cover and invert values in the problem messages are rounded to 2 decimals for the text only.
export function analyzeNetwork(net, surface, units = 'ft') {
const problems = [], structs = new Map(), pipes = new Map();
const D = (dia) => (Number(dia) || 0) / DIA_UNIT[units === 'm' ? 'm' : 'ft'].perUnit;
const R = net.rules || {};
for (const s of net.structures) {
const g = surface ? surface.elevationAt(s.x, s.y, 1e-6) : null;
const rim = Number.isFinite(s.rim) ? s.rim : g;
structs.set(s.id, { s, rim, rimFrom: Number.isFinite(s.rim) ? 'set' : g !== null ? 'surface' : null, ins: [], outs: [], sump: null });
if (rim === null) problems.push({ id: s.id, kind: 'structure', msg: t('pipes.p.offsurf', { name: s.name }) });
}
for (const p of net.pipes) {
const a = structs.get(p.from), b = structs.get(p.to);
if (!a || !b || p.from === p.to) { problems.push({ id: p.id, kind: 'pipe', msg: t('pipes.p.unconnected', { name: p.name }) }); continue; }
a.outs.push(p.id); b.ins.push(p.id);
const len = Math.hypot(b.s.x - a.s.x, b.s.y - a.s.y);
const info = { p, len, slope: null, cover: null, coverAt: null, ground: [] };
pipes.set(p.id, info);
if (!Number.isFinite(p.invUp) || !Number.isFinite(p.invDn)) { problems.push({ id: p.id, kind: 'pipe', msg: t('pipes.p.noinv', { name: p.name }) }); continue; }
info.slope = len > 0 ? (p.invUp - p.invDn) / len : 0;
if (info.slope <= 0) problems.push({ id: p.id, kind: 'pipe', msg: t(info.slope < 0 ? 'pipes.p.uphill' : 'pipes.p.flat', { name: p.name, slope: pctTxt(info.slope) }) });
else if (R.minSlope > 0 && info.slope < R.minSlope - 1e-9) problems.push({ id: p.id, kind: 'pipe', msg: t('pipes.p.flatter', { name: p.name, slope: pctTxt(info.slope), min: pctTxt(R.minSlope) }) });
// cover along the pipe
const n = Math.max(2, Math.min(200, Math.ceil(len / 2)));
let minC = Infinity, at = null, off = 0;
for (let k = 0; k <= n; k++) {
const t = k / n, x = a.s.x + (b.s.x - a.s.x) * t, y = a.s.y + (b.s.y - a.s.y) * t;
let g = surface ? surface.elevationAt(x, y, 1e-6) : null;
if (k === 0 && a.rimFrom === 'set') g = a.rim;
if (k === n && b.rimFrom === 'set') g = b.rim;
const crown = p.invUp + (p.invDn - p.invUp) * t + D(p.dia);
info.ground.push({ t, g, crown });
if (g === null) { off++; continue; }
if (g - crown < minC) { minC = g - crown; at = t; }
}
if (Number.isFinite(minC)) { info.cover = minC; info.coverAt = at; }
if (off) problems.push({ id: p.id, kind: 'pipe', msg: t('pipes.p.partoff', { name: p.name }) });
if (Number.isFinite(minC) && R.minCover > 0 && minC < R.minCover - 1e-9) problems.push({ id: p.id, kind: 'pipe', msg: at <= 0 ? t('pipes.p.cover.up', { name: p.name, cover: minC.toFixed(2), min: R.minCover }) : at >= 1 ? t('pipes.p.cover.down', { name: p.name, cover: minC.toFixed(2), min: R.minCover }) : t('pipes.p.cover.along', { name: p.name, cover: minC.toFixed(2), at: Math.round(at * len), min: R.minCover }) });
}
// structures: drops, sumps, splits
for (const [id, st] of structs) {
const inv = [];
for (const pid of st.ins) { const p = pipes.get(pid)?.p; if (Number.isFinite(p?.invDn)) inv.push(p.invDn); }
for (const pid of st.outs) { const p = pipes.get(pid)?.p; if (Number.isFinite(p?.invUp)) inv.push(p.invUp); }
st.sump = inv.length ? Math.min(...inv) - (Number(st.s.sumpDepth) || 0) : null;
if (st.outs.length > 1) problems.push({ id, kind: 'structure', msg: t('pipes.s.outs', { name: st.s.name, n: st.outs.length }) });
for (const o of st.outs) {
const po = pipes.get(o)?.p;
if (!Number.isFinite(po?.invUp)) continue;
for (const i of st.ins) {
const pi = pipes.get(i)?.p;
if (!Number.isFinite(pi?.invDn)) continue;
if (po.invUp > pi.invDn - (R.minDrop || 0) + 1e-9) problems.push({ id, kind: 'structure', msg: po.invUp > pi.invDn ? t('pipes.s.drop.above', { name: st.s.name, out: po.name, outInv: po.invUp.toFixed(2), in: pi.name, inInv: pi.invDn.toFixed(2) }) : t('pipes.s.drop.less', { name: st.s.name, out: po.name, outInv: po.invUp.toFixed(2), drop: R.minDrop, in: pi.name, inInv: pi.invDn.toFixed(2) }) });
}
}
if (st.rim !== null) for (const pid of [...st.ins, ...st.outs]) {
const p = pipes.get(pid)?.p; if (!p) continue;
const inv = st.ins.includes(pid) ? p.invDn : p.invUp;
if (Number.isFinite(inv) && inv + D(p.dia) > st.rim + 1e-9) problems.push({ id, kind: 'structure', msg: t('pipes.s.crown', { pipe: p.name, name: st.s.name }) });
}
}
if (hasCycle(net)) problems.push({ id: null, kind: 'network', msg: t('pipes.loop') });
return { structs, pipes, problems };
}