Skip to content

Commit

Permalink
Merge pull request #569 from mbta/bhw/bus-mezzanines
Browse files Browse the repository at this point in the history
support mezzanine mode for bus signs
  • Loading branch information
panentheos authored Feb 8, 2023
2 parents e1f2bf9 + 6a54c2c commit c377dc6
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 105 deletions.
196 changes: 115 additions & 81 deletions lib/signs/bus.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ defmodule Signs.Bus do
:audio_zones,
:max_minutes,
:sources,
:top_sources,
:bottom_sources,
:config_engine,
:prediction_engine,
:prev_predictions,
Expand All @@ -26,19 +28,9 @@ defmodule Signs.Bus do
text_zone: Map.fetch!(sign, "text_zone"),
audio_zones: Map.fetch!(sign, "audio_zones"),
max_minutes: Map.fetch!(sign, "max_minutes"),
sources:
for source <- Map.fetch!(sign, "sources") do
%{
stop_id: Map.fetch!(source, "stop_id"),
routes:
for route <- Map.fetch!(source, "routes") do
%{
route_id: Map.fetch!(route, "route_id"),
direction_id: Map.fetch!(route, "direction_id")
}
end
}
end,
sources: parse_sources(sign["sources"]),
top_sources: parse_sources(sign["top_sources"]),
bottom_sources: parse_sources(sign["bottom_sources"]),
config_engine: opts[:config_engine] || Engine.Config,
prediction_engine: opts[:prediction_engine] || Engine.BusPredictions,
prev_predictions: [],
Expand All @@ -49,6 +41,23 @@ defmodule Signs.Bus do
GenServer.start_link(__MODULE__, state)
end

defp parse_sources(nil), do: nil

defp parse_sources(sources) do
for source <- sources do
%{
stop_id: Map.fetch!(source, "stop_id"),
routes:
for route <- Map.fetch!(source, "routes") do
%{
route_id: Map.fetch!(route, "route_id"),
direction_id: Map.fetch!(route, "direction_id")
}
end
}
end
end

def init(state) do
schedule_run_loop(self())
{:ok, state}
Expand All @@ -63,6 +72,8 @@ defmodule Signs.Bus do
text_zone: text_zone,
max_minutes: max_minutes,
sources: sources,
top_sources: top_sources,
bottom_sources: bottom_sources,
config_engine: config_engine,
prediction_engine: predictions_engine,
prev_predictions: prev_predictions,
Expand All @@ -79,13 +90,10 @@ defmodule Signs.Bus do
{prediction_key(prediction), prediction}
end

predictions =
for %{stop_id: stop_id, routes: routes} <- sources,
fetch_predictions = fn source_list ->
for %{stop_id: stop_id, routes: routes} <- source_list,
prediction <- predictions_engine.predictions_for_stop(stop_id),
Enum.any?(
routes,
&(&1.route_id == prediction.route_id && &1.direction_id == prediction.direction_id)
) do
Map.take(prediction, [:route_id, :direction_id]) in routes do
prediction
end
# Exclude predictions whose times are missing or out of bounds
Expand Down Expand Up @@ -119,31 +127,44 @@ defmodule Signs.Bus do
%{prediction | departure_time: departure_time}
end)
|> Enum.sort_by(& &1.departure_time)
end

# Normally display one prediction per route, but if all the predictions are for the same
# route, then show a single page of two.
[top, bottom] =
case Enum.uniq_by(predictions, &{PaEss.Utilities.prediction_route_name(&1), &1.headsign}) do
[_] -> Enum.take(predictions, 2)
list -> list
end
|> Stream.chunk_every(2, 2, [nil])
|> Stream.map(&format_predictions(&1, current_time))
|> Enum.zip()
|> case do
[] -> [{}, {}]
[_, _] = list -> list
[predictions, top_predictions, bottom_predictions] =
for source_list <- [sources, top_sources, bottom_sources] do
if source_list, do: fetch_predictions.(source_list), else: []
end
|> Enum.map(fn pages ->
message =
case pages do
{} -> ""
{s} -> s
_ -> for s <- Tuple.to_list(pages), do: {s, 6}
end

%Content.Message.BusPredictions{message: message}
end)
[top, bottom] =
cond do
sources ->
# Platform mode. Display one prediction per route, but if all the predictions are for the
# same route, then show a single page of two.
pairs =
case Enum.uniq_by(predictions, &route_key(&1)) do
[_] -> Enum.take(predictions, 2)
list -> list
end
|> Stream.chunk_every(2, 2, [nil])
|> Stream.map(fn [first, second] ->
[
format_prediction(first, second, current_time),
format_prediction(second, first, current_time)
]
end)

