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