const axios = require('axios'); const winston = require('winston'); // Configure logger const logger = winston.createLogger({ level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.Console({ format: winston.format.simple() }) ] }); // Cache for geocoding results (simple in-memory cache) const geocodeCache = new Map(); const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours // Clean up old cache entries periodically setInterval(() => { const now = Date.now(); for (const [key, value] of geocodeCache.entries()) { if (now - value.timestamp > CACHE_TTL) { geocodeCache.delete(key); } } }, 60 * 60 * 1000); // Run every hour /** * Reverse geocode coordinates to get address * @param {number} lat - Latitude * @param {number} lng - Longitude * @returns {Promise} Geocoding result */ async function reverseGeocode(lat, lng) { // Create cache key - use full precision const cacheKey = `${lat},${lng}`; // Check cache first const cached = geocodeCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { logger.debug(`Geocoding cache hit for ${cacheKey}`); return cached.data; } try { // Add delay to respect Nominatim rate limits (max 1 request per second) await new Promise(resolve => setTimeout(resolve, 1000)); logger.info(`Reverse geocoding: ${lat}, ${lng}`); const response = await axios.get('https://nominatim.openstreetmap.org/reverse', { params: { format: 'json', lat: lat, lon: lng, zoom: 18, addressdetails: 1, 'accept-language': 'en' }, headers: { 'User-Agent': 'NocoDB Map Viewer 1.0 (https://github.com/yourusername/nocodb-map-viewer)' }, timeout: 10000 }); if (response.data.error) { throw new Error(response.data.error); } // Process the response const result = processGeocodeResponse(response.data); // Cache the result geocodeCache.set(cacheKey, { data: result, timestamp: Date.now() }); return result; } catch (error) { logger.error('Reverse geocoding error:', error.message); if (error.response?.status === 429) { throw new Error('Rate limit exceeded. Please try again later.'); } else if (error.code === 'ECONNABORTED') { throw new Error('Geocoding service timeout'); } else { throw new Error('Geocoding service unavailable'); } } } /** * Forward geocode address to get coordinates * @param {string} address - Address to geocode * @returns {Promise} Geocoding result */ async function forwardGeocode(address) { // Create cache key const cacheKey = `addr:${address.toLowerCase()}`; // Check cache first const cached = geocodeCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { logger.debug(`Geocoding cache hit for ${cacheKey}`); return cached.data; } try { // Add delay to respect rate limits await new Promise(resolve => setTimeout(resolve, 1000)); logger.info(`Forward geocoding: ${address}`); const response = await axios.get('https://nominatim.openstreetmap.org/search', { params: { format: 'json', q: address, limit: 1, addressdetails: 1, 'accept-language': 'en' }, headers: { 'User-Agent': 'NocoDB Map Viewer 1.0 (https://github.com/yourusername/nocodb-map-viewer)' }, timeout: 10000 }); if (!response.data || response.data.length === 0) { throw new Error('No results found'); } // Process the first result const result = processGeocodeResponse(response.data[0]); // Cache the result geocodeCache.set(cacheKey, { data: result, timestamp: Date.now() }); return result; } catch (error) { logger.error('Forward geocoding error:', error.message); if (error.response?.status === 429) { throw new Error('Rate limit exceeded. Please try again later.'); } else if (error.code === 'ECONNABORTED') { throw new Error('Geocoding service timeout'); } else { throw new Error('Geocoding service unavailable'); } } } /** * Process geocoding response into standardized format * @param {Object} data - Raw geocoding response * @returns {Object} Processed geocoding data */ function processGeocodeResponse(data) { // Extract address components const addressComponents = { house_number: data.address?.house_number || '', road: data.address?.road || '', suburb: data.address?.suburb || data.address?.neighbourhood || '', city: data.address?.city || data.address?.town || data.address?.village || '', state: data.address?.state || data.address?.province || '', postcode: data.address?.postcode || '', country: data.address?.country || '' }; // Create formatted address string let formattedAddress = ''; if (addressComponents.house_number) formattedAddress += addressComponents.house_number + ' '; if (addressComponents.road) formattedAddress += addressComponents.road + ', '; if (addressComponents.suburb) formattedAddress += addressComponents.suburb + ', '; if (addressComponents.city) formattedAddress += addressComponents.city + ', '; if (addressComponents.state) formattedAddress += addressComponents.state + ' '; if (addressComponents.postcode) formattedAddress += addressComponents.postcode; // Clean up formatting formattedAddress = formattedAddress.trim().replace(/,$/, ''); return { fullAddress: data.display_name || '', formattedAddress: formattedAddress, components: addressComponents, coordinates: { lat: parseFloat(data.lat), lng: parseFloat(data.lon) }, boundingBox: data.boundingbox || null, placeId: data.place_id || null, osmType: data.osm_type || null, osmId: data.osm_id || null }; } /** * Get cache statistics * @returns {Object} Cache statistics */ function getCacheStats() { return { size: geocodeCache.size, maxSize: 1000, // Could be made configurable ttl: CACHE_TTL }; } /** * Clear the geocoding cache */ function clearCache() { geocodeCache.clear(); logger.info('Geocoding cache cleared'); } module.exports = { reverseGeocode, forwardGeocode, getCacheStats, clearCache };