Resources Numerical accuracy Calculation policy

Parcels: deed calls, closure, area and legal description

Reads bearings and distances, runs deed calls into corners, reports the misclosure and precision, and works out a parcel's courses, area, perimeter, labels and legal description.

How the precision is kept

Bearings, courses and curve data are closed form in 64-bit. A traverse adds each leg to the corner before it, so corners carry the rounding of every earlier leg and of the size of the coordinates. The area is the shoelace sum on absolute coordinates plus an exact circular segment for each curve. Labels and the legal description round for the text only.

Open item. Run the traverse and the area sum relative to the point of beginning, so legs and products are not rounded at the size of the absolute coordinates.

Calculations under this policy

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

parcels.js CivilCraft 0.8.1 64-bit 11 calculations Reviewed

Tests bin/test-civilcraft-parcels.mjs

Angle from degrees, minutes and seconds

64-bit

Reads an angle typed as degrees, minutes and seconds or as decimal degrees.

degrees = d + m ÷ 60 + s ÷ 3600

Three terms added in 64-bit. Minutes and seconds of 60 or more are refused, and degrees and minutes must be whole numbers when a smaller part follows.

  • parcels.js
  • lines 42 to 49
  • CivilCraft 0.8.1
/** Degrees from 45°30'15", 45-30-15, 45 30 15 or 45.5 (minutes and seconds under 60) | null. */
export function parseAngle(x) {
  const n = (String(x ?? '').match(/\d+(?:\.\d+)?/g) || []).map(Number);
  if (!n.length || n.length > 3) return null;
  const [d, m = 0, sec = 0] = n;
  if (m >= 60 || sec >= 60 || (n.length > 1 && !Number.isInteger(d)) || (n.length > 2 && !Number.isInteger(m))) return null;
  return d + m / 60 + sec / 3600;
}

Bearing to direction

64-bit

Turns a quadrant bearing or an azimuth into a direction in radians from east, counter-clockwise.

direction = (90° − azimuth) × π ÷ 180, wrapped to 0…2π

The azimuth is formed from the quadrant by whole-number additions and subtractions, then converted with Math.PI in one expression and wrapped with the remainder operator.

  • parcels.js
  • lines 50 to 61
  • CivilCraft 0.8.1
export function parseBearing(s) {
  const str = String(s ?? '').trim().toUpperCase().replace(/[’′]/g, "'").replace(/[”″]/g, '"').replace(/º/g, '°');
  if (!str || str.length > 60) return null;
  const az = /^AZ(?:IMUTH)?\s*(.+)$/.exec(str);
  if (az) { const a = parseAngle(az[1]); return a === null || a > 360 ? null : wrap(((90 - a) * Math.PI) / 180); }
  const m = /^([NS])\s*(.*?)\s*([EW])$/.exec(str);
  if (!m) return null;
  const a = parseAngle(m[2]);
  if (a === null || a > 90) return null;
  const azd = m[1] === 'N' ? (m[3] === 'E' ? a : 360 - a) : m[3] === 'E' ? 180 - a : 180 + a;
  return wrap(((90 - azd) * Math.PI) / 180);
}

Central angle of a curve call

64-bit

The central angle of a deed curve from its arc length, or as typed.

Δ = L ÷ R

When an arc length is given it overrides a typed delta. The angle must be above 0 and below 2π.

  • parcels.js
  • lines 82 to 83
  • CivilCraft 0.8.1
if (L > 0) delta = L / R;
if (!(delta > 0 && delta < TAU)) { errors.push({ line: n, msg: t('pc.err.delta') }); return; }

Traverse and misclosure

64-bit

The corners reached by the calls, the gap back to the start, the total length and the precision ratio.

p ← p + d·(cos θ, sin θ); chord = 2R·sin(Δ ÷ 2); misclosure = |end − start|; precision = length ÷ misclosure

Each leg is added to the absolute coordinates of the corner before it. With a start point in the millions each addition is rounded to about 2e-10, so the misclosure is resolved to roughly that times the number of calls, not to 15 digits of the misclosure itself. The precision ratio is infinite when the misclosure is 1e-9 or less.

  • parcels.js
  • lines 99 to 119
  • CivilCraft 0.8.1
