diff --git a/lib/dotcom/schedule_finder/upcoming_departures.ex b/lib/dotcom/schedule_finder/upcoming_departures.ex index 58bdad3ff1..2784d56b50 100644 --- a/lib/dotcom/schedule_finder/upcoming_departures.ex +++ b/lib/dotcom/schedule_finder/upcoming_departures.ex @@ -248,6 +248,7 @@ defmodule Dotcom.ScheduleFinder.UpcomingDepartures do }) do trip = predicted_schedule |> PredictedSchedule.trip() stop_sequence = PredictedSchedule.stop_sequence(predicted_schedule) + vehicle_at_stop_status = PredictedSchedule.vehicle_at_stop_status(predicted_schedule) trip_details = trip_details(%{ @@ -263,7 +264,8 @@ defmodule Dotcom.ScheduleFinder.UpcomingDepartures do predicted_schedule: predicted_schedule, route_type: route_type, status: PredictedSchedule.status(predicted_schedule), - now: now + now: now, + vehicle_at_stop_status: vehicle_at_stop_status }), arrival_substatus: arrival_substatus(%{ @@ -327,7 +329,8 @@ defmodule Dotcom.ScheduleFinder.UpcomingDepartures do now: DateTime.t(), predicted_schedule: PredictedSchedule.t(), route_type: Route.route_type(), - status: nil | String.t() + status: nil | String.t(), + vehicle_at_stop_status: nil | Vehicles.Vehicle.status() }) :: __MODULE__.UpcomingDeparture.arrival_status_t() defp arrival_status(%{ predicted_schedule: %PredictedSchedule{prediction: nil}, @@ -376,7 +379,8 @@ defmodule Dotcom.ScheduleFinder.UpcomingDepartures do defp arrival_status(%{ predicted_schedule: %PredictedSchedule{prediction: prediction}, route_type: route_type, - now: now + now: now, + vehicle_at_stop_status: vehicle_at_stop_status }) when prediction != nil do arrival_seconds = seconds_between(prediction.arrival_time, now) @@ -385,7 +389,8 @@ defmodule Dotcom.ScheduleFinder.UpcomingDepartures do realtime_arrival_status(%{ arrival_seconds: arrival_seconds, departure_seconds: departure_seconds, - route_type: route_type + route_type: route_type, + vehicle_at_stop_status: vehicle_at_stop_status }) end @@ -407,41 +412,47 @@ defmodule Dotcom.ScheduleFinder.UpcomingDepartures do @spec realtime_arrival_status(%{ arrival_seconds: integer(), departure_seconds: integer(), - route_type: Route.route_type() + route_type: Route.route_type(), + vehicle_at_stop_status: nil | Vehicles.Vehicle.status() }) :: __MODULE__.UpcomingDeparture.realtime_arrival_status_t() defp realtime_arrival_status(%{ arrival_seconds: arrival_seconds, departure_seconds: departure_seconds, - route_type: :subway + route_type: :bus }) when (arrival_seconds <= 0 or arrival_seconds == nil) and departure_seconds <= 90, - do: :boarding + do: :now + defp realtime_arrival_status(%{arrival_seconds: seconds, route_type: :bus}) when seconds <= 30, + do: :now + + # vehicle says it's at the stop defp realtime_arrival_status(%{ arrival_seconds: arrival_seconds, departure_seconds: departure_seconds, - route_type: :bus + route_type: :subway, + vehicle_at_stop_status: :stopped }) - when (arrival_seconds <= 0 or arrival_seconds == nil) and departure_seconds <= 90, - do: :now + when (arrival_seconds < 0 and departure_seconds >= 0) or departure_seconds <= 90, + do: :boarding defp realtime_arrival_status(%{ - arrival_seconds: nil, - departure_seconds: seconds - }), - do: {:departure_seconds, seconds} - - defp realtime_arrival_status(%{arrival_seconds: seconds, route_type: :bus}) when seconds <= 30, - do: :now + arrival_seconds: arrival_seconds, + route_type: :subway, + vehicle_at_stop_status: status + }) + when status != nil and arrival_seconds <= 30, do: :arriving - defp realtime_arrival_status(%{arrival_seconds: seconds, route_type: :subway}) - when seconds <= 30, - do: :arriving + defp realtime_arrival_status(%{ + arrival_seconds: arrival_seconds, + route_type: :subway, + vehicle_at_stop_status: status + }) + when status != nil and arrival_seconds <= 60, do: :approaching - defp realtime_arrival_status(%{arrival_seconds: seconds, route_type: :subway}) - when seconds <= 60, - do: :approaching + defp realtime_arrival_status(%{arrival_seconds: nil, departure_seconds: seconds}), + do: {:departure_seconds, seconds} defp realtime_arrival_status(%{arrival_seconds: seconds}), do: {:arrival_seconds, seconds} diff --git a/lib/predicted_schedule.ex b/lib/predicted_schedule.ex index 9398758d7a..1f22959f89 100644 --- a/lib/predicted_schedule.ex +++ b/lib/predicted_schedule.ex @@ -21,6 +21,7 @@ defmodule PredictedSchedule do @predictions_repo Application.compile_env!(:dotcom, :repo_modules)[:predictions] @schedules_repo Application.compile_env!(:dotcom, :repo_modules)[:schedules] + @vehicles_repo Application.compile_env!(:dotcom, :repo_modules)[:vehicles] def get(route_id, stop_id, opts \\ []) do now = Keyword.get(opts, :now, Util.now()) @@ -278,6 +279,34 @@ defmodule PredictedSchedule do def status(%PredictedSchedule{prediction: %Prediction{status: status}}), do: status def status(_predicted_schedule), do: nil + @doc """ + Retrieves predicted schedule vehicle + """ + @spec vehicle(PredictedSchedule.t()) :: Vehicles.Vehicle.t() | nil + def vehicle(%PredictedSchedule{prediction: %Prediction{vehicle_id: vehicle_id}}) + when not is_nil(vehicle_id) do + @vehicles_repo.get(vehicle_id) + end + + def vehicle(_), do: nil + + @doc """ + Retrieves status from predicted schedule vehicle if a vehicle is at or approaching the predicted schedule trip/stop/stop_sequence + """ + @spec vehicle_at_stop_status(PredictedSchedule.t()) :: Vehicles.Vehicle.status() | nil + def vehicle_at_stop_status(ps) do + stop = stop(ps) + stop_sequence = stop_sequence(ps) + vehicle = vehicle(ps) + + if not is_nil(vehicle) and vehicle.stop_sequence == stop_sequence and + vehicle.stop_id in [stop.id | stop.child_ids] do + vehicle.status + end + end + + def vehicle_status(_predicted_schedule), do: nil + @doc """ Determines if the given predicted schedule occurs after the given time """ diff --git a/lib/vehicles/parser.ex b/lib/vehicles/parser.ex index 74ba4a53a0..6c18b93dd4 100644 --- a/lib/vehicles/parser.ex +++ b/lib/vehicles/parser.ex @@ -11,6 +11,7 @@ defmodule Vehicles.Parser do stop_id: optional_id(relationships["stop"]), direction_id: attributes["direction_id"], status: status(attributes["current_status"]), + stop_sequence: attributes["current_stop_sequence"], longitude: attributes["longitude"], latitude: attributes["latitude"], bearing: attributes["bearing"] || 0, diff --git a/lib/vehicles/supervisor.ex b/lib/vehicles/supervisor.ex index 4a9bd2b563..df1e812025 100644 --- a/lib/vehicles/supervisor.ex +++ b/lib/vehicles/supervisor.ex @@ -38,7 +38,7 @@ defmodule Vehicles.Supervisor do MBTA.Api.Stream.build_options( name: Vehicles.Api.SSES, path: - "/vehicles?fields[vehicle]=direction_id,current_status,longitude,latitude,bearing,occupancy_status" + "/vehicles?fields[vehicle]=direction_id,current_status,current_stop_sequence,longitude,latitude,bearing,occupancy_status" ) [ diff --git a/lib/vehicles/vehicle.ex b/lib/vehicles/vehicle.ex index fc311eaeb2..44182d386e 100644 --- a/lib/vehicles/vehicle.ex +++ b/lib/vehicles/vehicle.ex @@ -9,6 +9,7 @@ defmodule Vehicles.Vehicle do :longitude, :latitude, :status, + :stop_sequence, :bearing, :crowding ] @@ -28,6 +29,7 @@ defmodule Vehicles.Vehicle do latitude: float, bearing: non_neg_integer, status: status, + stop_sequence: non_neg_integer, crowding: crowding | nil } end diff --git a/test/dotcom/schedule_finder/upcoming_departures_test.exs b/test/dotcom/schedule_finder/upcoming_departures_test.exs index a1086c44fd..bc097192e0 100644 --- a/test/dotcom/schedule_finder/upcoming_departures_test.exs +++ b/test/dotcom/schedule_finder/upcoming_departures_test.exs @@ -17,6 +17,7 @@ defmodule Dotcom.ScheduleFinder.UpcomingDeparturesTest do end) stub(Vehicles.Repo.Mock, :trip, fn _ -> Factories.Vehicles.Vehicle.build(:vehicle) end) + stub(Vehicles.Repo.Mock, :get, fn _ -> Factories.Vehicles.Vehicle.build(:vehicle) end) stub(Schedules.Repo.Mock, :by_route_ids, fn _, _ -> [] end) stub(Predictions.Repo.Mock, :all, fn _ -> [] end) @@ -1216,7 +1217,7 @@ defmodule Dotcom.ScheduleFinder.UpcomingDeparturesTest do assert departures |> Enum.count() == 1 end - test "shows subway arrival_status as :approaching if it's between 30 and 60 seconds out" do + test "shows subway arrival_status as :approaching if it has an associated vehicle and it's between 30 and 60 seconds out" do # Setup now = Dotcom.Utils.DateTime.now() @@ -1224,25 +1225,38 @@ defmodule Dotcom.ScheduleFinder.UpcomingDeparturesTest do route_id = route.id stop_id = FactoryHelpers.build(:id) trip_id = FactoryHelpers.build(:id) + vehicle_id = FactoryHelpers.build(:id) direction_id = Faker.Util.pick([0, 1]) seconds_until_arrival = Faker.random_between(31, 60) arrival_time = now |> DateTime.shift(second: seconds_until_arrival) + prediction = + Factories.Predictions.Prediction.build(:prediction, + arrival_time: arrival_time, + stop: Factories.Stops.Stop.build(:stop, id: stop_id), + trip: Factories.Schedules.Trip.build(:trip, id: trip_id), + vehicle_id: vehicle_id + ) + expect(Predictions.Repo.Mock, :all, fn [ route: ^route_id, direction_id: ^direction_id, include_terminals: true ] -> - [ - Factories.Predictions.Prediction.build(:prediction, - arrival_time: arrival_time, - stop: Factories.Stops.Stop.build(:stop, id: stop_id), - trip: Factories.Schedules.Trip.build(:trip, id: trip_id) - ) - ] + [prediction] end) + vehicle = + Factories.Vehicles.Vehicle.build(:vehicle, + id: vehicle_id, + status: :incoming, + stop_id: stop_id, + stop_sequence: prediction.stop_sequence + ) + + expect(Vehicles.Repo.Mock, :get, fn ^vehicle_id -> vehicle end) + # Exercise departures = UpcomingDepartures.upcoming_departures(%{ @@ -1309,7 +1323,8 @@ defmodule Dotcom.ScheduleFinder.UpcomingDeparturesTest do stop_id = FactoryHelpers.build(:id) trip_id = FactoryHelpers.build(:id) direction_id = Faker.Util.pick([0, 1]) - + vehicle_id = FactoryHelpers.build(:id) + stop_sequence = Faker.random_between(0, 300) seconds_until_arrival = Faker.random_between(1, 30) arrival_time = now |> DateTime.shift(second: seconds_until_arrival) @@ -1322,11 +1337,23 @@ defmodule Dotcom.ScheduleFinder.UpcomingDeparturesTest do Factories.Predictions.Prediction.build(:prediction, arrival_time: arrival_time, stop: Factories.Stops.Stop.build(:stop, id: stop_id), - trip: Factories.Schedules.Trip.build(:trip, id: trip_id) + trip: Factories.Schedules.Trip.build(:trip, id: trip_id), + stop_sequence: stop_sequence, + vehicle_id: vehicle_id ) ] end) + vehicle = + Factories.Vehicles.Vehicle.build(:vehicle, + id: vehicle_id, + status: :incoming, + stop_id: stop_id, + stop_sequence: stop_sequence + ) + + expect(Vehicles.Repo.Mock, :get, fn ^vehicle_id -> vehicle end) + # Exercise departures = UpcomingDepartures.upcoming_departures(%{ @@ -1351,11 +1378,21 @@ defmodule Dotcom.ScheduleFinder.UpcomingDeparturesTest do stop_id = FactoryHelpers.build(:id) stop = Factories.Stops.Stop.build(:stop, id: stop_id) trip_id = FactoryHelpers.build(:id) + vehicle_id = FactoryHelpers.build(:id) direction_id = Faker.Util.pick([0, 1]) seconds_until_departure = Faker.random_between(1, 90) + stop_sequence = Faker.random_between(0, 300) departure_time = now |> DateTime.shift(second: seconds_until_departure) + vehicle = + Factories.Vehicles.Vehicle.build(:vehicle, + id: vehicle_id, + status: :stopped, + stop_id: stop.id, + stop_sequence: stop_sequence + ) + expect(Predictions.Repo.Mock, :all, fn [ route: ^route_id, direction_id: ^direction_id, @@ -1366,11 +1403,15 @@ defmodule Dotcom.ScheduleFinder.UpcomingDeparturesTest do arrival_time: now |> DateTime.shift(second: -30), departure_time: departure_time, stop: stop, - trip: Factories.Schedules.Trip.build(:trip, id: trip_id) + stop_sequence: stop_sequence, + trip: Factories.Schedules.Trip.build(:trip, id: trip_id), + vehicle_id: vehicle.id ) ] end) + expect(Vehicles.Repo.Mock, :get, fn _ -> vehicle end) + # Exercise departures = UpcomingDepartures.upcoming_departures(%{ @@ -1391,28 +1432,34 @@ defmodule Dotcom.ScheduleFinder.UpcomingDeparturesTest do now = Dotcom.Utils.DateTime.now() route = Factories.Routes.Route.build(:subway_route) - route_id = route.id stop_id = FactoryHelpers.build(:id) stop = Factories.Stops.Stop.build(:stop, id: stop_id) trip_id = FactoryHelpers.build(:id) direction_id = Faker.Util.pick([0, 1]) + vehicle_id = FactoryHelpers.build(:id) seconds_until_departure = Faker.random_between(1, 90) departure_time = now |> DateTime.shift(second: seconds_until_departure) - expect(Predictions.Repo.Mock, :all, fn [ - route: ^route_id, - direction_id: ^direction_id, - include_terminals: true - ] -> - [ - Factories.Predictions.Prediction.build(:prediction, - arrival_time: nil, - departure_time: departure_time, - stop: stop, - trip: Factories.Schedules.Trip.build(:trip, id: trip_id) - ) - ] + prediction = + Factories.Predictions.Prediction.build(:prediction, + arrival_time: nil, + departure_time: departure_time, + stop: stop, + trip: Factories.Schedules.Trip.build(:trip, id: trip_id), + vehicle_id: vehicle_id + ) + + expect(Predictions.Repo.Mock, :all, fn _ -> [prediction] end) + + expect(Vehicles.Repo.Mock, :get, fn _ -> + Factories.Vehicles.Vehicle.build(:vehicle, + id: vehicle_id, + status: :stopped, + stop_id: prediction.stop.id, + trip_id: prediction.trip.id, + stop_sequence: prediction.stop_sequence + ) end) # Exercise @@ -2762,6 +2809,94 @@ defmodule Dotcom.ScheduleFinder.UpcomingDeparturesTest do # Verify assert departures |> Enum.count() == 1 end + + test "uses vehicle :stopped status for subway arrival_status, if applicable" do + # Setup + now = Dotcom.Utils.DateTime.now() + + subway_route = Factories.Routes.Route.build(:subway_route) + vehicle_id = FactoryHelpers.build(:id) + seconds_until_departure = Faker.random_between(0, 90) + arrival_time = now |> DateTime.shift(second: -5) + departure_time = now |> DateTime.shift(second: seconds_until_departure) + + prediction = + Factories.Predictions.Prediction.build(:prediction, + arrival_time: arrival_time, + departure_time: departure_time, + route: subway_route + ) + + vehicle_at_stop = + Factories.Vehicles.Vehicle.build(:vehicle, + id: vehicle_id, + status: :stopped, + stop_id: prediction.stop.id, + trip_id: prediction.trip.id, + stop_sequence: prediction.stop_sequence + ) + + prediction = Map.put(prediction, :vehicle_id, vehicle_id) + + expect(Predictions.Repo.Mock, :all, fn _ -> [prediction] end) + expect(Vehicles.Repo.Mock, :get, fn ^vehicle_id -> vehicle_at_stop end) + + # Exercise + [departure] = + UpcomingDepartures.upcoming_departures(%{ + direction_id: prediction.direction_id, + now: now, + route: subway_route, + stop_id: prediction.stop.id + }) + + # Verify + assert departure.arrival_status == :boarding + end + + test "does not use vehicle :stopped status if vehicle not at stop" do + # Setup + now = Dotcom.Utils.DateTime.now() + + subway_route = Factories.Routes.Route.build(:subway_route) + other_stop_id = FactoryHelpers.build(:id) + vehicle_id = FactoryHelpers.build(:id) + seconds_until_departure = Faker.random_between(0, 90) + arrival_time = now |> DateTime.shift(second: -5) + departure_time = now |> DateTime.shift(second: seconds_until_departure) + + prediction = + Factories.Predictions.Prediction.build(:prediction, + arrival_time: arrival_time, + departure_time: departure_time, + route: subway_route + ) + + vehicle_at_stop = + Factories.Vehicles.Vehicle.build(:vehicle, + id: vehicle_id, + status: :stopped, + stop_id: other_stop_id, + trip_id: prediction.trip.id + ) + + prediction = Map.put(prediction, :vehicle_id, vehicle_id) + + expect(Predictions.Repo.Mock, :all, fn _ -> [prediction] end) + expect(Vehicles.Repo.Mock, :get, fn ^vehicle_id -> vehicle_at_stop end) + + # Exercise + [departure] = + UpcomingDepartures.upcoming_departures(%{ + direction_id: prediction.direction_id, + now: now, + route: subway_route, + stop_id: prediction.stop.id + }) + + # Verify + refute departure.arrival_status == :boarding + end end describe "last_trip_time/4" do