diff --git a/img/lrr24/t_station_code.png b/img/lrr24/T_station_code.png similarity index 100% rename from img/lrr24/t_station_code.png rename to img/lrr24/T_station_code.png 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..77c5659 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 @@ -663,7 +668,7 @@

FAQ section below for details about the team.

+

Anyone in the CSE community is welcome to run as part of the Race Condition Running team. See the section below for details about the team.

@@ -741,14 +746,14 @@
Solo runners
  • Mile 6.5: 7-Eleven
  • Mile 11.45: Chevron ExtraMile
  • Mile 14.6: Hilltop Red Apple Market
  • -
  • Mile 18.7: Target
  • -
  • Mile 20.7: M2M Mart
  • -
  • Mile 25.1: Trader Joe's
  • -
  • Mile 31.95: 7-Eleven
  • -
  • Mile 33.96 Arctic Mini-Mart
  • -
  • Mile 37.15 7-Eleven
  • +
  • Mile 19.35: H Mart
  • +
  • Mile 20.55: M2M Mart
  • +
  • Mile 25: Trader Joe's
  • +
  • Mile 31.8: 7-Eleven
  • +
  • Mile 33.8: Arctic Mini-Mart
  • +
  • Mile 37: 7-Eleven
  • -

    Seattle's shameful lack of public restrooms 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 may 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.

    +

    Seattle's shameful lack of public restrooms 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 may 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.

    All teams

    Exchanges: The easiest way is to meet at the exact points marked on the route. We've placed the markers next to station art, signage or other landmarks. Zoom into the map to see our recommended exchange landmarks.

    @@ -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!

    + +

    + +