From 8ed7b8d112be5750e6edf3b53d5bbed52d840ca3 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 15 Aug 2026 16:38:43 +0100 Subject: [PATCH] Reuse stops when zooming within loaded map bounds --- src/uk_bus_stops/static/app.js | 32 +++++++++++---------------- tests/test_uk_bus_stops_playwright.py | 8 +++---- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/uk_bus_stops/static/app.js b/src/uk_bus_stops/static/app.js index 3553d18..7f49426 100644 --- a/src/uk_bus_stops/static/app.js +++ b/src/uk_bus_stops/static/app.js @@ -17,6 +17,7 @@ let hoveredMarkerHalo = null; let userMapInteractionPending = false; let mapMoveTimer = null; let mapRequestController = null; +let loadedMapBounds = null; let returnToResults = false; let returnViaHistory = false; const MIN_MAP_STOP_ZOOM = 15; @@ -167,25 +168,6 @@ async function loadSharedStop(type, id) { const response = await fetch(stopApiUrl(API_URLS.stopTemplate, type, id)); const data = await response.json(); if (!response.ok) throw new Error(data.message || 'Could not load that stop.'); - let nearbyData = { - kind: 'location', - location: {lat: data.stop.lat, lon: data.stop.lon, label: data.stop.name}, - stops: [data.stop], - }; - try { - const nearbyParams = new URLSearchParams({lat: data.stop.lat, lon: data.stop.lon}); - const nearbyResponse = await fetch(`${API_URLS.stops}?${nearbyParams}`); - const nearbyResult = await nearbyResponse.json(); - if (nearbyResponse.ok) nearbyData = nearbyResult; - } catch (_) { - // The selected stop is still useful if the optional nearby lookup fails. - } - const selectedIndex = nearbyData.stops.findIndex( - stop => stop.type === data.stop.type && stop.id === data.stop.id - ); - if (selectedIndex === -1) nearbyData.stops.unshift(data.stop); - else nearbyData.stops[selectedIndex] = data.stop; - renderStops(nearbyData, true); returnToResults = true; returnViaHistory = false; await selectStop(data.stop, false); @@ -245,6 +227,7 @@ function setSearchMarker(locationResult, label) { } function renderStops(data, fitMap = true, sortOrigin = null, preserveSelection = false) { + if (data.kind !== 'map') loadedMapBounds = null; const origin = data.location || sortOrigin; stops = [...data.stops] .map(stop => ({...stop, distance_metres: distanceMetres(stop, origin)})) @@ -592,6 +575,7 @@ async function loadVisibleMapStops() { if (map.getZoom() < MIN_MAP_STOP_ZOOM) { markers.clearLayers(); clearHoveredStop(); + loadedMapBounds = null; byId('map-status').textContent = 'Zoom in to load transport stops'; show('map-status'); return; @@ -609,6 +593,14 @@ async function loadVisibleMapStops() { lat: center.lat.toFixed(6), lon: center.lng.toFixed(6), }), true); } + if (loadedMapBounds && loadedMapBounds.contains(bounds)) { + if (mapRequestController) { + mapRequestController.abort(); + mapRequestController = null; + } + show('map-status', false); + return; + } byId('map-status').textContent = 'Loading stops in this area…'; show('map-status'); if (mapRequestController) mapRequestController.abort(); @@ -619,6 +611,8 @@ async function loadVisibleMapStops() { }); const data = await response.json(); if (!response.ok) throw new Error(data.message || 'Could not load this map area.'); + // A capped result may be incomplete, so do not reuse its coverage when zooming in. + loadedMapBounds = data.stops.length < 1000 ? bounds : null; renderStops(data, false, map.getCenter(), Boolean(selectedStop)); show('map-status', false); } catch (error) { diff --git a/tests/test_uk_bus_stops_playwright.py b/tests/test_uk_bus_stops_playwright.py index 11a1d35..a6e7091 100644 --- a/tests/test_uk_bus_stops_playwright.py +++ b/tests/test_uk_bus_stops_playwright.py @@ -122,7 +122,7 @@ def test_search_and_stop_details_in_browser(chromium_browser: Any, flask_url: st def test_moving_map_loads_only_when_zoomed_in(chromium_browser: Any, flask_url: str) -> None: - """Panning a close map loads visible stops while a wide map makes no query.""" + """Map loads skip wide views and zoom-ins already covered by fetched bounds.""" page = chromium_browser.new_page(viewport={"width": 1280, "height": 800}) requests: list[str] = [] @@ -154,13 +154,13 @@ def test_moving_map_loads_only_when_zoomed_in(chromium_browser: Any, flask_url: playwright_api.expect(page.locator("#stop-name")).to_have_text("Temple Meads") page.wait_for_timeout(700) playwright_api.expect(page.locator("#stop-detail")).to_be_visible() - assert len(requests) == 3 - page.evaluate("userMapInteractionPending = true; map.panBy([80, 0])") + assert len(requests) == 2 + page.evaluate("userMapInteractionPending = true; map.panBy([2500, 0])") page.wait_for_timeout(700) playwright_api.expect(page.locator("#stop-name")).to_have_text("Temple Meads") playwright_api.expect(page.locator("#stop-detail")).to_be_visible() playwright_api.expect(page).to_have_url(re.compile(r"\?node=456$")) - assert len(requests) == 4 + assert len(requests) == 3 page.close()