Resources Numerical accuracy Calculation policy

Terrain import and GeoTIFF export settings

Works out the point spacing, the point count and the elevation scaling when an elevation grid or a LiDAR file is brought into a project, and the cell size offered when a surface is written as a GeoTIFF.

How the precision is kept

The dialog works in 64-bit numbers. The scale between file units and project units is measured by converting two nearby points and dividing the distance between them by their spacing. Default spacings and cell sizes are rounded to 6 significant digits before they are shown in the input box; the value in the box is the one used. Coordinates and elevations of imported points are multiplied or converted in 64-bit and are not rounded here.

Calculations under this policy

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

gis-terrain-ui.js CivilCraft 0.8.1 64-bit 6 calculations Reviewed

Tests None found by the review

Project units per file unit

64-bit

The factor that turns a spacing typed in project units into the file's own units.

k = |f(c + d) − f(c)| ÷ d, c = middle of the file

Two points d apart are converted and the distance between the results is divided by d; d is one cell for a grid and 1/1000 of the file width (at least 1e-6) for LiDAR. The subtraction of two converted coordinates that differ by only d loses digits in proportion to coordinate size ÷ d. The result falls back to 1 when it is not a positive finite number.

  • gis-terrain-ui.js
  • lines 269 to 279
  • CivilCraft 0.8.1
// project units per file unit near the file's middle (for spacings)
function unitScale() {
  const f = xf().fn, c = boxOk ? [(fileBox.minX + fileBox.maxX) / 2, (fileBox.minY + fileBox.maxY) / 2] : [0, 0];
  const d = kind === 'grid' ? Math.hypot(src.gt[1], src.gt[4]) : Math.max(1e-6, (fileBox.maxX - fileBox.minX) / 1000) || 1;
  try {
    const a = f(c[0], c[1]), b = f(c[0] + d, c[1]);
    const k = Math.hypot(b[0] - a[0], b[1] - a[1]) / d;
    if (Number.isFinite(k) && k > 0) return k;
  } catch { /* fall through */ }
  return 1;
}

Default thinning spacing

64-bit

The spacing the dialog proposes so the import stays under the point limit.

grid: cell × ⌈√(n ÷ (0.98 × cap))⌉; LiDAR: max(1.5 × √(area ÷ points), 1.1 × √(area ÷ cap)) rounded to a 1-2-5 step

The proposed value is rounded to 6 significant digits with toPrecision(6) before it goes in the input box. It is a setting, not a measurement; the number in the box is what the import uses.

  • gis-terrain-ui.js
  • lines 365 to 380
  • CivilCraft 0.8.1
function defaultSpacing() {
  const k = unitScale();
  if (kind === 'grid') {
    const cellP = Math.hypot(src.gt[1], src.gt[4]) * k;
    const { sBox } = areaBoxes();
    const win = sBox ? gridWindow(src, sBox) : { c0: 0, r0: 0, c1: src.width, r1: src.height };
    const n = win ? (win.c1 - win.c0) * (win.r1 - win.r0) : src.width * src.height;
    const cap = st.as === 'surface' ? LIMITS.tinNodes : LIMITS.points;
    const step = Math.max(1, Math.ceil(Math.sqrt(n / (cap * 0.98))));
    return +(cellP * step).toPrecision(6);
  }
  const area = (fileBox.maxX - fileBox.minX) * (fileBox.maxY - fileBox.minY) * k * k;
  const n = Math.max(1, src.count);
  const natural = Math.sqrt(area / n);
  return +niceStep(Math.max(natural * 1.5, Math.sqrt(area / LIMITS.points) * 1.1)).toPrecision(6);
}

Points a grid import will make

64-bit

The number of cells read and of points or triangles made, shown before the import runs.

count = ⌈columns ÷ step⌉ × ⌈rows ÷ step⌉; triangles = 2 × (⌈columns ÷ step⌉ − 1) × (⌈rows ÷ step⌉ − 1)

The counts are whole-number arithmetic on columns, rows and the step. The step is round(spacing ÷ cell size) from gridStep (thin.js), where the cell size in project units is hypot of two geotransform terms times the unit scale, a 64-bit value.

  • gis-terrain-ui.js
  • lines 344 to 349
  • CivilCraft 0.8.1
const cells = (win.c1 - win.c0) * (win.r1 - win.r0);
if (cells > LIMITS.readCells) { out.msg = t('tui.area.big', { cells: fmtBig(cells), w: num(win.c1 - win.c0), h: num(win.r1 - win.r0), max: fmtBig(LIMITS.readCells) }); return out; }
const cellP = Math.hypot(src.gt[1], src.gt[4]) * k;
const step = gridStep(sp, cellP);
const n = Math.ceil((win.c1 - win.c0) / step) * Math.ceil((win.r1 - win.r0) / step);
Object.assign(out, { win, step, count: n, cellP, pBox, sBox, cap });

LiDAR point into project coordinates

64-bit

Each kept LiDAR point's position and elevation as stored in the project.

(x, y) → f(x, y); z → z × z factor

The elevation is multiplied by the unit factor from zFactor (terrain.js) in one 64-bit multiplication. x and y pass through unchanged when the two coordinate systems are the same, otherwise through the conversion function. Nothing is rounded in this step.

  • gis-terrain-ui.js
  • lines 458 to 461
  • CivilCraft 0.8.1
if (sBox && (x < sBox.minX || x > sBox.maxX || y < sBox.minY || y > sBox.maxY)) { th.seen++; th.outside++; return; }
if (!st.classes.has(c)) { th.seen++; th.filtered++; return; }
if (id) th.add(x, y, z * zf, c);
else { const q = f(x, y); th.add(q[0], q[1], z * zf, c); }

Default GeoTIFF cell size

64-bit

The cell size proposed when a surface is exported as an elevation grid.

cell = average triangle edge ÷ 2.5, rounded to a 1-2-5 step

The proposal is rounded to 6 significant digits with toPrecision(6) and written into the input box; the export passes Number(box value) to surfaceGrid (geotiff-write.js). The cell size is a setting, so the rounding does not change any surveyed value.

  • gis-terrain-ui.js
  • line 555
  • CivilCraft 0.8.1
const cellDefault = () => { const a = averageEdge(surf()); return a > 0 ? +niceStep(a / 2.5).toPrecision(6) : 1; };

GeoTIFF grid size estimate

64-bit

The number of columns and rows the dialog says the export will have.

nx = ⌈width ÷ cell⌉ + 1; ny = ⌈height ÷ cell⌉ + 1

Width and height are differences of the surface bounds, multiplied by the local scale factor when the project uses a local system. This is the dialog's estimate; the written grid's size comes from surfaceGrid in geotiff-write.js.

  • gis-terrain-ui.js
  • lines 571 to 573
  • CivilCraft 0.8.1
let w = b.maxX - b.minX, h = b.maxY - b.minY;
if (f && crs.local) w = w * crs.local.scale, h = h * crs.local.scale;
const nx = Math.ceil(w / c) + 1, ny = Math.ceil(h / c) + 1;

In detail

Method
Finite-difference scale at the middle of the file; counts from whole-number grid steps; elevation × unit factor.
Number types
64-bit numbers; whole numbers for counts and steps.
Tolerances
The LiDAR probe distance is 1/1000 of the file width with a floor of 1e-6 file units. Default spacing and default cell size are rounded with toPrecision(6).
Limits
The unit scale is taken at one place (the middle of the file) and used for the whole file. The readers, the thinning and the coordinate conversion live in raster.js, las.js, thin.js, terrain.js, crs.js and geotiff-write.js, which this review of the dialog does not cover.

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