From c56432139ba2f32e17a2b81e4661eff771172219 Mon Sep 17 00:00:00 2001 From: JD <46619169+rudiejd@users.noreply.github.com> Date: Tue, 21 May 2024 20:06:25 -0400 Subject: [PATCH] fix(predictions): only fetch predictions for a train when user opens the train opover (#203) * fix: get departure predictions per route instead of per vehicle * fix: prediction fetching * fix: prediction fetching * fix: prediction * fix: remove extra file * fix: remove redundant variable * fix: some naming * fix: trailing whitespace --- server/application.py | 8 ++++++++ server/mbta_api.py | 21 ++++++++++----------- src/components/TrainPopover.tsx | 7 ++++++- src/hooks/useMbtaApi.ts | 4 +++- src/hooks/usePrediction.ts | 20 ++++++++++++++++++++ src/labels.tsx | 21 ++++++++++++++------- src/types.ts | 6 +++++- 7 files changed, 66 insertions(+), 21 deletions(-) create mode 100644 src/hooks/usePrediction.ts diff --git a/server/application.py b/server/application.py index a4cfc54..c8d16ad 100644 --- a/server/application.py +++ b/server/application.py @@ -55,6 +55,14 @@ def routes(route_ids_string): return flask.Response(json.dumps(route_data), mimetype="application/json") +# takes a single trip id +# returns the predicted departure of said trip from a given stop +@application.route("/predictions//") +def vehicles(trip_id, stop_id): + departure = asyncio.run(mbta_api.trip_departure_predictions(trip_id, stop_id)) + return flask.Response(json.dumps(departure), mimetype="application/json") + + @application.route("/healthcheck") def healthcheck(): return server.healthcheck.run() diff --git a/server/mbta_api.py b/server/mbta_api.py index e1a9861..97f24f9 100644 --- a/server/mbta_api.py +++ b/server/mbta_api.py @@ -86,14 +86,14 @@ def maybe_reverse(stops, route): return stops -async def departure_prediction_for_vehicle(vehicle_id, stop_id): - predictions = await getV3("predictions", {"filter[stop]": stop_id}) - - for prediction in predictions: - if prediction["vehicle"]["id"] == vehicle_id: - return prediction["departure_time"] +async def trip_departure_predictions(trip_id: str, stop_id: str): + try: + prediction = await getV3("predictions", {"filter[trip]": trip_id, "filter[stop]": stop_id}) - return None + return {'departure_time': prediction[0]['departure_time']} + except Exception as e: + print(f"Error getting predictions for trip: {e}") + return {"departure_time": "null"} # takes a list of route ids @@ -101,6 +101,7 @@ async def departure_prediction_for_vehicle(vehicle_id, stop_id): # returns list of all vehicles async def vehicle_data_for_routes(route_ids): route_ids = normalize_custom_route_ids(route_ids) + vehicles = await getV3( "vehicles", { @@ -125,10 +126,9 @@ async def vehicle_data_for_routes(route_ids): # determine if vehicle is new is_new = fleet.vehicle_array_is_new(custom_route, vehicle["label"].split("-")) - departure_prediction = await departure_prediction_for_vehicle(vehicle["id"], vehicle["stop"]["id"]) - vehicles_to_display.append( { + "vehicleId": vehicle["id"], "label": vehicle["label"], "route": custom_route, "direction": vehicle["direction_id"], @@ -138,8 +138,7 @@ async def vehicle_data_for_routes(route_ids): "stationId": vehicle["stop"]["parent_station"]["id"], "tripId": vehicle["trip"]["id"], "isNewTrain": is_new, - "updatedAt": vehicle["updated_at"], - "departurePrediction": departure_prediction, + "updatedAt": vehicle["updated_at"] } ) diff --git a/src/components/TrainPopover.tsx b/src/components/TrainPopover.tsx index 435503c..e340379 100644 --- a/src/components/TrainPopover.tsx +++ b/src/components/TrainPopover.tsx @@ -2,6 +2,7 @@ import { useState, useLayoutEffect } from 'react'; import { createPortal } from 'react-dom'; import classNames from 'classnames'; import { renderTrainLabel } from '../labels'; +import { usePrediction } from '../hooks/usePrediction'; const popoverDistance = 15; @@ -71,6 +72,8 @@ export const TrainPopover = (props) => { trainY, ]); + const prediction = usePrediction(isVisible ? train.tripId : null, train.stationId); + return createPortal(
{ aria-hidden="true" >
-
{renderTrainLabel(train, route, colors.train)}
+
+ {renderTrainLabel(train, prediction, route, colors.train)} +
, container diff --git a/src/hooks/useMbtaApi.ts b/src/hooks/useMbtaApi.ts index c707632..1ede2c3 100644 --- a/src/hooks/useMbtaApi.ts +++ b/src/hooks/useMbtaApi.ts @@ -24,7 +24,9 @@ const getTrainPositions = (routes: string[], isFirstRequest: boolean | null) => return Promise.resolve(initialTrainsData); } } - return fetch(`/trains/${routes.join(',')}`).then((res) => res.json()); + return fetch(`/trains/${routes.join(',')}`).then((res) => { + res.json(); + }); }; const filterNew = (trains: Train[]) => { diff --git a/src/hooks/usePrediction.ts b/src/hooks/usePrediction.ts new file mode 100644 index 0000000..11752d6 --- /dev/null +++ b/src/hooks/usePrediction.ts @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; +import { Prediction } from '../types'; + +const getPrediction = (tripId: string, stopId: string) => { + return fetch(`/predictions/${tripId}/${stopId}`).then((res) => { + return res.json(); + }); +}; + +export const usePrediction = (tripId: string, stopId: string) => { + const [pred, setPrediction] = useState(null); + useEffect(() => { + if (!tripId) { + return; + } + getPrediction(tripId, stopId).then((pred) => setPrediction(pred)); + }, [tripId, stopId]); + + return pred; +}; diff --git a/src/labels.tsx b/src/labels.tsx index a07b506..0858d32 100644 --- a/src/labels.tsx +++ b/src/labels.tsx @@ -1,4 +1,4 @@ -import { Route, Train } from './types'; +import { Route, Train, Prediction } from './types'; const abbreviateStationName = (station: string) => station @@ -19,8 +19,8 @@ const getReadableStatusLabel = (status) => { return ''; }; -const getDepartureTimePrediction = (prediction: Date) => { - if (prediction) { +const getDepartureTimePrediction = (prediction: Date | null) => { + if (prediction && !isNaN(prediction)) { return `Next departure ${prediction.toLocaleTimeString()}`; } @@ -77,8 +77,10 @@ const renderLeadCarLabel = (train: Train, backgroundColor) => { ); }; -const renderDetailsLabel = (train: Train) => { - const departurePrediction = getDepartureTimePrediction(new Date(train.departurePrediction)); +const renderDetailsLabel = (train: Train, prediction: Prediction | null) => { + const departurePrediction = getDepartureTimePrediction( + prediction ? new Date(prediction.departure_time) : null + ); const lastUpdated = getLastUpdatedAt(new Date(train.updatedAt)); return ( @@ -89,13 +91,18 @@ const renderDetailsLabel = (train: Train) => { ); }; -export const renderTrainLabel = (train: Train, route: Route, accentColor) => { +export const renderTrainLabel = ( + train: Train, + prediction: Prediction | null, + route: Route, + accentColor +) => { return ( <> {renderStationLabel(train, route)} {renderDestinationLabel(train, route)} {renderLeadCarLabel(train, accentColor)} - {renderDetailsLabel(train)} + {renderDetailsLabel(train, prediction)} ); }; diff --git a/src/types.ts b/src/types.ts index 7152b82..d3a2005 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,6 +28,7 @@ export interface Routes { export type CurrentStatus = 'IN_TRANSIT_TO' | 'INCOMING_AT' | 'STOPPED_AT'; export interface Train { + vehicleId: string; currentStatus: CurrentStatus; direction: number; isNewTrain: boolean; @@ -37,7 +38,6 @@ export interface Train { route: string; stationId: string; tripId: string; - departurePrediction: string; updatedAt: string; } @@ -86,3 +86,7 @@ export interface Pair { } export type VehiclesAge = 'vehicles' | 'new_vehicles' | 'old_vehicles'; + +export interface Prediction { + departure_time: Date; +}