M assets/css/app.css => assets/css/app.css +3 -0
@@ 24,6 24,8 @@
section + section { @apply mt-4 }
ul { @apply list-disc ml-4 }
+ summary { cursor: pointer; text-decoration: underline }
+ details[open] > summary { margin-bottom: 4px }
}
@layer components {
@@ 44,6 46,7 @@
}
.button:hover { background-color: Gainsboro }
.button[disabled] { cursor: not-allowed }
+ .button.small { @apply h-6 px-2 }
.box {
border: 1px solid black;
A lib/triglav_web/components/poi.ex => lib/triglav_web/components/poi.ex +118 -0
@@ 0,0 1,118 @@
+defmodule TriglavWeb.Components.Poi do
+ use Phoenix.Component
+
+ def tag_diff(assigns) do
+ poi_node = assigns.node
+ osm_element = if poi_node.mapping, do: poi_node.mapping.osm_node || poi_node.mapping.osm_way
+
+ if osm_element do
+ diffs = calc_diffs(assigns.node, osm_element)
+
+ summary =
+ diffs
+ |> Enum.frequencies_by(fn {_, _, _, cmd} -> cmd end)
+ |> Map.drop([:delete])
+ |> Enum.map(fn {cmd, count} ->
+ case cmd do
+ :insert -> "#{count} new"
+ :diff -> "#{count} different"
+ :same -> "#{count} same"
+ _ -> nil
+ end
+ end)
+ |> Enum.join(", ")
+
+ ~H"""
+ <details>
+ <summary><%= summary %></summary>
+
+ <table class="border border-gray-300">
+ <thead>
+ <tr>
+ <th>Key</th>
+ <th>Source value</th>
+ <th>OSM value</th>
+ </tr>
+ </thead>
+ <tbody>
+ <%= for {key, poi_value, osm_value, cmd} <- diffs do %>
+ <tr>
+ <td><%= key %></td>
+ <%= if cmd == :same do %>
+ <td class="text-gray-600"><%= poi_value %></td>
+ <td class="text-gray-600"><%= osm_value %></td>
+ <td></td>
+ <% end %>
+ <%= if cmd == :diff do %>
+ <td class="text-red-900"><%= poi_value %></td>
+ <td class="text-red-900"><%= osm_value %></td>
+ <td></td>
+ <% end %>
+ <%= if cmd == :insert do %>
+ <td class="text-green-900"><%= poi_value %></td>
+ <td><%= osm_value %></td>
+ <td></td>
+ <% end %>
+ <%= if cmd == :delete do %>
+ <td><%= poi_value %></td>
+ <td class="text-gray-600"><%= osm_value %></td>
+ <td></td>
+ <% end %>
+ </tr>
+ <% end %>
+ </tbody>
+ </table>
+ </details>
+
+ """
+ else
+ ~H"""
+ <details>
+ <summary>Show tags</summary>
+ <table>
+ <thead>
+ <tr>
+ <th>Key</th>
+ <th>Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ <%= for {key, value} <- poi_node.tags do %>
+ <tr>
+ <td><%= key %></td>
+ <td><%= value %></td>
+ </tr>
+ <% end %>
+ </tbody>
+ </table>
+ </details>
+ """
+ end
+ end
+
+ def calc_diffs(poi_node, osm_element) do
+ poi_tags = poi_node.tags
+ osm_tags = osm_element.tags
+
+ keys =
+ Map.keys(poi_tags)
+ |> Enum.concat(Map.keys(osm_tags))
+ |> Enum.uniq()
+ |> Enum.sort()
+
+ for key <- keys do
+ poi_value = Map.get(poi_tags, key)
+ osm_value = Map.get(osm_tags, key)
+
+ cmd =
+ cond do
+ is_nil(osm_value) -> :insert
+ is_nil(poi_value) -> :delete
+ poi_value == osm_value -> :same
+ true -> :diff
+ end
+
+ {key, poi_value, osm_value, cmd}
+ end
+ end
+end
A lib/triglav_web/controllers/poi_controller.ex => lib/triglav_web/controllers/poi_controller.ex +21 -0
@@ 0,0 1,21 @@
+defmodule TriglavWeb.PoiController do
+ use TriglavWeb, :controller
+
+ action_fallback TriglavWeb.FallbackController
+
+ alias Triglav.Repo
+ alias Triglav.Poi
+ alias Triglav.Poi.Source
+
+ def index(conn, _params) do
+ sources = Triglav.Poi.Sources.all()
+ render(conn, "index.html", sources: sources)
+ end
+
+ def nodes(conn, %{"source" => source}) do
+ with {:ok, source} <- Repo.fetch_by(Source, slug: source) do
+ nodes = Poi.nodes(source) |> Repo.preload(mapping: [:osm_node, :osm_way])
+ render(conn, "nodes.html", source: source, nodes: nodes)
+ end
+ end
+end
M lib/triglav_web/router.ex => lib/triglav_web/router.ex +3 -0
@@ 26,6 26,9 @@ defmodule TriglavWeb.Router do
get "/public_transport/:operator/routes/:ref/match/:relation_id", PublicTransportController, :match
get "/public_transport/:operator/stops", PublicTransportController, :stops
+ get "/poi/", PoiController, :index
+ get "/poi/:source", PoiController, :nodes
+
scope "/hps", alias: Hps, as: :hps do
get "/tracks", TracksController, :index
get "/tracks/:id", TracksController, :detail
M lib/triglav_web/templates/layout/app.html.heex => lib/triglav_web/templates/layout/app.html.heex +5 -0
@@ 20,6 20,11 @@
<% else %>
<a class="text-base text-white px-5 py-5" href="/public_transport">Public Transport</a>
<% end %>
+ <%= if active?(@conn, "/poi") do %>
+ <a class="text-base text-white px-5 py-5 font-bold bg-purple" href="/poi">POI</a>
+ <% else %>
+ <a class="text-base text-white px-5 py-5" href="/poi">POI</a>
+ <% end %>
</div>
</nav>
</header>
A lib/triglav_web/templates/poi/index.html.heex => lib/triglav_web/templates/poi/index.html.heex +34 -0
@@ 0,0 1,34 @@
+<main role="main" class="container">
+ <h1>Points of Interest</h1>
+
+ <%= if length(@sources) > 0 do %>
+ <table>
+ <tbody>
+ <tr>
+ <th>Source</th>
+ <th class="text-right">POIs</th>
+ <th class="text-right">Mapped</th>
+ <th class="text-right">Mapped %</th>
+ <th class="text-right">Last Updated</th>
+ </tr>
+ <%= for source <- @sources do %>
+ <tr>
+ <td><a href={Routes.poi_path(@conn, :nodes, source.slug)}><%= source.name %></a></td>
+ <td class="text-right"><%= source.node_count %></td>
+ <td class="text-right"><%= source.mapping_count %></td>
+ <td class="text-right">
+ <%= if source.node_count > 0 do %>
+ <%= round(100 * source.mapping_count / source.node_count) %>%
+ <% else %>
+ 0%
+ <% end %>
+ </td>
+ <td><%= source.updated_at %></td>
+ </tr>
+ <% end %>
+ </tbody>
+ </table>
+ <% else %>
+ <p>No data loaded. Run an import script.</p>
+ <% end %>
+</main>
A lib/triglav_web/templates/poi/nodes.html.heex => lib/triglav_web/templates/poi/nodes.html.heex +65 -0
@@ 0,0 1,65 @@
+<main role="main" class="container-wide">
+ <h1><%= @source.name %> POI</h1>
+
+ <%= if length(@nodes) > 0 do %>
+ <div class="flex space-x-4 mb-4">
+ <button id="expand-all" class="button small">Expand all</button>
+ <button id="collapse-all" class="button small">Collapse all</button>
+ </div>
+
+ <table>
+ <tr>
+ <th>Ref</th>
+ <th>Name</th>
+ <th>OSM element</th>
+ <th>Distance</th>
+ <th>Tags</th>
+ <th>JOSM</th>
+ </tr>
+ <%= for node <- @nodes do %>
+ <% {lon, lat} = node.geometry.coordinates %>
+ <% osm_element = if node.mapping, do: node.mapping.osm_node || node.mapping.osm_way %>
+ <tr>
+ <td><%= node.ref %></td>
+ <td><%= node.name %></td>
+ <%= if node.mapping do %>
+ <td><%= osm_link(osm_element) %></td>
+ <td class="text-right"><%= round(node.mapping.distance) %>m</td>
+ <% else %>
+ <td colspan="2">
+ <%= josm_add_node(lat, lon, node.tags) %>
+ <%= josm_add_square_way(lat, lon, node.tags) %>
+ </td>
+ <% end %>
+ <td>
+ <.tag_diff node={node} />
+ </td>
+ <td class="whitespace-nowrap">
+ <%= josm_zoom(lat, lon) %>
+ <%= if osm_element do %>
+ <%= josm_load_objects([osm_element]) %>
+ <% end %>
+ </td>
+ </tr>
+ <% end %>
+ </table>
+ <% else %>
+ <p>No nodes loaded for this source.</p>
+ <% end %>
+</main>
+
+<script>
+document.getElementById("expand-all").addEventListener("click", () => {
+ const elements = document.getElementsByTagName("details")
+ for (const element of elements) {
+ element.setAttribute("open", true)
+ }
+})
+
+document.getElementById("collapse-all").addEventListener("click", () => {
+ const elements = document.getElementsByTagName("details")
+ for (const element of elements) {
+ element.removeAttribute("open")
+ }
+})
+</script><
\ No newline at end of file
A lib/triglav_web/views/poi_view.ex => lib/triglav_web/views/poi_view.ex +8 -0
@@ 0,0 1,8 @@
+defmodule TriglavWeb.PoiView do
+ use TriglavWeb, :view
+
+ import TriglavWeb.Components.Poi
+
+ def title("index.html", _), do: "POI"
+ def title("source.html", %{"source" => source}), do: "POI #{source}"
+end