Merge branch 'geospatial' into 'master'
Add GeoSpatial backends for geocoding See merge request framasoft/mobilizon!95
This commit is contained in:
commit
c20eaa379c
@ -64,3 +64,20 @@ config :arc,
|
||||
storage: Arc.Storage.Local
|
||||
|
||||
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
|
||||
config :logger, level: System.get_env("MOBILIZON_LOGLEVEL") |> String.to_atom() || :info
|
||||
|
||||
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim
|
||||
|
||||
# ## SSL Support
|
||||
#
|
||||
# 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,
|
||||
vcr_cassette_library_dir: "test/fixtures/vcr_cassettes"
|
||||
|
||||
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Mock
|
||||
|
@ -108,6 +108,7 @@ defmodule Mobilizon.Addresses do
|
||||
@doc """
|
||||
Processes raw geo data informations and return a `Geo` geometry which can be one of `Geo.Point`.
|
||||
"""
|
||||
# TODO: Unused, remove me
|
||||
def process_geom(%{"type" => type_input, "data" => data}) do
|
||||
type =
|
||||
if !is_atom(type_input) && type_input != nil do
|
||||
@ -145,4 +146,62 @@ defmodule Mobilizon.Addresses do
|
||||
defp process_point(_, _) do
|
||||
{:error, "Latitude and longitude must be numbers"}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Search addresses in our database
|
||||
|
||||
We only look at the description for now, and eventually order by object distance
|
||||
"""
|
||||
@spec search_addresses(String.t(), list()) :: list(Address.t())
|
||||
def search_addresses(search, options) do
|
||||
limit = Keyword.get(options, :limit, 5)
|
||||
|
||||
query = from(a in Address, where: ilike(a.description, ^"%#{search}%"), limit: ^limit)
|
||||
|
||||
query =
|
||||
if coords = Keyword.get(options, :coords, false),
|
||||
do:
|
||||
from(a in query,
|
||||
order_by: [fragment("? <-> ?", a.geom, ^"POINT(#{coords.lon} #{coords.lat})'")]
|
||||
),
|
||||
else: query
|
||||
|
||||
query =
|
||||
if country = Keyword.get(options, :country, nil),
|
||||
do: from(a in query, where: ilike(a.addressCountry, ^"%#{country}%")),
|
||||
else: query
|
||||
|
||||
Repo.all(query)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reverse geocode from coordinates in our database
|
||||
|
||||
We only take addresses 50km around and sort them by distance
|
||||
"""
|
||||
@spec reverse_geocode(number(), number(), list()) :: list(Address.t())
|
||||
def reverse_geocode(lon, lat, options) do
|
||||
limit = Keyword.get(options, :limit, 5)
|
||||
radius = Keyword.get(options, :radius, 50_000)
|
||||
country = Keyword.get(options, :country, nil)
|
||||
srid = Keyword.get(options, :srid, 4326)
|
||||
|
||||
import Geo.PostGIS
|
||||
|
||||
with {:ok, point} <- Geo.WKT.decode("SRID=#{srid};POINT(#{lon} #{lat})") do
|
||||
query =
|
||||
from(a in Address,
|
||||
order_by: [fragment("? <-> ?", a.geom, ^point)],
|
||||
limit: ^limit,
|
||||
where: st_dwithin_in_meters(^point, a.geom, ^radius)
|
||||
)
|
||||
|
||||
query =
|
||||
if country,
|
||||
do: from(a in query, where: ilike(a.addressCountry, ^"%#{country}%")),
|
||||
else: query
|
||||
|
||||
Repo.all(query)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -12,11 +12,17 @@ defmodule MobilizonWeb.Context do
|
||||
end
|
||||
|
||||
def call(conn, _) do
|
||||
with %User{} = user <- Guardian.Plug.current_resource(conn) do
|
||||
put_private(conn, :absinthe, %{context: %{current_user: user}})
|
||||
else
|
||||
nil ->
|
||||
conn
|
||||
end
|
||||
context = %{ip: to_string(:inet_parse.ntoa(conn.remote_ip))}
|
||||
|
||||
context =
|
||||
case Guardian.Plug.current_resource(conn) do
|
||||
%User{} = user ->
|
||||
Map.put(context, :current_user, user)
|
||||
|
||||
nil ->
|
||||
context
|
||||
end
|
||||
|
||||
put_private(conn, :absinthe, %{context: context})
|
||||
end
|
||||
end
|
||||
|
33
lib/mobilizon_web/resolvers/address.ex
Normal file
33
lib/mobilizon_web/resolvers/address.ex
Normal file
@ -0,0 +1,33 @@
|
||||
defmodule MobilizonWeb.Resolvers.Address do
|
||||
@moduledoc """
|
||||
Handles the comment-related GraphQL calls
|
||||
"""
|
||||
require Logger
|
||||
alias Mobilizon.Addresses
|
||||
alias Mobilizon.Service.Geospatial
|
||||
|
||||
def search(_parent, %{query: query}, %{context: %{ip: ip}}) do
|
||||
country = Geolix.lookup(ip) |> Map.get(:country, nil)
|
||||
|
||||
local_addresses = Task.async(fn -> Addresses.search_addresses(query, country: country) end)
|
||||
|
||||
remote_addresses = Task.async(fn -> Geospatial.service().search(query) end)
|
||||
|
||||
addresses = Task.await(local_addresses) ++ Task.await(remote_addresses)
|
||||
|
||||
{:ok, addresses}
|
||||
end
|
||||
|
||||
def reverse_geocode(_parent, %{longitude: longitude, latitude: latitude}, %{context: %{ip: ip}}) do
|
||||
country = Geolix.lookup(ip) |> Map.get(:country, nil)
|
||||
|
||||
local_addresses =
|
||||
Task.async(fn -> Addresses.reverse_geocode(longitude, latitude, country: country) end)
|
||||
|
||||
remote_addresses = Task.async(fn -> Geospatial.service().geocode(longitude, latitude) end)
|
||||
|
||||
addresses = Task.await(local_addresses) ++ Task.await(remote_addresses)
|
||||
|
||||
{:ok, addresses}
|
||||
end
|
||||
end
|
@ -132,6 +132,7 @@ defmodule MobilizonWeb.Schema do
|
||||
import_fields(:event_queries)
|
||||
import_fields(:participant_queries)
|
||||
import_fields(:tag_queries)
|
||||
import_fields(:address_queries)
|
||||
end
|
||||
|
||||
@desc """
|
||||
|
@ -3,6 +3,7 @@ defmodule MobilizonWeb.Schema.AddressType do
|
||||
Schema representation for Address
|
||||
"""
|
||||
use Absinthe.Schema.Notation
|
||||
alias MobilizonWeb.Resolvers
|
||||
|
||||
object :physical_address do
|
||||
field(:type, :address_type)
|
||||
@ -36,4 +37,21 @@ defmodule MobilizonWeb.Schema.AddressType do
|
||||
value(:phone, description: "The address is a phone number for a conference")
|
||||
value(:other, description: "The address is something else")
|
||||
end
|
||||
|
||||
object :address_queries do
|
||||
@desc "Search for an address"
|
||||
field :search_address, type: list_of(:physical_address) do
|
||||
arg(:query, non_null(:string))
|
||||
|
||||
resolve(&Resolvers.Address.search/3)
|
||||
end
|
||||
|
||||
@desc "Reverse geocode coordinates"
|
||||
field :reverse_geocode, type: list_of(:physical_address) do
|
||||
arg(:longitude, non_null(:float))
|
||||
arg(:latitude, non_null(:float))
|
||||
|
||||
resolve(&Resolvers.Address.reverse_geocode/3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
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_postgis, "~> 3.1"},
|
||||
{:timex, "~> 3.0"},
|
||||
# Waiting for new release
|
||||
{:icalendar, "~> 0.7"},
|
||||
{:exgravatar, "~> 2.0.1"},
|
||||
{:httpoison, "~> 1.0"},
|
||||
@ -89,6 +88,7 @@ defmodule Mobilizon.Mixfile do
|
||||
{:atomex, "0.3.0"},
|
||||
{:cachex, "~> 3.1"},
|
||||
{:earmark, "~> 1.3.1"},
|
||||
{:geohax, "~> 0.3.0"},
|
||||
# Dev and test dependencies
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{: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"},
|
||||
"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"},
|
||||
"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"},
|
||||
"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"},
|
||||
|
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
|
65
test/mobilizon_web/resolvers/address_resolver_test.exs
Normal file
65
test/mobilizon_web/resolvers/address_resolver_test.exs
Normal file
@ -0,0 +1,65 @@
|
||||
defmodule MobilizonWeb.Resolvers.AddressResolverTest do
|
||||
use MobilizonWeb.ConnCase
|
||||
alias MobilizonWeb.AbsintheHelpers
|
||||
import Mobilizon.Factory
|
||||
|
||||
describe "Address Resolver" do
|
||||
test "search/3 search for addresses", %{conn: conn} do
|
||||
address = insert(:address, description: "10 rue Jangot, Lyon")
|
||||
|
||||
query = """
|
||||
{
|
||||
searchAddress(query: "10 Rue Jangot") {
|
||||
description,
|
||||
geom
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
res =
|
||||
conn
|
||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "address"))
|
||||
|
||||
json_response(res, 200)["data"]["searchAddress"]
|
||||
|> Enum.each(fn addr -> assert Map.get(addr, "description") == address.description end)
|
||||
end
|
||||
|
||||
test "geocode/3 reverse geocodes coordinates", %{conn: conn} do
|
||||
address =
|
||||
insert(:address,
|
||||
description: "10 rue Jangot, Lyon"
|
||||
)
|
||||
|
||||
query = """
|
||||
{
|
||||
reverseGeocode(longitude: -23.01, latitude: 30.01) {
|
||||
description,
|
||||
geom
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
res =
|
||||
conn
|
||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "address"))
|
||||
|
||||
assert json_response(res, 200)["data"]["reverseGeocode"] == []
|
||||
|
||||
query = """
|
||||
{
|
||||
reverseGeocode(longitude: 45.75, latitude: 4.85) {
|
||||
description,
|
||||
geom
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
res =
|
||||
conn
|
||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "address"))
|
||||
|
||||
assert json_response(res, 200)["data"]["reverseGeocode"] |> hd |> Map.get("description") ==
|
||||
address.description
|
||||
end
|
||||
end
|
||||
end
|
@ -70,7 +70,7 @@ defmodule Mobilizon.Factory do
|
||||
def address_factory do
|
||||
%Mobilizon.Addresses.Address{
|
||||
description: sequence("MyAddress"),
|
||||
geom: %Geo.Point{coordinates: {30, -90}, srid: 4326},
|
||||
geom: %Geo.Point{coordinates: {45.75, 4.85}, srid: 4326},
|
||||
floor: "Myfloor",
|
||||
addressCountry: "My Country",
|
||||
addressLocality: "My Locality",
|
||||
|
15
test/support/mocks/geospatial_mock.ex
Normal file
15
test/support/mocks/geospatial_mock.ex
Normal file
@ -0,0 +1,15 @@
|
||||
defmodule 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: []
|
||||
|
||||
@impl Provider
|
||||
def search(_q, _options \\ []), do: [%Address{description: "10 rue Jangot, Lyon"}]
|
||||
end
|
Loading…
Reference in New Issue
Block a user