freealberta/map/app/services/geocoding.js

232 lines
7.1 KiB
JavaScript

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<Object>} Geocoding result
*/
async function reverseGeocode(lat, lng) {
// Create cache key
const cacheKey = `${lat.toFixed(6)},${lng.toFixed(6)}`;
// 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<Object>} 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
};