Skip to content

Commit

Permalink
feat(OTP): Updated the API to use the graphql endpoint of OTP
Browse files Browse the repository at this point in the history
  • Loading branch information
kotva006 committed Nov 7, 2023
1 parent f933d85 commit 7efbea4
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 105 deletions.
2 changes: 1 addition & 1 deletion apps/site/lib/site_web/views/trip_plan_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ defmodule SiteWeb.TripPlanView do
acc
else
name =
if leg.name && leg.name =~ "Shuttle",
if leg.long_name && leg.long_name =~ "Shuttle",
do: Format.name(:shuttle),
else: Format.name(highest_fare.name)

Expand Down
1 change: 0 additions & 1 deletion apps/site/test/site/trip_plan/query_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ defmodule Site.TripPlan.QueryTest do
assert %Query{} = from_query(params, @connection_opts, @date_opts)

inaccessible_opts = [
max_walk_distance: 805,
wheelchair_accessible?: false,
depart_at: @date_time
]
Expand Down
1 change: 0 additions & 1 deletion apps/trip_plan/lib/trip_plan.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ defmodule TripPlan do
# Default options for the plans
@default_opts [
# ~0.5 miles
max_walk_distance: 805
]

@doc """
Expand Down
1 change: 0 additions & 1 deletion apps/trip_plan/lib/trip_plan/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ defmodule TripPlan.Api do
| {:depart_at, DateTime.t()}
| {:wheelchair_accessible?, boolean}
| {:optimize_for, :less_walking | :fewest_transfers}
| {:max_walk_distance, float}
@type plan_opts :: [plan_opt]

@type connection_opts :: [user_id: integer, force_otp1: boolean, force_otp2: boolean]
Expand Down
156 changes: 116 additions & 40 deletions apps/trip_plan/lib/trip_plan/api/open_trip_planner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,31 @@ defmodule TripPlan.Api.OpenTripPlanner do
@moduledoc "Fetches data from the OpenTripPlanner API."
@behaviour TripPlan.Api
require Logger
import __MODULE__.Builder, only: [build_params: 3]
import __MODULE__.Parser, only: [parse_json: 1]
alias TripPlan.NamedPosition
alias Util.Position
import __MODULE__.Builder, only: [build_query_params: 3]
import __MODULE__.Parser, only: [parse_ql: 1]

def plan(from, to, connection_opts, opts, _parent) do
plan(from, to, connection_opts, opts)
end

@impl true
def plan(from, to, connection_opts, opts) do
with {:ok, params} <- build_params(from, to, opts) do
params =
Map.merge(
params,
%{
"fromPlace" => location(from),
"toPlace" => location(to),
"showIntermediateStops" => "true",
"format" => "json",
"locale" => "en"
}
with {:ok, params} <- build_query_params(from, to, opts) do
param_string = Enum.map_join(params, "\n", fn {key, val} -> ~s{#{key}: #{val}} end)

graph_ql_query = """
{
plan(
#{param_string}
)
#{itinerary_shape()}
}
"""

root_url = params["root_url"] || pick_url(connection_opts)
full_url = "#{root_url}/otp/routers/default/plan"
send_request(full_url, params, &parse_json/1)
graphql_url = "#{root_url}/otp/routers/default/index/"

send_request(graphql_url, graph_ql_query, &parse_ql/1)
end
end

Expand Down Expand Up @@ -86,55 +84,133 @@ defmodule TripPlan.Api.OpenTripPlanner do
Util.config(:trip_plan, OpenTripPlanner, key)
end

defp send_request(url, params, parser) do
with {:ok, response} <- log_response(url, params),
%{status_code: 200, body: body} <- response do
defp send_request(url, query, parser) do
with {:ok, response} <- log_response(url, query),
%{status: 200, body: body} <- response do
parser.(body)
else
%{status_code: _} = response ->
%{status: _} = response ->
{:error, response}

error ->
error
end
end

defp log_response(url, params) do
defp log_response(url, query) do
graphql_req = Req.new(base_url: url) |> AbsintheClient.attach()

{duration, response} =
:timer.tc(
HTTPoison,
:get,
[url, build_headers(config(:wiremock_proxy)), [params: params, recv_timeout: 10_000]]
Req,
:post,
[graphql_req, [graphql: query]]
)

_ =
Logger.info(fn ->
"#{__MODULE__}.plan_response url=#{url} is_otp2=#{String.contains?(url, config(:otp2_url))} params=#{inspect(params)} #{status_text(response)} duration=#{duration / :timer.seconds(1)}"
"#{__MODULE__}.plan_response url=#{url} is_otp2=#{String.contains?(url, config(:otp2_url))} query=#{inspect(query)} #{status_text(response)} duration=#{duration / :timer.seconds(1)}"
end)

response
end

defp build_headers("true") do
proxy_url = Application.get_env(:trip_plan, OpenTripPlanner)[:wiremock_proxy_url]
[{"X-WM-Proxy-Url", proxy_url}]
end

defp build_headers(_), do: []

defp status_text({:ok, %{status_code: code, body: body}}) do
defp status_text({:ok, %{status: code, body: body}}) do
"status=#{code} content_length=#{byte_size(body)}"
end

defp status_text({:error, error}) do
"status=error error=#{inspect(error)}"
end

defp location(%NamedPosition{} = np) do
"#{np.name}::#{Position.latitude(np)},#{Position.longitude(np)}"
end

defp location(position) do
"#{Position.latitude(position)},#{Position.longitude(position)}"
defp itinerary_shape() do
"""
{
itineraries {
startTime
endTime
duration
legs {
mode
startTime
endTime
distance
duration
intermediateStops {
id
gtfsId
name
desc
lat
lon
code
locationType
}
transitLeg
headsign
realTime
realtimeState
agency {
id
gtfsId
name
}
alerts {
id
alertHeaderText
alertDescriptionText
}
fareProducts {
id
product {
id
name
riderCategory {
id
name
}
}
}
from {
name
lat
lon
departureTime
arrivalTime
}
to {
name
lat
lon
departureTime
arrivalTime
}
route {
gtfsId
longName
shortName
desc
color
textColor
}
trip {
gtfsId
}
steps {
distance
streetName
lat
lon
relativeDirection
stayOn
}
legGeometry {
points
}
}
}
}
"""
end
end
87 changes: 42 additions & 45 deletions apps/trip_plan/lib/trip_plan/api/open_trip_planner/builder.ex
Original file line number Diff line number Diff line change
@@ -1,26 +1,40 @@
defmodule TripPlan.Api.OpenTripPlanner.Builder do
alias TripPlan.Api.OpenTripPlanner, as: OTP
alias TripPlan.NamedPosition
alias Util.{Polygon, Position}
@ten_miles 16_093

@spec build_query_params(Position.t(), Position.t(), TripPlan.Api.plan_opts()) ::
{:ok, %{String.t() => String.t()}} | {:error, any}
def build_query_params(from, to, opts) do
from_string = location(from)
to_string = location(to)
default_mode_string = "[{mode: WALK}, {mode: TRANSIT}]"

do_build_params(opts, %{
"fromPlace" => from_string,
"toPlace" => to_string,
"transportModes" => default_mode_string,
"walkReluctance" => 5,
"locale" => "\"en\""
})
end

defp location(%NamedPosition{} = np) do
"\"#{np.name}::#{Position.latitude(np)},#{Position.longitude(np)}\""
end

defp location(position) do
"\"#{Position.latitude(position)},#{Position.longitude(position)}\""
end

@doc "Convert general planning options into query params for OTP"
@spec build_params(Position.t(), Position.t(), TripPlan.Api.plan_opts()) ::
{:ok, %{String.t() => String.t()}} | {:error, any}
def build_params(from, to, opts) do
opts = configure_walk_distance(from, to, opts)
do_build_params(opts, %{"mode" => "TRANSIT,WALK", "walkReluctance" => 5})
end

defp configure_walk_distance(from, to, opts) do
polygon = Application.get_env(:trip_plan, ReducedWalkingArea, [])

if Polygon.inside?(polygon, from) and Polygon.inside?(polygon, to) do
opts
else
put_in(opts[:max_walk_distance], @ten_miles)
end
end

defp do_build_params([], acc) do
{:ok, acc}
end
Expand All @@ -36,38 +50,13 @@ defmodule TripPlan.Api.OpenTripPlanner.Builder do
do_build_params(rest, acc)
end

defp do_build_params([{:max_walk_distance, meters} | rest], acc) when is_number(meters) do
acc = put_in(acc["maxWalkDistance"], "#{meters}")
do_build_params(rest, acc)
end

defp do_build_params([{:depart_at, %DateTime{} = datetime} | rest], acc) do
local = Timex.to_datetime(datetime, OTP.config(:timezone))
date = Timex.format!(local, "{ISOdate}")
time = Timex.format!(local, "{h12}:{0m}{am}")

acc =
Map.merge(acc, %{
"date" => date,
"time" => time,
"arriveBy" => "false"
})

acc = do_date_time(false, datetime, acc)
do_build_params(rest, acc)
end

defp do_build_params([{:arrive_by, %DateTime{} = datetime} | rest], acc) do
local = Timex.to_datetime(datetime, OTP.config(:timezone))
date = Timex.format!(local, "{ISOdate}")
time = Timex.format!(local, "{h12}:{0m}{am}")

acc =
Map.merge(acc, %{
"date" => date,
"time" => time,
"arriveBy" => "true"
})

acc = do_date_time(true, datetime, acc)
do_build_params(rest, acc)
end

Expand All @@ -76,8 +65,9 @@ defmodule TripPlan.Api.OpenTripPlanner.Builder do
end

defp do_build_params([{:mode, [_ | _] = modes} | rest], acc) do
all_modes = Enum.join(modes, ",") <> ",WALK"
do_build_params(rest, Map.put(acc, "mode", all_modes))
all_modes = Enum.map(modes, fn m -> "{mode: #{m}}" end)
joined_modes = "[#{Enum.join(all_modes, ",")}, {mode: WALK}]"
do_build_params(rest, Map.put(acc, "transportModes", joined_modes))
end

defp do_build_params([{:optimize_for, :less_walking} | rest], acc) do
Expand All @@ -88,12 +78,19 @@ defmodule TripPlan.Api.OpenTripPlanner.Builder do
do_build_params(rest, Map.put(acc, "transferPenalty", 100))
end

defp do_build_params([{:root_url, url} | rest], acc) do
acc = Map.put(acc, "root_url", url)
do_build_params(rest, acc)
end

defp do_build_params([option | _], _) do
{:error, {:bad_param, option}}
end

defp do_date_time(arriveBy, %DateTime{} = datetime, acc) do
local = Timex.to_datetime(datetime, OTP.config(:timezone))
date = Timex.format!(local, "\"{ISOdate}\"")
time = Timex.format!(local, "\"{h12}:{0m}{am}\"")

Map.merge(acc, %{
"date" => date,
"time" => time,
"arriveBy" => arriveBy
})
end
end
11 changes: 8 additions & 3 deletions apps/trip_plan/lib/trip_plan/api/open_trip_planner/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ defmodule TripPlan.Api.OpenTripPlanner.Parser do

@transit_modes ~w(SUBWAY TRAM BUS RAIL FERRY)s

@spec parse_ql(map) :: {:ok, [Itinerary.t()]} | {:error, TripPlan.Api.error()}
def parse_ql(%{"data" => data}) do
parse_map(data)
end

@doc """
Parse the JSON output from the plan endpoint.
"""
Expand Down Expand Up @@ -121,9 +126,9 @@ defmodule TripPlan.Api.OpenTripPlanner.Parser do

defp parse_mode(%{"mode" => mode} = json) when mode in @transit_modes do
%TransitDetail{
route_id: id_after_colon(json["routeId"]),
trip_id: id_after_colon(json["tripId"]),
intermediate_stop_ids: Enum.map(json["intermediateStops"], &id_after_colon(&1["stopId"]))
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

Expand Down
Loading

0 comments on commit 7efbea4

Please sign in to comment.