UNPKG

@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
// 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 };