diff --git a/src/uk_bus_stops/static/app.js b/src/uk_bus_stops/static/app.js index 4c8eb36..fc6463a 100644 --- a/src/uk_bus_stops/static/app.js +++ b/src/uk_bus_stops/static/app.js @@ -270,6 +270,7 @@ function renderStops(data, fitMap = true, sortOrigin = null, preserveSelection = radius: 7, color: '#0d6efd', fillColor: '#fff', fillOpacity: 1, weight: 3, }).bindTooltip(data.location.label).addTo(map); } + if (isMobile() && data.kind !== 'map') setPanel(true); for (const stop of stops) { const button = document.createElement('button'); @@ -305,14 +306,15 @@ function renderStops(data, fitMap = true, sortOrigin = null, preserveSelection = ? 'No transport stops were found in this map area.' : 'No transport stops were found within 1 km.'; list.appendChild(empty); - if (fitMap && data.location) moveMap(() => map.setView([data.location.lat, data.location.lon], 16)); + if (fitMap && data.location) { + moveMap(() => centrePointInVisibleMap(data.location.lat, data.location.lon, 16)); + } } else if (fitMap) { const bounds = markers.getBounds(); if (searchMarker) bounds.extend(searchMarker.getLatLng()); - moveMap(() => map.fitBounds(bounds, {padding: [30, 30], maxZoom: 17})); + moveMap(() => map.fitBounds(bounds, visibleMapFitOptions())); if (data.kind === 'atco' && stops.length === 1) selectStop(stops[0]); } - if (isMobile() && data.kind !== 'map') setPanel(true); } /** Show UK-only Nominatim matches and wait for the user to choose one. */ @@ -410,16 +412,37 @@ function clearHoveredStop() { hoveredMarkerHalo = null; } +/** Return how many map pixels are covered by the open mobile sheet. */ +function mobilePanelOverlayHeight() { + if (isMobile() && byId('sidebar').classList.contains('panel-open')) { + return Math.min(map.getSize().y, byId('sidebar').getBoundingClientRect().height); + } + return 0; +} + +/** Leaflet fit options that reserve the area hidden by the mobile sheet. */ +function visibleMapFitOptions() { + const overlayHeight = mobilePanelOverlayHeight(); + if (!overlayHeight) return {padding: [30, 30], maxZoom: 17}; + return { + paddingTopLeft: [30, 30], + paddingBottomRight: [30, overlayHeight + 30], + maxZoom: 17, + }; +} + +/** Centre a coordinate in the portion of the map not covered by the mobile sheet. */ +function centrePointInVisibleMap(lat, lon, zoom) { + const point = map.project(L.latLng(lat, lon), zoom); + const adjustedCenter = map.unproject( + point.add(L.point(0, mobilePanelOverlayHeight() / 2)), zoom + ); + map.setView(adjustedCenter, zoom); +} + /** Centre a selected stop in the portion of the map not covered by the mobile sheet. */ function centreSelectedStop(stop) { - const zoom = Math.max(map.getZoom(), 17); - let verticalOffset = 0; - if (isMobile() && byId('sidebar').classList.contains('panel-open')) { - verticalOffset = Math.min(map.getSize().y, byId('sidebar').getBoundingClientRect().height) / 2; - } - const stopPoint = map.project(L.latLng(stop.lat, stop.lon), zoom); - const adjustedCenter = map.unproject(stopPoint.add(L.point(0, verticalOffset)), zoom); - map.setView(adjustedCenter, zoom); + centrePointInVisibleMap(stop.lat, stop.lon, Math.max(map.getZoom(), 17)); } function osmUrl(stop, edit = false) { diff --git a/tests/test_uk_bus_stops_playwright.py b/tests/test_uk_bus_stops_playwright.py index 0374905..8d58c05 100644 --- a/tests/test_uk_bus_stops_playwright.py +++ b/tests/test_uk_bus_stops_playwright.py @@ -278,3 +278,40 @@ def test_mobile_selected_stop_stays_above_detail_sheet( }""") assert 0 < position["markerY"] < position["visibleBottom"] page.close() + + +def test_mobile_postcode_is_fitted_above_results_sheet( + chromium_browser: Any, flask_url: str +) -> None: + """A unique postcode marker is fitted inside the unobscured mobile map area.""" + page = chromium_browser.new_page(viewport={"width": 390, "height": 844}) + page.route("**/api/search?*", lambda route: route.fulfill(json={ + "kind": "geocode", "query": "SW1A 1AA", + "locations": [{ + "lat": 51.501, "lon": -0.142, "label": "Westminster, London", + "type": "postcode", + }], + })) + page.route("**/api/stops?*", lambda route: route.fulfill(json={ + "kind": "location", + "location": {"lat": 51.501, "lon": -0.142, "label": "Westminster, London"}, + "stops": [{ + "type": "node", "id": 999, "lat": 51.502, "lon": -0.141, + "name": "Nearby Stop", "atco_code": "490000001", "indicator": None, + "bearing": "N", "transport_type": "Bus stop", + "tags": {"highway": "bus_stop"}, + }], + })) + page.goto(flask_url, wait_until="networkidle") + page.get_by_label("Location or ATCO code").fill("SW1A 1AA") + page.get_by_role("button", name="Search").click() + playwright_api.expect(page.locator("#stop-count")).to_have_text("1 found") + page.wait_for_timeout(400) + position = page.evaluate("""() => { + const point = map.latLngToContainerPoint(searchMarker.getLatLng()); + const mapRect = document.getElementById('map').getBoundingClientRect(); + const sheetRect = document.getElementById('sidebar').getBoundingClientRect(); + return {markerY: point.y, visibleBottom: sheetRect.top - mapRect.top}; + }""") + assert 0 < position["markerY"] < position["visibleBottom"] + page.close()