~ihabunek/triglav

3cd3f5e5e6d5515948d231d68bb891c008021afe — Ivan Habunek 1 year, 6 months ago 3a18761 platform-errors
WIP
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})

A lib/triglav/haversine.ex => lib/triglav/haversine.ex +13 -0
@@ 0,0 1,13 @@
defmodule Haversine do
  @v :math.pi() / 180
  @radius 6372.8

  alias Geo.Point

  def distance({lat1, long1}, {lat2, long2}) do
    dlat = :math.sin((lat2 - lat1) * @v / 2)
    dlong = :math.sin((long2 - long1) * @v / 2)
    a = dlat * dlat + dlong * dlong * :math.cos(lat1 * @v) * :math.cos(lat2 * @v)
    @radius * 2 * :math.asin(:math.sqrt(a))
  end
end

M lib/triglav/schemas/error.ex => lib/triglav/schemas/error.ex +48 -8
@@ 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),

M lib/triglav/zet/errors.ex => lib/triglav/zet/errors.ex +18 -1
@@ 8,6 8,7 @@ defmodule Triglav.Zet.Errors do
  def list_unresolved(opts \\ []) do
    route_id = Keyword.get(opts, :route_id)

    # TODO: see if we can render error without preloading platform
    from(e in Error, where: is_nil(e.resolved_at))
    |> maybe_filter_by_route(route_id)
    |> Repo.all()


@@ 59,7 60,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 70,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 +38 -5
@@ 2,14 2,15 @@ defmodule Triglav.Zet.Validator do
  @moduledoc """
  Validates ZET routes in OSM based on ZET GTFS data.
  """
  alias Triglav.Derived.PublicTransport
  alias Triglav.Repo
  alias Triglav.Schemas.Error
  alias Triglav.Schemas.Osmosis.Relation
  alias Triglav.Schemas.Osmosis.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 45,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 220,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 316,37 @@ 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",
      # "highway" => "bus_stop",
      "public_transport" => "platform"
    }
  end

  defp expected_platform_tags(relation) do
    IO.inspect(relation.tags, label: "unmatched")
  end

  #
  # Generic relation validators
  #


@@ 343,7 376,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

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