~ihabunek/triglav

ffdb2224a08bc7a957a2c68df2c5139f0e1115ea — Ivan Habunek 1 year, 7 months ago 699daff router
Router WIP
M assets/js/routes.js => assets/js/routes.js +39 -7
@@ 1,5 1,13 @@
import "leaflet/dist/leaflet.css"
import { Map, TileLayer, Marker, Icon, FeatureGroup } from "leaflet"
import {
  CircleMarker,
  Control,
  FeatureGroup,
  Icon,
  Map,
  Marker,
  TileLayer,
} from "leaflet"

const map = new Map("routes-map").setView([45.8, 16], 10)



@@ 10,18 18,42 @@ const tiles = new TileLayer(tilesUrl, { attribution })

tiles.addTo(map)

const ways = JSON.parse(document.getElementById("ways-geojson").innerHTML)
const stops = JSON.parse(document.getElementById("stops-json").innerHTML)
const icon = new Icon({ iconUrl: "/images/stop.svg", iconSize: [14, 14] })

const waysGeoJSON = JSON.parse(document.getElementById("ways-geojson").innerHTML)
L.geoJSON(waysGeoJSON).addTo(map)

const markers = []
for (const stop of stops) {
  const marker = new Marker(stop, { icon: icon, title: stop.name })
  markers.push(marker)
}

const group = new FeatureGroup(markers)
group.addTo(map)
map.fitBounds(group.getBounds())
const zetStops = new FeatureGroup(markers).addTo(map)

const layers = { "ZET Stops": zetStops }

for (const name of Object.keys(ways)) {
  const way = L.geoJSON(ways[name], {
    style: feature => ({
      color: feature.properties.color
    }),
    pointToLayer: (feature, latlng) => {
      return new CircleMarker(latlng, {
        radius: 8,
        title: feature.properties.name,
      }).bindPopup(`
        <b>${feature.properties.name}</b><br/>
        Relation: ${feature.properties.relation_id}<br/>
        Sequence: ${feature.properties.sequence_id}<br/>
        GTFS ID: ${feature.properties.gtfs_stop_id || "MISSING"}
      `)
    }
  })

  way.addTo(map)
  layers[name] = way
}

new Control.Layers({}, layers).addTo(map)

map.fitBounds(zetStops.getBounds())

A lib/triglav/geo_json.ex => lib/triglav/geo_json.ex +115 -0
@@ 0,0 1,115 @@
defmodule Triglav.GeoJSON do
  alias Triglav.Osm.Router
  alias Triglav.Schemas.Osmosis.{Relation, Way, Node}

  @error_color "#eb4d4b"

  @route_colors [
    light_blue: "#3498db",
    purple: "#8e44ad",
    green: "#27ae60",
    dark_blue: "#3742fa",
    orange: "#d35400",
    dark_gray: "#2c3e50"
  ]

  @spec route_relation_feature(Relation.t(), map()) :: map()
  def route_relation_feature(relation, properties \\ %{}) do
    geometry = relation |> Router.list_nodes() |> nodes_to_linestring()

    default_properties = %{
      id: relation.tags["id"],
      type: relation.tags["type"]
    }

    combined_properties = Map.merge(default_properties, properties)

    feature(geometry, combined_properties)
  end

  @spec route_relations_to_feature_collections([Relation.t()], [Way.t() | Node.t()]) :: map()
  def route_relations_to_feature_collections(relations, platform_members) do
    colors = @route_colors |> Enum.map(fn {_, v} -> v end) |> Stream.cycle()

    relations
    |> Enum.filter(&Relation.is_route/1)
    |> Enum.zip(colors)
    |> Map.new(fn {relation, color} ->
      name = "#{relation.tags["name"]} (##{relation.id})"
      route_feature = route_relation_feature(relation, %{color: color})

      platform_features =
        platform_members
        |> Map.get(relation.id)
        |> Enum.map(fn member ->
          stop_id =
            member.member.tags["gtfs:stop_id"]
            |> IO.inspect()

          platform_color = if stop_id, do: color, else: @error_color

          to_feature(member.member, %{
            name: member.member.tags["name"],
            relation_id: member.relation_id,
            sequence_id: member.sequence_id,
            gtfs_stop_id: stop_id,
            color: platform_color
          })
        end)

      {name, feature_collection([route_feature | platform_features])}
    end)
  end

  def to_feature(object, props \\ %{})

  def to_feature(%Way{} = way, props) do
    props = Map.merge(%{id: way.id, type: "W"}, props)
    feature(geometry(way), props)
  end

  def to_feature(%Node{} = node, props) do
    props = Map.merge(%{id: node.id, type: "N"}, props)

    feature(geometry(node), props)
  end

  defp geometry(%Node{} = node) do
    %{
      type: "Point",
      coordinates: Tuple.to_list(node.geom.coordinates)
    }
  end

  defp geometry(%Way{} = way) do
    %{
      type: "LineString",
      coordinates: Enum.map(way.linestring.coordinates, &Tuple.to_list/1)
    }
  end

  def feature(geometry, properties \\ %{}) do
    %{
      type: "Feature",
      properties: properties,
      geometry: geometry
    }
  end

  def feature_collection(features) do
    %{
      type: "FeatureCollection",
      features: features
    }
  end

  @spec nodes_to_linestring([Node.t()]) :: map()
  def nodes_to_linestring(nodes) do
    coordinates = Enum.map(nodes, &Tuple.to_list(&1.geom.coordinates))

    %{
      type: "LineString",
      coordinates: coordinates
    }
  end
