Underground cable routes: cover, crossings, joints and trench quantities
Works out a cable route's length, its cover under the surface, its vertical separation from pipes and other routes it crosses, the joints it needs and the trench quantities.
How the precision is kept
64-bit numbers with closed-form geometry: lengths are sums of segment lengths, crossings are one division each, and separations are differences of levels in meters. The cable follows the ground at a fixed cover, so the profile samples only place the cable; they do not change the cover. Tolerances are small absolute values.
Calculations under this policy
10 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
The plan length of a cable route in project units.
L = Σ √(Δx² + Δy²)
Math.hypot per segment, added in order. Points with a non-finite coordinate are left out first.
powerline/underground.js
lines 57 to 62
CivilCraft 0.8.1
export function routeLength(route) {
const P = goodPts(route);
let L = 0;
for (let i = 1; i < P.length; i++) L += Math.hypot(P[i].x - P[i - 1].x, P[i].y - P[i - 1].y);
return L;
}
Cover and cable band of a trench
64-bit
The cover over the cable and the thickness of the band the cable or its ducts occupy, in meters.
cover = trench cover, or max(0, depth − bedding − band); band = duct diameter, or cable diameter
Millimeters are divided by 1000. When the trench type gives no cover it is a difference of three typed dimensions, floored at 0. The crossing clearance defaults to 0.2 m.
The ground and the top of the cable along the route, and whether the cover meets the minimum.
cable = ground − cover ÷ m per unit; samples every L ÷ 100 by default
The cable is the ground less a constant cover, so the least cover is the trench's cover wherever the ground is known. Sample chainages are j × step by multiplication, capped at 5000 samples, with the last one set to the exact length. The check passes within 1e-9 m.
powerline/underground.js
lines 96 to 119
CivilCraft 0.8.1
export function routeProfile(route, { groundAt, lib, units, step, rules } = {}) {
const um = unitM(units);
const P = goodPts(route);
const out = { samples: [], minCover_m: null, need_m: null, ok: null, noGround: 0 };
if (P.length < 2) return out;
const cum = cumul(P), L = cum[cum.length - 1];
const T = trenchOf(route, lib);
const st = fin(step) && step > 0 ? step : L / 100;
const n = L > 0 ? Math.min(5000, Math.max(1, Math.ceil(L / st - 1e-9))) : 1;
const g = typeof groundAt === 'function' ? groundAt : () => null;
for (let j = 0; j <= n; j++) {
const ch = j === n ? L : Math.min(L, j * st);
const q = pointAt(P, cum, ch);
let z = null;
try { z = g(q.x, q.y); } catch { z = null; }
if (!fin(z)) { z = null; out.noGround++; }
const cable = z != null && T.cover != null ? z - T.cover / um : null;
out.samples.push({ ch, ground: z, cable });
if (z != null && T.cover != null && (out.minCover_m == null || T.cover < out.minCover_m)) out.minCover_m = T.cover;
}
out.need_m = fin(rules?.coverMin_m) ? rules.coverMin_m : fin(T.tr?.cover_m) ? T.tr.cover_m : null;
out.ok = out.minCover_m == null || out.need_m == null ? null : out.minCover_m >= out.need_m - 1e-9;
return out;
}
Crossing of two plan segments
64-bit
Where two straight segments cross in plan, as a position along each.
t = ((p − a) × e) ÷ (d × e); u = ((p − a) × d) ÷ (d × e)
Parallel when the cross product is below 1e-12 in absolute value, a threshold in squared project units that does not scale with the segment lengths. t and u are accepted from −1e-12 to 1 + 1e-12 and are not clamped.
powerline/underground.js
lines 121 to 128
CivilCraft 0.8.1
/** Plan intersections of segment a→b with segment p→q → { t (on a→b), u (on p→q) } or null. */
function segHit(a, b, p, q) {
const dx = b.x - a.x, dy = b.y - a.y, ex = q.x - p.x, ey = q.y - p.y;
const den = dx * ey - dy * ex;
if (Math.abs(den) < 1e-12) return null;
const t = ((p.x - a.x) * ey - (p.y - a.y) * ex) / den, u = ((p.x - a.x) * dy - (p.y - a.y) * dx) / den;
return t >= -1e-12 && t <= 1 + 1e-12 && u >= -1e-12 && u <= 1 + 1e-12 ? { t, u } : null;
}
Crossings along the route
64-bit
Every place a segment crosses the route, with its chainage.
ch = chainage of the vertex before + t × segment length
A crossing found twice at a shared vertex is kept once when the two chainages differ by less than 1e-9, absolute.
powerline/underground.js
lines 129 to 139
CivilCraft 0.8.1
/** Every crossing of polyline P (the route) with segment p→q → [{ ch, x, y, u }]. */
function hitsOnRoute(P, cum, p, q) {
const out = [];
for (let i = 1; i < P.length; i++) {
const h = segHit(P[i - 1], P[i], p, q);
if (!h) continue;
out.push({ ch: cum[i - 1] + h.t * (cum[i] - cum[i - 1]), x: P[i - 1].x + (P[i].x - P[i - 1].x) * h.t, y: P[i - 1].y + (P[i].y - P[i - 1].y) * h.t, u: h.u });
}
// a crossing exactly on a shared vertex is found twice
return out.filter((h, k) => !out.slice(0, k).some((o) => Math.abs(o.ch - h.ch) < 1e-9));
}
Separation from a pipe at a crossing
64-bit
The clear vertical gap between the cable and a pipe where they cross.
invert = (inv up + (inv down − inv up)·u) × m per unit; pipe top = invert + diameter; cable top = ground − cover; gap = max(cable bottom − pipe top, invert − cable top)
The pipe diameter is divided by 1000 for millimeters or 12 for inches, both exact, then converted to meters. The gap is a difference of elevations; for elevations of a few hundred meters and a gap of decimeters that subtraction costs about three digits. The check passes within 1e-9 m.
The cable routes and pipes an overhead line crosses in plan, with the line's chainage and the level of the top of each.
cable top = ground − cover ÷ m per unit; pipe top = invert at the crossing + diameter
Crossings come from the same segment intersection as the route crossings. The pipe's invert is interpolated linearly along the pipe and its diameter divided by 1000 or 12; the levels are left in project units.
powerline/underground.js
lines 196 to 226
CivilCraft 0.8.1
export function lineUnderground(line, state, network, { groundAt, lib, units } = {}) {
const out = [];
try {
const um = unitM(units);
const S = goodPts({ pts: line?.supports });
if (S.length < 2) return out;
const cum = cumul(S);
const g = (x, y) => { try { const z = typeof groundAt === 'function' ? groundAt(x, y) : null; return fin(z) ? z : null; } catch { return null; } };
for (const r of Array.isArray(state?.routes) ? state.routes : []) {
const Q = goodPts(r);
const A = trenchOf(r, lib);
for (let j = 1; j < Q.length; j++) for (const h of hitsOnRoute(S, cum, Q[j - 1], Q[j])) {
const z = g(h.x, h.y);
out.push({ kind: 'route', id: r.id, name: r.name || r.id, ch: h.ch, z: z != null && A.cover != null ? z - A.cover / um : null });
}
}
const perUnit = units === 'm' ? 1000 : 12;
for (const nw of Array.isArray(network) ? network : network ? [network] : []) {
const St = new Map((Array.isArray(nw?.structures) ? nw.structures : []).filter((s) => s && fin(s.x) && fin(s.y)).map((s) => [s.id, s]));
for (const p of Array.isArray(nw?.pipes) ? nw.pipes : []) {
const a = St.get(p?.from), b = St.get(p?.to);
if (!a || !b) continue;
for (const h of hitsOnRoute(S, cum, a, b)) {
const z = fin(p.invUp) && fin(p.invDn) ? p.invUp + (p.invDn - p.invUp) * h.u + (fin(p.dia) ? p.dia / perUnit : 0) : null;
out.push({ kind: 'pipe', id: p.id, name: p.name || p.id, ch: h.ch, z });
}
}
}
} catch { /* odd input: what was found so far */ }
return out.sort((a, b) => a.ch - b.ch);
}
Joints from the drum length
64-bit
How many joints each cable needs and where along the route they fall.
run = length × (1 + slack %); joints per cable = ⌈run ÷ drum⌉ − 1; at = k × drum ÷ (1 + slack %)
The ceiling subtracts 1e-9 first, so a run that is an exact number of drums does not add a joint. Joint positions are computed by multiplication, not by repeated addition.
Plan crossings by segment intersection; a pipe's invert at the crossing by linear interpolation between its ends; the clear gap between two vertical bands; joints = ⌈cable run ÷ drum length⌉ − 1 per cable; trench volumes as width × depth × length.
Number types
64-bit numbers.
Tolerances
Parallel test |cross product| below 1e-12, absolute; crossing parameters accepted within 1e-12 outside 0 to 1; crossings within 1e-9 of each other in chainage merged; cover and separation checks pass within 1e-9 m; the joint count ceiling subtracts 1e-9.
Limits
Lengths are plan lengths. Trench volumes are bank volumes with the trench type's fixed depth. The pipe's wall thickness is not counted, as the file header says. Project feet are converted with 0.3048.
Approach: 64-bit, direct
Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.