Resources Numerical accuracy Calculation policy

Superelevation: runoff, runout and lane slopes

Works out where the lanes of a road rotate into and out of each curve and the cross slope of each lane at any station.

How the precision is kept

Runoff and runout lengths and the transition stations are closed-form 64-bit expressions of the lane width, the rate and the relative gradient. The rate comes from a table by radius, read as a step function without interpolation. The default table and the 0.5 % gradient are placeholders, as the file's header says.

Calculations under this policy

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

superelev.js CivilCraft 0.8.1 64-bit 5 calculations Reviewed

Tests bin/test-civilcraft-super.mjs

Default rate table

64-bit

The rates by radius and the relative gradient a new corridor starts with.

Rates from 2 % to 6 % by radius, one table for feet and one for meters, and a relative gradient of 0.005. The header calls them placeholders for the user to replace with their own design criteria.

  • superelev.js
  • lines 33 to 37
  • CivilCraft 0.8.1
export function SUPER_DEFAULTS(units = 'ft') {
  return units === 'm'
    ? { on: false, delta: 0.005, rate: [[2100, null], [1300, 0.02], [800, 0.03], [550, 0.04], [380, 0.05], [0, 0.06]] }
    : { on: false, delta: 0.005, rate: [[7000, null], [4300, 0.02], [2600, 0.03], [1800, 0.04], [1250, 0.05], [0, 0.06]] };
}

Rate for a radius

64-bit

Looks up the superelevation rate of a curve.

A step lookup in a table sorted by radius, largest first: the first row whose radius the curve reaches gives the rate, and null means normal crown. There is no interpolation between rows.

  • superelev.js
  • lines 48 to 51
  • CivilCraft 0.8.1
export function rateFor(R, rate) {
  for (const [r, e] of rate) if (R >= r) return e;
  return rate.length ? rate[rate.length - 1][1] : null;
}

Runoff, runout and transition stations

64-bit

The lengths and stations of the transition into and out of each curve.

L_r = lane × e ÷ Δ; L_t = (NC ÷ e) × L_r; without a spiral: level at TS − ⅔L_r, full at TS + ⅓L_r

64-bit products and quotients; e is floored at 1e-9 in the runout division. With a spiral at an end the runoff is the spiral's own length. Problem texts round the radius and lengths to 2 decimals; the stations kept in the plan are not rounded.

  • superelev.js
  • lines 53 to 75
  • CivilCraft 0.8.1
export function superPlan(al, { super: sp, nc = 0.02, lane = 12 } = {}) {
  const curves = [], problems = [];
  if (!al?.ok || !sp?.on) return { curves, problems };
  const cvs = al.curves?.length ? al.curves : al.elements.filter((e) => e.type === 'arc').map((e) => ({ r: e.r, ccw: e.ccw, ls1: 0, ls2: 0, ts: e.sta, sc: e.sta, cs: e.sta + e.len, st: e.sta + e.len }));
  cvs.forEach((c, i) => {
    const e = rateFor(c.r, sp.rate);
    if (e === null || !(e > 0)) return;
    const dir = c.ccw ? 'left' : 'right';
    const full = (lane * e) / sp.delta;
    const Lr1 = c.ls1 > 0 ? c.ls1 : full, Lr2 = c.ls2 > 0 ? c.ls2 : full;
    const Lt1 = (nc / Math.max(e, 1e-9)) * Lr1, Lt2 = (nc / Math.max(e, 1e-9)) * Lr2;
    const st = {};
    if (c.ls1 > 0) { st.lc0 = c.ts; st.fs0 = c.sc; } else { st.lc0 = c.ts - (2 * Lr1) / 3; st.fs0 = c.ts + Lr1 / 3; }
    if (c.ls2 > 0) { st.lc1 = c.st; st.fs1 = c.cs; } else { st.lc1 = c.st + (2 * Lr2) / 3; st.fs1 = c.st - Lr2 / 3; }
    st.nc0 = st.lc0 - Lt1; st.nc1 = st.lc1 + Lt2;
    st.rc0 = st.lc0 + (Lr1 * Math.min(1, nc / e)); st.rc1 = st.lc1 - (Lr2 * Math.min(1, nc / e));
    if (st.fs1 < st.fs0) problems.push(t('se.short', { n: i + 1, r: +c.r.toFixed(2) }));
    for (const [k, ls] of [[1, c.ls1], [2, c.ls2]]) if (ls > 0 && ls < full - 1e-9) problems.push(t('se.spiral.steep', { n: i + 1, end: k === 1 ? 'TS' : 'ST', ls: +ls.toFixed(2), need: +full.toFixed(2) }));
    curves.push({ i: i + 1, R: c.r, dir, outside: dir === 'right' ? 'L' : 'R', e, Lr: Lr1, Lt: Lt1, Lr1, Lr2, Lt1, Lt2, pc: c.ts, pt: c.st, spirals: c.ls1 > 0 || c.ls2 > 0, stations: st });
  });
  for (let k = 1; k < curves.length; k++) if (curves[k].stations.nc0 < curves[k - 1].stations.nc1) problems.push(t('se.overlap', { a: curves[k - 1].i, b: curves[k].i }));
  return { curves, problems };
}

