Skip to content

Commit

Permalink
defer crowding and new cars calculation until it's needed (#838)
Browse files Browse the repository at this point in the history
  • Loading branch information
panentheos authored Nov 1, 2024
1 parent ae1ab7d commit b9c3754
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 195 deletions.
6 changes: 3 additions & 3 deletions lib/content/audio/approaching.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ defmodule Content.Audio.Approaching do
crowding_description: {atom(), atom()} | nil
}

def from_message(%Message.Predictions{} = message, include_crowding?) do
def from_message(%Message.Predictions{} = message, crowding_description, new_cars?) do
[
%__MODULE__{
destination: message.destination,
trip_id: message.prediction.trip_id,
platform: Content.Utilities.stop_platform(message.prediction.stop_id),
route_id: message.prediction.route_id,
new_cars?: message.new_cars?,
crowding_description: if(include_crowding?, do: message.crowding_description)
new_cars?: new_cars?,
crowding_description: crowding_description
}
]
end
Expand Down
4 changes: 2 additions & 2 deletions lib/content/audio/train_is_arriving.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ defmodule Content.Audio.TrainIsArriving do
crowding_description: {atom(), atom()} | nil
}

def from_message(%Message.Predictions{} = message, include_crowding? \\ false) do
def from_message(%Message.Predictions{} = message, crowding_description) do
[
%__MODULE__{
destination: message.destination,
trip_id: message.prediction.trip_id,
platform: Content.Utilities.stop_platform(message.prediction.stop_id),
route_id: message.prediction.route_id,
crowding_description: if(include_crowding?, do: message.crowding_description)
crowding_description: crowding_description
}
]
end
Expand Down
144 changes: 7 additions & 137 deletions lib/content/message/predictions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,21 @@ defmodule Content.Message.Predictions do
:prediction,
:station_code,
:zone,
new_cars?: false,
terminal?: false,
crowding_data_confidence: nil,
crowding_description: nil
terminal?: false
]

@type t :: %__MODULE__{
destination: PaEss.destination(),
minutes: integer() | :boarding | :arriving | :approaching,
approximate?: boolean(),
prediction: Predictions.Prediction.t(),
new_cars?: boolean(),
station_code: String.t() | nil,
zone: String.t() | nil,
terminal?: boolean(),
crowding_data_confidence: :high | :low | nil,
crowding_description: {atom(), atom()} | nil
terminal?: boolean()
}

@spec non_terminal(Predictions.Prediction.t(), String.t(), String.t(), Signs.Realtime.t()) ::
t() | nil
def non_terminal(prediction, station_code, zone, sign) do
@spec non_terminal(Predictions.Prediction.t(), String.t(), String.t()) :: t()
def non_terminal(prediction, station_code, zone) do
# e.g., North Station which is non-terminal but has trips that begin there
predicted_time = prediction.seconds_until_arrival || prediction.seconds_until_departure

Expand All @@ -63,27 +56,18 @@ defmodule Content.Message.Predictions do
true -> compute_minutes(predicted_time, certainty)
end

{crowding_data_confidence, crowding_description} =
if Signs.Utilities.SourceConfig.multi_source?(sign.source_config),
do: {nil, nil},
else: do_crowding(prediction, sign)

%__MODULE__{
destination: Content.Utilities.destination_for_prediction(prediction),
minutes: minutes,
approximate?: approximate?,
prediction: prediction,
new_cars?: sign.location_engine.for_vehicle(prediction.vehicle_id) |> new_cars?(),
station_code: station_code,
zone: zone,
crowding_data_confidence: crowding_data_confidence,
crowding_description: crowding_description
zone: zone
}
end

@spec terminal(Predictions.Prediction.t(), String.t(), String.t(), Signs.Realtime.t()) ::
t() | nil
def terminal(prediction, station_code, zone, sign) do
@spec terminal(Predictions.Prediction.t(), String.t(), String.t()) :: t()
def terminal(prediction, station_code, zone) do
stopped_at? = prediction.stops_away == 0

{minutes, approximate?} =
Expand All @@ -98,7 +82,6 @@ defmodule Content.Message.Predictions do
minutes: minutes,
approximate?: approximate?,
prediction: prediction,
new_cars?: sign.location_engine.for_vehicle(prediction.vehicle_id) |> new_cars?(),
station_code: station_code,
zone: zone,
terminal?: true
Expand All @@ -115,119 +98,6 @@ defmodule Content.Message.Predictions do
end
end

defp do_crowding(prediction, sign) when prediction.route_id in ["Orange"] do
case sign.location_engine.for_vehicle(prediction.vehicle_id) do
%Locations.Location{} = location ->
{calculate_crowding_data_confidence(prediction, location),
get_crowding_description(location.multi_carriage_details)}

_ ->
{nil, nil}
end
end

defp do_crowding(_, _), do: {nil, nil}

defp calculate_crowding_data_confidence(prediction, location)
when location.stop_id == prediction.stop_id do
if location.status in [:incoming_at, :in_transit_to],
do: :high,
else: :low
end

defp calculate_crowding_data_confidence(_prediction, _location), do: nil

