112 lines
3.0 KiB
TypeScript
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));
|
|
}
|
|
}
|