Functional Weave
Code in Rust

agri.field-area@1.0.0

impl/typescript.ts

2,439 bytes · the TypeScript implementation · view 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),
  };
}