lib/geocoder/providers/google_maps.ex

defmodule Geocoder.Providers.GoogleMaps do
  @moduledoc """
  Google Map provider logic. Google requires a key

  See documentation details at https://developers.google.com/maps/documentation/geocoding/
  """
  use Towel

  @behaviour Geocoder.Provider

  @endpoint "https://maps.googleapis.com/"
  @components [
    "locality",
    "administrative_area_level_1",
    "administrative_area_level_2",
    "country",
    "postal_code",
    "street",
    "street_number",
    "route"
  ]
  @map %{
    "street_number" => :street_number,
    "route" => :street,
    "street_address" => :street,
    "locality" => :city,
    "administrative_area_level_1" => :state,
    "administrative_area_level_2" => :county,
    "postal_code" => :postal_code,
    "country" => :country
  }
  def geocode(payload_opts, opts \\ []) do
    request("maps/api/geocode/json", extract_payload_opts(payload_opts), opts)
    |> fmap(&parse_geocode/1)
  end

  def geocode_list(payload_opts, opts \\ []) do
    request_all("maps/api/geocode/json", extract_payload_opts(payload_opts), opts)
    |> fmap(fn r -> Enum.map(r, &parse_geocode/1) end)
  end

  def reverse_geocode(payload_opts, opts \\ []) do
    request("maps/api/geocode/json", extract_payload_opts(payload_opts), opts)
    |> fmap(&parse_reverse_geocode/1)
  end

  def reverse_geocode_list(payload_opts, opts \\ []) do
    request_all("maps/api/geocode/json", extract_payload_opts(payload_opts), opts)
    |> fmap(fn r -> Enum.map(r, &parse_reverse_geocode/1) end)
  end

  defp request(path, params, opts) do
    request_all(path, params, opts)
    |> fmap(&List.first/1)
  end

  defp extract_payload_opts(opts) do
    opts
    |> Keyword.take([
      :key,
      :address,
      :components,
      :bounds,
      :language,
      :region,
      :latlng,
      :placeid,
      :result_type,
      :location_type
    ])
    |> Keyword.update(:latlng, nil, fn
      {lat, lng} -> "#{lat},#{lng}"
      q -> q
    end)
  end

  defp parse_geocode(response) do
    coords = geocode_coords(response)
    bounds = geocode_bounds(response)
    location = geocode_location(response)
    partial_match = response["partial_match"]
    %{coords | bounds: bounds, location: location, partial_match: partial_match}
  end

  defp parse_reverse_geocode(response) do
    coords = geocode_coords(response)
    location = geocode_location(response)
    %{coords | location: location}
  end

  defp geocode_coords(%{"geometry" => %{"location" => coords}}) do
    %{"lat" => lat, "lng" => lon} = coords
    %Geocoder.Coords{lat: lat, lon: lon}
  end

  defp geocode_bounds(%{"geometry" => %{"bounds" => bounds}}) do
    %{
      "northeast" => %{"lat" => north, "lng" => east},
      "southwest" => %{"lat" => south, "lng" => west}
    } = bounds

    %Geocoder.Bounds{top: north, right: east, bottom: south, left: west}
  end

  defp geocode_bounds(_), do: %Geocoder.Bounds{}

  defp geocode_location(%{
         "address_components" => components,
         "formatted_address" => formatted_address
       }) do
    name = &Map.get(&1, "long_name")

    type = fn component ->
      component |> Map.get("types") |> Enum.find(&Enum.member?(@components, &1))
    end

    map = &{type.(&1), name.(&1)}

    reduce = fn {type, name}, location ->
      struct(location, [{@map[type], name}])
    end

    country =
      Enum.find(components, fn component ->
        component |> Map.get("types") |> Enum.member?("country")
      end)

    country_code =
      case country do
        nil ->
          nil

        %{"short_name" => name} ->
          name
      end

    location = %Geocoder.Location{
      country_code: country_code,
      formatted_address: formatted_address
    }

    components
    |> Enum.filter(type)
    |> Enum.map(map)
    |> Enum.reduce(location, reduce)
  end

  defp request_all(path, params, opts) do
    params = Keyword.merge(params, key: opts[:key])

    request = %{
      method: :get,
      url: @endpoint <> path,
      query_params: Enum.into(params, %{})
    }

    case Geocoder.Request.request(request, opts) do
      {:ok,
       %{
         status_code: 200,
         body: %{"results" => [], "error_message" => error_message, "status" => _status}
       }} ->
        {:error, error_message}

      {:ok, %{status_code: 200, body: %{"status" => "OK", "results" => results}}} ->
        {:ok, List.wrap(results)}

      {_, response} ->
        {:error, response}
    end
  end
end