defmodule Transit do require Logger NimbleCSV.define(CSVParser, separator: ",", escape: "\"") @moduledoc """ GTFS tools for the time being for Elixir applications. """ @doc """ Parse url that resolves to a *.zip for GTFS. """ def parse_url(url) when is_binary(url) do with {:ok, gtfs_zip} <- _fetch_gtfs_zip(url), {:ok, csv_data} <- _extract_csv_data_from_gtfs_zip(gtfs_zip), {:ok, mapped_feed_data} <- _convert_feed_csv_to_map(csv_data) do {:ok, mapped_feed_data} end end defp _fetch_gtfs_zip(url) when is_binary(url) do case HTTPoison.get(url) do {:ok, %HTTPoison.Response{status_code: status_code, headers: headers}} when status_code > 300 and status_code < 400 -> case _redirect_location(headers) do [url] when is_binary(url) -> _fetch_gtfs_zip(url) _ -> {:error, "There was an issue fetching fallback gtfs data. Error following redirects"} end {:ok, %HTTPoison.Response{body: gtfs_zip, status_code: 200}} -> {:ok, gtfs_zip} reason -> {:error, reason} end end defp _extract_csv_data_from_gtfs_zip(zip) do case :zip.zip_open(zip, [:memory]) do {:ok, zip_pid} -> {:ok, {'stops.txt', stops_csv}} = :zip.zip_get('stops.txt', zip_pid) {:ok, {'stop_times.txt', stop_times_csv}} = :zip.zip_get('stop_times.txt', zip_pid) {:ok, {'routes.txt', routes_csv}} = :zip.zip_get('routes.txt', zip_pid) {:ok, {'trips.txt', trips_csv}} = :zip.zip_get('trips.txt', zip_pid) :zip.zip_close(zip_pid) {:ok, %{ stops_csv: stops_csv, stop_times_csv: stop_times_csv, routes_csv: routes_csv, trips_csv: trips_csv }} {:error, reason} -> Logger.error(reason) end end defp _convert_feed_csv_to_map(feed_data) do # stops = CSVParser.parse_string(feed_data.stops_csv) |> IO.inspect stops = _csv_to_maps(feed_data.stops_csv) routes = _csv_to_maps(feed_data.routes_csv) stop_times = _csv_to_maps(feed_data.stop_times_csv) trips = _csv_to_maps(feed_data.trips_csv) {:ok, %{ stops: stops, stop_times: stop_times, routes: routes, trips: trips }} end defp _csv_to_maps(csv) do # headers {hd_row, rows} = CSVParser.parse_string(csv, headers: false) |> List.pop_at(0) rows |> Enum.map(fn row -> _csv_row_to_map(hd_row, row) end) end defp _csv_row_to_map(hd_row, row), do: Enum.zip(hd_row, row) |> Enum.into(%{}) defp _redirect_location(headers) do for {key, value} <- headers, String.downcase(key) == "location" do value end end end