~ihabunek/triglav

e293abae40f44ec86610f562a0243d78260768a6 — Ivan Habunek 9 months ago ee4251c
Store errors in the database
M TODO.md => TODO.md +4 -0
@@ 9,3 9,7 @@ TODO
Reading:

* https://www.volkerschatz.com/net/osm/osm2pgsql-usage.html

Errors:

* Ways with a role other than "platform"

A lib/mix/tasks/triglav/validate_routes.ex => lib/mix/tasks/triglav/validate_routes.ex +24 -0
@@ 0,0 1,24 @@
defmodule Mix.Tasks.Triglav.ValidateRoutes do
  alias Triglav.Zet.Validator
  alias Triglav.Repo
  alias Ecto.Multi
  use Mix.Task
  alias Triglav.Schemas.Error

  @shortdoc "Validates routes and saves errors to the database, replaces any existing errors"

  @impl Mix.Task
  def run(_args) do
    Application.put_env(:triglav, :repo_only, true)
    {:ok, _} = Application.ensure_all_started(:triglav)

    multi = Multi.new() |> Multi.delete_all(:delete, Error)

    Validator.validate_all_routes()
    |> Enum.with_index()
    |> Enum.reduce(multi, fn {error, index}, multi ->
      Multi.insert(multi, "error_#{index}", error)
    end)
    |> Repo.transaction()
  end
end

A lib/triglav/schemas/error.ex => lib/triglav/schemas/error.ex +104 -0
@@ 0,0 1,104 @@
defmodule Triglav.Schemas.Error do
  use Ecto.Schema
  import Ecto.Changeset

  alias Triglav.Schemas.Zet.Route
  alias Triglav.Schemas.Osm.Relation

  @derive {Inspect, only: [:id, :key]}

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

  def changeset(error, params) do
    error
    |> cast(params, [:key, :params, :route_id, :relation_id])
    |> validate_required([:key, :params, :route_id])
  end

  @spec missing_route_master(Route.t()) :: Error.t()
  def missing_route_master(%Route{} = route),
    do: %__MODULE__{key: "missing_route_master", route_id: route.id}

  @spec multiple_route_masters(Route.t()) :: Error.t()
  def multiple_route_masters(%Route{} = route),
    do: %__MODULE__{key: "multiple_route_masters", route_id: route.id}

  @spec no_relations(Route.t()) :: Error.t()
  def no_relations(%Route{} = route),
    do: %__MODULE__{key: "no_relations", route_id: route.id}

  @spec relation_not_contained_in_route_master(Route.t(), Relation.t()) :: Error.t()
  def relation_not_contained_in_route_master(%Route{} = route, %Relation{} = relation),
    do: %__MODULE__{
      key: "relation_not_contained_in_route_master",
      route_id: route.id,
      relation_id: relation.id
    }

  @spec unexpected_route_master_member_relation(Route.t(), Relation.t()) :: Error.t()
  def unexpected_route_master_member_relation(%Route{} = route, relation_id),
    do: %__MODULE__{
      key: "unexpected_route_master_member_relation",
      route_id: route.id,
      relation_id: relation_id
    }

  @spec relation_not_updated_to_ptv2(Route.t(), Relation.t()) :: Error.t()
  def relation_not_updated_to_ptv2(%Route{} = route, %Relation{} = relation),
    do: %__MODULE__{
      key: "relation_not_updated_to_ptv2",
      route_id: route.id,
      relation_id: relation.id
    }

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

  @spec relation_missing_required_tags(Route.t(), Relation.t(), [String.t()]) :: Error.t()
  def relation_missing_required_tags(%Route{} = route, %Relation{} = relation, tags),
    do: %__MODULE__{
      key: "relation_missing_required_tags",
      route_id: route.id,
      relation_id: relation.id,
      params: %{tags: tags}
    }

  @spec relation_contains_unexpected_tags(Route.t(), Relation.t(), [String.t()]) :: Error.t()
  def relation_contains_unexpected_tags(%Route{} = route, %Relation{} = relation, tags),
    do: %__MODULE__{
      key: "relation_contains_unexpected_tags",
      route_id: route.id,
      relation_id: relation.id,
      params: %{tags: tags}
    }

  @spec invalid_tag_value(Route.t(), Relation.t(), String.t(), String.t(), String.t()) ::
          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}
    }

  @spec broken_route(Route.t(), Relation.t(), integer()) :: Error.t()
  def broken_route(%Route{} = route, %Relation{} = relation, way_id),
    do: %__MODULE__{
      key: "broken_route",
      route_id: route.id,
      relation_id: relation.id,
      params: %{way_id: way_id}
    }
