~ihabunek/triglav

ref: 211705a54625aa10be1c420076a6e6065ffbbcd3 triglav/lib/triglav/osm/router.ex -rw-r--r-- 3.5 KiB
211705a5Ivan Habunek Don't break when relation has no platform members 10 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
defmodule Triglav.Osm.Router do
  alias Triglav.Osm
  alias Triglav.Repo
  alias Triglav.Schemas.Osmosis.{Node, Relation, Way}

  import Ecto.Query

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

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

    def backward(way) do
      %AnnotatedWay{
        way: way,
        direction: :backward,
        node_ids: Enum.reverse(way.node_ids)
      }
    end

    def custom(way, node_ids) do
      %AnnotatedWay{
        way: way,
        direction: :custom,
        node_ids: node_ids
      }
    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.
  """
  @spec list_nodes([Way.t()]) :: {:ok, [Node.t()]} | {:error, any()}
  def list_nodes(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()}
    end
  end

  defp annotate_ways(ways), do: annotate_ways(ways, [])

  defp annotate_ways([], annotated_ways), do: {:ok, Enum.reverse(annotated_ways)}

  # For the first way in relation, check out the second way to determine direction
  defp annotate_ways(ways, []) do
    [w1, w2 | _] = ways

    cond do
      List.last(w1.node_ids) in w2.node_ids ->
        annotate_ways(tl(ways), [AnnotatedWay.forward(w1)])

      List.first(w1.node_ids) in w2.node_ids ->
        annotate_ways(tl(ways), [AnnotatedWay.backward(w1)])

      true ->
        {:error, {:ways_not_connected, w1, w2}}
    end
  end

  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])
      end
    else
      common_node_id = List.last(annotated_w1.node_ids)

      cond do
        common_node_id == List.first(w2.node_ids) ->
          annotate_ways(ways, [AnnotatedWay.forward(w2) | annotated_ways])

        common_node_id == List.last(w2.node_ids) ->
          annotate_ways(ways, [AnnotatedWay.backward(w2) | annotated_ways])

        true ->
          {:error, {:ways_not_connected, annotated_w1.way, w2}}
      end
    end
  end

  @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
    entry_node_id = List.last(annotated_prev_way.node_ids)

    first_node_id = List.first(next_way.node_ids)
    last_node_id = List.last(next_way.node_ids)

    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
        true -> nil
      end

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

      is_nil(exit_node_id) ->
        {:error, {:ways_not_connected, roundabout_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))

        {:ok, node_ids ++ [exit_node_id]}
    end
  end
end