Files
fredy/lib/services/geocoding/client/nominatimClient.js
Christian Kellner d43c5b3f97 Map View in Fredy :D (#253)
* init map view

* switching off 3d buildings when sattelite view is on

* rename menu items

* upgrading dependencies, adding provider to popups

* adding screenshot for map view

* fixing readme

* next release version
2026-01-12 15:00:36 +01:00

106 lines
2.6 KiB
JavaScript

/*
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
import os from 'os';
import crypto from 'crypto';
import https from 'https';
import fetch from 'node-fetch';
import pThrottle from 'p-throttle';
import logger from '../../logger.js';
const API_URL = 'https://nominatim.openstreetmap.org/search';
const agent = new https.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
});
const throttle = pThrottle({
limit: 1,
interval: 1000,
});
function computeMachineId() {
const hostname = os.hostname() || 'unknown-host';
const nets = os.networkInterfaces?.() || {};
const macs = [];
for (const ifname of Object.keys(nets)) {
for (const addr of nets[ifname] || []) {
if (!addr) continue;
if (addr.internal) continue;
if (addr.mac && addr.mac !== '00:00:00:00:00:00') macs.push(addr.mac);
}
}
macs.sort();
const raw = [hostname, os.platform(), os.arch(), ...macs].join('|');
return crypto.createHash('sha256').update(raw).digest('hex').slice(0, 20);
}
/**
* Nominatim requires a specific User-Agent.
* Since Fredy is self-hosted, we use a unique machine ID to make it specific.
*/
const userAgent = `Fredy-Self-Hosted (${computeMachineId()}; https://github.com/orangecoding/fredy)`;
let last429 = 0;
const PAUSE_DURATION = 3600000; // 1 hour
/**
* Geocodes an address using Nominatim.
*
* @param {string} address - The address to geocode.
* @returns {Promise<{lat: number, lng: number}|null>} The geocoordinates or null if error. {lat: -1, lng: -1} if not found.
*/
async function doGeocode(address) {
if (Date.now() - last429 < PAUSE_DURATION) {
return null;
}
const url = `${API_URL}?q=${encodeURIComponent(address)}&format=json&countrycodes=de`;
try {
const response = await fetch(url, {
agent,
headers: {
'User-Agent': userAgent,
},
});
if (response.status === 429) {
logger.warn('Nominatim rate limit hit. Pausing for 1 hour.');
last429 = Date.now();
return null;
}
if (!response.ok) {
logger.error(`Nominatim API error: ${response.status} ${response.statusText}`);
return null;
}
const data = await response.json();
if (Array.isArray(data) && data.length > 0) {
const result = data[0];
return {
lat: parseFloat(result.lat),
lng: parseFloat(result.lon),
};
}
return { lat: -1, lng: -1 };
} catch (error) {
logger.error('Error during Nominatim geocoding:', error);
return null;
}
}
export const geocode = throttle(doGeocode);
export const isPaused = () => Date.now() - last429 < PAUSE_DURATION;