Skip to content

Commit 1d8767e

Browse files
committed
Fix LRR train arrivals bugs
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
1 parent 682a7f8 commit 1d8767e

File tree

5 files changed

+161
-101
lines changed

5 files changed

+161
-101
lines changed

js/RelayMap.js

Lines changed: 117 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,102 @@ export class RelayMap extends HTMLElement {
296296
}
297297

298298
registerLiveArrivalsSource(exchanges, endpoint) {
299+
const updateArrivals = async (popup, stopCodeNorth, stopCodeSouth) => {
300+
Promise.all([endpoint(stopCodeNorth), endpoint(stopCodeSouth)]).then(([northboundArrivals, southboundArrivals]) => {
301+
302+
const currentTime = new Date();
303+
304+
function formatArrival(arrival) {
305+
const arrivalTime = arrival.predictedArrivalTime || arrival.scheduledArrivalTime;
306+
const isRealtime = arrival.predictedArrivalTime !== null;
307+
const minutesUntilArrival = Math.round((new Date(arrivalTime) - currentTime) / 60000);
308+
let duration = `${minutesUntilArrival} min`;
309+
if (minutesUntilArrival === 0) {
310+
duration = 'now';
311+
}
312+
let realtimeSymbol = '';
313+
if (isRealtime) {
314+
realtimeSymbol = '<span class="realtime-symbol"></span>';
315+
}
316+
let tripId = ""
317+
if (arrival.tripId) {
318+
tripId = "#" + arrival.tripId.substring(arrival.tripId.length - 4)
319+
}
320+
return {
321+
...arrival,
322+
time: new Date(arrivalTime),
323+
realtime: isRealtime,
324+
minutesUntilArrival: minutesUntilArrival,
325+
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>`
326+
};
327+
}
328+
// Combine and sort arrivals by time
329+
let combinedArrivals = [
330+
...northboundArrivals,
331+
...southboundArrivals
332+
]
333+
// Remove duplicate trid IDs
334+
const seenTripIds = new Set();
335+
combinedArrivals = combinedArrivals.filter(arrival => {
336+
if (seenTripIds.has(arrival.tripId)) {
337+
return false;
338+
}
339+
seenTripIds.add(arrival.tripId);
340+
return true;
341+
});
342+
343+
combinedArrivals = combinedArrivals.map(arrival => formatArrival(arrival)).sort((a, b) => a.time - b.time);
344+
combinedArrivals = combinedArrivals.filter(arrival => new Date(arrival.predictedArrivalTime || arrival.scheduledArrivalTime) > currentTime);
345+
346+
// We have space to show 4 trips. We want to show 2 in each direction.
347+
// If there are fewer than 2 in one direction, we'll show more in the other direction
348+
const arrivals = []
349+
let dir0Count = 0
350+
let dir1Count = 0
351+
for (let i = 0; i < combinedArrivals.length; i++) {
352+
const arrival = combinedArrivals[i]
353+
if (arrivals.length < 4) {
354+
arrivals.push(arrival)
355+
arrival.directionId === 0 ? dir0Count++ : dir1Count++;
356+
} else {
357+
// Try to balance the count
358+
if (dir0Count < 2 && arrival.directionId === 0) {
359+
// Find the last trip in direction 1
360+
for (let idx = arrivals.length - 1; idx >= 0; idx--) {
361+
if (arrivals[idx].directionId === 1) {
362+
arrivals[idx] = arrival;
363+
dir0Count++;
364+
dir1Count--;
365+
break;
366+
}
367+
}
368+
} else if (dir1Count < 2 && arrival.directionId === 1) {
369+
// Find the last trip in direction 0
370+
for (let idx = arrivals.length - 1; idx >= 0; idx--) {
371+
if (arrivals[idx].directionId === 0) {
372+
arrivals[idx] = arrival;
373+
dir1Count++;
374+
dir0Count--;
375+
break;
376+
}
377+
}
378+
}
379+
}
380+
if (dir0Count === 2 && dir1Count === 2) break;
381+
}
382+
383+
384+
if (arrivals.length === 0) {
385+
arrivals.push({
386+
html: '<div>No upcoming arrivals</div>'
387+
});
388+
}
389+
390+
// Create HTML content for the merged popup
391+
const combinedContent = arrivals.map(arrival => arrival.html).join('');
392+
popup.setHTML(`<table>${combinedContent}</table>`);
393+
});
394+
};
299395
this.mapReady.then(() => {
300396
const map = this.map;
301397
const popupStore = new Map(); // Stores the popups and intervals by exchange ID
@@ -314,94 +410,35 @@ export class RelayMap extends HTMLElement {
314410
popupStore.clear();
315411
return;
316412
}
413+
// Clear out-of-bounds popups
414+
popupStore.forEach(({ popup, intervalId }) => {
415+
if (!bounds.contains(popup.getLngLat())) {
416+
clearInterval(intervalId);
417+
fadeOutAndRemovePopup(popup);
418+
popupStore.delete(popup);
419+
}
420+
});
317421

318422
for (const exchange of exchanges.features) {
319423
const exchangeCoords = exchange.geometry.coordinates;
320424
const exchangeId = exchange.properties.id;
425+
const { stopCodeNorth, stopCodeSouth } = exchange.properties;
321426

322-
// If the exchange is out of bounds, remove its popup and clear its interval
323-
if (!bounds.contains(exchangeCoords)) {
324-
if (popupStore.has(exchangeId)) {
325-
const { popup, intervalId } = popupStore.get(exchangeId);
326-
clearInterval(intervalId);
327-
fadeOutAndRemovePopup(popup);
328-
popupStore.delete(exchangeId);
329-
}
427+
if (popupStore.has(exchangeId) || !bounds.contains(exchangeCoords) || !(stopCodeNorth && stopCodeSouth)) {
330428
continue;
331429
}
332430

333-
// If the exchange is in bounds and doesn't already have a popup, create one
334-
if (!popupStore.has(exchangeId)) {
335-
const { stopCodeNorth, stopCodeSouth } = exchange.properties;
336-
if (!stopCodeNorth || !stopCodeSouth) {
337-
continue;
338-
}
339-
const updateArrivals = async () => {
340-
let northboundArrivals = await endpoint(stopCodeNorth);
341-
let southboundArrivals = await endpoint(stopCodeSouth);
342-
343-
const currentTime = new Date();
344-
345-
function formatArrival(arrival) {
346-
const arrivalTime = arrival.predictedArrivalTime || arrival.scheduledArrivalTime;
347-
const isRealtime = arrival.predictedArrivalTime !== null;
348-
const minutesUntilArrival = Math.round((new Date(arrivalTime) - currentTime) / 60000);
349-
let duration = `${minutesUntilArrival} min`;
350-
if (minutesUntilArrival === 0) {
351-
duration = 'now';
352-
}
353-
let realtimeSymbol = '';
354-
if (isRealtime) {
355-
realtimeSymbol = '<span class="realtime-symbol"></span>';
356-
}
357-
return {
358-
time: new Date(arrivalTime),
359-
realtime: isRealtime,
360-
minutesUntilArrival: minutesUntilArrival,
361-
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>`
362-
};
363-
}
364-
// Filter out arrivals that have already passed
365-
northboundArrivals = northboundArrivals.filter(arrival => new Date(arrival.predictedArrivalTime || arrival.scheduledArrivalTime) > currentTime);
366-
southboundArrivals = southboundArrivals.filter(arrival => new Date(arrival.predictedArrivalTime || arrival.scheduledArrivalTime) > currentTime);
367-
368-
369-
// At most, show next two arrivals for each direction
370-
northboundArrivals.splice(2);
371-
southboundArrivals.splice(2);
372-
373-
// Combine and sort arrivals by time
374-
const combinedArrivals = [
375-
...northboundArrivals.map(arrival => formatArrival(arrival)),
376-
...southboundArrivals.map(arrival => formatArrival(arrival))
377-
].sort((a, b) => a.time - b.time);
378-
379-
if (combinedArrivals.length === 0) {
380-
// If there are no arrivals, show a message
381-
combinedArrivals.push({
382-
html: '<div>No upcoming arrivals</div>'
383-
});
384-
}
385-
386-
// Create HTML content for the merged popup
387-
const combinedContent = combinedArrivals.map(arrival => arrival.html).join('');
388-
// Update the popup content.
389-
popup.setHTML(`<table>${combinedContent}</table>`);
390-
};
391-
392-
// Create and show a single popup anchored at the top left
393-
const popup = new maplibregl.Popup({ offset: [20, 40], anchor: 'top-left', className: 'arrivals-popup', closeOnClick: false, focusAfterOpen: false})
394-
.setLngLat(exchangeCoords)
395-
.setHTML('Loading...')
396-
.addTo(map);
397-
398-
// Store the popup in the state and start the update interval
399-
const intervalId = setInterval(updateArrivals, 20000); // Refresh every 20 seconds
400-
popupStore.set(exchangeId, { popup, intervalId });
401-
402-
// Initial update call
403-
await updateArrivals();
404-
}
431+
// Create and show a single popup anchored at the top left
432+
const popup = new maplibregl.Popup({ offset: [20, 40], anchor: 'top-left', className: 'arrivals-popup', closeOnClick: false, focusAfterOpen: false, maxWidth: '260px'})
433+
.setLngLat(exchangeCoords)
434+
.setHTML('Loading...')
435+
.addTo(map);
436+
437+
// Initial update call
438+
await updateArrivals(popup, stopCodeNorth, stopCodeSouth);
439+
// Store the popup in the state and start the update interval
440+
const intervalId = setInterval(updateArrivals.bind(this, popup, stopCodeNorth, stopCodeSouth), 20000); // Refresh every 20 seconds
441+
popupStore.set(exchangeId, { popup, intervalId });
405442
}
406443
};
407444

