Skip to content

Commit

Permalink
Add live arrivals to LRR24 map
Browse files Browse the repository at this point in the history
Add stop codes for all stations
Make on-map elevation profile slightly translucent
Tweak U Dist -> Roosevelt leg for new sidewalks, construction
  • Loading branch information
nickswalker committed Aug 18, 2024
1 parent f053fc6 commit c3effde
Show file tree
Hide file tree
Showing 6 changed files with 507 additions and 77 deletions.
1 change: 0 additions & 1 deletion js/ElevationProfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ export class ElevationProfile extends HTMLElement {

svg.append('path')
.datum(this.data)
.attr('fill', 'lightgray')
.attr('d', area);

const xAxis = d3.axisBottom(x).tickFormat((d, i) => i);
Expand Down
122 changes: 121 additions & 1 deletion js/RelayMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,127 @@ export class RelayMap extends HTMLElement {
})
}

connectedCallback() {
registerLiveArrivalsSource(exchanges, endpoint) {
this.mapReady.then(() => {
const map = this.map;
const popupStore = new Map(); // Stores the popups and intervals by exchange ID

const handleMapMoveEnd = async () => {
const zoom = map.getZoom();
const pitch = map.getPitch();
const bounds = map.getBounds();

// If conditions are not met, remove all popups and clear intervals
if (zoom < 17 || pitch > 50) {
popupStore.forEach(({ popup, intervalId }) => {
clearInterval(intervalId);
fadeOutAndRemovePopup(popup);
});
popupStore.clear();
return;
}

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

// 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);
}
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;

const updateArrivals = async () => {
const northboundArrivals = await endpoint(stopCodeNorth);
const 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,
html: `<div><span class="trip-destination float-start"><span class="line-marker line-${arrival.routeId}"></span> ${arrival.headsign}</span>&nbsp;&nbsp;<span class="trip-eta float-end">${realtimeSymbol}${duration}</span></div>`
};
}

// 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(`${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' })
.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();
}
}
};

// Bind the handler to the map's moveend event
map.on('moveend', handleMapMoveEnd);

// Call the handler immediately to handle the initial load
handleMapMoveEnd();
});

function fadeOutAndRemovePopup(popup) {
const popupElement = popup._content.parentElement;
if (!popupElement) return;
popupElement.classList.add('fade-out');
setTimeout(() => popup.remove(), 500); // Wait for the fade-out transition to complete
}
}


connectedCallback() {
this.innerHTML = `
<style>
relay-map {
Expand Down
36 changes: 33 additions & 3 deletions js/TransitVehicleTracker.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export class TransitVehicleTracker {
constructor(apiUrl, pollingInterval = 15000) { // Default polling interval is 30 seconds
constructor(apiUrl, routeId, key, pollingInterval = 15000) { // Default polling interval is 30 seconds
this.apiUrl = apiUrl;
this.routeId = routeId;
this.key = key
this.pollingInterval = pollingInterval;
this.timer = null;
this.vehicles = [];
Expand Down Expand Up @@ -29,9 +31,9 @@ export class TransitVehicleTracker {
}

async poll() {
console.log("Polling for vehicle data...")
try {
const response = await fetch(this.apiUrl);
const vehicleApiUrl = `${this.apiUrl}/where/trips-for-route/${this.routeId}.json?key=${this.key}&includeStatus=true&includeSchedule=false`;
const response = await fetch(vehicleApiUrl);
const data = await response.json();
this.extractVehicleData(data);
} catch (error) {
Expand Down Expand Up @@ -66,4 +68,32 @@ export class TransitVehicleTracker {
const event = new CustomEvent('vehicleDataUpdated', { detail: vehicles });
document.dispatchEvent(event);
}

async getArrivalsForStop(stopId) {
const arrivalsUrl = `${this.apiUrl}/where/arrivals-and-departures-for-stop/${stopId}.json?key=${this.key}&minutesAfter=30&minutesBefore=0`;

try {
const response = await fetch(arrivalsUrl);
const data = await response.json();
console.log(data)
if (!data) {
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
}));

return arrivals;

} catch (error) {
console.error('Error fetching arrivals for stop:', error);
return [];
}
}
}
Loading

0 comments on commit c3effde

Please sign in to comment.