From f6dd68ed75564df141a0069290b4ddd565eeb5c0 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 27 Feb 2026 19:18:05 +0000 Subject: [PATCH] Avoid map zoom-out when navigating between routes Route links (route_master list, other directions) now intercept clicks and call loadRoute() + history.pushState() instead of doing a full page reload, so the map stays at its current position and zooms smoothly into the new route. Ctrl/cmd/middle-click still opens in a new tab via href. Introduce currentRelationId (mutable) to track the loaded relation so loadSegment() uses the correct ID after in-page navigation. Co-Authored-By: Claude Sonnet 4.6 --- web/static/app.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/web/static/app.js b/web/static/app.js index a94b6b5..a471acf 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -22,6 +22,7 @@ let routeMasterLayers = []; // coloured polylines when viewing a route_master // ── State ───────────────────────────────────────────────────────────────── +let currentRelationId = RELATION_ID; // tracks the currently loaded relation let routeData = null; // response from /api/route/ let segmentGeoJson = null; // last response from /api/segment/ (always includes stops) let activeSlot = 'from'; // 'from' | 'to' | null @@ -301,6 +302,21 @@ document.getElementById('include-stops').addEventListener('change', () => { document.getElementById('slot-from').addEventListener('click', () => setActiveSlot('from')); document.getElementById('slot-to').addEventListener('click', () => setActiveSlot('to')); +// ── Navigation ───────────────────────────────────────────────────────────── + +/** + * Load a relation without a full page reload, updating the browser URL. + * Allows middle-click / ctrl-click to still open in a new tab via the href. + * @param {number} id + * @param {MouseEvent} e + */ +function navigateTo(id, e) { + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return; + e.preventDefault(); + history.pushState(null, '', `/${id}`); + loadRoute(id); +} + // ── API calls ────────────────────────────────────────────────────────────── /** @@ -320,6 +336,7 @@ async function loadRoute(relationId) { return; } routeData = data; + currentRelationId = relationId; // Clean up any previous route_master view for (const l of routeMasterLayers) l.remove(); routeMasterLayers = []; @@ -358,6 +375,7 @@ async function loadRoute(relationId) { a.href = `/${dir.id}`; a.className = 'stop-item d-block text-decoration-none'; a.textContent = dir.name; + a.addEventListener('click', (e) => navigateTo(dir.id, e)); dirList.appendChild(a); } } else { @@ -373,7 +391,7 @@ async function loadRoute(relationId) { */ async function loadSegment() { if (!selectedFrom || !selectedTo || !routeData) return; - const rid = RELATION_ID; + const rid = currentRelationId; const params = new URLSearchParams({ from: selectedFrom, to: selectedTo, stops: '1' }); try { const resp = await fetch(`/api/segment/${rid}?${params}`); @@ -444,6 +462,7 @@ async function loadRouteMaster(relationId) { a.href = `/${route.id}`; a.className = 'text-decoration-none text-reset flex-grow-1'; a.textContent = route.name; + a.addEventListener('click', (e) => navigateTo(route.id, e)); div.appendChild(dot); div.appendChild(a);