Files
location-mqtt-tracker-app/lib/geo-utils.ts
2025-11-24 20:33:15 +00:00

112 lines
3.0 KiB
TypeScript

/**
* 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<string> {
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<string> {
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<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}