From f41ce297a4b8ea2a9a793f39588deb048e6e2f25 Mon Sep 17 00:00:00 2001 From: Paul Kim Date: Thu, 20 Jul 2023 10:00:25 -0400 Subject: [PATCH 1/6] Add kenmore B stops (#659) --- lib/content/utilities.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/content/utilities.ex b/lib/content/utilities.ex index e7079c4db..23696543d 100644 --- a/lib/content/utilities.ex +++ b/lib/content/utilities.ex @@ -54,6 +54,7 @@ defmodule Content.Utilities do def destination_for_prediction("Red", 0, _), do: {:ok, :southbound} def destination_for_prediction(_, 0, "70151"), do: {:ok, :kenmore} + def destination_for_prediction(_, 0, "71151"), do: {:ok, :kenmore} def destination_for_prediction(_, 0, "70202"), do: {:ok, :government_center} def destination_for_prediction(_, 0, "70201"), do: {:ok, :government_center} def destination_for_prediction(_, 0, "70175"), do: {:ok, :reservoir} @@ -70,6 +71,7 @@ defmodule Content.Utilities do def destination_for_prediction(_, 1, "70200"), do: {:ok, :park_street} def destination_for_prediction(_, 1, "71199"), do: {:ok, :park_street} def destination_for_prediction(_, 1, "70150"), do: {:ok, :kenmore} + def destination_for_prediction(_, 1, "71150"), do: {:ok, :kenmore} def destination_for_prediction(_, 1, "70174"), do: {:ok, :reservoir} def destination_for_prediction(_, _, "Government Center-Brattle"), do: {:ok, :government_center} From 069399c9b838daa691ce36e9e96f6f40eae77bb6 Mon Sep 17 00:00:00 2001 From: Paul Kim Date: Thu, 20 Jul 2023 10:22:52 -0400 Subject: [PATCH 2/6] Add other GL routes to Medford branch source configs (#656) --- priv/signs.json | 59 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/priv/signs.json b/priv/signs.json index 5fe4035bf..13c811622 100644 --- a/priv/signs.json +++ b/priv/signs.json @@ -7807,6 +7807,9 @@ "stop_id": "70513", "direction_id": 1, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -7834,6 +7837,9 @@ "stop_id": "70514", "direction_id": 0, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -7860,6 +7866,9 @@ "stop_id": "70513", "direction_id": 1, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -7877,6 +7886,9 @@ "stop_id": "70514", "direction_id": 0, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -7905,6 +7917,9 @@ "stop_id": "70505", "direction_id": 1, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -7932,6 +7947,9 @@ "stop_id": "70506", "direction_id": 0, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -7960,6 +7978,9 @@ "stop_id": "70505", "direction_id": 1, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -7977,6 +7998,9 @@ "stop_id": "70506", "direction_id": 0, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -8005,6 +8029,9 @@ "stop_id": "70507", "direction_id": 1, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -8032,6 +8059,9 @@ "stop_id": "70508", "direction_id": 0, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -8060,6 +8090,9 @@ "stop_id": "70507", "direction_id": 1, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -8077,6 +8110,9 @@ "stop_id": "70508", "direction_id": 0, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -8105,6 +8141,9 @@ "stop_id": "70509", "direction_id": 1, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -8132,6 +8171,9 @@ "stop_id": "70510", "direction_id": 0, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -8160,6 +8202,9 @@ "stop_id": "70509", "direction_id": 1, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -8177,6 +8222,9 @@ "stop_id": "70510", "direction_id": 0, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -8203,6 +8251,9 @@ "stop_id": "70512", "direction_id": 0, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -8228,6 +8279,9 @@ "stop_id": "70512", "direction_id": 0, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -8257,6 +8311,9 @@ "stop_id": "70512", "direction_id": 0, "routes": [ + "Green-B", + "Green-C", + "Green-D", "Green-E" ], "platform": null, @@ -9325,4 +9382,4 @@ ], "max_minutes": 75 } -] \ No newline at end of file +] From 91dcce538c1a7990eb5cbe5c95c888767a4136d7 Mon Sep 17 00:00:00 2001 From: Paul Kim Date: Thu, 20 Jul 2023 13:12:25 -0400 Subject: [PATCH 3/6] Add temporary logging (#657) --- lib/signs/realtime.ex | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/signs/realtime.ex b/lib/signs/realtime.ex index 3ada23ba8..d4492f37b 100644 --- a/lib/signs/realtime.ex +++ b/lib/signs/realtime.ex @@ -159,7 +159,17 @@ defmodule Signs.Realtime do defp fetch_predictions(%{sources: sources}, state) do Enum.flat_map(sources, fn source -> state.prediction_engine.for_stop(source.stop_id, source.direction_id) - |> Enum.filter(&(source.routes == nil or &1.route_id in source.routes)) + |> Enum.filter(fn prediction -> + if source.routes == nil or prediction.route_id in source.routes do + true + else + Logger.info( + "filter_prediction_by_route sign_id=#{state.id} stop_id=#{source.stop_id} direction_id=#{source.direction_id} route_id=#{prediction.route_id}" + ) + + false + end + end) end) end From bedc0a15980a5b3e5192d9ee893871911a098696 Mon Sep 17 00:00:00 2001 From: Paul Kim Date: Thu, 20 Jul 2023 15:59:26 -0400 Subject: [PATCH 4/6] Add takes for general headway directions (#658) * Add new takes and adjust train announcements to maintain status quo * include inbound and outbound for ad hoc trip descriptions * Add helper for checking if destination is directional --- lib/content/audio/following_train.ex | 48 +++++++----- lib/content/audio/next_train_countdown.ex | 40 +++++----- lib/content/audio/stopped_train.ex | 54 +++++++------ lib/content/audio/train_is_boarding.ex | 40 ++++++---- lib/pa_ess/utilities.ex | 95 ++++++++++++----------- 5 files changed, 156 insertions(+), 121 deletions(-) diff --git a/lib/content/audio/following_train.ex b/lib/content/audio/following_train.ex index 7be0e68c1..577321369 100644 --- a/lib/content/audio/following_train.ex +++ b/lib/content/audio/following_train.ex @@ -60,36 +60,44 @@ defmodule Content.Audio.FollowingTrain do def to_params(audio) do case Utilities.destination_var(audio.destination) do {:ok, dest_var} -> - green_line_branch = Content.Utilities.route_branch_letter(audio.route_id) + if Utilities.directional_destination?(audio.destination) do + do_ad_hoc_message(audio) + else + green_line_branch = Content.Utilities.route_branch_letter(audio.route_id) - cond do - !is_nil(green_line_branch) -> - green_line_with_branch_params(audio, green_line_branch, dest_var) + cond do + !is_nil(green_line_branch) -> + green_line_with_branch_params(audio, green_line_branch, dest_var) - audio.minutes == 1 -> - {:canned, {"159", [dest_var, verb_var(audio)], :audio}} + audio.minutes == 1 -> + {:canned, {"159", [dest_var, verb_var(audio)], :audio}} - true -> - {:canned, {"160", [dest_var, verb_var(audio), minutes_var(audio)], :audio}} + true -> + {:canned, {"160", [dest_var, verb_var(audio), minutes_var(audio)], :audio}} + end end {:error, :unknown} -> - case Utilities.ad_hoc_trip_description(audio.destination, audio.route_id) do - {:ok, trip_description} -> - min_or_mins = if audio.minutes == 1, do: "minute", else: "minutes" + do_ad_hoc_message(audio) + end + end - text = - "The following #{trip_description} #{audio.verb} in #{audio.minutes} #{min_or_mins}" + defp do_ad_hoc_message(audio) do + case Utilities.ad_hoc_trip_description(audio.destination, audio.route_id) do + {:ok, trip_description} -> + min_or_mins = if audio.minutes == 1, do: "minute", else: "minutes" - {:ad_hoc, {text, :audio}} + text = + "The following #{trip_description} #{audio.verb} in #{audio.minutes} #{min_or_mins}" - {:error, :unknown} -> - Logger.error( - "FollowingTrain.to_params unknown destination: #{inspect(audio.destination)}" - ) + {:ad_hoc, {text, :audio}} - nil - end + {:error, :unknown} -> + Logger.error( + "FollowingTrain.to_params unknown destination: #{inspect(audio.destination)}" + ) + + nil end end diff --git a/lib/content/audio/next_train_countdown.ex b/lib/content/audio/next_train_countdown.ex index df8a0bea9..3ed84609a 100644 --- a/lib/content/audio/next_train_countdown.ex +++ b/lib/content/audio/next_train_countdown.ex @@ -40,6 +40,9 @@ defmodule Content.Audio.NextTrainCountdown do green_line_branch = Content.Utilities.route_branch_letter(audio.route_id) cond do + Utilities.directional_destination?(audio.destination) -> + do_ad_hoc_message(audio) + !is_nil(audio.track_number) -> terminal_track_params(audio, dest_var) @@ -70,29 +73,30 @@ defmodule Content.Audio.NextTrainCountdown do end {:error, :unknown} -> - case Utilities.ad_hoc_trip_description(audio.destination, audio.route_id) do - {:ok, trip_description} -> - min_or_mins = if audio.minutes == 1, do: "minute", else: "minutes" + do_ad_hoc_message(audio) + end + end - text = - "The next #{trip_description} #{audio.verb} in #{audio.minutes} #{min_or_mins}" + defp do_ad_hoc_message(audio) do + case Utilities.ad_hoc_trip_description(audio.destination, audio.route_id) do + {:ok, trip_description} -> + min_or_mins = if audio.minutes == 1, do: "minute", else: "minutes" - text = - if audio.track_number do - text <> " from track #{audio.track_number}" - else - text - end + text = "The next #{trip_description} #{audio.verb} in #{audio.minutes} #{min_or_mins}" - {:ad_hoc, {text, :audio}} + text = + if audio.track_number do + text <> " from track #{audio.track_number}" + else + text + end - {:error, :unknown} -> - Logger.error( - "NextTrainCountdown unknown destination: #{inspect(audio.destination)}" - ) + {:ad_hoc, {text, :audio}} - nil - end + {:error, :unknown} -> + Logger.error("NextTrainCountdown unknown destination: #{inspect(audio.destination)}") + + nil end end diff --git a/lib/content/audio/stopped_train.ex b/lib/content/audio/stopped_train.ex index e479135b8..6e73265a8 100644 --- a/lib/content/audio/stopped_train.ex +++ b/lib/content/audio/stopped_train.ex @@ -38,35 +38,43 @@ defmodule Content.Audio.StoppedTrain do def to_params(audio) do case PaEss.Utilities.destination_var(audio.destination) do {:ok, dest_var} -> - vars = [ - @the_next, - @train_to, - dest_var, - @is, - @stopped, - number_var(audio.stops_away), - stops_away_var(audio.stops_away) - ] - - PaEss.Utilities.take_message(vars, :audio) + if Utilities.directional_destination?(audio.destination) do + do_ad_hoc_message(audio) + else + vars = [ + @the_next, + @train_to, + dest_var, + @is, + @stopped, + number_var(audio.stops_away), + stops_away_var(audio.stops_away) + ] + + PaEss.Utilities.take_message(vars, :audio) + end {:error, :unknown} -> - case Utilities.ad_hoc_trip_description(audio.destination) do - {:ok, trip_description} -> - stop_or_stops = if audio.stops_away == 1, do: "stop", else: "stops" + do_ad_hoc_message(audio) + end + end - text = - "The next #{trip_description} is stopped #{audio.stops_away} #{stop_or_stops} away" + defp do_ad_hoc_message(audio) do + case Utilities.ad_hoc_trip_description(audio.destination) do + {:ok, trip_description} -> + stop_or_stops = if audio.stops_away == 1, do: "stop", else: "stops" - {:ad_hoc, {text, :audio}} + text = + "The next #{trip_description} is stopped #{audio.stops_away} #{stop_or_stops} away" - {:error, :unknown} -> - Logger.error( - "StoppedTrain.to_params unknown destination: #{inspect(audio.destination)}" - ) + {:ad_hoc, {text, :audio}} - nil - end + {:error, :unknown} -> + Logger.error( + "StoppedTrain.to_params unknown destination: #{inspect(audio.destination)}" + ) + + nil end end diff --git a/lib/content/audio/train_is_boarding.ex b/lib/content/audio/train_is_boarding.ex index 883483480..a95d03a6f 100644 --- a/lib/content/audio/train_is_boarding.ex +++ b/lib/content/audio/train_is_boarding.ex @@ -25,24 +25,32 @@ defmodule Content.Audio.TrainIsBoarding do def to_params(audio) do case PaEss.Utilities.destination_var(audio.destination) do {:ok, destination_var} -> - do_to_params(audio, destination_var) + if PaEss.Utilities.directional_destination?(audio.destination) do + do_ad_hoc_message(audio) + else + do_to_params(audio, destination_var) + end {:error, :unknown} -> - case PaEss.Utilities.ad_hoc_trip_description(audio.destination) do - {:ok, trip_description} -> - text = - if audio.track_number do - "The next #{trip_description} is now boarding, on track #{audio.track_number}" - else - "The next #{trip_description} is now boarding" - end - - {:ad_hoc, {text, :audio}} - - {:error, :unknown} -> - Logger.error("TrainIsBoarding.to_params unknown destination: #{audio.destination}") - nil - end + do_ad_hoc_message(audio) + end + end + + defp do_ad_hoc_message(audio) do + case PaEss.Utilities.ad_hoc_trip_description(audio.destination) do + {:ok, trip_description} -> + text = + if audio.track_number do + "The next #{trip_description} is now boarding, on track #{audio.track_number}" + else + "The next #{trip_description} is now boarding" + end + + {:ad_hoc, {text, :audio}} + + {:error, :unknown} -> + Logger.error("TrainIsBoarding.to_params unknown destination: #{audio.destination}") + nil end end diff --git a/lib/pa_ess/utilities.ex b/lib/pa_ess/utilities.ex index 950713d3c..5da6e42fa 100644 --- a/lib/pa_ess/utilities.ex +++ b/lib/pa_ess/utilities.ex @@ -207,6 +207,12 @@ defmodule PaEss.Utilities do def destination_var(:heath_street), do: {:ok, "4204"} def destination_var(:union_square), do: {:ok, "695"} def destination_var(:medford_tufts), do: {:ok, "852"} + def destination_var(:southbound), do: {:ok, "787"} + def destination_var(:northbound), do: {:ok, "788"} + def destination_var(:eastbound), do: {:ok, "867"} + def destination_var(:westbound), do: {:ok, "868"} + def destination_var(:inbound), do: {:ok, "33003"} + def destination_var(:outbound), do: {:ok, "33004"} def destination_var(_), do: {:error, :unknown} @doc """ @@ -348,50 +354,18 @@ defmodule PaEss.Utilities do end end + def directional_destination?(destination), + do: destination in [:eastbound, :westbound, :southbound, :northbound, :inbound, :outbound] + @spec ad_hoc_trip_description(PaEss.destination(), String.t() | nil) :: {:ok, String.t()} | {:error, :unknown} def ad_hoc_trip_description(destination, route_id \\ nil) - def ad_hoc_trip_description(destination, nil) - when destination in [:eastbound, :westbound, :southbound, :northbound] do - case destination_to_ad_hoc_string(destination) do - {:ok, destination_string} -> - {:ok, "#{destination_string} train"} - - _ -> - {:error, :unknown} - end - end - def ad_hoc_trip_description(destination, route_id) when destination == :eastbound and route_id in ["Green-B", "Green-C", "Green-D", "Green-E"] do ad_hoc_trip_description(destination) end - def ad_hoc_trip_description(destination, route_id) - when destination in [:eastbound, :westbound, :southbound, :northbound] do - case {destination_to_ad_hoc_string(destination), route_to_ad_hoc_string(route_id)} do - {{:ok, destination_string}, {:ok, route_string}} -> - {:ok, "#{destination_string} #{route_string} train"} - - {{:ok, _destination_string}, {:error, :unknown}} -> - ad_hoc_trip_description(destination) - - _ -> - {:error, :unknown} - end - end - - def ad_hoc_trip_description(destination, nil) do - case destination_to_ad_hoc_string(destination) do - {:ok, destination_string} -> - {:ok, "train to #{destination_string}"} - - _ -> - {:error, :unknown} - end - end - def ad_hoc_trip_description(destination, route_id) when destination in [ :lechmere, @@ -407,15 +381,48 @@ defmodule PaEss.Utilities do end def ad_hoc_trip_description(destination, route_id) do - case {destination_to_ad_hoc_string(destination), route_to_ad_hoc_string(route_id)} do - {{:ok, destination_string}, {:ok, route_string}} -> - {:ok, "#{route_string} train to #{destination_string}"} - - {{:ok, _destination_string}, {:error, :unknown}} -> - ad_hoc_trip_description(destination) - - _ -> - {:error, :unknown} + case {directional_destination?(destination), route_id} do + {true, nil} -> + case destination_to_ad_hoc_string(destination) do + {:ok, destination_string} -> + {:ok, "#{destination_string} train"} + + _ -> + {:error, :unknown} + end + + {true, route_id} -> + case {destination_to_ad_hoc_string(destination), route_to_ad_hoc_string(route_id)} do + {{:ok, destination_string}, {:ok, route_string}} -> + {:ok, "#{destination_string} #{route_string} train"} + + {{:ok, _destination_string}, {:error, :unknown}} -> + ad_hoc_trip_description(destination) + + _ -> + {:error, :unknown} + end + + {false, nil} -> + case destination_to_ad_hoc_string(destination) do + {:ok, destination_string} -> + {:ok, "train to #{destination_string}"} + + _ -> + {:error, :unknown} + end + + {false, route_id} -> + case {destination_to_ad_hoc_string(destination), route_to_ad_hoc_string(route_id)} do + {{:ok, destination_string}, {:ok, route_string}} -> + {:ok, "#{route_string} train to #{destination_string}"} + + {{:ok, _destination_string}, {:error, :unknown}} -> + ad_hoc_trip_description(destination) + + _ -> + {:error, :unknown} + end end end From 403ec672e594318d9aaa99922730a7217c57a5b2 Mon Sep 17 00:00:00 2001 From: Paul Kim Date: Fri, 21 Jul 2023 16:12:03 -0400 Subject: [PATCH 5/6] Early AM predictions suppression content (#660) * Add content changes * Add format_minutes helper * Address PR comments * Add a comment explaining JFK/UMass platform prediction audio for two-line prediction --- lib/content/audio/first_train_scheduled.ex | 58 ++++++++++++ lib/content/message/custom.ex | 2 +- .../early_am/destination_scheduled_time.ex | 18 ++++ .../message/early_am/destination_train.ex | 14 +++ .../message/early_am/scheduled_time.ex | 14 +++ lib/content/message/generic_paging.ex | 19 ++++ .../message/platform_prediction_bottom.ex | 16 ++++ lib/content/utilities.ex | 3 + lib/pa_ess/utilities.ex | 13 +++ lib/signs/utilities/audio.ex | 49 ++++++++++ .../audio/first_scheduled_train_test.exs | 57 ++++++++++++ test/signs/utilities/audio_test.exs | 91 +++++++++++++++++++ 12 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 lib/content/audio/first_train_scheduled.ex create mode 100644 lib/content/message/early_am/destination_scheduled_time.ex create mode 100644 lib/content/message/early_am/destination_train.ex create mode 100644 lib/content/message/early_am/scheduled_time.ex create mode 100644 lib/content/message/generic_paging.ex create mode 100644 lib/content/message/platform_prediction_bottom.ex create mode 100644 test/content/audio/first_scheduled_train_test.exs diff --git a/lib/content/audio/first_train_scheduled.ex b/lib/content/audio/first_train_scheduled.ex new file mode 100644 index 000000000..de4034d0a --- /dev/null +++ b/lib/content/audio/first_train_scheduled.ex @@ -0,0 +1,58 @@ +defmodule Content.Audio.FirstTrainScheduled do + defstruct [:destination, :scheduled_time] + + @type t :: %__MODULE__{ + destination: PaEss.destination(), + scheduled_time: DateTime.t() + } + + def from_messages( + %Content.Message.EarlyAm.DestinationTrain{destination: destination}, + %Content.Message.EarlyAm.ScheduledTime{scheduled_time: scheduled_time} + ) do + [ + %__MODULE__{ + destination: destination, + scheduled_time: scheduled_time + } + ] + end + + def from_messages(%Content.Message.EarlyAm.DestinationScheduledTime{ + destination: destination, + scheduled_time: scheduled_time + }) do + [ + %__MODULE__{ + destination: destination, + scheduled_time: scheduled_time + } + ] + end + + defimpl Content.Audio do + @the_first "866" + @train "864" + @is "533" + @scheduled_to_arrive_at "865" + + def to_params(%Content.Audio.FirstTrainScheduled{ + destination: destination, + scheduled_time: scheduled_time + }) do + {:ok, destination} = PaEss.Utilities.destination_var(destination) + + vars = [ + @the_first, + destination, + @train, + @is, + @scheduled_to_arrive_at, + PaEss.Utilities.time_hour_var(scheduled_time.hour), + PaEss.Utilities.time_minutes_var(scheduled_time.minute) + ] + + PaEss.Utilities.take_message(vars, :audio) + end + end +end diff --git a/lib/content/message/custom.ex b/lib/content/message/custom.ex index eaea878cd..2454eb588 100644 --- a/lib/content/message/custom.ex +++ b/lib/content/message/custom.ex @@ -37,7 +37,7 @@ defmodule Content.Message.Custom do cond do String.length(message) > max_length -> false - Regex.match?(~r/^[a-zA-Z0-9,\/!@' ]*$/, message) -> true + Regex.match?(~r/^[a-zA-Z0-9,\/!@': ]*$/, message) -> true true -> false end end diff --git a/lib/content/message/early_am/destination_scheduled_time.ex b/lib/content/message/early_am/destination_scheduled_time.ex new file mode 100644 index 000000000..dbff8edfd --- /dev/null +++ b/lib/content/message/early_am/destination_scheduled_time.ex @@ -0,0 +1,18 @@ +defmodule Content.Message.EarlyAm.DestinationScheduledTime do + @enforce_keys [:destination, :scheduled_time] + defstruct @enforce_keys + + @type t :: %__MODULE__{ + destination: PaEss.destination(), + scheduled_time: DateTime.t() + } + + defimpl Content.Message do + def to_string(%Content.Message.EarlyAm.DestinationScheduledTime{ + destination: destination, + scheduled_time: scheduled_time + }) do + "#{String.capitalize(PaEss.Utilities.destination_to_sign_string(destination))} due #{Content.Utilities.render_datetime_as_time(scheduled_time)}" + end + end +end diff --git a/lib/content/message/early_am/destination_train.ex b/lib/content/message/early_am/destination_train.ex new file mode 100644 index 000000000..3ed52f084 --- /dev/null +++ b/lib/content/message/early_am/destination_train.ex @@ -0,0 +1,14 @@ +defmodule Content.Message.EarlyAm.DestinationTrain do + @enforce_keys [:destination] + defstruct @enforce_keys + + @type t :: %__MODULE__{ + destination: PaEss.destination() + } + + defimpl Content.Message do + def to_string(%Content.Message.EarlyAm.DestinationTrain{destination: destination}) do + "#{String.capitalize(PaEss.Utilities.destination_to_sign_string(destination))} train" + end + end +end diff --git a/lib/content/message/early_am/scheduled_time.ex b/lib/content/message/early_am/scheduled_time.ex new file mode 100644 index 000000000..27309bdc5 --- /dev/null +++ b/lib/content/message/early_am/scheduled_time.ex @@ -0,0 +1,14 @@ +defmodule Content.Message.EarlyAm.ScheduledTime do + @enforce_keys [:scheduled_time] + defstruct @enforce_keys + + @type t :: %__MODULE__{ + scheduled_time: DateTime.t() + } + + defimpl Content.Message do + def to_string(%Content.Message.EarlyAm.ScheduledTime{scheduled_time: scheduled_time}) do + "due #{Content.Utilities.render_datetime_as_time(scheduled_time)}" + end + end +end diff --git a/lib/content/message/generic_paging.ex b/lib/content/message/generic_paging.ex new file mode 100644 index 000000000..9a8007370 --- /dev/null +++ b/lib/content/message/generic_paging.ex @@ -0,0 +1,19 @@ +defmodule Content.Message.GenericPaging do + @moduledoc """ + Can be used to page between multiple full-page messages e.g. headways and early AM timestamp + """ + @enforce_keys [:messages] + defstruct @enforce_keys + + @type t :: %__MODULE__{ + messages: [Content.Message.t()] + } + + defimpl Content.Message do + def to_string(%Content.Message.GenericPaging{messages: messages}) do + Enum.map(messages, fn message -> + {Content.Message.to_string(message), 6} + end) + end + end +end diff --git a/lib/content/message/platform_prediction_bottom.ex b/lib/content/message/platform_prediction_bottom.ex new file mode 100644 index 000000000..8c2471c70 --- /dev/null +++ b/lib/content/message/platform_prediction_bottom.ex @@ -0,0 +1,16 @@ +defmodule Content.Message.PlatformPredictionBottom do + defstruct [:stop_id, :minutes] + + @type t :: %__MODULE__{ + stop_id: String.t(), + minutes: integer() | :boarding | :arriving | :approaching | :max_time + } + + defimpl Content.Message do + def to_string(%Content.Message.PlatformPredictionBottom{stop_id: stop_id, minutes: minutes}) do + if minutes == :max_time or (is_integer(minutes) and minutes > 5), + do: "platform TBD", + else: "on #{Content.Utilities.stop_platform_name(stop_id)} platform" + end + end +end diff --git a/lib/content/utilities.ex b/lib/content/utilities.ex index 23696543d..8d8815de5 100644 --- a/lib/content/utilities.ex +++ b/lib/content/utilities.ex @@ -107,4 +107,7 @@ defmodule Content.Utilities do def route_branch_letter("Green-D"), do: :d def route_branch_letter("Green-E"), do: :e def route_branch_letter(_), do: nil + + def render_datetime_as_time(time), + do: Calendar.strftime(time, "%I:%M") |> String.replace_leading("0", "") end diff --git a/lib/pa_ess/utilities.ex b/lib/pa_ess/utilities.ex index 5da6e42fa..9d7ab4ee6 100644 --- a/lib/pa_ess/utilities.ex +++ b/lib/pa_ess/utilities.ex @@ -432,6 +432,19 @@ defmodule PaEss.Utilities do def green_line_branch_var(:d), do: "538" def green_line_branch_var(:e), do: "539" + def time_hour_var(hour) when hour >= 0 and hour < 24 do + adjusted_hour = rem(hour, 12) + + if adjusted_hour == 0, + do: "8011", + else: Integer.to_string(7999 + adjusted_hour) + end + + def time_minutes_var(min) + when min >= 0 and min < 60 do + Integer.to_string(9000 + min) + end + @spec replace_abbreviations(String.t()) :: String.t() def replace_abbreviations(text) when is_binary(text) do Enum.reduce( diff --git a/lib/signs/utilities/audio.ex b/lib/signs/utilities/audio.ex index 2bcb7255c..f097a9f60 100644 --- a/lib/signs/utilities/audio.ex +++ b/lib/signs/utilities/audio.ex @@ -281,6 +281,47 @@ defmodule Signs.Utilities.Audio do Audio.Predictions.from_sign_content(top_content, :top, multi_source?) end + defp get_audio( + %Message.GenericPaging{messages: top_messages}, + %Message.GenericPaging{messages: bottom_messages}, + _multi_source? + ) do + if length(top_messages) != length(bottom_messages) do + Logger.error( + "message_to_audio_warning Utilities.Audio generic_paging_mismatch some audios will be dropped: #{inspect(top_messages)} #{inspect(bottom_messages)}" + ) + end + + Enum.zip(top_messages, bottom_messages) + |> Enum.flat_map(fn {top, bottom} -> + get_audio(top, bottom, false) + end) + end + + defp get_audio( + %Message.EarlyAm.DestinationTrain{} = top, + %Message.EarlyAm.ScheduledTime{} = bottom, + _multi_source? + ) do + Audio.FirstTrainScheduled.from_messages(top, bottom) + end + + # Get audio for JFK/UMass special case two-line platform prediction + defp get_audio( + %Message.Predictions{station_code: "RJFK"} = top_content, + %Message.PlatformPredictionBottom{}, + multi_source? + ) do + # When the JFK/UMass Mezzanine sign is paging between two full pages where + # one page is a prediction with platform information on the second line, + # we have to override the zone field in Signs.Utilities.Messages.get_messages() + # to avoid triggering the usual paging platform prediction. The audio readout + # should be read normally though with platform info, so we add the zone back in here. + # + # Additionally, the second parameter here for from_sign_content/3 is arbitrary in this case. + Audio.Predictions.from_sign_content(%{top_content | zone: "m"}, :bottom, multi_source?) + end + defp get_audio(top, bottom, multi_source?) do get_audio_for_line(top, :top, multi_source?) ++ get_audio_for_line(bottom, :bottom, multi_source?) @@ -316,6 +357,14 @@ defmodule Signs.Utilities.Audio do Audio.NoServiceToDestination.from_message(message) end + defp get_audio_for_line( + %Message.EarlyAm.DestinationScheduledTime{} = message, + _line, + _multi_source? + ) do + Audio.FirstTrainScheduled.from_messages(message) + end + defp get_audio_for_line(%Message.Empty{}, _line, _multi_source?) do [] end diff --git a/test/content/audio/first_scheduled_train_test.exs b/test/content/audio/first_scheduled_train_test.exs new file mode 100644 index 000000000..130f6a8ae --- /dev/null +++ b/test/content/audio/first_scheduled_train_test.exs @@ -0,0 +1,57 @@ +defmodule Content.Audio.FirstTrainScheduledTest do + use ExUnit.Case, async: true + + describe "Content.Audio.to_params protocol" do + test "First train to Ashmont scheduled to arrive at 5 o'clock" do + audio = %Content.Audio.FirstTrainScheduled{ + destination: :ashmont, + scheduled_time: ~U[2023-07-19 05:00:00Z] + } + + assert Content.Audio.to_params(audio) == + {:canned, + {"115", + [ + "866", + "21000", + "4016", + "21000", + "864", + "21000", + "533", + "21000", + "865", + "21000", + "8004", + "21000", + "9000" + ], :audio}} + end + + test "First train to Ashmont scheduled to arrive at 5 oh 5" do + audio = %Content.Audio.FirstTrainScheduled{ + destination: :ashmont, + scheduled_time: ~U[2023-07-19 05:05:00Z] + } + + assert Content.Audio.to_params(audio) == + {:canned, + {"115", + [ + "866", + "21000", + "4016", + "21000", + "864", + "21000", + "533", + "21000", + "865", + "21000", + "8004", + "21000", + "9005" + ], :audio}} + end + end +end diff --git a/test/signs/utilities/audio_test.exs b/test/signs/utilities/audio_test.exs index f84f0981c..f7b3f18d4 100644 --- a/test/signs/utilities/audio_test.exs +++ b/test/signs/utilities/audio_test.exs @@ -733,5 +733,96 @@ defmodule Signs.Utilities.AudioTest do ^sign } = from_sign(sign) end + + test "Gets audios from full page paging" do + sign = %{ + @sign + | current_content_top: %Message.GenericPaging{ + messages: [ + %Message.Headways.Top{destination: :ashmont, vehicle_type: :train}, + %Message.Predictions{ + destination: :alewife, + minutes: 5, + route_id: "Red", + platform: :ashmont, + station_code: "RJFK" + } + ] + }, + current_content_bottom: %Message.GenericPaging{ + messages: [ + %Message.Headways.Bottom{range: {1, 3}}, + %Message.PlatformPredictionBottom{stop_id: "70086", minutes: 5} + ] + } + } + + assert { + [ + %Audio.VehiclesToDestination{ + language: :english, + destination: :ashmont, + headway_range: {1, 3} + }, + %Content.Audio.NextTrainCountdown{ + destination: :alewife, + minutes: 5, + platform: :ashmont + } + ], + ^sign + } = from_sign(sign) + end + + test "Drops audios from full page paging" do + sign = %{ + @sign + | current_content_top: %Message.GenericPaging{ + messages: [ + %Message.Headways.Top{destination: :ashmont, vehicle_type: :train}, + %Message.Predictions{ + destination: :alewife, + minutes: 5, + route_id: "Red", + platform: :ashmont, + station_code: "RJFK" + }, + %Message.Predictions{ + destination: :braintree, + minutes: 5, + route_id: "Red", + platform: :ashmont + } + ] + }, + current_content_bottom: %Message.GenericPaging{ + messages: [ + %Message.Headways.Bottom{range: {1, 3}}, + %Message.PlatformPredictionBottom{stop_id: "70086", minutes: 5} + ] + } + } + + log = + capture_log([level: :warn], fn -> + assert { + [ + %Audio.VehiclesToDestination{ + language: :english, + destination: :ashmont, + headway_range: {1, 3} + }, + %Content.Audio.NextTrainCountdown{ + destination: :alewife, + minutes: 5, + platform: :ashmont + } + ], + ^sign + } = from_sign(sign) + end) + + assert log =~ "message_to_audio_warning Utilities.Audio generic_paging_mismatch" + end end end From be77d8c87083beedd8d450bbcbe43d36119a7001 Mon Sep 17 00:00:00 2001 From: Paul Kim Date: Wed, 26 Jul 2023 09:27:51 -0400 Subject: [PATCH 6/6] JFK umass mezzanine refactor (#661) * Add content changes * Add kenmore B stops (#659) * Add other GL routes to Medford branch source configs (#656) * refactor jfk umass mezzanine code * Add format_minutes helper * add additional comment to clarify intent * Expand countup? logic to handle generic paging struct * Fix countup handling logic for JFK/UMass Mezzanine --- .../message/platform_prediction_bottom.ex | 5 +- lib/signs/utilities/messages.ex | 143 +++++++++++++++--- 2 files changed, 123 insertions(+), 25 deletions(-) diff --git a/lib/content/message/platform_prediction_bottom.ex b/lib/content/message/platform_prediction_bottom.ex index 8c2471c70..a593f5dd6 100644 --- a/lib/content/message/platform_prediction_bottom.ex +++ b/lib/content/message/platform_prediction_bottom.ex @@ -1,9 +1,10 @@ defmodule Content.Message.PlatformPredictionBottom do - defstruct [:stop_id, :minutes] + defstruct [:stop_id, :minutes, :destination] @type t :: %__MODULE__{ stop_id: String.t(), - minutes: integer() | :boarding | :arriving | :approaching | :max_time + minutes: integer() | :boarding | :arriving | :approaching | :max_time, + destination: PaEss.destination() } defimpl Content.Message do diff --git a/lib/signs/utilities/messages.ex b/lib/signs/utilities/messages.ex index a723d857d..f3654fe22 100644 --- a/lib/signs/utilities/messages.ex +++ b/lib/signs/utilities/messages.ex @@ -14,34 +14,100 @@ defmodule Signs.Utilities.Messages do Engine.Alerts.Fetcher.stop_status() ) :: Signs.Realtime.sign_messages() def get_messages(predictions, sign, sign_config, current_time, alert_status) do - cond do - match?({:static_text, {_, _}}, sign_config) -> - {:static_text, {line1, line2}} = sign_config - - {Content.Message.Custom.new(line1, :top), Content.Message.Custom.new(line2, :bottom)} - - sign_config == :off -> - {Content.Message.Empty.new(), Content.Message.Empty.new()} + messages = + cond do + match?({:static_text, {_, _}}, sign_config) -> + {:static_text, {line1, line2}} = sign_config + + {Content.Message.Custom.new(line1, :top), Content.Message.Custom.new(line2, :bottom)} + + sign_config == :off -> + {Content.Message.Empty.new(), Content.Message.Empty.new()} + + sign_config == :headway -> + get_headway_or_alert_messages(sign, current_time, alert_status) + + true -> + case Signs.Utilities.Predictions.get_messages(predictions, sign) do + {%Content.Message.Empty{}, %Content.Message.Empty{}} -> + get_headway_or_alert_messages(sign, current_time, alert_status) + + {top_message, %Content.Message.Empty{}} -> + {top_message, + get_paging_headway_or_alert_messages(sign, current_time, alert_status, :bottom)} + + {%Content.Message.Empty{}, bottom_message} -> + if match?( + %Content.Message.Predictions{station_code: "RJFK", zone: "m"}, + bottom_message + ) do + jfk_umass_headway_paging(bottom_message, sign, current_time, alert_status) + else + {get_paging_headway_or_alert_messages(sign, current_time, alert_status, :top), + bottom_message} + end + + messages -> + messages + end + end + + flip? = flip?(messages) + + if flip?, + do: do_flip(messages), + else: messages + end - sign_config == :headway -> - get_headway_or_alert_messages(sign, current_time, alert_status) + defp flip?(messages) do + case messages do + {%Content.Message.Headways.Paging{}, _} -> + true - true -> - case Signs.Utilities.Predictions.get_messages(predictions, sign) do - {%Content.Message.Empty{}, %Content.Message.Empty{}} -> - get_headway_or_alert_messages(sign, current_time, alert_status) + {%Content.Message.Empty{}, _} -> + true - {top_message, %Content.Message.Empty{}} -> - {top_message, - get_paging_headway_or_alert_messages(sign, current_time, alert_status, :bottom)} + _ -> + false + end + end - {%Content.Message.Empty{}, bottom_message} -> - {bottom_message, - get_paging_headway_or_alert_messages(sign, current_time, alert_status, :top)} + defp do_flip({top, bottom}) do + {bottom, top} + end - messages -> - messages - end + # Handles special case when JFK/UMass SB is on headways but NB is on platform prediction + defp jfk_umass_headway_paging(prediction, sign, current_time, alert_status) do + case get_paging_headway_or_alert_messages( + sign, + current_time, + alert_status, + :top + ) do + %Content.Message.Headways.Paging{destination: destination, range: range} -> + {%Content.Message.GenericPaging{ + messages: [ + # Make zone nil in order to prevent the usual paging platform message + %{prediction | zone: nil}, + %Content.Message.Headways.Top{ + destination: destination, + vehicle_type: :train + } + ] + }, + %Content.Message.GenericPaging{ + messages: [ + %Content.Message.PlatformPredictionBottom{ + stop_id: prediction.stop_id, + minutes: prediction.minutes, + destination: destination + }, + %Content.Message.Headways.Bottom{range: range} + ] + }} + + message -> + {message, prediction} end end @@ -136,6 +202,37 @@ defmodule Signs.Utilities.Messages do sign_msg == new_msg or countup?(sign_msg, new_msg) end + # Specific to JFK/UMass Mezzanine: + # Sign is remaining in full-page paging state + defp countup?( + %Content.Message.GenericPaging{messages: [%Content.Message.Predictions{} = p1 | _]}, + %Content.Message.GenericPaging{messages: [%Content.Message.Predictions{} = p2 | _]} + ) do + countup?(p1, p2) + end + + # Specific to JFK/UMass Mezzanine: + # Sign is transitioning from normal state to a full-page paging state + defp countup?( + %Content.Message.Predictions{} = p1, + %Content.Message.GenericPaging{ + messages: [%Content.Message.PlatformPredictionBottom{} = p2 | _] + } + ) do + countup?(p1, %Content.Message.Predictions{destination: p2.destination, minutes: p2.minutes}) + end + + # Specific to JFK/UMass Mezzanine: + # Sign is transitioning from full-page paging state to normal state + defp countup?( + %Content.Message.GenericPaging{ + messages: [%Content.Message.PlatformPredictionBottom{} = p1 | _] + }, + %Content.Message.Predictions{} = p2 + ) do + countup?(%Content.Message.Predictions{destination: p1.destination, minutes: p1.minutes}, p2) + end + defp countup?( %Content.Message.Predictions{destination: same, minutes: :arriving}, %Content.Message.Predictions{destination: same, minutes: :approaching}