~ihabunek/triglav

d511a0d619743754b79c685bea92ca3e1a2cc7e5 — Ivan Habunek 8 days ago b5f3e70
Use distinct trips database view
M assets/js/routes.js => assets/js/routes.js +3 -3
@@ 18,7 18,7 @@ const tiles = new TileLayer(tilesUrl, { attribution })

tiles.addTo(map)

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



@@ 32,8 32,8 @@ 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], {
for (const name of Object.keys(routes)) {
  const way = L.geoJSON(routes[name], {
    style: feature => ({
      color: feature.properties.color
    }),

M lib/triglav/zet/gtfs.ex => lib/triglav/zet/gtfs.ex +11 -30
@@ 19,42 19,23 @@ defmodule Triglav.Zet.Gtfs do
    |> Repo.one!()
  end

  @spec list_gtfs_stop_ids() :: [Zet.DistinctTrip.t()]
  def list_gtfs_stop_ids() do
    from(t in Zet.DistinctTrip)
  def list_stops_by_id(stop_ids) do
    Zet.Stop
    |> where([s], s.id in ^stop_ids)
    |> Repo.all()
  end

  @spec list_gtfs_stop_ids(Zet.Trip.id()) :: [Zet.DistinctTrip.t()]
  def list_gtfs_stop_ids(trip_id) do
    from(t in Zet.DistinctTrip, where: t.trip_id == ^trip_id)
  @spec list_distinct_trips([{:route_id, binary()}]) :: [Zet.DistinctTrip.t()]
  def list_distinct_trips(opts \\ []) do
    route_id = Keyword.get(opts, :route_id)

    from(t in Zet.DistinctTrip)
    |> maybe_filter_by_route_id(route_id)
    |> Repo.all()
  end

  def fetch_distinct_trips(%Zet.Route{} = route) do
    Repo.select!(
      """
        WITH trips AS (
          SELECT t.trip_id,
                 t.direction_id,
                 array_agg(
                   row(s.stop_id, s.stop_name, s.stop_lat, s.stop_lon, st.stop_sequence::text)
                   ORDER BY st.stop_sequence
                 ) AS stops
          FROM zet.stop_times st
          JOIN zet.trips t ON st.trip_id = t.trip_id
          JOIN zet.stops s ON s.stop_id = st.stop_id
          WHERE t.route_id = $1
          GROUP BY 1, 2
        )
        SELECT direction_id, stops, count(*) AS trip_count
        FROM trips
        GROUP BY 1, 2
        ORDER BY 1 ASC, 3 DESC;
      """,
      [route.id]
    )
  end
  defp maybe_filter_by_route_id(query, nil), do: query
  defp maybe_filter_by_route_id(query, route_id), do: where(query, [t], t.route_id == ^route_id)

  @doc """
  Returns a list of all stops contained in any trip in the given route.

M lib/triglav_web/controllers/zet/routes_controller.ex => lib/triglav_web/controllers/zet/routes_controller.ex +31 -12
@@ 47,7 47,7 @@ defmodule TriglavWeb.Zet.RoutesController do

  defp annotate_routes(routes, relations) do
    osm_stop_ids = relations |> Enum.map(& &1.id) |> Osmosis.list_gtfs_stop_ids()
    zet_stop_ids = Gtfs.list_gtfs_stop_ids() |> Enum.group_by(& &1.route_id)
    gtfs_distinct_trips = Gtfs.list_distinct_trips() |> Enum.group_by(& &1.route_id)

    for route <- routes do
      route_relations = Enum.filter(relations, &Relation.has_tag?(&1, "ref", route.id))


@@ 59,7 59,7 @@ defmodule TriglavWeb.Zet.RoutesController do
        |> MapSet.new()

      zet_variants =
        zet_stop_ids
        gtfs_distinct_trips
        |> Map.get(route.id)
        |> MapSet.new(&Map.get(&1, :stops))



@@ 90,18 90,34 @@ defmodule TriglavWeb.Zet.RoutesController do
      |> Osmosis.list_platform_members()
      |> Enum.group_by(& &1.relation_id)

    trips = Gtfs.list_distinct_trips(route_id: route.id)
    trips_by_stop_ids = trips |> Map.new(&{&1.stops, &1})

    relations_by_stop_ids =
      for relation <- relations, Relation.is_route(relation), into: %{} do
        {Osmosis.list_gtfs_stop_ids(relation.id), relation}
      end

    trips =
      for trip <- Gtfs.fetch_distinct_trips(route) do
        stop_ids = Enum.map(trip["stops"], &elem(&1, 0))
        {trip, Map.get(relations_by_stop_ids, stop_ids)}
      end
    annotated_trips =
      trips
      |> Enum.map(fn trip ->
        Map.put(trip, :matched_relation, Map.get(relations_by_stop_ids, trip.stops))
      end)
      |> Enum.group_by(& &1.direction_id)

    grouped_trips = Enum.group_by(trips, fn {trip, _} -> trip["direction_id"] end)
    annotated_relations =
      Enum.map(relations_by_stop_ids, fn {stops, relation} ->
        Map.put(relation, :matched_trip, Map.get(trips_by_stop_ids, stops))
      end)
      |> IO.inspect()

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

    # JSON encode stops for drawing on the map
    stops_json =


@@ 110,19 126,22 @@ defmodule TriglavWeb.Zet.RoutesController do
      |> Enum.map(&Map.take(&1, [:id, :name, :lat, :lon]))
      |> Jason.encode!()

    ways_geojson = routes_feature_collecton(relations, platform_members) |> Jason.encode!()
    routes_geojson =
      routes_feature_collecton(relations, platform_members)
      |> Jason.encode!()

    render(conn, "detail.html",
      conn: conn,
      hierarchy: hierarchy,
      platform_members: platform_members,
      relation_errors: relation_errors,
      relations: relations,
      relations: annotated_relations,
      route: route,
      route_errors: route_errors,
      routes_geojson: routes_geojson,
      stops: stops,
      stops_json: stops_json,
      trips: grouped_trips,
      ways_geojson: ways_geojson
      trips: annotated_trips
    )
  end


M lib/triglav_web/templates/zet/routes/detail.html.eex => lib/triglav_web/templates/zet/routes/detail.html.eex +28 -16
@@ 4,6 4,7 @@
  #routes-map {
    height: 600px;
    max-width:  1024px;
    margin: 1rem 0;
  }
</style>



@@ 11,8 12,8 @@
  <%= raw(@stops_json) %>
</script>

<script id="ways-geojson" type="application/json">
  <%= raw(@ways_geojson) %>
<script id="routes-geojson" type="application/json">
  <%= raw(@routes_geojson) %>
</script>

<main role="main" class="container-wide">


@@ 62,7 63,7 @@
  <section>
    <h3>Route relations</h3>

    <div style="display: flex; flex-direction: row;">
    <div style="display: flex; flex-direction: row; flex-wrap: wrap;">
      <%= for relation <- @relations, Relation.is_route(relation) do %>
        <% members = @platform_members[relation.id] || [] %>
        <div style="margin-left: 1rem;">


@@ 97,6 98,16 @@
              <% end %>
            </tbody>
          </table>

          <%= if relation.matched_trip do %>
            <div class="callout">
              <span class="text-green">✔ Matches a GTFS trip</span>
            </div>
          <% else %>
            <div class="callout error">
              ✖ No GTFS trip matched.
            </div>
          <% end %>
        </div>
      <% end %>
    </div>


@@ 104,18 115,18 @@

  <hr />

  <h2>ZET GTFS routes</h2>
  <h2>ZET GTFS trips</h2>

  <p>Routes extracted from GTFS data published by ZET.
  All distinct routes are shown along with the number of trips along this route. </p>
  <p>Trips extracted from GTFS data published by ZET.<br/>
  All distinct trips are shown along with the number of departures along this route. </p>

  <%= for {direction_id, trips} <- @trips do %>
    <h3>Direction #<%= direction_id %></h3>

    <div style="display: flex; flex-direction: row;">
      <%= for {trip, matched_relation} <- trips do %>
    <div style="display: flex; flex-direction: row; flex-wrap: wrap">
      <%= for trip <- trips do %>
        <div style="margin-left: 1rem;">
          <p><b><%= trip["trip_count"] %> departures</b></p>
          <p><b><%= trip.trip_count %> departures</b></p>
          <table>
            <thead>
              <tr>


@@ 126,21 137,22 @@
              </tr>
            </thead>
            <tbody>
              <%= for {id, name, lat, lon, ord} <- trip["stops"] do %>
              <%= for {stop_id, index} <- Enum.with_index(trip.stops) do %>
              <% stop = Map.get(@stops, stop_id) %>
              <tr>
                <td><%= ord %>.</td>
                <td><%= id %></td>
                <td><%= name %></td>
                <td><%= josm_zoom(lat, lon) %></td>
                <td><%= index %>.</td>
                <td><%= stop.id %></td>
                <td><%= stop.name %></td>
                <td><%= josm_zoom(stop.lat, stop.lon) %></td>
              </tr>
              <% end %>
            </tbody>
          </table>

          <%= if matched_relation do %>
          <%= if trip.matched_relation do %>
            <div class="callout">
              <span class="text-green">✔ Matched relation</span><br />
              <%= osm_link(matched_relation, [josm: true]) %>
              <%= osm_link(trip.matched_relation, [josm: true]) %>
            </div>
          <% else %>
            <div class="callout error">