geodesy-fn
Version:
Geodesic functions for GeoJSON, based on geodesy
163 lines (136 loc) • 6.65 kB
text/typescript
import { DEFAULT_RADIUS, equals, toRadians, toDegrees } from './common.js';
import { wrap360 } from './dms.js';
/**
* Returns the distance along the surface of the earth from start point to destination point.
*
* Uses haversine formula: a = sin²(Δφ/2) + cosφ1·cosφ2 · sin²(Δλ/2); d = 2 · atan2(√a, √(a-1)).
*
* @param {GeoJSON.Position} start - Longitude/latitude of start point.
* @param {GeoJSON.Position} destination - Longitude/latitude of destination point.
* @param {number} [radius] - Radius of earth (defaults to mean radius in metres).
* @returns {number} Distance between start point and destination point, in same units as radius.
*
* @example
* const p1 = [0.119, 52.205];
* const p2 = [2.351, 48.857];
* const d = distance(p1, p2); // 404.3×10³ m
* const m = distanceTo(p1, p2, 3959); // 251.2 miles
*/
export function distance(start: GeoJSON.Position, destination: GeoJSON.Position, radius: number = DEFAULT_RADIUS): number {
// 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 R = radius;
const φ1 = toRadians(start[1]), λ1 = toRadians(start[0]);
const φ2 = toRadians(destination[1]), λ2 = toRadians(destination[0]);
const Δφ = φ2 - φ1;
const Δλ = λ2 - λ1;
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const d = R * c;
return d;
}
/**
* Returns the initial bearing from start point to destination point.
*
* @param {GeoJSON.Position} start - Longitude/latitude of start point.
* @param {GeoJSON.Position} destination - Longitude/latitude of destination point.
* @returns {number} Initial bearing in degrees from north (0°..360°).
*
* @example
* const p1 = [0.119, 52.205];
* const p2 = [2.351, 48.857];
* const b1 = initialBearing(p1, p2); // 156.2°
*/
export function initialBearing(start: GeoJSON.Position, destination: GeoJSON.Position): number {
if (equals(start, destination)) return NaN; // coincident points
// tanθ = sinΔλ⋅cosφ2 / cosφ1⋅sinφ2 − sinφ1⋅cosφ2⋅cosΔλ
// see mathforum.org/library/drmath/view/55417.html for derivation
const φ1 = toRadians(start[1]);
const φ2 = toRadians(destination[1]);
const Δλ = toRadians(destination[0] - start[0]);
const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
const y = Math.sin(Δλ) * Math.cos(φ2);
const θ = Math.atan2(y, x);
const bearing = toDegrees(θ);
return wrap360(bearing);
}
/**
* Returns final bearing arriving at destination point from ‘this’ point; the final bearing will
* differ from the initial bearing by varying degrees according to distance and latitude.
*
* @param {GeoJSON.Position} start - Longitude/latitude of start point.
* @param {GeoJSON.Position} destination - Longitude/latitude of destination point.
* @returns {number} Final bearing in degrees from north (0°..360°).
*
* @example
* const p1 = [0.119, 52.205];
* const p2 = [2.351, 48.857];
* const b2 = finalBearing(p1, p2); // 157.9°
*/
export function finalBearing(start: GeoJSON.Position, destination: GeoJSON.Position): number {
// get initial bearing from destination point to this point & reverse it by adding 180°
const bearing = initialBearing(destination, start) + 180;
return wrap360(bearing);
}
/**
* Returns the midpoint between start point and destination point.
*
* @param {GeoJSON.Position} start - Longitude/latitude of start point.
* @param {GeoJSON.Position} destination - Longitude/latitude of destination point.
* @returns {GeoJSON.Position} Midpoint between this point and destination point.
*
* @example
* const p1 = [0.119, 52.205];
* const p2 = [2.351, 48.857];
* const pMid = midpoint(p1, p2); // [1.2746, 50.5363]
*/
export function midpoint(start: GeoJSON.Position, destination: GeoJSON.Position): GeoJSON.Position {
// φm = atan2( sinφ1 + sinφ2, √( (cosφ1 + cosφ2⋅cosΔλ)² + cos²φ2⋅sin²Δλ ) )
// λm = λ1 + atan2(cosφ2⋅sinΔλ, cosφ1 + cosφ2⋅cosΔλ)
// midpoint is sum of vectors to two points: mathforum.org/library/drmath/view/51822.html
const φ1 = toRadians(start[1]);
const λ1 = toRadians(start[0])
const φ2 = toRadians(destination[1]);
const Δλ = toRadians(destination[0] - start[0]);
// get cartesian coordinates for the two points
const A = { x: Math.cos(φ1), y: 0, z: Math.sin(φ1) }; // place point A on prime meridian y=0
const B = { x: Math.cos(φ2)*Math.cos(Δλ), y: Math.cos(φ2)*Math.sin(Δλ), z: Math.sin(φ2) };
// vector to midpoint is sum of vectors to two points (no need to normalise)
const C = { x: A.x + B.x, y: A.y + B.y, z: A.z + B.z };
const φm = Math.atan2(C.z, Math.sqrt(C.x*C.x + C.y*C.y));
const λm = λ1 + Math.atan2(C.y, C.x);
const lat = toDegrees(φm);
const lon = toDegrees(λm);
return [lon, lat];
}
/**
* Returns the destination point from start point having travelled the given distance on the
* given initial bearing (bearing normally varies around path followed).
*
* @param {GeoJSON.Position} start - Longitude/latitude of start point.
* @param {number} distance - Distance travelled, in same units as earth radius (default: metres).
* @param {number} bearing - Initial bearing in degrees from north.
* @param {number} [radius] - Radius of earth (defaults to mean radius in metres).
* @returns {GeoJSON.Position} Destination point.
*
* @example
* const p1 = [-0.00147, 51.47788];
* const p2 = destinationPoint(p1, 7794, 300.7); // [0.0983, 51.5136]
*/
export function destinationPoint(start: GeoJSON.Position, distance: number, bearing: number, radius: number = DEFAULT_RADIUS): GeoJSON.Position {
// sinφ2 = sinφ1⋅cosδ + cosφ1⋅sinδ⋅cosθ
// tanΔλ = sinθ⋅sinδ⋅cosφ1 / cosδ−sinφ1⋅sinφ2
// see mathforum.org/library/drmath/view/52049.html for derivation
const δ = distance / radius; // angular distance in radians
const θ = toRadians(bearing);
const φ1 = toRadians(start[1]), λ1 = toRadians(start[0]);
const sinφ2 = Math.sin(φ1) * Math.cos(δ) + Math.cos(φ1) * Math.sin(δ) * Math.cos(θ);
const φ2 = Math.asin(sinφ2);
const y = Math.sin(θ) * Math.sin(δ) * Math.cos(φ1);
const x = Math.cos(δ) - Math.sin(φ1) * sinφ2;
const λ2 = λ1 + Math.atan2(y, x);
const lat = toDegrees(φ2);
const lon = toDegrees(λ2);
return [lon, lat];
}