[
paginate_message(for [x, _] <- pairs, do: x),
paginate_message(for [_, x] <- pairs, do: x)
]

true ->
# Mezzanine mode. Display and paginate each line separately.
for predictions_list <- [top_predictions, bottom_predictions] do
Stream.uniq_by(predictions_list, &route_key(&1))
|> Enum.map(&format_prediction(&1, nil, current_time))
|> paginate_message()
end
end

# Update the sign if:
# 1. it has never been updated before (we just booted up)
Expand All @@ -170,7 +191,11 @@ defmodule Signs.Bus do

# Special case: Hold prediction for inbound SL1 with stale prediction

{:noreply, %{new_state | prev_predictions: predictions}}
{:noreply,
%{
new_state
| prev_predictions: Enum.concat([predictions, top_predictions, bottom_predictions])
}}
end

def handle_info(msg, state) do
Expand All @@ -190,49 +215,47 @@ defmodule Signs.Bus do
Map.take(prediction, [:stop_id, :route_id, :vehicle_id, :direction_id])
end

defp format_predictions([first, second], current_time) do
same = second && first.headsign == second.headsign
defp route_key(prediction) do
{PaEss.Utilities.prediction_route_name(prediction), prediction.headsign}
end

defp format_prediction(nil, _, _), do: ""

max_route_length =
for prediction <- [first, second], prediction do
String.length(format_route(prediction))
defp format_prediction(prediction, other, current_time) do
%{headsign: headsign} = prediction

other_route_length = if other, do: String.length(format_route(other)), else: 0

route =
case format_route(prediction) do
"" -> ""
str -> String.pad_trailing(str, other_route_length)
end
|> Enum.max()

for prediction <- [first, second] do
if prediction do
route =
case format_route(prediction) do
"" -> ""
str -> String.pad_trailing(str, max_route_length)
end

# If both predictions are for the same route, but the times are different sizes, we could
# end up using different abbreviations on the same page, e.g. "SouthSta" and "So Sta".
# To avoid that, format both times using the second one's potentially larger size. That
# may waste one space on the top line, but will ensure that the abbreviations match up.
time =
String.pad_leading(
format_time(prediction, current_time),
String.length(format_time(if(same, do: second, else: prediction), current_time))
)

dest_max = @line_max - String.length(route) - String.length(time) - 1

# Choose the longest abbreviation that will fit within the remaining space.
dest =
[prediction.headsign | PaEss.Utilities.headsign_abbreviations(prediction.headsign)]
|> Enum.filter(&(String.length(&1) <= dest_max))
|> Enum.max_by(&String.length/1, fn ->
Logger.warn("No abbreviation for headsign: #{inspect(prediction.headsign)}")
prediction.headsign
end)

Content.Utilities.width_padded_string("#{route}#{dest}", time, @line_max)
else
""
# If both predictions are for the same route, but the times are different sizes, we could
# end up using different abbreviations on the same page, e.g. "SouthSta" and "So Sta".
# To avoid that, format both times using the other one's potentially larger size. That
# may waste one space on the top line, but will ensure that the abbreviations match up.
other_time_length =
case other do
%{headsign: ^headsign} -> String.length(format_time(other, current_time))
_ -> 0
end
end

time = String.pad_leading(format_time(prediction, current_time), other_time_length)

dest_max = @line_max - String.length(route) - String.length(time) - 1

# Choose the longest abbreviation that will fit within the remaining space.
dest =
[headsign | PaEss.Utilities.headsign_abbreviations(headsign)]
|> Enum.filter(&(String.length(&1) <= dest_max))
|> Enum.max_by(&String.length/1, fn ->
Logger.warn("No abbreviation for headsign: #{inspect(headsign)}")
headsign
end)

Content.Utilities.width_padded_string("#{route}#{dest}", time, @line_max)
end

defp format_route(prediction) do
Expand All @@ -248,4 +271,15 @@ defmodule Signs.Bus do
minutes -> "#{minutes} min"
end
end

defp paginate_message(pages) do
message =
case pages do
[] -> ""
[s] -> s
_ -> for s <- pages, do: {s, 6}
end

