jagd-apps/stoeberhunde/backend/utils/geocode.js

143 lines
3.6 KiB
JavaScript

const https = require('https');
const fs = require('fs').promises;
const path = require('path');
const config = require('../config/env');
const logger = require('./logger');
const CACHE_FILE = path.join(__dirname, '..', 'geocode-cache.json');
const CACHE_SAVE_INTERVAL = 60000; // Save every 60 seconds
const cache = new Map();
let lastRequestTime = 0;
let cacheModified = false;
// Load cache from file on startup
const loadCache = async () => {
try {
const data = await fs.readFile(CACHE_FILE, 'utf8');
const parsed = JSON.parse(data);
Object.entries(parsed).forEach(([key, value]) => {
cache.set(key, value);
});
logger.info(`Geocoding cache loaded: ${cache.size} entries`);
} catch (error) {
if (error.code !== 'ENOENT') {
logger.warn('Failed to load geocoding cache:', error.message);
}
}
};
// Save cache to file
const saveCache = async () => {
if (!cacheModified) return;
try {
const obj = Object.fromEntries(cache);
await fs.writeFile(CACHE_FILE, JSON.stringify(obj, null, 2));
cacheModified = false;
logger.info(`Geocoding cache saved: ${cache.size} entries`);
} catch (error) {
logger.error('Failed to save geocoding cache:', error.message);
}
};
// Periodically save cache
setInterval(saveCache, CACHE_SAVE_INTERVAL).unref();
// Save on process exit
process.on('SIGINT', async () => {
await saveCache();
process.exit(0);
});
process.on('SIGTERM', async () => {
await saveCache();
process.exit(0);
});
// Initialize cache loading
loadCache().catch(err => logger.error('Cache initialization error:', err));
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const fetchJson = (url, headers) => new Promise((resolve, reject) => {
const req = https.get(url, { headers }, (res) => {
let data = '';
res.on('data', chunk => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode && res.statusCode >= 400) {
return reject(new Error(`Geocode HTTP ${res.statusCode}`));
}
try {
resolve(JSON.parse(data));
} catch (error) {
reject(error);
}
});
});
req.on('error', reject);
req.end();
});
const geocodeAddress = async (address) => {
const normalized = (address || '').trim();
if (!normalized) {
return null;
}
const cacheKey = normalized.toLowerCase();
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const elapsed = Date.now() - lastRequestTime;
if (elapsed < config.geocodeMinDelayMs) {
await sleep(config.geocodeMinDelayMs - elapsed);
}
const url = `${config.geocodeUrl}?format=json&limit=1&countrycodes=de&q=${encodeURIComponent(normalized)}`;
const headers = {
'User-Agent': config.geocodeUserAgent
};
try {
const results = await fetchJson(url, headers);
lastRequestTime = Date.now();
if (!Array.isArray(results) || results.length === 0) {
cache.set(cacheKey, null);
cacheModified = true;
return null;
}
const hit = results[0];
const lat = parseFloat(hit.lat);
const lng = parseFloat(hit.lon);
if (Number.isNaN(lat) || Number.isNaN(lng)) {
cache.set(cacheKey, null);
cacheModified = true;
return null;
}
const coords = { lat, lng };
if (cache.size >= 1000) { cache.delete(cache.keys().next().value); }
cache.set(cacheKey, coords);
cacheModified = true;
return coords;
} catch (error) {
logger.warn('Geocoding fehlgeschlagen', {
address: normalized,
error: error.message
});
lastRequestTime = Date.now();
return null;
}
};
module.exports = {
geocodeAddress
};