Resources Numerical accuracy Calculation policy

Map coordinate reprojection

Moves points, and a model's position on the map, between coordinate systems.

How the precision is kept

Coordinates travel as 64-bit arrays through proj4 2.22.0. The limit is the datum shift, not the arithmetic: without a loaded shift grid the result is approximate to the accuracy EPSG states for that shift, and the app raises a warning that says so. A map origin is rounded to 6 decimal places of the map unit.

Calculations under this policy

3 calculations in 2 source files. 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.

reproject-batch.js BimCraft 0.8.1 64-bit 1 calculation Reviewed

Tests tools/bimcraft-test/georef-test.mjs

Reproject points

64-bit

Every point of a file from one coordinate system to another.

(x, y) in the source system → (x, y) in the target system, point by point

Reads and writes a 64-bit array in place. A point the projection cannot carry becomes not-a-number and is counted, never a wrong coordinate.

  • reproject-batch.js
  • lines 12 to 23
  • BimCraft 0.8.1
export function reprojectBatch(proj4, fromDef, toDef, xy, { start = 0, end = xy.length / 2 } = {}) {
  const conv = proj4(fromDef, toDef);
  let bad = 0;
  const p = [0, 0];
  for (let i = start; i < end; i++) {
    p[0] = xy[2 * i]; p[1] = xy[2 * i + 1];
    let q = null;
    try { q = conv.forward(p); } catch (e) { q = null; }
    if (q && Number.isFinite(q[0]) && Number.isFinite(q[1])) { xy[2 * i] = q[0]; xy[2 * i + 1] = q[1]; } else { xy[2 * i] = NaN; xy[2 * i + 1] = NaN; bad++; }
  }
  return bad;
}
crs-georef.js BimCraft 0.8.1 64-bit 2 calculations Reviewed

Tests tools/bimcraft-test/georef-test.mjs

Map origin from latitude and longitude

64-bit

Eastings and northings of a typed latitude and longitude.

(lon, lat) → (E, N), rounded to 0.000001 of the map unit

The rounding is one micrometer when the map unit is the meter: far below any survey accuracy, and it keeps the saved origin a short, stable number.

  • crs-georef.js
  • lines 111 to 119
  • BimCraft 0.8.1
/** Eastings / northings (in the system's unit) of a latitude and longitude through the chosen system. */
export function originFromLonLat(crs, lon, lat) {
  if (!crs || crs.unit === 'deg') return { error: fail('projected'), problems: [] };
  if (!fin(lon) || !fin(lat) || Math.abs(lat) > 90 || Math.abs(lon) > 180) return { error: fail('latlon'), problems: [] };
  let q;
  try { q = gridOf(crs, lon, lat); } catch { q = null; }
  if (!q || !q.every(fin)) return { error: fail('outside', { name: crs.name }), problems: [] };
  return { eastings: r6(q[0]), northings: r6(q[1]), problems: [...areaProblems(crs, lon, lat, 'model'), ...shiftProblems({ proj4: '+datum=WGS84', name: 'WGS 84' }, crs)] };
}

Model to map conversion

64-bit

Where a model sits on the map: origin, rotation and scale.

X axis = (cos a, sin a) rounded to 1e-12; scale = model unit ÷ map unit

The axis direction is rounded at the twelfth decimal, which turns a model 1 km long by a thousandth of a micrometer at its far end.

  • crs-georef.js
  • lines 121 to 139
  • BimCraft 0.8.1
/**
 * The IfcMapConversion for a model placed at (eastings, northings) in `crs`'s unit, with its X axis `rotationDeg` degrees counter-clockwise from grid
 * east (0 = the model's Y axis points to grid north), and `scale` (default: the model's length unit over the map's, i.e. the unit difference only).
 */
export function makeConversion({ crs, eastings, northings, height = 0, rotationDeg = 0, scale, fileUnitM = 1 }) {
  if (!crs || crs.unit === 'deg') return { error: fail('projected'), problems: [] };
  const unitScale = fileUnitM / (METRES_PER[crs.unit] || 1);
  const s = scale === undefined || scale === null || scale === '' ? unitScale : Number(scale);
  const nums6 = [eastings, northings, height, rotationDeg].map(Number);
  if (!nums6.every(fin)) return { error: fail('numbers'), problems: [] };
  if (!fin(s) || s <= 0 || s / unitScale > 1000 || s / unitScale < 0.001) return { error: fail('scale', { scale: s }), problems: [] };
  if (Math.abs(nums6[0]) > 1e9 || Math.abs(nums6[1]) > 1e9) return { error: fail('numbers'), problems: [] };
  const a = nums6[3] * D2R;
  const conv = { Eastings: r6(nums6[0]), Northings: r6(nums6[1]), OrthogonalHeight: r6(nums6[2]), XAxisAbscissa: r12(Math.cos(a)), XAxisOrdinate: r12(Math.sin(a)), Scale: s };
  let ll;
  try { ll = lonLatOf(crs, conv.Eastings, conv.Northings); } catch { ll = null; }
  if (!ll || !ll.every(fin) || Math.abs(ll[1]) > 90) return { error: fail('outside', { name: crs.name }), problems: [] };
  return { conv, lonlat: ll, problems: areaProblems(crs, ll[0], ll[1], 'model') };
}

In detail

Method
Each coordinate system is a proj4 definition from the catalog. Large batches run in a background worker, or in slices that hand the page back between them.
Number types
A 64-bit array of x and y pairs. Points that do not project come back as not-a-number and are counted.
Tolerances
A map origin is rounded to 6 decimal places of the map unit. The direction of the model's X axis on the map is rounded to 12.
Checked by
The georeferencing test in the BimCraft test tools.
Limits
Grid-based datum shifts (NADCON, NTv2, OSTN15) need a loaded grid. Without one the shift is the grid-free one, approximate to the accuracy EPSG states. One run handles up to 25 million points.

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