Skip to content

Commit

Permalink
_
Browse files Browse the repository at this point in the history
  • Loading branch information
leifmetcalf committed Nov 25, 2024
1 parent 7ca0e7c commit ed0341c
Show file tree
Hide file tree
Showing 21 changed files with 275 additions and 180 deletions.
6 changes: 6 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ config :logger, :console,
# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason

# Use Oban for background jobs
config :munch, Oban,
engine: Oban.Engines.Basic,
queues: [default: 10],
repo: Munch.Repo

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"
2 changes: 2 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ config :phoenix, :plug_init_mode, :runtime
# Enable helpful, but potentially expensive runtime checks
config :phoenix_live_view,
enable_expensive_runtime_checks: true

config :munch, Oban, testing: :inline
1 change: 1 addition & 0 deletions lib/munch/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule Munch.Application do
children = [
MunchWeb.Telemetry,
Munch.Repo,
{Oban, Application.get_env(:munch, Oban)},
{DNSCluster, query: Application.get_env(:munch, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: Munch.PubSub},
# Start a worker by calling: Munch.Worker.start_link(arg)
Expand Down
11 changes: 11 additions & 0 deletions lib/munch/importer.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule Munch.Importer do
use Oban.Worker, queue: :fresh_restaurants

alias Munch.Restaurants

@impl Oban.Worker
def perform(%Oban.Job{args: %{"osm_type" => osm_type, "osm_id" => osm_id}}) do
{:ok, restaurant} = Munch.Osm.nominatim_get_details(osm_type, osm_id)
Restaurants.create_restaurant(restaurant)
end
end
171 changes: 130 additions & 41 deletions lib/munch/osm.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,60 +7,149 @@ defmodule Munch.Osm do
alias Munch.Repo
alias Munch.Restaurants

def api_get_object(osm_type, osm_id) do
resp = Req.get!("https://api.openstreetmap.org/api/0.6/#{osm_type}/#{osm_id}.json")
def osm_get!(path) do
Req.get!(
url: path,
base_url: "https://api.openstreetmap.org/api/0.6",
headers: [user_agent: "Munch/0.1"]
)
end

def nominatim_get!(path) do
Req.get!(
url: path,
base_url: "https://nominatim.openstreetmap.org",
headers: [user_agent: "Munch/0.1"]
)
end

decoded = Jason.decode!(resp.body)
def osm_get_object(:node, osm_id) do
resp = osm_get!("node/#{osm_id}.json")

case decoded do
case resp.body do
%{"elements" => [%{"lat" => lattitude, "lon" => longitude, "tags" => tags}]} ->
%{lattitude: lattitude, longitude: longitude, tags: tags}
{:ok, %{lattitude: lattitude, longitude: longitude, tags: tags}}

_ ->
raise "Invalid response from OpenStreetMaps API: #{decoded}"
{:error, "Invalid response from OpenStreetMaps API: #{resp}"}
end
end

def pull_restaurant(osm_type, osm_id) do
object = api_get_object(osm_type, osm_id)
%{"name" => name} = object.tags

Repo.insert!(%Restaurants.Restaurant{
osm_type: osm_type,
osm_id: osm_id,
name: name,
location: %Geo.Point{coordinates: {object.longitude, object.lattitude}, srid: 4326},
note: "",
iso_country_subdivision: "",
secondary_subdivision: ""
})
def osm_get_object(:way, _osm_id) do
{:error, "Importing ways is not implemented yet"}
end

def osm_get_object(:relation, _osm_id) do
{:error, "Importing relations is not implemented yet"}
end

@doc """
Import a restaurant and queue a task to pull its details from Nominatim.
"""
def import_fresh_restaurant(osm_type, osm_id) do
with {:ok, object} <- osm_get_object(osm_type, osm_id),
{:ok, name} <- Map.fetch(object.tags, "name") do
point = %Geo.Point{coordinates: {object.longitude, object.lattitude}, srid: 4326}

Oban.insert(
Munch.Importer.new(%{
"osm_type" => osm_type,
"osm_id" => osm_id
})
)

Repo.insert!(%Restaurants.Restaurant{
osm_type: osm_type,
osm_id: osm_id,
name: name,
location: point,
note: "",
country: "",
iso_country_subdivision: ""
})
end
end

def copy_osm_restaurants() do
osm_restaurants = Repo.all(Munch.Osm.Restaurant)

restaurants =
osm_restaurants
|> Enum.map(fn osm_restaurant ->
%Restaurants.Restaurant{
osm_type: osm_restaurant.osm_type,
osm_id: osm_restaurant.osm_id,
name: osm_restaurant.tags["name"],
location: osm_restaurant.location,
note: "",
iso_country_subdivision: nil,
secondary_subdivision: nil
def nominatim_get_details(osm_type, osm_id) do
osm_type_char =
case osm_type do
:node -> "N"
:way -> "W"
:relation -> "R"
end

resp = nominatim_get!("lookup?osm_ids=#{osm_type_char}#{osm_id}&format=jsonv2")

case resp.body do
[
%{
"lat" => latitude,
"lon" => longitude,
"name" => name,
"display_name" => display_name,
"address" =>
%{
"ISO3166-2-lvl4" => iso_country_subdivision,
"country" => country
} = address
}
end)
|> IO.inspect()
] ->
{:ok,
%{
osm_type: osm_type,
osm_id: osm_id,
name: name,
display_name: display_name,
location: %Geo.Point{
coordinates: {String.to_float(longitude), String.to_float(latitude)},
srid: 4326
},
note: "",
country: country,
iso_country_subdivision: iso_country_subdivision,
address: address
}}

restaurants
|> Enum.map(fn restaurant ->
Repo.insert!(restaurant)
end)
_ ->
{:error, "Invalid response from Nominatim API: #{resp}"}
end
end

def sync_restaurant(_restaurant) do
IO.inspect("TODO: Implement sync with OSM")
@doc """
Search for restaurants with Nominatim.
options:
* `:exclude` - A list of place IDs to exclude from the results.
"""
def search_restaurants(search, opts \\ []) do
exclude = Keyword.get(opts, :exclude, [])
# TODO: use AI to assemble a structured query from the search string
resp =
nominatim_get!(
"search?q=#{search}&layer=poi&exclude=#{exclude |> Enum.join(",")}&format=jsonv2"
)

resp.body
|> Map.new(fn x ->
case x do
%{
"place_id" => place_id,
"lat" => lattitude,
"lon" => longitude,
"display_name" => display_name,
"osm_type" => osm_type,
"osm_id" => osm_id
} ->
{place_id,
%{
lattitude: lattitude,
longitude: longitude,
display_name: display_name,
osm_type: String.to_atom(osm_type),
osm_id: osm_id
}}
end
end)
end
end
12 changes: 0 additions & 12 deletions lib/munch/osm/restaurant.ex

This file was deleted.

8 changes: 1 addition & 7 deletions lib/munch/restaurants.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,7 @@ defmodule Munch.Restaurants do
end

@doc """
Search restaurants by name or address. Words are aggregated with AND.
## Examples
iex> search_restaurants("Ma")
[%Restaurant{}, ...]
Search local restaurants by name or address. Words are aggregated with AND.
"""
def search_restaurants(search) do
words = String.split(search)
Expand Down
16 changes: 14 additions & 2 deletions lib/munch/restaurants/restaurant.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,29 @@ defmodule Munch.Restaurants.Restaurant do
field :osm_type, Ecto.Enum, values: [node: "N", way: "W", relation: "R"]
field :osm_id, :integer
field :name, :string
field :display_name, :string
field :location, Geo.PostGIS.Geometry
field :note, :string
field :country, :string
field :iso_country_subdivision, :string
field :secondary_subdivision, :string
field :address, :map

timestamps(type: :utc_datetime)
end

@doc false
def changeset(restaurant, attrs) do
restaurant
|> cast(attrs, [:note])
|> cast(attrs, [
:osm_type,
:osm_id,
:name,
:display_name,
:location,
:note,
:country,
:iso_country_subdivision,
:address
])
end
end
6 changes: 5 additions & 1 deletion lib/munch_web/live/restaurant_live/edit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ defmodule MunchWeb.RestaurantLive.Edit do
end

def handle_event("trigger_sync", _params, socket) do
Osm.sync_restaurant(socket.assigns.restaurant)
Osm.import_fresh_restaurant(
socket.assigns.restaurant.osm_type,
socket.assigns.restaurant.osm_id
)

{:noreply, socket}
end

Expand Down
54 changes: 54 additions & 0 deletions lib/munch_web/live/restaurant_live/import.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
defmodule MunchWeb.RestaurantLive.Import do
use MunchWeb, :live_view

alias Munch.Osm
alias Munch.Restaurants

@impl true
def render(assigns) do
~H"""
<.header>
Add a new restaurant by search
</.header>
<.simple_form for={@form} id="search-new-form" phx-submit="search">
<.input field={@form[:search]} type="text" label="Search" />
<:actions>
<.button phx-disable-with="Searching...">Search</.button>
</:actions>
</.simple_form>
<ul :if={@results} class="mt-4 divide-y divide-zinc-300">
<li :for={{place_id, result} <- @results}>
<button
phx-click={JS.push("import", value: %{place_id: place_id})}
phx-disable-with="Importing..."
>
<%= result.display_name %>
</button>
</li>
<li :if={@results == []}>
<a href={~p"/restaurants/new-by-id"} target="_blank">
Add by osm_type / osm_id
</a>
</li>
</ul>
"""
end

@impl true
def mount(_params, _session, socket) do
{:ok, socket |> assign(form: to_form(%{})) |> assign(results: %{})}
end

@impl true
def handle_event("search", %{"search" => search}, socket) do
{:noreply, socket |> assign(results: Osm.search_restaurants(search))}
end

def handle_event("import", %{"place_id" => place_id}, socket) do
place = socket.assigns.results[place_id]
{:ok, restaurant} = Osm.nominatim_get_details(place.osm_type, place.osm_id)
Restaurants.create_restaurant(restaurant)
{:noreply, socket}
end
end
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule MunchWeb.RestaurantLive.New do
defmodule MunchWeb.RestaurantLive.ImportManual do
use MunchWeb, :live_view

alias Munch.Restaurants
Expand All @@ -13,9 +13,8 @@ defmodule MunchWeb.RestaurantLive.New do
</.header>
<p>
New restaurants and other changes to the OpenStreetMap map should be
imported automatically every 24 hours. You can import a restaurant
ahead of schedule by entering its osm_type and osm_id below.
If you can't find the restaurant by search, you can import a restaurant
by entering its osm_type and osm_id below.
</p>
<.simple_form for={@form} id="osm-form" phx-submit="save">
<.input
Expand Down Expand Up @@ -52,7 +51,7 @@ defmodule MunchWeb.RestaurantLive.New do

@impl true
def handle_event("save", %{"osm_type" => osm_type, "osm_id" => osm_id}, socket) do
case Osm.pull_restaurant(osm_type, osm_id) do
case Osm.import_fresh_restaurant(osm_type, osm_id) do
{:ok, restaurant} ->
{:noreply,
socket
Expand Down
4 changes: 3 additions & 1 deletion lib/munch_web/live/restaurant_live/index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ defmodule MunchWeb.RestaurantLive.Index do
row_click={fn {_id, restaurant} -> JS.navigate(~p"/restaurant/#{restaurant}") end}
>
<:col :let={{_id, restaurant}} label="Name"><%= restaurant.name %></:col>
<:col :let={{_id, restaurant}} label="Address"><%= restaurant.address %></:col>
<:col :let={{_id, restaurant}} label="Address">
<%= restaurant.osm_type %> <%= restaurant.osm_id %>
</:col>
<:action :let={{_id, restaurant}}>
<div class="sr-only">
<.link navigate={~p"/restaurant/#{restaurant}"}>Show</.link>
Expand Down
Loading

0 comments on commit ed0341c

Please sign in to comment.