end

A lib/triglav/osm.ex => lib/triglav/osm.ex +18 -0
@@ 0,0 1,18 @@
defmodule Triglav.Osm do
  alias Triglav.Repo
  alias Triglav.Schemas.Osmosis.Node
  import Ecto.Query

  @doc """
  Returns a list of nodes with given ids, ordered the same as the given ids.
  """
  @spec list_ordered_nodes([integer()]) :: [Node.t()]
  def list_ordered_nodes(node_ids) do
    node_map =
      from(n in Node, where: n.id in ^node_ids)
      |> Repo.all()
      |> Map.new(&{&1.id, &1})

    Enum.map(node_ids, &Map.get(node_map, &1))
  end
end

A lib/triglav/osm/router.ex => lib/triglav/osm/router.ex +139 -0
@@ 0,0 1,139 @@
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 relation of `type=route` returns an ordered list of nodes contained in
  ways which are members of the relation. Only considers ways which are entered
  without a role.
  """
  @spec list_nodes(Relation.t()) :: [Node.t()] | {:error, any()}
  def list_nodes(%Relation{} = relation) do
    way_ids =
      relation
      |> Repo.preload(:members)
      |> Map.get(:members)
      |> Enum.filter(&(&1.member_type == "W" and &1.member_role == ""))
      |> Enum.map(& &1.member_id)

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

    ordered_ways = Enum.map(way_ids, &Map.get(ways, &1))

    with {:ok, annotated_ways} <- annotate_ways(ordered_ways) do
      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

M lib/triglav_web/controllers/zet/routes_controller.ex => lib/triglav_web/controllers/zet/routes_controller.ex +2 -26
@@ 1,6 1,7 @@
defmodule TriglavWeb.Zet.RoutesController do
  use TriglavWeb, :controller

  alias Triglav.GeoJSON
  alias Triglav.Import.Geofabrik
  alias Triglav.Schemas.Osmosis.Relation
  alias Triglav.Zet.Errors


@@ 97,7 98,7 @@ defmodule TriglavWeb.Zet.RoutesController do
      |> Jason.encode!()

    ways_geojson =
      ways_geojson_feature_collection(relations)
      GeoJSON.route_relations_to_feature_collections(relations, platform_members)
      |> Jason.encode!()

    render(conn, "detail.html",


@@ 114,31 115,6 @@ defmodule TriglavWeb.Zet.RoutesController do
    )
  end

  defp ways_geojson_feature_collection(relations) do
    features =
      for {relation, idx} <- Enum.with_index(relations), Relation.is_route(relation) do
        coordinates =
          Osmosis.list_ways(relations, geojson: true)
          |> Enum.map(&(&1.geojson |> Jason.decode!() |> Map.get("coordinates")))

        %{
          type: "Feature",
          properties: %{
            idx: idx
          },
          geometry: %{
            type: "MultiLineString",
            coordinates: coordinates
          }
        }
      end

    %{
      type: "FeatureCollection",
      features: features
    }
  end

  defp make_hierarchy(relations) do
    {route_masters, routes} = Enum.split_with(relations, &(Relation.type(&1) == "route_master"))