Resources Numerical accuracy Calculation policy

GIS import: reprojection and elevation units

Places the features of shapefiles, GeoJSON, KML and GeoPackage files in the project's coordinate system and converts their elevations to the project's unit.

How the precision is kept

Horizontal coordinates go through the project frame's transform from crs.js in 64-bit. Elevations are multiplied by one factor, the ratio of the source and project units in meters. The foot is 0.3048 m and the US survey foot is written as the fraction 1200/3937 m.

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.

gis-read.js CivilCraft 0.8.1 64-bit 4 calculations Reviewed

Tests bin/test-civilcraft-gisvec.mjs

Unit sizes in meters

Exact

The length of a meter, a foot and a US survey foot used for elevations.

ft = 0.3048 m; US survey ft = 1200 ÷ 3937 m

0.3048 is the definition of the foot. The US survey foot is written as the fraction 1200/3937 and evaluated once in 64-bit, not typed as a rounded decimal.

  • gis-read.js
  • line 172
  • CivilCraft 0.8.1
const UNIT_M = { m: 1, ft: 0.3048, ftUS: 1200 / 3937 };

Features placed in the project

64-bit

Reprojects every vertex of every layer and scales its elevation.

z′ = z × (source unit in m ÷ project unit in m)

Each vertex goes through the frame's forward transform; elevations are then multiplied by zk. zk is 1 ÷ project unit for GeoJSON and KML and source ÷ project otherwise, and the multiplication is skipped when zk is exactly 1. A geographic feature whose bounds leave ±180.000001° or ±90.000001° is dropped and counted.

  • gis-read.js
  • lines 173 to 206
  • CivilCraft 0.8.1
export function buildLayers(sources, { project = null, units = 'ft', overrides = new Map() } = {}) {
  const notes = [], layers = [];
  const projM = project ? UNIT_M[project.unit] : units === 'm' ? 1 : 1200 / 3937;
  sources.forEach((s, si) => {
    const src = overrides.get(si) || s.src;
    let fwd = null, zk = 1, geo = false;
    if (src.kind === 'same' || src.kind === 'none') { fwd = null; if (src.kind === 'none' && project) notes.push(t('gread.nocrs.same', { file: s.file })); }
    else if (src.kind === 'bad') { notes.push(t('gread.bad.crs', { file: s.file, why: src.why })); return; }
    else {
      if (!project) { notes.push(t('gread.noproject', { file: s.file, crs: srcName(src) })); return; }
      geo = src.kind === 'wgs84' || src.geographic;
      try { const f = frame(project); fwd = f.from(srcDef(src)); } catch (e) { notes.push(t('gread.noconvert', { file: s.file, why: e.message })); return; }
      const srcM = geo ? 1 : UNIT_M[src.crs.unit] || METRES_PER[src.crs.unit] || 1;
      zk = srcM / projM;
    }
    if ((s.format === 'GeoJSON' && !s.legacyCrs || s.format === 'KML') && src.kind !== 'same' && src.kind !== 'none') zk = 1 / projM;   // RFC 7946 / KML: heights in metres
    for (const l of s.layers) {
      let bad = 0;
      const feats = l.features.map((f) => {
        if (!f.g) return f;
        if (geo) { const b = bounds(f.g); if (b.minX < -180.000001 || b.maxX > 180.000001 || b.minY < -90.000001 || b.maxY > 90.000001) { bad++; return null; } }
        let g = fwd ? mapXY(f.g, fwd) : f.g;
        if (zk !== 1) g = mapZ(g, (z) => z * zk);
        return { g, p: f.p };
      });
      if (bad) notes.push(t('gread.outside', { layer: l.name, n: bad }));
      if (fwd && feats.some((f) => f?.g && !Number.isFinite(bounds(f.g).minX))) notes.push(t('gread.someconvert', { layer: l.name }));
      layers.push({ name: l.name, geom: l.geom, hasZ: l.hasZ, fields: l.fields, features: feats.filter((f) => f && (!f.g || Number.isFinite(bounds(f.g).minX))), source: { file: s.file, format: s.format, crs: src.kind === 'same' ? (project ? t('gread.src.asproject', { name: project.name }) : null) : src.kind === 'none' ? null : srcName(src) } });
    }
    if (zk !== 1 && s.layers.some((l) => l.hasZ)) notes.push(t('gread.z', { file: s.file, from: s.format === 'GeoJSON' || s.format === 'KML' || geo ? unitWord('m') : unitWord(src.crs.unit), to: unitWord(projM === 1 ? 'm' : 'ft') }));
    if (fwd && (src.kind === 'wgs84' || src.crs.proj4 !== project.proj4 || project.local)) notes.push(t(project.local ? 'gread.converted.local' : 'gread.converted', { file: s.file, from: srcName(src), to: project.name }));
  });
  return { layers, notes };
}

Middle of the data in longitude and latitude

64-bit

Finds where the imported data lies, to suggest a coordinate system near it.

c = (min + max) ÷ 2 of the bounding box

The box midpoint is computed in the source coordinates and then transformed to longitude and latitude. It is used to pick a system near the data, not to place anything.

  • gis-read.js
  • lines 143 to 154
  • CivilCraft 0.8.1
/** The middle of every source's data in longitude/latitude (null when no source has a known system). */
export function middleLonLat(sources) {
  for (const s of sources) {
    if (s.src.kind !== 'wgs84' && s.src.kind !== 'crs') continue;
    const b = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
    for (const l of s.layers) for (const f of l.features) bounds(f.g, b);
    if (!Number.isFinite(b.minX)) continue;
    const cx = (b.minX + b.maxX) / 2, cy = (b.minY + b.maxY) / 2;
    try { const ll = s.src.kind === 'wgs84' ? [cx, cy] : transformer(s.src.crs, null)(cx, cy); if (ll.every(Number.isFinite)) return ll; } catch { /* next */ }
  }
  return null;
}

Survey points given as latitude and longitude

64-bit

Converts point-file rows in degrees to project coordinates.

Longitude and latitude in degrees go through the frame's fromLonLat in 64-bit. A point whose result is not finite is dropped and counted. The elevation is left as read.

  • gis-read.js
  • lines 208 to 219
  • CivilCraft 0.8.1
/** Point-file rows with latitude/longitude columns (points.js `latlon`) → project coordinates through `crs` (state.crs);
 *  z stays as it is (the elevation column is taken in project units). */
export function latLonPoints(points, crs) {
  const f = frame(crs), out = [];
  let bad = 0;
  for (const p of points) {
    const [x, y] = f.fromLonLat(p.x, p.y);
    if (!Number.isFinite(x) || !Number.isFinite(y)) { bad++; continue; }
    out.push({ ...p, x, y });
  }
  return { points: out, bad };
}

In detail

Method
frame(project).from(source) from crs.js maps x and y; z is scaled by source unit ÷ project unit. GeoJSON without a legacy crs member and KML heights are taken as meters.
Number types
64-bit numbers.
Tolerances
A geographic feature is refused when its bounds leave ±180.000001° in longitude or ±90.000001° in latitude.
Limits
The elevation factor is a quotient of two unit sizes, so a conversion between feet and US survey feet is rounded once in the factor and once per elevation.

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