js/TransitVehicleTracker.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,20 @@ export class TransitVehicleTracker {
8080
return [];
8181
}
8282

83-
const arrivals = data.data.entry.arrivalsAndDepartures.map(arrival => ({
84-
tripId: arrival.tripId,
85-
routeId: arrival.routeId,
86-
scheduledArrivalTime: new Date(arrival.scheduledArrivalTime),
87-
predictedArrivalTime: arrival.predictedArrivalTime ? new Date(arrival.predictedArrivalTime) : null,
88-
stopId: arrival.stopId,
89-
headsign: arrival.tripHeadsign
90-
}));
83+
const trips = data.data.references.trips;
84+
85+
const arrivals = data.data.entry.arrivalsAndDepartures.map(arrival => {
86+
const trip = trips.find(trip => trip.id === arrival.tripId);
87+
return {
88+
tripId: arrival.tripId,
89+
routeId: arrival.routeId,
90+
scheduledArrivalTime: new Date(arrival.scheduledArrivalTime),
91+
predictedArrivalTime: arrival.predictedArrivalTime ? new Date(arrival.predictedArrivalTime) : null,
92+
stopId: arrival.stopId,
93+
headsign: arrival.tripHeadsign,
94+
directionId: trip ? Number(trip.directionId) : null
95+
};
96+
});
9197

