~ihabunek/triglav

c80b8e9dbb575aabd0c8132c1b5b8f045235103a — Ivan Habunek 8 months ago 803c765
Rework fetching and matching stops

Various changes, biggest being that ways are considered as well as
nodes.
M assets/css/app.scss => assets/css/app.scss +4 -0
@@ 42,6 42,10 @@ table {
    vertical-align: top;
  }

  td.success {
    background-color: rgba($green, 0.2);
  }

  th {
    background-color: WhiteSmoke;
    text-align: left;

A assets/static/images/osm/node.svg => assets/static/images/osm/node.svg +2 -0
@@ 0,0 1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" version="1.0" height="256" width="256"><title>OpenStreetMap node element icon</title><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title>OpenStreetMap node element icon</dc:title><cc:license rdf:resource="http://creativecommons.org/licenses/by/3.0/"/><dc:date>2014-03-10</dc:date><dc:creator><cc:Agent><dc:title>https://wiki.openstreetmap.org/wiki/User:Moresby</dc:title></cc:Agent></dc:creator></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/by/3.0/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/><cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/><cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/></cc:License></rdf:RDF></metadata><g><rect width="242" height="242" stroke="none" fill="white" ry="32" x="7" y="7"/><circle cx="128" cy="128" r="024" fill="#bee6be" stroke="black" stroke-width="10"/><rect width="242" height="242" stroke="black" fill="none" stroke-width="12" ry="32" x="7" y="7"/></g></svg>
\ No newline at end of file

A assets/static/images/osm/way.svg => assets/static/images/osm/way.svg +2 -0
@@ 0,0 1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" version="1.0" height="256" width="256"><title>OpenStreetMap way element icon</title><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title>OpenStreetMap way element icon</dc:title><cc:license rdf:resource="http://creativecommons.org/licenses/by/3.0/"/><dc:date>2014-03-10</dc:date><dc:creator><cc:Agent><dc:title>https://wiki.openstreetmap.org/wiki/User:Moresby</dc:title></cc:Agent></dc:creator></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/by/3.0/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/><cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/><cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/></cc:License></rdf:RDF></metadata><g><rect width="242" height="242" stroke="none" fill="white" ry="32" x="7" y="7"/><path stroke="#ccc" fill="none" stroke-width="16" d="M 169 058 L 057 145 L 195 199"/><g><circle cx="169" cy="058" r="024" fill="black"/><circle cx="057" cy="145" r="024" fill="black"/><circle cx="195" cy="199" r="024" fill="black"/></g><rect width="242" height="242" stroke="black" fill="none" stroke-width="12" ry="32" x="7" y="7"/></g></svg>
\ No newline at end of file

M lib/triglav/josm.ex => lib/triglav/josm.ex +12 -0
@@ 79,4 79,16 @@ defmodule Triglav.Josm do
      right: lon
    }
  end

  defp bounds(%Way{} = way) do
    # TODO: this only considers the first point
    {lon, lat} = List.first(way.linestring.coordinates)

    %{
      top: lat,
      bottom: lat,
      left: lon,
      right: lon
    }
  end
end

M lib/triglav/schemas/osmosis/relation_member.ex => lib/triglav/schemas/osmosis/relation_member.ex +7 -1
@@ 1,6 1,8 @@
defmodule Triglav.Schemas.Osmosis.RelationMember do
  use Ecto.Schema
  alias Triglav.Schemas.Osmosis.Node
  alias Triglav.Schemas.Osmosis.Relation
  alias Triglav.Schemas.Osmosis.Way

  @primary_key false
  @schema_prefix :osmosis


@@ 12,7 14,8 @@ defmodule Triglav.Schemas.Osmosis.RelationMember do
          sequence_id: Integer.t(),
          member_id: Integer.t(),
          member_type: String.t(),
          member_role: String.t()
          member_role: String.t(),
          member: Node.t() | Way.t() | Relation.t()
        }

  schema "relation_members" do


@@ 21,5 24,8 @@ defmodule Triglav.Schemas.Osmosis.RelationMember do
    field :member_id, :integer
    field :member_type, :string
    field :member_role, :string

    # Used to populate the member record (node, relation, or way)
    field :member, :map, virtual: true
  end
