-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: initial destinations framework
This adds a module which generates "destinations" from screen configs, the first step in the proposed "real-time destinations" logic. Both "permanent" (from typical route patterns) and "live" (from upcoming departures) destinations are generated. Initially there is only one possible state for a destination, `NoDepartures`. Following tasks will add more states and wire these into the `DupNew` screen variant.
- Loading branch information
1 parent
68b3470
commit 89cdbc0
Showing
10 changed files
with
443 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
defmodule Screens.RoutePatterns.RouteDirectionStops do | ||
@moduledoc false | ||
|
||
require Logger | ||
|
||
def parse_result(%{"data" => data, "included" => included}, route_id) do | ||
included_data = parse_included_data(included) | ||
parse_data(data, included_data, route_id) | ||
end | ||
|
||
def parse_result(_, _) do | ||
Logger.warning("Unrecognized format of route_pattern data.") | ||
:error | ||
end | ||
|
||
defp parse_included_data(data) do | ||
data | ||
|> Enum.map(fn item -> | ||
{{Map.get(item, "type"), Map.get(item, "id")}, parse_included(item)} | ||
end) | ||
|> Enum.into(%{}) | ||
end | ||
|
||
defp parse_included(%{"type" => "stop"} = item) do | ||
Screens.Stops.Parser.parse_stop(item) | ||
end | ||
|
||
defp parse_included(%{ | ||
"type" => "trip", | ||
"relationships" => %{"stops" => %{"data" => stops_data}} | ||
}) do | ||
Enum.map(stops_data, &parse_stop_data/1) | ||
end | ||
|
||
defp parse_stop_data(%{"id" => stop_id, "type" => "stop"}) do | ||
stop_id | ||
end | ||
|
||
defp parse_data(data, included_data, route_id) do | ||
filtered_data = filter_by_route(data, route_id) | ||
[typical_data | _] = filtered_data | ||
parse_route_pattern(typical_data, included_data) | ||
end | ||
|
||
defp filter_by_route(data, route_id) do | ||
Enum.filter(data, &has_related_route(&1, route_id)) | ||
end | ||
|
||
defp has_related_route(%{"relationships" => %{"route" => %{"data" => %{"id" => id}}}}, route_id) do | ||
route_id == id | ||
end | ||
|
||
defp has_related_route(_, _route_id) do | ||
false | ||
end | ||
|
||
defp parse_route_pattern( | ||
%{ | ||
"relationships" => %{ | ||
"representative_trip" => %{"data" => %{"id" => trip_id, "type" => "trip"}} | ||
} | ||
}, | ||
included_data | ||
) do | ||
# The only way this function output an empty array is if the trip data has an empty stop list | ||
# This happens occasionally in dev-green | ||
parsed = | ||
included_data | ||
|> Map.get({"trip", trip_id}) | ||
|> Enum.map(fn stop_id -> Map.get(included_data, {"stop", stop_id}) end) | ||
|
||
case parsed do | ||
# If `trip` is present, but the stop array is empty, there's a problem with the trip in the API | ||
[] -> | ||
Logger.warning("Trip data doesn't contain stop ids. trip_id: #{trip_id}") | ||
:error | ||
|
||
_ -> | ||
parsed | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
defmodule Screens.V2.RDS do | ||
@moduledoc """ | ||
Real-time Destination State. Represents a "destination" a rider could reach (ordinarily or | ||
currently) by taking a line from a stop, and the "state" of that destination, in the form of | ||
departure countdowns, a headway estimate, a message that service has ended for the day, etc. | ||
Conceptually, screen configuration is translated into a set of "destinations", each of which is | ||
assigned a "state", containing all the data required to present it. These can then be translated | ||
into widgets by screen-specific code. | ||
""" | ||
|
||
alias Screens.Lines.Line | ||
alias Screens.RoutePatterns.RoutePattern | ||
alias Screens.Routes.Route | ||
alias Screens.Stops.Stop | ||
alias Screens.V2.Departure | ||
|
||
alias ScreensConfig.V2.Departures | ||
alias ScreensConfig.V2.Departures.{Query, Section} | ||
|
||
alias __MODULE__.NoDepartures | ||
|
||
@type t :: %__MODULE__{ | ||
stop: Stop.t(), | ||
line: Line.t(), | ||
headsign: String.t(), | ||
state: NoDepartures.t() | ||
} | ||
@enforce_keys ~w[stop line headsign state]a | ||
defstruct @enforce_keys | ||
|
||
defmodule NoDepartures do | ||
@moduledoc """ | ||
The fallback state, presented as a headway or "no departures" message. A destination is in | ||
this state when A) displaying departures has been manually disabled for the relevant transit | ||
mode, or B) there simply aren't any upcoming departures we want to display, and as far as we | ||
can tell this is "normal"/expected. | ||
""" | ||
@type t :: %__MODULE__{} | ||
defstruct [] | ||
end | ||
|
||
@departure Application.compile_env(:screens, [__MODULE__, :departure_module], Departure) | ||
|
||
@route_pattern Application.compile_env( | ||
:screens, | ||
[__MODULE__, :route_pattern_module], | ||
RoutePattern | ||
) | ||
|
||
@stop Application.compile_env(:screens, [__MODULE__, :stop_module], Stop) | ||
|
||
@max_departure_minutes 90 | ||
|
||
@doc """ | ||
Generates destinations from departures widget configuration. | ||
Produces a list of destinations for each configured `Section`, in the same order the sections | ||
occur in the config. | ||
⚠️ Enforces that every section's query contains at least one ID in `stop_ids`. | ||
""" | ||
@spec get(Departures.t()) :: [[t()]] | ||
@spec get(Departures.t(), DateTime.t()) :: [[t()]] | ||
def get(%Departures{sections: sections}, now \\ DateTime.utc_now()), | ||
do: Enum.map(sections, &from_section(&1, now)) | ||
|
||
defp from_section( | ||
%Section{query: %Query{params: %Query.Params{stop_ids: stop_ids} = params}}, | ||
now | ||
) | ||
when stop_ids != [] do | ||
{:ok, child_stops} = @stop.fetch_child_stops(stop_ids) | ||
{:ok, typical_patterns} = params |> Map.put(:typicality, 1) |> @route_pattern.fetch() | ||
{:ok, departures} = @departure.fetch(params, include_schedules: true, now: now) | ||
|
||
(tuples_from_departures(departures, now) ++ | ||
tuples_from_patterns(typical_patterns, child_stops)) | ||
|> Enum.uniq() | ||
|> Enum.map(fn {stop, line, headsign} -> | ||
%__MODULE__{stop: stop, line: line, headsign: headsign, state: %NoDepartures{}} | ||
end) | ||
end | ||
|
||
defp tuples_from_departures(departures, now) do | ||
departures | ||
|> Enum.reject(fn d -> | ||
DateTime.diff(Departure.time(d), now, :minute) > @max_departure_minutes | ||
end) | ||
|> Enum.map(fn d -> | ||
{Departure.stop(d), Departure.route(d).line, Departure.representative_headsign(d)} | ||
end) | ||
end | ||
|
||
defp tuples_from_patterns(route_patterns, child_stops) do | ||
stop_ids = child_stops |> List.flatten() |> Enum.map(& &1.id) |> MapSet.new() | ||
|
||
Enum.flat_map( | ||
route_patterns, | ||
fn %RoutePattern{headsign: headsign, route: %Route{line: line}, stops: stops} -> | ||
stops | ||
|> Enum.filter(&(&1.id in stop_ids)) | ||
|> Enum.map(fn stop -> {stop, line, headsign} end) | ||
end | ||
) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
defmodule Screens.Stops.StopTest do | ||
use ExUnit.Case, async: true | ||
|
||
alias Screens.Stops.Stop | ||
|
||
describe "fetch_child_stops/2" do | ||
test "fetches the child stops of the provided stop IDs" do | ||
stop_attributes = %{ | ||
"name" => "test", | ||
"location_type" => 0, | ||
"platform_code" => "", | ||
"platform_name" => "" | ||
} | ||
|
||
get_json_fn = | ||
fn "stops", %{"filter[id]" => "sX,s1,p1,p2", "include" => "child_stops"} -> | ||
{ | ||
:ok, | ||
%{ | ||
"data" => [ | ||
# suppose sX doesn't exist | ||
%{ | ||
"id" => "s1", | ||
"attributes" => stop_attributes, | ||
"relationships" => %{"child_stops" => %{"data" => []}} | ||
}, | ||
%{ | ||
"id" => "p1", | ||
"attributes" => Map.put(stop_attributes, "location_type", 1), | ||
"relationships" => %{ | ||
"child_stops" => %{"data" => [%{"id" => "c1"}, %{"id" => "c2"}]} | ||
} | ||
}, | ||
%{ | ||
"id" => "p2", | ||
"attributes" => Map.put(stop_attributes, "location_type", 1), | ||
"relationships" => %{ | ||
"child_stops" => %{"data" => [%{"id" => "c3"}]} | ||
} | ||
} | ||
], | ||
"included" => [ | ||
%{"id" => "c1", "attributes" => stop_attributes}, | ||
%{"id" => "c2", "attributes" => stop_attributes}, | ||
%{"id" => "c3", "attributes" => stop_attributes} | ||
] | ||
} | ||
} | ||
end | ||
|
||
assert {:ok, [[], [%Stop{id: "s1"}], [%Stop{id: "c1"}, %Stop{id: "c2"}], [%Stop{id: "c3"}]]} = | ||
Stop.fetch_child_stops(~w[sX s1 p1 p2], get_json_fn) | ||
end | ||
end | ||
end |
Oops, something went wrong.