chore: km_berechnen Tool (BEO-Entfernungen/Fahrkosten-Recherche) hinzugefügt
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
.env
|
||||
geocode-cache.json
|
||||
@@ -0,0 +1,57 @@
|
||||
# km_berechnen
|
||||
|
||||
Berechnet die **Straßenentfernung (km)** zwischen der fixen **Sternwarte Welzheim**
|
||||
und dem Wohnort jedes BEOs (Tabelle `beos`) und schreibt das Ergebnis in die
|
||||
vorhandene Spalte `beos.km`.
|
||||
|
||||
Der DB-Zugriff läuft über den **PHP-Proxy** `DB4js_all.php`, da die echte
|
||||
Sternwarte-Datenbank nicht direkt erreichbar ist:
|
||||
|
||||
- Lesen: `cmd = LB_GET_BEO_ADRESSEN` (BEOs mit Adresse + km)
|
||||
- Schreiben: `cmd = LB_UPDATE_BEO_KM` `{ id, km }`
|
||||
|
||||
Distanzberechnung:
|
||||
|
||||
- **Geocoding:** OpenStreetMap [Nominatim](https://nominatim.openstreetmap.org) (kostenlos, ohne API-Key)
|
||||
- **Routing:** öffentlicher [OSRM](https://project-osrm.org)-Server (kostenlos, ohne API-Key)
|
||||
|
||||
## Einrichtung
|
||||
|
||||
1. `.env` öffnen und `PHP_DB_URL` auf den DB4js-Endpunkt setzen
|
||||
(lokal: `http://localhost:8080/DB4js_all.php`). Falls der Proxy Basic-Auth
|
||||
verlangt, zusätzlich `API_USER` / `API_PASS` eintragen.
|
||||
2. Optional den exakten Referenzpunkt der Sternwarte setzen
|
||||
(`STERNWARTE_LAT` / `STERNWARTE_LON`) — sonst wird er per Nominatim ermittelt,
|
||||
mit Ortsmitte Welzheim als Fallback.
|
||||
|
||||
Es werden nur Node-Bordmittel verwendet (globales `fetch`) — kein
|
||||
`npm install` nötig (Node ≥ 18).
|
||||
|
||||
## Ausführen
|
||||
|
||||
```bash
|
||||
cd km_berechnen
|
||||
node --env-file=.env berechne-km.mjs # berechnen und in beos.km schreiben
|
||||
node --env-file=.env berechne-km.mjs --dry-run # nur berechnen/anzeigen, DB unverändert
|
||||
```
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Geocoding-Ergebnisse werden in `geocode-cache.json` zwischengespeichert; ein
|
||||
zweiter Lauf ist dadurch deutlich schneller. Zum Neu-Geocodieren die Datei löschen.
|
||||
- Nominatim erlaubt max. 1 Anfrage/Sekunde — das Skript hält dies automatisch ein.
|
||||
- BEOs ohne (vollständige) Adresse bzw. ohne Geocoding-/Routing-Treffer erhalten
|
||||
`km = NULL` und werden am Ende aufgelistet.
|
||||
- Die öffentlichen Nominatim-/OSRM-Server sind „Fair Use"; für die ~20 BEOs
|
||||
unproblematisch.
|
||||
- Die beiden Proxy-Kommandos (`LB_GET_BEO_ADRESSEN`, `LB_UPDATE_BEO_KM`) sind in
|
||||
`DB4js_all.php` implementiert.
|
||||
|
||||
## Ergebnis prüfen
|
||||
|
||||
Per Proxy:
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$PHP_DB_URL" -H 'Content-Type: application/json' \
|
||||
-d '{"cmd":"LB_GET_BEO_ADRESSEN"}'
|
||||
```
|
||||
@@ -0,0 +1,151 @@
|
||||
<!doctype html>
|
||||
<html lang="de"><head><meta charset="utf-8"><title>BEO-Entfernungen</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 18mm 16mm; }
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: -apple-system, "Helvetica Neue", Arial, sans-serif; color: #1a1a1a; font-size: 11pt; }
|
||||
h1 { font-size: 17pt; margin: 0 0 2mm; }
|
||||
.sub { color: #555; font-size: 10pt; margin: 0 0 5mm; }
|
||||
.intro { font-size: 10pt; line-height: 1.45; margin: 0 0 6mm; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead th { text-align: left; font-size: 9.5pt; text-transform: uppercase; letter-spacing: .04em;
|
||||
color: #444; border-bottom: 1.5pt solid #333; padding: 2mm 2mm; }
|
||||
tbody td { padding: 1.8mm 2mm; border-bottom: 0.4pt solid #ccc; vertical-align: bottom; }
|
||||
tbody tr:nth-child(even) { background: #f6f6f6; }
|
||||
.nr { color: #999; width: 8mm; text-align: right; }
|
||||
.km { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; font-weight: 600; width: 22mm; }
|
||||
th.km { text-align: right; }
|
||||
.foot { margin-top: 7mm; font-size: 8.5pt; color: #777; }
|
||||
</style></head>
|
||||
<body>
|
||||
<h1>Entfernungen der BEOs zur Sternwarte Welzheim</h1>
|
||||
<p class="sub">Einfache Straßenentfernung in Kilometern · Stand: 10. Juli 2026</p>
|
||||
<p class="intro">
|
||||
Die folgende Tabelle listet die einfache Straßenentfernung (Auto) von deinem
|
||||
Wohnort zur Sternwarte Welzheim. Die Werte wurden automatisch über
|
||||
OpenStreetMap / OSRM ermittelt. <strong>Bitte prüfe deinen Wert und melde
|
||||
ihn in jedem Fall zurück</strong> – auch wenn er stimmt.
|
||||
</p>
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th class="nr">#</th><th>Name</th><th>Wohnort</th><th class="km">km</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="nr">1</td>
|
||||
<td>Ralf Bernhard</td>
|
||||
<td>Stuttgart</td>
|
||||
<td class="km">53.0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">2</td>
|
||||
<td>Steffen Brückner</td>
|
||||
<td>Weil im Schönbuch</td>
|
||||
<td class="km">68.6</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">3</td>
|
||||
<td>Hans Dschida</td>
|
||||
<td>Urbach</td>
|
||||
<td class="km">15.3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">4</td>
|
||||
<td>Andrea Ess</td>
|
||||
<td>Remshalden</td>
|
||||
<td class="km">23.8</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">5</td>
|
||||
<td>Ulrich Förnzler</td>
|
||||
<td>Stuttgart</td>
|
||||
<td class="km">39.3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">6</td>
|
||||
<td>Reinhard Fuerst</td>
|
||||
<td>Stuttgart</td>
|
||||
<td class="km">43.3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">7</td>
|
||||
<td>Martin Gertz</td>
|
||||
<td>Weinstadt</td>
|
||||
<td class="km">26.8</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">8</td>
|
||||
<td>Hubert Gräber</td>
|
||||
<td>Remshalden</td>
|
||||
<td class="km">19.1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">9</td>
|
||||
<td>Marc Güssmann</td>
|
||||
<td>Backnang</td>
|
||||
<td class="km">22.3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">10</td>
|
||||
<td>Rudolf Idler</td>
|
||||
<td>Kernen</td>
|
||||
<td class="km">29.3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">11</td>
|
||||
<td>Hans-Ulrich Keller</td>
|
||||
<td>Stuttgart</td>
|
||||
<td class="km">40.8</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">12</td>
|
||||
<td>Olaf Meyer-Hamme</td>
|
||||
<td>Winterbach</td>
|
||||
<td class="km">19.0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">13</td>
|
||||
<td>Xeno Nastos</td>
|
||||
<td>Alfdorf</td>
|
||||
<td class="km">14.7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">14</td>
|
||||
<td>Jörg Nikolaizig</td>
|
||||
<td>Ludwigsburg</td>
|
||||
<td class="km">46.9</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">15</td>
|
||||
<td>Christoph Riedl</td>
|
||||
<td>Aichwald</td>
|
||||
<td class="km">33.7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">16</td>
|
||||
<td>Eva Schneider</td>
|
||||
<td>Welzheim</td>
|
||||
<td class="km">8.1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">17</td>
|
||||
<td>Bernd Schuler</td>
|
||||
<td>Schorndorf</td>
|
||||
<td class="km">14.5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">18</td>
|
||||
<td>Christoph Weishaar</td>
|
||||
<td>Waiblingen-Bittenfeld</td>
|
||||
<td class="km">43.0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="nr">19</td>
|
||||
<td>Matthias Zoller</td>
|
||||
<td>Sindelfingen</td>
|
||||
<td class="km">60.6</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="foot">19 Beobachter · Sternwarte Welzheim · automatisch erzeugt</p>
|
||||
</body></html>
|
||||
Binary file not shown.
@@ -0,0 +1,201 @@
|
||||
// Berechnet die Straßenentfernung (km) zwischen der fixen Sternwarte Welzheim
|
||||
// und dem Wohnort jedes BEOs und schreibt das Ergebnis in die Spalte beos.km.
|
||||
//
|
||||
// Der DB-Zugriff läuft über den PHP-Proxy (DB4js_all.php), da die echte
|
||||
// Sternwarte-Datenbank nicht direkt erreichbar ist:
|
||||
// Lesen: cmd LB_GET_BEO_ADRESSEN
|
||||
// Schreiben: cmd LB_UPDATE_BEO_KM { id, km }
|
||||
//
|
||||
// Geocoding: OpenStreetMap Nominatim | Routing: öffentlicher OSRM-Server.
|
||||
// Beide kostenlos, ohne API-Key. Ausführen mit:
|
||||
// node --env-file=.env berechne-km.mjs
|
||||
//
|
||||
// Optionen:
|
||||
// --dry-run nichts in die DB schreiben, nur berechnen und anzeigen
|
||||
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const CACHE_FILE = join(__dirname, 'geocode-cache.json');
|
||||
const USER_AGENT = 'sternwarte-welzheim-km-berechnen/1.0 (rxf@gmx.de)';
|
||||
const DRY_RUN = process.argv.includes('--dry-run');
|
||||
|
||||
const PHP_DB_URL = process.env.PHP_DB_URL || 'http://localhost:8080/DB4js_all.php';
|
||||
|
||||
// Fallback-Referenzpunkt: Ortsmitte Welzheim (nur Näherung, per .env überschreibbar)
|
||||
const DEFAULT_STERNWARTE = { lat: 48.8756, lon: 9.6377 };
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
// ---- PHP-Proxy (DB4js) ----
|
||||
|
||||
async function callProxy(cmd, params = {}) {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (process.env.API_USER && process.env.API_PASS) {
|
||||
const token = Buffer.from(`${process.env.API_USER}:${process.env.API_PASS}`).toString('base64');
|
||||
headers.Authorization = `Basic ${token}`;
|
||||
}
|
||||
const res = await fetch(PHP_DB_URL, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ cmd, ...params }),
|
||||
});
|
||||
const text = await res.text();
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(text);
|
||||
} catch {
|
||||
throw new Error(`DB4js ${cmd} HTTP ${res.status}: ${text.slice(0, 200)}`);
|
||||
}
|
||||
if (json && typeof json === 'object' && !Array.isArray(json) && 'error' in json) {
|
||||
throw new Error(`DB4js ${cmd}: ${json.error}`);
|
||||
}
|
||||
if (!res.ok) throw new Error(`DB4js ${cmd} HTTP ${res.status}`);
|
||||
return json;
|
||||
}
|
||||
|
||||
// ---- Geocode-Cache (Adresse -> {lat, lon} | null) ----
|
||||
|
||||
async function loadCache() {
|
||||
try {
|
||||
return JSON.parse(await readFile(CACHE_FILE, 'utf8'));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCache(cache) {
|
||||
await writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
// ---- Nominatim Geocoding (max. 1 Anfrage/Sekunde) ----
|
||||
|
||||
async function geocode(query, cache) {
|
||||
if (Object.prototype.hasOwnProperty.call(cache, query)) {
|
||||
return cache[query]; // Treffer (auch gecachte Fehlschläge = null)
|
||||
}
|
||||
const url =
|
||||
'https://nominatim.openstreetmap.org/search?format=json&limit=1&q=' +
|
||||
encodeURIComponent(query);
|
||||
const res = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
|
||||
if (!res.ok) throw new Error(`Nominatim HTTP ${res.status}`);
|
||||
const json = await res.json();
|
||||
const hit = Array.isArray(json) && json[0]
|
||||
? { lat: parseFloat(json[0].lat), lon: parseFloat(json[0].lon) }
|
||||
: null;
|
||||
cache[query] = hit;
|
||||
await saveCache(cache);
|
||||
await sleep(1100); // Rate-Limit einhalten
|
||||
return hit;
|
||||
}
|
||||
|
||||
// ---- OSRM Straßenrouting -> Distanz in km ----
|
||||
|
||||
async function roadKm(from, to) {
|
||||
const url =
|
||||
`https://router.project-osrm.org/route/v1/driving/` +
|
||||
`${from.lon},${from.lat};${to.lon},${to.lat}?overview=false`;
|
||||
const res = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
|
||||
if (!res.ok) throw new Error(`OSRM HTTP ${res.status}`);
|
||||
const json = await res.json();
|
||||
if (json.code !== 'Ok' || !json.routes?.[0]) return null;
|
||||
return Math.round((json.routes[0].distance / 1000) * 10) / 10;
|
||||
}
|
||||
|
||||
function buildAddress(row) {
|
||||
const parts = [row.Adresse, [row.Plz, row.Ort].filter(Boolean).join(' ').trim()]
|
||||
.map((s) => (s || '').trim())
|
||||
.filter(Boolean);
|
||||
return parts.length ? parts.join(', ') + ', Deutschland' : '';
|
||||
}
|
||||
|
||||
async function resolveSternwarte(cache) {
|
||||
const envLat = parseFloat(process.env.STERNWARTE_LAT ?? '');
|
||||
const envLon = parseFloat(process.env.STERNWARTE_LON ?? '');
|
||||
if (Number.isFinite(envLat) && Number.isFinite(envLon)) {
|
||||
return { lat: envLat, lon: envLon, source: '.env' };
|
||||
}
|
||||
const hit = await geocode('Sternwarte, Welzheim, Deutschland', cache);
|
||||
if (hit) return { ...hit, source: 'Nominatim' };
|
||||
return { ...DEFAULT_STERNWARTE, source: 'Fallback (Ortsmitte Welzheim)' };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const cache = await loadCache();
|
||||
|
||||
const ref = await resolveSternwarte(cache);
|
||||
console.log(`Proxy: ${PHP_DB_URL}`);
|
||||
console.log(
|
||||
`Referenzpunkt Sternwarte Welzheim: ${ref.lat}, ${ref.lon} [${ref.source}]\n`
|
||||
);
|
||||
|
||||
const rows = await callProxy('LB_GET_BEO_ADRESSEN');
|
||||
|
||||
const done = [];
|
||||
const skipped = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const address = buildAddress(row);
|
||||
if (!address) {
|
||||
skipped.push({ row, reason: 'keine Adresse' });
|
||||
if (!DRY_RUN) await callProxy('LB_UPDATE_BEO_KM', { id: row.ID, km: null });
|
||||
continue;
|
||||
}
|
||||
|
||||
let km = null;
|
||||
let reason = '';
|
||||
try {
|
||||
const geo = await geocode(address, cache);
|
||||
if (!geo) {
|
||||
reason = 'Geocoding ohne Treffer';
|
||||
} else {
|
||||
km = await roadKm(ref, geo);
|
||||
if (km == null) reason = 'keine Route gefunden';
|
||||
}
|
||||
} catch (err) {
|
||||
reason = err.message;
|
||||
}
|
||||
|
||||
if (!DRY_RUN) await callProxy('LB_UPDATE_BEO_KM', { id: row.ID, km });
|
||||
|
||||
if (km == null) skipped.push({ row, reason });
|
||||
else done.push({ row, km });
|
||||
}
|
||||
|
||||
// ---- Zusammenfassung ----
|
||||
console.log('Berechnete Entfernungen:');
|
||||
console.log('Kürzel Name Ort km');
|
||||
console.log('─'.repeat(66));
|
||||
done
|
||||
.sort((a, b) => a.km - b.km)
|
||||
.forEach(({ row, km }) => {
|
||||
console.log(
|
||||
(row.Kuerzel || '').padEnd(7) +
|
||||
' ' +
|
||||
(row.Name || '').padEnd(24) +
|
||||
' ' +
|
||||
(row.Ort || '').padEnd(24) +
|
||||
' ' +
|
||||
String(km).padStart(6)
|
||||
);
|
||||
});
|
||||
|
||||
if (skipped.length) {
|
||||
console.log('\nÜbersprungen / ohne Ergebnis (km = NULL):');
|
||||
skipped.forEach(({ row, reason }) =>
|
||||
console.log(` ${(row.Kuerzel || '?').padEnd(7)} ${(row.Name || '').padEnd(24)} — ${reason}`)
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`\nFertig. ${done.length} berechnet, ${skipped.length} übersprungen.` +
|
||||
(DRY_RUN ? ' (DRY-RUN: nichts in die DB geschrieben)' : ' In Spalte beos.km gespeichert.')
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Fehler:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
// Erzeugt eine PDF-Tabelle aller BEOs mit Wohnort und Straßenentfernung (km)
|
||||
// zur Sternwarte Welzheim — zum Versand an die BEOs zur Verifizierung.
|
||||
//
|
||||
// Daten kommen über den PHP-Proxy (cmd GET_BEOS). Das HTML wird mit Headless-
|
||||
// Chrome nach PDF gerendert.
|
||||
// node --env-file=.env erzeuge-pdf.mjs
|
||||
//
|
||||
// Ergebnis: beo-entfernungen.html und beo-entfernungen.pdf
|
||||
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const execFileP = promisify(execFile);
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const HTML_FILE = join(__dirname, 'beo-entfernungen.html');
|
||||
const PDF_FILE = join(__dirname, 'beo-entfernungen.pdf');
|
||||
|
||||
const PHP_DB_URL = process.env.PHP_DB_URL || 'http://localhost:8080/DB4js_all.php';
|
||||
const CHROME = process.env.CHROME_PATH ||
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||
|
||||
async function callProxy(cmd, params = {}) {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (process.env.API_USER && process.env.API_PASS) {
|
||||
const token = Buffer.from(`${process.env.API_USER}:${process.env.API_PASS}`).toString('base64');
|
||||
headers.Authorization = `Basic ${token}`;
|
||||
}
|
||||
const res = await fetch(PHP_DB_URL, {
|
||||
method: 'POST', headers, body: JSON.stringify({ cmd, ...params }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json && !Array.isArray(json) && json.error) throw new Error(`DB4js ${cmd}: ${json.error}`);
|
||||
return json;
|
||||
}
|
||||
|
||||
const esc = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
||||
|
||||
function fullName(r) {
|
||||
return [r.vorname, r.name].map((x) => (x || '').trim()).filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function buildHtml(rows) {
|
||||
const stand = new Date().toLocaleDateString('de-DE', { day: '2-digit', month: 'long', year: 'numeric' });
|
||||
const body = rows.map((r, i) => `
|
||||
<tr>
|
||||
<td class="nr">${i + 1}</td>
|
||||
<td>${esc(fullName(r))}</td>
|
||||
<td>${esc(r.ort)}</td>
|
||||
<td class="km">${r.km == null || r.km === '' ? '—' : esc(r.km)}</td>
|
||||
</tr>`).join('');
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="de"><head><meta charset="utf-8"><title>BEO-Entfernungen</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 18mm 16mm; }
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: -apple-system, "Helvetica Neue", Arial, sans-serif; color: #1a1a1a; font-size: 11pt; }
|
||||
h1 { font-size: 17pt; margin: 0 0 2mm; }
|
||||
.sub { color: #555; font-size: 10pt; margin: 0 0 5mm; }
|
||||
.intro { font-size: 10pt; line-height: 1.45; margin: 0 0 6mm; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead th { text-align: left; font-size: 9.5pt; text-transform: uppercase; letter-spacing: .04em;
|
||||
color: #444; border-bottom: 1.5pt solid #333; padding: 2mm 2mm; }
|
||||
tbody td { padding: 1.8mm 2mm; border-bottom: 0.4pt solid #ccc; vertical-align: bottom; }
|
||||
tbody tr:nth-child(even) { background: #f6f6f6; }
|
||||
.nr { color: #999; width: 8mm; text-align: right; }
|
||||
.km { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; font-weight: 600; width: 22mm; }
|
||||
th.km { text-align: right; }
|
||||
.foot { margin-top: 7mm; font-size: 8.5pt; color: #777; }
|
||||
</style></head>
|
||||
<body>
|
||||
<h1>Entfernungen der BEOs zur Sternwarte Welzheim</h1>
|
||||
<p class="sub">Einfache Straßenentfernung in Kilometern · Stand: ${stand}</p>
|
||||
<p class="intro">
|
||||
Die folgende Tabelle listet die einfache Straßenentfernung (Auto) von deinem
|
||||
Wohnort zur Sternwarte Welzheim. Die Werte wurden automatisch über
|
||||
OpenStreetMap / OSRM ermittelt. <strong>Bitte prüfe deinen Wert und melde
|
||||
ihn in jedem Fall zurück</strong> – auch wenn er stimmt.
|
||||
</p>
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th class="nr">#</th><th>Name</th><th>Wohnort</th><th class="km">km</th>
|
||||
</tr></thead>
|
||||
<tbody>${body}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="foot">${rows.length} Beobachter · Sternwarte Welzheim · automatisch erzeugt</p>
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const rows = await callProxy('GET_BEOS', { what: 'id,vorname,name,ort,km' });
|
||||
rows.sort((a, b) => (a.name || '').localeCompare(b.name || '', 'de'));
|
||||
|
||||
const html = buildHtml(rows);
|
||||
await writeFile(HTML_FILE, html, 'utf8');
|
||||
|
||||
await execFileP(CHROME, [
|
||||
'--headless=new',
|
||||
'--disable-gpu',
|
||||
'--no-pdf-header-footer',
|
||||
`--print-to-pdf=${PDF_FILE}`,
|
||||
HTML_FILE,
|
||||
]);
|
||||
|
||||
console.log(`PDF erzeugt: ${PDF_FILE} (${rows.length} BEOs)`);
|
||||
}
|
||||
|
||||
main().catch((err) => { console.error('Fehler:', err.message); process.exit(1); });
|
||||
Reference in New Issue
Block a user