end

M lib/triglav/schemas/osm/relation.ex => lib/triglav/schemas/osm/relation.ex +10 -8
@@ 1,7 1,7 @@
defmodule Triglav.Schemas.Osm.Relation do
  use Ecto.Schema

  # @derive {Inspect, only: [:id, :tags]}
  @derive {Inspect, only: [:id, :tags]}
  @primary_key false

  schema "planet_osm_rels" do


@@ 19,11 19,13 @@ defmodule Triglav.Schemas.Osm.Relation do
    field :is_zet, :boolean
  end

  def get_tag(relation, name) do
    Map.get(relation.tags, name)
  end

  def has_tag?(relation, name) do
    Map.has_key?(relation.tags, name)
  end
  def get_tag(relation, name), do: Map.get(relation.tags, name)
  def has_tag?(relation, name), do: Map.has_key?(relation.tags, name)
  def has_tags?(relation, names), do: Enum.all?(names, &has_tag?(relation, &1))
  def type(relation), do: get_tag(relation, "type")
  def ref(relation), do: get_tag(relation, "ref")
  def is_route(relation), do: type(relation) == "route"
  def is_route_master(relation), do: type(relation) == "route_master"
  def is_roundtrip(relation), do: get_tag(relation, "roundtrip") == "yes"
  def is_ptv2(relation), do: get_tag(relation, "public_transport:version") == "2"
end

M lib/triglav/schemas/zet/route.ex => lib/triglav/schemas/zet/route.ex +4 -0
@@ 1,6 1,8 @@
defmodule Triglav.Schemas.Zet.Route do
  use Ecto.Schema
  alias Triglav.Schemas.Error

  @derive {Inspect, only: [:id]}
  @primary_key false
  @schema_prefix :zet



@@ 14,5 16,7 @@ defmodule Triglav.Schemas.Zet.Route do
    field :url, :string, source: :route_url
    field :color, :string, source: :route_color
    field :text_color, :string, source: :route_text_color

    has_many :errors, Error, references: :id
  end
end

A lib/triglav/zet/errors.ex => lib/triglav/zet/errors.ex +53 -0
@@ 0,0 1,53 @@
defmodule Triglav.Zet.Errors do
  alias Triglav.Schemas.Error

  def render(%Error{key: "missing_route_master"}) do
    "Missing route_master relation"
  end

  def render(%Error{key: "multiple_route_masters"}) do
    "Multiple route_master relations"
  end

  def render(%Error{key: "no_relations"}) do
    "No OSM relations found"
  end

  def render(%Error{key: "relation_not_updated_to_ptv2"}) do
    "Relation not updated to [public_transport:version=2]"
  end

  def render(%Error{
        key: "invalid_relation_name",
        params: %{"expected" => expected, "actual" => actual}
      }) do
    "Expected name based on tags: '#{expected}', actual name: '#{actual}"
  end

  def render(%Error{key: "relation_missing_required_tags", params: %{"tags" => tags}}) do
    "Missing required tags: #{Enum.join(tags, ", ")}"
  end

  def render(%Error{key: "relation_contains_unexpected_tags", params: %{"tags" => tags}}) do
    "Unexpected tags: #{Enum.join(tags, ", ")}"
  end

  def render(%Error{
        key: "invalid_tag_value",
        params: %{"name" => name, "expected" => expected, "actual" => actual}
      }) do
    if actual do
      "Invalid tag [#{name}=#{actual}], expected [#{name}=#{expected}]"
    else
      "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

  def render(%Error{key: key}) do
    "FIXME: missing render function for error '#{key}'"
  end
