Add GeoSpatial backends for geocoding
Signed-off-by: Thomas Citharel <tcit@tcit.fr> Geospatial Backend Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
f7284740e3
commit
98b7618338
@ -64,3 +64,20 @@ config :arc,
|
|||||||
storage: Arc.Storage.Local
|
storage: Arc.Storage.Local
|
||||||
|
|
||||||
config :phoenix, :format_encoders, json: Jason
|
config :phoenix, :format_encoders, json: Jason
|
||||||
|
|
||||||
|
config :mobilizon, Mobilizon.Service.Geospatial.Nominatim,
|
||||||
|
endpoint:
|
||||||
|
System.get_env("GEOSPATIAL_NOMINATIM_ENDPOINT") || "https://nominatim.openstreetmap.org",
|
||||||
|
api_key: System.get_env("GEOSPATIAL_NOMINATIM_API_KEY") || nil
|
||||||
|
|
||||||
|
config :mobilizon, Mobilizon.Service.Geospatial.Addok,
|
||||||
|
endpoint: System.get_env("GEOSPATIAL_ADDOK_ENDPOINT") || "https://api-adresse.data.gouv.fr"
|
||||||
|
|
||||||
|
config :mobilizon, Mobilizon.Service.Geospatial.Photon,
|
||||||
|
endpoint: System.get_env("GEOSPATIAL_PHOTON_ENDPOINT") || "https://photon.komoot.de"
|
||||||
|
|
||||||
|
config :mobilizon, Mobilizon.Service.Geospatial.GoogleMaps,
|
||||||
|
api_key: System.get_env("GEOSPATIAL_GOOGLE_MAPS_API_KEY") || nil
|
||||||
|
|
||||||
|
config :mobilizon, Mobilizon.Service.Geospatial.MapQuest,
|
||||||
|
api_key: System.get_env("GEOSPATIAL_MAP_QUEST_API_KEY") || nil
|
||||||
|
@ -58,6 +58,8 @@ config :mobilizon, Mobilizon.Mailer,
|
|||||||
# Do not print debug messages in production
|
# Do not print debug messages in production
|
||||||
config :logger, level: System.get_env("MOBILIZON_LOGLEVEL") |> String.to_atom() || :info
|
config :logger, level: System.get_env("MOBILIZON_LOGLEVEL") |> String.to_atom() || :info
|
||||||
|
|
||||||
|
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim
|
||||||
|
|
||||||
# ## SSL Support
|
# ## SSL Support
|
||||||
#
|
#
|
||||||
# To get SSL working, you will need to add the `https` key
|
# To get SSL working, you will need to add the `https` key
|
||||||
|
@ -32,3 +32,5 @@ config :mobilizon, Mobilizon.Mailer, adapter: Bamboo.TestAdapter
|
|||||||
|
|
||||||
config :exvcr,
|
config :exvcr,
|
||||||
vcr_cassette_library_dir: "test/fixtures/vcr_cassettes"
|
vcr_cassette_library_dir: "test/fixtures/vcr_cassettes"
|
||||||
|
|
||||||
|
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Mock
|
||||||
|
85
lib/service/geospatial/addok.ex
Normal file
85
lib/service/geospatial/addok.ex
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
defmodule Mobilizon.Service.Geospatial.Addok do
|
||||||
|
@moduledoc """
|
||||||
|
[Addok](https://github.com/addok/addok) backend.
|
||||||
|
"""
|
||||||
|
alias Mobilizon.Service.Geospatial.Provider
|
||||||
|
require Logger
|
||||||
|
alias Mobilizon.Addresses.Address
|
||||||
|
|
||||||
|
@behaviour Provider
|
||||||
|
|
||||||
|
@endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
|
||||||
|
|
||||||
|
@impl Provider
|
||||||
|
@doc """
|
||||||
|
Addok implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
|
||||||
|
"""
|
||||||
|
@spec geocode(String.t(), keyword()) :: list(Address.t())
|
||||||
|
def geocode(lon, lat, options \\ []) do
|
||||||
|
url = build_url(:geocode, %{lon: lon, lat: lat}, options)
|
||||||
|
|
||||||
|
Logger.debug("Asking addok for addresses with #{url}")
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(url),
|
||||||
|
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||||
|
processData(features)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl Provider
|
||||||
|
@doc """
|
||||||
|
Addok implementation for `c:Mobilizon.Service.Geospatial.Provider.search/2`.
|
||||||
|
"""
|
||||||
|
@spec search(String.t(), keyword()) :: list(Address.t())
|
||||||
|
def search(q, options \\ []) do
|
||||||
|
url = build_url(:search, %{q: q}, options)
|
||||||
|
Logger.debug("Asking addok for addresses with #{url}")
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(url),
|
||||||
|
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||||
|
processData(features)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec build_url(atom(), map(), list()) :: String.t()
|
||||||
|
defp build_url(method, args, options) do
|
||||||
|
limit = Keyword.get(options, :limit, 10)
|
||||||
|
coords = Keyword.get(options, :coords, nil)
|
||||||
|
endpoint = Keyword.get(options, :endpoint, @endpoint)
|
||||||
|
|
||||||
|
case method do
|
||||||
|
:geocode ->
|
||||||
|
"#{endpoint}/reverse/?lon=#{args.lon}&lat=#{args.lat}&limit=#{limit}"
|
||||||
|
|
||||||
|
:search ->
|
||||||
|
url = "#{endpoint}/search/?q=#{URI.encode(args.q)}&limit=#{limit}"
|
||||||
|
if is_nil(coords), do: url, else: url <> "&lat=#{coords.lat}&lon=#{coords.lon}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp processData(features) do
|
||||||
|
features
|
||||||
|
|> Enum.map(fn %{"geometry" => geometry, "properties" => properties} ->
|
||||||
|
%Address{
|
||||||
|
addressCountry: Map.get(properties, "country"),
|
||||||
|
addressLocality: Map.get(properties, "city"),
|
||||||
|
addressRegion: Map.get(properties, "state"),
|
||||||
|
description: Map.get(properties, "name") || streetAddress(properties),
|
||||||
|
floor: Map.get(properties, "floor"),
|
||||||
|
geom: Map.get(geometry, "coordinates") |> Provider.coordinates(),
|
||||||
|
postalCode: Map.get(properties, "postcode"),
|
||||||
|
streetAddress: properties |> streetAddress()
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp streetAddress(properties) do
|
||||||
|
if Map.has_key?(properties, "housenumber") do
|
||||||
|
Map.get(properties, "housenumber") <> " " <> Map.get(properties, "street")
|
||||||
|
else
|
||||||
|
Map.get(properties, "street")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
15
lib/service/geospatial/geospatial.ex
Normal file
15
lib/service/geospatial/geospatial.ex
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
defmodule Mobilizon.Service.Geospatial do
|
||||||
|
@moduledoc """
|
||||||
|
Module to load the service adapter defined inside the configuration
|
||||||
|
|
||||||
|
See `Mobilizon.Service.Geospatial.Provider`
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the appropriate service adapter
|
||||||
|
|
||||||
|
According to the config behind `config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Module`
|
||||||
|
"""
|
||||||
|
@spec service() :: module()
|
||||||
|
def service(), do: Application.get_env(:mobilizon, __MODULE__) |> get_in([:service])
|
||||||
|
end
|
126
lib/service/geospatial/google_maps.ex
Normal file
126
lib/service/geospatial/google_maps.ex
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
defmodule Mobilizon.Service.Geospatial.GoogleMaps do
|
||||||
|
@moduledoc """
|
||||||
|
Google Maps [Geocoding service](https://developers.google.com/maps/documentation/geocoding/intro)
|
||||||
|
|
||||||
|
Note: Endpoint is hardcoded to Google Maps API
|
||||||
|
"""
|
||||||
|
alias Mobilizon.Service.Geospatial.Provider
|
||||||
|
alias Mobilizon.Addresses.Address
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@behaviour Provider
|
||||||
|
|
||||||
|
@api_key Application.get_env(:mobilizon, __MODULE__) |> get_in([:api_key])
|
||||||
|
|
||||||
|
@components [
|
||||||
|
"street_number",
|
||||||
|
"route",
|
||||||
|
"locality",
|
||||||
|
"administrative_area_level_1",
|
||||||
|
"country",
|
||||||
|
"postal_code"
|
||||||
|
]
|
||||||
|
|
||||||
|
@api_key_missing_message "API Key required to use Google Maps"
|
||||||
|
|
||||||
|
@impl Provider
|
||||||
|
@doc """
|
||||||
|
Google Maps implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
|
||||||
|
"""
|
||||||
|
@spec geocode(String.t(), keyword()) :: list(Address.t())
|
||||||
|
def geocode(lon, lat, options \\ []) do
|
||||||
|
url = build_url(:geocode, %{lon: lon, lat: lat}, options)
|
||||||
|
|
||||||
|
Logger.debug("Asking Google Maps for reverse geocode with #{url}")
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(url),
|
||||||
|
{:ok, %{"results" => results, "status" => "OK"}} <- Poison.decode(body) do
|
||||||
|
Enum.map(results, &process_data/1)
|
||||||
|
else
|
||||||
|
{:ok, %{"status" => "REQUEST_DENIED", "error_message" => error_message}} ->
|
||||||
|
raise ArgumentError, message: to_string(error_message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl Provider
|
||||||
|
@doc """
|
||||||
|
Google Maps implementation for `c:Mobilizon.Service.Geospatial.Provider.search/2`.
|
||||||
|
"""
|
||||||
|
@spec search(String.t(), keyword()) :: list(Address.t())
|
||||||
|
def search(q, options \\ []) do
|
||||||
|
url = build_url(:search, %{q: q}, options)
|
||||||
|
|
||||||
|
Logger.debug("Asking Google Maps for addresses with #{url}")
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(url),
|
||||||
|
{:ok, %{"results" => results, "status" => "OK"}} <- Poison.decode(body) do
|
||||||
|
Enum.map(results, fn entry -> process_data(entry) end)
|
||||||
|
else
|
||||||
|
{:ok, %{"status" => "REQUEST_DENIED", "error_message" => error_message}} ->
|
||||||
|
raise ArgumentError, message: to_string(error_message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec build_url(atom(), map(), list()) :: String.t()
|
||||||
|
defp build_url(method, args, options) do
|
||||||
|
limit = Keyword.get(options, :limit, 10)
|
||||||
|
lang = Keyword.get(options, :lang, "en")
|
||||||
|
api_key = Keyword.get(options, :api_key, @api_key)
|
||||||
|
if is_nil(api_key), do: raise(ArgumentError, message: @api_key_missing_message)
|
||||||
|
|
||||||
|
url =
|
||||||
|
"https://maps.googleapis.com/maps/api/geocode/json?limit=#{limit}&key=#{api_key}&language=#{
|
||||||
|
lang
|
||||||
|
}"
|
||||||
|
|
||||||
|
case method do
|
||||||
|
:search ->
|
||||||
|
url <> "&address=#{URI.encode(args.q)}"
|
||||||
|
|
||||||
|
:geocode ->
|
||||||
|
url <> "&latlng=#{args.lat},#{args.lon}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_data(%{
|
||||||
|
"formatted_address" => description,
|
||||||
|
"geometry" => %{"location" => %{"lat" => lat, "lng" => lon}},
|
||||||
|
"address_components" => components
|
||||||
|
}) do
|
||||||
|
components =
|
||||||
|
@components
|
||||||
|
|> Enum.reduce(%{}, fn component, acc ->
|
||||||
|
Map.put(acc, component, extract_component(components, component))
|
||||||
|
end)
|
||||||
|
|
||||||
|
%Address{
|
||||||
|
addressCountry: Map.get(components, "country"),
|
||||||
|
addressLocality: Map.get(components, "locality"),
|
||||||
|
addressRegion: Map.get(components, "administrative_area_level_1"),
|
||||||
|
description: description,
|
||||||
|
floor: nil,
|
||||||
|
geom: [lon, lat] |> Provider.coordinates(),
|
||||||
|
postalCode: Map.get(components, "postal_code"),
|
||||||
|
streetAddress: street_address(components)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extract_component(components, key) do
|
||||||
|
case components
|
||||||
|
|> Enum.filter(fn component -> key in component["types"] end)
|
||||||
|
|> Enum.map(& &1["long_name"]) do
|
||||||
|
[] -> nil
|
||||||
|
component -> hd(component)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp street_address(body) do
|
||||||
|
if Map.has_key?(body, "street_number") && !is_nil(Map.get(body, "street_number")) do
|
||||||
|
Map.get(body, "street_number") <> " " <> Map.get(body, "route")
|
||||||
|
else
|
||||||
|
Map.get(body, "route")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
116
lib/service/geospatial/map_quest.ex
Normal file
116
lib/service/geospatial/map_quest.ex
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
defmodule Mobilizon.Service.Geospatial.MapQuest do
|
||||||
|
@moduledoc """
|
||||||
|
[MapQuest](https://developer.mapquest.com/documentation) backend.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
In addition to the [the shared options](Mobilizon.Service.Geospatial.Provider.html#module-shared-options),
|
||||||
|
MapQuest methods support the following options:
|
||||||
|
* `:open_data` Whether to use [Open Data or Licenced Data](https://developer.mapquest.com/documentation/open/).
|
||||||
|
Defaults to `true`
|
||||||
|
"""
|
||||||
|
alias Mobilizon.Service.Geospatial.Provider
|
||||||
|
alias Mobilizon.Addresses.Address
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@behaviour Provider
|
||||||
|
|
||||||
|
@api_key Application.get_env(:mobilizon, __MODULE__) |> get_in([:api_key])
|
||||||
|
|
||||||
|
@api_key_missing_message "API Key required to use MapQuest"
|
||||||
|
|
||||||
|
@impl Provider
|
||||||
|
@doc """
|
||||||
|
MapQuest implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
|
||||||
|
"""
|
||||||
|
@spec geocode(String.t(), keyword()) :: list(Address.t())
|
||||||
|
def geocode(lon, lat, options \\ []) do
|
||||||
|
api_key = Keyword.get(options, :api_key, @api_key)
|
||||||
|
limit = Keyword.get(options, :limit, 10)
|
||||||
|
open_data = Keyword.get(options, :open_data, true)
|
||||||
|
|
||||||
|
prefix = if open_data, do: "open", else: "www"
|
||||||
|
|
||||||
|
if is_nil(api_key), do: raise(ArgumentError, message: @api_key_missing_message)
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(
|
||||||
|
"https://#{prefix}.mapquestapi.com/geocoding/v1/reverse?key=#{api_key}&location=#{
|
||||||
|
lat
|
||||||
|
},#{lon}&maxResults=#{limit}"
|
||||||
|
),
|
||||||
|
{:ok, %{"results" => results, "info" => %{"statuscode" => 0}}} <- Poison.decode(body) do
|
||||||
|
results |> Enum.map(&processData/1)
|
||||||
|
else
|
||||||
|
{:ok, %HTTPoison.Response{status_code: 403, body: err}} ->
|
||||||
|
raise(ArgumentError, message: err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl Provider
|
||||||
|
@doc """
|
||||||
|
MapQuest implementation for `c:Mobilizon.Service.Geospatial.Provider.search/2`.
|
||||||
|
"""
|
||||||
|
@spec search(String.t(), keyword()) :: list(Address.t())
|
||||||
|
def search(q, options \\ []) do
|
||||||
|
limit = Keyword.get(options, :limit, 10)
|
||||||
|
api_key = Keyword.get(options, :api_key, @api_key)
|
||||||
|
|
||||||
|
open_data = Keyword.get(options, :open_data, true)
|
||||||
|
|
||||||
|
prefix = if open_data, do: "open", else: "www"
|
||||||
|
|
||||||
|
if is_nil(api_key), do: raise(ArgumentError, message: @api_key_missing_message)
|
||||||
|
|
||||||
|
url =
|
||||||
|
"https://#{prefix}.mapquestapi.com/geocoding/v1/address?key=#{api_key}&location=#{
|
||||||
|
URI.encode(q)
|
||||||
|
}&maxResults=#{limit}"
|
||||||
|
|
||||||
|
Logger.debug("Asking MapQuest for addresses with #{url}")
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(url),
|
||||||
|
{:ok, %{"results" => results, "info" => %{"statuscode" => 0}}} <- Poison.decode(body) do
|
||||||
|
results |> Enum.map(&processData/1)
|
||||||
|
else
|
||||||
|
{:ok, %HTTPoison.Response{status_code: 403, body: err}} ->
|
||||||
|
raise(ArgumentError, message: err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp processData(
|
||||||
|
%{
|
||||||
|
"locations" => addresses,
|
||||||
|
"providedLocation" => %{"latLng" => %{"lat" => lat, "lng" => lng}}
|
||||||
|
} = _body
|
||||||
|
) do
|
||||||
|
case addresses do
|
||||||
|
[] -> nil
|
||||||
|
addresses -> addresses |> hd |> produceAddress(lat, lng)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp processData(%{"locations" => addresses}) do
|
||||||
|
case addresses do
|
||||||
|
[] -> nil
|
||||||
|
addresses -> addresses |> hd |> produceAddress()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp produceAddress(%{"latLng" => %{"lat" => lat, "lng" => lng}} = address) do
|
||||||
|
produceAddress(address, lat, lng)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp produceAddress(address, lat, lng) do
|
||||||
|
%Address{
|
||||||
|
addressCountry: Map.get(address, "adminArea1"),
|
||||||
|
addressLocality: Map.get(address, "adminArea5"),
|
||||||
|
addressRegion: Map.get(address, "adminArea3"),
|
||||||
|
description: Map.get(address, "street"),
|
||||||
|
floor: Map.get(address, "floor"),
|
||||||
|
geom: [lng, lat] |> Provider.coordinates(),
|
||||||
|
postalCode: Map.get(address, "postalCode"),
|
||||||
|
streetAddress: Map.get(address, "street")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
89
lib/service/geospatial/nominatim.ex
Normal file
89
lib/service/geospatial/nominatim.ex
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
defmodule Mobilizon.Service.Geospatial.Nominatim do
|
||||||
|
@moduledoc """
|
||||||
|
[Nominatim](https://wiki.openstreetmap.org/wiki/Nominatim) backend.
|
||||||
|
"""
|
||||||
|
alias Mobilizon.Service.Geospatial.Provider
|
||||||
|
alias Mobilizon.Addresses.Address
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@behaviour Provider
|
||||||
|
|
||||||
|
@endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
|
||||||
|
@api_key Application.get_env(:mobilizon, __MODULE__) |> get_in([:api_key])
|
||||||
|
|
||||||
|
@impl Provider
|
||||||
|
@doc """
|
||||||
|
Nominatim implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
|
||||||
|
"""
|
||||||
|
@spec geocode(String.t(), keyword()) :: list(Address.t())
|
||||||
|
def geocode(lon, lat, options \\ []) do
|
||||||
|
url = build_url(:geocode, %{lon: lon, lat: lat}, options)
|
||||||
|
Logger.debug("Asking Nominatim for geocode with #{url}")
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(url),
|
||||||
|
{:ok, body} <- Poison.decode(body) do
|
||||||
|
[process_data(body)]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl Provider
|
||||||
|
@doc """
|
||||||
|
Nominatim implementation for `c:Mobilizon.Service.Geospatial.Provider.search/2`.
|
||||||
|
"""
|
||||||
|
@spec search(String.t(), keyword()) :: list(Address.t())
|
||||||
|
def search(q, options \\ []) do
|
||||||
|
url = build_url(:search, %{q: q}, options)
|
||||||
|
Logger.debug("Asking Nominatim for addresses with #{url}")
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(url),
|
||||||
|
{:ok, body} <- Poison.decode(body) do
|
||||||
|
Enum.map(body, fn entry -> process_data(entry) end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec build_url(atom(), map(), list()) :: String.t()
|
||||||
|
defp build_url(method, args, options) do
|
||||||
|
limit = Keyword.get(options, :limit, 10)
|
||||||
|
lang = Keyword.get(options, :lang, "en")
|
||||||
|
endpoint = Keyword.get(options, :endpoint, @endpoint)
|
||||||
|
api_key = Keyword.get(options, :api_key, @api_key)
|
||||||
|
|
||||||
|
url =
|
||||||
|
case method do
|
||||||
|
:search ->
|
||||||
|
"#{endpoint}/search?format=jsonv2&q=#{URI.encode(args.q)}&limit=#{limit}&accept-language=#{
|
||||||
|
lang
|
||||||
|
}&addressdetails=1"
|
||||||
|
|
||||||
|
:geocode ->
|
||||||
|
"#{endpoint}/reverse?format=jsonv2&lat=#{args.lat}&lon=#{args.lon}&addressdetails=1"
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_nil(api_key), do: url, else: url <> "&key=#{api_key}"
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec process_data(map()) :: Address.t()
|
||||||
|
defp process_data(%{"address" => address} = body) do
|
||||||
|
%Address{
|
||||||
|
addressCountry: Map.get(address, "country"),
|
||||||
|
addressLocality: Map.get(address, "city"),
|
||||||
|
addressRegion: Map.get(address, "state"),
|
||||||
|
description: Map.get(body, "display_name"),
|
||||||
|
floor: Map.get(address, "floor"),
|
||||||
|
geom: [Map.get(body, "lon"), Map.get(body, "lat")] |> Provider.coordinates(),
|
||||||
|
postalCode: Map.get(address, "postcode"),
|
||||||
|
streetAddress: street_address(address)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec street_address(map()) :: String.t()
|
||||||
|
defp street_address(body) do
|
||||||
|
if Map.has_key?(body, "house_number") do
|
||||||
|
Map.get(body, "house_number") <> " " <> Map.get(body, "road")
|
||||||
|
else
|
||||||
|
Map.get(body, "road")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
87
lib/service/geospatial/photon.ex
Normal file
87
lib/service/geospatial/photon.ex
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
defmodule Mobilizon.Service.Geospatial.Photon do
|
||||||
|
@moduledoc """
|
||||||
|
[Photon](https://photon.komoot.de) backend.
|
||||||
|
"""
|
||||||
|
alias Mobilizon.Service.Geospatial.Provider
|
||||||
|
require Logger
|
||||||
|
alias Mobilizon.Addresses.Address
|
||||||
|
|
||||||
|
@behaviour Provider
|
||||||
|
|
||||||
|
@endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
|
||||||
|
|
||||||
|
@impl Provider
|
||||||
|
@doc """
|
||||||
|
Photon implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
|
||||||
|
|
||||||
|
Note: It seems results are quite wrong.
|
||||||
|
"""
|
||||||
|
@spec geocode(number(), number(), keyword()) :: list(Address.t())
|
||||||
|
def geocode(lon, lat, options \\ []) do
|
||||||
|
url = build_url(:geocode, %{lon: lon, lat: lat}, options)
|
||||||
|
Logger.debug("Asking photon for reverse geocoding with #{url}")
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(url),
|
||||||
|
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||||
|
processData(features)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl Provider
|
||||||
|
@doc """
|
||||||
|
Photon implementation for `c:Mobilizon.Service.Geospatial.Provider.search/2`.
|
||||||
|
"""
|
||||||
|
@spec search(String.t(), keyword()) :: list(Address.t())
|
||||||
|
def search(q, options \\ []) do
|
||||||
|
url = build_url(:search, %{q: q}, options)
|
||||||
|
Logger.debug("Asking photon for addresses with #{url}")
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(url),
|
||||||
|
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||||
|
processData(features)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec build_url(atom(), map(), list()) :: String.t()
|
||||||
|
defp build_url(method, args, options) do
|
||||||
|
limit = Keyword.get(options, :limit, 10)
|
||||||
|
lang = Keyword.get(options, :lang, "en")
|
||||||
|
coords = Keyword.get(options, :coords, nil)
|
||||||
|
endpoint = Keyword.get(options, :endpoint, @endpoint)
|
||||||
|
|
||||||
|
case method do
|
||||||
|
:search ->
|
||||||
|
url = "#{endpoint}/api/?q=#{URI.encode(args.q)}&lang=#{lang}&limit=#{limit}"
|
||||||
|
if is_nil(coords), do: url, else: url <> "&lat=#{coords.lat}&lon=#{coords.lon}"
|
||||||
|
|
||||||
|
:geocode ->
|
||||||
|
"#{endpoint}/reverse?lon=#{args.lon}&lat=#{args.lat}&lang=#{lang}&limit=#{limit}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp processData(features) do
|
||||||
|
features
|
||||||
|
|> Enum.map(fn %{"geometry" => geometry, "properties" => properties} ->
|
||||||
|
%Address{
|
||||||
|
addressCountry: Map.get(properties, "country"),
|
||||||
|
addressLocality: Map.get(properties, "city"),
|
||||||
|
addressRegion: Map.get(properties, "state"),
|
||||||
|
description: Map.get(properties, "name") || streetAddress(properties),
|
||||||
|
floor: Map.get(properties, "floor"),
|
||||||
|
geom: Map.get(geometry, "coordinates") |> Provider.coordinates(),
|
||||||
|
postalCode: Map.get(properties, "postcode"),
|
||||||
|
streetAddress: properties |> streetAddress()
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp streetAddress(properties) do
|
||||||
|
if Map.has_key?(properties, "housenumber") do
|
||||||
|
Map.get(properties, "housenumber") <> " " <> Map.get(properties, "street")
|
||||||
|
else
|
||||||
|
Map.get(properties, "street")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
72
lib/service/geospatial/provider.ex
Normal file
72
lib/service/geospatial/provider.ex
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
defmodule Mobilizon.Service.Geospatial.Provider do
|
||||||
|
@moduledoc """
|
||||||
|
Provider Behaviour for Geospatial stuff.
|
||||||
|
|
||||||
|
## Supported backends
|
||||||
|
|
||||||
|
* `Mobilizon.Service.Geospatial.Nominatim` [🔗](https://wiki.openstreetmap.org/wiki/Nominatim)
|
||||||
|
* `Mobilizon.Service.Geospatial.Photon` [🔗](https://photon.komoot.de)
|
||||||
|
* `Mobilizon.Service.Geospatial.Addok` [🔗](https://github.com/addok/addok)
|
||||||
|
* `Mobilizon.Service.Geospatial.MapQuest` [🔗](https://developer.mapquest.com/documentation/open/)
|
||||||
|
* `Mobilizon.Service.Geospatial.GoogleMaps` [🔗](https://developers.google.com/maps/documentation/geocoding/intro)
|
||||||
|
|
||||||
|
|
||||||
|
## Shared options
|
||||||
|
|
||||||
|
* `:user_agent` User-Agent string to send to the backend. Defaults to `"Mobilizon"`
|
||||||
|
* `:lang` Lang in which to prefer results. Used as a request parameter or through an `Accept-Language` HTTP header.
|
||||||
|
Defaults to `"en"`.
|
||||||
|
* `:limit` Maximum limit for the number of results returned by the backend. Defaults to `10`
|
||||||
|
* `:api_key` Allows to override the API key (if the backend requires one) set inside the configuration.
|
||||||
|
* `:endpoint` Allows to override the endpoint set inside the configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Mobilizon.Addresses.Address
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get an address from longitude and latitude coordinates.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
Most backends implement all of [the shared options](#module-shared-options).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> geocode(48.11, -1.77)
|
||||||
|
%Address{}
|
||||||
|
"""
|
||||||
|
@callback geocode(longitude :: number(), latitude :: number(), options :: keyword()) ::
|
||||||
|
list(Address.t())
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Search for an address
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
In addition to [the shared options](#module-shared-options), `c:search/2` also accepts the following options:
|
||||||
|
|
||||||
|
* `coords` Map of coordinates (ex: `%{lon: 48.11, lat: -1.77}`) allowing to give a geographic priority to the search.
|
||||||
|
Defaults to `nil`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> search("10 rue Jangot")
|
||||||
|
%Address{}
|
||||||
|
"""
|
||||||
|
@callback search(address :: String.t(), options :: keyword()) :: list(Address.t())
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a `Geo.Point` for given coordinates
|
||||||
|
"""
|
||||||
|
@spec coordinates(list(number()), number()) :: Geo.Point.t()
|
||||||
|
def coordinates(coords, srid \\ 4326)
|
||||||
|
|
||||||
|
def coordinates([x, y], srid) when is_number(x) and is_number(y),
|
||||||
|
do: %Geo.Point{coordinates: {x, y}, srid: srid}
|
||||||
|
|
||||||
|
def coordinates([x, y], srid) when is_bitstring(x) and is_bitstring(y),
|
||||||
|
do: %Geo.Point{coordinates: {String.to_float(x), String.to_float(y)}, srid: srid}
|
||||||
|
|
||||||
|
@spec coordinates(any()) :: nil
|
||||||
|
def coordinates(_, _), do: nil
|
||||||
|
end
|
2
mix.exs
2
mix.exs
@ -65,7 +65,6 @@ defmodule Mobilizon.Mixfile do
|
|||||||
{:geo, "~> 3.0"},
|
{:geo, "~> 3.0"},
|
||||||
{:geo_postgis, "~> 3.1"},
|
{:geo_postgis, "~> 3.1"},
|
||||||
{:timex, "~> 3.0"},
|
{:timex, "~> 3.0"},
|
||||||
# Waiting for new release
|
|
||||||
{:icalendar, "~> 0.7"},
|
{:icalendar, "~> 0.7"},
|
||||||
{:exgravatar, "~> 2.0.1"},
|
{:exgravatar, "~> 2.0.1"},
|
||||||
{:httpoison, "~> 1.0"},
|
{:httpoison, "~> 1.0"},
|
||||||
@ -89,6 +88,7 @@ defmodule Mobilizon.Mixfile do
|
|||||||
{:atomex, "0.3.0"},
|
{:atomex, "0.3.0"},
|
||||||
{:cachex, "~> 3.1"},
|
{:cachex, "~> 3.1"},
|
||||||
{:earmark, "~> 1.3.1"},
|
{:earmark, "~> 1.3.1"},
|
||||||
|
{:geohax, "~> 0.3.0"},
|
||||||
# Dev and test dependencies
|
# Dev and test dependencies
|
||||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||||
{:ex_machina, "~> 2.2", only: [:dev, :test]},
|
{:ex_machina, "~> 2.2", only: [:dev, :test]},
|
||||||
|
1
mix.lock
1
mix.lock
@ -48,6 +48,7 @@
|
|||||||
"gen_smtp": {:hex, :gen_smtp, "0.12.0", "97d44903f5ca18ca85cb39aee7d9c77e98d79804bbdef56078adcf905cb2ef00", [:rebar3], [], "hexpm"},
|
"gen_smtp": {:hex, :gen_smtp, "0.12.0", "97d44903f5ca18ca85cb39aee7d9c77e98d79804bbdef56078adcf905cb2ef00", [:rebar3], [], "hexpm"},
|
||||||
"geo": {:hex, :geo, "3.1.0", "727e005262430d037e870ff364e65d80ca5ca21d5ac8eddd57a1ada72c3f83b0", [:mix], [], "hexpm"},
|
"geo": {:hex, :geo, "3.1.0", "727e005262430d037e870ff364e65d80ca5ca21d5ac8eddd57a1ada72c3f83b0", [:mix], [], "hexpm"},
|
||||||
"geo_postgis": {:hex, :geo_postgis, "3.1.0", "d06c8fa5fd140a52a5c9dab4ad6623a696dd7d99dd791bb361d3f94942442ff9", [:mix], [{:geo, "~> 3.1", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm"},
|
"geo_postgis": {:hex, :geo_postgis, "3.1.0", "d06c8fa5fd140a52a5c9dab4ad6623a696dd7d99dd791bb361d3f94942442ff9", [:mix], [{:geo, "~> 3.1", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
"geohax": {:hex, :geohax, "0.3.0", "c2e7d8cc6cdf4158120b50fcbe03a296da561d2089eb7ad68d84b6f5d3df5607", [:mix], [], "hexpm"},
|
||||||
"geolix": {:hex, :geolix, "0.17.0", "8f3f4068be08599912de67ae24372a6c148794a0152f9f83ffd5a2ffcb21d29a", [:mix], [{:mmdb2_decoder, "~> 0.3.0", [hex: :mmdb2_decoder, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.0", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm"},
|
"geolix": {:hex, :geolix, "0.17.0", "8f3f4068be08599912de67ae24372a6c148794a0152f9f83ffd5a2ffcb21d29a", [:mix], [{:mmdb2_decoder, "~> 0.3.0", [hex: :mmdb2_decoder, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.0", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"},
|
"gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"},
|
||||||
"guardian": {:hex, :guardian, "1.2.1", "bdc8dd3dbf0fb7216cb6f91c11831faa1a64d39cdaed9a611e37f2413e584983", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"},
|
"guardian": {:hex, :guardian, "1.2.1", "bdc8dd3dbf0fb7216cb6f91c11831faa1a64d39cdaed9a611e37f2413e584983", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"},
|
||||||
|
28
test/fixtures/vcr_cassettes/geospatial/addok/geocode.json
vendored
Normal file
28
test/fixtures/vcr_cassettes/geospatial/addok/geocode.json
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"body": "",
|
||||||
|
"headers": [],
|
||||||
|
"method": "get",
|
||||||
|
"options": [],
|
||||||
|
"request_body": "",
|
||||||
|
"url": "https://api-adresse.data.gouv.fr/reverse/?lon=4.842569&lat=45.751718"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"binary": false,
|
||||||
|
"body": "{\"limit\": 1, \"features\": [{\"geometry\": {\"coordinates\": [4.842569, 45.751718], \"type\": \"Point\"}, \"properties\": {\"y\": 6518613.6, \"city\": \"Lyon\", \"label\": \"10 Rue Jangot 69007 Lyon\", \"score\": 1.0, \"distance\": 0, \"type\": \"housenumber\", \"street\": \"Rue Jangot\", \"name\": \"10 Rue Jangot\", \"x\": 843191.7, \"id\": \"69387_3650_f5ec2a\", \"housenumber\": \"10\", \"citycode\": \"69387\", \"context\": \"69, Rh\\u00f4ne, Auvergne-Rh\\u00f4ne-Alpes (Rh\\u00f4ne-Alpes)\", \"postcode\": \"69007\", \"importance\": 0.3164}, \"type\": \"Feature\"}], \"attribution\": \"BAN\", \"version\": \"draft\", \"type\": \"FeatureCollection\", \"licence\": \"ODbL 1.0\"}",
|
||||||
|
"headers": {
|
||||||
|
"Server": "nginx/1.13.4",
|
||||||
|
"Date": "Wed, 13 Mar 2019 17:22:17 GMT",
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Content-Length": "598",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"X-Cache-Status": "MISS",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Headers": "X-Requested-With"
|
||||||
|
},
|
||||||
|
"status_code": 200,
|
||||||
|
"type": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
29
test/fixtures/vcr_cassettes/geospatial/addok/search.json
vendored
Normal file
29
test/fixtures/vcr_cassettes/geospatial/addok/search.json
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"body": "",
|
||||||
|
"headers": [],
|
||||||
|
"method": "get",
|
||||||
|
"options": [],
|
||||||
|
"request_body": "",
|
||||||
|
"url": "https://api-adresse.data.gouv.fr/search/?q=10%20rue%20Jangot&limit=10"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"binary": false,
|
||||||
|
"body": "{\"limit\": 10, \"features\": [{\"geometry\": {\"coordinates\": [4.842569, 45.751718], \"type\": \"Point\"}, \"properties\": {\"y\": 6518573.3, \"city\": \"Lyon\", \"label\": \"10 Rue Jangot 69007 Lyon\", \"score\": 0.8469454545454544, \"type\": \"housenumber\", \"street\": \"Rue Jangot\", \"name\": \"10 Rue Jangot\", \"x\": 843232.2, \"id\": \"ADRNIVX_0000000260022046\", \"housenumber\": \"10\", \"citycode\": \"69387\", \"context\": \"69, Rh\\u00f4ne, Auvergne-Rh\\u00f4ne-Alpes (Rh\\u00f4ne-Alpes)\", \"postcode\": \"69007\", \"importance\": 0.3164}, \"type\": \"Feature\"}, {\"geometry\": {\"coordinates\": [2.440118, 50.371066], \"type\": \"Point\"}, \"properties\": {\"y\": 7030518.3, \"city\": \"Bailleul-aux-Cornailles\", \"label\": \"Rue Jangon 62127 Bailleul-aux-Cornailles\", \"score\": 0.5039055944055943, \"name\": \"Rue Jangon\", \"x\": 660114.7, \"id\": \"62070_0100_9b8d3c\", \"type\": \"street\", \"citycode\": \"62070\", \"context\": \"62, Pas-de-Calais, Hauts-de-France (Nord-Pas-de-Calais)\", \"postcode\": \"62127\", \"importance\": 0.0045}, \"type\": \"Feature\"}], \"attribution\": \"BAN\", \"version\": \"draft\", \"type\": \"FeatureCollection\", \"licence\": \"ODbL 1.0\", \"query\": \"10 rue Jangot\"}",
|
||||||
|
"headers": {
|
||||||
|
"Server": "nginx/1.13.4",
|
||||||
|
"Date": "Wed, 13 Mar 2019 17:01:21 GMT",
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Content-Length": "1087",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Vary": "Accept-Encoding",
|
||||||
|
"X-Cache-Status": "MISS",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Headers": "X-Requested-With"
|
||||||
|
},
|
||||||
|
"status_code": 200,
|
||||||
|
"type": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
33
test/fixtures/vcr_cassettes/geospatial/google_maps/geocode.json
vendored
Normal file
33
test/fixtures/vcr_cassettes/geospatial/google_maps/geocode.json
vendored
Normal file
File diff suppressed because one or more lines are too long
34
test/fixtures/vcr_cassettes/geospatial/google_maps/search.json
vendored
Normal file
34
test/fixtures/vcr_cassettes/geospatial/google_maps/search.json
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"body": "",
|
||||||
|
"headers": [],
|
||||||
|
"method": "get",
|
||||||
|
"options": [],
|
||||||
|
"request_body": "",
|
||||||
|
"url": "https://maps.googleapis.com/maps/api/geocode/json?address=10%20rue%20Jangot&limit=10&key=toto&language=en"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"binary": false,
|
||||||
|
"body": "{\n \"results\" : [\n {\n \"address_components\" : [\n {\n \"long_name\" : \"10\",\n \"short_name\" : \"10\",\n \"types\" : [ \"street_number\" ]\n },\n {\n \"long_name\" : \"Rue Jangot\",\n \"short_name\" : \"Rue Jangot\",\n \"types\" : [ \"route\" ]\n },\n {\n \"long_name\" : \"Lyon\",\n \"short_name\" : \"Lyon\",\n \"types\" : [ \"locality\", \"political\" ]\n },\n {\n \"long_name\" : \"Rhône\",\n \"short_name\" : \"Rhône\",\n \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n },\n {\n \"long_name\" : \"Auvergne-Rhône-Alpes\",\n \"short_name\" : \"Auvergne-Rhône-Alpes\",\n \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n },\n {\n \"long_name\" : \"France\",\n \"short_name\" : \"FR\",\n \"types\" : [ \"country\", \"political\" ]\n },\n {\n \"long_name\" : \"69007\",\n \"short_name\" : \"69007\",\n \"types\" : [ \"postal_code\" ]\n }\n ],\n \"formatted_address\" : \"10 Rue Jangot, 69007 Lyon, France\",\n \"geometry\" : {\n \"location\" : {\n \"lat\" : 45.75164940000001,\n \"lng\" : 4.8424032\n },\n \"location_type\" : \"ROOFTOP\",\n \"viewport\" : {\n \"northeast\" : {\n \"lat\" : 45.75299838029151,\n \"lng\" : 4.843752180291502\n },\n \"southwest\" : {\n \"lat\" : 45.75030041970851,\n \"lng\" : 4.841054219708497\n }\n }\n },\n \"place_id\" : \"ChIJtW0QikTq9EcRLI4Vy6bRx0U\",\n \"plus_code\" : {\n \"compound_code\" : \"QR2R+MX Lyon, France\",\n \"global_code\" : \"8FQ6QR2R+MX\"\n },\n \"types\" : [ \"street_address\" ]\n }\n ],\n \"status\" : \"OK\"\n}\n",
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json; charset=UTF-8",
|
||||||
|
"Date": "Wed, 13 Mar 2019 17:50:19 GMT",
|
||||||
|
"Expires": "Thu, 14 Mar 2019 17:50:19 GMT",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Server": "mafe",
|
||||||
|
"X-XSS-Protection": "1; mode=block",
|
||||||
|
"X-Frame-Options": "SAMEORIGIN",
|
||||||
|
"Server-Timing": "gfet4t7; dur=52",
|
||||||
|
"Cache-Control": "public, max-age=86400",
|
||||||
|
"Age": "17",
|
||||||
|
"Alt-Svc": "quic=\":443\"; ma=2592000; v=\"46,44,43,39\"",
|
||||||
|
"Accept-Ranges": "none",
|
||||||
|
"Vary": "Accept-Encoding",
|
||||||
|
"Transfer-Encoding": "chunked"
|
||||||
|
},
|
||||||
|
"status_code": 200,
|
||||||
|
"type": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
36
test/fixtures/vcr_cassettes/geospatial/map_quest/geocode.json
vendored
Normal file
36
test/fixtures/vcr_cassettes/geospatial/map_quest/geocode.json
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"body": "",
|
||||||
|
"headers": [],
|
||||||
|
"method": "get",
|
||||||
|
"options": [],
|
||||||
|
"request_body": "",
|
||||||
|
"url": "https://open.mapquestapi.com/geocoding/v1/reverse?key=secret_key&location=45.751718,4.842569&maxResults=10"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"binary": false,
|
||||||
|
"body": "{\"info\":{\"statuscode\":0,\"copyright\":{\"text\":\"\\u00A9 2019 MapQuest, Inc.\",\"imageUrl\":\"http://api.mqcdn.com/res/mqlogo.gif\",\"imageAltText\":\"\\u00A9 2019 MapQuest, Inc.\"},\"messages\":[]},\"options\":{\"maxResults\":1,\"thumbMaps\":true,\"ignoreLatLngInput\":false},\"results\":[{\"providedLocation\":{\"latLng\":{\"lat\":45.751718,\"lng\":4.842569}},\"locations\":[{\"street\":\"10 Rue Jangot\",\"adminArea6\":\"\",\"adminArea6Type\":\"Neighborhood\",\"adminArea5\":\"Lyon\",\"adminArea5Type\":\"City\",\"adminArea4\":\"\",\"adminArea4Type\":\"County\",\"adminArea3\":\"Auvergne-Rh\\u00F4ne-Alpes\",\"adminArea3Type\":\"State\",\"adminArea1\":\"FR\",\"adminArea1Type\":\"Country\",\"postalCode\":\"69007\",\"geocodeQualityCode\":\"P1AAA\",\"geocodeQuality\":\"POINT\",\"dragPoint\":false,\"sideOfStreet\":\"N\",\"linkId\":\"0\",\"unknownInput\":\"\",\"type\":\"s\",\"latLng\":{\"lat\":45.751714,\"lng\":4.842566},\"displayLatLng\":{\"lat\":45.751714,\"lng\":4.842566},\"mapUrl\":\"http://open.mapquestapi.com/staticmap/v5/map?key=secret_key&type=map&size=225,160&locations=45.7517141,4.8425657|marker-sm-50318A-1&scalebar=true&zoom=15&rand=-570915433\"}]}]}",
|
||||||
|
"headers": {
|
||||||
|
"Access-Control-Allow-Methods": "OPTIONS,GET,POST",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Cache-Control": "no-cache, must-revalidate",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
"Date": "Thu, 14 Mar 2019 09:27:01 GMT",
|
||||||
|
"Expires": "Mon, 20 Dec 1998 01:00:00 GMT",
|
||||||
|
"GeocodeTransactionCount": "0",
|
||||||
|
"Last-Modified": "Thu, 14 Mar 2019 09:27:01 GMT",
|
||||||
|
"Pragma": "no-cache",
|
||||||
|
"ReverseGeocodeTransactionCount": "1",
|
||||||
|
"Server": "Apache-Coyote/1.1",
|
||||||
|
"Set-Cookie": "JSESSIONID=something; Path=/; HttpOnly",
|
||||||
|
"status": "success",
|
||||||
|
"transactionWeight": "1.0",
|
||||||
|
"Content-Length": "1063",
|
||||||
|
"Connection": "keep-alive"
|
||||||
|
},
|
||||||
|
"status_code": 200,
|
||||||
|
"type": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
36
test/fixtures/vcr_cassettes/geospatial/map_quest/search.json
vendored
Normal file
36
test/fixtures/vcr_cassettes/geospatial/map_quest/search.json
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"body": "",
|
||||||
|
"headers": [],
|
||||||
|
"method": "get",
|
||||||
|
"options": [],
|
||||||
|
"request_body": "",
|
||||||
|
"url": "https://open.mapquestapi.com/geocoding/v1/address?key=secret_key&location=10%20rue%20Jangot&maxResults=10"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"binary": false,
|
||||||
|
"body": "{\"info\":{\"statuscode\":0,\"copyright\":{\"text\":\"\\u00A9 2019 MapQuest, Inc.\",\"imageUrl\":\"http://api.mqcdn.com/res/mqlogo.gif\",\"imageAltText\":\"\\u00A9 2019 MapQuest, Inc.\"},\"messages\":[]},\"options\":{\"maxResults\":10,\"thumbMaps\":true,\"ignoreLatLngInput\":false},\"results\":[{\"providedLocation\":{\"location\":\"10 rue Jangot\"},\"locations\":[{\"street\":\"10 Rue Jangot\",\"adminArea6\":\"7e\",\"adminArea6Type\":\"Neighborhood\",\"adminArea5\":\"Lyon\",\"adminArea5Type\":\"City\",\"adminArea4\":\"Lyon\",\"adminArea4Type\":\"County\",\"adminArea3\":\"Auvergne-Rh\\u00F4ne-Alpes\",\"adminArea3Type\":\"State\",\"adminArea1\":\"FR\",\"adminArea1Type\":\"Country\",\"postalCode\":\"69007\",\"geocodeQualityCode\":\"P1AXX\",\"geocodeQuality\":\"POINT\",\"dragPoint\":false,\"sideOfStreet\":\"N\",\"linkId\":\"0\",\"unknownInput\":\"\",\"type\":\"s\",\"latLng\":{\"lat\":45.751714,\"lng\":4.842566},\"displayLatLng\":{\"lat\":45.751714,\"lng\":4.842566},\"mapUrl\":\"http://open.mapquestapi.com/staticmap/v5/map?key=secret_key&type=map&size=225,160&locations=45.7517141,4.8425657|marker-sm-50318A-1&scalebar=true&zoom=15&rand=1358091752\"}]}]}",
|
||||||
|
"headers": {
|
||||||
|
"Access-Control-Allow-Methods": "OPTIONS,GET,POST",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Cache-Control": "no-cache, must-revalidate",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
"Date": "Thu, 14 Mar 2019 09:27:01 GMT",
|
||||||
|
"Expires": "Mon, 20 Dec 1998 01:00:00 GMT",
|
||||||
|
"GeocodeTransactionCount": "1",
|
||||||
|
"Last-Modified": "Thu, 14 Mar 2019 09:27:01 GMT",
|
||||||
|
"Pragma": "no-cache",
|
||||||
|
"ReverseGeocodeTransactionCount": "0",
|
||||||
|
"Server": "Apache-Coyote/1.1",
|
||||||
|
"Set-Cookie": "JSESSIONID=something; Path=/; HttpOnly",
|
||||||
|
"status": "success",
|
||||||
|
"transactionWeight": "1.0",
|
||||||
|
"Content-Length": "1055",
|
||||||
|
"Connection": "keep-alive"
|
||||||
|
},
|
||||||
|
"status_code": 200,
|
||||||
|
"type": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
30
test/fixtures/vcr_cassettes/geospatial/nominatim/geocode.json
vendored
Normal file
30
test/fixtures/vcr_cassettes/geospatial/nominatim/geocode.json
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"body": "",
|
||||||
|
"headers": [],
|
||||||
|
"method": "get",
|
||||||
|
"options": [],
|
||||||
|
"request_body": "",
|
||||||
|
"url": "https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=45.751718&lon=4.842569&addressdetails=1"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"binary": false,
|
||||||
|
"body": "{\"place_id\":41453794,\"licence\":\"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\",\"osm_type\":\"node\",\"osm_id\":3078260611,\"lat\":\"45.7517141\",\"lon\":\"4.8425657\",\"place_rank\":30,\"category\":\"place\",\"type\":\"house\",\"importance\":\"0\",\"addresstype\":\"place\",\"name\":null,\"display_name\":\"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, France métropolitaine, 69007, France\",\"address\":{\"house_number\":\"10\",\"road\":\"Rue Jangot\",\"suburb\":\"La Guillotière\",\"city_district\":\"Lyon 7e Arrondissement\",\"city\":\"Lyon\",\"county\":\"Lyon\",\"state_district\":\"Circonscription départementale du Rhône\",\"state\":\"Auvergne-Rhône-Alpes\",\"country\":\"France\",\"postcode\":\"69007\",\"country_code\":\"fr\"},\"boundingbox\":[\"45.7516141\",\"45.7518141\",\"4.8424657\",\"4.8426657\"]}",
|
||||||
|
"headers": {
|
||||||
|
"Date": "Thu, 14 Mar 2019 10:26:11 GMT",
|
||||||
|
"Server": "Apache/2.4.29 (Ubuntu)",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "OPTIONS,GET",
|
||||||
|
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
|
||||||
|
"Expect-CT": "max-age=0, report-uri=\"https://openstreetmap.report-uri.com/r/d/ct/reportOnly\"",
|
||||||
|
"Upgrade": "h2",
|
||||||
|
"Connection": "Upgrade, close",
|
||||||
|
"Transfer-Encoding": "chunked",
|
||||||
|
"Content-Type": "application/json; charset=UTF-8"
|
||||||
|
},
|
||||||
|
"status_code": 200,
|
||||||
|
"type": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
30
test/fixtures/vcr_cassettes/geospatial/nominatim/search.json
vendored
Normal file
30
test/fixtures/vcr_cassettes/geospatial/nominatim/search.json
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"body": "",
|
||||||
|
"headers": [],
|
||||||
|
"method": "get",
|
||||||
|
"options": [],
|
||||||
|
"request_body": "",
|
||||||
|
"url": "https://nominatim.openstreetmap.org/search?format=jsonv2&q=10%20rue%20Jangot&limit=10&accept-language=en&addressdetails=1"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"binary": false,
|
||||||
|
"body": "[{\"place_id\":41453794,\"licence\":\"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\",\"osm_type\":\"node\",\"osm_id\":3078260611,\"boundingbox\":[\"45.7516641\",\"45.7517641\",\"4.8425157\",\"4.8426157\"],\"lat\":\"45.7517141\",\"lon\":\"4.8425657\",\"display_name\":\"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France\",\"place_rank\":30,\"category\":\"place\",\"type\":\"house\",\"importance\":0.31100000000000005,\"address\":{\"house_number\":\"10\",\"road\":\"Rue Jangot\",\"suburb\":\"La Guillotière\",\"city_district\":\"Lyon 7e Arrondissement\",\"city\":\"Lyon\",\"county\":\"Lyon\",\"state_district\":\"Departemental constituency of Rhône\",\"state\":\"Auvergne-Rhône-Alpes\",\"country\":\"France\",\"postcode\":\"69007\",\"country_code\":\"fr\"}}]",
|
||||||
|
"headers": {
|
||||||
|
"Date": "Thu, 14 Mar 2019 10:24:24 GMT",
|
||||||
|
"Server": "Apache/2.4.29 (Ubuntu)",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "OPTIONS,GET",
|
||||||
|
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
|
||||||
|
"Expect-CT": "max-age=0, report-uri=\"https://openstreetmap.report-uri.com/r/d/ct/reportOnly\"",
|
||||||
|
"Upgrade": "h2",
|
||||||
|
"Connection": "Upgrade, close",
|
||||||
|
"Transfer-Encoding": "chunked",
|
||||||
|
"Content-Type": "application/json; charset=UTF-8"
|
||||||
|
},
|
||||||
|
"status_code": 200,
|
||||||
|
"type": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
26
test/fixtures/vcr_cassettes/geospatial/photon/geocode.json
vendored
Normal file
26
test/fixtures/vcr_cassettes/geospatial/photon/geocode.json
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"body": "",
|
||||||
|
"headers": [],
|
||||||
|
"method": "get",
|
||||||
|
"options": [],
|
||||||
|
"request_body": "",
|
||||||
|
"url": "https://photon.komoot.de/reverse?lon=4.842569&lat=45.751718"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"binary": false,
|
||||||
|
"body": "{\"features\":[{\"geometry\":{\"coordinates\":[4.8416864,45.7605435],\"type\":\"Point\"},\"type\":\"Feature\",\"properties\":{\"osm_id\":4662865602,\"osm_type\":\"N\",\"country\":\"France\",\"osm_key\":\"leisure\",\"city\":\"Lyon\",\"street\":\"Rue Pravaz\",\"osm_value\":\"fitness_centre\",\"postcode\":\"69003\",\"name\":\"L'appart Fitness\",\"state\":\"Auvergne-Rhône-Alpes\"}}],\"type\":\"FeatureCollection\"}",
|
||||||
|
"headers": {
|
||||||
|
"Server": "nginx/1.9.3 (Ubuntu)",
|
||||||
|
"Date": "Thu, 14 Mar 2019 10:46:45 GMT",
|
||||||
|
"Content-Type": "application/json;charset=utf-8",
|
||||||
|
"Transfer-Encoding": "chunked",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Access-Control-Allow-Origin": "*"
|
||||||
|
},
|
||||||
|
"status_code": 200,
|
||||||
|
"type": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
26
test/fixtures/vcr_cassettes/geospatial/photon/search.json
vendored
Normal file
26
test/fixtures/vcr_cassettes/geospatial/photon/search.json
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"body": "",
|
||||||
|
"headers": [],
|
||||||
|
"method": "get",
|
||||||
|
"options": [],
|
||||||
|
"request_body": "",
|
||||||
|
"url": "https://photon.komoot.de/api/?q=10%20rue%20Jangot&lang=en&limit=10"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"binary": false,
|
||||||
|
"body": "{\"features\":[{\"geometry\":{\"coordinates\":[4.8425657,45.7517141],\"type\":\"Point\"},\"type\":\"Feature\",\"properties\":{\"osm_id\":3078260611,\"osm_type\":\"N\",\"country\":\"France\",\"osm_key\":\"place\",\"housenumber\":\"10\",\"city\":\"Lyon\",\"street\":\"Rue Jangot\",\"osm_value\":\"house\",\"postcode\":\"69007\",\"state\":\"Auvergne-Rhône-Alpes\"}},{\"geometry\":{\"coordinates\":[4.8424254,45.7517056],\"type\":\"Point\"},\"type\":\"Feature\",\"properties\":{\"osm_id\":3078260612,\"osm_type\":\"N\",\"country\":\"France\",\"osm_key\":\"place\",\"housenumber\":\"10bis\",\"city\":\"Lyon\",\"street\":\"Rue Jangot\",\"osm_value\":\"house\",\"postcode\":\"69007\",\"state\":\"Auvergne-Rhône-Alpes\"}}],\"type\":\"FeatureCollection\"}",
|
||||||
|
"headers": {
|
||||||
|
"Server": "nginx/1.9.3 (Ubuntu)",
|
||||||
|
"Date": "Thu, 14 Mar 2019 10:46:43 GMT",
|
||||||
|
"Content-Type": "application/json;charset=utf-8",
|
||||||
|
"Transfer-Encoding": "chunked",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Access-Control-Allow-Origin": "*"
|
||||||
|
},
|
||||||
|
"status_code": 200,
|
||||||
|
"type": "ok"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
59
test/mobilizon/service/geospatial/addok_test.exs
Normal file
59
test/mobilizon/service/geospatial/addok_test.exs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
defmodule Mobilizon.Service.Geospatial.AddokTest do
|
||||||
|
use Mobilizon.DataCase, async: false
|
||||||
|
alias Mobilizon.Service.Geospatial.Addok
|
||||||
|
alias Mobilizon.Addresses.Address
|
||||||
|
|
||||||
|
import Mock
|
||||||
|
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
|
||||||
|
|
||||||
|
@endpoint Application.get_env(:mobilizon, Mobilizon.Service.Geospatial.Addok)
|
||||||
|
|> get_in([:endpoint])
|
||||||
|
@fake_endpoint "https://domain.tld"
|
||||||
|
|
||||||
|
describe "search address" do
|
||||||
|
test "produces a valid search address" do
|
||||||
|
with_mock HTTPoison, get: fn _url -> "{}" end do
|
||||||
|
Addok.search("10 Rue Jangot")
|
||||||
|
assert_called(HTTPoison.get("#{@endpoint}/search/?q=10%20Rue%20Jangot&limit=10"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "produces a valid search address with options" do
|
||||||
|
with_mock HTTPoison, get: fn _url -> "{}" end do
|
||||||
|
Addok.search("10 Rue Jangot",
|
||||||
|
endpoint: @fake_endpoint,
|
||||||
|
limit: 5,
|
||||||
|
coords: %{lat: 49, lon: 12}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_called(
|
||||||
|
HTTPoison.get("#{@fake_endpoint}/search/?q=10%20Rue%20Jangot&limit=5&lat=49&lon=12")
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns a valid address from search" do
|
||||||
|
use_cassette "geospatial/addok/search" do
|
||||||
|
assert %Address{
|
||||||
|
addressLocality: "Lyon",
|
||||||
|
description: "10 Rue Jangot",
|
||||||
|
postalCode: "69007",
|
||||||
|
streetAddress: "10 Rue Jangot",
|
||||||
|
geom: %Geo.Point{coordinates: {4.842569, 45.751718}, properties: %{}, srid: 4326}
|
||||||
|
} == Addok.search("10 rue Jangot") |> hd
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns a valid address from reverse geocode" do
|
||||||
|
use_cassette "geospatial/addok/geocode" do
|
||||||
|
assert %Address{
|
||||||
|
addressLocality: "Lyon",
|
||||||
|
description: "10 Rue Jangot",
|
||||||
|
postalCode: "69007",
|
||||||
|
streetAddress: "10 Rue Jangot",
|
||||||
|
geom: %Geo.Point{coordinates: {4.842569, 45.751718}, properties: %{}, srid: 4326}
|
||||||
|
} == Addok.geocode(4.842569, 45.751718) |> hd
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
8
test/mobilizon/service/geospatial/geospatial_test.exs
Normal file
8
test/mobilizon/service/geospatial/geospatial_test.exs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
defmodule Mobilizon.Service.GeospatialTest do
|
||||||
|
use Mobilizon.DataCase
|
||||||
|
alias Mobilizon.Service.Geospatial
|
||||||
|
|
||||||
|
describe "get service" do
|
||||||
|
assert Geospatial.service() === Elixir.Mobilizon.Service.Geospatial.Mock
|
||||||
|
end
|
||||||
|
end
|
80
test/mobilizon/service/geospatial/google_maps_test.exs
Normal file
80
test/mobilizon/service/geospatial/google_maps_test.exs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
defmodule Mobilizon.Service.Geospatial.GoogleMapsTest do
|
||||||
|
use Mobilizon.DataCase, async: false
|
||||||
|
alias Mobilizon.Service.Geospatial.GoogleMaps
|
||||||
|
alias Mobilizon.Addresses.Address
|
||||||
|
|
||||||
|
import Mock
|
||||||
|
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
|
||||||
|
|
||||||
|
describe "search address" do
|
||||||
|
test "without API Key triggers an error" do
|
||||||
|
assert_raise ArgumentError, "API Key required to use Google Maps", fn ->
|
||||||
|
GoogleMaps.search("10 Rue Jangot")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "produces a valid search address with options" do
|
||||||
|
with_mock HTTPoison,
|
||||||
|
get: fn _url ->
|
||||||
|
{:ok,
|
||||||
|
%HTTPoison.Response{status_code: 200, body: "{\"status\": \"OK\", \"results\": []}"}}
|
||||||
|
end do
|
||||||
|
GoogleMaps.search("10 Rue Jangot",
|
||||||
|
limit: 5,
|
||||||
|
lang: "fr",
|
||||||
|
api_key: "toto"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_called(
|
||||||
|
HTTPoison.get(
|
||||||
|
"https://maps.googleapis.com/maps/api/geocode/json?limit=5&key=toto&language=fr&address=10%20Rue%20Jangot"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "triggers an error with an invalid API Key" do
|
||||||
|
assert_raise ArgumentError, "The provided API key is invalid.", fn ->
|
||||||
|
GoogleMaps.search("10 rue Jangot", api_key: "secret_key")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns a valid address from search" do
|
||||||
|
use_cassette "geospatial/google_maps/search" do
|
||||||
|
assert %Address{
|
||||||
|
addressLocality: "Lyon",
|
||||||
|
description: "10 Rue Jangot, 69007 Lyon, France",
|
||||||
|
addressRegion: "Auvergne-Rhône-Alpes",
|
||||||
|
addressCountry: "France",
|
||||||
|
postalCode: "69007",
|
||||||
|
streetAddress: "10 Rue Jangot",
|
||||||
|
geom: %Geo.Point{
|
||||||
|
coordinates: {4.8424032, 45.75164940000001},
|
||||||
|
properties: %{},
|
||||||
|
srid: 4326
|
||||||
|
}
|
||||||
|
} == GoogleMaps.search("10 rue Jangot", api_key: "toto") |> hd
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns a valid address from reverse geocode" do
|
||||||
|
use_cassette "geospatial/google_maps/geocode" do
|
||||||
|
assert %Address{
|
||||||
|
addressLocality: "Lyon",
|
||||||
|
description: "10 Rue Jangot, 69007 Lyon, France",
|
||||||
|
addressRegion: "Auvergne-Rhône-Alpes",
|
||||||
|
addressCountry: "France",
|
||||||
|
postalCode: "69007",
|
||||||
|
streetAddress: "10 Rue Jangot",
|
||||||
|
geom: %Geo.Point{
|
||||||
|
coordinates: {4.8424967, 45.751725},
|
||||||
|
properties: %{},
|
||||||
|
srid: 4326
|
||||||
|
}
|
||||||
|
} ==
|
||||||
|
GoogleMaps.geocode(4.842569, 45.751718, api_key: "toto")
|
||||||
|
|> hd
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
85
test/mobilizon/service/geospatial/map_quest_test.exs
Normal file
85
test/mobilizon/service/geospatial/map_quest_test.exs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
defmodule Mobilizon.Service.Geospatial.MapQuestTest do
|
||||||
|
use Mobilizon.DataCase, async: false
|
||||||
|
alias Mobilizon.Service.Geospatial.MapQuest
|
||||||
|
alias Mobilizon.Addresses.Address
|
||||||
|
|
||||||
|
import Mock
|
||||||
|
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
|
||||||
|
|
||||||
|
describe "search address" do
|
||||||
|
test "without API Key triggers an error" do
|
||||||
|
assert_raise ArgumentError, "API Key required to use MapQuest", fn ->
|
||||||
|
MapQuest.search("10 Rue Jangot")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "produces a valid search address with options" do
|
||||||
|
with_mock HTTPoison,
|
||||||
|
get: fn _url ->
|
||||||
|
{:ok,
|
||||||
|
%HTTPoison.Response{
|
||||||
|
status_code: 200,
|
||||||
|
body: "{\"info\": {\"statuscode\": 0}, \"results\": []}"
|
||||||
|
}}
|
||||||
|
end do
|
||||||
|
MapQuest.search("10 Rue Jangot",
|
||||||
|
limit: 5,
|
||||||
|
lang: "fr",
|
||||||
|
api_key: "toto"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_called(
|
||||||
|
HTTPoison.get(
|
||||||
|
"https://open.mapquestapi.com/geocoding/v1/address?key=toto&location=10%20Rue%20Jangot&maxResults=5"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "triggers an error with an invalid API Key" do
|
||||||
|
assert_raise ArgumentError, "The AppKey submitted with this request is invalid.", fn ->
|
||||||
|
MapQuest.search("10 rue Jangot", api_key: "secret_key")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns a valid address from search" do
|
||||||
|
use_cassette "geospatial/map_quest/search" do
|
||||||
|
assert %Address{
|
||||||
|
addressLocality: "Lyon",
|
||||||
|
description: "10 Rue Jangot",
|
||||||
|
addressRegion: "Auvergne-Rhône-Alpes",
|
||||||
|
addressCountry: "FR",
|
||||||
|
postalCode: "69007",
|
||||||
|
streetAddress: "10 Rue Jangot",
|
||||||
|
geom: %Geo.Point{
|
||||||
|
coordinates: {4.842566, 45.751714},
|
||||||
|
properties: %{},
|
||||||
|
srid: 4326
|
||||||
|
}
|
||||||
|
} ==
|
||||||
|
MapQuest.search("10 rue Jangot", api_key: "secret_key")
|
||||||
|
|> hd
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns a valid address from reverse geocode" do
|
||||||
|
use_cassette "geospatial/map_quest/geocode" do
|
||||||
|
assert %Address{
|
||||||
|
addressLocality: "Lyon",
|
||||||
|
description: "10 Rue Jangot",
|
||||||
|
addressRegion: "Auvergne-Rhône-Alpes",
|
||||||
|
addressCountry: "FR",
|
||||||
|
postalCode: "69007",
|
||||||
|
streetAddress: "10 Rue Jangot",
|
||||||
|
geom: %Geo.Point{
|
||||||
|
coordinates: {4.842569, 45.751718},
|
||||||
|
properties: %{},
|
||||||
|
srid: 4326
|
||||||
|
}
|
||||||
|
} ==
|
||||||
|
MapQuest.geocode(4.842569, 45.751718, api_key: "secret_key")
|
||||||
|
|> hd
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
68
test/mobilizon/service/geospatial/nominatim_test.exs
Normal file
68
test/mobilizon/service/geospatial/nominatim_test.exs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
defmodule Mobilizon.Service.Geospatial.NominatimTest do
|
||||||
|
use Mobilizon.DataCase, async: false
|
||||||
|
alias Mobilizon.Service.Geospatial.Nominatim
|
||||||
|
alias Mobilizon.Addresses.Address
|
||||||
|
|
||||||
|
import Mock
|
||||||
|
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
|
||||||
|
|
||||||
|
describe "search address" do
|
||||||
|
test "produces a valid search address with options" do
|
||||||
|
with_mock HTTPoison,
|
||||||
|
get: fn _url ->
|
||||||
|
{:ok, %HTTPoison.Response{status_code: 200, body: "[]"}}
|
||||||
|
end do
|
||||||
|
Nominatim.search("10 Rue Jangot",
|
||||||
|
limit: 5,
|
||||||
|
lang: "fr"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_called(
|
||||||
|
HTTPoison.get(
|
||||||
|
"https://nominatim.openstreetmap.org/search?format=jsonv2&q=10%20Rue%20Jangot&limit=5&accept-language=fr&addressdetails=1"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns a valid address from search" do
|
||||||
|
use_cassette "geospatial/nominatim/search" do
|
||||||
|
assert %Address{
|
||||||
|
addressLocality: "Lyon",
|
||||||
|
description:
|
||||||
|
"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France",
|
||||||
|
addressRegion: "Auvergne-Rhône-Alpes",
|
||||||
|
addressCountry: "France",
|
||||||
|
postalCode: "69007",
|
||||||
|
streetAddress: "10 Rue Jangot",
|
||||||
|
geom: %Geo.Point{
|
||||||
|
coordinates: {4.8425657, 45.7517141},
|
||||||
|
properties: %{},
|
||||||
|
srid: 4326
|
||||||
|
}
|
||||||
|
} == Nominatim.search("10 rue Jangot") |> hd
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns a valid address from reverse geocode" do
|
||||||
|
use_cassette "geospatial/nominatim/geocode" do
|
||||||
|
assert %Address{
|
||||||
|
addressLocality: "Lyon",
|
||||||
|
description:
|
||||||
|
"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, France métropolitaine, 69007, France",
|
||||||
|
addressRegion: "Auvergne-Rhône-Alpes",
|
||||||
|
addressCountry: "France",
|
||||||
|
postalCode: "69007",
|
||||||
|
streetAddress: "10 Rue Jangot",
|
||||||
|
geom: %Geo.Point{
|
||||||
|
coordinates: {4.8425657, 45.7517141},
|
||||||
|
properties: %{},
|
||||||
|
srid: 4326
|
||||||
|
}
|
||||||
|
} ==
|
||||||
|
Nominatim.geocode(4.842569, 45.751718)
|
||||||
|
|> hd
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
65
test/mobilizon/service/geospatial/photon_test.exs
Normal file
65
test/mobilizon/service/geospatial/photon_test.exs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
defmodule Mobilizon.Service.Geospatial.PhotonTest do
|
||||||
|
use Mobilizon.DataCase, async: false
|
||||||
|
alias Mobilizon.Service.Geospatial.Photon
|
||||||
|
alias Mobilizon.Addresses.Address
|
||||||
|
|
||||||
|
import Mock
|
||||||
|
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
|
||||||
|
|
||||||
|
describe "search address" do
|
||||||
|
test "produces a valid search address with options" do
|
||||||
|
with_mock HTTPoison,
|
||||||
|
get: fn _url ->
|
||||||
|
{:ok, %HTTPoison.Response{status_code: 200, body: "{\"features\": []"}}
|
||||||
|
end do
|
||||||
|
Photon.search("10 Rue Jangot",
|
||||||
|
limit: 5,
|
||||||
|
lang: "fr"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_called(
|
||||||
|
HTTPoison.get("https://photon.komoot.de/api/?q=10%20Rue%20Jangot&lang=fr&limit=5")
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns a valid address from search" do
|
||||||
|
use_cassette "geospatial/photon/search" do
|
||||||
|
assert %Address{
|
||||||
|
addressLocality: "Lyon",
|
||||||
|
description: "10 Rue Jangot",
|
||||||
|
addressRegion: "Auvergne-Rhône-Alpes",
|
||||||
|
addressCountry: "France",
|
||||||
|
postalCode: "69007",
|
||||||
|
streetAddress: "10 Rue Jangot",
|
||||||
|
geom: %Geo.Point{
|
||||||
|
coordinates: {4.8425657, 45.7517141},
|
||||||
|
properties: %{},
|
||||||
|
srid: 4326
|
||||||
|
}
|
||||||
|
} == Photon.search("10 rue Jangot") |> hd
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Photon returns something quite wrong, so no need to test this right now.
|
||||||
|
# test "returns a valid address from reverse geocode" do
|
||||||
|
# use_cassette "geospatial/photon/geocode" do
|
||||||
|
# assert %Address{
|
||||||
|
# addressLocality: "Lyon",
|
||||||
|
# description: "",
|
||||||
|
# addressRegion: "Auvergne-Rhône-Alpes",
|
||||||
|
# addressCountry: "France",
|
||||||
|
# postalCode: "69007",
|
||||||
|
# streetAddress: "10 Rue Jangot",
|
||||||
|
# geom: %Geo.Point{
|
||||||
|
# coordinates: {4.8425657, 45.7517141},
|
||||||
|
# properties: %{},
|
||||||
|
# srid: 4326
|
||||||
|
# }
|
||||||
|
# } ==
|
||||||
|
# Photon.geocode(4.8425657, 45.7517141)
|
||||||
|
# |> hd
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
end
|
15
test/support/mocks/geospatial_mock.ex
Normal file
15
test/support/mocks/geospatial_mock.ex
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
defmodule Mobilizon.Mobilizon.Service.Geospatial.Mock do
|
||||||
|
@moduledoc """
|
||||||
|
Mock for Geospatial Provider implementations
|
||||||
|
"""
|
||||||
|
alias Mobilizon.Service.Geospatial.Provider
|
||||||
|
alias Mobilizon.Addresses.Address
|
||||||
|
|
||||||
|
@behaviour Provider
|
||||||
|
|
||||||
|
@impl Provider
|
||||||
|
def geocode(_lon, _lat, _options \\ []), do: [%Address{}]
|
||||||
|
|
||||||
|
@impl Provider
|
||||||
|
def search(_q, _options \\ []), do: [%Address{}]
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user