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
Copy updates
  • Loading branch information
nickswalker committed Sep 24, 2024
1 parent 682a7f8 commit 1d8767e
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 101 deletions.
File renamed without changes
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
39 changes: 28 additions & 11 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 @@ -663,7 +668,7 @@ <h2 id="faq">FAQ <a href="#faq" class="anchor-link" aria-label="Link to this sec
<accordion-header>Who can participate?</accordion-header>
<accordion-body>
<p>Anyone can form a team to complete the relay. See the instructions below for making a team.</p>
<p>Members of the CSE community are welcome to run as part of the Race Condition Running team. See the <a href="#faq-rcr">section below</a> for details about the team.</p>
<p>Anyone in the CSE community is welcome to run as part of the Race Condition Running team. See the <a href="#faq-rcr">section below</a> for details about the team.</p>
</accordion-body>
</accordion-item>
<accordion-item>
Expand Down Expand Up @@ -741,14 +746,14 @@ <h5>Solo runners</h5>
<li><b>Mile 6.5:</b> 7-Eleven</li>
<li><b>Mile 11.45:</b> Chevron ExtraMile</li>
<li><b>Mile 14.6:</b> Hilltop Red Apple Market</li>
<li><b>Mile 18.7:</b> Target</li>
<li><b>Mile 20.7:</b> M2M Mart</li>
<li><b>Mile 25.1:</b> Trader Joe's</li>
<li><b>Mile 31.95:</b> 7-Eleven</li>
<li><b>Mile 33.96</b> Arctic Mini-Mart</li>
<li><b>Mile 37.15</b> 7-Eleven</li>
<li><b>Mile 19.35:</b> H Mart</li>
<li><b>Mile 20.55:</b> M2M Mart</li>
<li><b>Mile 25:</b> Trader Joe's</li>
<li><b>Mile 31.8:</b> 7-Eleven</li>
<li><b>Mile 33.8:</b> Arctic Mini-Mart</li>
<li><b>Mile 37:</b> 7-Eleven</li>
</ul>
<p>Seattle's <a href="https://www.theurbanist.org/2022/08/20/why-peeing-your-pants-in-seattle-is-not-a-personal-failure/">shameful lack of public restrooms</a> is an obstacle. Libraries are your safest bet. The Beacon Hill Branch is only a block off the course and will be open in time for most runners, as are the Central Library downtown on 4th Ave, and University Branch on Roosevelt. Stations from Northgate northward have restrooms. Some of the above stores <i>may</i> have restrooms available, and you can zoom into the map to see other restrooms from OpenStreetMap. Do not count on access to any restrooms on the UW campus on weekends.</p>
<p>Seattle's <a href="https://www.theurbanist.org/2022/08/20/why-peeing-your-pants-in-seattle-is-not-a-personal-failure/">shameful lack of public restrooms</a> means libraries are your safest bet. The Beacon Hill Branch is only a block off the course and will be open in time for most runners, as are the Central Library downtown on 4th Ave, and University Branch on Roosevelt. Stations from Northgate northward have restrooms. Some of the above stores <i>may</i> have restrooms available, and you can zoom into the map to see other restrooms from OpenStreetMap. Do not count on access to any restrooms on the UW campus on weekends.</p>

<h5>All teams</h5>
<p>Exchanges: The easiest way is to meet at the exact points marked on the route. We've placed the markers next to <a href="https://www.soundtransit.org/sites/default/files/2016_start-on-link-map.pdf">station art</a>, signage or other landmarks. Zoom into the map to see our recommended exchange landmarks.</p>
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 1d8767e

Please sign in to comment.