~ihabunek/triglav

3caf5e02e66757c2a4d1af2e8b2bd064d7b16968 — Ivan Habunek 8 days ago d511a0d
Implement matching OSM relations to GTFS trips
M assets/css/app.scss => assets/css/app.scss +12 -1
@@ 165,6 165,17 @@ a.josm-remote {
  display: inline-block;
}

ul.breadcrumbs {
  padding: 0;
  list-style-type: none;
  display: flex;

  li:not(:first-child):before {
    content: ">";
    padding: 0 .75rem;
  }
}

// Utils

.tag { color: DimGray; white-space: nowrap; }


@@ 176,7 187,7 @@ a.josm-remote {
.w-full { width: 100%; }

.bg-red { background-color: rgba($red, 0.3); }
.bg-green { background-color: rbga($green, 0.3); }
.bg-green { background-color: rgba($green, 0.3); }

.text-bold { font-weight: bold; }
.text-smaller { font-size: 0.9rem; }

M lib/triglav/osm.ex => lib/triglav/osm.ex +4 -0
@@ 6,6 6,10 @@ defmodule Triglav.Osm do

  import Ecto.Query

  def get_relation(relation_id) do
    Repo.get(Relation, relation_id)
  end

  @doc """
  Returns a list of nodes with given ids, ordered the same as the given ids.
  """

M lib/triglav/zet/osmosis.ex => lib/triglav/zet/osmosis.ex +1 -1
@@ 94,7 94,7 @@ defmodule Triglav.Zet.Osmosis do
  """
  @spec list_platform_members(Relation.id() | [Relation.id()]) :: [RelationMember.t()]

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

  def list_platform_members(relation_ids) when is_list(relation_ids) do

M lib/triglav_web/controllers/zet/routes_controller.ex => lib/triglav_web/controllers/zet/routes_controller.ex +73 -30
@@ 45,36 45,6 @@ defmodule TriglavWeb.Zet.RoutesController do
    )
  end

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

    for route <- routes do
      route_relations = Enum.filter(relations, &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 =
        gtfs_distinct_trips
        |> 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)


@@ 145,6 115,79 @@ defmodule TriglavWeb.Zet.RoutesController do
    )
  end

  def match(conn, %{"id" => id, "relation_id" => relation_id}) do
    trips = Gtfs.list_distinct_trips(route_id: id)
    relation = Osm.get_relation(relation_id)

    platform_members = Osmosis.list_platform_members(relation_id)

    platforms =
      platform_members
      |> Enum.map(& &1.member)
      |> Map.new(&{&1.tags["gtfs:stop_id"], &1})

    stops =
      trips
      |> Enum.map(& &1.stops)
      |> Enum.concat()
      |> Enum.uniq()
      |> Gtfs.list_stops_by_id()
      |> Map.new(&{&1.id, &1})

    relation_stop_ids = platform_members |> Enum.map(& &1.member.tags["gtfs:stop_id"])

    diffs =
      for trip <- trips do
        List.myers_difference(relation_stop_ids, trip.stops)
      end
      |> Enum.sort_by(&count_eq/1, &>=/2)

    render(conn, "match.html",
      diffs: diffs,
      platforms: platforms,
      relation: relation,
      route_id: id,
      stops: stops
    )
  end

  defp count_eq(diffs) do
    diffs
    |> Enum.filter(fn {k, _} -> k == :eq end)
    |> Enum.map(fn {_, v} -> length(v) end)
    |> Enum.sum()
  end

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

    for route <- routes do
      route_relations = Enum.filter(relations, &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 =
        gtfs_distinct_trips
        |> 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

  defp routes_feature_collecton(relations, platform_members) do
    colors = @route_colors |> Enum.map(fn {_, v} -> v end) |> Stream.cycle()


M lib/triglav_web/router.ex => lib/triglav_web/router.ex +1 -0
@@ 22,6 22,7 @@ defmodule TriglavWeb.Router do
    scope "/zet", alias: Zet, as: :zet do
      get "/routes", RoutesController, :index
      get "/routes/:id", RoutesController, :detail
      get "/routes/:id/match/:relation_id", RoutesController, :match
      get "/stops", StopsController, :index
      get "/errors/history", ErrorsController, :index
      get "/errors/history/atom", ErrorsController, :atom

M lib/triglav_web/templates/zet/routes/detail.html.eex => lib/triglav_web/templates/zet/routes/detail.html.eex +8 -0
@@ 17,6 17,11 @@
</script>

<main role="main" class="container-wide">
  <ul class="breadcrumbs">
    <li><a href="<%= Routes.zet_routes_path(@conn, :index) %>">Rotues</a></li>
    <li>#<%= @route.id %></li>
  </ul>

  <h1>#<%= @route.id %>: <%= @route.long_name %></h1>

  <div id="routes-map"></div>


@@ 106,6 111,9 @@
          <% else %>
            <div class="callout error">
              ✖ No GTFS trip matched.
              <a href="<%= Routes.zet_routes_path(TriglavWeb.Endpoint, :match, @route.id, relation.id) %>">
                Find a match
              </a>
            </div>
          <% end %>
        </div>

A lib/triglav_web/templates/zet/routes/match.html.eex => lib/triglav_web/templates/zet/routes/match.html.eex +52 -0
@@ 0,0 1,52 @@
<main role="main" class="container-wide">
  <ul class="breadcrumbs">
    <li><a href="<%= Routes.zet_routes_path(@conn, :index) %>">Rotues</a></li>
    <li><a href="<%= Routes.zet_routes_path(@conn, :detail, @route_id) %>">#<%= @route_id %></a></li>
    <li>Match relation #<%= @relation.id %></li>
  </ul>

  <h1>Find a matching trip for relation #<%= @relation.id %></h1>
  <p>Sorted by most to least probable.</p>

  <%= for trip_diffs <- @diffs do %>
    <table>
      <thead>
        <tr>
          <th colspan="2">OSM platforms</th>
          <th colspan="2">GTFS stops</th>
        </tr>
      </thead>
      <tbody>
      <%= for {op, ids} <- trip_diffs do %>
        <%= for id <- ids do %>
          <% platform = Map.get(@platforms, id) %>
          <% stop = Map.get(@stops, id) %>
          <tr>
            <%= if op == :eq do %>
              <td><%= id %></td>
              <td><%= platform.tags["name"] %></td>
              <td><%= id %></td>
              <td><%= stop.name %></td>
            <% end %>

            <%= if op == :ins do %>
              <td></td>
              <td></td>
              <td class="bg-green"><%= id %></td>
              <td class="bg-green"><%= stop.name %></td>
            <% end %>

            <%= if op == :del do %>
              <td class="bg-red"><%= id %></td>
              <td class="bg-red"><%= platform.tags["name"] %></td>
              <td></td>
              <td></td>
            <% end %>
          </tr>
        <% end %>
      <% end %>
      </tbody>
    </table>
    <hr style="margin: 2rem 0" />
  <% end %>
</main>

M lib/triglav_web/views/zet/routes_view.ex => lib/triglav_web/views/zet/routes_view.ex +1 -0
@@ 5,6 5,7 @@ defmodule TriglavWeb.Zet.RoutesView do

  def title("index.html", _), do: "Routes"
  def title("detail.html", assigns), do: "Route ##{assigns.route.id}"
  def title("match.html", assigns), do: "Matching ##{assigns.relation.id}"

  def render_relation_hierarchy(hierarchy) do
    ~E"""