diff --git a/lib/mobilizon/addresses/addresses.ex b/lib/mobilizon/addresses/addresses.ex index 891167a31..78607311f 100644 --- a/lib/mobilizon/addresses/addresses.ex +++ b/lib/mobilizon/addresses/addresses.ex @@ -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 diff --git a/lib/mobilizon_web/context.ex b/lib/mobilizon_web/context.ex index c9abf01d0..be5834d04 100644 --- a/lib/mobilizon_web/context.ex +++ b/lib/mobilizon_web/context.ex @@ -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 diff --git a/lib/mobilizon_web/resolvers/address.ex b/lib/mobilizon_web/resolvers/address.ex new file mode 100644 index 000000000..df2493659 --- /dev/null +++ b/lib/mobilizon_web/resolvers/address.ex @@ -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 diff --git a/lib/mobilizon_web/schema.ex b/lib/mobilizon_web/schema.ex index 9d140130f..0373b80ce 100644 --- a/lib/mobilizon_web/schema.ex +++ b/lib/mobilizon_web/schema.ex @@ -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 """ diff --git a/lib/mobilizon_web/schema/address.ex b/lib/mobilizon_web/schema/address.ex index d3b560a16..577ead164 100644 --- a/lib/mobilizon_web/schema/address.ex +++ b/lib/mobilizon_web/schema/address.ex @@ -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 diff --git a/test/mobilizon_web/resolvers/address_resolver_test.exs b/test/mobilizon_web/resolvers/address_resolver_test.exs new file mode 100644 index 000000000..dfaf79cca --- /dev/null +++ b/test/mobilizon_web/resolvers/address_resolver_test.exs @@ -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 diff --git a/test/support/factory.ex b/test/support/factory.ex index 3584b3d5b..ba31afd4b 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -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", diff --git a/test/support/mocks/geospatial_mock.ex b/test/support/mocks/geospatial_mock.ex index 8fcf74505..f5a3de8b4 100644 --- a/test/support/mocks/geospatial_mock.ex +++ b/test/support/mocks/geospatial_mock.ex @@ -1,4 +1,4 @@ -defmodule Mobilizon.Mobilizon.Service.Geospatial.Mock do +defmodule Mobilizon.Service.Geospatial.Mock do @moduledoc """ Mock for Geospatial Provider implementations """ @@ -8,8 +8,8 @@ defmodule Mobilizon.Mobilizon.Service.Geospatial.Mock do @behaviour Provider @impl Provider - def geocode(_lon, _lat, _options \\ []), do: [%Address{}] + def geocode(_lon, _lat, _options \\ []), do: [] @impl Provider - def search(_q, _options \\ []), do: [%Address{}] + def search(_q, _options \\ []), do: [%Address{description: "10 rue Jangot, Lyon"}] end