diff --git a/js/RelayMap.js b/js/RelayMap.js index a13a093..701f57d 100644 --- a/js/RelayMap.js +++ b/js/RelayMap.js @@ -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 = ''; + } + let tripId = "" + if (arrival.tripId) { + tripId = "#" + arrival.tripId.substring(arrival.tripId.length - 4) + } + return { + ...arrival, + time: new Date(arrivalTime), + realtime: isRealtime, + minutesUntilArrival: minutesUntilArrival, + html: ` ${arrival.headsign} ${tripId}${realtimeSymbol}${duration}` + }; + } + // 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: '
No upcoming arrivals
' + }); + } + + // Create HTML content for the merged popup + const combinedContent = arrivals.map(arrival => arrival.html).join(''); + popup.setHTML(`${combinedContent}
`); + }); + }; this.mapReady.then(() => { const map = this.map; const popupStore = new Map(); // Stores the popups and intervals by exchange ID @@ -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 = ''; - } - return { - time: new Date(arrivalTime), - realtime: isRealtime, - minutesUntilArrival: minutesUntilArrival, - html: ` ${arrival.headsign}${realtimeSymbol}${duration}` - }; - } - // 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: '
No upcoming arrivals
' - }); - } - - // Create HTML content for the merged popup - const combinedContent = combinedArrivals.map(arrival => arrival.html).join(''); - // Update the popup content. - popup.setHTML(`${combinedContent}
`); - }; - - // 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 }); } }; diff --git a/js/TransitVehicleTracker.js b/js/TransitVehicleTracker.js index 879ddd9..5868a60 100644 --- a/js/TransitVehicleTracker.js +++ b/js/TransitVehicleTracker.js @@ -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; diff --git a/maps/lrr-future.geojson b/maps/lrr-future.geojson index 071c2e3..edf9e72 100644 --- a/maps/lrr-future.geojson +++ b/maps/lrr-future.geojson @@ -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": [ diff --git a/pages/light-rail-relay-24.html b/pages/light-rail-relay-24.html index a773fb9..f4cd6dd 100644 --- a/pages/light-rail-relay-24.html +++ b/pages/light-rail-relay-24.html @@ -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; @@ -576,10 +581,10 @@

Ultra relay along Seattle's Link Light Rail by Race Condition Running.
08:30 September 28th

- Join Team + Join Team - Add Team - Enter Solo + Add Team + Enter Solo @@ -833,6 +838,18 @@

RCR Team + + +

Results will be posted the day after the event. Send station photos to your team captain to avoid delays!

+ +

+ +