From aeb429768b51c0f99febb5e73d96f27e21d5b233 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 15 Aug 2026 12:35:09 +0100 Subject: [PATCH] Highlight map stops from result list --- src/uk_bus_stops/static/app.js | 33 +++++++++++++++++++++++++++ src/uk_bus_stops/static/style.css | 1 + tests/test_uk_bus_stops_playwright.py | 3 +++ 3 files changed, 37 insertions(+) diff --git a/src/uk_bus_stops/static/app.js b/src/uk_bus_stops/static/app.js index 58a2535..c26c5bb 100644 --- a/src/uk_bus_stops/static/app.js +++ b/src/uk_bus_stops/static/app.js @@ -13,6 +13,7 @@ let selectedStop = null; let markers = L.featureGroup().addTo(map); let searchMarker = null; let selectedMarkerHalo = null; +let hoveredMarkerHalo = null; let userMapInteractionPending = false; let mapMoveTimer = null; let mapRequestController = null; @@ -132,6 +133,7 @@ function restoreSearchFromUrl() { markers.clearLayers(); if (searchMarker) { searchMarker.remove(); searchMarker = null; } if (selectedMarkerHalo) { selectedMarkerHalo.remove(); selectedMarkerHalo = null; } + if (hoveredMarkerHalo) { hoveredMarkerHalo.remove(); hoveredMarkerHalo = null; } selectedStop = null; show('results-panel', false); show('stop-detail', false); @@ -249,6 +251,7 @@ function renderStops(data, fitMap = true, sortOrigin = null, preserveSelection = } if (fitMap) show('map-status', false); markers.clearLayers(); + if (hoveredMarkerHalo) { hoveredMarkerHalo.remove(); hoveredMarkerHalo = null; } if (!preserveSelection) { show('stop-detail', false); show('results-panel'); @@ -281,6 +284,10 @@ function renderStops(data, fitMap = true, sortOrigin = null, preserveSelection = .filter(Boolean).join(' · '); button.append(title, meta); button.addEventListener('click', () => selectStop(stop)); + button.addEventListener('mouseenter', () => highlightHoveredStop(stop)); + button.addEventListener('mouseleave', clearHoveredStop); + button.addEventListener('focus', () => highlightHoveredStop(stop)); + button.addEventListener('blur', clearHoveredStop); list.appendChild(button); const tooltipParts = [stop.name, bearingLabel(stop), stop.transport_type || 'Transport stop']; @@ -316,6 +323,7 @@ function renderGeocodeChoices(data) { } selectedStop = null; markers.clearLayers(); + clearHoveredStop(); if (searchMarker) { searchMarker.remove(); searchMarker = null; } if (selectedMarkerHalo) { selectedMarkerHalo.remove(); selectedMarkerHalo = null; } show('stop-detail', false); @@ -379,6 +387,29 @@ function highlightSelectedStop(stop) { selectedMarkerHalo.bringToFront(); } +/** Temporarily identify the map marker corresponding to a list item. */ +function highlightHoveredStop(stop) { + clearHoveredStop(); + hoveredMarkerHalo = L.circleMarker([stop.lat, stop.lon], { + radius: 12, + color: '#0dcaf0', + fillColor: '#0dcaf0', + fillOpacity: 0.16, + weight: 4, + opacity: 1, + interactive: false, + className: 'hovered-stop-marker', + }).addTo(map); + hoveredMarkerHalo.bringToFront(); +} + +/** Remove the temporary list-hover map highlight. */ +function clearHoveredStop() { + if (!hoveredMarkerHalo) return; + hoveredMarkerHalo.remove(); + hoveredMarkerHalo = null; +} + function osmUrl(stop, edit = false) { return `https://www.openstreetmap.org/${edit ? 'edit?' : ''}${edit ? `${stop.type}=${stop.id}` : `${stop.type}/${stop.id}`}`; } @@ -397,6 +428,7 @@ function renderTags(tags) { async function selectStop(stop, updateUrl = true) { selectedStop = stop; + clearHoveredStop(); highlightSelectedStop(stop); userMapInteractionPending = false; clearTimeout(mapMoveTimer); @@ -508,6 +540,7 @@ async function loadUrl(url) { async function loadVisibleMapStops() { if (map.getZoom() < MIN_MAP_STOP_ZOOM) { markers.clearLayers(); + clearHoveredStop(); byId('map-status').textContent = 'Zoom in to load transport stops'; show('map-status'); return; diff --git a/src/uk_bus_stops/static/style.css b/src/uk_bus_stops/static/style.css index dc2115c..65ce344 100644 --- a/src/uk_bus_stops/static/style.css +++ b/src/uk_bus_stops/static/style.css @@ -22,6 +22,7 @@ body { color: #1c2333; } .tag-list dt { font-family: monospace; color: #495057; } .leaflet-container { font-family: inherit; } .selected-stop-marker { animation: selected-stop-pulse 1.5s ease-in-out infinite; } +.hovered-stop-marker { filter: drop-shadow(0 0 3px rgba(13, 202, 240, .9)); } @keyframes selected-stop-pulse { 0%, 100% { stroke-opacity: 1; fill-opacity: .25; stroke-width: 5; } 50% { stroke-opacity: .55; fill-opacity: .08; stroke-width: 8; } diff --git a/tests/test_uk_bus_stops_playwright.py b/tests/test_uk_bus_stops_playwright.py index c885427..a47f574 100644 --- a/tests/test_uk_bus_stops_playwright.py +++ b/tests/test_uk_bus_stops_playwright.py @@ -95,11 +95,14 @@ def test_search_and_stop_details_in_browser(chromium_browser: Any, flask_url: st assert page.locator("#stop-list .stop-title").all_inner_texts() == ["Central Stop", "Nearby Stop"] playwright_api.expect(page.locator("#stop-list .stop-meta").first).to_contain_text("m · Bus stop") playwright_api.expect(page.locator("#stop-list .stop-meta").first).to_contain_text("NW-bound") + page.get_by_text("Central Stop", exact=True).hover() + playwright_api.expect(page.locator(".hovered-stop-marker")).to_be_visible() assert page.evaluate("map.getZoom()") > 6 page.evaluate("markers.getLayers()[0].openTooltip()") playwright_api.expect(page.get_by_text("Central Stop · NW-bound · Bus stop", exact=True)).to_be_visible() page.get_by_text("Central Stop", exact=True).click() playwright_api.expect(page).to_have_url(re.compile(r"\?node=123$")) + playwright_api.expect(page.locator(".hovered-stop-marker")).to_be_hidden() playwright_api.expect(page.locator("#atco-code")).to_have_text("0100BRP90314") playwright_api.expect(page.locator("#stop-type")).to_have_text("Bus stop") playwright_api.expect(page.locator("#stop-bearing")).to_have_text("NW-bound")