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);
}