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.
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.
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.
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.
A draft description: the point of beginning, each course, and the area.
Distances, radii and curve lengths are written with 2 decimals and the point of beginning's coordinates with 3; bearings and central angles are formatted by bearing and dms in units.js. The courses written are therefore rounded and need not close exactly on paper.
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.
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.
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.