Functional Weave
Code in TypeScript

agri.field-area

Field area in square metres, hectares and acres from a boundary polygon of latitude/longitude points.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 14 tests, run in TypeScript, Python and Rust.

What it does

The area of a field from its boundary: the corners as latitude/longitude points (`GeoPoint`, from `geo.point-in-polygon`), in order, walked either way. The answer is in square metres, hectares and acres. A field near Cambridge with corners 0.0009° of latitude and 0.0014° of longitude apart is 9548 m², 0.9548 ha, 2.3595 acres.

## Method

For example

  • fieldArea(boundary ×4) → square metres 9,548, hectares 0.955, acres 2.36 a field near Cambridge, about 100 m by 95 m, is just under a hectare
  • fieldArea(boundary ×4) → square metres 9,548, hectares 0.955, acres 2.36 the same field walked clockwise has the same area
  • fieldArea(boundary ×5) → square metres 9,548, hectares 0.955, acres 2.36 closing the ring with the first point again changes nothing

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

export function fieldArea(boundary: readonly GeoPoint[]): FieldArea
boundaryGeoPoint[]the field's corners in order, either direction; closing the ring (last = first) is optional
returnsFieldArea

The type it declares, generated into your project

/** The enclosed area on a sphere of radius 6,371,008.8 m, three ways. */
export interface FieldArea {
  /** to the whole square metre */
  readonly squareMetres: number;
  /** to 0.0001 ha (1 m²) */
  readonly hectares: number;
  /** international acres (4046.8564224 m²), to 0.0001 */
  readonly acres: number;
}

Your code names it in one line, in the file that uses it

import { fieldArea } from "#fune/agri.field-area@^1";
impl/typescript.ts · 60 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { type GeoPoint } from "./geo_point_in_polygon_types.ts";
import { sinCos } from "./math_sin_cos.ts";  ← from math.sin-cos ^1.0.0 · built alongside by fune
import { roundFloat } from "./math_round_float.ts";  ← from math.round-float ^1.0.0 · built alongside by fune
import { type FieldArea } from "./agri_field_area_types.ts";

const RADIUS = 6371008.8;
const RADIANS = 3.141592653589793 / 180;
const ACRE = 4046.8564224;

/**
 * Area enclosed by a boundary, by the shoelace formula in the Lambert
 * cylindrical equal-area projection (x = R lng, y = R sin lat), which is the
 * spherical polygon formula of Chamberlain and Duquette (2007).
 *
 * Longitudes are unwrapped from the first point and sines are taken relative
 * to the first point's, which keeps a one-hectare field accurate when the
 * coordinates are large; sin comes from math.sin-cos so every language adds
 * the same doubles in the same order.
 */