end

M lib/triglav/zet/gtfs.ex => lib/triglav/zet/gtfs.ex +17 -6
@@ 32,6 32,18 @@ defmodule Triglav.Zet.Gtfs do
    |> Repo.preload(:relation)
  end

  @spec list_gtfs_stop_ids() :: [Zet.DistinctTrip.t()]
  def list_gtfs_stop_ids() do
    from(t in Zet.DistinctTrip)
    |> Repo.all()
  end

  @spec list_gtfs_stop_ids(Zet.Trip.id()) :: [Zet.DistinctTrip.t()]
  def list_gtfs_stop_ids(trip_id) do
    from(t in Zet.DistinctTrip, where: t.trip_id == ^trip_id)
    |> Repo.all()
  end

  def fetch_distinct_trips(%Zet.Route{} = route) do
    Repo.select!(
      """


@@ 74,18 86,17 @@ defmodule Triglav.Zet.Gtfs do
    |> Repo.all()
  end

  @spec find_closest_stop(Geo.Point.t()) :: Zet.Stop.t() | nil
  def find_closest_stop(%Geo.Point{} = point) do
    # TODO: speed up by searching only within a given radius around the point
  @spec find_closest_stop(Geo.Point.t() | Geo.LineString.t()) :: Zet.Stop.t() | nil
  def find_closest_stop(geom) do
    # TODO: speed up this query or replace with a materialized view
    # see: https://www.postgis.net/workshops/postgis-intro/knn.html

    # Casting to `geography` makes distance be returned in meters rather than degrees

    from(s in Zet.Stop,
      select:
        {s, st_distance(fragment("?::geography", s.geom), fragment("?::geography", ^point))},
      select: {s, st_distance(fragment("?::geography", s.geom), fragment("?::geography", ^geom))},
      order_by: [
        asc: st_distance(fragment("?::geography", s.geom), fragment("?::geography", ^point))
        asc: st_distance(fragment("?::geography", s.geom), fragment("?::geography", ^geom))
      ],
      limit: 1
    )

M lib/triglav/zet/osmosis.ex => lib/triglav/zet/osmosis.ex +74 -3
@@ 5,6 5,7 @@ defmodule Triglav.Zet.Osmosis do
  alias Triglav.Repo
  alias Triglav.Schemas.Osmosis.Node
  alias Triglav.Schemas.Osmosis.Relation
  alias Triglav.Schemas.Osmosis.RelationMember
  alias Triglav.Schemas.Osmosis.ReplicationChanges
  alias Triglav.Schemas.Osmosis.Way



@@ 54,6 55,63 @@ defmodule Triglav.Zet.Osmosis do
    end
  end

  @type stop_id :: String.t()

  @spec list_gtfs_stop_ids(Relation.id() | [Relation.id()]) :: %{Relation.id() => [stop_id]}

  def list_gtfs_stop_ids(relation_id) when is_integer(relation_id) do
    list_gtfs_stop_ids([relation_id])
    |> Map.get(relation_id)
  end

  def list_gtfs_stop_ids(relation_ids) do
    Repo.query!(
      """
        SELECT rm.relation_id,
               array_agg(
                 CASE
                   WHEN rm.member_type = 'W' THEN w.tags->'gtfs:stop_id'
                   WHEN rm.member_type = 'N' THEN n.tags->'gtfs:stop_id'
                 END
                 ORDER BY sequence_id
               ) as gtfs_stop_ids
        FROM osmosis.relation_members rm
        LEFT JOIN osmosis.ways w on rm.member_type = 'W' and w.id = rm.member_id
        LEFT JOIN osmosis.nodes n on rm.member_type = 'N' and n.id = rm.member_id
        WHERE rm.relation_id = ANY($1)
        AND rm.member_role ilike 'platform%'
        GROUP BY rm.relation_id
        ORDER BY rm.relation_id
      """,
      [relation_ids]
    )
    |> Map.get(:rows)
    |> Map.new(&List.to_tuple/1)
  end

  @doc """
  For a given relation id or list of ids returns a list of relation members with
  a platform role.
  """
  @spec list_platform_members(Relation.id() | [Relation.id()]) :: [RelationMember.t()]

  def list_platform_members(relation_id) when is_integer(relation_id),
    do: list_platform_members([relation_id])

  def list_platform_members(relation_ids) when is_list(relation_ids) do
    from(m in RelationMember,
      left_join: n in Node,
      on: m.member_type == "N" and m.member_id == n.id,
      left_join: w in Way,
      on: m.member_type == "W" and m.member_id == w.id,
      where: m.relation_id in ^relation_ids and ilike(m.member_role, "platform%"),
      select: {m, n, w},
      order_by: m.sequence_id
    )
    |> Repo.all()
    |> Enum.map(fn {member, node, way} -> %{member | member: node || way} end)
  end

  @doc """
  Returns a list of all nodes with platform* roles which are members of any of
  the given relations.


@@ 66,11 124,24 @@ defmodule Triglav.Zet.Osmosis do
    |> Repo.all()
  end

  defp platform_node_ids(relations) do
  @spec list_platform_ways([Relation.t()]) :: [Way.t()]
  def list_platform_ways(relations) do
    ids = platform_way_ids(relations)

    from(w in Way, where: w.id in ^ids, order_by: [asc: fragment("tags->?", "name")])
    |> Repo.all()
  end

  defp platform_node_ids(relations), do: platform_member_ids(relations, "N")
  defp platform_way_ids(relations), do: platform_member_ids(relations, "W")

  defp platform_member_ids(relations, member_type) do
    relations
    |> Enum.map(fn r ->
      r.members
      |> Enum.filter(&(&1.member_type == "N" and String.starts_with?(&1.member_role, "platform")))
      |> Enum.filter(
        &(&1.member_type == member_type and String.starts_with?(&1.member_role, "platform"))
      )
      |> Enum.map(& &1.member_id)
    end)
    |> List.flatten()


@@ 80,7 151,7 @@ defmodule Triglav.Zet.Osmosis do
    from(r in Relation,
      where:
        fragment("tags->?", "type") in ["route", "route_master"] and
          fragment("lower(tags->?)", "network") == "zet"
          fragment("tags->?", "network") == "ZET"
    )
  end


M lib/triglav_web/controllers/zet/routes_controller.ex => lib/triglav_web/controllers/zet/routes_controller.ex +48 -40
@@ 12,13 12,6 @@ defmodule TriglavWeb.Zet.RoutesController do
    osm_info = DataImport.load_state()
    gtfs_info = Gtfs.get_feed_info()

    annotated_routes =
      for route <- routes do
        route_relations = route_relations(route, relations)
        hierarchy = make_hierarchy(route_relations)
        {route, hierarchy}
      end

    # Relations which were not matched to any known route
    route_ids = Enum.map(routes, & &1.id)



@@ 27,24 20,63 @@ defmodule TriglavWeb.Zet.RoutesController do
      |> sort_by_ref()

    render(conn, "index.html",
      routes: annotated_routes,
      routes: annotate_routes(routes, relations),
      unmached_relations: unmached_relations,
      osm_info: osm_info,
      gtfs_info: gtfs_info
    )
  end

  defp annotate_routes(routes, relations) do
    osm_stop_ids = relations |> Enum.map(& &1.id) |> Osmosis.list_gtfs_stop_ids()
    zet_stop_ids = Gtfs.list_gtfs_stop_ids() |> Enum.group_by(& &1.route_id)

    for route <- routes do
      route_relations =
        relations
        |> Enum.filter(&Relation.is_route/1)
        |> Enum.filter(&Relation.has_tag?(&1, "ref", route.id))

      osm_variants =
        route_relations
        |> Enum.map(&Map.get(osm_stop_ids, &1.id))
        |> Enum.filter(& &1)
        |> MapSet.new()

      zet_variants =
        zet_stop_ids
        |> Map.get(route.id)
        |> MapSet.new(&Map.get(&1, :stops))

      counts = %{
        total: Enum.count(zet_variants),
        correct: MapSet.intersection(osm_variants, zet_variants) |> Enum.count(),
        incorrect: MapSet.difference(osm_variants, zet_variants) |> Enum.count()
      }

      hierarchy = make_hierarchy(route_relations)
      {route, hierarchy, counts}
    end
  end

  def detail(conn, %{"id" => id}) do
    route = Gtfs.get_route(id)
    relations = Osmosis.list_public_transport_relations(ref: id, members: true)
    hierarchy = make_hierarchy(relations)
    platforms = Osmosis.list_platform_nodes(relations) |> Map.new(&{&1.id, &1})
    {errors, rel_errors} = Gtfs.get_errors(id) |> group_errors_by_relation()

    relations_by_stop_ids =
      for relation <- relations, Relation.is_route(relation), into: %{} do
        {Osmosis.list_gtfs_stop_ids(relation.id), relation}
      end

    trips =
      Gtfs.fetch_distinct_trips(route)
      |> match_trips_with_relations(relations, platforms)
      |> Enum.group_by(fn {trip, _} -> trip["direction_id"] end)
      for trip <- Gtfs.fetch_distinct_trips(route) do
        stop_ids = Enum.map(trip["stops"], &elem(&1, 0))
        {trip, Map.get(relations_by_stop_ids, stop_ids)}
      end

    grouped_trips = Enum.group_by(trips, fn {trip, _} -> trip["direction_id"] end)

    # JSON encode stops for drawing on the map
    stops_json =


@@ 53,7 85,9 @@ defmodule TriglavWeb.Zet.RoutesController do
      |> Enum.map(&Map.take(&1, [:id, :name, :lat, :lon]))
      |> Jason.encode!()

    ways_geojson = ways_geojson_feature_collection(relations) |> Jason.encode!(pretty: true)
    ways_geojson =
      ways_geojson_feature_collection(relations)
      |> Jason.encode!()

    render(conn, "detail.html",
      conn: conn,


@@ 62,7 96,7 @@ defmodule TriglavWeb.Zet.RoutesController do
      hierarchy: hierarchy,
      errors: errors,
      rel_errors: rel_errors,
      trips: trips,
      trips: grouped_trips,
      stops_json: stops_json,
      ways_geojson: ways_geojson
    )


@@ 93,28 127,6 @@ defmodule TriglavWeb.Zet.RoutesController do
    }
  end

  defp match_trips_with_relations(trips, relations, platforms) do
    route_relations = Enum.filter(relations, &Relation.is_route/1)

    relations_stop_ids =
      for relation <- route_relations do
        relation.members
        |> Enum.filter(
          &(String.starts_with?(&1.member_role, "platform") and &1.member_type == :node)
        )
        |> Enum.map(&Map.get(platforms, &1.member_id))
        |> Enum.map(&Map.get(&1.tags, "gtfs:stop_id"))
      end

    for trip <- trips do
      stop_ids = Enum.map(trip["stops"], &elem(&1, 0))
      index = Enum.find_index(relations_stop_ids, &(&1 == stop_ids))
      matched_relation = if index, do: Enum.at(route_relations, index)

      {trip, matched_relation}
    end
  end

  def group_errors_by_relation(errors) do
    {errors, rel_errors} = Enum.split_with(errors, &is_nil(&1.relation_id))



