Functional Weave
Code in Rust

geo.distance@2.0.0

impl/typescript.ts

2,952 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 { atan } from "./math_atan.ts";  ← from math.atan ^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 { sinCos } from "./math_sin_cos.ts";  ← from math.sin-cos ^1.0.0 · built alongside by fune

/**
 * Great-circle distance between two points, by the haversine formula, on a
 * sphere of the IUGG mean Earth radius, in metres rounded to millimetres.
 *
 * Sine, cosine and arctangent come from math.sin-cos and math.atan, not
 * Math.sin or Math.atan2: those come from each platform's maths library, which
 * may differ in the last bit between JavaScript, Python and Rust, and a value
 * next to a rounding boundary can then go either way. With the shared helpers
 * the unrounded distance is the same double in every language, and
 * math.round-float rounds that double the same way everywhere.
 */

// IUGG mean radius R1 = (2a + b) / 3 of the GRS 80 ellipsoid (Moritz,
// "Geodetic Reference System 1980", Journal of Geodesy 74 (2000) 128-133).
const EARTH_RADIUS_METRES = 6371008.8;

const PI = 3.141592653589793;
const HALF_PI = PI / 2;
const DEGREES = PI / 180;

// atan2 for y >= 0 and x >= 0, which is all haversine needs. x is 0 only when
// the points are exactly antipodal (a = 1); y / 0 would be infinite, which
// math.atan refuses.
function atan2Positive(y: number, x: number): number {
  if (x === 0) return y === 0 ? 0 : HALF_PI;
  return atan(y / x);
}

function checkLatitude(value: number): void {
  if (typeof value !== "number" || !Number.isFinite(value)) {
    throw new TypeError("latitude must be a finite number of degrees");
  }
  if (value < -90 || value > 90) throw new RangeError("latitude must be between -90 and 90 degrees");
}

function checkLongitude(value: number): void {
  if (typeof value !== "number" || !Number.isFinite(value)) {
    throw new TypeError("longitude must be a finite number of degrees");
  }
  if (value < -180 || value > 180) throw new RangeError("longitude must be between -180 and 180 degrees");
}

export function distance(fromLat: number, fromLng: number, toLat: number, toLng: number): number {
  checkLatitude(fromLat);
  checkLongitude(fromLng);
  checkLatitude(toLat);
  checkLongitude(toLng);

  // A longitude difference of 359 degrees is a 1 degree step across the
  // antimeridian; sin^2 of the half-angle is the same either way, so no
  // wrapping is needed.
  const sinHalfLat = sinCos(((toLat - fromLat) * DEGREES) / 2).sin;
  const sinHalfLng = sinCos(((toLng - fromLng) * DEGREES) / 2).sin;
  const cos1 = sinCos(fromLat * DEGREES).cos;
  const cos2 = sinCos(toLat * DEGREES).cos;

  let a = sinHalfLat * sinHalfLat + cos1 * cos2 * sinHalfLng * sinHalfLng;
  // Rounding can push a a hair outside [0, 1] for antipodal points.
  if (a < 0) a = 0;
  if (a > 1) a = 1;
  // atan2 rather than asin(sqrt(a)): stays accurate for antipodal points too.
  const c = 2 * atan2Positive(Math.sqrt(a), Math.sqrt(1 - a));
  // Half away from zero on the exact value of the double (math.round-float).
  return roundFloat(EARTH_RADIUS_METRES * c, 3);
}