end

M lib/triglav/zet/gtfs.ex => lib/triglav/zet/gtfs.ex +15 -2
@@ 1,6 1,7 @@
defmodule Triglav.Zet.Gtfs do
  alias Triglav.Repo
  alias Triglav.Schemas.Zet
  alias Triglav.Schemas.Error

  import Ecto.Query



@@ 8,8 9,14 @@ defmodule Triglav.Zet.Gtfs do
    Zet.FeedInfo |> Repo.one!()
  end

  def list_routes do
    Zet.Route |> Repo.all()
  def list_routes(opts \\ []) do
    routes = Repo.all(Zet.Route)

    if Keyword.get(opts, :with_errors, false) do
      Repo.preload(routes, :errors, prefix: nil)
    else
      routes
    end
  end

  def get_route(id) do


@@ 18,6 25,12 @@ defmodule Triglav.Zet.Gtfs do
    |> Repo.one!()
  end

  def get_errors(route_id) do
    from(e in Error, where: e.route_id == ^route_id)
    |> Repo.all()
    |> Repo.preload(:relation)
  end

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

M lib/triglav/zet/osm.ex => lib/triglav/zet/osm.ex +2 -2
@@ 16,6 16,7 @@ defmodule Triglav.Zet.Osm do
  which do not have a role (to skip stations entered as ways). Returns them as
  a map indexed by way ID for easier lookup.
  """
  @spec list_ways([Relation.t()]) :: Way.t()
  def list_ways(relations) do
    ids =
      relations


@@ 28,7 29,6 @@ defmodule Triglav.Zet.Osm do

    from(w in Way, where: w.id in ^ids)
    |> Repo.all()
    |> Map.new(& {&1.id, &1})
  end

  # Private


@@ 43,7 43,7 @@ defmodule Triglav.Zet.Osm do
  defp filter_by_ref(query, _), do: query

  # Consider moving this to the database
  def process_members(rel) do
  defp process_members(rel) do
    Map.update!(rel, :members, fn members ->
      members
      |> Enum.chunk_every(2)

M lib/triglav/zet/validator.ex => lib/triglav/zet/validator.ex +79 -74
@@ 2,19 2,33 @@ defmodule Triglav.Zet.Validator do
  @moduledoc """
  Validates ZET routes in OSM based on ZET GTFS data.
  """
  alias Triglav.Schemas.Error
  alias Triglav.Schemas.Osm.Relation
  alias Triglav.Schemas.Osm.Way
  alias Triglav.Schemas.Zet.Route
  alias Triglav.Zet.Gtfs
  alias Triglav.Zet.Osm

  @spec validate_all_routes() :: [Error.t()]
  def validate_all_routes() do
    routes = Gtfs.list_routes()
    relations = Osm.list_public_transport_relations()
    ways = Osm.list_ways(relations) |> Map.new(&{&1.id, &1})

    for route <- routes do
      route_relations = Enum.filter(relations, &(Relation.ref(&1) == route.id))
      validate_route(route, route_relations, ways)
    end
    |> List.flatten()
  end

  @type error :: binary

  @spec validate_route(Route.t(), [Relation.t()], %{required(integer) => Way.t()}) :: [error]
  @spec validate_route(Route.t(), [Relation.t()], %{required(integer) => Way.t()}) :: [Error.t()]
  def validate_route(route, relations, ways) do
    [
      validate_has_route_master(relations),
      validate_has_relations(relations),
      validate_routes_are_contained_in_route_master(relations),
      validate_routes_master_route_has_no_unknown_members(relations),
      validate_has_relations(route, relations),
      validate_has_route_master(route, relations),
      validate_routes_are_contained_in_route_master(route, relations),
      validate_routes_master_route_has_no_unknown_members(route, relations),
      Enum.map(relations, &validate_route_master_relation(route, &1)),
      Enum.map(relations, &validate_route_relation(route, &1, ways))
    ]


@@ 26,61 40,64 @@ defmodule Triglav.Zet.Validator do
  # Validation fns
  #

  defp validate_has_route_master(relations) do
    count = Enum.count(relations, &is_route_master/1)
    all_roundtrips = Enum.all?(relations, &is_roundtrip/1)
  defp validate_has_route_master(route, relations) do
    count = Enum.count(relations, &Relation.is_route_master/1)
    all_roundtrips = Enum.all?(relations, &Relation.is_roundtrip/1)

    cond do
      count == 0 and !all_roundtrips -> "Missing route master relation"
      count > 1 -> "Multiple route masters"
      count == 0 and !all_roundtrips -> Error.missing_route_master(route)
      count > 1 -> Error.multiple_route_masters(route)
      true -> nil
    end
  end

  defp validate_has_relations(relations) do
  defp validate_has_relations(route, relations) do
    if Enum.empty?(relations) do
      "No OSM relations found"
      Error.no_relations(route)
    end
  end

  defp validate_routes_are_contained_in_route_master(relations) do
    route_masters = Enum.filter(relations, &is_route_master/1)
  defp validate_routes_are_contained_in_route_master(route, relations) do
    route_masters = Enum.filter(relations, &Relation.is_route_master/1)

    if length(route_masters) == 1 do
      [route_master] = route_masters
      route_relations = Enum.filter(relations, &is_route/1)
      route_relations = Enum.filter(relations, &Relation.is_route/1)
      contained_ids = MapSet.new(route_master.parts)
      route_ids = route_relations |> Enum.map(& &1.id) |> MapSet.new()
      extra_ids = MapSet.difference(route_ids, contained_ids)
      extra_relations = Enum.filter(relations, &(&1.id in extra_ids))

      for relation <- extra_relations do
        {relation, "Not contained in route master"}
        Error.relation_not_contained_in_route_master(route, relation)
      end
    end
  end

  defp validate_routes_master_route_has_no_unknown_members(relations) do
    route_masters = Enum.filter(relations, &is_route_master/1)
  defp validate_routes_master_route_has_no_unknown_members(route, relations) do
    route_masters = Enum.filter(relations, &Relation.is_route_master/1)

    if length(route_masters) == 1 do
      [route_master] = route_masters
      routes = Enum.filter(relations, &is_route/1)
      routes = Enum.filter(relations, &Relation.is_route/1)
      contained_ids = MapSet.new(route_master.parts)
      route_ids = routes |> Enum.map(& &1.id) |> MapSet.new()
      ids = MapSet.difference(contained_ids, route_ids)
      route_ids = MapSet.new(routes, & &1.id)
      unexpected_ids = MapSet.difference(contained_ids, route_ids)

      if !Enum.empty?(ids) do
        "Master route has unknown members: #{Enum.join(ids, ", ")}"
      for relation_id <- unexpected_ids do
        Error.unexpected_route_master_member_relation(route, relation_id)
      end
    end
  end

  # ROUTE MASTER RELATION

  defp validate_route_master_relation(%Route{} = route, %Relation{type: "route_master"} = rel) do
  defp validate_route_master_relation(
         %Route{} = route,
         %Relation{type: "route_master"} = relation
       ) do
    required_tags = ["type", "ref", "name", "operator"]
    allowed_tags = Enum.concat(required_tags, ["route_master", "network"])
    allowed_tags = required_tags ++ ["route_master", "network"]

    expected_name = "#{route |> route_tag() |> String.capitalize()} #{route.id}"



@@ 92,9 109,9 @@ defmodule Triglav.Zet.Validator do
    }

    [
      validate_required_tags(rel, required_tags),
      validate_allowed_tags(rel, allowed_tags),
      validate_tag_values(rel, expected_tags)
      validate_required_tags(route, relation, required_tags),
      validate_allowed_tags(route, relation, allowed_tags),
      validate_tag_values(route, relation, expected_tags)
    ]
  end



@@ 113,31 130,31 @@ defmodule Triglav.Zet.Validator do
    }

    [
      validate_is_ptv2(relation),
      validate_route_naming(relation),
      validate_required_tags(relation, required_tags),
      validate_allowed_tags(relation, allowed_tags),
      validate_tag_values(relation, expected_tags),
      validate_continuous_ways(relation, ways)
      validate_is_ptv2(route, relation),
      validate_route_naming(route, relation),
      validate_required_tags(route, relation, required_tags),
      validate_allowed_tags(route, relation, allowed_tags),
      validate_tag_values(route, relation, expected_tags),
      validate_continuous_ways(route, relation, ways)
    ]
  end

  defp validate_route_relation(_, _, _), do: nil

  defp validate_is_ptv2(relation) do
    if !is_ptv2(relation) do
      {relation, "Not updated to [public_transport:version=2]"}
  defp validate_is_ptv2(route, relation) do
    if !Relation.is_ptv2(relation) do
      Error.relation_not_updated_to_ptv2(route, relation)
    end
  end

  defp validate_route_naming(rel) do
    if has_tags(rel, ["name", "ref", "from", "to"]) do
      name = get_tag(rel, "name")
      ref = get_tag(rel, "ref")
      type = get_tag(rel, "route")
      from = get_tag(rel, "from")
      to = get_tag(rel, "to")
      via = get_tag(rel, "via")
  defp validate_route_naming(route, relation) do
    if Relation.has_tags?(relation, ["name", "ref", "from", "to"]) do
      actual = Relation.get_tag(relation, "name")
      ref = Relation.get_tag(relation, "ref")
      type = Relation.get_tag(relation, "route")
      from = Relation.get_tag(relation, "from")
      to = Relation.get_tag(relation, "to")
      via = Relation.get_tag(relation, "via")

      expected =
        if via do


@@ 146,8 163,8 @@ defmodule Triglav.Zet.Validator do
          "#{String.capitalize(type)} #{ref}: #{from} => #{to}"
        end

      if name != expected do
        {rel, "Expected name based on tags: '#{expected}', actual name: '#{name}'"}
      if actual != expected do
        Error.invalid_relation_name(route, relation, actual, expected)
      end
    end
  end


@@ 156,7 173,7 @@ defmodule Triglav.Zet.Validator do
  Validates that for each way included in a relation, the following way starts
  with the same node with which the previous way ends.
  """
  def validate_continuous_ways(relation, ways) do
  def validate_continuous_ways(route, relation, ways) do
    relation.members
    # Skip stops
    |> Enum.filter(&(&1.type == :way and &1.role == ""))


