@cdeshpande/geo-utils
Version:
A lightweight, blazing-fast TypeScript library for calculating distances (Haversine) and geospatial math with support for kilometers and miles.
151 lines (145 loc) • 5.8 kB
JavaScript
// src/validator.ts
function validateCoordinates(lat, lon, label) {
if (typeof lat !== "number" || typeof lon !== "number") {
throw new TypeError(`${label} latitude and longitude must be numbers.`);
}
if (lat < -90 || lat > 90) {
throw new RangeError(`${label} latitude must be between -90 and 90 degrees.`);
}
if (lon < -180 || lon > 180) {
throw new RangeError(`${label} longitude must be between -180 and 180 degrees.`);
}
}
function validateUnit(unit) {
if (!["km", "miles"].includes(unit)) {
throw new Error(`Invalid unit '${unit}'. Valid options are 'km' or 'miles'.`);
}
}
// src/haversine.ts
var RADIUS = {
km: 6371,
miles: 3958.8
};
function haversineDistance(lat1, lon1, lat2, lon2, unit = "km") {
validateCoordinates(lat1, lon1, "Point 1");
validateCoordinates(lat2, lon2, "Point 2");
validateUnit(unit);
const toRad = (value) => value * Math.PI / 180;
const R = RADIUS[unit];
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a2 = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a2), Math.sqrt(1 - a2));
}
// src/midpoint.ts
function midpoint(lat1, lon1, lat2, lon2) {
validateCoordinates(lat1, lon1, "Point 1");
validateCoordinates(lat2, lon2, "Point 2");
const toRad = (deg) => deg * Math.PI / 180;
const toDeg = (rad) => rad * 180 / Math.PI;
const dLon = toRad(lon2 - lon1);
const \u03C61 = toRad(lat1);
const \u03C62 = toRad(lat2);
const \u03BB1 = toRad(lon1);
const Bx = Math.cos(\u03C62) * Math.cos(dLon);
const By = Math.cos(\u03C62) * Math.sin(dLon);
const \u03C63 = Math.atan2(
Math.sin(\u03C61) + Math.sin(\u03C62),
Math.sqrt((Math.cos(\u03C61) + Bx) ** 2 + By ** 2)
);
const \u03BB3 = \u03BB1 + Math.atan2(By, Math.cos(\u03C61) + Bx);
return {
latitude: parseFloat(toDeg(\u03C63).toFixed(6)),
longitude: parseFloat(toDeg(\u03BB3).toFixed(6))
};
}
// src/vincenty.ts
var a = 6378137;
var f = 1 / 298.257223563;
var b = (1 - f) * a;
function vincentyDistance(lat1, lon1, lat2, lon2) {
validateCoordinates(lat1, lon1, "Point 1");
validateCoordinates(lat2, lon2, "Point 2");
const toRad = (deg) => deg * Math.PI / 180;
const \u03C61 = toRad(lat1);
const \u03C62 = toRad(lat2);
const L = toRad(lon2 - lon1);
const U1 = Math.atan((1 - f) * Math.tan(\u03C61));
const U2 = Math.atan((1 - f) * Math.tan(\u03C62));
const sinU1 = Math.sin(U1), cosU1 = Math.cos(U1);
const sinU2 = Math.sin(U2), cosU2 = Math.cos(U2);
let \u03BB = L;
let \u03BBP, iterLimit = 100;
let cosSqAlpha, sinSigma, cos2SigmaM, cosSigma, sigma;
do {
const sinLambda = Math.sin(\u03BB);
const cosLambda = Math.cos(\u03BB);
sinSigma = Math.sqrt((cosU2 * sinLambda) ** 2 + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) ** 2);
if (sinSigma === 0) return 0;
cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;
sigma = Math.atan2(sinSigma, cosSigma);
const sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;
cosSqAlpha = 1 - sinAlpha ** 2;
cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha;
if (isNaN(cos2SigmaM)) cos2SigmaM = 0;
const C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha));
\u03BBP = \u03BB;
\u03BB = L + (1 - C) * f * sinAlpha * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM ** 2)));
} while (Math.abs(\u03BB - \u03BBP) > 1e-12 && --iterLimit > 0);
if (iterLimit === 0) throw new Error("Vincenty formula failed to converge");
const uSq = cosSqAlpha * ((a ** 2 - b ** 2) / b ** 2);
const A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
const B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
const deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * (-1 + 2 * cos2SigmaM ** 2) - B / 6 * cos2SigmaM * (-3 + 4 * sinSigma ** 2) * (-3 + 4 * cos2SigmaM ** 2)));
const s = b * A * (sigma - deltaSigma);
return s;
}
// src/bearing.ts
function initialBearing(lat1, lon1, lat2, lon2) {
validateCoordinates(lat1, lon1, "Point 1");
validateCoordinates(lat2, lon2, "Point 2");
const toRad = (deg) => deg * Math.PI / 180;
const toDeg = (rad) => rad * 180 / Math.PI;
const \u03C61 = toRad(lat1);
const \u03C62 = toRad(lat2);
const \u0394\u03BB = toRad(lon2 - lon1);
const y = Math.sin(\u0394\u03BB) * Math.cos(\u03C62);
const x = Math.cos(\u03C61) * Math.sin(\u03C62) - Math.sin(\u03C61) * Math.cos(\u03C62) * Math.cos(\u0394\u03BB);
const \u03B8 = Math.atan2(y, x);
const bearing = (toDeg(\u03B8) + 360) % 360;
return parseFloat(bearing.toFixed(6));
}
// src/destination.ts
var RADIUS2 = {
km: 6371,
miles: 3958.8
};
function destinationPoint(lat, lon, distance, bearing, unit = "km") {
validateCoordinates(lat, lon, "Start Point");
validateUnit(unit);
if (typeof distance !== "number" || distance < 0) {
throw new TypeError("Distance must be a non-negative number.");
}
if (typeof bearing !== "number") {
throw new TypeError("Bearing must be a number.");
}
const R = RADIUS2[unit];
const \u03B4 = distance / R;
const \u03B8 = bearing * Math.PI / 180;
const \u03C61 = lat * Math.PI / 180;
const \u03BB1 = lon * Math.PI / 180;
const \u03C62 = Math.asin(Math.sin(\u03C61) * Math.cos(\u03B4) + Math.cos(\u03C61) * Math.sin(\u03B4) * Math.cos(\u03B8));
const \u03BB2 = \u03BB1 + Math.atan2(Math.sin(\u03B8) * Math.sin(\u03B4) * Math.cos(\u03C61), Math.cos(\u03B4) - Math.sin(\u03C61) * Math.sin(\u03C62));
return {
latitude: +(\u03C62 * (180 / Math.PI)).toFixed(6),
longitude: +((\u03BB2 * 180 / Math.PI + 540) % 360 - 180).toFixed(6)
// normalize
};
}
export {
destinationPoint,
haversineDistance,
initialBearing,
midpoint,
vincentyDistance
};