Resources Numerical accuracy Calculation policy

Surface exported as a GeoTIFF elevation grid

Samples a surface on a regular grid and writes it as a GeoTIFF with one elevation band.

How the precision is kept

Each elevation is computed in 64-bit from the surface and then stored in a 32-bit array, which is what the file holds. A 32-bit number keeps about 7 significant digits. The grid's origin and cell size are written to the file as 64-bit numbers.

Open item. Offer a 64-bit sample format for the elevation band, or state the 32-bit step next to the export.

Calculations under this policy

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

geotiff-write.js CivilCraft 0.8.1 32-bit in part 4 calculations Reviewed

Tests bin/test-civilcraft-terrain.mjs

Elevation grid from a surface

32-bit in part

The elevation at the center of every grid cell, row by row from the north.

x₀ = ⌊minX ÷ cell⌋·cell; y₀ = ⌈maxY ÷ cell⌉·cell; center = (x₀ + (c + 0.5)·cell, y₀ − (r + 0.5)·cell)

The elevation comes from the surface as a 64-bit number and is stored straight into a Float32Array, so the exported value keeps about 7 significant digits. Cell centers are index × cell from the origin, not running sums. Cells outside the surface take the no-data value.

  • geotiff-write.js
  • lines 40 to 62
  • CivilCraft 0.8.1
export async function surfaceGrid(surface, { cell, toGrid = (x, y) => [x, y], fromGrid = (x, y) => [x, y], signal = null, progress = null, maxCells = 25e6 } = {}) {
  if (!(cell > 0) || !Number.isFinite(cell)) throw new Error(t('gtw.cell.bad'));
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
  for (const loop of surface.outline()) for (const p of loop) {
    const [gx, gy] = toGrid(p.x, p.y);
    minX = Math.min(minX, gx); maxX = Math.max(maxX, gx); minY = Math.min(minY, gy); maxY = Math.max(maxY, gy);
  }
  if (!Number.isFinite(minX)) throw new Error(t('gtw.no_outline'));
  const x0 = Math.floor(minX / cell) * cell, y0 = Math.ceil(maxY / cell) * cell;
  const width = Math.max(1, Math.ceil((maxX - x0) / cell - 1e-9)), height = Math.max(1, Math.ceil((y0 - minY) / cell - 1e-9));
  if (width * height > maxCells) throw new Error(t('gtw.too_many', { w: width.toLocaleString('en-US'), h: height.toLocaleString('en-US'), cells: (width * height / 1e6).toFixed(1), max: String(maxCells / 1e6) }));
  const values = new Float32Array(width * height);
  for (let r = 0; r < height; r++) {
    const gy = y0 - (r + 0.5) * cell;
    for (let c = 0; c < width; c++) {
      const [x, y] = fromGrid(x0 + (c + 0.5) * cell, gy);
      const z = surface.elevationAt(x, y);
      values[r * width + c] = z === null ? NODATA : z;
    }
    if ((r & 63) === 63) { aborted(signal); if (progress) { progress(r / height); await tick(); } }
  }
  return { width, height, gt: [x0, cell, 0, y0, 0, -cell], values };
}

Elevations written to the file

32-bit in part

The tiles or strips of elevation values as they go into the TIFF.

Values are copied from one Float32Array to another and written as 32-bit little-endian numbers; nothing is rescaled or rounded further. Unused cells of an edge tile are filled with the no-data value.

  • geotiff-write.js
  • lines 80 to 88
  • CivilCraft 0.8.1
const buf = new Float32Array(rows * cols);
if (tiled) buf.fill(NODATA);
for (let r = 0; r < rows; r++) {
  const sr = by * bh + r;
  if (sr >= height) break;
  const sc = bx * bw, n = Math.min(cols, width - sc);
  buf.set(values.subarray(sr * width + sc, sr * width + sc + n), r * cols);
}
let bytes = new Uint8Array(buf.buffer);

Grid origin and cell size in the file

64-bit

The pixel scale and the tie point that place the grid.

pixel scale = (cell, cell, 0); tie point = pixel (0, 0) at (x₀, y₀)

Both tags are written as 64-bit numbers, so the position of the grid keeps full precision even though the elevations do not.

  • geotiff-write.js
  • lines 111 to 112
  • CivilCraft 0.8.1
dbl(33550, [gt[1], -gt[5], 0]);
dbl(33922, [0, 0, 0, gt[0], gt[3], 0]);

Default cell size

64-bit

The mean plan length of the surface's triangle edges, offered as the cell size.

Σ |edge| ÷ number of edges

Edge lengths by hypot from the surface's 64-bit coordinate arrays, added in one running sum and divided once.

  • geotiff-write.js
  • lines 29 to 35
  • CivilCraft 0.8.1
export function averageEdge(surface) {
  const e = surface.edges(), X = surface.X, Y = surface.Y;
  if (!e.length) return 0;
  let s = 0;
  for (let k = 0; k < e.length; k += 2) s += Math.hypot(X[e[k]] - X[e[k + 1]], Y[e[k]] - Y[e[k + 1]]);
  return s / (e.length / 2);
}

In detail

Method
The surface outline's box in grid coordinates is snapped outward to multiples of the cell; each cell center takes the surface elevation there. Written as a little-endian TIFF with SampleFormat 3 and 32 bits per sample, in tiles or strips, as the file header states (TIFF 6.0 and GeoTIFF 1.1).
Number types
Elevations: Float32Array. Pixel scale and tie point: 64-bit (TIFF type DOUBLE). No-data value: the lowest 32-bit number, −3.4028235e38.
Tolerances
Grid width and height: ⌈extent ÷ cell − 1e-9⌉, so an extent that is a whole number of cells does not gain one.
Limits
A 32-bit elevation between 1,024 and 2,048 is stored in steps of 0.00012; between 8,192 and 16,384 in steps of 0.00098. The elevation is that of the cell center, not an average over the cell.

Approach: 32-bit step, flagged

One step stores or exchanges 32-bit values that reach a number a person reads. It is listed here with its open item, and the step is either moved to 64-bit or the app will warn where the number is read.

Every policy that takes this approach