Resources Numerical accuracy Calculation policy

Flood extent on the ground surface

Lays the computed river water surface over the ground and gives the flooded area, the waterline and the greatest depth.

How the precision is kept

The water surface at a point is a straight-line interpolation between the two sections around it by river station. Depths are taken at the surface's triangle corners and the waterline is where the depth crosses zero along triangle edges, in 64-bit. The flooded area is summed triangle by triangle from absolute coordinates.

Open item. Sum each wet piece's area in the surface's local coordinates (before the origin is added back), so large coordinates do not cost digits in the flooded area.

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.

river-extent.js CivilCraft 0.8.1 64-bit 5 calculations Reviewed

Tests bin/test-civilcraft-river.mjs

Water surface at a plan point

64-bit

The water level at any point from the results at the sections either side.

rs = centerline length − distance along; WS = WS_a + (WS_b − WS_a) × (rs − rs_a) ÷ (rs_b − rs_a)

The point takes the river station of its nearest centerline point, then the level is one linear interpolation. Points past either end of the centerline, or outside the range of stations by more than 1e-9, have no water surface.

  • river-extent.js
  • lines 33 to 47
  • CivilCraft 0.8.1
export function wsAt(model, rows, p) {
  const cl = model.centreline, L = lineLength(cl), at = locateOn(cl, p);
  if (!at || !rows?.length) return null;
  // past either end of the centreline: no water surface there
  const past = (q, dir) => (p.x - q.x) * Math.cos(dir) + (p.y - q.y) * Math.sin(dir) > 1e-9;
  if (at.d <= 1e-9 && past(cl[0], at.dir + Math.PI)) return null;
  if (at.d >= L - 1e-9 && past(cl[cl.length - 1], at.dir)) return null;
  const rs = L - at.d;
  const r = rows.slice().sort((a, b) => a.rs - b.rs);
  if (rs < r[0].rs - 1e-9 || rs > r[r.length - 1].rs + 1e-9) return null;
  for (let i = 1; i < r.length; i++) {
    if (rs <= r[i].rs + 1e-9) { const a = r[i - 1], b = r[i], d = b.rs - a.rs; return d > 0 ? a.wse + ((b.wse - a.wse) * (rs - a.rs)) / d : b.wse; }
  }
  return r[0].wse;
}

Behind a levee

64-bit

Whether a point is kept dry by a levee the water does not top.

offset and crest interpolated between the two sections; dry when beyond the levee and WS < crest

The levee's offset and crest are linear interpolations between the two sections around the point; the comparison WS < crest has no tolerance.

  • river-extent.js
  • lines 49 to 67
  • CivilCraft 0.8.1
export function blockedAt(model, rows, p, ws) {
  const cl = model.centreline, at = locateOn(cl, p);
  if (!at) return false;
  const rs = lineLength(cl) - at.d;
  const secs = model.sections.filter((s) => s.levees).sort((a, b) => a.rs - b.rs);
  if (!secs.length) return false;
  for (const side of ['left', 'right']) {
    const all = model.sections.slice().sort((a, b) => a.rs - b.rs);
    for (let i = 1; i < all.length; i++) {
      const a = all[i - 1], b = all[i];
      if (rs < a.rs - 1e-9 || rs > b.rs + 1e-9) continue;
      const la = a.levees?.[side], lb = b.levees?.[side];
      if (!la || !lb) continue;
      const f = b.rs > a.rs ? (rs - a.rs) / (b.rs - a.rs) : 0, o = la.o + (lb.o - la.o) * f, crest = la.z + (lb.z - la.z) * f;
      if ((side === 'left' ? at.off < o : at.off > o) && ws < crest) return true;
    }
  }
  return false;
}

Depth at a point

64-bit

The depth of water at a plan point.

depth = WS − ground

One subtraction of the interpolated water level and the ground elevation at the point.

  • river-extent.js
  • lines 69 to 72
  • CivilCraft 0.8.1
export function depthAt(surface, model, rows, p) {
  const ws = wsAt(model, rows, p), z = surface.elevationAt(p.x, p.y, 1e-7);
  return ws === null || z === null ? null : ws - z;
}

Waterline point on a triangle edge

64-bit

Where the depth is zero along an edge between a wet corner and a dry corner.

u = d_a ÷ (d_a − d_b); point = a + u × (b − a)

The two depths have opposite signs, so the divisor adds their sizes and does not cancel. The point is computed once per edge and shared by both triangles, so the waterline joins exactly.

  • river-extent.js
  • lines 89 to 96
  • CivilCraft 0.8.1
const cross = (a, b) => {
  const k = a < b ? a + ',' + b : b + ',' + a;
  if (keyed.has(k)) return keyed.get(k);
  const da = dOf(a), db = dOf(b), u = da / (da - db);
  const p = { x: X[a] + ox + (X[b] - X[a]) * u, y: Y[a] + oy + (Y[b] - Y[a]) * u, k };
  keyed.set(k, p);
  return p;
};

Flooded area and greatest depth

64-bit

Adds up the wet part of each triangle and tracks the greatest depth at a corner.

piece area = |Σ (x_i·y_j − x_j·y_i)| ÷ 2; area = Σ piece areas

The surface holds X and Y relative to an origin, but the origin is added back before each piece's shoelace sum, so the products are of absolute coordinates: with coordinates near 2,000,000 each small piece's area is resolved to about 0.001 square units, and those errors add over the triangles. The greatest depth is taken at triangle corners only.

  • river-extent.js
  • lines 110 to 121
  • CivilCraft 0.8.1
for (let i = 0; i < 3; i++) {
  const a = v[i], b = v[(i + 1) % 3], da = d[i], db = d[(i + 1) % 3];
  if (da > 0) piece.push({ x: X[a] + ox, y: Y[a] + oy });
  if ((da > 0) !== (db > 0)) { const p = cross(a, b); piece.push({ x: p.x, y: p.y }); line.push(p); }
  if (da > out.maxDepth) out.maxDepth = da;
}
if (piece.length >= 3) {
  out.wet.push(piece);
  let a2 = 0;
  for (let i = 0; i < piece.length; i++) { const p = piece[i], q = piece[(i + 1) % piece.length]; a2 += p.x * q.y - q.x * p.y; }
  out.area += Math.abs(a2) / 2;
}

In detail

Method
Linear interpolation of water level by river station; depth at TIN vertices; waterline by linear interpolation along TIN edges; area as the sum of wet triangle pieces.
Number types
64-bit numbers; the surface's X, Y, Z arrays come from tin.js.
Tolerances
Station tests allow 1e-9. Ground elevation lookups pass 1e-7 to elevationAt.
Limits
The water surface is level across the river. A triangle is in or out of the river corridor by its middle. The greatest depth is the greatest at a triangle corner. Each wet piece's area is a shoelace sum on absolute coordinates.

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.

Every policy that takes this approach