@@ 168,7 185,7 @@ defmodule Triglav.Zet.Validator do

      [a, b] ->
        if !Way.adjacent?(a, b) do
          {relation, "Broken route starting at way #{b.id}"}
          Error.broken_route(route, relation, b.id)
        end
    end)
  end


@@ 177,34 194,32 @@ defmodule Triglav.Zet.Validator do
  # Generic relation validators
  #

  def validate_required_tags(relation, required_tags) do
  def validate_required_tags(route, relation, required_tags) do
    required = MapSet.new(required_tags)
    existing = MapSet.new(Map.keys(relation.tags))
    missing = MapSet.difference(required, existing)
    missing = MapSet.difference(required, existing) |> MapSet.to_list()

    if MapSet.size(missing) > 0 do
      {relation, "Missing required tags: #{Enum.join(missing, ", ")}"}
    if length(missing) > 0 do
      Error.relation_missing_required_tags(route, relation, missing)
    end
  end

  defp validate_allowed_tags(relation, allowed_tags) do
  defp validate_allowed_tags(route, relation, allowed_tags) do
    allowed = MapSet.new(allowed_tags)
    existing = MapSet.new(Map.keys(relation.tags))
    unexpected = MapSet.difference(existing, allowed)
    unexpected = MapSet.difference(existing, allowed) |> MapSet.to_list()

    if MapSet.size(unexpected) > 0 do
      {relation, "Unexpected tags: #{Enum.join(unexpected, ", ")}"}
    if length(unexpected) > 0 do
      Error.relation_contains_unexpected_tags(route, relation, unexpected)
    end
  end

  defp validate_tag_values(relation, expected_tags) do
  defp validate_tag_values(route, relation, expected_tags) do
    for {k, expected} <- expected_tags do
      actual = Map.get(relation.tags, k)

      case actual do
        nil -> {relation, "Missing tag [#{k}=#{expected}]"}
        ^expected -> nil
        _ -> {relation, "Invalid tag [#{k}=#{actual}], expected [#{k}=#{expected}]"}
      if actual != expected do
        Error.invalid_tag_value(route, relation, k, actual, expected)
      end
    end
  end


@@ 215,14 230,4 @@ defmodule Triglav.Zet.Validator do

  defp route_tag(%Route{type: 0}), do: "tram"
  defp route_tag(%Route{type: 3}), do: "bus"

  defp get_tag(%Relation{} = relation, name), do: Map.get(relation.tags, name)
  defp has_tag(%Relation{} = relation, name), do: get_tag(relation, name) not in [nil, ""]
  defp has_tags(%Relation{} = relation, names), do: Enum.all?(names, &has_tag(relation, &1))
  defp type(%Relation{} = relation), do: get_tag(relation, "type")

  defp is_route(relation), do: type(relation) == "route"
  defp is_route_master(relation), do: type(relation) == "route_master"
  defp is_roundtrip(relation), do: get_tag(relation, "roundtrip") == "yes"
  defp is_ptv2(relation), do: get_tag(relation, "public_transport:version") == "2"
end

M lib/triglav_web/controllers/zet/routes_controller.ex => lib/triglav_web/controllers/zet/routes_controller.ex +8 -17
@@ 2,14 2,13 @@ defmodule TriglavWeb.Zet.RoutesController do
  use TriglavWeb, :controller

  alias Triglav.DataImport
  alias Triglav.Schemas.Osm.Relation
  alias Triglav.Zet.Gtfs
  alias Triglav.Zet.Osm
  alias Triglav.Zet.Validator

  def index(conn, _params) do
    routes = Gtfs.list_routes()
    routes = Gtfs.list_routes(with_errors: true)
    relations = Osm.list_public_transport_relations()
    ways = Osm.list_ways(relations)
    osm_info = DataImport.load_state()
    gtfs_info = Gtfs.get_feed_info()