9298
return arrivals;
9399

maps/lrr-future.geojson

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -632,8 +632,8 @@
632632
"name": "Tacoma Dome",
633633
"id": "T72",
634634
"stationInfo": "https://www.soundtransit.org/ride-with-us/stops-stations/tacoma-dome-station",
635-
"stopCodeNorth": "40_T01-T2",
636-
"stopCodeSouth": "40_T01-T1"
635+
"stopCodeNorth": "40_T01",
636+
"stopCodeSouth": "40_T01"
637637
},
638638
"geometry": {
639639
"coordinates": [

pages/light-rail-relay-24.html

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,11 @@
497497
width: .9rem;
498498
height: .9rem;
499499
}
500+
.trip-id {
501+
font-size: .8rem;
502+
font-weight: 300;
503+
color: #888;
504+
}
500505
.line-40_100479 {
501506
background-color: var(--theme-primary-color);
502507
width: .9rem;
@@ -576,10 +581,10 @@
576581
<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>
577582
<div class="gap-2 d-flex flex-wrap">
578583

579-
<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
584+
<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
580585
</a>
581-
<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>
582-
<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>
586+
<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>
587+
<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>
583588

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

@@ -663,7 +668,7 @@ <h2 id="faq">FAQ <a href="#faq" class="anchor-link" aria-label="Link to this sec
663668
<accordion-header>Who can participate?</accordion-header>
664669
<accordion-body>
665670
<p>Anyone can form a team to complete the relay. See the instructions below for making a team.</p>
666-
<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>
671+
<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>
667672
</accordion-body>
668673
</accordion-item>
669674
<accordion-item>
@@ -741,14 +746,14 @@ <h5>Solo runners</h5>
741746
<li><b>Mile 6.5:</b> 7-Eleven</li>
742747
<li><b>Mile 11.45:</b> Chevron ExtraMile</li>
743748
<li><b>Mile 14.6:</b> Hilltop Red Apple Market</li>
744-
<li><b>Mile 18.7:</b> Target</li>
745-
<li><b>Mile 20.7:</b> M2M Mart</li>
746-
<li><b>Mile 25.1:</b> Trader Joe's</li>
747-
<li><b>Mile 31.95:</b> 7-Eleven</li>
748-
<li><b>Mile 33.96</b> Arctic Mini-Mart</li>
749-
<li><b>Mile 37.15</b> 7-Eleven</li>
749+
<li><b>Mile 19.35:</b> H Mart</li>
750+
<li><b>Mile 20.55:</b> M2M Mart</li>
751+
<li><b>Mile 25:</b> Trader Joe's</li>
752+
<li><b>Mile 31.8:</b> 7-Eleven</li>
753+
<li><b>Mile 33.8:</b> Arctic Mini-Mart</li>
754+
<li><b>Mile 37:</b> 7-Eleven</li>
750755
</ul>
751-
<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>
756+
<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>
752757

753758
<h5>All teams</h5>
754759
<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>
@@ -833,6 +838,18 @@ <h4 id="faq-rcr">RCR Team <a href="#faq-rcr" class="anchor-link" aria-label="Lin
833838

834839
</section>
835840

841+
<section>
842+
<div class="container">
843+
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-baseline">
844+
<h2 id="results">Results <a href="#results" class="anchor-link" aria-label="Link to this section. Results"></a></h2>
845+
<a href="https://forms.gle/GBAd4JjGyNRvTWaBA" class="btn btn-outline-primary mb-3 m-sm-0">Upload Photos</a>
846+
</div>
847+
848+
<p>Results will be posted the day after the event. <i>Send station photos to your team captain to avoid delays!</i></p>
849+
850+
</div>
851+
</section>
852+
836853
<section>
837854
<figure id="teaser-gallery">
838855
<div class="row mb-2 g-0 gap-2">

0 commit comments

Comments
 (0)