Reads total-station field books (RW5, GSI, JobXML, CSV), reduces each shot to coordinates, and prepares directions and distances for the network adjustment.
How the precision is kept
Numbers are parsed to 64-bit and reduced with one expression per coordinate. What the files hold sets the accuracy: angles written as DDD.MMSSss are read to hundredths of a second, and GSI words are whole counts of a fixed unit. No earth-curvature or refraction term is applied to height differences.
Open item. Read a decimal comma in JobXML text as a decimal point instead of removing it.
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.
A coordinate, angle, distance or height read from a field of the file.
Every character that is not a digit, sign, point or e is removed before the text is converted, so a unit suffix is ignored. A comma is removed too: 105,32 would read as 10532. RW5 and CSV lines are split on commas first, so this can only occur in JobXML text.
collector.js
line 19
CivilCraft 0.8.1
const num = (v) => { const x = Number(String(v).replace(/[^\d.+\-eE]/g, '')); return Number.isFinite(x) && String(v).trim() !== '' ? x : NaN; };
Dotted degrees, minutes, seconds to radians
64-bit
An angle written as DDD.MMSSss, as many data collectors write it, as radians.
angle = (D + M ÷ 60 + S ÷ 3600) × π ÷ 180
The fractional part is rounded to 6 decimals before it is split, which removes the binary rounding of the typed value and also drops anything finer than a hundredth of a second. 1e-7 is added before the minutes are taken, so that a product such as 45.99999999999999, which stands for 46 minutes, is not cut to 45.
collector.js
lines 20 to 21
CivilCraft 0.8.1
/** DDD.MMSSss → radians (the dotted degrees-minutes-seconds many data collectors write). */
export function dmsDotted(v) { const s = Math.abs(v), d = Math.floor(s), f = Math.round((s - d) * 1e6) / 1e6, m = Math.floor(f * 100 + 1e-7), sec = (f * 100 - m) * 100; return Math.sign(v || 1) * (d + m / 60 + sec / 3600) * D2R; }
Degrees or gons to radians
64-bit
A file angle in decimal degrees or gons as radians.
degrees × π ÷ 180; gons × π ÷ 200
One multiplication by the 64-bit value of π and one division. π is the nearest 64-bit value.
The value of a GSI data word from its digits and its unit code.
millimeters ÷ 1000; 1/10 mm ÷ 10⁴; 1/100 mm ÷ 10⁵; gon or degree counts ÷ 10⁵; sexagesimal digits split into D, M, S
The digits are a whole number, so the only rounding is the single division by a power of ten and, for angles, the multiplication by π. The resolution is that of the word: 1 mm down to 0.01 mm for lengths; 1e-4 or 1e-5 gon, 1e-5 degree or a tenth of a second for angles.
collector.js
lines 50 to 55
CivilCraft 0.8.1
const unit = (info, raw) => {
const sign = raw[0] === '-' ? -1 : 1, digits = raw.replace(/^[+-]/, ''), u = info.slice(-1), n = Number(digits);
switch (u) { case '0': return { v: sign * n / 1000, k: 'len' }; case '1': return { v: sign * n / 1000, k: 'len' }; case '2': return { v: sign * n / 1e5 * Math.PI / 200, k: 'ang' };
case '3': return { v: sign * n / 1e5 * D2R, k: 'ang' }; case '4': { const s = String(Math.abs(n)).padStart(9, '0'); const d = Number(s.slice(0, -5)), m = Number(s.slice(-5, -3)), sc = Number(s.slice(-3, -1)) + Number(s.slice(-1)) / 10; return { v: sign * (d + m / 60 + sc / 3600) * D2R, k: 'ang' }; }
case '6': return { v: sign * n / 1e4 * Math.PI / 200, k: 'ang' }; case '7': return { v: sign * n / 1e4, k: 'len' }; case '8': return { v: sign * n / 1e5, k: 'len' }; default: return { v: NaN, k: '?' }; }
};
Polar reduction
64-bit
Northing, easting and elevation of every shot from its setup.
orientation = atan2(ΔE, ΔN) to the backsight − circle reading; hd = sd·sin(vz)·scale; N = N₀ + hd·cos(az), E = E₀ + hd·sin(az); Z = Z₀ + sd·cos(vz) + hi − hr
Each coordinate is one expression from the station; nothing accumulates from shot to shot. The scale factor multiplies the horizontal distance only. No earth-curvature or refraction term is added to the height difference. A setup without a usable backsight is reduced with orientation 0 and reported.
collector.js
lines 134 to 150
CivilCraft 0.8.1
/** Polar reduction. Station coordinates are northing/easting (n, e); azimuth clockwise from north; the backsight fixes the orientation. */
export function reduce(job, { scale = 1 } = {}) {
const points = {}, shots = [], problems = [];
for (const s of job.setups) {
const st = s.coords || job.points[s.station];
if (!st) { problems.push({ code: 'col.nostation', station: s.station }); continue; }
const bs = s.backsight && job.points[s.backsight.id];
let orient = 0;
if (bs && s.backsight.hz !== null) orient = Math.atan2(bs.e - st.e, bs.n - st.n) - s.backsight.hz; else problems.push({ code: 'col.nobacksight', station: s.station });
for (const k of s.shots) {
const hd = (k.horizontal ? k.sd : k.sd * Math.sin(k.vz)) * scale, az = orient + k.hz, dz = k.horizontal ? 0 : k.sd * Math.cos(k.vz) + s.hi - k.hr;
const p = { n: st.n + hd * Math.cos(az), e: st.e + hd * Math.sin(az), z: Number.isFinite(st.z) ? st.z + dz : NaN, code: k.code };
shots.push({ station: s.station, id: k.id, az, hd, ...p }); if (k.id && !points[k.id]) points[k.id] = p;
}
}
return { points, shots, problems };
}
Shots as adjustment observations
64-bit
Directions and horizontal distances with their standard deviations, and starting coordinates for the free points.
The distance observation is the reduced horizontal distance at scale 1. Starting coordinates for free points are the polar reduction's. The default standard deviations are fixed numbers, not read from the file.
collector.js
lines 152 to 170
CivilCraft 0.8.1
/** Turn a job into an adjustment: stations with coordinates + known backsights are control; every other shot target is free. x = easting, y = northing. */
export function toObservations(job, { sdDir = (5 / 3600) * D2R, sdDist = 0.005, sdCtrl = 0 } = {}) {
const pts = {}, obs = []; const red = reduce(job);
const known = (id) => job.points[id];
for (const [id, p] of Object.entries(job.points)) pts[id] = { x: p.e, y: p.n, fixed: true, ...(sdCtrl ? { sx: sdCtrl, sy: sdCtrl } : {}) };
const free = new Set();
job.setups.forEach((s, k) => {
if (!pts[s.station]) return;
if (s.backsight?.id && pts[s.backsight.id] && s.backsight.hz !== null) obs.push({ type: 'dir', at: s.station, to: s.backsight.id, value: s.backsight.hz, sd: sdDir, set: k });
for (const sh of s.shots) {
const r = red.shots.find((q) => q.station === s.station && q.id === sh.id); if (!sh.id || !r) continue;
if (!pts[sh.id]) { pts[sh.id] = { x: r.e, y: r.n }; free.add(sh.id); }
obs.push({ type: 'dir', at: s.station, to: sh.id, value: sh.hz, sd: sdDir, set: k });
obs.push({ type: 'dist', from: s.station, to: sh.id, value: r.hd, sd: sdDist });
}
});
void known;
return { points: pts, obs, free: [...free] };
}
In detail
Method
Polar reduction from each setup: orientation from the backsight, horizontal distance = slope distance × sin(zenith angle), height difference = slope distance × cos(zenith angle) + instrument height − target height.
Number types
64-bit numbers. Angles are held in radians.
Tolerances
Dotted degrees-minutes-seconds: the fractional part is rounded to 6 decimals, with 1e-7 added before the minutes are split off. Default standard deviations handed to the adjustment: 5 seconds of arc for directions and 0.005 plan units for distances.
Limits
The file header says the readers were tested on files written from the formats' public descriptions and that no real instrument file has been through them yet. The reduce function takes a scale factor only; curvature and refraction are not modeled.
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.