~ihabunek/triglav

26f2c3b921aea5c0e33d1f7c4320e9e6ceb08ea0 — Ivan Habunek 3 months ago 1709b44
Calculate and store derived data for public transport
A lib/mix/tasks/triglav/generate_derived_data.ex => lib/mix/tasks/triglav/generate_derived_data.ex +15 -0
@@ 0,0 1,15 @@
defmodule Mix.Tasks.Triglav.GenerateDerivedData do
  use Mix.Task

  @shortdoc "(re)generates data derived from imported OSM and GTFS data"

  @impl Mix.Task
  def run(_) do
    Application.put_env(:triglav, :repo_only, true)
    {:ok, _} = Application.ensure_all_started(:triglav)

    {time, _} = :timer.tc(&Triglav.Derived.PublicTransport.generate/0)
    time_seconds = :erlang.float_to_binary(time / 1_000_000, decimals: 2)
    IO.puts("Done. Took #{time_seconds} seconds")
  end
end

A lib/triglav/derived/public_transport.ex => lib/triglav/derived/public_transport.ex +123 -0
@@ 0,0 1,123 @@
defmodule Triglav.Derived.PublicTransport do
  @moduledoc """
  Generates derived data for public transport.
  """

  alias Ecto.Multi
  alias Triglav.GeoJSON
  alias Triglav.Osm.Router
  alias Triglav.Repo
  alias Triglav.Schemas.Osmosis.Relation
  alias Triglav.Schemas.PublicTransport.{Trip, Platform}
  alias Triglav.Zet.Gtfs
  alias Triglav.Zet.Osmosis

  def generate() do
    relations = Osmosis.list_public_transport_relations(members: true, type: "route")
    relations_map = Map.new(relations, &{&1.id, &1})
    ways_map = Osmosis.list_ways(relations) |> Map.new(&{&1.id, &1})
    routes_map = Gtfs.list_routes() |> Map.new(&{&1.id, &1})
    platform_members = relations |> Enum.map(& &1.id) |> Osmosis.list_platform_members()
    platform_member_map = Map.new(platform_members, &{{&1.relation_id, &1.sequence_id}, &1})

    trips = Enum.map(relations, &generate_trip(&1, ways_map, routes_map, platform_member_map))

    stops_map =
      trips
      |> Enum.map(& &1.stop_ids)
      |> Enum.concat()
      |> Gtfs.list_stops_by_id()
      |> Map.new(&{&1.id, &1})

    platforms =
      platform_members
      |> Enum.sort_by(&{&1.relation_id, &1.sequence_id})
      |> Enum.map(&generate_platform(&1, stops_map, routes_map, relations_map))

    persist(trips, platforms)
    nil
  end

  defp persist(trips, platforms) do
    Multi.new()
    |> Multi.delete_all(:delete_trips, Trip)
    |> persist_trips(trips)
    |> Multi.delete_all(:delete_platforms, Platform)
    |> persist_platforms(platforms)
    |> Repo.transaction()
  end

  defp persist_trips(multi, trips) do
    Enum.reduce(trips, multi, fn trip, multi ->
      Multi.insert(multi, {:trip, trip.relation.id}, trip)
    end)
  end

  defp persist_platforms(multi, platforms) do
    Enum.reduce(platforms, multi, fn platform, multi ->
      Multi.insert(multi, {:platform, platform.relation_id, platform.sequence_id}, platform)
    end)
  end

  defp generate_trip(relation, ways_map, routes_map, platform_member_map) do
    {geometry, exact} = trip_geometry(relation, ways_map)
    stop_ids = trip_stop_ids(relation, platform_member_map)
    route = Map.get(routes_map, Relation.ref(relation))

    %Trip{
      relation: relation,
      route: route,
      geometry: geometry,
      exact: exact,
      stop_ids: stop_ids
    }
  end

  defp generate_platform(platform_member, stops_map, routes_map, relations_map) do
    node = if platform_member.member_type == "N", do: platform_member.member
    way = if platform_member.member_type == "W", do: platform_member.member
    stop = Map.get(stops_map, platform_member.member.tags["gtfs:stop_id"])

    relation = Map.fetch!(relations_map, platform_member.relation_id)
    route = Map.get(routes_map, relation.tags["ref"])

    geometry =
      case platform_member.member_type do
        "N" -> platform_member.member.geom
        "W" -> platform_member.member.linestring
      end

    %Platform{
      relation_id: platform_member.relation_id,
      node: node,
      way: way,
      stop: stop,
      route: route,
      sequence_id: platform_member.sequence_id,
      geometry: geometry
    }
  end

  defp trip_stop_ids(relation, platform_member_map) do
    relation.members
    |> Enum.filter(&String.starts_with?(&1.member_role, "platform"))
    |> Enum.map(&Map.get(platform_member_map, {&1.relation_id, &1.sequence_id}))
    |> Enum.map(& &1.member.tags["gtfs:stop_id"])
  end

  defp trip_geometry(relation, ways_map) do
    ordered_ways =
      relation.members
      |> Enum.filter(&(&1.member_type == "W" and &1.member_role == ""))
      |> Enum.map(fn member -> Map.fetch!(ways_map, member.member_id) end)

    case Router.route_ways(ordered_ways) do
      {:ok, geometry} ->
        {geometry, true}

      {:error, _} ->
        geometry = GeoJSON.ways_to_multilinestring(ordered_ways)
        {geometry, false}
    end
  end