%Content.Message.BusPredictions{message: message}
end
end
6 changes: 4 additions & 2 deletions lib/signs/utilities/signs_config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ defmodule Signs.Utilities.SignsConfig do

@spec all_bus_stop_ids() :: [String.t()]
def all_bus_stop_ids do
for %{"type" => "bus", "sources" => sources} <- children_config(),
%{"stop_id" => stop_id} <- sources,
for %{"type" => "bus"} = sign <- children_config(),
source_list <- [sign["sources"], sign["top_sources"], sign["bottom_sources"]],
source_list,
%{"stop_id" => stop_id} <- source_list,
uniq: true do
stop_id
end
Expand Down
57 changes: 35 additions & 22 deletions scripts/transitway_engine/parse_config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ lines =
{initialize_lines, lines} = Enum.split_while(lines, &(&1 != " if (view_processRoutesFile())"))
# TODO continue parsing audio zones

routes_list =
for line <- station_lines do
routes_lookup =
for line <- station_lines,
into: %{} do
[id | source_strings] = String.split(line, ",")

routes =
Expand All @@ -37,7 +38,7 @@ routes_list =
Jason.OrderedObject.new(route_id: route_id, direction_id: String.to_integer(direction_id))
end

%{id: id, routes: routes}
{id, routes}
end

for line <- route_lines do
Expand All @@ -50,21 +51,23 @@ end
abbreviations_code =
for [line1, line2] <- Enum.chunk_every(abbrev_lines, 2) do
[headsign, _] = String.split(line1, ",")

abbreviations =
for abbrev <- String.split(line2, ",") do
String.trim(abbrev)
end
|> Enum.uniq()

%{headsign: headsign, abbreviations: abbreviations}
end
|> Enum.map(fn (%{headsign: headsign, abbreviations: abbreviations}) ->
|> Enum.map(fn %{headsign: headsign, abbreviations: abbreviations} ->
" {#{inspect(headsign)}, #{inspect(abbreviations)}}"
end)
|> Enum.concat([" {\"Silver Line Way\", [\"Slvr Ln Way\"]}"])
|> Enum.join(",\n")
|> (fn lines ->
" @headsign_abbreviation_mappings [\n" <> lines <> "\n ]"
end).()
" @headsign_abbreviation_mappings [\n" <> lines <> "\n ]"
end).()

signs_json =
for [line1, line2] <- Enum.chunk_every(initialize_lines, 2) do
Expand All @@ -75,23 +78,33 @@ signs_json =
)

Jason.OrderedObject.new(
id: id,
pa_ess_loc: pa_ess_loc,
text_zone: text_zone,
audio_zones:
for {c, i} <- Enum.with_index(["m", "c", "n", "s", "e", "w"]),
String.at(audio_zones, i) == "1" do
c
end,
type: "bus",
sources:
with [_, sid] <- Regex.run(~r/STOP_ID_(\d+)_/, stop_id),
%{routes: routes} <- Enum.find(routes_list, &match?(%{id: ^id}, &1)) do
[Jason.OrderedObject.new(stop_id: sid, routes: routes)]
[
id: id,
pa_ess_loc: pa_ess_loc,
text_zone: text_zone,
audio_zones:
for {c, i} <- Enum.with_index(["m", "c", "n", "s", "e", "w"]),
String.at(audio_zones, i) == "1" do
c
end,
type: "bus"
] ++
if id == "Silver_Line.World_Trade_Ctr_mezz" do
top_routes = Map.fetch!(routes_lookup, "Silver_Line.World_Trade_Ctr_WB")
bottom_routes = Map.fetch!(routes_lookup, "Silver_Line.World_Trade_Ctr_EB")

[
top_sources: [Jason.OrderedObject.new(stop_id: "74615", routes: top_routes)],
bottom_sources: [Jason.OrderedObject.new(stop_id: "74613", routes: bottom_routes)]
]
else
_ -> []
end,
max_minutes: String.to_integer(max_minutes)
[_, sid] = Regex.run(~r/STOP_ID_(\d+)_/, stop_id)
routes = Map.fetch!(routes_lookup, id)
[sources: [Jason.OrderedObject.new(stop_id: sid, routes: routes)]]
end ++
[
max_minutes: String.to_integer(max_minutes)
]
)
end
|> Jason.encode!()
Expand Down

0 comments on commit c377dc6

Please sign in to comment.