Outside lane slope through a transition

64-bit

The cross slope of the outside lane at a station.

runout: NC − NC × (x − nc₀) ÷ L_t; runoff: −e × (x − lc₀) ÷ L_r; result = max(up, down, −e)

Linear ramps in 64-bit, with the lengths floored at 1e-9 so the division cannot be by zero. Taking the maximum of the two ramps and −e handles a curve too short to reach full superelevation.

  • superelev.js
  • lines 77 to 92
  • CivilCraft 0.8.1
/** The outside lane's fall-away slope along one curve's transition (+NC outside it). */
function outsideAt(c, sta, nc) {
  const s = c.stations, e = c.e;
  // a curve too short for full superelevation reaches only part of e: the two ramps meet in the middle
  const up = (x) => {
    if (x <= s.nc0) return nc;
    if (x <= s.lc0) return nc - (nc * (x - s.nc0)) / Math.max(c.Lt1, 1e-9);                // runout: NC → 0
    return -(e * (x - s.lc0)) / Math.max(c.Lr1, 1e-9);                                       // runoff: 0 → −e
  };
  const down = (x) => {
    if (x >= s.nc1) return nc;
    if (x >= s.lc1) return nc - (nc * (s.nc1 - x)) / Math.max(c.Lt2, 1e-9);
    return -(e * (s.lc1 - x)) / Math.max(c.Lr2, 1e-9);
  };
  return Math.max(up(sta), down(sta), -e);
}

Lane slopes at a station

64-bit

The fall-away slope of the left and right lanes.

inside = max(NC, −outside)

Outside every transition both lanes keep the normal crown. Where the transitions of two curves overlap, the later curve's values replace the earlier ones, because the loop runs in order.

  • superelev.js
  • lines 94 to 103
  • CivilCraft 0.8.1
export function slopesAt(plan, sta, nc = 0.02) {
  let L = nc, R = nc;
  for (const c of plan?.curves || []) {
    if (sta <= c.stations.nc0 || sta >= c.stations.nc1) continue;
    const o = outsideAt(c, sta, nc);
    const inside = Math.max(nc, -o);   // the inside lane keeps its crown until the outside one rolls past it, then one plane
    if (c.outside === 'L') { L = o; R = inside; } else { R = o; L = inside; }
  }
  return { L, R };
}

In detail

Method
Runoff L_r = lane × e ÷ Δ and runout L_t = NC ÷ e × L_r; two thirds of the runoff on the tangent and one third on the curve, or the spiral's length where an end has a spiral (the header cites AASHTO's n₁ = 1, b_w = 1).
Number types
64-bit numbers.
Tolerances
Divisors are floored at 1e-9: the rate e, and the runoff and runout lengths. A spiral counts as too short when it is more than 1e-9 under the needed length.
Limits
The rate table is a step lookup. The default rates and gradient are placeholders, not a standard.

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