~ihabunek/triglav

f4ed6478c8860ff744dfb772b85ffce547b37129 — Ivan Habunek 1 year, 22 days ago 0751e8c
Add continuous route validation
A lib/triglav/schemas/osm/way.ex => lib/triglav/schemas/osm/way.ex +12 -0
@@ 0,0 1,12 @@
defmodule Triglav.Schemas.Osm.Way do
  use Ecto.Schema

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

  schema "planet_osm_ways" do
    field :id, :integer, primary_key: true
    field :nodes, {:array, :integer}
    field :tags, {:array, :string}
  end
end

M lib/triglav/zet/osm.ex => lib/triglav/zet/osm.ex +22 -1
@@ 1,6 1,7 @@
defmodule Triglav.Zet.Osm do
  alias Triglav.Repo
  alias Triglav.Schemas.Osm.Relation
  alias Triglav.Schemas.Osm.Way
  import Ecto.Query

  def list_public_transport_relations(opts \\ []) do


@@ 10,6 11,26 @@ defmodule Triglav.Zet.Osm do
    |> Enum.map(&process_members/1)
  end

  @doc """
  For a given list of relations returns all included members of the :way variety
  which do not have a role (to skip stations entered as ways). Returns them as
  a map indexed by way ID for easier lookup.
  """
  def list_ways(relations) do
    ids =
      relations
      |> Enum.map(fn r ->
        r.members
        |> Enum.filter(&(&1.type == :way and &1.role == ""))
        |> Enum.map(& &1.id)
      end)
      |> List.flatten()

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

  # Private

  defp pt_relations() do


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

  # Consider moving this to the database
  defp process_members(rel) do
  def 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 +36 -23
@@ 3,34 3,20 @@ defmodule Triglav.Zet.Validator do
  Validates ZET routes in OSM based on ZET GTFS data.
  """
  alias Triglav.Schemas.Osm.Relation
  alias Triglav.Schemas.Osm.Way
  alias Triglav.Schemas.Zet.Route
  alias Triglav.Zet.Gtfs
  alias Triglav.Zet.Osm

  @type error :: binary

  @spec validate_all() :: [error]
  def validate_all() do
    routes = Gtfs.list_routes()

    relations =
      Osm.list_public_transport_relations()
      |> Enum.group_by(& &1.tags["ref"])

    for route <- routes do
      validate_route(route, Map.get(relations, route.id, []))
    end
  end

  @spec validate_route(Route.t(), [Relation.t()]) :: [error]
  def validate_route(route, relations) do
  @spec validate_route(Route.t(), [Relation.t()], %{required(integer) => Way.t()}) :: [error]
  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),
      Enum.map(relations, &validate_route_master_relation(route, &1)),
      Enum.map(relations, &validate_route_relation(route, &1)),
      Enum.map(relations, &validate_route_relation(route, &1, ways)),
    ]
    |> List.flatten()
    |> Enum.filter(& &1)


@@ 113,7 99,7 @@ defmodule Triglav.Zet.Validator do

  defp validate_route_master_relation(_, _), do: nil

  defp validate_route_relation(%Route{} = route, %Relation{type: "route"} = relation) do
  defp validate_route_relation(%Route{} = route, %Relation{type: "route"} = relation, ways) do
    required_tags = ["type", "route", "ref", "operator", "from", "to", "name"]
    allowed_tags = Enum.concat(required_tags, ["public_transport:version", "roundtrip", "via", "network"])
    expected_tags = %{


@@ 123,16 109,22 @@ defmodule Triglav.Zet.Validator do
    }

    [
      (if !is_ptv2(relation),
        do: {relation, "Not updated to [public_transport:version=2]"}),
      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_tag_values(relation, expected_tags),
      validate_continuous_ways(relation, ways)
    ]
  end

  defp validate_route_relation(_, _), do: nil
  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]"}
    end
  end

  defp validate_route_naming(rel) do
    if has_tags(rel, ["name", "ref", "from", "to"]) do


@@ 156,6 148,27 @@ defmodule Triglav.Zet.Validator do
    end
  end

  @doc """
  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
    relation.members
    |> Enum.filter(&(&1.type == :way and &1.role == ""))  # Skip stops
    |> Enum.map(fn member ->
      Map.fetch!(ways, member.id)
    end)
    |> Enum.chunk_every(2, 1)
    |> Enum.find_value(fn
        [_] -> nil

        [a, b] ->
          if List.last(a.nodes) != List.first(b.nodes) do
            {relation, "Broken route starting at way #{b.id}"}
          end
      end)
  end

  #
  # Generic relation validators
  #

M lib/triglav_web/controllers/zet/routes_controller.ex => lib/triglav_web/controllers/zet/routes_controller.ex +4 -2
@@ 9,6 9,7 @@ defmodule TriglavWeb.Zet.RoutesController do
  def index(conn, _params) do
    routes = Gtfs.list_routes()
    relations = Osm.list_public_transport_relations()
    ways = Osm.list_ways(relations)
    osm_info = DataImport.load_state()
    gtfs_info = Gtfs.get_feed_info()



@@ 16,7 17,7 @@ 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)
        errors = Validator.validate_route(route, route_relations, ways)
        {route, hierarchy, errors}
      end



@@ 40,8 41,9 @@ 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 = Validator.validate_route(route, relations)
    errors = Validator.validate_route(route, relations, ways)

    trips =
      Gtfs.fetch_distinct_trips(route.id)