Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show diversion diagrams in diversion alerts #2067

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions assets/js/alert-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export const addAlertItemEventHandlers = () => {
[...document.querySelectorAll(`.${ITEM_SELECTOR}`)].forEach(alertItem => {
alertItem.addEventListener("click", handleAlertItemClick);
alertItem.addEventListener("keydown", handleAlertItemKeyPress);

if (document.querySelector(".diversions-template") && alertItem.querySelector("img")) {
alertItem.click();
}
});
}
};
Expand Down
93 changes: 61 additions & 32 deletions lib/alerts/alert.ex
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
defmodule Alerts.Alert do
@moduledoc "Module for representation of an alert, including information such as description, severity or additional URL to learn more"
alias Alerts.Priority
alias Alerts.InformedEntitySet, as: IESet
@moduledoc """
Module for representation of an alert, including information such as description, severity or additional URL to learn more
"""

use Timex

alias Alerts.{InformedEntitySet, Priority}

defstruct id: "",
header: "",
informed_entity: %IESet{},
active_period: [],
effect: :unknown,
severity: 5,
lifecycle: :unknown,
banner: "",
cause: "",
updated_at: Timex.now(),
created_at: nil,
description: "",
effect: :unknown,
header: "",
image_alternative_text: nil,
image: nil,
informed_entity: %InformedEntitySet{},
lifecycle: :unknown,
priority: :low,
url: "",
banner: ""
severity: 5,
updated_at: Timex.now(),
url: ""

@type period_pair :: {DateTime.t() | nil, DateTime.t() | nil}

Expand Down Expand Up @@ -51,30 +58,31 @@ defmodule Alerts.Alert do
| :unknown

@type severity :: 0..10

@type lifecycle :: :ongoing | :upcoming | :ongoing_upcoming | :new | :unknown

@type id_t :: String.t()

@type t :: %Alerts.Alert{
id: id_t(),
header: String.t(),
informed_entity: IESet.t(),
active_period: [period_pair],
banner: String.t() | nil,
cause: String.t(),
description: String.t() | nil,
effect: effect,
severity: severity,
header: String.t(),
image: String.t() | nil,
image_alternative_text: String.t() | nil,
informed_entity: InformedEntitySet.t(),
lifecycle: lifecycle,
updated_at: DateTime.t(),
description: String.t() | nil,
priority: Priority.priority_level(),
url: String.t() | nil,
banner: String.t() | nil
severity: severity,
updated_at: DateTime.t(),
url: String.t() | nil
}

@type icon_type :: :alert | :cancel | :none | :shuttle | :snow

use Timex

@stops_repo Application.compile_env!(:dotcom, :repo_modules)[:stops]

@ongoing_effects [
:cancellation,
:detour,
Expand Down Expand Up @@ -107,12 +115,17 @@ defmodule Alerts.Alert do
]

@diversion_effects [
:detour,
:shuttle,
:stop_closure,
:station_closure,
:detour
:stop_closure,
:suspension
]

@lifecycles [:ongoing, :upcoming, :ongoing_upcoming, :new, :unknown]

@stops_repo Application.compile_env!(:dotcom, :repo_modules)[:stops]

@spec new(Keyword.t()) :: t()
def new(keywords \\ [])

Expand Down Expand Up @@ -145,7 +158,7 @@ defmodule Alerts.Alert do

@spec ensure_entity_set(map) :: t()
defp ensure_entity_set(alert) do
%__MODULE__{alert | informed_entity: IESet.new(alert.informed_entity)}
%__MODULE__{alert | informed_entity: InformedEntitySet.new(alert.informed_entity)}
end

@spec all_types :: [effect]
Expand All @@ -154,13 +167,24 @@ defmodule Alerts.Alert do
@spec ongoing_effects :: [effect]
def ongoing_effects, do: @ongoing_effects

@spec lifecycles :: [lifecycle]
def lifecycles, do: @lifecycles

@spec get_entity(t, :route | :stop | :route_type | :trip | :direction_id) :: Enumerable.t()
@doc "Helper function for retrieving InformedEntity values for an alert"
def get_entity(%__MODULE__{informed_entity: %IESet{route: set}}, :route), do: set
def get_entity(%__MODULE__{informed_entity: %IESet{stop: set}}, :stop), do: set
def get_entity(%__MODULE__{informed_entity: %IESet{route_type: set}}, :route_type), do: set
def get_entity(%__MODULE__{informed_entity: %IESet{trip: set}}, :trip), do: set
def get_entity(%__MODULE__{informed_entity: %IESet{direction_id: set}}, :direction_id), do: set
def get_entity(%__MODULE__{informed_entity: %InformedEntitySet{route: set}}, :route), do: set
def get_entity(%__MODULE__{informed_entity: %InformedEntitySet{stop: set}}, :stop), do: set