defp get_crowding_description([_, _, _, _, _, _] = carriage_details) do
crowding_levels =
Enum.map(carriage_details, &occupancy_percentage_to_crowding_level(&1.occupancy_percentage))

min_crowding_level = Enum.min(crowding_levels)

relative_crowding_levels =
for crowding_level <- crowding_levels do
if crowding_level == min_crowding_level,
do: :e,
else: :f
end

{get_emptier_location(
{Enum.count(relative_crowding_levels, &Kernel.==(&1, :e)), relative_crowding_levels}
), crowding_level_to_atom(min_crowding_level)}
end

defp get_crowding_description(_), do: nil

defp occupancy_percentage_to_crowding_level(occupancy_percentage) do
cond do
occupancy_percentage <= 12 -> 1
occupancy_percentage <= 40 -> 2
occupancy_percentage > 40 -> 3
occupancy_percentage == nil -> 4
end
end

defp crowding_level_to_atom(crowding_level) do
case crowding_level do
1 -> :not_crowded
2 -> :some_crowding
3 -> :crowded
4 -> :unknown_crowding
end
end

defp get_emptier_location(car_crowding_levels) do
case car_crowding_levels do
{1, [_, _, :f, :f, :f, :f]} -> :front
{1, [:f, :f, :f, :f, _, _]} -> :back
{1, [:f, :f, _, _, :f, :f]} -> :middle
{2, [_, _, _, :f, :f, :f]} -> :front
{2, [_, :f, :f, _, :f, :f]} -> :front
{2, [:f, :f, :f, _, _, _]} -> :back
{2, [:f, :f, _, :f, :f, _]} -> :back
{2, [_, _, :f, :f, _, _]} -> :front_and_back
{2, _} -> :middle
{3, [:f, _, _, _, _, :f]} -> :middle
{3, [_, _, _, _, :f, :f]} -> :front
{3, [:f, :f, _, _, _, _]} -> :back
{3, [:f, _, :f, _, :f, _]} -> :train_level
{3, [_, :f, _, :f, _, :f]} -> :train_level
{3, _} -> :front_and_back
{4, [:f, _, _, _, _, :f]} -> :middle
{4, [_, _, _, _, _, :f]} -> :front
{4, [_, _, _, :f, :f, _]} -> :front
{4, [:f, _, _, _, _, _]} -> :back
{4, [_, :f, :f, _, _, _]} -> :back
{4, [_, _, :f, :f, _, _]} -> :front_and_back
{4, _} -> :train_level
{5, [_, _, _, _, _, :f]} -> :front
{5, [:f, _, _, _, _, _]} -> :back
{5, _} -> :train_level
{6, _} -> :train_level
_ -> :train_level
end
end

@spec new_cars?(Locations.Location.t() | nil) :: boolean()
defp new_cars?(nil) do
false
end

defp new_cars?(%Locations.Location{
multi_carriage_details: multi_carriage_details,
route_id: route_id
}) do
Enum.any?(multi_carriage_details, fn carriage ->
# See http://roster.transithistory.org/ for numbers of new cars
case Integer.parse(carriage.label) do
:error ->
false

{n, _remaining} ->
route_id == "Red" and 1900 <= n and n <= 2151
end
end)
end

defimpl Content.Message do
require Logger

Expand Down
17 changes: 17 additions & 0 deletions lib/pa_ess/utilities.ex
Original file line number Diff line number Diff line change
Expand Up @@ -716,4 +716,21 @@ defmodule PaEss.Utilities do
|> Stream.chunk_every(2, 2, [""])
|> Enum.map(fn [top, bottom] -> {top, bottom, 3} end)
end

@spec prediction_new_cars?(Predictions.Prediction.t(), Signs.Realtime.t()) :: boolean()
def prediction_new_cars?(prediction, sign) do
case sign.location_engine.for_vehicle(prediction.vehicle_id) do
%Locations.Location{route_id: "Red", multi_carriage_details: carriage_details} ->
Enum.any?(carriage_details, fn carriage ->
# See http://roster.transithistory.org/ for numbers of new cars
case Integer.parse(carriage.label) do
:error -> false
{n, _remaining} -> n in 1900..2151
end
end)

_ ->
false
end
end
end
18 changes: 10 additions & 8 deletions lib/signs/utilities/audio.ex
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ defmodule Signs.Utilities.Audio do
Audio.TrainIsBoarding.from_message(prediction)

:arriving ->
Audio.TrainIsArriving.from_message(prediction)
Audio.TrainIsArriving.from_message(prediction, nil)

:approaching ->
Audio.NextTrainCountdown.from_message(%{prediction | minutes: 1})
Expand Down Expand Up @@ -198,29 +198,31 @@ defmodule Signs.Utilities.Audio do
match?(%Message.Predictions{minutes: :arriving}, message) &&
message.prediction.trip_id not in sign.announced_arrivals &&
announce_arriving?(sign, message) ->
include_crowding? =
message.crowding_data_confidence == :high &&
message.prediction.trip_id not in sign.announced_approachings_with_crowding
crowding =
if message.prediction.trip_id not in sign.announced_approachings_with_crowding do
Signs.Utilities.Crowding.crowding_description(message.prediction, sign)
end

