~ihabunek/triglav

3a62b8feb3be418cf9f3fb176ac6c84a36e66a2f — Ivan Habunek 3 months ago 3a18761
Validate tags on platforms
M lib/triglav/derived/public_transport.ex => lib/triglav/derived/public_transport.ex +6 -0
@@ 31,6 31,12 @@ defmodule Triglav.Derived.PublicTransport do
    from(p in Platform) |> Repo.all()
  end

  def list_platforms_with_members() do
    from(p in Platform, preload: [:node, :way])
    |> Repo.all()
    |> Enum.map(fn p -> Map.put(p, :member, p.node || p.way) end)
  end

  def generate() do
    relations = Osmosis.list_public_transport_relations(members: true, type: "route")
    relations_map = Map.new(relations, &{&1.id, &1})

M lib/triglav/schemas/error.ex => lib/triglav/schemas/error.ex +50 -9
@@ 9,9 9,13 @@ defmodule Triglav.Schemas.Error do

  @type t :: %__MODULE__{}

  @type tag_name :: String.t()
  @type tag_value :: String.t()

  schema "errors" do
    belongs_to :route, Route, type: :string
    belongs_to :relation, Relation
    belongs_to :platform, Platform
    field :key, :string
    field :params, :map, default: %{}
    field :created_at, :utc_datetime


@@ 137,15 141,51 @@ defmodule Triglav.Schemas.Error do
      params: %{count: count}
    }

  @spec invalid_tag_value(Route.t(), Relation.t(), String.t(), String.t(), String.t()) ::
  @spec relation_invalid_tag_value(Route.t(), Relation.t(), tag_name(), tag_value(), tag_value()) ::
          Error.t()
  def invalid_tag_value(%Route{} = route, %Relation{} = relation, name, actual, expected),
    do: %__MODULE__{
      key: "invalid_tag_value",
      route_id: route.id,
      relation_id: relation.id,
      params: %{name: name, expected: expected, actual: actual}
    }
  def relation_invalid_tag_value(
        %Route{} = route,
        %Relation{} = relation,
        name,
        actual,
        expected
      ),
      do: %__MODULE__{
        key: "relation_invalid_tag_value",
        route_id: route.id,
        relation_id: relation.id,
        params: %{name: name, expected: expected, actual: actual}
      }

  @spec platform_invalid_tag_value(
          Route.t(),
          Relation.t(),
          Platform.t(),
          tag_name(),
          tag_value(),
          tag_value()
        ) ::
          Error.t()
  def platform_invalid_tag_value(
        %Route{} = route,
        %Relation{} = relation,
        %Platform{} = platform,
        name,
        actual,
        expected
      ),
      do: %__MODULE__{
        key: "platform_invalid_tag_value",
        route_id: route.id,
        relation_id: relation.id,
        platform_id: platform.id,
        params: %{
          sequence_id: platform.sequence_id,
          name: name,
          expected: expected,
          actual: actual
        }
      }

  @spec broken_route(Route.t(), Relation.t(), integer()) :: Error.t()
  def broken_route(%Route{} = route, %Relation{} = relation, way_id),


@@ 179,6 219,7 @@ defmodule Triglav.Schemas.Error do
        key: "platform_too_far_from_route",
        route_id: route.id,
        relation_id: relation.id,
        params: Map.take(platform, [:node_id, :way_id, :sequence_id, :distance_from_route])
        platform_id: platform.id,
        params: Map.take(platform, [:sequence_id, :distance_from_route])
      }
end

M lib/triglav/schemas/public_transport/trip.ex => lib/triglav/schemas/public_transport/trip.ex +1 -1
@@ 9,7 9,7 @@ defmodule Triglav.Schemas.PublicTransport.Trip do
  alias Triglav.Schemas.Zet
  alias Triglav.Schemas.PublicTransport.Platform

  @derive {Inspect, only: [:id, :relation_id, :sample_trip_id]}
  # @derive {Inspect, only: [:id, :relation_id, :sample_trip_id]}

  @type t :: %__MODULE__{}


M lib/triglav/zet/errors.ex => lib/triglav/zet/errors.ex +17 -1
@@ 59,7 59,7 @@ defmodule Triglav.Zet.Errors do
  end

  def render(%Error{
        key: "invalid_tag_value",
        key: "relation_invalid_tag_value",
        params: %{"name" => name, "expected" => expected, "actual" => actual}
      }) do
    if actual do


@@ 69,6 69,22 @@ defmodule Triglav.Zet.Errors do
    end
  end

  def render(%Error{
        key: "platform_invalid_tag_value",
        params: %{
          "sequence_id" => sequence_id,
          "name" => name,
          "expected" => expected,
          "actual" => actual
        }
      }) do
    if actual do
      "Platform #{sequence_id} has invalid tag [#{name}=#{actual}], expected [#{name}=#{expected}]"
    else
      "Platform #{sequence_id} is missing tag [#{name}=#{expected}]"
    end
  end

  def render(%Error{key: "broken_route", params: %{"way_id" => way_id}}) do
    "Broken route starting at way ##{way_id}"
  end

M lib/triglav/zet/validator.ex => lib/triglav/zet/validator.ex +34 -7
@@ 2,14 2,13 @@ defmodule Triglav.Zet.Validator do
  @moduledoc """
  Validates ZET routes in OSM based on ZET GTFS data.
  """
  alias Triglav.Derived.PublicTransport
  alias Triglav.Schemas.Error
  alias Triglav.Schemas.Osmosis.Relation
  alias Triglav.Schemas.Osmosis.Way
  alias Triglav.Schemas.Osmosis.{Relation, Way}
  alias Triglav.Schemas.PublicTransport.Platform
  alias Triglav.Schemas.Zet.Route
  alias Triglav.Zet.Gtfs
  alias Triglav.Zet.Osmosis
  alias Triglav.Derived.PublicTransport
  alias Triglav.Schemas.PublicTransport.Platform

  # Maximum distance from platform to route
  @max_platform_distance_from_route 20


@@ 44,7 43,7 @@ defmodule Triglav.Zet.Validator do
    relations_by_ref = Enum.group_by(relations, & &1.tags["ref"])
    trips = PublicTransport.list_trips()
    trips_map = trips |> Map.new(&{&1.relation_id, &1})
    platforms = PublicTransport.list_platforms()
    platforms = PublicTransport.list_platforms_with_members()
    platforms_by_relation_id = Enum.group_by(platforms, & &1.relation_id)

    # Ways are only used for finding where routes are broken, so only fetch ways


@@ 219,7 218,8 @@ defmodule Triglav.Zet.Validator do
      validate_continuous_ways(route, relation, trip, data),
      validate_has_gtfs_stop_ids(route, relation, platforms),
      validate_member_roles(route, relation),
      validate_platform_distance_from_route(route, relation, platforms)
      validate_platform_distance_from_route(route, relation, platforms),
      validate_platform_tags(route, relation, platforms)
    ]
  end



@@ 314,6 314,33 @@ defmodule Triglav.Zet.Validator do
    end
  end

  defp validate_platform_tags(route, relation, platforms) do
    for platform <- platforms,
        {name, expected} <- expected_platform_tags(relation) do
      actual = Map.get(platform.member.tags, name)

      if actual != expected do
        Error.platform_invalid_tag_value(route, relation, platform, name, actual, expected)
      end
    end
  end

  defp expected_platform_tags(%Relation{tags: %{"type" => "route", "route" => "bus"}}) do
    %{
      "bus" => "yes",
      "highway" => "bus_stop",
      "public_transport" => "platform"
    }
  end

  defp expected_platform_tags(%Relation{tags: %{"type" => "route", "route" => "tram"}}) do
    %{
      "tram" => "yes",
      "railway" => "platform",
      "public_transport" => "platform"
    }
  end

  #
  # Generic relation validators
  #


@@ 343,7 370,7 @@ defmodule Triglav.Zet.Validator do
      actual = Map.get(relation.tags, k)

      if actual != expected do
        Error.invalid_tag_value(route, relation, k, actual, expected)
        Error.relation_invalid_tag_value(route, relation, k, actual, expected)
      end
    end
  end

