Resources Numerical accuracy Calculation policy

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.

powerline/underground.js CivilCraft 0.8.1 64-bit 10 calculations Reviewed

Tests bin/test-powerline.mjs, bin/test-powerline-net.mjs, bin/test-powerline-out.mjs

Route length

64-bit

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.

  • powerline/underground.js
  • lines 81 to 94
  • CivilCraft 0.8.1
/** The trench's numbers in metres, and the cable band's thickness. */
function trenchOf(route, lib) {
  const tr = getItem(lib, route?.trench), cab = getItem(lib, route?.cable);
  const depth = fin(tr?.depth_m) ? tr.depth_m : null;
  const bedding = fin(tr?.bedding_m) ? tr.bedding_m : 0;
  const ducts = fin(tr?.ducts) ? tr.ducts : 0;
  const ductD = fin(tr?.ductDiam_mm) ? tr.ductDiam_mm / 1000 : 0;
  const cabD = fin(cab?.diameter_mm) ? cab.diameter_mm / 1000 : 0;
  const band = ducts > 0 && ductD > 0 ? ductD : cabD;
  let cover = fin(tr?.cover_m) ? tr.cover_m : null;
  if (cover == null && depth != null) cover = Math.max(0, depth - bedding - band);
  const clearance = fin(tr?.clearance_m) ? tr.clearance_m : DEFAULT_CLEAR_M;
  return { tr, cab, depth, bedding, ducts, ductD, cabD, band, cover, clearance, width: fin(tr?.width_m) ? tr.width_m : null };
}

Cable profile and cover check

64-bit

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.

  • powerline/underground.js
  • lines 167 to 171
  • CivilCraft 0.8.1
  const inv = (p.invUp + (p.invDn - p.invUp) * h.u) * um, top = inv + (fin(p.dia) ? p.dia / perUnit : 0) * um;
  const cTop = z * um - A.cover;
  sep = gap(cTop - A.band, cTop, inv, top);
}
out.push({ kind: 'pipe', id: p.id, at_ch: h.ch, sep_m: sep, need_m: needA, ok: sep == null ? null : sep >= needA - 1e-9 });

Separation from another cable route

64-bit

The clear vertical gap between two cable routes that cross, each at its own cover under the same ground.

gap between [−cover A − band A, −cover A] and [−cover B − band B, −cover B]

Both bands are measured down from the same ground, so the ground elevation does not enter and no large numbers are subtracted.

  • powerline/underground.js
  • line 183
  • CivilCraft 0.8.1
const sep = A.cover != null && B.cover != null ? gap(-A.cover - A.band, -A.cover, -B.cover - B.band, -B.cover) : null;

Cables and pipes under an overhead line

64-bit

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.

  • powerline/underground.js
  • lines 228 to 243
  • CivilCraft 0.8.1
export function joints(route, { lib, units } = {}) {
  const um = unitM(units);
  const cab = getItem(lib, route?.cable);
  const L = routeLength(route), Lm = L * um;
  const slack = 1 + (fin(route?.slack_pct) ? route.slack_pct : 0) / 100;
  const count = fin(route?.count) && route.count >= 1 ? route.count : 1;
  const run = Lm * slack;
  const drum = fin(cab?.drum_m) && cab.drum_m > 0 ? cab.drum_m : null;
  const res = { count: 0, perCable: 0, at: [], cableRun_m: run, drum_m: drum };
  if (!drum || !(run > 0)) return res;
  const per = Math.max(0, Math.ceil(run / drum - 1e-9) - 1);
  for (let k = 1; k <= per; k++) res.at.push((k * drum) / slack / um);
  res.perCable = per;
  res.count = per * count;
  return res;
}

Trench quantities

64-bit

Trench length, excavation, bedding and backfill volumes, duct length and cable length of a route.

excavation = width × depth × length; bedding = width × bedding × length; backfill = excavation − bedding − ducts × π(d ÷ 2)² × length; cable = (length × slack + 2 × depth) × count

Closed-form products in meters. The backfill is a difference floored at 0. The cable's own volume is not subtracted from the backfill.

  • powerline/underground.js
  • lines 245 to 262
  • CivilCraft 0.8.1
export function trenchQuantities(route, { lib, units, groundAt } = {}) {
  void groundAt;
  const um = unitM(units);
  const T = trenchOf(route, lib);
  const length = routeLength(route) * um;
  const slack = 1 + (fin(route?.slack_pct) ? route.slack_pct : 0) / 100;
  const count = fin(route?.count) && route.count >= 1 ? route.count : 1;
  const q = { length_m: length, trench_m: length, excavation_m3: null, bedding_m3: null, backfill_m3: null, duct_m: 0, cable_m: null, joints: joints(route, { lib, units }).count };
  if (route?.cable) q.cable_m = (length * slack + 2 * (T.depth ?? 0)) * count;
  if (T.tr && T.depth != null && T.width != null) {
    q.excavation_m3 = T.width * T.depth * length;
    q.bedding_m3 = T.width * T.bedding * length;
    q.duct_m = T.ducts * length;
    const ductVol = T.ducts * Math.PI * (T.ductD / 2) ** 2 * length;
    q.backfill_m3 = Math.max(0, q.excavation_m3 - q.bedding_m3 - ductVol);
  }
  return q;
}

In detail

Method
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.

Every policy that takes this approach