{Audio.TrainIsArriving.from_message(message, include_crowding?),
{Audio.TrainIsArriving.from_message(message, crowding),
update_in(sign.announced_arrivals, &cache_value(&1, message.prediction.trip_id))}

# Announce approaching if configured to
match?(%Message.Predictions{minutes: :approaching}, message) &&
message.prediction.trip_id not in sign.announced_approachings &&
announce_arriving?(sign, message) &&
message.prediction.route_id in @heavy_rail_routes ->
include_crowding? = message.crowding_data_confidence == :high
crowding = Signs.Utilities.Crowding.crowding_description(message.prediction, sign)
new_cars? = PaEss.Utilities.prediction_new_cars?(message.prediction, sign)

{Audio.Approaching.from_message(message, include_crowding?),
{Audio.Approaching.from_message(message, crowding, new_cars?),
sign
|> update_in(
[Access.key!(:announced_approachings)],
&cache_value(&1, message.prediction.trip_id)
)
|> update_in(
[Access.key!(:announced_approachings_with_crowding)],
&if(include_crowding?, do: cache_value(&1, message.prediction.trip_id), else: &1)
&if(!!crowding, do: cache_value(&1, message.prediction.trip_id), else: &1)
)}

# Announce stopped trains
Expand Down
94 changes: 94 additions & 0 deletions lib/signs/utilities/crowding.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
defmodule Signs.Utilities.Crowding do
@spec crowding_description(Predictions.Prediction.t(), Signs.Realtime.t()) ::
{:front | :back | :middle | :front_and_back | :train_level,
:not_crowded | :some_crowding | :crowded | :unknown_crowding}
| nil
def crowding_description(_, %{source_config: {_, _}}), do: nil

def crowding_description(%{route_id: "Orange"} = prediction, sign) do
case sign.location_engine.for_vehicle(prediction.vehicle_id) do
%Locations.Location{
stop_id: stop_id,
status: status,
multi_carriage_details: carriage_details
}
when stop_id == prediction.stop_id and status in [:incoming_at, :in_transit_to] ->
get_crowding_description(carriage_details)

_ ->
nil
end
end

def crowding_description(_, _), do: nil

defp get_crowding_description([_, _, _, _, _, _] = carriage_details) do
crowding_levels =
Enum.map(carriage_details, &occupancy_percentage_to_crowding_level(&1.occupancy_percentage))

min_crowding_level = Enum.min(crowding_levels)

relative_crowding_levels =
for crowding_level <- crowding_levels do
if crowding_level == min_crowding_level,
do: :e,
else: :f
end

{get_emptier_location(
{Enum.count(relative_crowding_levels, &Kernel.==(&1, :e)), relative_crowding_levels}
), crowding_level_to_atom(min_crowding_level)}
end

defp get_crowding_description(_), do: nil

defp occupancy_percentage_to_crowding_level(occupancy_percentage) do
cond do
occupancy_percentage <= 12 -> 1
occupancy_percentage <= 40 -> 2
occupancy_percentage > 40 -> 3
occupancy_percentage == nil -> 4
end
end

defp crowding_level_to_atom(crowding_level) do
case crowding_level do
1 -> :not_crowded
2 -> :some_crowding
3 -> :crowded
4 -> :unknown_crowding
end
end

defp get_emptier_location(car_crowding_levels) do
case car_crowding_levels do
{1, [_, _, :f, :f, :f, :f]} -> :front
{1, [:f, :f, :f, :f, _, _]} -> :back
{1, [:f, :f, _, _, :f, :f]} -> :middle
{2, [_, _, _, :f, :f, :f]} -> :front
{2, [_, :f, :f, _, :f, :f]} -> :front
{2, [:f, :f, :f, _, _, _]} -> :back
{2, [:f, :f, _, :f, :f, _]} -> :back
{2, [_, _, :f, :f, _, _]} -> :front_and_back
{2, _} -> :middle
{3, [:f, _, _, _, _, :f]} -> :middle
{3, [_, _, _, _, :f, :f]} -> :front
{3, [:f, :f, _, _, _, _]} -> :back
{3, [:f, _, :f, _, :f, _]} -> :train_level
{3, [_, :f, _, :f, _, :f]} -> :train_level
{3, _} -> :front_and_back
{4, [:f, _, _, _, _, :f]} -> :middle
{4, [_, _, _, _, _, :f]} -> :front
{4, [_, _, _, :f, :f, _]} -> :front
{4, [:f, _, _, _, _, _]} -> :back
{4, [_, :f, :f, _, _, _]} -> :back
{4, [_, _, :f, :f, _, _]} -> :front_and_back
{4, _} -> :train_level
{5, [_, _, _, _, _, :f]} -> :front
{5, [:f, _, _, _, _, _]} -> :back
{5, _} -> :train_level
{6, _} -> :train_level
_ -> :train_level
end
end
end
Loading

0 comments on commit b9c3754

Please sign in to comment.