<%= Routes.Route.direction_name(@route, @itinerary_row.trip.direction_id) %>
diff --git a/lib/dotcom_web/templates/trip_plan/_to_from_inputs.html.eex b/lib/dotcom_web/templates/trip_plan/_to_from_inputs.html.eex
index 62f71313c8..8629405f90 100644
--- a/lib/dotcom_web/templates/trip_plan/_to_from_inputs.html.eex
+++ b/lib/dotcom_web/templates/trip_plan/_to_from_inputs.html.eex
@@ -24,7 +24,7 @@
>
diff --git a/lib/dotcom_web/templates/trip_plan/_walk_list_expand_link.html.eex b/lib/dotcom_web/templates/trip_plan/_walk_list_expand_link.html.eex
index 98a08d0466..9f9ea39c80 100644
--- a/lib/dotcom_web/templates/trip_plan/_walk_list_expand_link.html.eex
+++ b/lib/dotcom_web/templates/trip_plan/_walk_list_expand_link.html.eex
@@ -16,7 +16,7 @@
- <%= "(#{display_meters_as_miles(@itinerary_row.distance)} mi, #{display_seconds_as_minutes(@itinerary_row.duration)} min)" %>
+ <%= "(#{@itinerary_row.distance} mi, #{@itinerary_row.duration} min)" %>
Hide
Show
diff --git a/lib/dotcom_web/views/helpers.ex b/lib/dotcom_web/views/helpers.ex
index 46fa0aab4c..eab0a1e588 100644
--- a/lib/dotcom_web/views/helpers.ex
+++ b/lib/dotcom_web/views/helpers.ex
@@ -111,6 +111,19 @@ defmodule DotcomWeb.ViewHelpers do
svg("icon-#{name}-#{size}.svg")
end
+ # Massport shuttle routes
+ def line_icon(%Route{external_agency_name: "Massport", name: name}, _)
+ when is_binary(name) do
+ route_number = String.slice(name, 0..1)
+ svg("icon-massport-#{route_number}.svg")
+ end
+
+ # Logan Express shuttle routes
+ def line_icon(%Route{external_agency_name: "Logan Express", name: name}, _)
+ when is_binary(name) do
+ svg("icon-logan-express-#{name}.svg")
+ end
+
def line_icon(%Route{} = route, size) do
route
|> Route.icon_atom()
@@ -232,6 +245,10 @@ defmodule DotcomWeb.ViewHelpers do
@spec route_to_class(Routes.Route.t()) :: String.t()
def route_to_class(nil), do: ""
+ def route_to_class(%Routes.Route{external_agency_name: "Logan Express", name: name}) do
+ "logan-express-#{name}"
+ end
+
def route_to_class(route) do
route
|> Routes.Route.to_naive()
diff --git a/lib/dotcom_web/views/partial/svg_icon_with_circle.ex b/lib/dotcom_web/views/partial/svg_icon_with_circle.ex
index 8d6375fc05..87732624ec 100644
--- a/lib/dotcom_web/views/partial/svg_icon_with_circle.ex
+++ b/lib/dotcom_web/views/partial/svg_icon_with_circle.ex
@@ -218,6 +218,8 @@ defmodule DotcomWeb.PartialView.SvgIconWithCircle do
def title(%Routes.Route{id: "Green-" <> branch}),
do: DotcomWeb.ViewHelpers.mode_name(:green_line) <> " #{branch}"
+ def title(%Routes.Route{external_agency_name: "Massport", long_name: name}), do: name
+ def title(%Routes.Route{external_agency_name: "Logan Express", long_name: name}), do: name
def title(%Routes.Route{type: type}), do: DotcomWeb.ViewHelpers.mode_name(type)
def title(_icon), do: ""
end
diff --git a/lib/dotcom_web/views/trip_plan_view.ex b/lib/dotcom_web/views/trip_plan_view.ex
index caa5fbca44..b41a89f195 100644
--- a/lib/dotcom_web/views/trip_plan_view.ex
+++ b/lib/dotcom_web/views/trip_plan_view.ex
@@ -14,8 +14,6 @@ defmodule DotcomWeb.TripPlanView do
import Schedules.Repo, only: [end_of_rating: 0]
- @meters_per_mile 1609.34
-
@type fare_calculation :: %{
mode: Route.gtfs_route_type(),
# e.g. :commuter_rail
@@ -303,7 +301,7 @@ defmodule DotcomWeb.TripPlanView do
"_itinerary_row_step.html",
step: step.description,
alerts: step.alerts,
- stop_id: step.stop_id,
+ stop_id: if(step.stop, do: step.stop.id, else: ""),
itinerary_idx: itinerary_id,
row_idx: row_id,
mode_class: mode_class,
@@ -313,17 +311,6 @@ defmodule DotcomWeb.TripPlanView do
end
end
- @spec display_meters_as_miles(float) :: String.t()
- def display_meters_as_miles(meters) do
- :erlang.float_to_binary(meters / @meters_per_mile, decimals: 1)
- end
-
- @spec display_seconds_as_minutes(integer) :: String.t()
- def display_seconds_as_minutes(seconds) do
- minutes = Timex.Duration.to_minutes(seconds, :seconds)
- :erlang.integer_to_binary(Kernel.max(1, Kernel.round(minutes)))
- end
-
def format_additional_route(%Route{id: "Green" <> _branch} = route, direction_id) do
[
format_green_line_name(route.name),
@@ -375,7 +362,7 @@ defmodule DotcomWeb.TripPlanView do
svg_icon_with_circle(%SvgIconWithCircle{icon: :bus})
end
- def icon_for_route(%Route{type: 3} = route) do
+ def icon_for_route(%Route{type: 3, external_agency_name: nil} = route) do
DotcomWeb.ViewHelpers.bus_icon_pill(route)
end
@@ -383,6 +370,7 @@ defmodule DotcomWeb.TripPlanView do
svg_icon_with_circle(%SvgIconWithCircle{icon: route})
end
+ @spec datetime_from_query(nil | Dotcom.TripPlan.Query.t()) :: any()
def datetime_from_query(%Query{time: {:error, _}}), do: datetime_from_query(nil)
def datetime_from_query(%Query{time: {_depart_or_arrive, dt}}), do: dt
def datetime_from_query(nil), do: Util.now() |> Dotcom.TripPlan.DateTime.round_minute()
@@ -679,7 +667,7 @@ defmodule DotcomWeb.TripPlanView do
acc
else
name =
- if leg.long_name && leg.long_name =~ "Shuttle",
+ if leg.mode.route && leg.mode.route.long_name =~ "Shuttle",
do: Format.name(:shuttle),
else: Format.name(highest_fare.name)
@@ -759,16 +747,7 @@ defmodule DotcomWeb.TripPlanView do
transit_legs |> Enum.any?(fn leg -> leg_is_from_or_to_airport?(leg) end)
:contains_capeflyer ->
- transit_legs |> Enum.any?(fn leg -> leg.name == "CapeFLYER" end)
-
- :contains_blue_line ->
- transit_legs |> Enum.any?(fn leg -> leg.name == "Blue Line" end)
-
- :contains_east_boston_ferry ->
- transit_legs |> Enum.any?(fn leg -> leg.name == "East Boston Ferry" end)
-
- :contains_newburyport_rockport_line ->
- transit_legs |> Enum.any?(fn leg -> leg.name == "Newburyport/Rockport Line" end)
+ transit_legs |> Enum.any?(fn leg -> leg.mode.route.name == "CapeFLYER" end)
_ ->
false
diff --git a/lib/fares/fares.ex b/lib/fares/fares.ex
index 77111aed17..453716b2d8 100644
--- a/lib/fares/fares.ex
+++ b/lib/fares/fares.ex
@@ -145,11 +145,14 @@ defmodule Fares do
def express, do: @express_routes
- @type fare_atom :: Route.gtfs_route_type() | :express_bus
+ @type fare_atom :: Route.gtfs_route_type() | :express_bus | :free_service
@spec to_fare_atom(fare_atom | Route.id_t() | Route.t()) :: fare_atom
def to_fare_atom(route_or_atom) do
case route_or_atom do
+ %Route{description: :rail_replacement_bus} ->
+ :free_service
+
%Route{type: 3, id: id} ->
cond do
silver_line_rapid_transit?(id) -> :subway
diff --git a/lib/fares/format.ex b/lib/fares/format.ex
index 704add18f6..a36a3f5367 100644
--- a/lib/fares/format.ex
+++ b/lib/fares/format.ex
@@ -96,6 +96,7 @@ defmodule Fares.Format do
def name(:premium_ride), do: "Premium Ride"
def name(:invalid), do: "Invalid Fare"
def name(:massport_shuttle), do: "Massport Shuttle"
+ def name(:logan_express), do: "Logan Express"
def name("Massport-" <> _id), do: "Massport Shuttle"
@spec full_name(Fare.t() | nil) :: String.t() | iolist
diff --git a/lib/fares/month.ex b/lib/fares/month.ex
index db8e01ba84..1541fb8a4d 100644
--- a/lib/fares/month.ex
+++ b/lib/fares/month.ex
@@ -7,8 +7,6 @@ defmodule Fares.Month do
alias Routes.Route
alias Stops.Stop
- @routes_repo Application.compile_env!(:dotcom, :repo_modules)[:routes]
-
@type fare_fn :: (Keyword.t() -> [Fare.t()])
@spec recommended_pass(
@@ -21,12 +19,6 @@ defmodule Fares.Month do
def recommended_pass(route, origin_id, destination_id, fare_fn \\ &Repo.all/1)
def recommended_pass(nil, _, _, _), do: nil
- def recommended_pass(route_id, origin_id, destination_id, fare_fn)
- when is_binary(route_id) do
- route = @routes_repo.get(route_id)
- recommended_pass(route, origin_id, destination_id, fare_fn)
- end
-
def recommended_pass(route, origin_id, destination_id, fare_fn) do
route
|> get_fares(origin_id, destination_id, fare_fn)
@@ -43,11 +35,6 @@ defmodule Fares.Month do
def base_pass(route, origin_id, destination_id, fare_fn \\ &Repo.all/1)
def base_pass(nil, _, _, _), do: nil
- def base_pass(route_id, origin_id, destination_id, fare_fn) when is_binary(route_id) do
- route = @routes_repo.get(route_id)
- base_pass(route, origin_id, destination_id, fare_fn)
- end
-
def base_pass(route, origin_id, destination_id, fare_fn) do
route
|> get_fares(origin_id, destination_id, fare_fn)
@@ -55,7 +42,7 @@ defmodule Fares.Month do
end
@spec reduced_pass(
- Route.t() | Route.id_t(),
+ Route.t(),
Stop.id_t(),
Stop.id_t(),
fare_fn()
@@ -64,11 +51,6 @@ defmodule Fares.Month do
def reduced_pass(route, origin_id, destination_id, fare_fn \\ &Repo.all/1)
def reduced_pass(nil, _, _, _), do: nil
- def reduced_pass(route_id, origin_id, destination_id, fare_fn) when is_binary(route_id) do
- route = @routes_repo.get(route_id)
- reduced_pass(route, origin_id, destination_id, fare_fn)
- end
-
def reduced_pass(route, origin_id, destination_id, fare_fn) do
route
|> get_fares(origin_id, destination_id, fare_fn, :any)
diff --git a/lib/routes/parser.ex b/lib/routes/parser.ex
index 3baf74be3b..c048933a7a 100644
--- a/lib/routes/parser.ex
+++ b/lib/routes/parser.ex
@@ -69,18 +69,18 @@ defmodule Routes.Parser do
defp add_direction_suffix(name), do: name
@spec parse_gtfs_desc(String.t()) :: Route.gtfs_route_desc()
- defp parse_gtfs_desc(description)
- defp parse_gtfs_desc("Airport Shuttle"), do: :airport_shuttle
- defp parse_gtfs_desc("Commuter Rail"), do: :commuter_rail
- defp parse_gtfs_desc("Rapid Transit"), do: :rapid_transit
- defp parse_gtfs_desc("Local Bus"), do: :local_bus
- defp parse_gtfs_desc("Ferry"), do: :ferry
- defp parse_gtfs_desc("Rail Replacement Bus"), do: :rail_replacement_bus
- defp parse_gtfs_desc("Key Bus"), do: :key_bus_route
- defp parse_gtfs_desc("Supplemental Bus"), do: :supplemental_bus
- defp parse_gtfs_desc("Commuter Bus"), do: :commuter_bus
- defp parse_gtfs_desc("Community Bus"), do: :community_bus
- defp parse_gtfs_desc(_), do: :unknown
+ def parse_gtfs_desc(description)
+ def parse_gtfs_desc("Airport Shuttle"), do: :airport_shuttle
+ def parse_gtfs_desc("Commuter Rail"), do: :commuter_rail
+ def parse_gtfs_desc("Rapid Transit"), do: :rapid_transit
+ def parse_gtfs_desc("Local Bus"), do: :local_bus
+ def parse_gtfs_desc("Ferry"), do: :ferry
+ def parse_gtfs_desc("Rail Replacement Bus"), do: :rail_replacement_bus
+ def parse_gtfs_desc("Key Bus"), do: :key_bus_route
+ def parse_gtfs_desc("Supplemental Bus"), do: :supplemental_bus
+ def parse_gtfs_desc("Commuter Bus"), do: :commuter_bus
+ def parse_gtfs_desc("Community Bus"), do: :community_bus
+ def parse_gtfs_desc(_), do: :unknown
@spec parse_gtfs_fare_class(String.t()) :: Route.gtfs_fare_class()
defp parse_gtfs_fare_class(fare_class) when fare_class in ["Inner Express", "Outer Express"],
diff --git a/lib/routes/route.ex b/lib/routes/route.ex
index 952f7b906c..8f275dcba6 100644
--- a/lib/routes/route.ex
+++ b/lib/routes/route.ex
@@ -102,12 +102,6 @@ defmodule Routes.Route do
def type_atom("subway"), do: :subway
def type_atom("bus"), do: :bus
def type_atom("ferry"), do: :ferry
- def type_atom("Logan Express"), do: :logan_express
- def type_atom("909"), do: :logan_express
- def type_atom("2274"), do: :logan_express
- def type_atom("983"), do: :massport_shuttle
- def type_atom("2272"), do: :massport_shuttle
- def type_atom("Massport" <> _), do: :massport_shuttle
def type_atom("the_ride"), do: :the_ride
@spec types_for_mode(gtfs_route_type | subway_lines_type) :: [0..4]
@@ -138,7 +132,6 @@ defmodule Routes.Route do
def icon_atom(%__MODULE__{id: "Green-C"}), do: :green_line_c
def icon_atom(%__MODULE__{id: "Green-D"}), do: :green_line_d
def icon_atom(%__MODULE__{id: "Green-E"}), do: :green_line_e
- def icon_atom(%__MODULE__{id: "Massport" <> _}), do: :massport_shuttle
for silver_line_route <- @silver_line do
def icon_atom(%__MODULE__{id: unquote(silver_line_route)}), do: unquote(:silver_line)
diff --git a/lib/schedules/parser.ex b/lib/schedules/parser.ex
index 76b9307bc8..b4b1ea9c7b 100644
--- a/lib/schedules/parser.ex
+++ b/lib/schedules/parser.ex
@@ -105,6 +105,8 @@ defmodule Schedules.Parser do
nil
end
+ def trip(%JsonApi{data: []}), do: nil
+
def stop_id(%JsonApi.Item{
relationships: %{
"stop" => [
diff --git a/lib/trip_plan/api/open_trip_planner.ex b/lib/trip_plan/api/open_trip_planner.ex
deleted file mode 100644
index 553f041113..0000000000
--- a/lib/trip_plan/api/open_trip_planner.ex
+++ /dev/null
@@ -1,147 +0,0 @@
-defmodule TripPlan.Api.OpenTripPlanner do
- @moduledoc "Fetches data from the OpenTripPlanner API."
- alias TripPlan.{
- Itinerary,
- Leg,
- NamedPosition,
- PersonalDetail,
- PersonalDetail.Step,
- TransitDetail
- }
-
- @transit_modes ~w(SUBWAY TRAM BUS RAIL FERRY)s
-
- def plan(%NamedPosition{} = from, %NamedPosition{} = to, opts) do
- plan(NamedPosition.to_keywords(from), NamedPosition.to_keywords(to), opts)
- end
-
- def plan(from, to, opts) do
- otp_impl().plan(from, to, opts)
- |> parse()
- end
-
- defp otp_impl, do: Application.get_env(:dotcom, :trip_planner, OpenTripPlannerClient)
-
- defp parse({:error, _} = error), do: error
-
- defp parse({:ok, itineraries}) do
- {:ok, Enum.map(itineraries, &parse_itinerary/1)}
- end
-
- defp parse_itinerary(json) do
- score = json["accessibilityScore"]
-
- %Itinerary{
- start: parse_time(json["start"]),
- stop: parse_time(json["end"]),
- legs: Enum.map(json["legs"], &parse_leg/1),
- accessible?: if(score, do: score == 1.0),
- tag: json["tag"]
- }
- end
-
- defp parse_time(iso8601_formatted_datetime) do
- Timex.parse!(iso8601_formatted_datetime, "{ISO:Extended}")
- end
-
- defp parse_leg(json) do
- estimated_or_scheduled = fn key ->
- if json[key]["estimated"] do
- json[key]["estimated"]["time"]
- else
- json[key]["scheduledTime"]
- end
- end
-
- %Leg{
- start: parse_time(estimated_or_scheduled.("start")),
- stop: parse_time(estimated_or_scheduled.("end")),
- mode: parse_mode(json),
- from: parse_named_position(json["from"], "stop"),
- to: parse_named_position(json["to"], "stop"),
- polyline: json["legGeometry"]["points"],
- name: json["route"]["shortName"],
- long_name: json["route"]["longName"],
- type: json["agency"]["name"],
- url: json["agency"]["url"],
- description: json["mode"],
- distance: json["distance"],
- duration: json["duration"]
- }
- end
-
- def parse_named_position(json, "stop") do
- stop = json["stop"]
-
- %NamedPosition{
- name: json["name"],
- stop_id: if(stop, do: id_after_colon(stop["gtfsId"])),
- longitude: json["lon"],
- latitude: json["lat"]
- }
- end
-
- defp parse_mode(%{"mode" => "WALK"} = json) do
- %PersonalDetail{
- distance: json["distance"],
- steps: Enum.map(json["steps"], &parse_step/1)
- }
- end
-
- defp parse_mode(%{"mode" => mode} = json) when mode in @transit_modes do
- %TransitDetail{
- route_id: id_after_colon(json["route"]["gtfsId"]),
- trip_id: id_after_colon(json["trip"]["gtfsId"]),
- intermediate_stop_ids: Enum.map(json["intermediateStops"], &id_after_colon(&1["gtfsId"]))
- }
- end
-
- defp parse_step(json) do
- %Step{
- distance: json["distance"],
- relative_direction: parse_relative_direction(json["relativeDirection"]),
- absolute_direction: parse_absolute_direction(json["absoluteDirection"]),
- street_name: json["streetName"]
- }
- end
-
- # http://dev.opentripplanner.org/apidoc/1.0.0/json_RelativeDirection.html
- for dir <- ~w(
- depart
- hard_left
- left
- slightly_left
- continue
- slightly_right
- right
- hard_right
- circle_clockwise
- circle_counterclockwise
- elevator
- uturn_left
- uturn_right
- enter_station
- exit_station
- follow_signs)a do
- defp parse_relative_direction(unquote(String.upcase(Atom.to_string(dir)))), do: unquote(dir)
- end
-
- # http://dev.opentripplanner.org/apidoc/1.0.0/json_AbsoluteDirection.html
- for dir <- ~w(north northeast east southeast south southwest west northwest)a do
- defp parse_absolute_direction(unquote(String.upcase(Atom.to_string(dir)))), do: unquote(dir)
- end
-
- defp parse_absolute_direction(nil), do: nil
-
- defp id_after_colon(feed_colon_id) do
- [feed, id] = String.split(feed_colon_id, ":", parts: 2)
-
- # feed id is either mbta-ma-us (MBTA) or 22722274 (Massport), if it's neither, assume it's MBTA
- case feed do
- "mbta-ma-us" -> id
- "22722274" -> "Massport-" <> id
- "2272_2274" -> "Massport-" <> id
- _ -> id
- end
- end
-end
diff --git a/lib/trip_plan/itinerary.ex b/lib/trip_plan/itinerary.ex
index bd7e29ee3d..7fada75152 100644
--- a/lib/trip_plan/itinerary.ex
+++ b/lib/trip_plan/itinerary.ex
@@ -16,20 +16,24 @@ defmodule TripPlan.Itinerary do
@derive {Jason.Encoder, except: [:passes]}
@enforce_keys [:start, :stop]
defstruct [
+ :duration,
:start,
:stop,
:passes,
:tag,
+ :walk_distance,
legs: [],
accessible?: false
]
@type t :: %__MODULE__{
+ duration: non_neg_integer(),
start: DateTime.t(),
stop: DateTime.t(),
legs: [Leg.t()],
accessible?: boolean | nil,
passes: passes(),
+ walk_distance: float(),
tag: OpenTripPlannerClient.ItineraryTag.tag()
}
@@ -76,7 +80,7 @@ defmodule TripPlan.Itinerary do
def stop_ids(%__MODULE__{} = itinerary) do
itinerary
|> positions
- |> Enum.map(& &1.stop_id)
+ |> Enum.map(& &1.stop.id)
|> Enum.uniq()
end
@@ -86,6 +90,7 @@ defmodule TripPlan.Itinerary do
itinerary
|> Enum.map(&Leg.walking_distance/1)
|> Enum.sum()
+ |> Float.round(2)
end
@doc "Return a lost of all of the "
@@ -93,6 +98,7 @@ defmodule TripPlan.Itinerary do
def intermediate_stop_ids(itinerary) do
itinerary
|> Enum.flat_map(&leg_intermediate/1)
+ |> Enum.reject(&is_nil/1)
|> Enum.uniq()
end
@@ -102,8 +108,10 @@ defmodule TripPlan.Itinerary do
end
end
- defp leg_intermediate(%Leg{mode: %TransitDetail{intermediate_stop_ids: ids}}) do
- ids
+ defp leg_intermediate(%Leg{mode: %TransitDetail{intermediate_stops: stops}}) do
+ stops
+ |> Enum.reject(&is_nil/1)
+ |> Enum.map(& &1.id)
end
defp leg_intermediate(_) do
diff --git a/lib/trip_plan/leg.ex b/lib/trip_plan/leg.ex
index dbe30fab0b..3beb841fe2 100644
--- a/lib/trip_plan/leg.ex
+++ b/lib/trip_plan/leg.ex
@@ -9,16 +9,11 @@ defmodule TripPlan.Leg do
alias TripPlan.{NamedPosition, PersonalDetail, TransitDetail}
@derive {Jason.Encoder, only: [:from, :to, :mode]}
- defstruct start: DateTime.from_unix!(-1),
- stop: DateTime.from_unix!(0),
+ defstruct start: Timex.now(),
+ stop: Timex.now(),
mode: nil,
from: nil,
to: nil,
- name: nil,
- long_name: nil,
- type: nil,
- description: nil,
- url: nil,
polyline: "",
distance: 0.0,
duration: 0
@@ -28,23 +23,16 @@ defmodule TripPlan.Leg do
start: DateTime.t(),
stop: DateTime.t(),
mode: mode,
- from: NamedPosition.t() | nil,
+ from: NamedPosition.t(),
to: NamedPosition.t(),
- name: String.t(),
- long_name: String.t(),
- type: String.t(),
- description: String.t(),
- url: String.t(),
polyline: String.t(),
distance: Float.t(),
duration: Integer.t()
}
- @routes_repo Application.compile_env!(:dotcom, :repo_modules)[:routes]
-
@doc "Returns the route ID for the leg, if present"
@spec route_id(t) :: {:ok, Routes.Route.id_t()} | :error
- def route_id(%__MODULE__{mode: %TransitDetail{route_id: route_id}}), do: {:ok, route_id}
+ def route_id(%__MODULE__{mode: %TransitDetail{route: route}}), do: {:ok, route.id}
def route_id(%__MODULE__{}), do: :error
@doc "Returns the trip ID for the leg, if present"
@@ -54,7 +42,7 @@ defmodule TripPlan.Leg do
@spec route_trip_ids(t) :: {:ok, {Routes.Route.id_t(), Schedules.Trip.id_t()}} | :error
def route_trip_ids(%__MODULE__{mode: %TransitDetail{} = mode}) do
- {:ok, {mode.route_id, mode.trip_id}}
+ {:ok, {mode.route.id, mode.trip_id}}
end
def route_trip_ids(%__MODULE__{}) do
@@ -74,9 +62,9 @@ defmodule TripPlan.Leg do
@doc "Returns the stop IDs for the leg"
@spec stop_ids(t) :: [Stops.Stop.id_t()]
def stop_ids(%__MODULE__{from: from, to: to}) do
- for %NamedPosition{stop_id: stop_id} <- [from, to],
- stop_id do
- stop_id
+ for %NamedPosition{stop: stop} <- [from, to],
+ stop do
+ stop.id
end
end
@@ -84,11 +72,11 @@ defmodule TripPlan.Leg do
def stop_is_silver_line_airport?([], _), do: false
def stop_is_silver_line_airport?([leg], key) when not is_nil(leg) do
- route_id = leg.mode.route_id
+ route_id = leg.mode.route.id
stop_id =
leg
- |> Kernel.get_in([Access.key(key), Access.key(:stop_id)])
+ |> Kernel.get_in([Access.key(key), Access.key(:stop), Access.key(:id)])
Fares.silver_line_airport_stop?(route_id, stop_id)
end
@@ -104,15 +92,13 @@ defmodule TripPlan.Leg do
# between stops where we don't know the zones
@spec leg_missing_zone?(t) :: boolean
defp leg_missing_zone?(%__MODULE__{
- mode: %TransitDetail{route_id: route_id},
- from: %NamedPosition{stop_id: origin_id},
- to: %NamedPosition{stop_id: destination_id}
+ mode: %TransitDetail{route: route},
+ from: %NamedPosition{stop: origin},
+ to: %NamedPosition{stop: destination}
}) do
- route = @routes_repo.get(route_id)
-
if route do
Routes.Route.type_atom(route) == :commuter_rail and
- not Enum.all?([origin_id, destination_id], &Stops.Stop.has_zone?(&1))
+ not Enum.all?([origin, destination], &Stops.Stop.has_zone?(&1))
else
true
end
diff --git a/lib/trip_plan/named_position.ex b/lib/trip_plan/named_position.ex
index 25502cfdba..e23809dd20 100644
--- a/lib/trip_plan/named_position.ex
+++ b/lib/trip_plan/named_position.ex
@@ -3,13 +3,13 @@ defmodule TripPlan.NamedPosition do
@derive Jason.Encoder
defstruct name: "",
- stop_id: nil,
+ stop: nil,
latitude: nil,
longitude: nil
@type t :: %__MODULE__{
name: String.t(),
- stop_id: Stops.Stop.id_t() | nil,
+ stop: Stops.Stop.t() | nil,
latitude: float | nil,
longitude: float | nil
}
@@ -19,8 +19,12 @@ defmodule TripPlan.NamedPosition do
def longitude(%{longitude: longitude}), do: longitude
end
- def to_keywords(%__MODULE__{name: name, stop_id: stop_id, latitude: lat, longitude: lon}) do
- [name: name, stop_id: stop_id, lat_lon: {lat, lon}]
+ def to_keywords(%__MODULE__{name: name, stop: stop, latitude: lat, longitude: lon}) do
+ if stop do
+ [name: name, stop_id: stop.id, lat_lon: {lat, lon}]
+ else
+ [name: name, lat_lon: {lat, lon}]
+ end
end
def new(%LocationService.Address{} = address) do
diff --git a/lib/trip_plan/transfer.ex b/lib/trip_plan/transfer.ex
index f5c772f11d..effa20e06b 100644
--- a/lib/trip_plan/transfer.ex
+++ b/lib/trip_plan/transfer.ex
@@ -26,14 +26,15 @@ defmodule TripPlan.Transfer do
@doc "Searches a list of legs for evidence of an in-station subway transfer."
@spec subway_transfer?([Leg.t()]) :: boolean
def subway_transfer?([
- %Leg{to: %NamedPosition{stop_id: to_stop}, mode: %TransitDetail{route_id: route_to}},
+ %Leg{to: %NamedPosition{stop: to_stop}, mode: %TransitDetail{route: route_to}},
%Leg{
- from: %NamedPosition{stop_id: from_stop},
- mode: %TransitDetail{route_id: route_from}
+ from: %NamedPosition{stop: from_stop},
+ mode: %TransitDetail{route: route_from}
}
| _
- ]) do
- same_station?(from_stop, to_stop) and subway?(route_to) and subway?(route_from)
+ ])
+ when not is_nil(to_stop) and not is_nil(from_stop) do
+ same_station?(from_stop.id, to_stop.id) and subway?(route_to) and subway?(route_from)
end
def subway_transfer?([_ | legs]), do: subway_transfer?(legs)
@@ -48,12 +49,16 @@ defmodule TripPlan.Transfer do
- no transfers from a shuttle to any other mode
"""
@spec maybe_transfer?([Leg.t()]) :: boolean
- def maybe_transfer?([%Leg{mode: %TransitDetail{route_id: "Shuttle-" <> _}} | _]), do: false
-
def maybe_transfer?([
- first_leg = %Leg{mode: %TransitDetail{route_id: first_route}},
- middle_leg = %Leg{mode: %TransitDetail{route_id: middle_route}},
- last_leg = %Leg{mode: %TransitDetail{route_id: last_route}}
+ first_leg = %Leg{
+ mode: %TransitDetail{route: %Routes.Route{external_agency_name: nil} = first_route}
+ },
+ middle_leg = %Leg{
+ mode: %TransitDetail{route: %Routes.Route{external_agency_name: nil} = middle_route}
+ },
+ last_leg = %Leg{
+ mode: %TransitDetail{route: %Routes.Route{external_agency_name: nil} = last_route}
+ }
]) do
@multi_ride_transfers
|> Map.get(Fares.to_fare_atom(first_route), [])
@@ -63,8 +68,8 @@ defmodule TripPlan.Transfer do
end
def maybe_transfer?([
- %Leg{mode: %TransitDetail{route_id: from_route}},
- %Leg{mode: %TransitDetail{route_id: to_route}}
+ %Leg{mode: %TransitDetail{route: %Routes.Route{external_agency_name: nil} = from_route}},
+ %Leg{mode: %TransitDetail{route: %Routes.Route{external_agency_name: nil} = to_route}}
]) do
if from_route === to_route and
Enum.all?([from_route, to_route], &bus?/1) do
@@ -91,10 +96,10 @@ defmodule TripPlan.Transfer do
end
def bus_to_subway_transfer?([
- %Leg{mode: %TransitDetail{route_id: from_route}},
- %Leg{mode: %TransitDetail{route_id: to_route}}
+ %Leg{mode: %TransitDetail{route: %Routes.Route{external_agency_name: nil} = from_route}},
+ %Leg{mode: %TransitDetail{route: %Routes.Route{external_agency_name: nil} = to_route}}
]) do
- Fares.to_fare_atom(from_route) == :bus && Fares.to_fare_atom(to_route) == :subway
+ bus?(from_route) && subway?(to_route)
end
def bus_to_subway_transfer?(_), do: false
diff --git a/lib/trip_plan/transit_detail.ex b/lib/trip_plan/transit_detail.ex
index 83a1873e70..2588d35107 100644
--- a/lib/trip_plan/transit_detail.ex
+++ b/lib/trip_plan/transit_detail.ex
@@ -4,15 +4,25 @@ defmodule TripPlan.TransitDetail do
"""
alias Fares.Fare
+ alias OpenTripPlannerClient.Schema.Leg
@derive {Jason.Encoder, except: [:fares]}
- defstruct [:fares, route_id: "", trip_id: "", intermediate_stop_ids: []]
+ defstruct fares: %{
+ highest_one_way_fare: nil,
+ lowest_one_way_fare: nil,
+ reduced_one_way_fare: nil
+ },
+ mode: :TRANSIT,
+ route: nil,
+ trip_id: "",
+ intermediate_stops: []
@type t :: %__MODULE__{
- route_id: Routes.Route.id_t(),
+ fares: fares,
+ mode: Leg.mode(),
+ route: Routes.Route.t(),
trip_id: Schedules.Trip.id_t(),
- intermediate_stop_ids: [Stops.Stop.id_t()],
- fares: fares
+ intermediate_stops: [Stops.Stop.t()]
}
@type fares :: %{
diff --git a/lib/trip_planner/fare_passes.ex b/lib/trip_planner/fare_passes.ex
new file mode 100644
index 0000000000..2e6ba75227
--- /dev/null
+++ b/lib/trip_planner/fare_passes.ex
@@ -0,0 +1,262 @@
+defmodule Dotcom.TripPlanner.FarePasses do
+ @moduledoc """
+ Computing fare passes and prices for trip plan itineraries.
+ """
+ alias Fares.{Fare, Month, OneWay}
+ alias TripPlan.{Itinerary, Leg, NamedPosition, PersonalDetail, TransitDetail}
+
+ @spec with_passes(Itinerary.t()) :: Itinerary.t()
+ def with_passes(itinerary) do
+ base_month_pass = base_month_pass_for_itinerary(itinerary)
+
+ passes = %{
+ base_month_pass: base_month_pass,
+ recommended_month_pass: recommended_month_pass_for_itinerary(itinerary),
+ reduced_month_pass: reduced_month_pass_for_itinerary(itinerary, base_month_pass)
+ }
+
+ %Itinerary{itinerary | passes: passes}
+ end
+
+ @spec with_free_legs_if_from_airport(Itinerary.t()) :: Itinerary.t()
+ def with_free_legs_if_from_airport(itinerary) do
+ # If Logan airport is the origin, all subsequent subway trips from there should be free
+ first_transit_leg = itinerary |> Itinerary.transit_legs() |> List.first()
+
+ if Leg.stop_is_silver_line_airport?([first_transit_leg], :from) do
+ readjust_itinerary_with_free_fares(itinerary)
+ else
+ itinerary
+ end
+ end
+
+ @spec leg_with_fares(Leg.t()) :: Leg.t()
+ def leg_with_fares(%Leg{mode: %PersonalDetail{}} = leg), do: leg
+
+ # Logan Express is $9, except Back Bay to Logan is $3 and free from Logan.
+ def leg_with_fares(
+ %Leg{mode: %TransitDetail{route: %Routes.Route{external_agency_name: agency}}} =
+ leg
+ )
+ when is_binary(agency) do
+ logan_express? = agency == "Logan Express"
+ price = if logan_express?, do: logan_express_fare(leg), else: 0
+ mode = if logan_express?, do: :logan_express, else: :massport_shuttle
+ name = if logan_express?, do: :logan_express, else: :massport_shuttle
+
+ fares = %{
+ highest_one_way_fare: %Fares.Fare{
+ name: name,
+ cents: price,
+ duration: :single_trip,
+ mode: mode
+ },
+ lowest_one_way_fare: %Fares.Fare{
+ name: name,
+ cents: 0,
+ duration: :single_trip,
+ mode: mode,
+ reduced: :any
+ },
+ reduced_one_way_fare: %Fares.Fare{
+ name: name,
+ cents: 0,
+ duration: :single_trip,
+ mode: mode,
+ reduced: :any
+ }
+ }
+
+ mode_with_fares = %TransitDetail{leg.mode | fares: fares}
+ %Leg{leg | mode: mode_with_fares}
+ end
+
+ def leg_with_fares(%Leg{from: %NamedPosition{stop: nil}} = leg), do: leg
+ def leg_with_fares(%Leg{to: %NamedPosition{stop: nil}} = leg), do: leg
+
+ def leg_with_fares(%Leg{mode: %TransitDetail{route: route}} = leg) do
+ origin_id = leg.from.stop.id
+ destination_id = leg.to.stop.id
+
+ fares =
+ if Leg.fare_complete_transit_leg?(leg) do
+ recommended_fare = OneWay.recommended_fare(route, origin_id, destination_id)
+ base_fare = OneWay.base_fare(route, origin_id, destination_id)
+ reduced_fare = OneWay.reduced_fare(route, origin_id, destination_id)
+
+ %{
+ highest_one_way_fare: base_fare,
+ lowest_one_way_fare: recommended_fare,
+ reduced_one_way_fare: reduced_fare
+ }
+ else
+ %{
+ highest_one_way_fare: nil,
+ lowest_one_way_fare: nil,
+ reduced_one_way_fare: nil
+ }
+ end
+
+ mode_with_fares = %TransitDetail{leg.mode | fares: fares}
+ %Leg{leg | mode: mode_with_fares}
+ end
+
+ defp logan_express_fare(%Leg{from: from, mode: %TransitDetail{route: %Routes.Route{name: "BB"}}}) do
+ if String.contains?(from.name, "Logan Airport") || String.contains?(from.name, "Terminal") do
+ 0
+ else
+ 300
+ end
+ end
+
+ defp logan_express_fare(_), do: 900
+
+ @spec base_month_pass_for_itinerary(Itinerary.t()) :: Fare.t() | nil
+ defp base_month_pass_for_itinerary(%Itinerary{legs: legs}) do
+ legs
+ |> Enum.map(&highest_month_pass/1)
+ |> max_by_cents()
+ end
+
+ @spec recommended_month_pass_for_itinerary(Itinerary.t()) :: Fare.t() | nil
+ defp recommended_month_pass_for_itinerary(%Itinerary{legs: legs}) do
+ legs
+ |> Enum.map(&lowest_month_pass/1)
+ |> max_by_cents()
+ end
+
+ @spec reduced_month_pass_for_itinerary(Itinerary.t(), Fare.t() | nil) :: Fare.t() | nil
+ defp reduced_month_pass_for_itinerary(%Itinerary{legs: legs}, base_month_pass) do
+ reduced_pass =
+ legs
+ |> Enum.map(&reduced_pass/1)
+ |> max_by_cents()
+
+ if Fare.valid_modes(base_month_pass) -- Fare.valid_modes(reduced_pass) == [] do
+ reduced_pass
+ else
+ nil
+ end
+ end
+
+ @spec highest_month_pass(Leg.t()) :: Fare.t() | nil
+ defp highest_month_pass(%Leg{mode: %PersonalDetail{}}), do: nil
+ defp highest_month_pass(%Leg{from: %NamedPosition{stop: nil}}), do: nil
+ defp highest_month_pass(%Leg{to: %NamedPosition{stop: nil}}), do: nil
+
+ defp highest_month_pass(
+ %Leg{
+ mode: %TransitDetail{route: route},
+ from: %NamedPosition{stop: origin},
+ to: %NamedPosition{stop: destination}
+ } = leg
+ ) do
+ if Leg.fare_complete_transit_leg?(leg) do
+ Month.base_pass(route, origin.id, destination.id)
+ else
+ nil
+ end
+ end
+
+ @spec lowest_month_pass(Leg.t()) :: Fare.t() | nil
+ defp lowest_month_pass(%Leg{mode: %PersonalDetail{}}), do: nil
+ defp lowest_month_pass(%Leg{from: %NamedPosition{stop: nil}}), do: nil
+ defp lowest_month_pass(%Leg{to: %NamedPosition{stop: nil}}), do: nil
+
+ defp lowest_month_pass(
+ %Leg{
+ mode: %TransitDetail{route: route},
+ from: %NamedPosition{stop: origin},
+ to: %NamedPosition{stop: destination}
+ } = leg
+ ) do
+ if Leg.fare_complete_transit_leg?(leg) do
+ Month.recommended_pass(route, origin.id, destination.id)
+ else
+ nil
+ end
+ end
+
+ @spec reduced_pass(Leg.t()) :: Fare.t() | nil
+ defp reduced_pass(%Leg{mode: %PersonalDetail{}}), do: nil
+ defp reduced_pass(%Leg{from: %NamedPosition{stop: nil}}), do: nil
+ defp reduced_pass(%Leg{to: %NamedPosition{stop: nil}}), do: nil
+
+ defp reduced_pass(
+ %Leg{
+ mode: %TransitDetail{route: route},
+ from: %NamedPosition{stop: origin},
+ to: %NamedPosition{stop: destination}
+ } = leg
+ ) do
+ if Leg.fare_complete_transit_leg?(leg) do
+ Month.reduced_pass(route, origin.id, destination.id)
+ else
+ nil
+ end
+ end
+
+ @spec max_by_cents([Fare.t() | nil]) :: Fare.t() | nil
+ defp max_by_cents(fares), do: Enum.max_by(fares, ¢s_for_max/1, fn -> nil end)
+
+ @spec cents_for_max(Fare.t() | nil) :: non_neg_integer
+ defp cents_for_max(nil), do: 0
+ defp cents_for_max(%Fare{cents: cents}), do: cents
+
+ @spec readjust_itinerary_with_free_fares(Itinerary.t()) :: Itinerary.t()
+ def readjust_itinerary_with_free_fares(itinerary) do
+ transit_legs =
+ itinerary.legs
+ |> Enum.with_index()
+ |> Enum.filter(fn {leg, _idx} -> Leg.transit?(leg) end)
+
+ # set the subsequent subway legs' highest_fare to nil so they get ignored by the fare calculations afterwards:
+ legs_after_airport = List.delete_at(transit_legs, 0)
+
+ free_subway_legs =
+ if Enum.empty?(legs_after_airport) do
+ []
+ else
+ Enum.filter(
+ legs_after_airport,
+ fn {leg, _idx} ->
+ leg
+ |> Fares.get_fare_by_type(:highest_one_way_fare)
+ |> Map.get(:mode)
+ |> Kernel.==(:subway)
+ end
+ )
+ end
+
+ free_subway_indexes =
+ if Enum.empty?(free_subway_legs) do
+ []
+ else
+ free_subway_legs
+ |> Enum.map(fn {_leg, index} ->
+ index
+ end)
+ end
+
+ readjusted_legs =
+ itinerary.legs
+ |> Enum.with_index()
+ |> Enum.map(fn {leg, index} ->
+ if index in free_subway_indexes do
+ %{
+ leg
+ | mode: %{
+ leg.mode
+ | fares: %{
+ highest_one_way_fare: nil
+ }
+ }
+ }
+ else
+ leg
+ end
+ end)
+
+ %Itinerary{itinerary | legs: readjusted_legs}
+ end
+end
diff --git a/lib/trip_planner/open_trip_planner.ex b/lib/trip_planner/open_trip_planner.ex
new file mode 100644
index 0000000000..e55ea69383
--- /dev/null
+++ b/lib/trip_planner/open_trip_planner.ex
@@ -0,0 +1,47 @@
+defmodule TripPlanner.OpenTripPlanner do
+ @moduledoc """
+ Makes requests to OpenTripPlanner via the OpenTripPlannerClient library, and
+ parses the result.
+ """
+
+ alias Dotcom.TripPlanner.Parser
+
+ alias OpenTripPlannerClient.ItineraryTag.{
+ EarliestArrival,
+ LeastWalking,
+ MostDirect,
+ ShortestTrip
+ }
+
+ alias TripPlan.NamedPosition
+
+ @otp_module Application.compile_env!(:dotcom, :otp_module)
+
+ @doc """
+ Requests to OpenTripPlanner's /plan GraphQL endpoint and parses the response..
+ """
+ @spec plan(NamedPosition.t(), NamedPosition.t(), Keyword.t()) ::
+ OpenTripPlannerClient.Behaviour.plan_result()
+ def plan(%NamedPosition{} = from, %NamedPosition{} = to, opts) do
+ with from <- NamedPosition.to_keywords(from),
+ to <- NamedPosition.to_keywords(to),
+ opts <- Keyword.put_new(opts, :tags, tags(opts)) do
+ @otp_module.plan(from, to, opts)
+ |> parse()
+ end
+ end
+
+ def tags(opts) do
+ if Keyword.has_key?(opts, :arrive_by) do
+ [ShortestTrip, MostDirect, LeastWalking]
+ else
+ [EarliestArrival, MostDirect, LeastWalking]
+ end
+ end
+
+ defp parse({:error, _} = error), do: error
+
+ defp parse({:ok, itineraries}) do
+ {:ok, Enum.map(itineraries, &Parser.parse/1)}
+ end
+end
diff --git a/lib/trip_planner/parser.ex b/lib/trip_planner/parser.ex
new file mode 100644
index 0000000000..8ee28696ea
--- /dev/null
+++ b/lib/trip_planner/parser.ex
@@ -0,0 +1,198 @@
+defmodule Dotcom.TripPlanner.Parser do
+ @moduledoc """
+ Parse results from OpenTripPlanner:
+
+ 1. Convert all distances from meters, to miles.
+ 2. Convert all durations from seconds, to minutes.
+ 3. Add fare passes to each itinerary
+ 4. Add fare to each leg.
+ 5. For places, get more information for Stops and Routes if they're within the
+ MBTA system.
+ """
+
+ alias TripPlan.{Itinerary, Leg, NamedPosition, PersonalDetail, TransitDetail}
+ alias Dotcom.TripPlanner.FarePasses
+ alias OpenTripPlannerClient.Schema
+
+ @spec parse(Schema.Itinerary.t()) :: Itinerary.t()
+ def parse(
+ %Schema.Itinerary{
+ accessibility_score: accessibility_score,
+ duration: seconds,
+ legs: legs,
+ walk_distance: meters
+ } = itinerary
+ ) do
+ legs_with_fares = Enum.map(legs, &parse/1)
+
+ struct(
+ Itinerary,
+ Map.merge(Map.from_struct(itinerary), %{
+ accessible?: accessibility_score == 1,
+ duration: minutes(seconds),
+ legs: legs_with_fares,
+ stop: itinerary.end,
+ walk_distance: miles(meters)
+ })
+ )
+ |> FarePasses.with_passes()
+ |> FarePasses.with_free_legs_if_from_airport()
+ end
+
+ @spec parse(Schema.Leg.t()) :: Leg.t()
+ def parse(%Schema.Leg{agency: agency} = leg) do
+ agency_name = if(agency, do: agency.name)
+
+ %Leg{
+ from: place(leg.from),
+ mode: mode(leg, agency_name),
+ start: time(leg.start),
+ stop: time(leg.end),
+ to: place(leg.to),
+ polyline: leg.leg_geometry.points,
+ distance: miles(leg.distance),
+ duration: minutes(leg.duration)
+ }
+ |> FarePasses.leg_with_fares()
+ end
+
+ defp time(%Schema.LegTime{estimated: nil, scheduled_time: time}), do: time
+ defp time(%Schema.LegTime{estimated: %{time: time}}), do: time
+
+ @spec place(Schema.Place.t()) :: NamedPosition.t()
+ def place(%Schema.Place{
+ stop: stop,
+ lon: longitude,
+ lat: latitude,
+ name: name
+ }) do
+ stop =
+ if(match?(%Schema.Stop{}, stop),
+ do: build_stop(stop, %{latitude: latitude, longitude: longitude})
+ )
+
+ %NamedPosition{
+ stop: stop,
+ name: name,
+ latitude: latitude,
+ longitude: longitude
+ }
+ end
+
+ def mode(%Schema.Leg{distance: distance, mode: "WALK", steps: steps}, _) do
+ %PersonalDetail{
+ distance: miles(distance),
+ steps: Enum.map(steps, &step/1)
+ }
+ end
+
+ def mode(
+ %Schema.Leg{
+ intermediate_stops: stops,
+ mode: mode,
+ route: route,
+ transit_leg: true,
+ trip: trip
+ },
+ agency_name
+ ) do
+ %TransitDetail{
+ mode: mode,
+ intermediate_stops: Enum.map(stops, &build_stop/1),
+ route: build_route(route, agency_name),
+ trip_id: id_from_gtfs(trip.gtfs_id)
+ }
+ end
+
+ def step(%Schema.Step{
+ distance: distance,
+ absolute_direction: absolute_direction,
+ relative_direction: relative_direction,
+ street_name: street_name
+ }) do
+ struct(PersonalDetail.Step, %{
+ distance: miles(distance),
+ street_name: street_name,
+ absolute_direction:
+ if(absolute_direction, do: String.downcase(absolute_direction) |> String.to_atom()),
+ relative_direction:
+ if(relative_direction, do: String.downcase(relative_direction) |> String.to_atom())
+ })
+ end
+
+ defp build_route(
+ %Schema.Route{
+ gtfs_id: gtfs_id,
+ short_name: short_name,
+ long_name: long_name,
+ type: type,
+ color: color,
+ desc: desc
+ },
+ agency_name
+ ) do
+ id = id_from_gtfs(gtfs_id)
+
+ %Routes.Route{
+ id: id,
+ external_agency_name: if(agency_name !== "MBTA", do: agency_name),
+ # Massport GTFS sometimes omits short_name
+ name: short_name || id,
+ long_name: route_name(agency_name, short_name, long_name),
+ type: type,
+ color: route_color(agency_name, short_name, color),
+ description: Routes.Parser.parse_gtfs_desc(desc)
+ }
+ end
+
+ defp route_name("Logan Express", _short_name, long_name), do: "#{long_name} Logan Express"
+
+ defp route_name("Massport", short_name, long_name) do
+ name = if long_name, do: long_name, else: short_name
+ "Massport Shuttle #{name}"
+ end
+
+ defp route_name(_, short_name, long_name) do
+ if long_name, do: long_name, else: short_name
+ end
+
+ defp route_color("Logan Express", "WO", _), do: "00954c"
+ defp route_color("Logan Express", "BB", _), do: "f16823"
+ defp route_color("Logan Express", "PB", _), do: "704c9f"
+ defp route_color(_, _, color), do: color
+
+ # only create a %Stop{} if the GTFS ID is from MBTA
+ defp build_stop(stop, attributes \\ %{})
+
+ defp build_stop(
+ %Schema.Stop{
+ gtfs_id: "mbta-ma-us:" <> gtfs_id,
+ name: name
+ },
+ attributes
+ ) do
+ %Stops.Stop{
+ id: gtfs_id,
+ name: name
+ }
+ |> struct(attributes)
+ end
+
+ defp build_stop(_, _), do: nil
+
+ defp id_from_gtfs(gtfs_id) do
+ case String.split(gtfs_id, ":") do
+ [_, id] -> id
+ _ -> nil
+ end
+ end
+
+ defp miles(meters) do
+ Float.ceil(meters / 1609.34, 1)
+ end
+
+ defp minutes(seconds) do
+ minutes = Timex.Duration.to_minutes(seconds, :seconds)
+ Kernel.max(1, Kernel.round(minutes))
+ end
+end
diff --git a/mix.exs b/mix.exs
index 1b9a85144a..2901029554 100644
--- a/mix.exs
+++ b/mix.exs
@@ -69,7 +69,7 @@ defmodule DotCom.Mixfile do
# Note that you should also update `.github/dependabot.yml` and remove ignore overrides for any dependencies you update.
defp deps do
[
- {:absinthe_client, "0.1.0"},
+ {:absinthe_client, "0.1.1"},
# latest version 1.0.7; cannot upgrade because of server_sent_event_stage expects castore < 1
{:castore, "0.1.22"},
{:crc, "0.10.5"},
@@ -84,11 +84,11 @@ defmodule DotCom.Mixfile do
{:ex_aws_s3, "2.5.3"},
{:ex_aws_ses, "2.4.1"},
{:ex_doc, "0.34.0", only: :dev},
- {:ex_machina, "2.7.0", only: [:dev, :test]},
+ {:ex_machina, "2.7.0"},
{:ex_unit_summary, "0.1.0", only: [:dev, :test]},
# latest version 0.18.1; cannot upgrade because expects castore >= 1
{:excoveralls, "0.16.1", only: :test},
- {:faker, "0.18.0", only: [:dev, :test]},
+ {:faker, "0.18.0"},
{:floki, "0.36.2"},
{:gen_stage, "1.2.1"},
{:gettext, "0.24.0"},
@@ -105,11 +105,7 @@ defmodule DotCom.Mixfile do
{:mox, "1.1.0", [only: :test]},
{:nebulex, "2.6.1"},
{:nebulex_redis_adapter, "2.4.0"},
- {:open_trip_planner_client,
- [
- github: "thecristen/open_trip_planner_client",
- ref: "v0.8.1"
- ]},
+ {:open_trip_planner_client, [github: "thecristen/open_trip_planner_client", tag: "v0.9.1"]},
{:parallel_stream, "1.1.0"},
# latest version 1.7.12
{:phoenix, "1.6.16"},
diff --git a/mix.lock b/mix.lock
index fb0ace971d..c0d6a166f0 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,5 +1,5 @@
%{
- "absinthe_client": {:hex, :absinthe_client, "0.1.0", "a3bafc1dff141073a2a7fd926942fb10afb4d45295f0b6df46f6f1955ececaac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:req, "~> 0.3.0", [hex: :req, repo: "hexpm", optional: false]}, {:slipstream, "~> 1.0", [hex: :slipstream, repo: "hexpm", optional: false]}], "hexpm", "a7ec3e13da9b463cb024dba4733c2fa31a0690a3bfa897b9df6bdd544a4d6f91"},
+ "absinthe_client": {:hex, :absinthe_client, "0.1.1", "1e778d587a27b85ecc35e4a5fedc64c85d9fdfd05395745c7af5345564dff54e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: false]}, {:slipstream, "~> 1.0", [hex: :slipstream, repo: "hexpm", optional: false]}], "hexpm", "e75a28c5bb647f485e9c03bbc3a47e7783742794bd4c10f3307a495a9e7273b6"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
@@ -40,6 +40,7 @@
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
+ "jason_structs": {:git, "https://github.com/ygunayer/jason_structs.git", "e12ab67150192a7fe5c41c9203222e206cd57e4a", [branch: "ygunayer-namespaced-structs"]},
"logster": {:hex, :logster, "1.1.1", "d6fddac540dd46adde0c894024500867fe63b0043713f842c62da5815e21db10", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d18e852c430812ad1c9756998ebe46ec814c724e6eb551a512d7e3f8dee24cef"},
"mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"},
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
@@ -59,7 +60,7 @@
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
- "open_trip_planner_client": {:git, "https://github.com/thecristen/open_trip_planner_client.git", "bac452739c732ceebab0762e10bd362a88417a4a", [ref: "v0.8.1"]},
+ "open_trip_planner_client": {:git, "https://github.com/thecristen/open_trip_planner_client.git", "6944639bbfdd5b02e71cbe5e0431bdb3cd891a8c", [tag: "v0.9.1"]},
"parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"phoenix": {:hex, :phoenix, "1.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"},
@@ -99,6 +100,7 @@
"telemetry_test": {:hex, :telemetry_test, "0.1.2", "122d927567c563cf57773105fa8104ae4299718ec2cbdddcf6776562c7488072", [:mix], [{:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7bd41a49ecfd33ecd82d2c7edae19a5736f0d2150206d0ee290dcf3885d0e14d"},
"tesla": {:hex, :tesla, "1.9.0", "8c22db6a826e56a087eeb8cdef56889731287f53feeb3f361dec5d4c8efb6f14", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7c240c67e855f7e63e795bf16d6b3f5115a81d1f44b7fe4eadbf656bae0fef8a"},
"timex": {:hex, :timex, "3.1.24", "d198ae9783ac807721cca0c5535384ebdf99da4976be8cefb9665a9262a1e9e3", [:mix], [{:combine, "~> 0.7", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "ca852258d788542c263b12dbf55375fe2ccf5674e7b20995e3d84d2d4412bc0f"},
+ "typed_struct": {:hex, :typed_struct, "0.2.1", "e1993414c371f09ff25231393b6430bd89d780e2a499ae3b2d2b00852f593d97", [:mix], [], "hexpm", "8f5218c35ec38262f627b2c522542f1eae41f625f92649c0af701a6fab2e11b3"},
"tzdata": {:hex, :tzdata, "0.5.22", "f2ba9105117ee0360eae2eca389783ef7db36d533899b2e84559404dbc77ebb8", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cd66c8a1e6a9e121d1f538b01bef459334bb4029a1ffb4eeeb5e4eae0337e7b6"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"unrooted_polytree": {:hex, :unrooted_polytree, "0.1.1", "95027b1619d707fcbbd8980708a50efd170782142dd3de5112e9332d4cc27fef", [:mix], [], "hexpm", "9c8143d2015526ae49c3642ca509802e4db129685a57a0ec413e66546fe0c251"},
diff --git a/priv/static/icon-svg/icon-logan-express-BB.svg b/priv/static/icon-svg/icon-logan-express-BB.svg
new file mode 100644
index 0000000000..36903a2d62
--- /dev/null
+++ b/priv/static/icon-svg/icon-logan-express-BB.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/priv/static/icon-svg/icon-logan-express-BT.svg b/priv/static/icon-svg/icon-logan-express-BT.svg
new file mode 100644
index 0000000000..3e61896a46
--- /dev/null
+++ b/priv/static/icon-svg/icon-logan-express-BT.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/priv/static/icon-svg/icon-logan-express-FH.svg b/priv/static/icon-svg/icon-logan-express-FH.svg
new file mode 100644
index 0000000000..019482da49
--- /dev/null
+++ b/priv/static/icon-svg/icon-logan-express-FH.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/priv/static/icon-svg/icon-logan-express-PB.svg b/priv/static/icon-svg/icon-logan-express-PB.svg
new file mode 100644
index 0000000000..64783cb844
--- /dev/null
+++ b/priv/static/icon-svg/icon-logan-express-PB.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/priv/static/icon-svg/icon-logan-express-WO.svg b/priv/static/icon-svg/icon-logan-express-WO.svg
new file mode 100644
index 0000000000..c58bca184e
--- /dev/null
+++ b/priv/static/icon-svg/icon-logan-express-WO.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/priv/static/icon-svg/icon-massport-11.svg b/priv/static/icon-svg/icon-massport-11.svg
new file mode 100644
index 0000000000..514ce8106b
--- /dev/null
+++ b/priv/static/icon-svg/icon-massport-11.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/priv/static/icon-svg/icon-massport-22.svg b/priv/static/icon-svg/icon-massport-22.svg
new file mode 100644
index 0000000000..466f3ac58f
--- /dev/null
+++ b/priv/static/icon-svg/icon-massport-22.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/priv/static/icon-svg/icon-massport-33.svg b/priv/static/icon-svg/icon-massport-33.svg
new file mode 100644
index 0000000000..9eb0e09ca7
--- /dev/null
+++ b/priv/static/icon-svg/icon-massport-33.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/priv/static/icon-svg/icon-massport-44.svg b/priv/static/icon-svg/icon-massport-44.svg
new file mode 100644
index 0000000000..28be608154
--- /dev/null
+++ b/priv/static/icon-svg/icon-massport-44.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/priv/static/icon-svg/icon-massport-55.svg b/priv/static/icon-svg/icon-massport-55.svg
new file mode 100644
index 0000000000..98813eddc5
--- /dev/null
+++ b/priv/static/icon-svg/icon-massport-55.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/priv/static/icon-svg/icon-massport-66.svg b/priv/static/icon-svg/icon-massport-66.svg
new file mode 100644
index 0000000000..0ac71168bb
--- /dev/null
+++ b/priv/static/icon-svg/icon-massport-66.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/priv/static/icon-svg/icon-massport-77.svg b/priv/static/icon-svg/icon-massport-77.svg
new file mode 100644
index 0000000000..e4464c0a60
--- /dev/null
+++ b/priv/static/icon-svg/icon-massport-77.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/priv/static/icon-svg/icon-massport-88.svg b/priv/static/icon-svg/icon-massport-88.svg
new file mode 100644
index 0000000000..c8d1132b98
--- /dev/null
+++ b/priv/static/icon-svg/icon-massport-88.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/priv/static/icon-svg/icon-massport-99.svg b/priv/static/icon-svg/icon-massport-99.svg
new file mode 100644
index 0000000000..a18a425faa
--- /dev/null
+++ b/priv/static/icon-svg/icon-massport-99.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/test/dotcom/trip_plan/alerts_test.exs b/test/dotcom/trip_plan/alerts_test.exs
index 06aefaec43..e5a5a8dd89 100644
--- a/test/dotcom/trip_plan/alerts_test.exs
+++ b/test/dotcom/trip_plan/alerts_test.exs
@@ -1,33 +1,39 @@
defmodule Dotcom.TripPlan.AlertsTest do
use ExUnit.Case, async: true
- @moduletag :external
import Dotcom.TripPlan.Alerts
+ import Mox
import Test.Support.Factories.TripPlanner.TripPlanner
alias Alerts.Alert
alias Alerts.InformedEntity, as: IE
alias TripPlan.Itinerary
- setup_all do
+ setup :verify_on_exit!
+
+ setup do
+ leg = build(:transit_leg)
+
itinerary =
build(:itinerary,
- legs: [
- build(:leg,
- from: build(:stop_named_position),
- to: build(:stop_named_position),
- mode: build(:transit_detail)
- )
- ]
+ legs: [leg]
)
[route_id] = Itinerary.route_ids(itinerary)
[trip_id] = Itinerary.trip_ids(itinerary)
- {:ok, %{itinerary: itinerary, route_id: route_id, trip_id: trip_id}}
+ {:ok, %{itinerary: itinerary, route_id: route_id, trip_id: trip_id, route: leg.mode.route}}
end
describe "filter_for_itinerary/2" do
test "returns an alert if it affects the route", %{itinerary: itinerary, route_id: route_id} do
+ expect(MBTA.Api.Mock, :get_json, fn "/trips/" <> id, [] ->
+ %JsonApi{
+ data: [
+ Test.Support.Factories.MBTA.Api.build(:trip_item, %{id: id})
+ ]
+ }
+ end)
+
good_alert =
Alert.new(
active_period: [valid_active_period(itinerary)],
@@ -39,6 +45,14 @@ defmodule Dotcom.TripPlan.AlertsTest do
end
test "returns an alert if it affects the trip", %{itinerary: itinerary, trip_id: trip_id} do
+ expect(MBTA.Api.Mock, :get_json, fn "/trips/" <> ^trip_id, [] ->
+ %JsonApi{
+ data: [
+ Test.Support.Factories.MBTA.Api.build(:trip_item, %{id: trip_id})
+ ]
+ }
+ end)
+
good_alert =
Alert.new(
active_period: [valid_active_period(itinerary)],
@@ -53,6 +67,17 @@ defmodule Dotcom.TripPlan.AlertsTest do
itinerary: itinerary,
route_id: route_id
} do
+ expect(MBTA.Api.Mock, :get_json, fn "/trips/" <> id, [] ->
+ %JsonApi{
+ data: [
+ Test.Support.Factories.MBTA.Api.build(:trip_item, %{
+ id: id,
+ attributes: %{"direction_id" => 1}
+ })
+ ]
+ }
+ end)
+
good_alert =
Alert.new(
active_period: [valid_active_period(itinerary)],
@@ -67,9 +92,15 @@ defmodule Dotcom.TripPlan.AlertsTest do
test "returns an alert if it affects the route's type", %{
itinerary: itinerary,
- route_id: route_id
+ route: route
} do
- route = route_by_id(route_id)
+ expect(MBTA.Api.Mock, :get_json, fn "/trips/" <> id, [] ->
+ %JsonApi{
+ data: [
+ Test.Support.Factories.MBTA.Api.build(:trip_item, %{id: id})
+ ]
+ }
+ end)
good_alert =
Alert.new(
@@ -77,11 +108,19 @@ defmodule Dotcom.TripPlan.AlertsTest do
informed_entity: [%IE{route_type: route.type}]
)
- bad_alert = Alert.update(good_alert, informed_entity: [%IE{route_type: 0}])
+ bad_alert = Alert.update(good_alert, informed_entity: [%IE{route_type: route.type + 1}])
assert_only_good_alert(good_alert, bad_alert, itinerary)
end
test "returns an alert if it matches a transfer stop", %{itinerary: itinerary} do
+ expect(MBTA.Api.Mock, :get_json, fn "/trips/" <> id, [] ->
+ %JsonApi{
+ data: [
+ Test.Support.Factories.MBTA.Api.build(:trip_item, %{id: id})
+ ]
+ }
+ end)
+
stop_id = itinerary |> Itinerary.stop_ids() |> Enum.at(1)
good_alert =
@@ -97,6 +136,14 @@ defmodule Dotcom.TripPlan.AlertsTest do
end
test "ignores an alert if it's at the wrong time", %{itinerary: itinerary, route_id: route_id} do
+ expect(MBTA.Api.Mock, :get_json, fn "/trips/" <> id, [] ->
+ %JsonApi{
+ data: [
+ Test.Support.Factories.MBTA.Api.build(:trip_item, %{id: id})
+ ]
+ }
+ end)
+
good_alert =
Alert.new(
active_period: [valid_active_period(itinerary)],
@@ -109,7 +156,7 @@ defmodule Dotcom.TripPlan.AlertsTest do
end
defp assert_only_good_alert(good_alert, bad_alert, itinerary) do
- assert filter_for_itinerary([good_alert, bad_alert], itinerary, opts()) == [good_alert]
+ assert filter_for_itinerary([good_alert, bad_alert], itinerary) == [good_alert]
end
defp valid_active_period(%Itinerary{start: start, stop: stop}) do
@@ -119,24 +166,4 @@ defmodule Dotcom.TripPlan.AlertsTest do
defp invalid_active_period(%Itinerary{start: start}) do
{nil, Timex.shift(start, hours: -1)}
end
-
- defp opts do
- [route_by_id: &route_by_id/1, trip_by_id: &trip_by_id/1]
- end
-
- defp route_by_id(id) when id in ["Blue", "Red"] do
- %Routes.Route{type: 1, id: id, name: "Subway"}
- end
-
- defp route_by_id("CR-Lowell" = id) do
- %Routes.Route{type: 2, id: id, name: "Commuter Rail"}
- end
-
- defp route_by_id(id) when id in ["1", "350"] do
- %Routes.Route{type: 3, id: id, name: "Bus"}
- end
-
- defp trip_by_id(trip) do
- %Schedules.Trip{id: trip, direction_id: 1}
- end
end
diff --git a/test/dotcom/trip_plan/itinerary_row_list_test.exs b/test/dotcom/trip_plan/itinerary_row_list_test.exs
index fd176f8de2..610fc3aa94 100644
--- a/test/dotcom/trip_plan/itinerary_row_list_test.exs
+++ b/test/dotcom/trip_plan/itinerary_row_list_test.exs
@@ -1,74 +1,52 @@
defmodule Dotcom.TripPlan.ItineraryRowListTest do
use ExUnit.Case, async: true
- alias Test.Support.Factories.{MBTA.Api, Routes.Route, Stops.Stop}
-
import Dotcom.TripPlan.ItineraryRowList
import Mox
import Test.Support.Factories.TripPlanner.TripPlanner
@date_time ~N[2017-06-27T11:43:00]
- @from build(:stop_named_position, stop_id: "place-sstat")
- @to build(:named_position)
setup :verify_on_exit!
+ setup_all %{} do
+ # Start parent supervisor - Dotcom.TripPlan.ItineraryRow.get_additional_routes/5 needs this to be running.
+ _ = start_supervised(Dotcom.GreenLine.Supervisor)
+
+ :ok
+ end
+
describe "from_itinerary" do
setup do
# Start parent supervisor - Dotcom.TripPlan.ItineraryRow.get_additional_routes/5 needs this to be running.
- _ = start_supervised(Dotcom.GreenLine.Supervisor)
-
- # these rows depend on the itinerary having some sort of logic more
- # advanced than randomly generated data. so here we make sure the legs are
- # timed in sequence.
- itinerary =
- build(:itinerary,
- start: @date_time,
- stop: Timex.shift(@date_time, minutes: 60),
- legs: [
- build(:leg,
- from: @from,
- start: Timex.shift(@date_time, minutes: 10),
- stop: Timex.shift(@date_time, minutes: 20)
- ),
- build(:leg,
- start: Timex.shift(@date_time, minutes: 30),
- stop: Timex.shift(@date_time, minutes: 40)
- ),
- build(:leg,
- to: @to,
- start: Timex.shift(@date_time, minutes: 50),
- stop: Timex.shift(@date_time, minutes: 60)
- )
- ]
- )
+ stub(Stops.Repo.Mock, :by_route, fn "Green" <> _, _, _ ->
+ []
+ end)
- stub(Routes.Repo.Mock, :get, fn id -> Route.build(:route, %{id: id}) end)
- stub(Stops.Repo.Mock, :get_parent, fn id -> Stop.build(:stop, %{id: id}) end)
+ itinerary = build(:itinerary)
- stub(MBTA.Api.Mock, :get_json, fn "/trips" <> _, [] ->
- %JsonApi{data: [Api.build(:trip_item)]}
+ stub(MBTA.Api.Mock, :get_json, fn "/trips" <> _, _ ->
+ %JsonApi{data: [Test.Support.Factories.MBTA.Api.build(:trip_item)]}
end)
{:ok, %{itinerary: itinerary, itinerary_row_list: from_itinerary(itinerary)}}
end
- @tag :external
test "ItineraryRow contains given stop name when no stop_id present" do
- from = build(:stop_named_position, stop_id: nil)
- to = build(:stop_named_position, stop_id: "place-sstat")
+ from = build(:stop_named_position, stop: nil)
+ to = build(:stop_named_position, stop: %Stops.Stop{id: "place-sstat"})
date_time = ~N[2017-06-27T11:43:00]
itinerary =
build(:itinerary,
start: date_time,
- legs: [build(:leg, from: from)] ++ build_list(3, :leg) ++ [build(:leg, to: to)]
+ legs: [build(:transit_leg, from: from), build(:transit_leg, to: to)]
)
itinerary_row_list = from_itinerary(itinerary)
itinerary_destination =
- itinerary.legs |> Enum.reject(& &1.from.stop_id) |> List.first() |> Map.get(:from)
+ itinerary.legs |> Enum.reject(& &1.from.stop) |> List.first() |> Map.get(:from)
row_destination =
Enum.find(itinerary_row_list.rows, fn %{stop: {_stop_name, stop_id}} ->
@@ -106,7 +84,7 @@ defmodule Dotcom.TripPlan.ItineraryRowListTest do
{stop_name, _stop_id, _arrival_time, _} = row_list.destination
position_name =
- case stop_mapper(last_position.stop_id) do
+ case stop_mapper(last_position.stop) do
nil -> last_position.name
%{name: name} -> name
end
@@ -127,9 +105,7 @@ defmodule Dotcom.TripPlan.ItineraryRowListTest do
end
test "Distance is given with personal steps", %{itinerary: itinerary} do
- leg =
- build(:leg, mode: build(:personal_detail))
-
+ leg = build(:walking_leg)
personal_itinerary = %{itinerary | legs: [leg]}
row_list = from_itinerary(personal_itinerary)
@@ -147,51 +123,42 @@ defmodule Dotcom.TripPlan.ItineraryRowListTest do
end
test "Uses to name when one is provided", %{itinerary: itinerary} do
- {destination, stop_id, _datetime, _alerts} =
+ {destination, _, _datetime, _alerts} =
from_itinerary(itinerary, to: "Final Destination").destination
assert destination == "Final Destination"
- refute stop_id
end
- @tag :external
test "Does not replace to stop_id" do
- to = build(:stop_named_position, stop_id: "place-north")
-
- itinerary =
- build(:itinerary,
- start: @date_time,
- legs: [build(:leg, from: @from)] ++ build_list(3, :leg) ++ [build(:leg, to: to)]
- )
+ stop_id = Faker.Internet.slug()
+ stop_name = Faker.Address.city()
+ to = build(:stop_named_position, stop: %Stops.Stop{id: stop_id})
+
+ itinerary = %TripPlan.Itinerary{
+ start: nil,
+ stop: nil,
+ legs: [build(:transit_leg, to: to)]
+ }
{name, id, _datetime, _alerts} =
- itinerary |> from_itinerary(to: "Final Destination") |> Map.get(:destination)
+ itinerary |> from_itinerary(to: stop_name) |> Map.get(:destination)
- assert name == "Final Destination"
- assert id == "place-north"
+ assert name == stop_name
+ assert id == stop_id
end
- test "Uses given from name when one is provided" do
- from = build(:named_position)
-
- itinerary =
- build(:itinerary,
- start: @date_time,
- legs: [build(:leg, from: from)] ++ build_list(3, :leg) ++ [build(:leg, to: @to)]
- )
-
- {name, nil} =
+ test "Uses given from name when one is provided", %{itinerary: itinerary} do
+ {name, _} =
itinerary |> from_itinerary(from: "Starting Point") |> Enum.at(0) |> Map.get(:stop)
assert name == "Starting Point"
end
test "Does not replace from stop_id", %{itinerary: itinerary} do
- {name, id} =
+ {name, _} =
itinerary |> from_itinerary(from: "Starting Point") |> Enum.at(0) |> Map.get(:stop)
assert name == "Starting Point"
- assert id == "place-sstat"
end
@tag :external
@@ -199,12 +166,12 @@ defmodule Dotcom.TripPlan.ItineraryRowListTest do
green_leg = %TripPlan.Leg{
start: @date_time,
stop: @date_time,
- from: %TripPlan.NamedPosition{stop_id: "place-kencl", name: "Kenmore"},
- to: %TripPlan.NamedPosition{stop_id: "place-pktrm", name: "Park Street"},
+ from: %TripPlan.NamedPosition{stop: %Stops.Stop{id: "place-kencl"}, name: "Kenmore"},
+ to: %TripPlan.NamedPosition{stop: %Stops.Stop{id: "place-pktrm"}, name: "Park Street"},
mode: %TripPlan.TransitDetail{
- route_id: "Green-C",
+ route: %Routes.Route{id: "Green-C"},
trip_id: "Green-1",
- intermediate_stop_ids: []
+ intermediate_stops: []
}
}
@@ -230,11 +197,11 @@ defmodule Dotcom.TripPlan.ItineraryRowListTest do
red_leg = %TripPlan.Leg{
start: @date_time,
stop: @date_time,
- from: %TripPlan.NamedPosition{stop_id: "place-sstat", name: "South Station"},
- to: %TripPlan.NamedPosition{stop_id: "place-pktrm", name: "Park Street"},
+ from: %TripPlan.NamedPosition{stop: %Stops.Stop{id: "place-sstat"}, name: "South Station"},
+ to: %TripPlan.NamedPosition{stop: %Stops.Stop{id: "place-pktrm"}, name: "Park Street"},
mode: %TripPlan.TransitDetail{
- route_id: "Red",
- intermediate_stop_ids: ["place-dwnxg"]
+ route: %Routes.Route{id: "Red"},
+ intermediate_stops: [%Stops.Stop{id: "place-dwnxg"}]
}
}
@@ -257,23 +224,23 @@ defmodule Dotcom.TripPlan.ItineraryRowListTest do
red_leg = %TripPlan.Leg{
start: @date_time,
stop: @date_time,
- from: %TripPlan.NamedPosition{stop_id: "place-sstat", name: "South Station"},
- to: %TripPlan.NamedPosition{stop_id: "place-pktrm", name: "Park Street"},
+ from: %TripPlan.NamedPosition{stop: %Stops.Stop{id: "place-sstat"}, name: "South Station"},
+ to: %TripPlan.NamedPosition{stop: %Stops.Stop{id: "place-pktrm"}, name: "Park Street"},
mode: %TripPlan.TransitDetail{
- route_id: "Red",
- intermediate_stop_ids: []
+ route: %Routes.Route{id: "Red"},
+ intermediate_stops: []
}
}
green_leg = %TripPlan.Leg{
start: @date_time,
stop: @date_time,
- from: %TripPlan.NamedPosition{stop_id: "place-pktrm", name: "Park Street"},
- to: %TripPlan.NamedPosition{stop_id: "place-kencl", name: "Kenmore"},
+ from: %TripPlan.NamedPosition{stop: %Stops.Stop{id: "place-pktrm"}, name: "Park Street"},
+ to: %TripPlan.NamedPosition{stop: %Stops.Stop{id: "place-kencl"}, name: "Kenmore"},
mode: %TripPlan.TransitDetail{
- route_id: "Green-C",
+ route: %Routes.Route{id: "Green-C"},
trip_id: "Green-1",
- intermediate_stop_ids: []
+ intermediate_stops: []
}
}
diff --git a/test/dotcom/trip_plan/itinerary_row_test.exs b/test/dotcom/trip_plan/itinerary_row_test.exs
index 40e8aa9251..e7fabef467 100644
--- a/test/dotcom/trip_plan/itinerary_row_test.exs
+++ b/test/dotcom/trip_plan/itinerary_row_test.exs
@@ -6,15 +6,16 @@ defmodule TripPlan.ItineraryRowTest do
import Test.Support.Factories.TripPlanner.TripPlanner
alias Dotcom.TripPlan.ItineraryRow
+ alias Routes.Route
alias Alerts.{Alert, InformedEntity}
- alias Test.Support.Factories.{Routes.Route, Stops.Stop}
- alias TripPlan.NamedPosition
+ alias Test.Support.Factories.MBTA.Api
+ alias TripPlan.{Leg, NamedPosition, PersonalDetail}
setup :verify_on_exit!
describe "route_id/1" do
test "returns the route id when a route is present" do
- row = %ItineraryRow{route: %Routes.Route{id: "route"}}
+ row = %ItineraryRow{route: %Route{id: "route"}}
assert route_id(row) == "route"
end
@@ -28,7 +29,7 @@ defmodule TripPlan.ItineraryRowTest do
describe "route_type/1" do
test "returns the route type when a route is present" do
- row = %ItineraryRow{route: %Routes.Route{type: 0}}
+ row = %ItineraryRow{route: %Route{type: 0}}
assert route_type(row) == 0
end
@@ -42,7 +43,7 @@ defmodule TripPlan.ItineraryRowTest do
describe "route_name/1" do
test "returns the route name when a route is present" do
- row = %ItineraryRow{route: %Routes.Route{name: "Red Line"}}
+ row = %ItineraryRow{route: %Route{name: "Red Line"}}
assert route_name(row) == "Red Line"
end
@@ -62,8 +63,11 @@ defmodule TripPlan.ItineraryRowTest do
departure: DateTime.from_unix!(2),
transit?: true,
steps: [
- %Dotcom.TripPlan.IntermediateStop{description: "step1", stop_id: "intermediate_stop"},
- %Dotcom.TripPlan.IntermediateStop{description: "step2", stop_id: nil}
+ %Dotcom.TripPlan.IntermediateStop{
+ description: "step1",
+ stop: %Stops.Stop{id: "intermediate_stop"}
+ },
+ %Dotcom.TripPlan.IntermediateStop{description: "step2", stop: nil}
],
additional_routes: []
}
@@ -295,7 +299,7 @@ defmodule TripPlan.ItineraryRowTest do
transit?: true,
steps: [
%Dotcom.TripPlan.IntermediateStop{alerts: [Alert.new()]},
- %Dotcom.TripPlan.IntermediateStop{description: "step1", stop_id: "intermediate_stop"}
+ %Dotcom.TripPlan.IntermediateStop{description: "step1", stop: %Stops.Stop{}}
],
additional_routes: []
}
@@ -305,33 +309,26 @@ defmodule TripPlan.ItineraryRowTest do
end
describe "name_from_position" do
- test "doesn't return stop id if mapper returns nil" do
- stub(Stops.Repo.Mock, :get_parent, fn "ignored" ->
- nil
- end)
-
- stop_id = "ignored"
+ test "doesn't return stop id if stop is nil" do
name = "stop name"
assert {^name, nil} =
- name_from_position(%NamedPosition{stop_id: stop_id, name: name})
+ name_from_position(%NamedPosition{stop: nil, name: name})
end
end
describe "from_leg/3" do
- @deps %ItineraryRow.Dependencies{}
- @leg build(:leg)
- @personal_leg build(:leg, mode: build(:personal_detail))
- @transit_leg build(:leg, mode: build(:transit_detail))
+ @personal_leg build(:walking_leg)
+ @transit_leg build(:transit_leg)
setup do
stub(MBTA.Api.Mock, :get_json, fn path, _ ->
cond do
String.contains?(path, "trips") ->
- %JsonApi{data: [Test.Support.Factories.MBTA.Api.build(:trip_item)]}
+ %JsonApi{data: [Api.build(:trip_item)]}
String.contains?(path, "routes") ->
- %JsonApi{data: [Test.Support.Factories.MBTA.Api.build(:route_item)]}
+ %JsonApi{data: [Api.build(:route_item)]}
true ->
%JsonApi{data: []}
@@ -342,33 +339,29 @@ defmodule TripPlan.ItineraryRowTest do
end
test "returns an itinerary row from a Leg" do
- # stubs instead of expect because these don't always get called
- stub(Routes.Repo.Mock, :get, fn id -> Route.build(:route, %{id: id}) end)
- stub(Stops.Repo.Mock, :get_parent, fn id -> Stop.build(:stop, %{id: id}) end)
-
- row = from_leg(@leg, @deps, nil)
- assert %ItineraryRow{} = row
- end
+ leg = build(:transit_leg)
- test "formats transfer steps differently based on subsequent Leg" do
stub(Stops.Repo.Mock, :get_parent, fn id ->
%Stops.Stop{id: id}
end)
- leg =
- build(
- :leg,
- %{
- mode:
- build(
- :personal_detail,
- %{steps: [build(:step, %{relative_direction: :depart, street_name: "Transfer"})]}
- )
+ row = from_leg(leg, nil)
+ assert %ItineraryRow{} = row
+ end
+
+ test "formats transfer steps differently based on subsequent Leg" do
+ leg = %Leg{
+ @personal_leg
+ | mode: %PersonalDetail{
+ steps: [
+ %PersonalDetail.Step{relative_direction: :depart, street_name: "Transfer"}
+ | @personal_leg.mode.steps
+ ]
}
- )
+ }
- %ItineraryRow{steps: [xfer_step_to_personal | _]} = from_leg(leg, @deps, @personal_leg)
- %ItineraryRow{steps: [xfer_step_to_transit | _]} = from_leg(leg, @deps, @transit_leg)
+ %ItineraryRow{steps: [xfer_step_to_personal | _]} = from_leg(leg, @personal_leg)
+ %ItineraryRow{steps: [xfer_step_to_transit | _]} = from_leg(leg, @transit_leg)
assert xfer_step_to_personal.description != xfer_step_to_transit.description
end
end
diff --git a/test/dotcom/trip_plan/location_test.exs b/test/dotcom/trip_plan/location_test.exs
index fa0b6687ec..d395393962 100644
--- a/test/dotcom/trip_plan/location_test.exs
+++ b/test/dotcom/trip_plan/location_test.exs
@@ -21,7 +21,7 @@ defmodule Dotcom.TripPlan.LocationTest do
params = %{
"to_latitude" => "42.5678",
"to_longitude" => "-71.2345",
- "to_stop_id" => "To_Id",
+ "to_stop_id" => "",
"to" => "To Location"
}
@@ -29,7 +29,7 @@ defmodule Dotcom.TripPlan.LocationTest do
to: %NamedPosition{
latitude: 42.5678,
longitude: -71.2345,
- stop_id: "To_Id",
+ stop: nil,
name: "To Location"
}
}
@@ -68,7 +68,7 @@ defmodule Dotcom.TripPlan.LocationTest do
params = %{
"from_latitude" => "42.5678",
"from_longitude" => "-71.2345",
- "from_stop_id" => "From_Id",
+ "from_stop_id" => "",
"from" => "From Location"
}
@@ -76,7 +76,7 @@ defmodule Dotcom.TripPlan.LocationTest do
from: %NamedPosition{
latitude: 42.5678,
longitude: -71.2345,
- stop_id: "From_Id",
+ stop: nil,
name: "From Location"
}
}
@@ -141,7 +141,7 @@ defmodule Dotcom.TripPlan.LocationTest do
"from" => "From Location"
})
- assert nil == result.from.stop_id
+ assert nil == result.from.stop
end
test "sets :same_address error if from and to params are same" do
diff --git a/test/dotcom/trip_plan/related_link_test.exs b/test/dotcom/trip_plan/related_link_test.exs
index 3035ea956d..3f5b6aea43 100644
--- a/test/dotcom/trip_plan/related_link_test.exs
+++ b/test/dotcom/trip_plan/related_link_test.exs
@@ -1,19 +1,20 @@
defmodule Dotcom.TripPlan.RelatedLinkTest do
use ExUnit.Case, async: true
- @moduletag :external
import Dotcom.TripPlan.RelatedLink
import DotcomWeb.Router.Helpers, only: [fare_path: 4]
+ import Mox
import Test.Support.Factories.TripPlanner.TripPlanner
+ alias Test.Support.Factories.Stops.Stop
alias TripPlan.Itinerary
- setup_all do
+ setup :verify_on_exit!
+
+ setup do
itinerary =
build(:itinerary,
- legs: [
- build(:leg, mode: build(:transit_detail))
- ]
+ legs: [build(:transit_leg)]
)
{:ok, %{itinerary: itinerary}}
@@ -21,33 +22,58 @@ defmodule Dotcom.TripPlan.RelatedLinkTest do
describe "links_for_itinerary/1" do
test "returns a list of related links", %{itinerary: itinerary} do
- {expected_route, expected_icon} =
- case Itinerary.route_ids(itinerary) do
- ["Blue"] -> {"Blue Line schedules", :blue_line}
- ["Red"] -> {"Red Line schedules", :red_line}
- ["1"] -> {"Route 1 schedules", :bus}
- ["350"] -> {"Route 350 schedules", :bus}
- ["CR-Lowell"] -> {"Lowell Line schedules", :commuter_rail}
- end
+ %{legs: [%{from: from, to: to, mode: %{route: route}}]} = itinerary
+
+ expect(
+ Stops.Repo.Mock,
+ :get_parent,
+ if(route.type in [2, 4], do: 2, else: 0),
+ fn id -> %Stops.Stop{id: id} end
+ )
[trip_id] = Itinerary.trip_ids(itinerary)
assert [route_link, fare_link] = links_for_itinerary(itinerary)
- assert text(route_link) == expected_route
assert url(route_link) =~ Timex.format!(itinerary.start, "date={ISOdate}")
- assert url(route_link) =~ ~s(trip=#{String.replace(trip_id, ":", "%3A")})
- assert route_link.icon_name == expected_icon
+ assert url(route_link) =~ ~s(trip=#{trip_id}) |> URI.encode()
assert fare_link.text == "View fare information"
- # fare URL is tested later
+
+ if route.type == 3 do
+ assert text(route_link) == "Route #{route.name} schedules"
+ else
+ assert text(route_link) == "#{route.name} schedules"
+ end
+
+ case route.type do
+ 4 ->
+ assert route_link.icon_name == :ferry
+
+ assert fare_link.url ==
+ "/fares/ferry?origin=#{from.stop.id}&destination=#{to.stop.id}" |> URI.encode()
+
+ 3 ->
+ assert route_link.icon_name == :bus
+ assert fare_link.url == "/fares/bus-fares"
+
+ 2 ->
+ assert route_link.icon_name == :commuter_rail
+
+ assert fare_link.url ==
+ "/fares/commuter_rail?origin=#{from.stop.id}&destination=#{to.stop.id}"
+ |> URI.encode()
+
+ _ ->
+ assert route_link.icon_name == :subway
+ assert fare_link.url == "/fares/subway-fares"
+ end
end
test "returns a non-empty list for multiple kinds of itineraries" do
+ stub(Stops.Repo.Mock, :get_parent, fn _ -> Stop.build(:stop) end)
+
for _i <- 0..100 do
itinerary =
build(:itinerary,
- legs: [
- build(:leg, mode: build(:transit_detail)),
- build(:leg, mode: build(:transit_detail))
- ]
+ legs: build_list(2, :transit_leg)
)
assert [_ | _] = links_for_itinerary(itinerary)
@@ -57,12 +83,10 @@ defmodule Dotcom.TripPlan.RelatedLinkTest do
test "with multiple types of fares, returns one link to the fare overview", %{
itinerary: itinerary
} do
- for _i <- 0..10 do
- leg =
- build(:leg, %{
- mode: build(:transit_detail)
- })
+ stub(Stops.Repo.Mock, :get_parent, fn _ -> Stop.build(:stop) end)
+ for _i <- 0..10 do
+ leg = build(:transit_leg)
itinerary = %Itinerary{itinerary | legs: [leg | itinerary.legs]}
links = links_for_itinerary(itinerary)
@@ -70,21 +94,31 @@ defmodule Dotcom.TripPlan.RelatedLinkTest do
# we only have one expected text, assert that we've cleaned up the text
# to be only "View fare information".
expected_text_url = fn leg ->
- case leg.mode do
- %{route_id: id} when id in ["1", "350"] ->
+ case leg.mode.route.type do
+ 3 ->
{"bus", fare_path(DotcomWeb.Endpoint, :show, "bus-fares", [])}
- %{route_id: id} when id in ["Red", "Blue"] ->
+ type when type in [0, 1] ->
{"subway", fare_path(DotcomWeb.Endpoint, :show, "subway-fares", [])}
- %{route_id: "CR-Lowell"} ->
+ 2 ->
{"commuter rail",
fare_path(
DotcomWeb.Endpoint,
:show,
:commuter_rail,
- origin: fare_stop_id(leg.from.stop_id),
- destination: fare_stop_id(leg.to.stop_id)
+ origin: leg.from.stop.id,
+ destination: leg.to.stop.id
+ )}
+
+ 4 ->
+ {"ferry",
+ fare_path(
+ DotcomWeb.Endpoint,
+ :show,
+ :ferry,
+ origin: leg.from.stop.id,
+ destination: leg.to.stop.id
)}
_ ->
@@ -114,18 +148,4 @@ defmodule Dotcom.TripPlan.RelatedLinkTest do
end
end
end
-
- describe "links_for_itinerary/2" do
- test "returns custom links for custom routes", %{itinerary: itinerary} do
- url = "http://custom.url"
- legs = Enum.map(itinerary.legs, &%{&1 | url: url})
- itinerary = %TripPlan.Itinerary{itinerary | legs: legs}
-
- assert [%Dotcom.TripPlan.RelatedLink{text: "Route information", url: ^url}] =
- links_for_itinerary(itinerary)
- end
- end
-
- defp fare_stop_id("North Station"), do: "place-north"
- defp fare_stop_id(other), do: other
end
diff --git a/test/dotcom_web/controllers/trip_plan_controller_test.exs b/test/dotcom_web/controllers/trip_plan_controller_test.exs
index be9e533399..e0de01effb 100644
--- a/test/dotcom_web/controllers/trip_plan_controller_test.exs
+++ b/test/dotcom_web/controllers/trip_plan_controller_test.exs
@@ -1,16 +1,14 @@
defmodule DotcomWeb.TripPlanControllerTest do
use DotcomWeb.ConnCase, async: true
- import Mox
-
- alias Dotcom.TripPlan.Query
- alias DotcomWeb.TripPlanController
alias Fares.Fare
- alias Test.Support.Factories
+ alias Dotcom.TripPlan.Query
alias TripPlan.{Itinerary, PersonalDetail, TransitDetail}
doctest DotcomWeb.TripPlanController
+ import Mox
+
@system_time "2017-01-01T12:20:00-05:00"
@morning %{
"year" => "2017",
@@ -55,92 +53,6 @@ defmodule DotcomWeb.TripPlanControllerTest do
"plan" => %{"from" => "no results", "to" => "too many results", "date_time" => @afternoon}
}
- @subway_fare %Fare{
- additional_valid_modes: [:bus],
- cents: 290,
- duration: :single_trip,
- media: [:charlie_ticket, :cash],
- mode: :subway,
- name: :subway,
- price_label: nil,
- reduced: nil
- }
-
- @free_sl_fare %Fare{
- additional_valid_modes: [],
- cents: 0,
- duration: :single_trip,
- media: [],
- mode: :bus,
- name: :free_fare,
- price_label: nil,
- reduced: nil
- }
-
- @login_sl_plus_subway_itinerary %Itinerary{
- legs: [
- %TripPlan.Leg{
- description: "WALK",
- mode: %TripPlan.PersonalDetail{
- distance: 385.75800000000004
- }
- },
- %TripPlan.Leg{
- description: "BUS",
- from: %TripPlan.NamedPosition{
- name: "Terminal A",
- stop_id: "17091"
- },
- mode: %TripPlan.TransitDetail{
- route_id: "741",
- fares: %{
- highest_one_way_fare: @free_sl_fare,
- lowest_one_way_fare: @free_sl_fare,
- reduced_one_way_fare: @free_sl_fare
- }
- },
- name: "SL1",
- to: %TripPlan.NamedPosition{
- name: "South Station",
- stop_id: "74617"
- },
- type: "1"
- },
- %TripPlan.Leg{
- description: "WALK",
- mode: %TripPlan.PersonalDetail{
- distance: 0.0
- },
- name: ""
- },
- %TripPlan.Leg{
- description: "SUBWAY",
- from: %TripPlan.NamedPosition{
- name: "South Station",
- stop_id: "70080"
- },
- mode: %TripPlan.TransitDetail{
- route_id: "Red",
- fares: %{
- highest_one_way_fare: @subway_fare,
- lowest_one_way_fare: @subway_fare,
- reduced_one_way_fare: @subway_fare
- }
- },
- name: "Red Line",
- to: %TripPlan.NamedPosition{
- name: "Downtown Crossing",
- stop_id: "70078"
- },
- type: "1"
- }
- ],
- start: DateTime.from_unix!(0),
- stop: DateTime.from_unix!(0)
- }
-
- doctest DotcomWeb.TripPlanController
-
setup :verify_on_exit!
setup do
@@ -167,7 +79,7 @@ defmodule DotcomWeb.TripPlanControllerTest do
end)
stub(LocationService.Mock, :geocode, fn name ->
- {:ok, Factories.LocationService.build_list(2, :address, %{formatted: name})}
+ {:ok, Test.Support.Factories.LocationService.build_list(2, :address, %{formatted: name})}
end)
stub(Stops.Repo.Mock, :get_parent, fn _ ->
@@ -804,96 +716,4 @@ defmodule DotcomWeb.TripPlanControllerTest do
assert redirected_to(conn) == trip_plan_path(conn, :index, plan_params)
end
end
-
- describe "routes_for_query/1" do
- setup do
- itineraries = Factories.TripPlanner.TripPlanner.build_list(3, :itinerary)
- {:ok, %{itineraries: itineraries}}
- end
-
- test "doesn't set external_agency_name flag for regular routes", %{itineraries: itineraries} do
- # called variable number of times, depending on the generated itineraries
- stub(Routes.Repo.Mock, :get, fn id ->
- %Routes.Route{id: id}
- end)
-
- rfq = TripPlanController.routes_for_query(itineraries)
- assert Enum.all?(rfq, fn {_route_id, route} -> !route.external_agency_name end)
- end
-
- test "sets external_agency_name value for routes not present in API", %{
- itineraries: itineraries
- } do
- # set up itineraries which have a leg type associated with external agency
- itineraries =
- Enum.map(itineraries, fn i ->
- legs =
- Enum.map(i.legs, fn l ->
- case l do
- %{mode: %{route_id: _route_id}} ->
- %{l | type: "Logan Express"}
-
- _ ->
- l
- end
- end)
-
- %{i | legs: legs}
- end)
-
- # called variable number of times, depending on the generated itineraries
- stub(Routes.Repo.Mock, :get, fn id ->
- nil
- end)
-
- rfq = TripPlanController.routes_for_query(itineraries)
- assert Enum.all?(rfq, fn {_route_id, route} -> route.external_agency_name end)
- end
-
- test "identifies subsequent subway legs as free when trip is from the airport" do
- it = TripPlanController.readjust_itinerary_with_free_fares(@login_sl_plus_subway_itinerary)
-
- fares = DotcomWeb.TripPlanView.get_calculated_fares(it)
-
- assert fares == %{
- free_service: %{
- mode: %{
- fares: %{
- highest_one_way_fare: @free_sl_fare,
- lowest_one_way_fare: @free_sl_fare,
- reduced_one_way_fare: @free_sl_fare
- },
- mode: :bus,
- mode_name: "Bus",
- name: "Free Service"
- }
- }
- }
- end
-
- test "does not modify itinerary since trip is not from the airport" do
- # reuse @login_sl_plus_subway_itinerary except the SL leg:
- subway_legs = List.delete_at(@login_sl_plus_subway_itinerary.legs, 1)
- subway_itinerary = %Itinerary{@login_sl_plus_subway_itinerary | legs: subway_legs}
-
- it = TripPlanController.readjust_itinerary_with_free_fares(subway_itinerary)
-
- fares = DotcomWeb.TripPlanView.get_calculated_fares(it)
-
- assert fares == %{
- subway: %{
- mode: %{
- fares: %{
- highest_one_way_fare: @subway_fare,
- lowest_one_way_fare: @subway_fare,
- reduced_one_way_fare: @subway_fare
- },
- mode: :subway,
- mode_name: "Subway",
- name: "Subway"
- }
- }
- }
- end
- end
end
diff --git a/test/dotcom_web/views/trip_plan_view_test.exs b/test/dotcom_web/views/trip_plan_view_test.exs
index 13ce6c6567..85aae5f7d8 100644
--- a/test/dotcom_web/views/trip_plan_view_test.exs
+++ b/test/dotcom_web/views/trip_plan_view_test.exs
@@ -7,16 +7,15 @@ defmodule DotcomWeb.TripPlanViewTest do
import Schedules.Repo, only: [end_of_rating: 0]
alias Fares.Fare
- alias Routes.Route
alias Dotcom.TripPlan.{IntermediateStop, ItineraryRow, Query}
alias Test.Support.Factories.TripPlanner.TripPlanner
alias TripPlan.{Itinerary, Leg, NamedPosition, TransitDetail}
@highest_one_way_fare %Fares.Fare{
additional_valid_modes: [:bus],
- cents: 290,
+ cents: 240,
duration: :single_trip,
- media: [:charlie_ticket, :cash],
+ media: [:charlie_card, :charlie_ticket, :cash],
mode: :subway,
name: :subway,
price_label: nil,
@@ -27,7 +26,7 @@ defmodule DotcomWeb.TripPlanViewTest do
additional_valid_modes: [:bus],
cents: 240,
duration: :single_trip,
- media: [:charlie_card],
+ media: [:charlie_card, :charlie_ticket, :cash],
mode: :subway,
name: :subway,
price_label: nil,
@@ -78,8 +77,22 @@ defmodule DotcomWeb.TripPlanViewTest do
setup :verify_on_exit!
setup do
- stub(Routes.Repo.Mock, :green_line, fn ->
- Routes.Repo.green_line()
+ stub(MBTA.Api.Mock, :get_json, fn "/schedules/", [route: "Red", date: "1970-01-01"] ->
+ {:error,
+ [
+ %JsonApi.Error{
+ code: "no_service",
+ source: %{
+ "parameter" => "date"
+ },
+ detail: "The current rating does not describe service on that date.",
+ meta: %{
+ "end_date" => "2024-06-15",
+ "start_date" => "2024-05-10",
+ "version" => "Spring 2024, 2024-05-17T21:10:15+00:00, version D"
+ }
+ }
+ ]}
end)
:ok
@@ -178,7 +191,6 @@ closest arrival to 12:00 AM, Thursday, January 1st."
end
describe "plan_error_description" do
- @tag :external
test "renders too_future error" do
end_of_rating = end_of_rating() |> Timex.format!("{M}/{D}/{YY}")
@@ -191,7 +203,6 @@ closest arrival to 12:00 AM, Thursday, January 1st."
assert error =~ end_of_rating
end
- @tag :external
test "renders past error" do
end_of_rating = end_of_rating() |> Timex.format!("{M}/{D}/{YY}")
@@ -249,7 +260,7 @@ closest arrival to 12:00 AM, Thursday, January 1st."
describe "mode_class/1" do
test "returns the icon atom if a route is present" do
- row = %ItineraryRow{route: %Route{id: "Red"}}
+ row = %ItineraryRow{route: %Routes.Route{id: "Red"}}
assert mode_class(row) == "red-line"
end
@@ -292,7 +303,7 @@ closest arrival to 12:00 AM, Thursday, January 1st."
transit?: true,
stop: {"Park Street", "place-park"},
steps: ["Boylston", "Arlington", "Copley"],
- route: %Route{id: "Green", name: "Green Line", type: 1}
+ route: %Routes.Route{id: "Green", name: "Green Line", type: 1}
}
test "builds bubble_params for each step" do
@@ -425,45 +436,37 @@ closest arrival to 12:00 AM, Thursday, January 1st."
end
end
- describe "display_meters_as_miles/1" do
- test "123.456 mi" do
- assert display_meters_as_miles(123.456 * 1609.34) == "123.5"
- end
-
- test "0.123 mi" do
- assert display_meters_as_miles(0.123 * 1609.34) == "0.1"
- end
-
- test "10.001 mi" do
- assert display_meters_as_miles(10.001 * 1609.34) == "10.0"
- end
- end
-
- describe "display_seconds_as_minutes/1" do
- test "converts seconds to minutes" do
- assert display_seconds_as_minutes(5) == "1"
- assert display_seconds_as_minutes(59) == "1"
- assert display_seconds_as_minutes(100) == "2"
- end
- end
-
describe "format_additional_route/2" do
- @tag :external
test "Correctly formats Green Line route" do
- route = %Route{name: "Green Line B", id: "Green-B", direction_names: %{1 => "Eastbound"}}
+ destination = Test.Support.Factories.Stops.Stop.build(:stop, %{name: "Destination"})
+
+ expect(Stops.Repo.Mock, :by_route, 8, fn route_id, direction_id ->
+ if route_id == "Green-B" && direction_id == 1 do
+ [destination]
+ else
+ Test.Support.Factories.Stops.Stop.build_list(3, :stop)
+ end
+ end)
+
+ route = %Routes.Route{
+ name: "Green Line B",
+ id: "Green-B",
+ direction_names: %{1 => "Eastbound"}
+ }
+
actual = route |> format_additional_route(1) |> IO.iodata_to_binary()
- assert actual == "Green Line (B) Eastbound towards Government Center"
+ assert actual == "Green Line (B) Eastbound towards #{destination.name}"
end
end
describe "icon_for_routes/1" do
test "returns a list of icons for the given routes" do
routes = [
- %Route{
+ %Routes.Route{
id: "Red",
type: 1
},
- %Route{
+ %Routes.Route{
id: "Green",
type: 0
}
@@ -481,7 +484,7 @@ closest arrival to 12:00 AM, Thursday, January 1st."
describe "icon_for_route/1" do
test "non-subway transit legs" do
for {gtfs_type, expected_icon_class} <- [{2, "commuter-rail"}, {3, "bus"}, {4, "ferry"}] do
- route = %Route{
+ route = %Routes.Route{
id: "id",
type: gtfs_type
}
@@ -499,7 +502,7 @@ closest arrival to 12:00 AM, Thursday, January 1st."
{"Blue", 1, "blue-line"},
{"Green", 0, "green-line"}
] do
- route = %Route{
+ route = %Routes.Route{
id: id,
type: type
}
@@ -522,13 +525,18 @@ closest arrival to 12:00 AM, Thursday, January 1st."
describe "transfer_route_name/1" do
test "for subway" do
- assert transfer_route_name(%Route{id: "Mattapan", type: 0, name: "Mattapan Trolley"}) ==
+ stub(Routes.Repo.Mock, :green_line, fn ->
+ Routes.Repo.green_line()
+ end)
+
+ assert transfer_route_name(%Routes.Route{id: "Mattapan", type: 0, name: "Mattapan Trolley"}) ==
"Mattapan Trolley"
- assert transfer_route_name(%Route{id: "Green", type: 0, name: "Green Line"}) == "Green Line"
+ assert transfer_route_name(%Routes.Route{id: "Green", type: 0, name: "Green Line"}) ==
+ "Green Line"
for branch <- ["B", "C", "D", "E"] do
- assert transfer_route_name(%Route{
+ assert transfer_route_name(%Routes.Route{
id: "Green-" <> branch,
type: 0,
name: "Green Line " <> branch
@@ -536,87 +544,87 @@ closest arrival to 12:00 AM, Thursday, January 1st."
end
for line <- ["Red", "Orange", "Blue"] do
- assert transfer_route_name(%Route{id: line, type: 1, name: line <> " Line"}) ==
+ assert transfer_route_name(%Routes.Route{id: line, type: 1, name: line <> " Line"}) ==
line <> " Line"
end
end
test "for other modes" do
- assert transfer_route_name(%Route{id: "CR-Fitchburg", type: 2, name: "Fitchburg Line"}) ==
+ assert transfer_route_name(%Routes.Route{
+ id: "CR-Fitchburg",
+ type: 2,
+ name: "Fitchburg Line"
+ }) ==
"Commuter Rail"
- assert transfer_route_name(%Route{id: "77", type: 3, name: "77"}) == "Bus"
+ assert transfer_route_name(%Routes.Route{id: "77", type: 3, name: "77"}) == "Bus"
- assert transfer_route_name(%Route{id: "Boat-Hingham", type: 4, name: "Hingham Ferry"}) ==
+ assert transfer_route_name(%Routes.Route{id: "Boat-Hingham", type: 4, name: "Hingham Ferry"}) ==
"Ferry"
end
end
describe "transfer_note/1" do
+ setup do
+ stub(Stops.Repo.Mock, :get_parent, fn id ->
+ %Stops.Stop{id: id}
+ end)
+
+ :ok
+ end
+
@note_text "Total may be less with
transfers"
@base_itinerary %Itinerary{start: nil, stop: nil, legs: []}
- leg_for_route = &%Leg{mode: %TransitDetail{route_id: &1}}
- @bus_leg leg_for_route.("77")
- @other_bus_leg leg_for_route.("28")
- @subway_leg leg_for_route.("Red")
- @other_subway_leg leg_for_route.("Orange")
- @cr_leg leg_for_route.("CR-Lowell")
- @ferry_leg leg_for_route.("Boat-F4")
- @express_bus_leg leg_for_route.("505")
- @sl_rapid_leg leg_for_route.("741")
- @sl_bus_leg leg_for_route.("751")
-
- @tag :external
+ defp bus_leg, do: TripPlanner.build(:bus_leg)
+ defp subway_leg, do: TripPlanner.build(:subway_leg)
+ defp cr_leg, do: TripPlanner.build(:cr_leg)
+ defp ferry_leg, do: TripPlanner.build(:ferry_leg)
+ defp xp_leg, do: TripPlanner.build(:express_bus_leg)
+ defp sl_rapid_leg, do: TripPlanner.build(:sl_rapid_leg)
+ defp sl_bus_leg, do: TripPlanner.build(:sl_bus_leg)
+
test "shows note for subway-bus transfer" do
- note = %{@base_itinerary | legs: [@subway_leg, @bus_leg]} |> transfer_note
+ note = %{@base_itinerary | legs: [subway_leg(), bus_leg()]} |> transfer_note
assert note |> safe_to_string() =~ @note_text
end
- @tag :external
test "shows note for bus-subway transfer" do
- note = %{@base_itinerary | legs: [@bus_leg, @subway_leg]} |> transfer_note
+ note = %{@base_itinerary | legs: [bus_leg(), subway_leg()]} |> transfer_note
assert note |> safe_to_string() =~ @note_text
end
- @tag :external
test "shows note for bus-bus transfer" do
- note = %{@base_itinerary | legs: [@bus_leg, @other_bus_leg]} |> transfer_note
+ note = %{@base_itinerary | legs: [bus_leg(), bus_leg()]} |> transfer_note
assert note |> safe_to_string() =~ @note_text
end
- @tag :external
test "shows note for SL4-bus transfer" do
- note = %{@base_itinerary | legs: [@sl_bus_leg, @bus_leg]} |> transfer_note
+ note = %{@base_itinerary | legs: [sl_bus_leg(), bus_leg()]} |> transfer_note
assert note |> safe_to_string() =~ @note_text
end
- @tag :external
test "shows note for SL1-bus transfer" do
- note = %{@base_itinerary | legs: [@sl_rapid_leg, @bus_leg]} |> transfer_note
+ note = %{@base_itinerary | legs: [sl_rapid_leg(), bus_leg()]} |> transfer_note
assert note |> safe_to_string() =~ @note_text
end
- @tag :external
test "shows note for express bus-subway transfer" do
- note = %{@base_itinerary | legs: [@express_bus_leg, @subway_leg]} |> transfer_note
+ note = %{@base_itinerary | legs: [xp_leg(), subway_leg()]} |> transfer_note
assert note |> safe_to_string() =~ @note_text
end
- @tag :external
test "shows note for express bus-local bus transfer" do
- note = %{@base_itinerary | legs: [@express_bus_leg, @bus_leg]} |> transfer_note
+ note = %{@base_itinerary | legs: [xp_leg(), bus_leg()]} |> transfer_note
assert note |> safe_to_string() =~ @note_text
end
- @tag :external
test "no note when transfer involves ferry" do
- note = %{@base_itinerary | legs: [@ferry_leg, @bus_leg]} |> transfer_note
+ note = %{@base_itinerary | legs: [ferry_leg(), bus_leg()]} |> transfer_note
refute note
end
- @tag :external
test "no note when transfer involves commuter rail" do
- note = %{@base_itinerary | legs: [@cr_leg, @bus_leg]} |> transfer_note
+ note = %{@base_itinerary | legs: [cr_leg(), bus_leg()]} |> transfer_note
refute note
end
@@ -625,8 +633,8 @@ closest arrival to 12:00 AM, Thursday, January 1st."
%{
@base_itinerary
| legs: [
- TripPlanner.build(:leg, mode: TripPlanner.build(:personal_detail)),
- TripPlanner.build(:leg, mode: TripPlanner.build(:personal_detail))
+ TripPlanner.build(:walking_leg),
+ TripPlanner.build(:walking_leg)
]
}
|> transfer_note
@@ -639,9 +647,9 @@ closest arrival to 12:00 AM, Thursday, January 1st."
%{
@base_itinerary
| legs: [
- TripPlanner.build(:leg, mode: TripPlanner.build(:personal_detail)),
- @bus_leg,
- TripPlanner.build(:leg, mode: TripPlanner.build(:personal_detail))
+ TripPlanner.build(:walking_leg),
+ bus_leg(),
+ TripPlanner.build(:walking_leg)
]
}
|> transfer_note
@@ -649,18 +657,24 @@ closest arrival to 12:00 AM, Thursday, January 1st."
refute note
end
- @tag :external
test "no note for subway-subway transfer - handles parent stops" do
- leg1 = %{@subway_leg | to: %NamedPosition{stop_id: "place-dwnxg"}}
- leg2 = %{@other_subway_leg | from: %NamedPosition{stop_id: "place-dwnxg"}}
+ expect(Stops.Repo.Mock, :get_parent, 2, fn id ->
+ %Stops.Stop{id: id}
+ end)
+
+ leg1 = %{subway_leg() | to: %NamedPosition{stop: %Stops.Stop{id: "place-dwnxg"}}}
+ leg2 = %{subway_leg() | from: %NamedPosition{stop: %Stops.Stop{id: "place-dwnxg"}}}
note = %{@base_itinerary | legs: [leg1, leg2]} |> transfer_note
refute note
end
- @tag :external
test "no note for subway-subway transfer - handles child stops" do
- leg1 = %{@subway_leg | to: %NamedPosition{stop_id: "70020"}}
- leg2 = %{@other_subway_leg | from: %NamedPosition{stop_id: "70021"}}
+ expect(Stops.Repo.Mock, :get_parent, 2, fn _ ->
+ %Stops.Stop{id: "place-dwnxg"}
+ end)
+
+ leg1 = %{subway_leg() | to: %NamedPosition{stop: %Stops.Stop{id: "70020"}}}
+ leg2 = %{subway_leg() | from: %NamedPosition{stop: %Stops.Stop{id: "70021"}}}
note = %{@base_itinerary | legs: [leg1, leg2]} |> transfer_note
refute note
end
@@ -704,15 +718,6 @@ closest arrival to 12:00 AM, Thursday, January 1st."
end
describe "index.html" do
- plan_datetime_selector_fields = %{
- dateEl: %{
- container: "plan-date",
- input: "plan-date-input",
- select: "plan-date-select",
- label: "plan-date-label"
- }
- }
-
@index_assigns %{
date: Util.now(),
date_time: Util.now(),
@@ -720,37 +725,60 @@ closest arrival to 12:00 AM, Thursday, January 1st."
modes: %{},
wheelchair: false,
initial_map_data: Dotcom.TripPlan.Map.initial_map_data(),
- plan_datetime_selector_fields: plan_datetime_selector_fields
+ chosen_date_time: nil,
+ chosen_time: nil
}
- @tag :external
test "renders the form with all fields", %{conn: conn} do
- html =
+ form =
"_sidebar.html"
|> render(Map.put(@index_assigns, :conn, conn))
|> safe_to_string()
- # two blocks because of the