Skip to content

Commit

Permalink
Fix LRR train arrivals bugs
Browse files Browse the repository at this point in the history
Handle special case where Tacoma Dome has same stop ID in both dirs
Fix an issue where popups were getting insta-cleared due to a bug where the wrong exchange coords were checked
  • Loading branch information
nickswalker committed Sep 21, 2024
1 parent 682a7f8 commit 3526372
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 93 deletions.
197 changes: 117 additions & 80 deletions js/RelayMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,102 @@ export class RelayMap extends HTMLElement {
}

registerLiveArrivalsSource(exchanges, endpoint) {
const updateArrivals = async (popup, stopCodeNorth, stopCodeSouth) => {
Promise.all([endpoint(stopCodeNorth), endpoint(stopCodeSouth)]).then(([northboundArrivals, southboundArrivals]) => {

const currentTime = new Date();

function formatArrival(arrival) {
const arrivalTime = arrival.predictedArrivalTime || arrival.scheduledArrivalTime;
const isRealtime = arrival.predictedArrivalTime !== null;
const minutesUntilArrival = Math.round((new Date(arrivalTime) - currentTime) / 60000);
let duration = `${minutesUntilArrival} min`;
if (minutesUntilArrival === 0) {
duration = 'now';
}
let realtimeSymbol = '';
if (isRealtime) {
realtimeSymbol = '<span class="realtime-symbol"></span>';
}
let tripId = ""
if (arrival.tripId) {
tripId = "#" + arrival.tripId.substring(arrival.tripId.length - 4)
}
return {
...arrival,
time: new Date(arrivalTime),
realtime: isRealtime,
minutesUntilArrival: minutesUntilArrival,
html: `<tr><td><span class="line-marker line-${arrival.routeId}"></span></td><td class="trip-destination"> ${arrival.headsign} <span class="trip-id">${tripId}</span></td><td class="trip-eta text-end" nowrap="true">${realtimeSymbol}${duration}</td></tr>`
};
}
// Combine and sort arrivals by time
let combinedArrivals = [
...northboundArrivals,
...southboundArrivals
]
// Remove duplicate trid IDs
const seenTripIds = new Set();
combinedArrivals = combinedArrivals.filter(arrival => {
if (seenTripIds.has(arrival.tripId)) {
return false;
}
seenTripIds.add(arrival.tripId);
return true;
});

combinedArrivals = combinedArrivals.map(arrival => formatArrival(arrival)).sort((a, b) => a.time - b.time);
combinedArrivals = combinedArrivals.filter(arrival => new Date(arrival.predictedArrivalTime || arrival.scheduledArrivalTime) > currentTime);

// We have space to show 4 trips. We want to show 2 in each direction.
// If there are fewer than 2 in one direction, we'll show more in the other direction
const arrivals = []
let dir0Count = 0
let dir1Count = 0
for (let i = 0; i < combinedArrivals.length; i++) {
const arrival = combinedArrivals[i]
if (arrivals.length < 4) {
arrivals.push(arrival)
arrival.directionId === 0 ? dir0Count++ : dir1Count++;
} else {
// Try to balance the count
if (dir0Count < 2 && arrival.directionId === 0) {
// Find the last trip in direction 1
for (let idx = arrivals.length - 1; idx >= 0; idx--) {
if (arrivals[idx].directionId === 1) {
arrivals[idx] = arrival;
dir0Count++;
dir1Count--;
break;
}
}
} else if (dir1Count < 2 && arrival.directionId === 1) {
// Find the last trip in direction 0
for (let idx = arrivals.length - 1; idx >= 0; idx--) {
if (arrivals[idx].directionId === 0) {
arrivals[idx] = arrival;
dir1Count++;
dir0Count--;
break;
}
}
}
}
if (dir0Count === 2 && dir1Count === 2) break;
}


if (arrivals.length === 0) {
arrivals.push({
html: '<div>No upcoming arrivals</div>'
});
}

// Create HTML content for the merged popup
const combinedContent = arrivals.map(arrival => arrival.html).join('');
popup.setHTML(`<table>${combinedContent}</table>`);
});
};
this.mapReady.then(() => {
const map = this.map;
const popupStore = new Map(); // Stores the popups and intervals by exchange ID
Expand All @@ -314,94 +410,35 @@ export class RelayMap extends HTMLElement {
popupStore.clear();
return;
}
// Clear out-of-bounds popups
popupStore.forEach(({ popup, intervalId }) => {
if (!bounds.contains(popup.getLngLat())) {
clearInterval(intervalId);
fadeOutAndRemovePopup(popup);
popupStore.delete(popup);
}
});

for (const exchange of exchanges.features) {
const exchangeCoords = exchange.geometry.coordinates;
const exchangeId = exchange.properties.id;
const { stopCodeNorth, stopCodeSouth } = exchange.properties;

// If the exchange is out of bounds, remove its popup and clear its interval
if (!bounds.contains(exchangeCoords)) {
if (popupStore.has(exchangeId)) {
const { popup, intervalId } = popupStore.get(exchangeId);
clearInterval(intervalId);
fadeOutAndRemovePopup(popup);
popupStore.delete(exchangeId);
}
if (popupStore.has(exchangeId) || !bounds.contains(exchangeCoords) || !(stopCodeNorth && stopCodeSouth)) {
continue;
}

// If the exchange is in bounds and doesn't already have a popup, create one
if (!popupStore.has(exchangeId)) {
const { stopCodeNorth, stopCodeSouth } = exchange.properties;
if (!stopCodeNorth || !stopCodeSouth) {
continue;
}
const updateArrivals = async () => {
let northboundArrivals = await endpoint(stopCodeNorth);
let southboundArrivals = await endpoint(stopCodeSouth);

const currentTime = new Date();

function formatArrival(arrival) {
const arrivalTime = arrival.predictedArrivalTime || arrival.scheduledArrivalTime;
const isRealtime = arrival.predictedArrivalTime !== null;
const minutesUntilArrival = Math.round((new Date(arrivalTime) - currentTime) / 60000);
let duration = `${minutesUntilArrival} min`;
if (minutesUntilArrival === 0) {
duration = 'now';
}
let realtimeSymbol = '';
if (isRealtime) {
realtimeSymbol = '<span class="realtime-symbol"></span>';
}
return {
time: new Date(arrivalTime),
realtime: isRealtime,
minutesUntilArrival: minutesUntilArrival,
html: `<tr><td><span class="line-marker line-${arrival.routeId}"></span></td><td class="trip-destination"> ${arrival.headsign}</td><td class="trip-eta text-end" nowrap="true">${realtimeSymbol}${duration}</td></tr>`
};
}
// Filter out arrivals that have already passed
northboundArrivals = northboundArrivals.filter(arrival => new Date(arrival.predictedArrivalTime || arrival.scheduledArrivalTime) > currentTime);
southboundArrivals = southboundArrivals.filter(arrival => new Date(arrival.predictedArrivalTime || arrival.scheduledArrivalTime) > currentTime);


// At most, show next two arrivals for each direction
northboundArrivals.splice(2);
southboundArrivals.splice(2);

// Combine and sort arrivals by time
const combinedArrivals = [
...northboundArrivals.map(arrival => formatArrival(arrival)),
...southboundArrivals.map(arrival => formatArrival(arrival))
].sort((a, b) => a.time - b.time);

if (combinedArrivals.length === 0) {
// If there are no arrivals, show a message
combinedArrivals.push({
html: '<div>No upcoming arrivals</div>'
});
}

// Create HTML content for the merged popup
const combinedContent = combinedArrivals.map(arrival => arrival.html).join('');
// Update the popup content.
popup.setHTML(`<table>${combinedContent}</table>`);
};

// Create and show a single popup anchored at the top left
const popup = new maplibregl.Popup({ offset: [20, 40], anchor: 'top-left', className: 'arrivals-popup', closeOnClick: false, focusAfterOpen: false})
.setLngLat(exchangeCoords)
.setHTML('Loading...')
.addTo(map);

// Store the popup in the state and start the update interval
const intervalId = setInterval(updateArrivals, 20000); // Refresh every 20 seconds
popupStore.set(exchangeId, { popup, intervalId });

// Initial update call
await updateArrivals();
}
// Create and show a single popup anchored at the top left
const popup = new maplibregl.Popup({ offset: [20, 40], anchor: 'top-left', className: 'arrivals-popup', closeOnClick: false, focusAfterOpen: false, maxWidth: '260px'})
.setLngLat(exchangeCoords)
.setHTML('Loading...')
.addTo(map);

// Initial update call
await updateArrivals(popup, stopCodeNorth, stopCodeSouth);
// Store the popup in the state and start the update interval
const intervalId = setInterval(updateArrivals.bind(this, popup, stopCodeNorth, stopCodeSouth), 20000); // Refresh every 20 seconds
popupStore.set(exchangeId, { popup, intervalId });
}
};

Expand Down
22 changes: 14 additions & 8 deletions js/TransitVehicleTracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,20 @@ export class TransitVehicleTracker {
return [];
}

const arrivals = data.data.entry.arrivalsAndDepartures.map(arrival => ({
tripId: arrival.tripId,
routeId: arrival.routeId,
scheduledArrivalTime: new Date(arrival.scheduledArrivalTime),
predictedArrivalTime: arrival.predictedArrivalTime ? new Date(arrival.predictedArrivalTime) : null,
stopId: arrival.stopId,
headsign: arrival.tripHeadsign
}));
const trips = data.data.references.trips;

const arrivals = data.data.entry.arrivalsAndDepartures.map(arrival => {
const trip = trips.find(trip => trip.id === arrival.tripId);
return {
tripId: arrival.tripId,
routeId: arrival.routeId,
scheduledArrivalTime: new Date(arrival.scheduledArrivalTime),
predictedArrivalTime: arrival.predictedArrivalTime ? new Date(arrival.predictedArrivalTime) : null,
stopId: arrival.stopId,
headsign: arrival.tripHeadsign,
directionId: trip ? Number(trip.directionId) : null
};
});

return arrivals;

Expand Down
4 changes: 2 additions & 2 deletions maps/lrr-future.geojson
Original file line number Diff line number Diff line change
Expand Up @@ -632,8 +632,8 @@
"name": "Tacoma Dome",
"id": "T72",
"stationInfo": "https://www.soundtransit.org/ride-with-us/stops-stations/tacoma-dome-station",
"stopCodeNorth": "40_T01-T2",
"stopCodeSouth": "40_T01-T1"
"stopCodeNorth": "40_T01",
"stopCodeSouth": "40_T01"
},
"geometry": {
"coordinates": [
Expand Down
23 changes: 20 additions & 3 deletions pages/light-rail-relay-24.html
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,11 @@
width: .9rem;
height: .9rem;
}
.trip-id {
font-size: .8rem;
font-weight: 300;
color: #888;
}
.line-40_100479 {
background-color: var(--theme-primary-color);
width: .9rem;
Expand Down Expand Up @@ -576,10 +581,10 @@
<p>Ultra relay along Seattle's Link Light Rail by <a href="{{ site.baseurl }}/" class="fst-italic text-decoration-none">Race Condition Running</a>. <br/>08:30 September 28th</p>
<div class="gap-2 d-flex flex-wrap">

<a href="{{ page.registration_link }}" class="btn btn-outline btn-outline-primary btn-lg" data-bs-toggle="tooltip" data-bs-title="by 9-24" data-bs-placement="bottom">Join Team
<a href="{{ page.registration_link }}" class="btn btn-outline btn-outline-primary btn-lg" data-bs-toggle="tooltip" data-bs-title="by 9-26" data-bs-placement="bottom">Join Team
</a>
<a href="{{ page.team_registration_link }}" class="btn btn-outline btn-outline-primary btn-lg" data-bs-toggle="tooltip" data-bs-title="by 9-21" data-bs-placement="bottom">Add Team</a>
<a href="{{ page.team_registration_link }}" class="btn btn-outline btn-outline-primary btn-lg" data-bs-toggle="tooltip" data-bs-title="by 9-21" data-bs-placement="bottom">Enter Solo</a>
<a href="{{ page.team_registration_link }}" class="btn btn-outline btn-outline-primary btn-lg" data-bs-toggle="tooltip" data-bs-title="by 9-24" data-bs-placement="bottom">Add Team</a>
<a href="{{ page.team_registration_link }}" class="btn btn-outline btn-outline-primary btn-lg" data-bs-toggle="tooltip" data-bs-title="by 9-24" data-bs-placement="bottom">Enter Solo</a>

<!--a href="#epilogue" class="btn btn-outline btn-outline-primary btn-lg">Read Epilogue</a-->

Expand Down Expand Up @@ -833,6 +838,18 @@ <h4 id="faq-rcr">RCR Team <a href="#faq-rcr" class="anchor-link" aria-label="Lin

</section>

<section>
<div class="container">
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-baseline">
<h2 id="results">Results <a href="#results" class="anchor-link" aria-label="Link to this section. Results"></a></h2>
<a href="https://forms.gle/GBAd4JjGyNRvTWaBA" class="btn btn-outline-primary mb-3 m-sm-0">Upload Photos</a>
</div>

<p>Results will be posted the day after the event. <i>Send station photos to your team captain to avoid delays!</i></p>

</div>
</section>

<section>
<figure id="teaser-gallery">
<div class="row mb-2 g-0 gap-2">
Expand Down

0 comments on commit 3526372

Please sign in to comment.