Overhead line checks: clearances, support loads and sizing
Checks an overhead line span by span and support by support: clearance to the ground and to obstacles, phase spacing, insulator swing, the loads on each support, its size, foundation and fittings.
How the precision is kept
Everything is converted to meters with 0.3048 per project foot and computed in 64-bit. The clearance along a span is the smallest of samples taken about every 2 m, so a ground high point between samples is not seen. Loads follow closed formulas given in the file header, with rule-of-thumb defaults for embedment and foundation size. Plants from the landscape arrive converted with the survey foot and are converted back with the international foot.
Open item. Use one foot for the landscape obstacles and the line (landscape-obstacles.js divides by 1200 ÷ 3937 and checks.js multiplies by 0.3048).
Calculations under this policy
17 calculations in 4 source files. 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.
turn = |atan2 of the span ahead − atan2 of the span behind|, wrapped to 0 … 180°
A difference of two Math.atan2 bearings, wrapped by adding or subtracting 2π, then multiplied by 180 ÷ Math.PI. For a nearly straight line the two bearings are nearly equal and the small turn is their difference.
powerline/model.js
lines 162 to 172
CivilCraft 0.8.1
export function turnAngles(line) {
const P = line.supports;
return P.map((p, i) => {
if (i === 0 || i === P.length - 1) return 0;
const a = Math.atan2(p.y - P[i - 1].y, p.x - P[i - 1].x), b = Math.atan2(P[i + 1].y - p.y, P[i + 1].x - p.x);
let d = b - a;
while (d > Math.PI) d -= 2 * Math.PI;
while (d < -Math.PI) d += 2 * Math.PI;
return Math.abs(d) * 180 / Math.PI;
});
}
Suspension, angle or dead-end
64-bit
Decides each support's role from the line angle when none is set.
dead-end when turn ≥ dead-end angle; angle when turn > suspension angle
The turn is compared with the rule set's two angles with no tolerance, so a turn equal to a limit within rounding can fall either way.
powerline/model.js
lines 174 to 183
CivilCraft 0.8.1
export function supportRoles(line, rules) {
const ang = turnAngles(line), n = line.supports.length;
return line.supports.map((p, i) => {
if (p.role && p.role !== 'auto') return p.role;
if (i === 0 || i === n - 1) return 'deadend';
if (ang[i] >= (rules?.angleDeadEnd_deg ?? 60)) return 'deadend';
if (ang[i] > (rules?.angleSusp_deg ?? 5)) return 'angle';
return 'suspension';
});
}
Chainage of the supports
64-bit
The cumulative horizontal distance along the line at each support, in project units.
ch = Σ √(Δx² + Δy²)
Math.hypot per span, added in order along the line.
powerline/model.js
lines 192 to 195
CivilCraft 0.8.1
export function chainage(line) {
let c = 0;
return line.supports.map((p, i) => { if (i) c += Math.hypot(p.x - line.supports[i - 1].x, p.y - line.supports[i - 1].y); return c; });
}
The elevation at which the conductor is attached at each support, in meters.
attach = ground × m per unit + height above ground − fitting drop − insulator length; embedment = height ÷ 10 + 0.5 when the item gives none
A height typed on the support is taken as the height above ground; otherwise the item's length less its embedment. The ground elevation is multiplied by 0.3048 in a feet project. The insulator length is subtracted on suspension supports only.
powerline/checks.js
lines 69 to 74
CivilCraft 0.8.1
const H_m = p.h != null ? p.h * um : item?.height_m ?? null;
const embed = item?.embed_m ?? (H_m != null ? H_m / 10 + 0.5 : 0);
const above = H_m != null ? H_m - (p.h != null ? 0 : embed) : null; // a height typed on the support is the height above ground
const insL = roles[i] === 'suspension' && ins ? ins.length_m || 0 : 0;
const attach = g != null && above != null ? g * um + above - fitDrop - insL : null;
const massN = eq.reduce((t, e) => t + (e.mass_kg || 0) * G, 0);
Span length and level difference
64-bit
The horizontal length of each span and the difference in attachment height between its ends.
L = √(Δx² + Δy²) × m per unit; dz = attach right − attach left
Math.hypot in project units, then one multiplication to meters. The level difference is taken as 0 when either attachment is unknown.
The lowest height of the conductor above the ground in each load case, and where it is.
clearance = min over samples of (attach + y(x) − ground); samples = max(20, ⌈L ÷ 2⌉) + 1
A sampled minimum: the ground and the catenary are compared at evenly spaced points, about every 2 m or closer, so a ridge between two samples is missed. The check passes within 1e-6 m of the required clearance.
powerline/checks.js
lines 109 to 127
CivilCraft 0.8.1
const N = Math.max(20, Math.ceil(span.L_m / 2));
for (const lc of cases) {
const cat = S.solve.cases[lc.id].spans[k];
let min = Infinity, at = 0;
for (let j = 0; j <= N; j++) {
const t = j / N, x = t * span.L_m;
const gz = groundAt(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
if (gz == null) continue;
const cz = sup[i].attach + cat.yAt(x);
const cl = cz - gz * um;
if (cl < min) { min = cl; at = x; }
}
if (min < Infinity) {
span.clear[lc.id] = { min_m: min, at_m: at, need_m: lineNeed, ok: min >= lineNeed - 1e-6 };
const margin = min - lineNeed;
if (!span.worst || margin < span.worst.margin_m) span.worst = { caseId: lc.id, margin_m: margin };
}
}
if (span.worst && span.worst.margin_m < -1e-6) issues.push({ code: 'clearance', sev: 'error', span: i, vals: { span: i + 1, caseId: span.worst.caseId, have: lineNeed + span.worst.margin_m, need: lineNeed } });
Clearance over an obstacle
64-bit
The height of the conductor above each obstacle the span crosses in plan, in the worst load case.
clear = min over cases of (attach + y(t × L)) − top; top = the obstacle's own top, or ground + height
The conductor is evaluated exactly at the plan crossing from the catenary, not sampled. Only the vertical distance at the crossing is measured. The check passes within 1e-6 m.
powerline/checks.js
lines 131 to 140
CivilCraft 0.8.1
const hits = crossings(a, b, o.pts);
let bad = null; // one problem per obstacle and span: its lowest clearance
for (const t of hits) {
const x = t * span.L_m, gz = o.ground != null ? o.ground : groundAt(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
if (gz == null && o.top == null) continue;
const top = (o.top != null ? o.top : gz + (o.h || 0)) * um, need = clearNeed(o.clearance);
let worst = Infinity, wc = null;
for (const lc of cases) { const cz = sup[i].attach + S.solve.cases[lc.id].spans[k].yAt(x); if (cz - top < worst) { worst = cz - top; wc = lc.id; } }
const hit = { id: o.id, name: o.name, kind: o.kind || null, source: o.source || null, top: top / um, at_m: x, clear_m: worst, need_m: need, caseId: wc, ok: worst >= need - 1e-6 };
span.obstacles.push(hit);
Phase spacing needed
64-bit
The spacing between phases a span needs, from its largest sag.
need = k·√(sag + insulator length) + U ÷ 150 (U in kV)
One 64-bit expression; the square root's argument is floored at 0. The file header calls it a common form with k from the rule set. The check passes within 1e-6 m.
powerline/checks.js
lines 149 to 151
CivilCraft 0.8.1
const f = S.solve.cases[hot].spans[k].sag, l = Math.max(sup[i].insL, sup[i + 1].insL);
const need = (R.spacingK || 0) * Math.sqrt(Math.max(0, f + l)) + kV / 150;
span.spacing = { need_m: need, have_m: have, ok: have >= need - 1e-6 };
Loads on a support
64-bit
The transverse, longitudinal and vertical load on a support in each load case and the equivalent load at its nominal point.
The wind span is half the sum of the two span lengths and the weight span runs between the low points either side, which can be negative. At a dead-end the longitudinal load is the difference of the two horizontal tensions, a subtraction of similar numbers when both sides are strung alike. A stay takes the resultant ÷ sin of its angle.
powerline/checks.js
lines 172 to 189
CivilCraft 0.8.1
const windSpan = ((catL?.L ?? 0) + (catR?.L ?? 0)) / 2;
// weight span: from the left span's lowest point to the right span's lowest point
const wL = catL ? catL.L - catL.x0 : 0, wR = catR ? catR.x0 : 0;
const weightSpan = wL + wR;
const turn = (angles[i] || 0) * DEG;
const T = nCond * (L0.wh * windSpan + (HL + HR) * Math.sin(turn / 2));
let Lg;
if (s.role === 'deadend' || !left || !right) Lg = nCond * Math.abs(HL * Math.cos(turn / 2) - HR * Math.cos(turn / 2));
else Lg = lc.id === R.asymCase ? nCond * (unb.get(i) || 0) : 0;
const V = nCond * L0.wv * weightSpan + s.massN;
const Fh = Math.hypot(T, Lg);
const nomH = s.item && s.above != null ? s.above - (s.item.loadDrop_m ?? 0.25) : null;
const attachH = s.attach != null && s.groundZ != null ? s.attach - s.groundZ * um : null;
// a stay carries the horizontal resultant at its angle (45° from the vertical unless the item says): the support keeps none of it,
// and takes the stay's downward pull as extra vertical load
const stayN = s.stay ? Fh / Math.sin(((s.stay.angle_deg ?? 45) * Math.PI) / 180) : 0;
const Feq = s.stay ? 0 : nomH > 0 && attachH != null ? Fh * attachH / nomH : Fh;
out.loads[lc.id] = { T, L: Lg, V: V + (s.stay ? stayN * Math.cos(((s.stay.angle_deg ?? 45) * Math.PI) / 180) : 0), Feq, windSpan, weightSpan, stay: stayN };
Insulator swing angle
64-bit
The angle a suspension insulator swings to under the wind case.
swing = atan2(T per conductor, V per conductor + insulator weight ÷ 2)
Math.atan2 divided by Math.PI ÷ 180. The vertical term is floored at 1e-9 N, so with uplift the angle comes out close to 90°.
The dimensions are rules of thumb written into the code, not a foundation design. The volume is the full cylinder; the pole's own volume is not taken out.
The positions along a span, from 0 to 1, where it crosses an obstacle's outline or passes a single point.
t = ((p − a) × e) ÷ (d × e); u = ((p − a) × d) ÷ (d × e)
Segments are treated as parallel when the cross product is below 1e-12 in absolute value; that threshold is in squared project units and does not scale with the lengths. t and u must lie within 0 and 1 with no tolerance, and crossings closer than 1e-9 in t are merged.
powerline/checks.js
lines 248 to 266
CivilCraft 0.8.1
/** Where the plan segment a→b crosses the polyline pts (or passes a single point within 0.5 % of its length): → [t in 0..1]. */
export function crossings(a, b, pts) {
const out = [];
const dx = b.x - a.x, dy = b.y - a.y, L2 = dx * dx + dy * dy;
if (!(L2 > 0)) return out;
if (pts.length === 1) {
const p = pts[0], t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / L2;
if (t > 0 && t < 1 && Math.abs((p.x - a.x) * dy - (p.y - a.y) * dx) / Math.sqrt(L2) <= Math.sqrt(L2) * 0.005 + 1e-9) out.push(t);
return out;
}
for (let j = 0; j < pts.length - 1; j++) {
const p = pts[j], q = pts[j + 1], ex = q.x - p.x, ey = q.y - p.y;
const den = dx * ey - dy * ex;
if (Math.abs(den) < 1e-12) continue;
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;
if (t >= 0 && t <= 1 && u >= 0 && u <= 1 && !out.some((o) => Math.abs(o - t) < 1e-9)) out.push(t); // a shared vertex: once
}
return out;
}
The factor that turns a plant's mature height and spread in meters into project units.
project units per meter = 1 ÷ (1200 ÷ 3937) for feet
Plant sizes are divided by the US survey foot here. checks.js then multiplies heights by 0.3048, so in a feet project an obstacle's height comes back about 2 parts in a million smaller than the plant's size in meters.
powerline/landscape-obstacles.js
line 45
CivilCraft 0.8.1
const perM = 1 / (units === 'm' ? 1 : FT_M); // project units per metre (plant sizes are metres)
Tree canopy as a ring
64-bit
The outline of a plant's canopy used to find where a span passes over it.
12 points on the circle of radius r: (x + r·cos(2πk ÷ 12), y + r·sin(2πk ÷ 12))
The straight sides lie inside the true circle by up to r × (1 − cos 15°), about 3.4 percent of the radius, so a span that only clips the canopy's edge between two points is not counted as crossing it.
powerline/landscape-obstacles.js
lines 34 to 38
CivilCraft 0.8.1
function ring(x, y, r, n = 12) {
const pts = [];
for (let k = 0; k <= n; k++) { const a = (k % n) / n * 2 * Math.PI; pts.push({ x: x + r * Math.cos(a), y: y + r * Math.sin(a) }); }
return pts;
}
A plant as an obstacle
64-bit
A single plant's canopy radius, height and ground level as an obstacle under a line.
The foundation volume in the support's properties, in cubic yards for a feet project.
yd³ = m³ × 1.307951
The factor is written to 7 digits here, while outputs.js converts volumes with the exact 0.764554857984 m³ per cubic yard. The value is shown to 2 decimals, so the difference does not reach the display.
Wind span and weight span from the catenaries either side; transverse load from wind and the line angle; equivalent load at the support's nominal point by the ratio of heights; phase spacing k·√(sag + insulator length) + U ÷ 150; swing angle from atan of transverse over vertical, as listed in the file header.
Number types
64-bit numbers.
Tolerances
Clearance, obstacle and spacing checks pass within 1e-6 m, absolute. Support strength passes within a relative 1e-6. Parallel test in plan crossings: |cross product| below 1e-12, absolute. A single-point obstacle counts within 0.5 percent of the span length plus 1e-9.
Limits
Clearance is sampled at max(20, ⌈L ÷ 2⌉) intervals per span. A tree canopy is a 12-sided ring and only vertical clearance where the span crosses it in plan is checked. Embedment defaults to height ÷ 10 + 0.5 m; foundation diameter to max(0.6, foot diameter + 0.3) m.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.