Skip to content

Instantly share code, notes, and snippets.

@joeskeen
Last active November 1, 2024 14:13
Show Gist options
  • Save joeskeen/ea5aff21ca3d67da12c133ee66e1f5c0 to your computer and use it in GitHub Desktop.
Save joeskeen/ea5aff21ca3d67da12c133ee66e1f5c0 to your computer and use it in GitHub Desktop.
Calculating distance between two global coordinates (latitude, longitude)
/*
* Based on 'distance-from' NPM package: https://github.com/rickyplouis/distance-from
* refactored to:
* - make the Distance object immutable
* - remove superfluous distance options
* - make types more succinct
* - improve amount of validation for positions
*/
export type Position = [latitude: number, longitude: number];
export type Units = "km" | "m" | "cm" | "mi" | "ft" | "in" | "yd";
export function validatePosition(position: Position) {
if (
!Array.isArray(position) ||
position.length !== 2 ||
position.some((p) => typeof p !== "number") ||
position[0] < -90 ||
position[0] > 90 ||
position[1] < -180 ||
position[1] > 180
) {
throw new Error(
"Must use array of [lat, lng] as valid numbers for position"
);
}
}
// This implementation originally appeared at http://www.movable-type.co.uk/scripts/latlong.html
// Courtesy of @chrisveness
export function distanceInKm(pos1: Position, pos2: Position): number {
const [lat1, lon1] = pos1;
const [lat2, lon2] = pos2;
// A = sin²(Δφ/2) + cos(φ1)⋅cos(φ2)⋅sin²(Δλ/2)
// δ = 2·atan2(√(a), √(1−a))
// see mathforum.org/library/drmath/view/51879.html for derivation
const sine = (num: number) => Math.sin(num / 2);
const cos = (num: number) => Math.cos(num);
const radius = 6371; // Earth's radius in kilometers. Note that as Earth is not a perfect sphere, this is an approximation.
const φ1 = degreeToRadians(lat1);
const λ1 = degreeToRadians(lon1);
const φ2 = degreeToRadians(lat2);
const λ2 = degreeToRadians(lon2);
const Δφ = φ2 - φ1;
const Δλ = λ2 - λ1;
const a = sine(Δφ) * sine(Δφ) + cos(φ1) * cos(φ2) * Math.pow(sine(Δλ), 2);
return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * radius;
}
export function degreeToRadians(degrees = 0) {
// Math.PI / 180
if (isNaN(degrees)) {
throw new Error("Must input valid number for degrees");
}
return degrees * 0.017453292519943295;
}
export const conversionCoefficients = {
km: 1,
m: 1000,
cm: 100000,
mi: 0.6213712,
ft: 3280.84,
in: 39370.1,
yd: 1093.61,
}
export class DistanceBetween {
public readonly distance: number;
get from(): Position {
return [...this.origin];
}
get to(): Position | undefined {
return [...this.destination];
}
constructor(
private readonly origin: Position,
private readonly destination: Position
) {
validatePosition(origin);
validatePosition(destination);
this.distance = distanceInKm(origin, destination);
}
in(units: Units): number {
if (!Object.keys(conversionCoefficients).includes(units)) {
throw new Error(`Need to use valid units (${Object.keys(conversionCoefficients).join(", ")})`);
}
return this.distance * conversionCoefficients[units];
}
}
export default function distanceBetween(
origin: Position,
destination: Position
) {
return new DistanceBetween(origin, destination);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment