diff --git a/assets/css/_stop-bubbles.scss b/assets/css/_stop-bubbles.scss index ee11442be6..7f76efb0d5 100644 --- a/assets/css/_stop-bubbles.scss +++ b/assets/css/_stop-bubbles.scss @@ -298,6 +298,11 @@ $location-line-width: $space-6; @include stop-bubble-mode-color('.green-line-e', $brand-green-line); @include stop-bubble-mode-color('.bus', $brand-bus); @include stop-bubble-mode-color('.logan-express', $brand-logan-express); +@include stop-bubble-mode-color('.logan-express-FH', #ff505d); +@include stop-bubble-mode-color('.logan-express-BB', #f16823); +@include stop-bubble-mode-color('.logan-express-BT', #0055a0); +@include stop-bubble-mode-color('.logan-express-WO', #00954c); +@include stop-bubble-mode-color('.logan-express-PB', #704c9f); @include stop-bubble-mode-color('.massport-shuttle', $brand-massport-shuttle); @include stop-bubble-mode-color('.silver-line', $brand-silver-line); @include stop-bubble-mode-color('.ferry', $brand-ferry); diff --git a/config/config.exs b/config/config.exs index eec7642b46..fe2dbf7ddb 100644 --- a/config/config.exs +++ b/config/config.exs @@ -22,6 +22,7 @@ config :dotcom, :redis, Dotcom.Cache.Multilevel.Redis config :dotcom, :redix, Redix config :dotcom, :redix_pub_sub, Redix.PubSub +config :dotcom, :otp_module, OpenTripPlannerClient config :dotcom, :req_module, Req for config_file <- Path.wildcard("config/{deps,dotcom}/*.exs") do diff --git a/config/dotcom/trip_planner.exs b/config/dotcom/trip_planner.exs deleted file mode 100644 index 0e7e7e5a64..0000000000 --- a/config/dotcom/trip_planner.exs +++ /dev/null @@ -1,5 +0,0 @@ -import Config - -if config_env() == :test do - config :dotcom, :trip_planner, OpenTripPlannerClient.Mock -end diff --git a/config/test.exs b/config/test.exs index 4dc080433e..b7ba507d8c 100644 --- a/config/test.exs +++ b/config/test.exs @@ -21,6 +21,7 @@ config :dotcom, :redis, Dotcom.Redis.Mock config :dotcom, :redix, Dotcom.Redix.Mock config :dotcom, :redix_pub_sub, Dotcom.Redix.PubSub.Mock +config :dotcom, :otp_module, OpenTripPlannerClient.Mock config :dotcom, :req_module, Req.Mock config :dotcom, :trip_plan_feedback_cache, Dotcom.Cache.TestCache diff --git a/lib/dotcom/trip_plan/alerts.ex b/lib/dotcom/trip_plan/alerts.ex index df1ae721d5..e3e9d9715f 100644 --- a/lib/dotcom/trip_plan/alerts.ex +++ b/lib/dotcom/trip_plan/alerts.ex @@ -12,19 +12,12 @@ defmodule Dotcom.TripPlan.Alerts do alias Alerts.InformedEntity, as: IE alias TripPlan.{Itinerary, Leg, TransitDetail} - @default_opts [ - trip_by_id: &Schedules.Repo.trip/1 - ] - @routes_repo Application.compile_env!(:dotcom, :repo_modules)[:routes] - @doc "Filters a list of Alerts to those relevant to the Itinerary" - @spec filter_for_itinerary([Alert.t()], Itinerary.t(), Keyword.t()) :: [Alert.t()] - def filter_for_itinerary(alerts, itinerary, opts \\ []) do - opts = Keyword.merge(@default_opts, opts) - + @spec filter_for_itinerary([Alert.t()], Itinerary.t()) :: [Alert.t()] + def filter_for_itinerary(alerts, itinerary) do Alerts.Match.match( alerts, - Enum.concat(intermediate_entities(itinerary), entities(itinerary, opts)), + Enum.concat(intermediate_entities(itinerary), entities(itinerary)), itinerary.start ) end @@ -35,23 +28,25 @@ defmodule Dotcom.TripPlan.Alerts do |> Enum.map(&%IE{stop: &1}) end - @spec entities(Itinerary.t(), Keyword.t()) :: [IE.t()] - defp entities(itinerary, opts) do + @spec entities(Itinerary.t()) :: [IE.t()] + defp entities(itinerary) do itinerary - |> Enum.flat_map(&leg_entities(&1, opts)) + |> Enum.flat_map(&leg_entities(&1)) |> Enum.uniq() end - defp leg_entities(%Leg{mode: mode} = leg, opts) do - for entity <- mode_entities(mode, opts), + defp leg_entities(%Leg{mode: mode} = leg) do + for entity <- mode_entities(mode), stop_id <- Leg.stop_ids(leg) do %{entity | stop: stop_id} end end - defp mode_entities(%TransitDetail{route_id: route_id, trip_id: trip_id}, opts) do - route = @routes_repo.get(route_id) - trip = Keyword.get(opts, :trip_by_id).(trip_id) + defp mode_entities(%TransitDetail{route: route, trip_id: trip_id}) do + trip = + if is_nil(route.external_agency_name) do + Schedules.Repo.trip(trip_id) + end route_type = if route do @@ -63,10 +58,10 @@ defmodule Dotcom.TripPlan.Alerts do trip.direction_id end - [%IE{route_type: route_type, route: route_id, trip: trip_id, direction_id: direction_id}] + [%IE{route_type: route_type, route: route.id, trip: trip_id, direction_id: direction_id}] end - defp mode_entities(_, _opts) do + defp mode_entities(_) do [] end end diff --git a/lib/dotcom/trip_plan/intermediate_stop.ex b/lib/dotcom/trip_plan/intermediate_stop.ex index 1754b1f327..1bfc810958 100644 --- a/lib/dotcom/trip_plan/intermediate_stop.ex +++ b/lib/dotcom/trip_plan/intermediate_stop.ex @@ -1,11 +1,11 @@ defmodule Dotcom.TripPlan.IntermediateStop do defstruct description: nil, - stop_id: nil, + stop: nil, alerts: [] @type t :: %__MODULE__{ description: iodata, - stop_id: Stops.Stop.id_t(), + stop: Stops.Stop.t(), alerts: [Alerts.Alert.t()] } end diff --git a/lib/dotcom/trip_plan/itinerary_row.ex b/lib/dotcom/trip_plan/itinerary_row.ex index 57c2a1fd96..99c82321c9 100644 --- a/lib/dotcom/trip_plan/itinerary_row.ex +++ b/lib/dotcom/trip_plan/itinerary_row.ex @@ -17,6 +17,8 @@ defmodule Dotcom.TripPlan.ItineraryRow do distance: 0.0, duration: 0 + @routes_repo Application.compile_env!(:dotcom, :repo_modules)[:routes] + @typep name_and_id :: {String.t(), String.t() | nil} @typep step :: String.t() @type t :: %__MODULE__{ @@ -32,9 +34,6 @@ defmodule Dotcom.TripPlan.ItineraryRow do duration: Integer.t() } - @routes_repo Application.compile_env!(:dotcom, :repo_modules)[:routes] - @stops_repo Application.compile_env!(:dotcom, :repo_modules)[:stops] - defmodule Dependencies do @moduledoc false @@ -56,16 +55,24 @@ defmodule Dotcom.TripPlan.ItineraryRow do def route_type(%__MODULE__{route: %Route{type: type}}), do: type def route_type(_row), do: nil + def route_name(%__MODULE__{route: %Route{external_agency_name: agency, long_name: name}}) + when is_binary(agency) and is_binary(name), + do: name + def route_name(%__MODULE__{route: %Route{name: name}}), do: name def route_name(_row), do: nil @doc """ Builds an ItineraryRow struct from the given leg and options """ - @spec from_leg(Leg.t(), Dependencies.t(), Leg.t() | nil) :: t - def from_leg(leg, deps, next_leg) do - trip = leg |> Leg.trip_id() |> parse_trip_id(deps.trip_mapper) - route = leg |> Leg.route_id() |> parse_route_id() + @spec from_leg(Leg.t(), Leg.t() | nil) :: t + def from_leg(leg, next_leg) do + transit? = Leg.transit?(leg) + route = if(transit?, do: leg.mode.route) + + trip = + if(route && is_nil(route.external_agency_name), do: leg |> Leg.trip_id() |> parse_trip_id()) + stop = name_from_position(leg.from) %__MODULE__{ @@ -134,12 +141,12 @@ defmodule Dotcom.TripPlan.ItineraryRow do Enum.map(steps, &match_step(&1, alerts)) end - def match_step(%IntermediateStop{stop_id: nil} = step, _alerts) do + def match_step(%IntermediateStop{stop: nil} = step, _alerts) do step end def match_step(step, alerts) do - %{step | alerts: Alerts.Stop.match(alerts, step.stop_id, activities: ~w(ride)a)} + %{step | alerts: Alerts.Stop.match(alerts, step.stop.id, activities: ~w(ride)a)} end def intermediate_alerts?(%__MODULE__{steps: steps}) do @@ -148,12 +155,8 @@ defmodule Dotcom.TripPlan.ItineraryRow do @spec name_from_position(NamedPosition.t()) :: {String.t(), String.t()} - def name_from_position(%NamedPosition{stop_id: stop_id, name: name}) - when not is_nil(stop_id) do - case @stops_repo.get_parent(stop_id) do - nil -> {name, nil} - stop -> {stop.name, stop.id} - end + def name_from_position(%NamedPosition{stop: %Stops.Stop{id: id}, name: name}) do + {name, id} end def name_from_position(%NamedPosition{name: name}) do @@ -168,25 +171,19 @@ defmodule Dotcom.TripPlan.ItineraryRow do defp get_steps(%PersonalDetail{steps: steps}, _next_leg), do: Enum.map(steps, &format_personal_to_personal_step/1) - defp get_steps(%TransitDetail{intermediate_stop_ids: stop_ids}, _next_leg) do - for {:ok, stop} <- Task.async_stream(stop_ids, fn id -> @stops_repo.get_parent(id) end), - stop do + defp get_steps(%TransitDetail{intermediate_stops: stops}, _next_leg) do + for stop <- stops, stop do %IntermediateStop{ description: stop.name, - stop_id: stop.id + stop: stop } end end - @spec parse_route_id(:error | {:ok, String.t()}) :: - Routes.Route.t() | nil - defp parse_route_id(:error), do: nil - defp parse_route_id({:ok, route_id}), do: @routes_repo.get(route_id) - - @spec parse_trip_id(:error | {:ok, String.t()}, Dependencies.trip_mapper()) :: + @spec parse_trip_id(:error | {:ok, String.t()}) :: Schedules.Trip.t() | nil - defp parse_trip_id(:error, _trip_mapper), do: nil - defp parse_trip_id({:ok, trip_id}, trip_mapper), do: trip_mapper.(trip_id) + defp parse_trip_id(:error), do: nil + defp parse_trip_id({:ok, trip_id}), do: Schedules.Repo.trip(trip_id) defp format_personal_to_personal_step(%{relative_direction: :depart, street_name: "Transfer"}), do: %IntermediateStop{description: "Depart"} diff --git a/lib/dotcom/trip_plan/itinerary_row_list.ex b/lib/dotcom/trip_plan/itinerary_row_list.ex index 83e4b323d1..6e0a003fbb 100644 --- a/lib/dotcom/trip_plan/itinerary_row_list.ex +++ b/lib/dotcom/trip_plan/itinerary_row_list.ex @@ -33,9 +33,8 @@ defmodule Dotcom.TripPlan.ItineraryRowList do %Itinerary{legs: legs, accessible?: accessible?} = itinerary, opts \\ [] ) do - deps = %ItineraryRow.Dependencies{} - alerts = get_alerts(itinerary, deps) - rows = get_rows(itinerary, deps, opts, alerts) + alerts = get_alerts(itinerary) + rows = get_rows(itinerary, opts, alerts) %__MODULE__{ rows: rows, @@ -45,28 +44,25 @@ defmodule Dotcom.TripPlan.ItineraryRowList do } end - @spec get_rows(Itinerary.t(), ItineraryRow.Dependencies.t(), opts, [Alerts.Alert.t()]) :: [ + @spec get_rows(Itinerary.t(), opts, [Alerts.Alert.t()]) :: [ ItineraryRow.t() ] - defp get_rows(itinerary, deps, opts, alerts) do + defp get_rows(itinerary, opts, alerts) do rows = for {leg, index} <- Enum.with_index(itinerary.legs) do leg - |> ItineraryRow.from_leg(deps, Enum.at(itinerary.legs, index + 1)) + |> ItineraryRow.from_leg(Enum.at(itinerary.legs, index + 1)) |> ItineraryRow.fetch_alerts(alerts) end update_from_name(rows, opts[:from]) end - @spec get_alerts(Itinerary.t(), ItineraryRow.Dependencies.t()) :: [Alerts.Alert.t()] - defp get_alerts(itinerary, deps) do + @spec get_alerts(Itinerary.t()) :: [Alerts.Alert.t()] + defp get_alerts(itinerary) do itinerary.start - |> deps.alerts_repo.() - |> Dotcom.TripPlan.Alerts.filter_for_itinerary( - itinerary, - trip_by_id: deps.trip_mapper - ) + |> Alerts.Repo.all() + |> Dotcom.TripPlan.Alerts.filter_for_itinerary(itinerary) end @spec get_destination([TripPlan.Leg.t()], Keyword.t(), [Alerts.Alert.t()]) :: destination @@ -74,7 +70,9 @@ defmodule Dotcom.TripPlan.ItineraryRowList do last_leg = List.last(legs) {name, stop_id} = - last_leg |> Map.get(:to) |> ItineraryRow.name_from_position() + last_leg + |> Map.get(:to) + |> ItineraryRow.name_from_position() alerts = Alerts.Stop.match(alerts, stop_id) {destination_name(name, opts[:to]), stop_id, last_leg.stop, alerts} diff --git a/lib/dotcom/trip_plan/location.ex b/lib/dotcom/trip_plan/location.ex index 4c4633015e..88ad624ead 100644 --- a/lib/dotcom/trip_plan/location.ex +++ b/lib/dotcom/trip_plan/location.ex @@ -4,6 +4,7 @@ defmodule Dotcom.TripPlan.Location do alias TripPlan.NamedPosition @location_service Application.compile_env!(:dotcom, :location_service) + @stops_repo Application.compile_env!(:dotcom, :repo_modules)[:stops] @spec validate(Query.t(), map) :: Query.t() def validate( @@ -80,7 +81,7 @@ defmodule Dotcom.TripPlan.Location do latitude: lat, longitude: lng, name: encode_name(name), - stop_id: nil_if_empty(stop_id) + stop: if(stop_id && stop_id != "", do: @stops_repo.get(stop_id)) } query @@ -92,9 +93,6 @@ defmodule Dotcom.TripPlan.Location do end end - defp nil_if_empty(""), do: nil - defp nil_if_empty(value), do: value - @spec encode_name(String.t()) :: String.t() defp encode_name(name) do name diff --git a/lib/dotcom/trip_plan/map.ex b/lib/dotcom/trip_plan/map.ex index a09fccd624..98608fcf0b 100644 --- a/lib/dotcom/trip_plan/map.ex +++ b/lib/dotcom/trip_plan/map.ex @@ -1,4 +1,7 @@ defmodule Dotcom.TripPlan.Map do + @moduledoc """ + Handles generating the maps displayed within the TripPlan Controller + """ alias Leaflet.{MapData, MapData.Marker} alias Leaflet.MapData.Polyline, as: LeafletPolyline alias Routes.Route @@ -6,17 +9,6 @@ defmodule Dotcom.TripPlan.Map do alias Util.Position @type t :: MapData.t() - @type route_mapper :: (String.t() -> Route.t() | nil) - - @default_opts [ - route_mapper: &Routes.Repo.get/1 - ] - - @stops_repo Application.compile_env!(:dotcom, :repo_modules)[:stops] - - @moduledoc """ - Handles generating the maps displayed within the TripPlan Controller - """ def initial_map_data do {630, 400} @@ -29,20 +21,15 @@ defmodule Dotcom.TripPlan.Map do Accepts a function that will return either a Route or nil when given a route_id """ - @spec itinerary_map([Leg.t()], Keyword.t()) :: t - def itinerary_map(itinerary, opts \\ []) do - itinerary_map_data(itinerary, Keyword.merge(@default_opts, opts)) - end - - @spec itinerary_map_data([Leg.t()], Keyword.t()) :: MapData.t() - defp itinerary_map_data(itinerary, opts) do + @spec itinerary_map([Leg.t()]) :: t + def itinerary_map(itinerary) do markers = itinerary |> markers_for_legs() |> Enum.with_index() |> Enum.map(fn {marker, idx} -> %{marker | id: "marker-#{idx}"} end) - paths = Enum.map(itinerary, &build_leg_path(&1, opts[:route_mapper])) + paths = Enum.map(itinerary, &build_leg_path(&1)) {600, 600} |> MapData.new() @@ -50,9 +37,9 @@ defmodule Dotcom.TripPlan.Map do |> MapData.add_polylines(paths) end - @spec build_leg_path(Leg.t(), route_mapper) :: LeafletPolyline.t() - defp build_leg_path(leg, route_mapper) do - color = leg_color(leg, route_mapper) + @spec build_leg_path(Leg.t()) :: LeafletPolyline.t() + defp build_leg_path(leg) do + color = leg_color(leg) path_weight = if Leg.transit?(leg), do: 5, else: 1 leg.polyline @@ -145,22 +132,19 @@ defmodule Dotcom.TripPlan.Map do def stop_icon_size("map-pin-b"), do: nil def stop_icon_size(_), do: %{icon_size: [22, 22], icon_anchor: [0, 0]} - @spec leg_color(Leg.t(), route_mapper) :: String.t() - defp leg_color(%Leg{mode: %TransitDetail{route_id: route_id}}, route_mapper) do - with route <- route_mapper.(route_id), do: "#" <> route.color + @spec leg_color(Leg.t()) :: String.t() + defp leg_color(%Leg{mode: %TransitDetail{route: %Route{color: color}}}) + when not is_nil(color) do + "#" <> color end - defp leg_color(_leg, _route_mapper) do + defp leg_color(_) do "#000000" end @spec tooltip_for_position(NamedPosition.t()) :: String.t() - defp tooltip_for_position(%NamedPosition{stop_id: stop_id} = position) do - case @stops_repo.get_parent(stop_id) do - nil -> position.name - stop -> stop.name - end - end + defp tooltip_for_position(%NamedPosition{name: name, stop: nil}), do: name + defp tooltip_for_position(%NamedPosition{stop: %Stops.Stop{name: name}}), do: name @spec z_index(map) :: 0 | 1 def z_index(%{current: idx, start: idx}), do: 100 diff --git a/lib/dotcom/trip_plan/query.ex b/lib/dotcom/trip_plan/query.ex index 91587e9609..47890b9d24 100644 --- a/lib/dotcom/trip_plan/query.ex +++ b/lib/dotcom/trip_plan/query.ex @@ -1,16 +1,8 @@ defmodule Dotcom.TripPlan.Query do @moduledoc "Fetch trip plan via OTP and handle response" - alias OpenTripPlannerClient.ItineraryTag.{ - EarliestArrival, - LeastWalking, - MostDirect, - ShortestTrip - } - - alias TripPlan.Api.OpenTripPlanner - alias TripPlan.{Itinerary, NamedPosition} + alias TripPlanner.OpenTripPlanner defstruct [ :from, @@ -49,9 +41,6 @@ defmodule Dotcom.TripPlan.Query do defp encode_value(values), do: [values] end - @otp_depart_at_tags [EarliestArrival, MostDirect, LeastWalking] - @otp_arrive_by_tags [ShortestTrip, MostDirect, LeastWalking] - @type query_itineraries :: {:ok, [Itinerary.t()]} | {:error, any()} @type position :: NamedPosition.t() | {:error, any()} | nil @type t :: %__MODULE__{ @@ -104,22 +93,11 @@ defmodule Dotcom.TripPlan.Query do query end - @spec fetch_itineraries(t, Keyword.t()) :: OpenTripPlannerClient.Behaviour.plan() + @spec fetch_itineraries(t, Keyword.t()) :: OpenTripPlannerClient.Behaviour.plan_result() defp fetch_itineraries( %__MODULE__{from: %NamedPosition{} = from, to: %NamedPosition{} = to}, opts ) do - opts = - Keyword.put_new( - opts, - :tags, - if Keyword.has_key?(opts, :arrive_by) do - @otp_arrive_by_tags - else - @otp_depart_at_tags - end - ) - OpenTripPlanner.plan(from, to, opts) end diff --git a/lib/dotcom/trip_plan/related_link.ex b/lib/dotcom/trip_plan/related_link.ex index 59144fa82b..17fbfa6734 100644 --- a/lib/dotcom/trip_plan/related_link.ex +++ b/lib/dotcom/trip_plan/related_link.ex @@ -10,7 +10,7 @@ defmodule Dotcom.TripPlan.RelatedLink do alias DotcomWeb.PartialView.SvgIconWithCircle alias Routes.Route - alias TripPlan.{Itinerary, Leg} + alias TripPlan.{Itinerary, Leg, TransitDetail} @stops_repo Application.compile_env!(:dotcom, :repo_modules)[:stops] @@ -60,10 +60,10 @@ defmodule Dotcom.TripPlan.RelatedLink do end @doc "Builds a list of related links for an Itinerary" - @spec links_for_itinerary(Itinerary.t(), Keyword.t()) :: [t] - def links_for_itinerary(itinerary, opts \\ []) do - for func <- [&route_links/2, &fare_links/2], - link <- func.(itinerary, opts) do + @spec links_for_itinerary(Itinerary.t()) :: [t] + def links_for_itinerary(itinerary) do + for func <- [&route_links/1, &fare_links/1], + link <- func.(itinerary) do link end end @@ -74,51 +74,54 @@ defmodule Dotcom.TripPlan.RelatedLink do SvgIconWithCircle.svg_icon_with_circle(%SvgIconWithCircle{icon: icon_name, size: :small}) end - defp route_links(itinerary, opts) do - route_by_id = Keyword.get(opts, :route_by_id) + defp route_links(itinerary) do not_shuttle? = fn route -> route.description !== :rail_replacement_bus end - for {route_id, trip_id} <- Itinerary.route_trip_ids(itinerary), - %Route{} = route <- [route_by_id.(route_id)], + for %Leg{mode: %TransitDetail{route: route, trip_id: trip_id}} <- itinerary.legs, not_shuttle?.(route) do route_link(route, trip_id, itinerary) end end + defp route_link( + %Route{external_agency_name: "Logan Express", name: name, long_name: long_name} = route, + _, + _ + ) do + url = + if name == "BB" do + "https://www.massport.com/logan-airport/to-from-logan/transportation-options/logan-express/back-bay/" + else + route_name = String.split(long_name, " ") |> List.first() + "https://www.massport.com/logan-airport/getting-to-logan/logan-express/#{route_name}" + end + + new("#{long_name} schedules", url, route) + end + + defp route_link(%Route{external_agency_name: "Massport"}, _, _) do + url = "https://massport.com/" + new("Massport schedules", url, :massport_shuttle) + end + defp route_link(route, trip_id, itinerary) do icon_name = Route.icon_atom(route) - cond do - String.starts_with?(route.id, "Massport") -> - new("Massport schedules", "https://massport.com/", icon_name) - - route.external_agency_name == "Logan Express" -> - new( - "Logan Express schedules", - "https://www.massport.com/logan-airport/getting-to-logan/logan-express", - icon_name - ) - - true -> - base_text = - if Route.type_atom(route) == :bus do - ["Route ", route.name] - else - route.name - end - - date = Date.to_iso8601(itinerary.start) - url = schedule_path(DotcomWeb.Endpoint, :show, route, date: date, trip: trip_id) - new([base_text, " schedules"], url, icon_name) - end - end + base_text = + if Route.type_atom(route) == :bus do + ["Route ", route.name] + else + route.name + end - defp fare_links(itinerary, opts) do - route_by_id = Keyword.get(opts, :route_by_id) + date = Date.to_iso8601(itinerary.start) + url = schedule_path(DotcomWeb.Endpoint, :show, route, date: date, trip: trip_id) + new([base_text, " schedules"], url, icon_name) + end - for leg <- itinerary, - {:ok, route_id} <- [Leg.route_id(leg)], - %Route{external_agency_name: nil} = route <- [route_by_id.(route_id)] do + defp fare_links(itinerary) do + for %Leg{mode: %TransitDetail{route: %Route{external_agency_name: nil} = route}} = leg <- + itinerary.legs do fare_link(route, leg) end |> Enum.uniq() @@ -133,13 +136,13 @@ defmodule Dotcom.TripPlan.RelatedLink do new(["View ", text, " fare information"], url) end - defp fare_link_text(type) do + defp fare_link_text(type) when type in [:commuter_rail, :ferry, :bus, :subway] do Atom.to_string(type) |> String.replace("_", " ") end defp fare_link_url_opts(type, leg) when type in [:commuter_rail, :ferry] do link_opts = - for {key, stop_id} <- [origin: leg.from.stop_id, destination: leg.to.stop_id], + for {key, stop_id} <- [origin: leg.from.stop.id, destination: leg.to.stop.id], is_binary(stop_id) do # fetch a parent stop ID stop_id = @stops_repo.get_parent(stop_id).id @@ -153,8 +156,6 @@ defmodule Dotcom.TripPlan.RelatedLink do {"#{type}-fares", []} end - defp fare_link_url_opts(_, _leg), do: {"", []} - # if there are multiple fare links, show fare overview link defp simplify_fare_text(fare_links) when Kernel.length(fare_links) > 1, do: [fare_overview_link()] diff --git a/lib/dotcom_web/controllers/trip_plan_controller.ex b/lib/dotcom_web/controllers/trip_plan_controller.ex index d22c202640..4994fc338d 100644 --- a/lib/dotcom_web/controllers/trip_plan_controller.ex +++ b/lib/dotcom_web/controllers/trip_plan_controller.ex @@ -9,12 +9,10 @@ defmodule DotcomWeb.TripPlanController do alias Dotcom.TripPlan.{ItineraryRowList, Query, RelatedLink} alias Dotcom.TripPlan.Map, as: TripPlanMap - alias Fares.{Fare, Month, OneWay} alias Routes.Route - alias TripPlan.{Itinerary, Leg, NamedPosition, PersonalDetail, Transfer, TransitDetail} + alias TripPlan.{Itinerary, Leg, NamedPosition, PersonalDetail, TransitDetail} @location_service Application.compile_env!(:dotcom, :location_service) - @routes_repo Application.compile_env!(:dotcom, :repo_modules)[:routes] @type route_map :: %{optional(Route.id_t()) => Route.t()} @type route_mapper :: (Route.id_t() -> Route.t() | nil) @@ -50,7 +48,7 @@ defmodule DotcomWeb.TripPlanController do latitude: String.to_float(latitude), longitude: String.to_float(longitude), name: name, - stop_id: nil + stop: nil } do_from(conn, destination) @@ -85,14 +83,8 @@ defmodule DotcomWeb.TripPlanController do from: destination, to: nil, mode: %PersonalDetail{}, - description: "", start: now, - stop: now, - name: "", - long_name: "", - type: "", - url: "", - polyline: "" + stop: now } ]) @@ -120,7 +112,7 @@ defmodule DotcomWeb.TripPlanController do latitude: String.to_float(latitude), longitude: String.to_float(longitude), name: name, - stop_id: nil + stop: nil } do_to(conn, destination) @@ -155,14 +147,8 @@ defmodule DotcomWeb.TripPlanController do from: nil, to: destination, mode: %PersonalDetail{}, - description: "", start: now, - stop: now, - name: "", - long_name: "", - type: "", - url: "", - polyline: "" + stop: now } ]) @@ -232,11 +218,7 @@ defmodule DotcomWeb.TripPlanController do itineraries = query |> Query.get_itineraries() - |> with_fares_and_passes() - |> with_free_legs_if_from_airport() - route_map = routes_for_query(itineraries) - route_mapper = &Map.get(route_map, &1) itinerary_row_lists = itinerary_row_lists(itineraries, plan_params) conn @@ -244,153 +226,14 @@ defmodule DotcomWeb.TripPlanController do query: query, itineraries: itineraries, plan_error: MapSet.to_list(query.errors), - routes: Enum.map(itineraries, &routes_for_itinerary(&1, route_mapper)), - itinerary_maps: - Enum.map(itineraries, &TripPlanMap.itinerary_map(&1, route_mapper: route_mapper)), + routes: Enum.map(itineraries, &routes_for_itinerary(&1)), + itinerary_maps: Enum.map(itineraries, &TripPlanMap.itinerary_map(&1)), related_links: - filter_duplicate_links( - Enum.map(itineraries, &RelatedLink.links_for_itinerary(&1, route_by_id: route_mapper)) - ), + filter_duplicate_links(Enum.map(itineraries, &RelatedLink.links_for_itinerary(&1))), itinerary_row_lists: itinerary_row_lists ) end - @spec with_fares_and_passes([Itinerary.t()]) :: [Itinerary.t()] - defp with_fares_and_passes(itineraries) do - Enum.map(itineraries, fn itinerary -> - legs_with_fares = itinerary.legs |> Enum.map(&leg_with_fares/1) - - 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 | legs: legs_with_fares, passes: passes} - end) - end - - @spec leg_with_fares(Leg.t()) :: Leg.t() - defp leg_with_fares(%Leg{mode: %PersonalDetail{}} = leg) do - leg - end - - defp leg_with_fares(%Leg{mode: %TransitDetail{}} = leg) do - route = @routes_repo.get(leg.mode.route_id) - 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 | mode: mode_with_fares} - end - - @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{ - mode: %TransitDetail{route_id: route_id}, - from: %NamedPosition{stop_id: origin_id}, - to: %NamedPosition{stop_id: destination_id} - } = leg - ) do - if Leg.fare_complete_transit_leg?(leg) do - Month.base_pass(route_id, 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{ - mode: %TransitDetail{route_id: route_id}, - from: %NamedPosition{stop_id: origin_id}, - to: %NamedPosition{stop_id: destination_id} - } = leg - ) do - if Leg.fare_complete_transit_leg?(leg) do - Month.recommended_pass(route_id, 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{ - mode: %TransitDetail{route_id: route_id}, - from: %NamedPosition{stop_id: origin_id}, - to: %NamedPosition{stop_id: destination_id} - } = leg - ) do - if Leg.fare_complete_transit_leg?(leg) do - Month.reduced_pass(route_id, 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 itinerary_row_lists([Itinerary.t()], map) :: [ItineraryRowList.t()] defp itinerary_row_lists(itineraries, plan) do Enum.map(itineraries, &ItineraryRowList.from_itinerary(&1, to_and_from(plan))) @@ -434,20 +277,11 @@ defmodule DotcomWeb.TripPlanController do assign(conn, :wheelchair, true) end - @spec routes_for_query([Itinerary.t()]) :: route_map - def routes_for_query(itineraries) do - itineraries - |> Enum.flat_map(&Itinerary.route_ids/1) - |> add_additional_routes() - |> Enum.uniq() - |> Map.new(&{&1, get_route(&1, itineraries)}) - end - - @spec routes_for_itinerary(Itinerary.t(), route_mapper) :: [Route.t()] - defp routes_for_itinerary(itinerary, route_mapper) do - itinerary - |> Itinerary.route_ids() - |> Enum.map(route_mapper) + @spec routes_for_itinerary(Itinerary.t()) :: [Route.t()] + defp routes_for_itinerary(itinerary) do + itinerary.legs + |> Enum.filter(&match?(%TransitDetail{}, &1.mode)) + |> Enum.map(& &1.mode.route) end @spec to_and_from(map) :: [to: String.t() | nil, from: String.t() | nil] @@ -455,54 +289,6 @@ defmodule DotcomWeb.TripPlanController do [to: Map.get(plan, "to"), from: Map.get(plan, "from")] end - defp add_additional_routes(ids) do - if Enum.any?(ids, &String.starts_with?(&1, "Green")) do - # no cover - Enum.concat(ids, GreenLine.branch_ids()) - else - ids - end - end - - defp get_route(id, itineraries) do - case @routes_repo.get(id) do - %Route{} = route -> route - nil -> get_route_from_itinerary(itineraries, id) - end - end - - @spec get_route_from_itinerary([Itinerary.t()], Route.id_t()) :: Route.t() - defp get_route_from_itinerary(itineraries, id) do - # used for non-MBTA routes that are returned by - # OpenTripPlanner but do not exist in our repo, - # such as Logan Express. - - %Itinerary{legs: legs} = - Enum.find(itineraries, &(&1 |> Itinerary.route_ids() |> Enum.member?(id))) - - %Leg{ - description: description, - mode: mode, - long_name: long_name, - name: name, - type: type - } = Enum.find(legs, &(Leg.route_id(&1) == {:ok, id})) - - %Route{ - external_agency_name: agency_name(type), - description: description, - id: mode.route_id, - long_name: long_name, - name: name, - type: type, - color: "000000" - } - end - - defp agency_name("Logan Express"), do: "Logan Express" - defp agency_name("2"), do: "Massport" - defp agency_name(_), do: nil - defp meta_description(conn, _) do conn |> assign( @@ -511,82 +297,4 @@ defmodule DotcomWeb.TripPlanController do "and suggestions based on real-time data." ) end - - @spec with_free_legs_if_from_airport([Itinerary.t()]) :: [Itinerary.t()] - defp with_free_legs_if_from_airport(itineraries) do - Enum.map(itineraries, fn itinerary -> - # 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) - end - - defp chunk_subway_legs({leg, _idx}) do - highest_fare = - Fares.get_fare_by_type(leg, :highest_one_way_fare) - - if is_nil(highest_fare) do - false - else - Transfer.subway?(highest_fare.mode) - end - end - - @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.chunk_by( - legs_after_airport, - &chunk_subway_legs/1 - ) - |> Enum.at(0) - 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 | legs: readjusted_legs} - end end diff --git a/lib/dotcom_web/templates/partial/_trip_planner_widget.html.eex b/lib/dotcom_web/templates/partial/_trip_planner_widget.html.eex index ff1019c8f7..74d1414494 100644 --- a/lib/dotcom_web/templates/partial/_trip_planner_widget.html.eex +++ b/lib/dotcom_web/templates/partial/_trip_planner_widget.html.eex @@ -8,8 +8,8 @@ from_error = "" to_error = "" - from_position = %{name: "", latitude: "", longitude: "", stop_id: ""} - to_position = %{name: "", latitude: "", longitude: "", stop_id: ""} + from_position = %{name: "", latitude: "", longitude: "", stop: nil} + to_position = %{name: "", latitude: "", longitude: "", stop: nil} %>
diff --git a/lib/dotcom_web/templates/trip_plan/_itinerary_tab.html.eex b/lib/dotcom_web/templates/trip_plan/_itinerary_tab.html.eex index c6c7cbae07..95f0a05eea 100644 --- a/lib/dotcom_web/templates/trip_plan/_itinerary_tab.html.eex +++ b/lib/dotcom_web/templates/trip_plan/_itinerary_tab.html.eex @@ -15,6 +15,6 @@ <%= @routes |> Enum.map(&icon_for_route/1) |> Enum.intersperse(fa "angle-right") %> <%= svg "walk.svg" %> - <%= @itinerary |> TripPlan.Itinerary.walking_distance |> display_meters_as_miles %> mi + <%= @itinerary |> TripPlan.Itinerary.walking_distance %> mi
diff --git a/lib/dotcom_web/templates/trip_plan/_leg_summary.html.heex b/lib/dotcom_web/templates/trip_plan/_leg_summary.html.heex index da76862b2d..3bfaa811c5 100644 --- a/lib/dotcom_web/templates/trip_plan/_leg_summary.html.heex +++ b/lib/dotcom_web/templates/trip_plan/_leg_summary.html.heex @@ -4,7 +4,7 @@ Ride the <%= @branch_display %> - <%= "(#{@intermediate_stop_count} #{Inflex.inflect("Stop", @intermediate_stop_count)}, #{display_seconds_as_minutes(@itinerary_row.duration)} min)" %> + <%= "(#{@intermediate_stop_count} #{Inflex.inflect("Stop", @intermediate_stop_count)}, #{@itinerary_row.duration} min)" %> <%= if @itinerary_row.trip do %>
<%= 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 @@ > - + ">
@@ -59,7 +59,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 @@ Walk to <%= @next_stop_name %> - <%= "(#{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