defmodule Transit do require Logger NimbleCSV.define(CSVParser, separator: ",", escape: "\"") @files [ {"stops.txt", GTFS.Stop}, {"stop_times.txt", GTFS.StopTime}, {"routes.txt", GTFS.Route}, {"trips.txt", GTFS.Trip}, {"calendar.txt", GTFS.Calendar}, {"calendar_dates.txt", GTFS.CalendarDate} ] @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, gtfs_stream_data, gtfs_tmp_dir} <- _extract_stream_data_from_gtfs_zip(gtfs_zip), {:ok, mapped_feed_data} <- _convert_feed_stream_to_map(gtfs_stream_data) do File.rm_rf!(gtfs_tmp_dir) {: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} res -> res end end defp _extract_stream_data_from_gtfs_zip(zip) do tmp_dir = '/tmp/transit/gtfs_' ++ to_charlist(:os.system_time) case :zip.zip_open(zip, [{:cwd, tmp_dir}]) do {:ok, zip_pid} -> file_paths = @files |> Enum.map(fn {filename, module} -> {:ok, path} = :zip.zip_get(to_charlist(filename), zip_pid) {path, module} end) gtfs_streams_with_structs = file_paths |> _file_paths_to_stream :zip.zip_close(zip_pid) {:ok, gtfs_streams_with_structs, to_string(tmp_dir)} {:error, reason} -> Logger.error(reason) end end defp _file_paths_to_stream(file_paths) do file_paths |> Enum.map(fn {path, module} -> stream = to_string(path) |> File.stream! {stream, module} end) end defp _convert_feed_stream_to_map(streams) do feed_map = streams |> Enum.reduce(%{}, fn {stream, module}, acc -> gtfs_data_arr = stream |> _parse_gtfs_file_stream(module) key = (Kernel.inspect(module) |> String.replace("GTFS.", "") |> Macro.underscore) <> "s" |> String.to_atom acc |> Map.put(key, gtfs_data_arr) end) {:ok, feed_map} end defp _parse_gtfs_file_stream(file_stream, s) do try do csv_stream = file_stream |> CSVParser.parse_stream(headers: false) headers = csv_stream |> Stream.take(1) |> Enum.to_list |> List.first |> Enum.map(&(String.to_atom(&1))) csv_stream |> Stream.drop(1) |> Enum.map(fn row -> map = Enum.zip(headers, row) |> Enum.into(%{}) struct(s, map) end) rescue File.Error -> Logger.error("Error reading GTFS files. Ensure that all files are present.") end end defp _redirect_location(headers) do for {key, value} <- headers, String.downcase(key) == "location" do value end end end