Race 3 Overpass mirrors for speed, show more per-route detail
The single shared overpass-api.de instance varies wildly in load — Promise.any against three independent public mirrors (also CORS-open, verified) means only the slowest one determines wait time, not whichever happens to be hit. Also show the description tag (falls back to it for the title when a route has no name, e.g. many KST route relations), the ref number, and a "View on OpenStreetMap" link — the popup was skipping data that was already in the response for free.
This commit is contained in:
1 parent
6f1577f957
commit
cc3d8ca5f3
1 file changed
+32
-10
+32
-10
@@ -13,7 +13,18 @@ const LAYER_ID = "hiking-trails-line";
|
|||||||
// hits from here. One query per click (not per pan/move like the
|
// hits from here. One query per click (not per pan/move like the
|
||||||
// Wikipedia layer's live refetch), so this stays well within Overpass's
|
// Wikipedia layer's live refetch), so this stays well within Overpass's
|
||||||
// shared rate limits.
|
// shared rate limits.
|
||||||
const OVERPASS_API = "https://overpass-api.de/api/interpreter";
|
//
|
||||||
|
// Three independent public mirrors (also CORS-open, verified), raced with
|
||||||
|
// Promise.any rather than called one at a time — the shared free instances
|
||||||
|
// vary a lot in load from one moment to the next, and a multi-second wait
|
||||||
|
// on a single one was the main complaint. Whichever answers first wins;
|
||||||
|
// the others are simply left to finish and get GC'd, not worth the extra
|
||||||
|
// AbortController plumbing for a single one-off per-click request.
|
||||||
|
const OVERPASS_APIS = [
|
||||||
|
"https://overpass-api.de/api/interpreter",
|
||||||
|
"https://overpass.kumi.systems/api/interpreter",
|
||||||
|
"https://overpass.private.coffee/api/interpreter",
|
||||||
|
];
|
||||||
const CLICK_RADIUS_M = 40;
|
const CLICK_RADIUS_M = 40;
|
||||||
// OSM colour tag values are free text (untrusted) — only used as a CSS
|
// OSM colour tag values are free text (untrusted) — only used as a CSS
|
||||||
// background if they look like an actual color (hex or a plain word),
|
// background if they look like an actual color (hex or a plain word),
|
||||||
@@ -128,14 +139,15 @@ export class HikingTrailsControl {
|
|||||||
// waymarkedtrails.org's own site. `out geom` inlines each member way's
|
// waymarkedtrails.org's own site. `out geom` inlines each member way's
|
||||||
// full coordinate list right on the relation, so no separate stitching
|
// full coordinate list right on the relation, so no separate stitching
|
||||||
// step is needed to get real geometry back.
|
// step is needed to get real geometry back.
|
||||||
|
async function queryOverpass(url, query) {
|
||||||
|
const response = await fetch(url, { method: "POST", body: `data=${encodeURIComponent(query)}` });
|
||||||
|
if (!response.ok) throw new Error(`overpass ${response.status}`);
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchNearbyRoutes(lng, lat) {
|
async function fetchNearbyRoutes(lng, lat) {
|
||||||
const query = `[out:json][timeout:15];way(around:${CLICK_RADIUS_M},${lat},${lng})->.w;rel(bw.w)["route"="hiking"];out geom;`;
|
const query = `[out:json][timeout:15];way(around:${CLICK_RADIUS_M},${lat},${lng})->.w;rel(bw.w)["route"="hiking"];out geom;`;
|
||||||
const response = await fetch(OVERPASS_API, {
|
const data = await Promise.any(OVERPASS_APIS.map((url) => queryOverpass(url, query)));
|
||||||
method: "POST",
|
|
||||||
body: `data=${encodeURIComponent(query)}`,
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error(`overpass ${response.status}`);
|
|
||||||
const data = await response.json();
|
|
||||||
return (data.elements || []).filter((element) => element.type === "relation");
|
return (data.elements || []).filter((element) => element.type === "relation");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,16 +163,26 @@ function routeDistance(relation) {
|
|||||||
|
|
||||||
function routeHtml(relation) {
|
function routeHtml(relation) {
|
||||||
const tags = relation.tags || {};
|
const tags = relation.tags || {};
|
||||||
const name = tags.name || tags.ref || "Hiking trail";
|
const name = tags.name || tags.description || "Hiking trail";
|
||||||
const swatch = SAFE_COLOR.test(tags.colour || "")
|
const swatch = SAFE_COLOR.test(tags.colour || "")
|
||||||
? `<span class="hiking-trail-popup__swatch" style="background:${tags.colour}"></span>`
|
? `<span class="hiking-trail-popup__swatch" style="background:${tags.colour}"></span>`
|
||||||
: "";
|
: "";
|
||||||
const meta = [formatDistance(routeDistance(relation))];
|
const meta = [formatDistance(routeDistance(relation))];
|
||||||
|
if (tags.ref) meta.push(`#${tags.ref}`);
|
||||||
if (tags.network) meta.push(tags.network);
|
if (tags.network) meta.push(tags.network);
|
||||||
if (tags.operator) meta.push(escapeHtml(tags.operator));
|
if (tags.operator) meta.push(escapeHtml(tags.operator));
|
||||||
|
|
||||||
return [
|
const parts = [
|
||||||
`<div class="gpx-popup__header"><strong>${escapeHtml(name)}</strong>${swatch}</div>`,
|
`<div class="gpx-popup__header"><strong>${escapeHtml(name)}</strong>${swatch}</div>`,
|
||||||
`<p class="gpx-popup__category">${meta.filter(Boolean).join(" · ")}</p>`,
|
`<p class="gpx-popup__category">${meta.filter(Boolean).join(" · ")}</p>`,
|
||||||
].join("");
|
];
|
||||||
|
// description already used as the title above when there's no name — only
|
||||||
|
// shown again here as its own line if there both is a name *and* a
|
||||||
|
// description to add, so it's never rendered twice for the common
|
||||||
|
// no-name case.
|
||||||
|
if (tags.name && tags.description) parts.push(`<p class="gpx-popup__description">${escapeHtml(tags.description)}</p>`);
|
||||||
|
parts.push(
|
||||||
|
`<p class="gpx-popup__link"><a href="https://www.openstreetmap.org/relation/${relation.id}" target="_blank" rel="noopener noreferrer">View on OpenStreetMap →</a></p>`,
|
||||||
|
);
|
||||||
|
return parts.join("");
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user