export function runCalls(start, calls) {
  const pts = [{ x: start.x, y: start.y }];
  let p = { ...start }, heading = null, length = 0;
  for (const c of calls) {
    if (c.kind === 'line') {
      p = { x: p.x + c.dist * Math.cos(c.dir), y: p.y + c.dist * Math.sin(c.dir) };
      heading = c.dir; length += c.dist;
      pts.push({ ...p });
      continue;
    }
    const chordDir = c.chordDir ?? (heading === null ? 0 : heading + (c.turn * c.delta) / 2);
    const chord = 2 * c.R * Math.sin(c.delta / 2);
    pts[pts.length - 1].b = c.turn * Math.tan(c.delta / 4);
    p = { x: p.x + chord * Math.cos(chordDir), y: p.y + chord * Math.sin(chordDir) };
    heading = chordDir + (c.turn * c.delta) / 2;
    length += c.R * c.delta;
    pts.push({ ...p });
  }
  const dx = p.x - start.x, dy = p.y - start.y, dist = Math.hypot(dx, dy);
  return { pts, misclosure: { dx, dy, dist, dir: Math.atan2(dy, dx) }, length, precision: dist > 1e-9 ? length / dist : Infinity };
}

Curve from a bulge

64-bit

The radius, length, central angle, chord, tangent directions and center of a curved course.

Δ = 4·atan|b|; R = chord ÷ (2·sin(Δ ÷ 2)); L = R × Δ; center = start + R·(cos, sin)(dir_in ± 90°)

Closed form from the two corners and the bulge. For a very flat curve sin(Δ ÷ 2) is small and R is large; R × Δ still returns a length close to the chord.

  • parcels.js
  • lines 121 to 128
  • CivilCraft 0.8.1
// ------------------------------------------------------------------ courses, area
function arcOf(a, b, bulge) {
  const chord = Math.hypot(b.x - a.x, b.y - a.y), delta = 4 * Math.atan(Math.abs(bulge)), turn = bulge > 0 ? 1 : -1;
  const R = chord / (2 * Math.sin(delta / 2)), chordDir = Math.atan2(b.y - a.y, b.x - a.x);
  const dirIn = chordDir - (turn * delta) / 2, dirOut = chordDir + (turn * delta) / 2;
  const toC = dirIn + (turn * Math.PI) / 2, c = { x: a.x + R * Math.cos(toC), y: a.y + R * Math.sin(toC) };
  return { R, L: R * delta, delta, turn, chordDir, chord, dirIn, dirOut, c };
}

Courses of a parcel

64-bit

Each side of the parcel as a line (direction and distance) or a curve.

direction = atan2(Δy, Δx); distance = hypot(Δx, Δy)

Directions and distances come from differences of neighboring corners, so they keep their digits at large coordinates.

  • parcels.js
  • lines 129 to 137
  • CivilCraft 0.8.1
export function courses(pts) {
  const out = [];
  for (let i = 0; i < pts.length; i++) {
    const a = pts[i], b = pts[(i + 1) % pts.length];
    if (a.b) out.push({ kind: 'curve', from: a, to: b, ...arcOf(a, b, a.b) });
    else out.push({ kind: 'line', from: a, to: b, dir: Math.atan2(b.y - a.y, b.x - a.x), dist: Math.hypot(b.x - a.x, b.y - a.y) });
  }
  return out;
}

Parcel area

64-bit

The area of the parcel, with curved sides counted exactly.

2A = Σ (x_i·y_j − x_j·y_i) + Σ ± R²·(Δ − sin Δ); area = |2A| ÷ 2

The polygon part multiplies absolute coordinates with no shift to a local origin, so large coordinates cost digits. Each curve adds its circular segment in closed form; for a flat curve Δ − sin Δ subtracts nearly equal numbers, and the segment concerned is small.

  • parcels.js
  • lines 138 to 146
  • CivilCraft 0.8.1
export function parcelArea(pts) {
  let a = 0;
  for (let i = 0; i < pts.length; i++) {
    const p = pts[i], q = pts[(i + 1) % pts.length];
    a += p.x * q.y - q.x * p.y;
    if (p.b) { const c = arcOf(p, q, p.b); a += 2 * Math.sign(p.b) * ((c.R * c.R) / 2) * (c.delta - Math.sin(c.delta)); }
  }
  return Math.abs(a / 2);
}

Perimeter

64-bit

The length round the parcel.

perimeter = Σ line distances + Σ curve lengths

A plain sum of the course lengths in order.

  • parcels.js
  • line 147
  • CivilCraft 0.8.1
export const perimeter = (pts) => courses(pts).reduce((m, c) => m + (c.kind === 'line' ? c.dist : c.L), 0);

Area in words

64-bit

The area as square units with acres or hectares.

acres = sq ft ÷ 43560; hectares = sq m ÷ 10000

