Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9962a6932d | |||
| fff8a8382b | |||
| 195c84106e |
3 changed files with 64 additions and 13 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1590,7 +1590,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rs_maps"
|
||||
version = "0.3.1"
|
||||
version = "0.3.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Cargo.toml
|
||||
[package]
|
||||
name = "rs_maps"
|
||||
version = "0.3.1"
|
||||
version = "0.3.4"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -26,7 +26,20 @@ const NOMINATIM = 'https://nominatim.openstreetmap.org/search';
|
|||
// Nominatim's search endpoint doesn't return object tags, so opening hours,
|
||||
// phone and website need a second lookup by OSM id. Overpass is donated
|
||||
// infrastructure like Nominatim: fine on an explicit click, not per result.
|
||||
const OVERPASS = 'https://overpass-api.de/api/interpreter';
|
||||
//
|
||||
// Tried in order. The main overpass-api.de instance is by far the busiest and
|
||||
// returns 504 under load even for a trivial single-object query, so the mirrors
|
||||
// are a practical necessity rather than belt and braces.
|
||||
const OVERPASS_ENDPOINTS = [
|
||||
'https://overpass.kumi.systems/api/interpreter',
|
||||
'https://overpass-api.de/api/interpreter',
|
||||
'https://overpass.private.coffee/api/interpreter',
|
||||
];
|
||||
|
||||
// Overpass queues requests when busy — a trivial single-object lookup has been
|
||||
// observed taking over a minute before returning a perfectly good 200. 30s is
|
||||
// the most that's reasonable to make someone wait; past that, give up quietly.
|
||||
const OVERPASS_TIMEOUT_MS = 30000;
|
||||
|
||||
const COLOURS = ['#4E9C6B', '#D2467F', '#E2A93C', '#4E8FC9', '#B07BD4', '#D3574B'];
|
||||
const DEFAULT_COLOUR = COLOURS[0];
|
||||
|
|
@ -693,16 +706,40 @@ async function fetchOsmTags(osmType, osmId) {
|
|||
const short = OSM_SHORT[osmType];
|
||||
if (!short) return null;
|
||||
|
||||
const query = `[out:json][timeout:20];${short}(${osmId});out tags;`;
|
||||
const res = await fetch(OVERPASS, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'data=' + encodeURIComponent(query),
|
||||
});
|
||||
if (!res.ok) throw new Error('overpass ' + res.status);
|
||||
// Server-side budget matched to ours, so it sheds the request rather than
|
||||
// holding it in a queue we've already stopped waiting on.
|
||||
const query = `[out:json][timeout:30];${short}(${osmId});out tags;`;
|
||||
let lastError = null;
|
||||
|
||||
const body = await res.json();
|
||||
return (body.elements && body.elements[0] && body.elements[0].tags) || {};
|
||||
for (const endpoint of OVERPASS_ENDPOINTS) {
|
||||
// AbortController rather than relying on the server: a hung connection
|
||||
// would otherwise block the fallback for as long as the browser allows.
|
||||
const abort = new AbortController();
|
||||
const timer = setTimeout(() => abort.abort(), OVERPASS_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'data=' + encodeURIComponent(query),
|
||||
signal: abort.signal,
|
||||
});
|
||||
|
||||
// 429 and 504 mean this mirror is busy, not that the object is missing —
|
||||
// worth asking someone else.
|
||||
if (!res.ok) throw new Error('overpass ' + res.status);
|
||||
|
||||
const body = await res.json();
|
||||
return (body.elements && body.elements[0] && body.elements[0].tags) || {};
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
console.warn('overpass mirror failed:', endpoint, err.message);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('no overpass endpoint responded');
|
||||
}
|
||||
|
||||
/* Renders whatever came back. Deliberately raw values — no "open now", no
|
||||
|
|
@ -873,9 +910,11 @@ function showSearchHit(hit) {
|
|||
slot.replaceChildren(node);
|
||||
more.remove();
|
||||
} catch {
|
||||
// Overpass being busy is routine and not worth interrupting anyone
|
||||
// over. The button simply comes back so it can be pressed again;
|
||||
// the reason stays in the console.
|
||||
more.disabled = false;
|
||||
more.textContent = 'More details';
|
||||
toast('Could not load details right now', true);
|
||||
}
|
||||
popup.update(); // re-measure after the content grew
|
||||
});
|
||||
|
|
@ -1071,6 +1110,18 @@ async function loadGpxList() {
|
|||
edit.addEventListener('click', () => editRoute(f.name));
|
||||
li.appendChild(edit);
|
||||
|
||||
// A plain anchor rather than a fetch-and-blob: same-origin navigation
|
||||
// carries the session cookie, and the browser handles the save dialog,
|
||||
// resumability and large files without any of it passing through JS.
|
||||
const download = document.createElement('a');
|
||||
download.className = 'link';
|
||||
download.textContent = 'Download';
|
||||
download.href = 'api/gpx/file/' + encodeURIComponent(f.name);
|
||||
// Without an explicit filename the browser would name it after the URL's
|
||||
// last segment, which is percent-encoded.
|
||||
download.download = f.name;
|
||||
li.appendChild(download);
|
||||
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue