/** * Geographic utility functions. * * All calculations use the Haversine formula and operate on * WGS-84 (standard GPS) coordinates. */ const EARTH_RADIUS_KM = 6371; /** * Calculate the straight-line distance between two points using the * Haversine formula. * * @returns Distance in kilometers */ export function calculateDistance( lat1: number, lng1: number, lat2: number, lng2: number, ): number { const toRad = (deg: number) => (deg * Math.PI) / 180; const dLat = toRad(lat2 - lat1); const dLng = toRad(lng2 - lng1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return EARTH_RADIUS_KM * c; } /** * Calculate the initial bearing (forward azimuth) from point 1 to point 2. * * @returns Bearing in degrees (0–360), where 0/360 = North, 90 = East, etc. */ export function calculateBearing( lat1: number, lng1: number, lat2: number, lng2: number, ): number { const toRad = (deg: number) => (deg * Math.PI) / 180; const toDeg = (rad: number) => (rad * 180) / Math.PI; const dLng = toRad(lng2 - lng1); const y = Math.sin(dLng) * Math.cos(toRad(lat2)); const x = Math.cos(toRad(lat1)) * Math.sin(toRad(lat2)) - Math.sin(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.cos(dLng); const bearing = toDeg(Math.atan2(y, x)); return (bearing + 360) % 360; } /** * Get a simple compass direction label from a bearing in degrees. */ export function bearingToCompass(bearing: number): string { const dirs = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']; const index = Math.round(bearing / 45) % 8; return dirs[index]; } /** * Calculate the geographic midpoint between two coordinates. */ export function midpoint( lat1: number, lng1: number, lat2: number, lng2: number, ): { lat: number; lng: number } { const toRad = (deg: number) => (deg * Math.PI) / 180; const toDeg = (rad: number) => (rad * 180) / Math.PI; const dLng = toRad(lng2 - lng1); const lat1Rad = toRad(lat1); const lat2Rad = toRad(lat2); const lng1Rad = toRad(lng1); const bx = Math.cos(lat2Rad) * Math.cos(dLng); const by = Math.cos(lat2Rad) * Math.sin(dLng); const lat = toDeg( Math.atan2( Math.sin(lat1Rad) + Math.sin(lat2Rad), Math.sqrt( (Math.cos(lat1Rad) + bx) * (Math.cos(lat1Rad) + bx) + by * by, ), ), ); const lng = toDeg(lng1Rad + Math.atan2(by, Math.cos(lat1Rad) + bx)); return { lat, lng }; } /** * Check if a point is within a given radius of another point. */ export function isWithinRadius( lat1: number, lng1: number, lat2: number, lng2: number, radiusKm: number, ): boolean { return calculateDistance(lat1, lng1, lat2, lng2) <= radiusKm; } /** * Calculate approximate bounding box around a center point. * * @param lat - Center latitude * @param lng - Center longitude * @param radiusKm - Radius in kilometers * @returns Bounding box { north, south, east, west } */ export function boundingBox( lat: number, lng: number, radiusKm: number, ): { north: number; south: number; east: number; west: number } { const toRad = (deg: number) => (deg * Math.PI) / 180; const toDeg = (rad: number) => (rad * 180) / Math.PI; const latDelta = toDeg(radiusKm / EARTH_RADIUS_KM); const lngDelta = toDeg( radiusKm / (EARTH_RADIUS_KM * Math.cos(toRad(lat))), ); return { north: lat + latDelta, south: lat - latDelta, east: lng + lngDelta, west: lng - lngDelta, }; }