Both divisors are exact. The text shows whole square units, acres to 3 decimals and hectares to 4; the rounding is for the text only.

  • parcels.js
  • lines 151 to 155
  • CivilCraft 0.8.1
export function areaWords(a, units) {
  return units === 'm'
    ? t('pc.area.m', { a: grouped(a, 0), ha: fixed(a / 10000, 4) })
    : t('pc.area.ft', { a: grouped(a, 0), ac: fixed(a / 43560, 3) });
}

Course and area labels

64-bit

The bearing and distance text along each side and the area text at the middle.

Distances, radii and lengths are shown with 2 decimals, the area as whole square units with acres to 3 decimals or hectares to 4. The label values are computed from the 64-bit courses each time; nothing rounded is stored.

  • parcels.js
  • lines 189 to 195
  • CivilCraft 0.8.1
  const text = c.kind === 'line' ? bearing(c.dir) + '  ' + len(c.dist) : 'R=' + len(c.R) + '  L=' + len(c.L) + '  Delta=' + dms((c.delta * 180) / Math.PI);   // no Δ: the PDF's text is WinAnsi
  items.push({ layer: 'V-PROP-TEXT', kind: 'text', x: mid.x + Math.cos(out) * off, y: mid.y + Math.sin(out) * off, rot, mm: 1.6, align: 'center', middle: true, text });
}
const ctr = centroid(pts), a = parcelArea(pts);
items.push({ layer: 'V-PROP-TEXT', kind: 'text', x: ctr.x, y: ctr.y + 1.6 * unitPerMm, rot: 0, mm: 2.2, align: 'center', middle: true, text: parcel.name, mask: true });
items.push({ layer: 'V-PROP-TEXT', kind: 'text', x: ctr.x, y: ctr.y - 1.6 * unitPerMm, rot: 0, mm: 1.8, align: 'center', middle: true,
  text: grouped(a, 0) + ' ' + U.area + '  ' + (ft ? fixed(a / 43560, 3) + ' ac' : fixed(a / 10000, 4) + ' ha'), mask: true });
parcels-ui.js CivilCraft 0.8.1 64-bit 2 calculations Reviewed

Tests None found by the review

Bulge from a typed radius

64-bit

Turns a radius typed for one side of a parcel into the stored curve.

bulge = ± tan(asin(chord ÷ 2R) ÷ 2)

The radius is raised to at least half the chord before it is used, so the asin argument does not exceed 1; a radius up to 1e-9 short of half the chord is accepted and treated as a semicircle. The box shows the radius with 2 decimals, but the stored curve changes only when a value is typed.

  • parcels-ui.js
  • lines 56 to 62
  • CivilCraft 0.8.1
const setR = (R, dir) => edit(p.id, t('pc.undo.curve'), (q) => { if (R === null) delete q.pts[i].b; else q.pts[i].b = dir * Math.tan(Math.asin(chord / (2 * R)) / 2); });
const rIn = ctx.numInput(c.kind === 'curve' ? fixed(c.R, 2).replace(/,/g, '') : '', (v) => {
  const s = String(v).trim();
  if (s === '') { setR(null); return true; }
  const R = Number(s);
  if (!(R >= chord / 2 - 1e-9)) return false;   // a radius shorter than half the chord cannot reach
  setR(Math.max(R, chord / 2), Number(turn.value)); return true;

Does the traverse close

64-bit

Decides whether typed deed calls return to the start, in which case the last point is dropped and the parcel closes on its first corner.

closes when misclosure ≤ 0.0005 × max(1, length ÷ 1000)

The tolerance is 0.0005 project units for traverses up to 1000 units long and 1 part in 2,000,000 of the length beyond that. A traverse that closes within it is stored closed, without the remaining gap.

  • parcels-ui.js
  • lines 129 to 130
  • CivilCraft 0.8.1
const closes = run.misclosure.dist <= 0.0005 * Math.max(1, run.length / 1000);
const pts = closes ? run.pts.slice(0, -1) : run.pts;

In detail

Method
Traverse by bearing and distance; curve by chord = 2R·sin(Δ ÷ 2) and bulge = tan(Δ ÷ 4); area = shoelace + Σ ± R² ÷ 2 × (Δ − sin Δ).
Number types
64-bit numbers.
Tolerances
Precision is reported when the misclosure is over 1e-9. In the dialog a traverse closes when the misclosure is at most 0.0005 × max(1, length ÷ 1000). A typed radius may be 1e-9 short of half the chord.
Limits
Curved courses are at most a semicircle (|bulge| ≤ 1). Text values: distances 2 decimals, coordinates 3, acres 3, hectares 4.

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