Functional Weave
Code in Rust

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

  • field_area(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
  • field_area(boundary ×4) → square metres 9,548, hectares 0.955, acres 2.36 the same field walked clockwise has the same area
  • field_area(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.

pub fn field_area(boundary: &[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.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FieldArea {
    /// to the whole square metre
    pub square_metres: f64,
    /// to 0.0001 ha (1 m²)
    pub hectares: f64,
    /// international acres (4046.8564224 m²), to 0.0001
    pub acres: f64,
}

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

fune!(agri.field-area@^1);  // then call field_area(…)
impl/rust.rs · 85 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::geo_point_in_polygon::{geo_point_from_value, GeoPoint};  ← from geo.point-in-polygon ^1.0.0 · built alongside by fune
use super::math_round_float::round_float;  ← from math.round-float ^1.0.0 · built alongside by fune
use super::math_sin_cos::sin_cos;  ← from math.sin-cos ^1.0.0 · built alongside by fune

const RADIUS: f64 = 6371008.8;
const RADIANS: f64 = 3.141592653589793 / 180.0;
const ACRE: f64 = 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.
///
/// # Panics
/// Panics on a coordinate out of range or fewer than 3 distinct points.
pub fn field_area(boundary: &[GeoPoint]) -> FieldArea {
    for (i, p) in boundary.iter().enumerate() {
        if !p.lat.is_finite() || p.lat < -90.0 || p.lat > 90.0 {
            panic!("point {}: latitude must be a finite number from -90 to 90 degrees", i + 1);
        }
        if !p.lng.is_finite() || p.lng < -180.0 || p.lng > 180.0 {
            panic!("point {}: longitude must be a finite number from -180 to 180 degrees", i + 1);
        }
    }
    let mut ring: &[GeoPoint] = boundary;
    if ring.len() > 1 && ring[0].lat == ring[ring.len() - 1].lat && ring[0].lng == ring[ring.len() - 1].lng {
        ring = &ring[..ring.len() - 1];
    }
    let mut distinct: Vec<(f64, f64)> = Vec::new();
    for p in ring {
        if !distinct.iter().any(|&(a, b)| a == p.lat && b == p.lng) {
            distinct.push((p.lat, p.lng));
        }
    }
    if distinct.len() < 3 {
        panic!("a boundary needs at least 3 distinct points");
    }

    let n = ring.len();
    let mut x: Vec<f64> = Vec::with_capacity(n);
    let mut y: Vec<f64> = Vec::with_capacity(n);
    let sin0 = sin_cos(ring[0].lat * RADIANS).sin;
    let mut previous = 0.0;
    for p in ring {
        let mut d = p.lng - ring[0].lng;
        // Take the short way round, so a field across the antimeridian works.
        while d - previous > 180.0 {
            d -= 360.0;
        }
        while d - previous < -180.0 {
            d += 360.0;
        }
        previous = d;
        x.push(d * RADIANS);
        y.push(sin_cos(p.lat * RADIANS).sin - sin0);
    }
    let mut total = 0.0;
    for i in 0..n {
        total += (x[(i + 1) % n] - x[(i + n - 1) % n]) * y[i];
    }
    let area = total.abs() * RADIUS * RADIUS / 2.0;
    FieldArea {
        square_metres: round_float(area, 0),
        hectares: round_float(area / 10000.0, 4),
        acres: round_float(area / ACRE, 4),
    }
}

pub fn field_area_to_value(f: &FieldArea) -> Value {
    Value::obj(vec![
        ("squareMetres", Value::Float(f.square_metres)),
        ("hectares", Value::Float(f.hectares)),
        ("acres", Value::Float(f.acres)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let boundary: Vec<GeoPoint> = args[0].as_arr().iter().map(geo_point_from_value).collect();
    field_area_to_value(&field_area(&boundary))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add agri.field-area
Download for Rust agri.field-area-1.0.0-rust.fune · 14,991 bytes sha256 a4f28343a5f145cd27e5c3141de2b7331afb481c751dc7a4730d94e45d6bfcb1

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

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