Reuse stops when zooming within loaded map bounds

This commit is contained in:
Edward Betts 2026-08-15 16:38:43 +01:00
parent e8d0ff5c2c
commit 8ed7b8d112
2 changed files with 17 additions and 23 deletions

View file

@ -17,6 +17,7 @@ let hoveredMarkerHalo = null;
let userMapInteractionPending = false; let userMapInteractionPending = false;
let mapMoveTimer = null; let mapMoveTimer = null;
let mapRequestController = null; let mapRequestController = null;
let loadedMapBounds = null;
let returnToResults = false; let returnToResults = false;
let returnViaHistory = false; let returnViaHistory = false;
const MIN_MAP_STOP_ZOOM = 15; 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 response = await fetch(stopApiUrl(API_URLS.stopTemplate, type, id));
const data = await response.json(); const data = await response.json();
if (!response.ok) throw new Error(data.message || 'Could not load that stop.'); 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; returnToResults = true;
returnViaHistory = false; returnViaHistory = false;
await selectStop(data.stop, false); await selectStop(data.stop, false);
@ -245,6 +227,7 @@ function setSearchMarker(locationResult, label) {
} }
function renderStops(data, fitMap = true, sortOrigin = null, preserveSelection = false) { function renderStops(data, fitMap = true, sortOrigin = null, preserveSelection = false) {
if (data.kind !== 'map') loadedMapBounds = null;
const origin = data.location || sortOrigin; const origin = data.location || sortOrigin;
stops = [...data.stops] stops = [...data.stops]
.map(stop => ({...stop, distance_metres: distanceMetres(stop, origin)})) .map(stop => ({...stop, distance_metres: distanceMetres(stop, origin)}))
@ -592,6 +575,7 @@ async function loadVisibleMapStops() {
if (map.getZoom() < MIN_MAP_STOP_ZOOM) { if (map.getZoom() < MIN_MAP_STOP_ZOOM) {
markers.clearLayers(); markers.clearLayers();
clearHoveredStop(); clearHoveredStop();
loadedMapBounds = null;
byId('map-status').textContent = 'Zoom in to load transport stops'; byId('map-status').textContent = 'Zoom in to load transport stops';
show('map-status'); show('map-status');
return; return;
@ -609,6 +593,14 @@ async function loadVisibleMapStops() {
lat: center.lat.toFixed(6), lon: center.lng.toFixed(6), lat: center.lat.toFixed(6), lon: center.lng.toFixed(6),
}), true); }), 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…'; byId('map-status').textContent = 'Loading stops in this area…';
show('map-status'); show('map-status');
if (mapRequestController) mapRequestController.abort(); if (mapRequestController) mapRequestController.abort();
@ -619,6 +611,8 @@ async function loadVisibleMapStops() {
}); });
const data = await response.json(); const data = await response.json();
if (!response.ok) throw new Error(data.message || 'Could not load this map area.'); 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)); renderStops(data, false, map.getCenter(), Boolean(selectedStop));
show('map-status', false); show('map-status', false);
} catch (error) { } catch (error) {

View file

@ -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: 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}) page = chromium_browser.new_page(viewport={"width": 1280, "height": 800})
requests: list[str] = [] 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") playwright_api.expect(page.locator("#stop-name")).to_have_text("Temple Meads")
page.wait_for_timeout(700) page.wait_for_timeout(700)
playwright_api.expect(page.locator("#stop-detail")).to_be_visible() playwright_api.expect(page.locator("#stop-detail")).to_be_visible()
assert len(requests) == 3 assert len(requests) == 2
page.evaluate("userMapInteractionPending = true; map.panBy([80, 0])") page.evaluate("userMapInteractionPending = true; map.panBy([2500, 0])")
page.wait_for_timeout(700) page.wait_for_timeout(700)
playwright_api.expect(page.locator("#stop-name")).to_have_text("Temple Meads") 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.locator("#stop-detail")).to_be_visible()
playwright_api.expect(page).to_have_url(re.compile(r"\?node=456$")) playwright_api.expect(page).to_have_url(re.compile(r"\?node=456$"))
assert len(requests) == 4 assert len(requests) == 3
page.close() page.close()