feat: implement nearby bus stop selection and display in the UI

This commit is contained in:
2026-06-01 14:32:32 +08:00
parent c04112cadd
commit 9d1d93d113
2 changed files with 121 additions and 31 deletions
+37 -1
View File
@@ -197,6 +197,41 @@ button:hover {
margin-bottom: 0.2rem;
}
.nearby-stops {
margin-top: 0.85rem;
}
.nearby-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.55rem;
}
.stop-choice {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.6rem;
text-align: left;
width: 100%;
border: 1px solid #d7c3a6;
background: #fff8ea;
color: #3f3325;
padding: 0.55rem 0.7rem;
}
.stop-choice small {
color: #846950;
font-weight: 600;
white-space: nowrap;
}
.stop-choice.active {
border-color: #b14d14;
background: #ffe9d7;
box-shadow: inset 0 0 0 1px #d25f1f;
}
.arrivals-actions {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
@@ -300,7 +335,8 @@ button:hover {
}
.stop-meta,
.arrivals-actions {
.arrivals-actions,
.nearby-list {
grid-template-columns: 1fr;
}
+84 -30
View File
@@ -11,7 +11,8 @@
let loadingArrivals = false;
let userLocation = null;
let nearestStop = null;
let selectedStop = null;
let nearbyStops = [];
let locationMessage = "Allow location access to find your nearest bus stop.";
let mapContainer;
@@ -20,6 +21,7 @@
let userMarker;
let stopMarker;
let linkLine;
let nearbyStopMarkers = [];
function setStatus(text, state = "") {
statusText = text;
@@ -104,9 +106,9 @@
return allStops;
}
async function resolveNearestBusStop(position) {
async function resolveNearbyBusStops(position, limit = 8) {
const stops = await loadAllBusStops();
let closest = null;
const candidates = [];
stops.forEach((stop) => {
const lat = parseCoordinate(stop.Latitude);
@@ -116,23 +118,21 @@
}
const distanceMeters = haversineMeters(position.lat, position.lon, lat, lon);
if (!closest || distanceMeters < closest.distanceMeters) {
closest = {
code: stop.BusStopCode,
description: stop.Description || "Unnamed stop",
roadName: stop.RoadName || "",
lat,
lon,
distanceMeters
};
}
candidates.push({
code: stop.BusStopCode,
description: stop.Description || "Unnamed stop",
roadName: stop.RoadName || "",
lat,
lon,
distanceMeters
});
});
if (!closest) {
if (candidates.length === 0) {
throw new Error("No valid bus stop coordinates were returned.");
}
return closest;
return candidates.sort((a, b) => a.distanceMeters - b.distanceMeters).slice(0, limit);
}
function getBrowserPosition() {
@@ -188,9 +188,9 @@
userMarker.bindPopup("You are here");
}
if (nearestStop) {
const stopLatLng = [nearestStop.lat, nearestStop.lon];
const stopLabel = `${nearestStop.description} (${nearestStop.code})`;
if (selectedStop) {
const stopLatLng = [selectedStop.lat, selectedStop.lon];
const stopLabel = `${selectedStop.description} (${selectedStop.code})`;
if (!stopMarker) {
stopMarker = leaflet.marker(stopLatLng, { title: stopLabel }).addTo(map);
} else {
@@ -199,10 +199,31 @@
stopMarker.bindPopup(stopLabel);
}
if (userLocation && nearestStop) {
nearbyStopMarkers.forEach((marker) => marker.remove());
nearbyStopMarkers = [];
nearbyStops.forEach((stop) => {
const marker = leaflet
.circleMarker([stop.lat, stop.lon], {
radius: selectedStop?.code === stop.code ? 8 : 5,
color: selectedStop?.code === stop.code ? "#d25f1f" : "#7f6a52",
fillColor: selectedStop?.code === stop.code ? "#d25f1f" : "#cbbca2",
fillOpacity: 0.85,
weight: 2
})
.addTo(map);
marker.bindPopup(`${stop.description} (${stop.code})`);
marker.on("click", () => {
selectBusStop(stop, true);
});
nearbyStopMarkers.push(marker);
});
if (userLocation && selectedStop) {
const linePoints = [
[userLocation.lat, userLocation.lon],
[nearestStop.lat, nearestStop.lon]
[selectedStop.lat, selectedStop.lon]
];
if (!linkLine) {
@@ -254,16 +275,16 @@
}
async function runArrival() {
if (!nearestStop) {
if (!selectedStop) {
await resolveLocationAndNearestStop(false);
if (!nearestStop) {
if (!selectedStop) {
return;
}
}
loadingArrivals = true;
try {
await callArrivalEndpoint(nearestStop.code);
await callArrivalEndpoint(selectedStop.code);
setStatus("Arrivals Loaded", "ok");
} catch (error) {
setStatus("Arrival Error", "error");
@@ -281,6 +302,15 @@
}
}
async function selectBusStop(stop, loadArrivals = false) {
selectedStop = stop;
locationMessage = `Selected stop: ${stop.description} (${stop.code})`;
await updateMap();
if (loadArrivals) {
await runArrival();
}
}
async function resolveLocationAndNearestStop(loadArrivals = true) {
locating = true;
locationMessage = "Resolving your location...";
@@ -292,9 +322,13 @@
lon: position.coords.longitude
};
locationMessage = "Finding nearest bus stop...";
nearestStop = await resolveNearestBusStop(userLocation);
locationMessage = `Nearest stop: ${nearestStop.description} (${nearestStop.code})`;
locationMessage = "Finding nearby bus stops...";
nearbyStops = await resolveNearbyBusStops(userLocation);
selectedStop = nearbyStops[0] || null;
if (!selectedStop) {
throw new Error("No nearby bus stop found.");
}
locationMessage = `Nearest stop found: ${selectedStop.description} (${selectedStop.code}). You can choose another nearby stop below.`;
await updateMap();
setStatus("Location Ready", "ok");
@@ -356,18 +390,38 @@
<div class="stop-meta">
<div class="meta-item">
<span class="meta-label">Nearest stop</span>
<strong>{nearestStop ? `${nearestStop.code} - ${nearestStop.description}` : "Not resolved"}</strong>
<span class="meta-label">Selected stop</span>
<strong>{selectedStop ? `${selectedStop.code} - ${selectedStop.description}` : "Not resolved"}</strong>
</div>
<div class="meta-item">
<span class="meta-label">Road</span>
<strong>{nearestStop?.roadName || "-"}</strong>
<strong>{selectedStop?.roadName || "-"}</strong>
</div>
<div class="meta-item">
<span class="meta-label">Distance</span>
<strong>{nearestStop ? formatDistance(nearestStop.distanceMeters) : "-"}</strong>
<strong>{selectedStop ? formatDistance(selectedStop.distanceMeters) : "-"}</strong>
</div>
</div>
<div class="nearby-stops">
<p class="meta-label">Nearby stops</p>
{#if nearbyStops.length > 0}
<div class="nearby-list">
{#each nearbyStops as stop}
<button
class={`stop-choice ${selectedStop?.code === stop.code ? "active" : ""}`}
type="button"
on:click={() => selectBusStop(stop, true)}
>
<span>{stop.code} - {stop.description}</span>
<small>{formatDistance(stop.distanceMeters)}</small>
</button>
{/each}
</div>
{:else}
<p class="hint">No nearby stops loaded yet.</p>
{/if}
</div>
</section>
<section class="panel">