end

A lib/triglav/schemas/public_transport/platform.ex => lib/triglav/schemas/public_transport/platform.ex +23 -0
@@ 0,0 1,23 @@
defmodule Triglav.Schemas.PublicTransport.Platform do
  @moduledoc """
  Contains extended information for a public transport stop.
  """
  use Ecto.Schema

  alias Triglav.Schemas.Osmosis
  alias Triglav.Schemas.Zet

  @type t :: %__MODULE__{}

  @primary_key false

  schema "public_transport_platforms" do
    belongs_to :relation, Osmosis.Relation, primary_key: true
    belongs_to :stop, Zet.Stop, type: :binary
    belongs_to :route, Zet.Route, type: :binary
    belongs_to :node, Osmosis.Node
    belongs_to :way, Osmosis.Way
    field :sequence_id, :integer, primary_key: true
    field :geometry, Geo.PostGIS.Geometry
  end
end

A lib/triglav/schemas/public_transport/trip.ex => lib/triglav/schemas/public_transport/trip.ex +22 -0
@@ 0,0 1,22 @@
defmodule Triglav.Schemas.PublicTransport.Trip do
  @moduledoc """
  Matches an OSM relation of type="route" to a ZET route and contains some
  added information.
  """
  use Ecto.Schema

  alias Triglav.Schemas.Osmosis
  alias Triglav.Schemas.Zet

  @type t :: %__MODULE__{}

  @primary_key false

  schema "public_transport_trips" do
    belongs_to :relation, Osmosis.Relation, primary_key: true
    belongs_to :route, Zet.Route, type: :binary
    field :geometry, Geo.PostGIS.Geometry
    field :exact, :boolean
    field :stop_ids, {:array, :string}
  end
end

A priv/repo/migrations/20210227082725_create_public_transport_tables.exs => priv/repo/migrations/20210227082725_create_public_transport_tables.exs +31 -0
@@ 0,0 1,31 @@
defmodule Triglav.Repo.Migrations.CreatePublicTransportTables do
  use Ecto.Migration

  def up do
    create table("public_transport_trips", primary_key: false) do
      add :relation_id, :bigint, null: false, primary_key: true
      add :route_id, :string
      add :geometry, :geometry, null: false
      add :exact, :boolean, null: false
      add :stop_ids, {:array, :string}
    end

    create table("public_transport_platforms", primary_key: false) do
      add :relation_id, :bigint, null: false, primary_key: true
      add :route_id, :string
      add :stop_id, :string
      add :node_id, :bigint
      add :way_id, :bigint
      add :sequence_id, :integer, null: false, primary_key: true
      add :geometry, :geometry, null: false
    end

    create index("public_transport_trips", [:geometry], using: :gist)
    create index("public_transport_platforms", [:geometry], using: :gist)
  end

  def down do
    drop table("public_transport_trips")
    drop table("public_transport_platforms")
  end
end