export function fieldArea(boundary: readonly GeoPoint[]): FieldArea {
  boundary.forEach((p, i) => {
    if (typeof p.lat !== "number" || !Number.isFinite(p.lat) || p.lat < -90 || p.lat > 90) {
      throw new RangeError(`point ${i + 1}: latitude must be a finite number from -90 to 90 degrees`);
    }
    if (typeof p.lng !== "number" || !Number.isFinite(p.lng) || p.lng < -180 || p.lng > 180) {
      throw new RangeError(`point ${i + 1}: longitude must be a finite number from -180 to 180 degrees`);
    }
  });
  let ring = boundary;
  if (ring.length > 1 && ring[0].lat === ring[ring.length - 1].lat && ring[0].lng === ring[ring.length - 1].lng) {
    ring = ring.slice(0, -1);
  }
  const distinct = new Set(ring.map((p) => `${p.lat},${p.lng}`));
  if (distinct.size < 3) throw new RangeError("a boundary needs at least 3 distinct points");

  const n = ring.length;
  const x: number[] = [];
  const y: number[] = [];
  const sin0 = sinCos(ring[0].lat * RADIANS).sin;
  let previous = 0;
  for (let i = 0; i < n; i++) {
    let d = ring[i].lng - ring[0].lng;
    // Take the short way round, so a field across the antimeridian works.
    while (d - previous > 180) d -= 360;
    while (d - previous < -180) d += 360;
    previous = d;
    x.push(d * RADIANS);
    y.push(sinCos(ring[i].lat * RADIANS).sin - sin0);
  }
  let sum = 0;
  for (let i = 0; i < n; i++) {
    sum += (x[(i + 1) % n] - x[(i + n - 1) % n]) * y[i];
  }
  const area = (Math.abs(sum) * RADIUS * RADIUS) / 2;
  return {
    squareMetres: roundFloat(area, 0),
    hectares: roundFloat(area / 10000, 4),
    acres: roundFloat(area / ACRE, 4),
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add agri.field-area
Download for TypeScript agri.field-area-1.0.0-typescript.fune · 14,251 bytes sha256 a24951f9a691a8dca110fa154a12e29e1869d492506a9bb2785dcd6c1b2b6844

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./agri.field-area-1.0.0-typescript.fune, or fetch it from a terminal with fune pull agri.field-area@1.0.0:typescript.

The whole function, every language, is one file too: agri.field-area-1.0.0.fune, 20,067 bytes, sha256 2e2dc272023c9eff0d5dfb894cadecee64c19cb251b22efe036999e3cac3a1e7. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before agri.field-area

after — your function gets the result and the arguments, and returns the final result.

// fune: after agri.field-area

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace geo.point-in-polygon in agri.field-area
// fune: replace math.round-float in agri.field-area
// fune: replace math.sin-cos in agri.field-area

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show agri.field-area --steps.

// fune: step agri.field-area after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
a field near Cambridge, about 100 m by 95 m, is just under a hectare boundary ×4 → square metres 9,548, hectares 0.955, acres 2.36
the same field walked clockwise has the same area boundary ×4 → square metres 9,548, hectares 0.955, acres 2.36
closing the ring with the first point again changes nothing boundary ×5 → square metres 9,548, hectares 0.955, acres 2.36
a triangular field in Oxfordshire boundary ×3 → square metres 153,931, hectares 15.393, acres 38.037
an L-shaped field with six corners boundary ×6 → square metres 33,484, hectares 3.348, acres 8.274
a field across the antimeridian (Chatham Islands side) is measured the short way round boundary ×4 → square metres 9,332, hectares 0.933, acres 2.306
the same shape at longitude 0 has the same area boundary ×4 → square metres 9,332, hectares 0.933, acres 2.306
a one-degree square on the equator is R² x (pi/180) x sin 1° boundary ×4 → square metres 12,363,718,145, hectares 1,236,371.815, acres 3,055,141.289
points in a straight line enclose nothing boundary ×3 → square metres 0, hectares 0, acres 0
two points are not a boundary boundary ×2 → error: a boundary needs at least 3 distinct points
Show the other 4 tests
CaseArgumentsExpected
three points with a repeat are not a boundary boundary ×3 → error: a boundary needs at least 3 distinct points
an empty boundary is an error → error: a boundary needs at least 3 distinct points
a latitude over 90 is an error (latitude and longitude swapped?) boundary ×3 → error: point 2: latitude must be a finite number from -90 to 90 degrees
a longitude over 180 is an error boundary ×3 → error: point 2: longitude must be a finite number from -180 to 180 degrees

More from the author

The Earth is a sphere of radius 6,371,008.8 m, the IUGG mean radius that `geo.distance` also uses. The boundary is projected with the Lambert cylindrical equal-area projection (x = R x longitude in radians, y = R x sin latitude), which preserves area, and the enclosed area is the shoelace formula there:

A = R² / 2 x | sum over i of (lng[i+1] - lng[i-1]) x sin(lat[i]) |

This is the spherical polygon area formula of Chamberlain and Duquette (2007), used by many web mapping libraries. The edges are straight lines in that projection rather than great circles; for anything the size of a field the difference is far below the uncertainty of the corner positions.

**Accuracy.** A sphere is not the Earth: against the WGS 84 ellipsoid a spherical area is typically out by a few tenths of a percent (it depends on latitude), so a 10 ha field can be a few hundredths of a hectare out. That is fine for planning inputs and checking a sketch; claims for payment (Rural Payments, IACS) must use the official land parcel area, and survey work needs an ellipsoidal method.

## Determinism

Sines come from `math.sin-cos` (the platform maths libraries differ in the last bit between languages). Longitudes are taken relative to the first point and sines relative to the first point's sine, which keeps precision for small fields far from the origin, and every sum is formed in the same order in all three languages, so the unrounded area is the same double. Each result is then rounded half away from zero with `math.round-float`: square metres to 0 places, hectares and acres to 4 (acres are international acres of 4046.8564224 m²).

## Edge cases

- Closing the ring (repeating the first point last) is optional. - Direction does not matter; the absolute value is taken. - A field crossing the 180° meridian is measured the short way round: successive longitudes are unwrapped to within 180° of each other. - Collinear points enclose 0. - A self-intersecting boundary (a figure of eight) is not detected; its "area" is the difference of its loops. Holes are not supported: subtract them yourself. - Fewer than 3 distinct points, or a coordinate outside ±90 / ±180 (usually swapped latitude and longitude), is an error.

## Source

R. G. Chamberlain and W. H. Duquette, "Some Algorithms for Polygons on a Sphere", JPL Publication 07-03, Jet Propulsion Laboratory (2007), https://trs.jpl.nasa.gov/handle/2014/41271. Earth radius: H. Moritz, "Geodetic Reference System 1980", Journal of Geodesy 74 (2000) 128-133.

Files

PathBytes
README.md2,887
impl/python.py2,455
impl/rust.rs3,168
impl/typescript.ts2,439
vectors.json6,034