def get_entity(%__MODULE__{informed_entity: %InformedEntitySet{route_type: set}}, :route_type),
do: set

def get_entity(%__MODULE__{informed_entity: %InformedEntitySet{trip: set}}, :trip), do: set

def get_entity(
%__MODULE__{informed_entity: %InformedEntitySet{direction_id: set}},
:direction_id
),
do: set

def access_alert_types do
[elevator_closure: "Elevator", escalator_closure: "Escalator", access_issue: "Other"]
Expand Down Expand Up @@ -232,8 +256,13 @@ defmodule Alerts.Alert do
def high_severity_or_high_priority?(_), do: false

@spec diversion?(t) :: boolean()
def diversion?(%{effect: effect}),
do: effect in @diversion_effects
def diversion?(alert) do
alert.effect in @diversion_effects &&
alert.active_period
|> List.first()
|> Kernel.elem(0)
|> Timex.after?(alert.created_at)
end

@spec municipality(t) :: String.t() | nil
def municipality(alert) do
Expand Down
61 changes: 37 additions & 24 deletions lib/alerts/informed_entity.ex
Original file line number Diff line number Diff line change
@@ -1,43 +1,51 @@
defmodule Alerts.InformedEntity do
@fields [:route, :route_type, :stop, :trip, :direction_id, :facility, :activities]
@empty_activities MapSet.new()
defstruct route: nil,
route_type: nil,
stop: nil,
trip: nil,
defstruct activities: MapSet.new(),
direction_id: nil,
facility: nil,
activities: @empty_activities
route: nil,
route_type: nil,
stop: nil,
trip: nil

@type t :: %Alerts.InformedEntity{
activities: MapSet.t(activity),
direction_id: 0 | 1 | nil,
facility: String.t() | nil,
route: String.t() | nil,
route_type: String.t() | nil,
stop: String.t() | nil,
trip: String.t() | nil,
direction_id: 0 | 1 | nil,
facility: String.t() | nil,
activities: MapSet.t(activity_type)
trip: String.t() | nil
}

@type activity_type ::
@type activity ::
:board
| :bringing_bike
| :exit
| :ride
| :park_car
| :bringing_bike
| :ride
| :store_bike
| :using_wheelchair
| :using_escalator
| :using_wheelchair

alias __MODULE__, as: IE
@activities [
:board,
:bringing_bike,
:exit,
:park_car,
:ride,
:store_bike,
:using_escalator,
:using_wheelchair
]

@doc """
@spec activities() :: list(activity)
def activities(), do: @activities

@doc """
Given a keyword list (with keys matching our fields), returns a new
InformedEntity. Additional keys are ignored.

"""
@spec from_keywords(list) :: IE.t()
@spec from_keywords(list) :: Alerts.InformedEntity.t()
def from_keywords(options) do
options
|> Enum.map(&ensure_value_type/1)
Expand All @@ -60,21 +68,24 @@ defmodule Alerts.InformedEntity do
Otherwise the nil can match any value in the other InformedEntity.

"""
@spec match?(IE.t(), IE.t()) :: boolean
def match?(%IE{} = first, %IE{} = second) do
@spec match?(Alerts.InformedEntity.t(), Alerts.InformedEntity.t()) :: boolean
def match?(%__MODULE__{} = first, %__MODULE__{} = second) do
share_a_key?(first, second) && do_match?(first, second)
end

@spec mapsets_match?(MapSet.t(), MapSet.t()) :: boolean()
def mapsets_match?(%MapSet{} = a, %MapSet{} = b)
when a == @empty_activities or b == @empty_activities,
when a == %MapSet{} or b == %MapSet{},
do: true

def mapsets_match?(%MapSet{} = a, %MapSet{} = b), do: has_intersect?(a, b)

defp has_intersect?(a, b), do: Enum.any?(a, &(&1 in b))

defp do_match?(f, s) do
@fields
__MODULE__.__struct__()
|> Map.keys()
|> List.delete(:__struct__)
|> Enum.all?(&key_match(Map.get(f, &1), Map.get(s, &1)))
end

Expand All @@ -85,7 +96,9 @@ defmodule Alerts.InformedEntity do
defp key_match(_, _), do: false