@@ 152,10 164,6 @@ defmodule TriglavWeb.Zet.RoutesController do
    Enum.concat(route_masters, remaining_routes)
  end

  defp route_relations(route, all_relations) do
    Enum.filter(all_relations, fn r -> r.tags["ref"] == route.id end)
  end

  defp sort_by_ref(relations) do
    Enum.sort_by(relations, fn rel ->
      if is_nil(rel.tags["ref"]) do

M lib/triglav_web/controllers/zet/stops_controller.ex => lib/triglav_web/controllers/zet/stops_controller.ex +19 -8
@@ 1,25 1,36 @@
defmodule TriglavWeb.Zet.StopsController do
  use TriglavWeb, :controller

  alias Triglav.Schemas.Osmosis.Node
  alias Triglav.Schemas.Osmosis.Relation
  alias Triglav.Schemas.Osmosis.Way
  alias Triglav.Zet.Gtfs
  alias Triglav.Zet.Osmosis

  def index(conn, _params) do
    relations = Osmosis.list_public_transport_relations(members: true)
    platforms = Osmosis.list_platform_nodes(relations)
    relations = Osmosis.list_public_transport_relations()
    relation_ids = Enum.map(relations, & &1.id)

    {matched_platforms, unmatched_platforms} =
      Enum.split_with(platforms, &Relation.get_tag(&1, "gtfs:stop_id"))
    unmatched_platforms =
      Osmosis.list_platform_members(relation_ids)
      |> Enum.map(& &1.member)
      # TODO: move filtering to database
      |> Enum.reject(&Relation.has_tag?(&1, "gtfs:stop_id"))

    IO.inspect(length(unmatched_platforms))

    annotated_unmatched_platforms =
      Enum.map(unmatched_platforms, fn node ->
        {stop, distance} = Gtfs.find_closest_stop(node.geom)
        {node, stop, distance}
      Enum.map(unmatched_platforms, fn member ->
        {stop, distance} =
          case member do
            %Node{} = node -> Gtfs.find_closest_stop(node.geom)
            %Way{} = way -> Gtfs.find_closest_stop(way.linestring)
          end

        {member, stop, distance}
      end)

    render(conn, "index.html",
      matched_platforms: matched_platforms,
      unmatched_platforms: unmatched_platforms,
      annotated_unmatched_platforms: annotated_unmatched_platforms
    )

M lib/triglav_web/templates/zet/routes/index.html.eex => lib/triglav_web/templates/zet/routes/index.html.eex +9 -1
@@ 70,11 70,12 @@
        <th>ID</th>
        <th>GTFS Route</th>
        <th>OSM Relations</th>
        <th>Route variants</th>
        <th>Errors</th>
      </tr>
    </thead>
    <tbody>
      <%= for {route, hierarchy} <- @routes do %>
      <%= for {route, hierarchy, counts} <- @routes do %>
      <tr>
        <td><%= route.id %></td>
        <td>


@@ 85,6 86,13 @@
        <td>
          <%= render_relation_hierarchy(hierarchy) %>
        </td>
        <td class="<%= if counts.total == counts.correct and counts.incorrect == 0, do: "success" %>">
          ZET: <%= counts.total %><br />
          Correct: <%= counts.correct %><br />
          <span class="<%= if counts.incorrect > 0, do: "red" %>">
            Incorrect: <%= counts.incorrect %>
          </span>
        </td>
        <td>
          <%= if !Enum.empty?(route.errors) do %>
          <ul>

M lib/triglav_web/templates/zet/stops/index.html.eex => lib/triglav_web/templates/zet/stops/index.html.eex +15 -8
@@ 7,38 7,45 @@

  <h2>Missing <code>gtfs:stop_id</code> tag</h2>

  <p>This view lists all nodes which are contained in public transport relations with [role=platform] but which don't have a gtfs:stop_id tag.</p>
  <p>This view lists nodes and ways contained in public transport relations with [role=platform] but which don't have a gtfs:stop_id tag.</p>

  <p>For each such stop, it shows the closest stop from ZET GTFS data along with a JOSM shortcut to add the corresponding stop_id to the OSM node.</p>
  <p>For each such stop, it shows the closest stop from ZET GTFS data along with a JOSM shortcut to add the corresponding <code>gtfs:stop_id</code> tag to the OSM node.</p>

  <%= josm_load_objects(@unmatched_platforms, title: "Open all in JOSM", relation_members: false) %>

  <table style="margin-top: 1rem; width: 100%;">
    <thead>
      <tr>
        <th colspan="2">OSM Node</th>
        <th colspan="2" class="bl">Closest GTFS Stop</th>
        <th colspan="3">OSM Node</th>
        <th colspan="3" class="bl">Closest GTFS Stop</th>
        <th rowspan="2" class="bl">Distance</th>
        <th rowspan="2" class="bl">JOSM Actions</th>
      </tr>
      <tr>
        <th>ID</th>
        <th>Name</th>
        <th>JOSM</th>
        <th class="bl">ID</th>
        <th>Name</th>
        <th>JOSM</th>
      </tr>
    </thead>
    <tbody>
      <%= for {node, closest_stop, distance} <- @annotated_unmatched_platforms do %>
      <%= for {member, closest_stop, distance} <- @annotated_unmatched_platforms do %>
        <tr>
          <td><%= osm_link(node, josm: true) %></td>
          <td><%= node.tags["name"] %></td>
          <td><%= osm_link(member) %></td>
          <td><%= member.tags["name"] %></td>
          <td><%= josm_load_objects([member], title: "Load") %></td>
          <td class="bl"><%= closest_stop.id %></td>
          <td><%= closest_stop.name %></td>
          <td>
            <% {lon, lat} = closest_stop.geom.coordinates %>
            <%= josm_zoom(lat, lon, title: "Zoom") %>
          </td>
          <td class="bl <%= if round(distance) > 500 do %>bg-red<% end %>">
            <%= round(distance) %>
          </td>
          <td class="bl"><%= josm_add_tags(node, %{"gtfs:stop_id": closest_stop.id}) %></td>
          <td class="bl"><%= josm_add_tags(member, %{"gtfs:stop_id": closest_stop.id}) %></td>
        </tr>
      <% end %>
    </tbody>

M lib/triglav_web/views/osm_helpers.ex => lib/triglav_web/views/osm_helpers.ex +43 -4
@@ 3,9 3,11 @@ defmodule TriglavWeb.OsmHelpers do
  Helpers for displaying OSM data.
  """
  use Phoenix.HTML
  alias Triglav.Zet.Errors
  alias Triglav.Schemas.Osmosis.Relation

  alias Triglav.Schemas.Osmosis.Node
  alias Triglav.Schemas.Osmosis.Relation
  alias Triglav.Schemas.Osmosis.Way
  alias Triglav.Zet.Errors
  alias TriglavWeb.Router.Helpers

  @doc """


@@ 83,6 85,41 @@ defmodule TriglavWeb.OsmHelpers do
    """
  end

  def osm_link(%Way{} = way, opts) do
    image_url = Helpers.static_path(TriglavWeb.Endpoint, "/images/osm/way.svg")
    target_url = "https://www.openstreetmap.org/way/#{way.id}"
    name? = Keyword.get(opts, :name)
    josm? = Keyword.get(opts, :josm)

    josm_link = Triglav.Josm.load_object(way)

    tags =
      opts
      |> Keyword.get(:tags, [])
      |> Enum.filter(&Way.has_tag?(way, &1))
      |> Enum.map(&{&1, Way.get_tag(way, &1)})

    ~E"""
    <span class="osm-link">
      <img src="<%= image_url %>" alt="way" />
      <a href="<%= target_url %>"><%= way.id %></a>
      <%= if name? do %>
        <%= way.tags["name"] %>
      <% end %>
      <%= if !Enum.empty?(tags) do %>
        <span class="tag smaller">
          <%= for {k, v} <- tags do %>[<%= k %>=<%= v %>]<% end %>
        </span>
      <% end %>
      <%= if josm? do %>
        <a class="josm-remote" href="<%= josm_link %>" title="Open way in JOSM" target="_blank">
          🜨
        </a>
      <% end %>
    </span>
    """
  end

  def josm_load_objects(objects, opts \\ []) do
    {title, opts} = Keyword.pop(opts, :title, "Open in JOSM")
    link = Triglav.Josm.load_objects(objects, opts)


@@ 95,12 132,14 @@ defmodule TriglavWeb.OsmHelpers do
    """
  end

  def josm_zoom(lat, lon) do
  def josm_zoom(lat, lon, opts \\ []) do
    {title, opts} = Keyword.pop(opts, :title, "Open in JOSM")
    link = Triglav.Josm.zoom(lat, lon)

    ~E"""
    <a href="<%= link %>" class="josm-remote" title="Open in JOSM" target="josm">
      🜨
      <%= title %>
    </a>
    """
  end


@@ 111,7 150,7 @@ defmodule TriglavWeb.OsmHelpers do

    ~E"""
    <a href="<%= link %>" class="josm-remote" title="Add tags" target="josm">
      🜨 Add tags [<%= str_tags %>]
      🜨 Add <%= if map_size(tags) > 1, do: "tags", else: "tag" %> [<%= str_tags %>]
    </a>
    """
  end