add user accounts and OSM peak catalog with zoom-based notability
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env node
|
||||
// Import all named OSM peaks in the Alpine region into the Streba database.
|
||||
//
|
||||
// node scripts/import-peaks.js [--db data/streba.db]
|
||||
//
|
||||
// Fetches natural=peak nodes from the Overpass API, scores them for
|
||||
// notability, assigns per-zoom visibility (minzoom), upgrades the built-in
|
||||
// seed peaks to their OSM counterparts (keeping everyone's ascents), and
|
||||
// recomputes visibility across the whole catalog. Idempotent - safe to
|
||||
// re-run to refresh the data.
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
|
||||
import { assignMinzoom, scorePeak } from '../src/lib/server/peak-score.js';
|
||||
|
||||
// honor HTTP(S)_PROXY/NO_PROXY env vars (no-op when unset)
|
||||
setGlobalDispatcher(new EnvHttpProxyAgent());
|
||||
|
||||
const OVERPASS_URL = 'https://overpass-api.de/api/interpreter';
|
||||
// the Alps, generously: Nice to Vienna
|
||||
const BBOX = '43.0,4.7,48.7,16.8';
|
||||
const QUERY = `[out:json][timeout:600];node["natural"="peak"]["name"](${BBOX});out body;`;
|
||||
|
||||
const dbPath = process.argv.includes('--db')
|
||||
? process.argv[process.argv.indexOf('--db') + 1]
|
||||
: (process.env.STREBA_DATA_DIR ?? 'data') + '/streba.db';
|
||||
|
||||
function parseEle(raw) {
|
||||
if (raw === undefined) return null;
|
||||
const ele = parseFloat(String(raw).replace(',', '.'));
|
||||
return Number.isFinite(ele) && ele > 0 && ele < 5000 ? ele : null;
|
||||
}
|
||||
|
||||
console.log(`Fetching peaks from Overpass (bbox ${BBOX}) - this can take a few minutes…`);
|
||||
const response = await fetch(OVERPASS_URL, {
|
||||
method: 'POST',
|
||||
body: 'data=' + encodeURIComponent(QUERY),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'streba-peak-import/1.0 (self-hosted GPX analyser)'
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error(`Overpass request failed: ${response.status} ${response.statusText}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const osm = await response.json();
|
||||
console.log(`Received ${osm.elements.length} peaks.`);
|
||||
|
||||
const peaks = osm.elements
|
||||
.filter((el) => el.type === 'node' && el.tags?.name)
|
||||
.map((el) => ({
|
||||
osm_id: `node:${el.id}`,
|
||||
name: el.tags.name,
|
||||
elevation_m: parseEle(el.tags.ele),
|
||||
lat: el.lat,
|
||||
lon: el.lon,
|
||||
wikipedia: el.tags.wikipedia ?? null,
|
||||
wikidata: el.tags.wikidata ?? null,
|
||||
prominence_m: parseEle(el.tags.prominence),
|
||||
score: 0,
|
||||
minzoom: 14
|
||||
}));
|
||||
for (const p of peaks) p.score = scorePeak(p);
|
||||
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
const hasPeaksTable = db
|
||||
.prepare(`SELECT count(*) n FROM sqlite_master WHERE type = 'table' AND name = 'peaks'`)
|
||||
.get().n;
|
||||
if (!hasPeaksTable) {
|
||||
console.error(`No peaks table in ${dbPath} - start the app once first so it creates the schema.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const upsert = db.prepare(`
|
||||
INSERT INTO peaks (osm_id, name, elevation_m, lat, lon, wikipedia, wikidata, score, minzoom)
|
||||
VALUES (@osm_id, @name, @elevation_m, @lat, @lon, @wikipedia, @wikidata, @score, @minzoom)
|
||||
ON CONFLICT (osm_id) DO UPDATE SET
|
||||
name = excluded.name, elevation_m = excluded.elevation_m,
|
||||
lat = excluded.lat, lon = excluded.lon,
|
||||
wikipedia = excluded.wikipedia, wikidata = excluded.wikidata,
|
||||
score = excluded.score
|
||||
`);
|
||||
|
||||
db.transaction(() => {
|
||||
for (const p of peaks) upsert.run(p);
|
||||
|
||||
// upgrade curated seed rows to their OSM counterpart, keeping ascents.
|
||||
// OSM names are often multilingual ("Mont Blanc / Monte Bianco"), so match
|
||||
// by name containment first, then by plain summit proximity.
|
||||
const seeds = db.prepare(`SELECT * FROM peaks WHERE osm_id LIKE 'seed:%'`).all();
|
||||
const candidatesFor = db.prepare(
|
||||
`SELECT id, name, lat, lon, elevation_m FROM peaks
|
||||
WHERE osm_id LIKE 'node:%' AND abs(lat - ?) < 0.01 AND abs(lon - ?) < 0.015`
|
||||
);
|
||||
let upgraded = 0;
|
||||
for (const seed of seeds) {
|
||||
const candidates = candidatesFor.all(seed.lat, seed.lon);
|
||||
const seedName = seed.name.toLowerCase();
|
||||
const dist = (c) => Math.hypot(c.lat - seed.lat, (c.lon - seed.lon) * 0.7);
|
||||
const byName = candidates
|
||||
.filter((c) => {
|
||||
const n = c.name.toLowerCase();
|
||||
return n.includes(seedName) || seedName.includes(n);
|
||||
})
|
||||
.sort((a, b) => dist(a) - dist(b))[0];
|
||||
const byProximity = candidates
|
||||
.filter(
|
||||
(c) =>
|
||||
dist(c) < 0.004 &&
|
||||
(c.elevation_m == null || Math.abs(c.elevation_m - seed.elevation_m) < 200)
|
||||
)
|
||||
.sort((a, b) => dist(a) - dist(b))[0];
|
||||
const match = byName ?? byProximity;
|
||||
if (match) {
|
||||
db.prepare('UPDATE OR IGNORE ascents SET peak_id = ? WHERE peak_id = ?').run(match.id, seed.id);
|
||||
db.prepare('DELETE FROM peaks WHERE id = ?').run(seed.id);
|
||||
upgraded++;
|
||||
}
|
||||
}
|
||||
console.log(`Upgraded ${upgraded}/${seeds.length} seed peaks to OSM nodes.`);
|
||||
|
||||
// recompute zoom visibility across the merged catalog
|
||||
const all = db.prepare('SELECT id, lat, lon, score FROM peaks').all();
|
||||
assignMinzoom(all);
|
||||
const setZoom = db.prepare('UPDATE peaks SET minzoom = ? WHERE id = ?');
|
||||
for (const p of all) setZoom.run(p.minzoom, p.id);
|
||||
})();
|
||||
|
||||
const total = db.prepare('SELECT count(*) n FROM peaks').get().n;
|
||||
const byZoom = db
|
||||
.prepare('SELECT minzoom, count(*) n FROM peaks GROUP BY minzoom ORDER BY minzoom')
|
||||
.all();
|
||||
console.log(`Catalog now holds ${total} peaks.`);
|
||||
console.log('Visible from zoom:', byZoom.map((r) => `${r.minzoom}: ${r.n}`).join(', '));
|
||||
Reference in New Issue
Block a user