/** * Geo utilities for distance calculation and geocoding */ /** * Calculate distance between two GPS coordinates using Haversine formula * @param lat1 Latitude of first point * @param lon1 Longitude of first point * @param lat2 Latitude of second point * @param lon2 Longitude of second point * @returns Distance in kilometers */ export function calculateDistance( lat1: number, lon1: number, lat2: number, lon2: number ): number { const R = 6371; // Earth's radius in kilometers const dLat = toRadians(lat2 - lat1); const dLon = toRadians(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const distance = R * c; return distance; } function toRadians(degrees: number): number { return degrees * (Math.PI / 180); } /** * Reverse geocode coordinates to address using Nominatim (OpenStreetMap) * @param lat Latitude * @param lon Longitude * @returns Address string or coordinates if geocoding fails */ export async function reverseGeocode( lat: number, lon: number ): Promise { try { const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}&zoom=18&addressdetails=1`; const response = await fetch(url, { headers: { 'User-Agent': 'GPS-Tracker-App/1.0', // Nominatim requires User-Agent }, }); if (!response.ok) { throw new Error(`Nominatim API error: ${response.status}`); } const data = await response.json(); if (data.error) { return `${lat.toFixed(6)}, ${lon.toFixed(6)}`; } // Build address from components const address = data.address; const parts: string[] = []; if (address.road) parts.push(address.road); if (address.house_number) parts.push(address.house_number); if (address.postcode) parts.push(address.postcode); if (address.city || address.town || address.village) { parts.push(address.city || address.town || address.village); } return parts.length > 0 ? parts.join(', ') : data.display_name; } catch (error) { console.error('Geocoding error:', error); // Fallback to coordinates return `${lat.toFixed(6)}, ${lon.toFixed(6)}`; } } /** * Rate-limited reverse geocoding (max 1 request per second for Nominatim) */ export class RateLimitedGeocoder { private lastRequestTime = 0; private minInterval = 1000; // 1 second between requests async geocode(lat: number, lon: number): Promise { const now = Date.now(); const timeSinceLastRequest = now - this.lastRequestTime; if (timeSinceLastRequest < this.minInterval) { const waitTime = this.minInterval - timeSinceLastRequest; await this.sleep(waitTime); } this.lastRequestTime = Date.now(); return reverseGeocode(lat, lon); } private sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } }