~ihabunek/triglav

de1e14586f097040c360c40de6b86a1debb09005 — Ivan Habunek 7 months ago 78bb75d
Don't fetch nodes for routing
M lib/triglav/geo_json.ex => lib/triglav/geo_json.ex +2 -8
@@ 55,14 55,8 @@ defmodule Triglav.GeoJSON do

  @spec ways_to_multilinestring([Way.t()]) :: map()
  def ways_to_multilinestring(ways) do
    coordinates =
      Enum.map(ways, fn way ->
        Enum.map(way.linestring.coordinates, &Tuple.to_list/1)
      end)

    %{
      type: "MultiLineString",
      coordinates: coordinates
    %Geo.MultiLineString{
      coordinates: Enum.map(ways, & &1.linestring.coordinates)
    }
  end
end

M lib/triglav/osm/router.ex => lib/triglav/osm/router.ex +52 -34
@@ 1,16 1,16 @@
defmodule Triglav.Osm.Router do
  alias Triglav.Osm
  alias Triglav.Schemas.Osmosis.Node
  alias Geo.LineString
  alias Triglav.Schemas.Osmosis.Way

  defmodule AnnotatedWay do
    defstruct [:way, :direction, :node_ids]
    defstruct [:way, :direction, :node_ids, :linestring]

    def forward(way) do
      %AnnotatedWay{
        way: way,
        direction: :forward,
        node_ids: way.node_ids
        node_ids: way.node_ids,
        linestring: way.linestring
      }
    end



@@ 18,34 18,35 @@ defmodule Triglav.Osm.Router do
      %AnnotatedWay{
        way: way,
        direction: :backward,
        node_ids: Enum.reverse(way.node_ids)
        node_ids: Enum.reverse(way.node_ids),
        linestring: Map.update!(way.linestring, :coordinates, &Enum.reverse/1)
      }
    end

    def custom(way, node_ids) do
    def custom(way, node_ids, linestring) do
      %AnnotatedWay{
        way: way,
        direction: :custom,
        node_ids: node_ids
        node_ids: node_ids,
        linestring: linestring
      }
    end
  end

  @doc """
  Given a list of ordered ways returns an ordered list of nodes contained in them.

  TODO: This could be changed to return a list of coordinates read from
  `way.linestring` so nodes don't need to be loaded at all.
  Given a list of ordered, consecutive ways returns a LineString containing all
  points through which the ways pass. Fails if ways are not consecutive.
  """
  @spec list_nodes([Way.t()]) :: {:ok, [Node.t()]} | {:error, any()}
  def list_nodes(ordered_ways) do
  @spec route_ways([Way.t()]) :: {:ok, LineString.t()} | {:error, any()}
  def route_ways(ordered_ways) do
    with {:ok, annotated_ways} <- annotate_ways(ordered_ways) do
      {:ok,
       annotated_ways
       |> Enum.map(& &1.node_ids)
       |> Enum.concat()
       |> Enum.dedup()
       |> Osm.list_ordered_nodes()}
      coordinates =
        annotated_ways
        |> Enum.map(& &1.linestring.coordinates)
        |> Enum.concat()
        |> Enum.dedup()

      {:ok, %Geo.LineString{coordinates: coordinates}}
    end
  end



@@ 71,8 72,8 @@ defmodule Triglav.Osm.Router do

  defp annotate_ways([w2 | ways], [annotated_w1 | _] = annotated_ways) do
    if Way.is_whole_roundabout?(w2) do
      with {:ok, node_ids} <- get_roundabout_segment(w2, annotated_w1, hd(ways)) do
        annotate_ways(ways, [AnnotatedWay.custom(w2, node_ids) | annotated_ways])
      with {:ok, node_ids, linestring} <- get_roundabout_segment(w2, annotated_w1, hd(ways)) do
        annotate_ways(ways, [AnnotatedWay.custom(w2, node_ids, linestring) | annotated_ways])
      end
    else
      common_node_id = List.last(annotated_w1.node_ids)


@@ 90,9 91,11 @@ defmodule Triglav.Osm.Router do
    end
  end

  # Given a way which is a full roundabout (circular, same first and last node)
  # returns a segment which connects the previous and next way.
  @spec get_roundabout_segment(Way.t(), AnnotatedWay.t(), Way.t()) ::
          {:ok, [integer()]} | {:error, any()}
  defp get_roundabout_segment(roundabout_way, annotated_prev_way, next_way) do
          {:ok, [integer()], LineString.t()} | {:error, any()}
  defp get_roundabout_segment(way, annotated_prev_way, next_way) do
    entry_node_id = List.last(annotated_prev_way.node_ids)

    first_node_id = List.first(next_way.node_ids)


@@ 100,26 103,41 @@ defmodule Triglav.Osm.Router do

    exit_node_id =
      cond do
        first_node_id in roundabout_way.node_ids -> first_node_id
        last_node_id in roundabout_way.node_ids -> last_node_id
        first_node_id in way.node_ids -> first_node_id
        last_node_id in way.node_ids -> last_node_id
        true -> nil
      end

    cond do
      entry_node_id not in roundabout_way.node_ids ->
        {:error, {:ways_not_connected, annotated_prev_way.way, roundabout_way}}
      entry_node_id not in way.node_ids ->
        {:error, {:ways_not_connected, annotated_prev_way.way, way}}

      is_nil(exit_node_id) ->
        {:error, {:ways_not_connected, roundabout_way, next_way}}
        {:error, {:ways_not_connected, way, next_way}}

      true ->
        node_ids =
          roundabout_way.node_ids
          |> Stream.cycle()
          |> Stream.drop_while(&(&1 != entry_node_id))
          |> Enum.take_while(&(&1 != exit_node_id))
        entry_index = Enum.find_index(way.node_ids, &(&1 == entry_node_id))
        exit_index = Enum.find_index(way.node_ids, &(&1 == exit_node_id))

        node_ids = slice_roundabout(way.node_ids, entry_index, exit_index)
        coordinates = slice_roundabout(way.linestring.coordinates, entry_index, exit_index)
        linestring = %Geo.LineString{coordinates: coordinates}

        {:ok, node_ids, linestring}
    end
  end

  defp slice_roundabout(items, start_index, end_index) do
    if start_index <= end_index do
      Enum.slice(items, start_index..end_index)
    else
      count = end_index + length(items) - start_index

        {:ok, node_ids ++ [exit_node_id]}
      items
      |> Stream.cycle()
      |> Stream.dedup()
      |> Stream.drop(start_index)
      |> Enum.take(count)
    end
  end
end

M lib/triglav_web/controllers/zet/routes_controller.ex => lib/triglav_web/controllers/zet/routes_controller.ex +4 -7
@@ 224,14 224,11 @@ defmodule TriglavWeb.Zet.RoutesController do
  defp route_feature(relation, color) do
    ordered_ways = Osm.list_ordered_ways(relation)

    # Attempt to get route as a single linestring, fall back to route segments
    route_geometry =
      case Router.list_nodes(ordered_ways) do
        {:ok, nodes} ->
          GeoJSON.nodes_to_linestring(nodes)

        # fallback if routing fails, e.g. because of a broken route
        {:error, _} ->
          GeoJSON.ways_to_multilinestring(ordered_ways)
      case Router.route_ways(ordered_ways) do
        {:ok, linestring} -> linestring
        {:error, _} -> GeoJSON.ways_to_multilinestring(ordered_ways)
      end

    GeoJSON.feature(route_geometry, %{