@@ 17,17 16,14 @@ defmodule TriglavWeb.Zet.RoutesController do
      for route <- routes do
        route_relations = route_relations(route, relations)
        hierarchy = make_hierarchy(route_relations)
        errors = Validator.validate_route(route, route_relations, ways)
        {route, hierarchy, errors}
        {route, hierarchy}
      end

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

    unmached_relations =
      Enum.filter(relations, fn rel ->
        rel.tags["ref"] not in route_ids
      end)
      Enum.filter(relations, &(Relation.ref(&1) not in route_ids))
      |> sort_by_ref()

    render(conn, "index.html",


@@ 41,13 37,8 @@ defmodule TriglavWeb.Zet.RoutesController do
  def detail(conn, %{"id" => id}) do
    route = Gtfs.get_route(id)
    relations = Osm.list_public_transport_relations(ref: id)
    ways = Osm.list_ways(relations)
    hierarchy = make_hierarchy(relations)

    {errors, rel_errors} =
      Validator.validate_route(route, relations, ways)
      |> group_errors_by_relation()

    {errors, rel_errors} = Gtfs.get_errors(id) |> group_errors_by_relation()
    trips = Gtfs.fetch_distinct_trips(route) |> Enum.group_by(& &1["direction_id"])

    # JSON encode stops for drawing on the map


@@ 70,11 61,11 @@ defmodule TriglavWeb.Zet.RoutesController do
  end

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

    rel_errors =
      Enum.reduce(rel_errors, %{}, fn {rel, error}, acc ->
        Map.update(acc, rel, [error], fn errors -> [error | errors] end)
      Enum.reduce(rel_errors, %{}, fn error, acc ->
        Map.update(acc, error.relation, [error], fn errors -> [error | errors] end)
      end)

    {errors, rel_errors}

M lib/triglav_web/templates/zet/routes/detail.html.eex => lib/triglav_web/templates/zet/routes/detail.html.eex +2 -2
@@ 28,7 28,7 @@
        <ul>
          <%= for error <- @errors do %>
            <li>
              <span class="red"><%= error %></span>
              <span class="red"><%= render_error(error) %></span>
            </li>
          <% end %>
        </ul>


@@ 40,7 40,7 @@
              <%= osm_link(relation, tags: ["type"], name: true) %>
              <ul>
                <%= for error <- errors do %>
                  <li><span class="red"><%= error %></span></li>
                  <li><span class="red"><%= render_error(error) %></span></li>
                <% end %>
              </ul>
            </li>

M lib/triglav_web/templates/zet/routes/index.html.eex => lib/triglav_web/templates/zet/routes/index.html.eex +4 -9
@@ 74,7 74,7 @@
      </tr>
    </thead>
    <tbody>
      <%= for {route, hierarchy, errors} <- @routes do %>
      <%= for {route, hierarchy} <- @routes do %>
      <tr>
        <td><%= route.id %></td>
        <td>


@@ 86,16 86,11 @@
          <%= render_relation_hierarchy(hierarchy) %>
        </td>
        <td>
          <%= if !Enum.empty?(errors) do %>
          <%= if !Enum.empty?(route.errors) do %>
          <ul>
            <%= for error <- errors do %>
            <%= for error <- route.errors do %>
              <li>
                <span class="red">
                  <%= case error do
                    {relation, error} -> [osm_link(relation, tags: ["type"]), error]
                    error -> error
                  end %>
                </span>
                <span class="red"><%= render_error(error) %></span>
              </li>
            <% end %>
          </ul>

M lib/triglav_web/views/osm_helpers.ex => lib/triglav_web/views/osm_helpers.ex +5 -0
@@ 3,6 3,7 @@ defmodule TriglavWeb.OsmHelpers do
  Helpers for displaying OSM data.
  """
  use Phoenix.HTML
  alias Triglav.Zet.Errors
  alias Triglav.Schemas.Osm.Relation
  alias TriglavWeb.Router.Helpers



@@ 64,4 65,8 @@ defmodule TriglavWeb.OsmHelpers do
    </a>
    """
  end

  def render_error(error) do
    Errors.render(error)
  end
end

A priv/repo/migrations/20201231080104_create_error.exs => priv/repo/migrations/20201231080104_create_error.exs +17 -0
@@ 0,0 1,17 @@
defmodule Triglav.Repo.Migrations.CreateError do
  use Ecto.Migration

  def up do
    create table("errors") do
      add :key, :text, null: false
      add :params, :map, null: false, default: %{}
      add :route_id, references("routes", prefix: :zet, type: :text, column: :route_id), null: false
      add :relation_id, references("planet_osm_rels")
    end
  end

  def down do
    drop table("errors")
  end
end