~ihabunek/triglav

888c69d3072002c8f828439db412a79fdb9b75d3 — Ivan Habunek 1 year, 8 months ago 8a24bb6 derived-relations
Update validation to use new data
M lib/triglav/html.ex => lib/triglav/html.ex +14 -3
@@ 70,9 70,6 @@ defimpl Phoenix.HTML.Safe, for: Triglav.PublicTransport.Schemas.Error do
      }),
      do: ~s(Unexpected role "#{member_role}" on relation member #{member_type}#{member_id})

  def to_iodata(%{key: "platform_too_far_from_route", params: %{"sequence_no" => sequence_no, "distance" => distance}}),
    do: "Platform #{sequence_no} is #{round(distance)}m from route"

  def to_iodata(%{key: "relation_not_contained_in_route_master"}),
    do: "Route relation not contained in route master"



@@ 85,6 82,20 @@ defimpl Phoenix.HTML.Safe, for: Triglav.PublicTransport.Schemas.Error do
    """
  end

  def to_iodata(%{
        key: "platform_too_far_from_route",
        sequence_id: sequence_id,
        params: %{"way_id" => way_id, "node_id" => node_id, "distance" => distance}
      }) do
    object = if way_id, do: "w#{way_id}", else: "n#{node_id}"
    href = "http://127.0.0.1:8111/load_object?objects=#{object}"

    """
    Platform #{sequence_id + 1} is #{round(distance)}m from route.
    <a class="josm-remote" href="#{href}" target="josm">🜨 Load</a>
    """
  end

  def to_iodata(%{key: key, params: params}),
    do: "MISSING ERROR CAPTION: #{key} #{inspect(params)}"
end

M lib/triglav/osm/router.ex => lib/triglav/osm/router.ex +1 -1
@@ 7,7 7,7 @@ defmodule Triglav.Osm.Router do
            way: Way.t(),
            direction: :forward | :backward,
            node_ids: [Node.id()],
            linestring: Linestring.t()
            linestring: Geo.LineString.t()
          }

    defstruct [:way, :direction, :node_ids, :linestring]

M lib/triglav/public_transport.ex => lib/triglav/public_transport.ex +3 -1
@@ 3,7 3,7 @@ defmodule Triglav.PublicTransport do
  alias Triglav.Import.Geofabrik
  alias Triglav.Osm.Router
  alias Triglav.PublicTransport.Schemas.{Error, ErrorHistory, Feed, Platform, Operator, Route, RouteVariant}
  alias Triglav.PublicTransport.{Mappings, Validation}
  alias Triglav.PublicTransport.{Derived, Mappings, Validation}
  alias Triglav.Repo
  alias Triglav.Schemas.Osmosis.Relation



@@ 34,6 34,8 @@ defmodule Triglav.PublicTransport do
  called after updating OSM data.
  """
  def regenerate_derived_data() do
    Derived.generate_all()

    Repo.all(where(Feed, active: true))
    |> Enum.each(fn feed ->
      generate_mappings(feed.id)

M lib/triglav/public_transport/validation.ex => lib/triglav/public_transport/validation.ex +63 -52
@@ 3,13 3,12 @@ defmodule Triglav.PublicTransport.Validation do
  Validates public transport routes in OSM based on imported data.
  """
  alias Triglav.Osm
  alias Triglav.PublicTransport.Schemas.{Error, Feed}
  alias Triglav.PublicTransport.Schemas.{Error, Feed, RouteRelation}
  alias Triglav.PublicTransport.Validation.Errors
  alias Triglav.Repo
  alias Triglav.Schemas.Osmosis.Relation

  # Maximum distance from platform to route
  @max_platform_distance_from_route 20
  import Ecto.Query

  @spec validate(Feed.id()) :: [Error.t()]
  def validate(feed_id) do


@@ 19,40 18,54 @@ defmodule Triglav.PublicTransport.Validation do
      |> Repo.preload([
        :operator,
        routes: [
          [variants: [platforms: [platform: [mappings: [:node, :way]]]]],
          :variants,
          [mapped_route_relations: :members],
          [mapped_route_master_relations: :members]
        ]
      ])

    # TODO: this is slow, seek to avoid it
    route_relations =
      RouteRelation
      |> where(operator_id: ^feed.operator_id)
      |> Repo.all()
      |> Repo.preload([:relation, platforms: [:node, :way]])
      |> Map.new(&{&1.id, &1})

    relation_map =
      feed.routes
      |> Enum.flat_map(& &1.mapped_route_relations)
      |> Osm.preload_members()
      route_relations
      |> Map.values()
      |> Enum.map(& &1.relation)
      |> Osm.preload_members(&(&1.member_role == "" and &1.member_type == "W"))
      |> Map.new(&{&1.id, &1})

    # # TODO: this is slow, seek to avoid it
    # relation_map =
    #   feed.routes
    #   |> Enum.flat_map(& &1.mapped_route_relations)
    #   |> Osm.preload_members()
    #   |> Map.new(&{&1.id, &1})

    feed
    |> validate_routes(relation_map)
    |> validate_routes(relation_map, route_relations)
    |> List.flatten()
    |> Enum.reject(&is_nil/1)
  end

  defp validate_routes(feed, relation_map) do
  defp validate_routes(feed, relation_map, route_relations) do
    for route <- feed.routes do
      validate_route(feed, route, relation_map)
      validate_route(feed, route, relation_map, route_relations)
    end
  end

  defp validate_route(feed, route, relation_map) do
  defp validate_route(feed, route, relation_map, route_relations) do
    [
      validate_has_relations(feed, route),
      validate_has_route_master(feed, route),
      validate_routes_are_contained_in_route_master(feed, route),
      validate_routes_master_route_has_no_unknown_members(feed, route),
      validate_platform_tags(feed, route, relation_map),
      validate_platform_tags(feed, route, route_relations),
      Enum.map(route.mapped_route_master_relations, &validate_route_master_relation(feed, route, &1)),
      Enum.map(route.mapped_route_relations, &validate_route_relation(feed, route, &1, relation_map))
      Enum.map(route.mapped_route_relations, &validate_route_relation(feed, route, &1, relation_map, route_relations))
    ]
  end



@@ 140,7 153,7 @@ defmodule Triglav.PublicTransport.Validation do
    ]
  end

  defp validate_route_relation(feed, route, relation, relation_map) do
  defp validate_route_relation(feed, route, relation, relation_map, route_relations) do
    required_tags = ["type", "route", "ref", "from", "to", "name"]

    allowed_tags =


@@ 168,11 181,11 @@ defmodule Triglav.PublicTransport.Validation do
      validate_required_tags(feed, route, relation, required_tags),
      validate_allowed_tags(feed, route, relation, allowed_tags),
      validate_tag_values(feed, route, relation, expected_tags),
      validate_continuous_ways(feed, route, relation, relation_map),
      validate_continuous_ways(feed, route, relation, route_relations),
      validate_oneway_streets(feed, route, relation, relation_map),
      validate_has_gtfs_stop_ids(feed, route, relation, relation_map),
      validate_member_roles(feed, route, relation)
      # validate_platform_distance_from_route(feed, route, relation)
      validate_has_gtfs_stop_ids(feed, route, relation, route_relations),
      validate_member_roles(feed, route, relation),
      validate_platform_distance_from_route(feed, route, relation, route_relations)
    ]
  end



@@ 207,22 220,11 @@ defmodule Triglav.PublicTransport.Validation do

  # If trip has an exact linestring, that means route is complete, so only
  # run this check if exact is false.
  defp validate_continuous_ways(feed, route, relation, relation_map) do
    relation = Map.fetch!(relation_map, relation.id)

    ordered_ways =
      relation.members
      |> Enum.filter(&(&1.member_type == "W" and &1.member_role == ""))
      |> Enum.map(& &1.member)

    router_result = Triglav.Osm.Router.route_ways(ordered_ways)

    case router_result do
      {:ok, _} ->
        nil
  defp validate_continuous_ways(feed, route, relation, route_relations) do
    relation = Map.fetch!(route_relations, relation.id)

      {:error, {:ways_not_connected, _w1, _w2}} ->
        Errors.broken_route(feed, route, relation)
    if not relation.contiguous do
      Errors.broken_route(feed, route, relation.relation)
    end
  end



@@ 255,13 257,9 @@ defmodule Triglav.PublicTransport.Validation do
    end
  end

  defp validate_has_gtfs_stop_ids(feed, route, relation, relation_map) do
    relation = Map.fetch!(relation_map, relation.id)

    empty_count =
      relation.members
      |> Enum.filter(&String.starts_with?(&1.member_role, "platform"))
      |> Enum.count(&(Map.get(&1.member.tags, "gtfs:stop_id", "") == ""))
  defp validate_has_gtfs_stop_ids(feed, route, relation, route_relations) do
    route_relation = Map.fetch!(route_relations, relation.id)
    empty_count = Enum.count(route_relation.platforms, &is_nil(&1.stop_id))

    if empty_count > 0 do
      Errors.relation_missing_gtfs_stop_ids(feed, route, relation, empty_count)


@@ 295,24 293,37 @@ defmodule Triglav.PublicTransport.Validation do
    end
  end

  # defp validate_platform_distance_from_route(feed, route, relation) do
  #   for platform <- platforms, platform.distance_from_route > @max_platform_distance_from_route do
  #     Error.platform_too_far_from_route(route, relation, platform)
  #   end
  # end
  defp validate_platform_distance_from_route(feed, route, relation, route_relations) do
    route_relation = Map.fetch!(route_relations, relation.id)

    for platform <- route_relation.platforms do
      if platform.distance_from_route > 25 do
        Errors.platform_too_far_from_route(feed, route, relation, platform)
      end
    end
  end

  defp validate_platform_tags(feed, route, relation_map) do
  defp validate_platform_tags(feed, route, route_relations) do
    for relation <- route.mapped_route_relations do
      relation = Map.fetch!(relation_map, relation.id)
      route_relation = Map.fetch!(route_relations, relation.id)

      for member <- relation.members, String.starts_with?(member.member_role, "platform") do
        expected_tags = expected_platform_tags(relation)
      for platform <- route_relation.platforms do
        expected_tags = expected_platform_tags(route_relation.relation)
        member = platform.way || platform.node

        for {name, expected} <- expected_tags do
          actual = Map.get(member.member.tags, name)
          actual = Map.get(member.tags, name)

          if actual != expected do
            Errors.platform_invalid_tag_value(feed, route, relation, member.sequence_id, name, actual, expected)
            Errors.platform_invalid_tag_value(
              feed,
              route,
              relation,
              platform.sequence_id,
              name,
              actual,
              expected
            )
          end
        end
      end

M lib/triglav/public_transport/validation/errors.ex => lib/triglav/public_transport/validation/errors.ex +10 -6
@@ 1,7 1,7 @@
defmodule Triglav.PublicTransport.Validation.Errors do
  alias Triglav.PublicTransport.Schemas.Error
  alias Triglav.PublicTransport.Schemas.{Feed, Route}
  alias Triglav.Schemas.Osmosis.{Relation, RelationMember}
  alias Triglav.PublicTransport.Schemas.{Feed, Route, RouteRelationPlatform}
  alias Triglav.Schemas.Osmosis.{Relation, RelationMember, Way}

  @spec no_relations(Feed.t(), Route.t()) :: Error.t()
  def no_relations(feed, route) do


@@ 183,16 183,20 @@ defmodule Triglav.PublicTransport.Validation.Errors do
    }
  end

  @spec platform_too_far_from_route(Feed.t(), Route.t(), Relation.t(), integer(), float()) :: Error.t()
  def platform_too_far_from_route(feed, route, relation, sequence_id, distance) do
  @spec platform_too_far_from_route(Feed.t(), Route.t(), Relation.t(), RouteRelationPlatform.t()) :: Error.t()
  def platform_too_far_from_route(feed, route, relation, platform) do
    %Error{
      key: "platform_too_far_from_route",
      operator_id: feed.operator_id,
      feed_id: feed.id,
      route_ref: route.ref,
      relation_id: relation.id,
      sequence_id: sequence_id,
      params: %{distance: distance}
      sequence_id: platform.sequence_id,
      params: %{
        distance: platform.distance_from_route,
        node_id: platform.node_id,
        way_id: platform.way_id
      }
    }
  end
end