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

142 lines
3.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
// Cache beim Herunterfahren sichern ohne process.exit(): das Beenden gehört
// dem Shutdown-Handler in server.js, der sonst mittendrin abgeschnitten wird.
const flushOnShutdown = () => {
saveCache().catch(err => logger.error('Failed to flush geocoding cache:', err.message));
};
process.on('SIGINT', flushOnShutdown);
process.on('SIGTERM', flushOnShutdown);
// 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
};