defp share_a_key?(first, second) do
@fields
__MODULE__.__struct__()
|> Map.keys()
|> List.delete(:__struct__)
|> Enum.any?(&shared_key(Map.get(first, &1), Map.get(second, &1)))
end

Expand Down
35 changes: 18 additions & 17 deletions lib/alerts/informed_entity_set.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,31 @@ defmodule Alerts.InformedEntitySet do
it's present in the InformedEntitySet. If it's not, there's no way for it
to match any of the InformedEntities inside.
"""
alias Alerts.InformedEntity, as: IE

defstruct route: MapSet.new(),
route_type: MapSet.new(),
stop: MapSet.new(),
trip: MapSet.new(),
alias Alerts.InformedEntity

defstruct activities: MapSet.new(),
direction_id: MapSet.new(),
entities: [],
facility: MapSet.new(),
activities: MapSet.new(),
entities: []
route: MapSet.new(),
route_type: MapSet.new(),
stop: MapSet.new(),
trip: MapSet.new()

@type t :: %__MODULE__{
activities: MapSet.t(),
direction_id: MapSet.t(),
entities: [InformedEntity.t()],
facility: MapSet.t(),
route: MapSet.t(),
route_type: MapSet.t(),
stop: MapSet.t(),
trip: MapSet.t(),
direction_id: MapSet.t(),
facility: MapSet.t(),
activities: MapSet.t(),
entities: [IE.t()]
trip: MapSet.t()
}

@doc "Create a new InformedEntitySet from a list of InformedEntitys"
@spec new([IE.t()]) :: t
@spec new([InformedEntity.t()]) :: t
def new(%__MODULE__{} = entity_set) do
entity_set
end
Expand All @@ -40,8 +41,8 @@ defmodule Alerts.InformedEntitySet do
end

@doc "Returns whether the given entity matches the set"
@spec match?(t, IE.t()) :: boolean
def match?(%__MODULE__{} = set, %IE{} = entity) do
@spec match?(t, InformedEntity.t()) :: boolean
def match?(%__MODULE__{} = set, %InformedEntity{} = entity) do
entity
|> Map.from_struct()
|> Enum.all?(&field_in_set?(set, &1))
Expand Down Expand Up @@ -73,7 +74,7 @@ defmodule Alerts.InformedEntitySet do
end

defp field_in_set?(set, {:activities, %MapSet{} = value}) do
IE.mapsets_match?(set.activities, value)
InformedEntity.mapsets_match?(set.activities, value)
end

defp field_in_set?(set, {key, value}) do
Expand All @@ -89,7 +90,7 @@ defmodule Alerts.InformedEntitySet do

defp try_all_entity_match(true, set, entity) do
# we only try matching against the whole set when the MapSets overlapped
Enum.any?(set, &IE.match?(&1, entity))
Enum.any?(set, &InformedEntity.match?(&1, entity))
end
end

Expand Down
17 changes: 10 additions & 7 deletions lib/alerts/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@ defmodule Alerts.Parser do
def parse(%JsonApi.Item{type: "alert", id: id, attributes: attributes}) do
Alerts.Alert.new(
id: id,
header: attributes["header"],
informed_entity: parse_informed_entity(attributes["informed_entity"]),
active_period: Enum.map(attributes["active_period"], &active_period/1),
effect: effect(attributes),
banner: description(attributes["banner"]),
cause: cause(attributes["cause"]),
severity: severity(attributes["severity"]),
created_at: parse_time(attributes["created_at"]),
description: description(attributes["description"]),
effect: effect(attributes),
header: attributes["header"],
image_alternative_text: attributes["image_alternative_text"],
image: attributes["image"],
informed_entity: parse_informed_entity(attributes["informed_entity"]),
lifecycle: lifecycle(attributes["lifecycle"]),
severity: severity(attributes["severity"]),
updated_at: parse_time(attributes["updated_at"]),
description: description(attributes["description"]),
url: description(attributes["url"]),
banner: description(attributes["banner"])
url: description(attributes["url"])
)
end

Expand Down
5 changes: 5 additions & 0 deletions lib/alerts/priority.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ defmodule Alerts.Priority do
@type priority_level :: :high | :low | :system
@ongoing_effects Alerts.Alert.ongoing_effects()

@priority_levels [:high, :low, :system]

@spec priority_levels() :: [priority_level]
def priority_levels, do: @priority_levels

@spec priority(map, DateTime.t()) :: priority_level
def priority(map, now \\ Util.now())

Expand Down
Loading