M lib/triglav_web/controllers/zet/routes_controller.ex => lib/triglav_web/controllers/zet/routes_controller.ex +7 -1
@@ 57,6 57,11 @@ defmodule TriglavWeb.Zet.RoutesController do
    relation_errors = Enum.filter(errors, & &1.relation_id) |> Enum.group_by(& &1.relation_id)
    route_errors = Enum.filter(errors, &is_nil(&1.relation_id))

    platform_errors =
      errors
      |> Enum.filter(&(!is_nil(&1.platform_id)))
      |> Enum.group_by(& &1.platform_id)

    trips = PublicTransport.list_route_trips(id)
    zet_stops = Gtfs.fetch_distinct_stops(route)
    zet_stops_map = Map.new(zet_stops, &{&1.id, &1})


@@ 79,12 84,13 @@ defmodule TriglavWeb.Zet.RoutesController do

    render(conn, "detail.html",
      conn: conn,
      trips: trips,
      hierarchy: hierarchy,
      platform_errors: platform_errors,
      relation_errors: relation_errors,
      relations: relations,
      route: route,
      route_errors: route_errors,
      trips: trips,
      trips_geojson: trips_geojson,
      zet_stops_geojson: zet_stops_geojson,
      zet_trips: zet_trips

M lib/triglav_web/templates/zet/routes/detail.html.eex => lib/triglav_web/templates/zet/routes/detail.html.eex +19 -2
@@ 69,6 69,7 @@

  <section>
    <%= for trip <- @trips do %>
      <% has_platform_errors = Enum.any?(trip.platforms, &Map.has_key?(@platform_errors, &1.id)) %>
      <h3 style="margin-top: 1rem"><%= osm_link(trip.relation, name: true) %></h3>
      <div style="margin-left: 1rem;">
        <%= if length(trip.platforms) > 0 do %>


@@ 77,6 78,9 @@
              <tr>
                <th colspan="6" class="text-center">OSM Platform</th>
                <th colspan="2" class="text-center bl">GTFS Stop</th>
                <%= if has_platform_errors do %>
                  <th colspan="1" rowspan="2" class="text-center bl">Errors</th>
                <% end %>
              </tr>
              <tr>
                <th>#</th>


@@ 92,6 96,7 @@
            <tbody>
              <%= for platform <- trip.platforms do %>
              <% member = platform.node || platform.way %>
              <% errors = Map.get(@platform_errors, platform.id, []) %>
              <tr>
                <td><%= platform.sequence_id %></td>
                <td><%= osm_link(member) %></td>


@@ 110,19 115,31 @@
                  <td class="bl"></td>
                  <td></td>
                <% end %>
                <%= if has_platform_errors do %>
                  <td class="bl">
                    <%= if length(errors) > 0 do %>
                      <ul class="no-margin text-red">
                        <%= for error <- errors do %>
                          <li><%= render_error(error) %></li>
                        <% end %>
                      </ul>
                    <% end %>
                  </td>
                <% end %>
              </tr>
              <% end %>
            </tbody>
            <tfoot>
              <% colspan = if has_platform_errors, do: 9, else: 8 %>
              <%= if trip.sample_trip_id do %>
                <tr>
                  <td colspan="8" class="success text-green text-center">
                  <td colspan="<%= colspan %>" class="success text-green text-center">
                    ✔ Matches ZET trip <b><%= trip.sample_trip_id %></b>
                  </td>
                </tr>
              <% else %>
                <tr>
                  <td class="error text-center" colspan="8">
                  <td class="error text-center" colspan="<%= colspan %>">
                    ✖ No GTFS trip matched.
                    <a href="<%= Routes.zet_routes_path(TriglavWeb.Endpoint, :match, @route.id, trip.relation.id) %>">
                      Find a match

A priv/repo/migrations/20210311085351_alter_error_add_platform.exs => priv/repo/migrations/20210311085351_alter_error_add_platform.exs +15 -0
@@ 0,0 1,15 @@
defmodule Triglav.Repo.Migrations.AlterErrorAddPlatform do
  use Ecto.Migration

  def up do
    alter table("errors") do
      add :platform_id, :integer
    end
  end

  def down do
    alter table("errors") do
      remove :platform_id, :integer
    end
  end
end