R/export_gtfs.R

Defines functions error_missing_specified_file error_non_standard_files export_gtfs

Documented in export_gtfs

#' Export GTFS objects
#'
#' Writes GTFS objects to disk as GTFS transit feeds. The object must be
#' formatted according to the standards for reading and writing GTFS transit
#' feeds, as specified in \code{\link{get_gtfs_standards}} (i.e. data types are
#' not checked). If present, does not write auxiliary tables held in a sub-list
#' named \code{"."}.
#'
#' @param gtfs A GTFS object.
#' @param path A string. Where the resulting \code{.zip} file must be written
#'   to.
#' @param files A character vector. The name of the elements to be written to
#'   the feed.
#' @param standard_only A logical. Whether only standard files and fields should
#'   be written (defaults to \code{TRUE}, which drops extra files and fields).
#' @param compression_level A numeric, between 1 and 9. The higher the value,
#'   the best the compression, which demands more processing time. Defaults to 9
#'   (best compression).
#' @param as_dir A logical. Whether the feed should be exported as a directory,
#'   instead of a \code{.zip} file. Defaults to \code{FALSE}.
#' @param overwrite A logical. Whether to overwrite an existing \code{.zip} file
#'   (defaults to \code{TRUE}).
#' @param quiet A logical. Whether to hide log messages and progress bars
#'   (defaults to \code{TRUE}).
#'
#' @return Invisibly returns the same GTFS object passed to \code{gtfs}.
#'
#' @seealso \code{\link{get_gtfs_standards}}
#'
#' @family io functions
#'
#' @examples
#' gtfs_path <- system.file("extdata/ggl_gtfs.zip", package = "gtfsio")
#'
#' gtfs <- import_gtfs(gtfs_path)
#'
#' tmpf <- tempfile(pattern = "gtfs", fileext = ".zip")
#'
#' export_gtfs(gtfs, tmpf)
#' zip::zip_list(tmpf)$filename
#'
#' export_gtfs(gtfs, tmpf, files = c("shapes", "trips"))
#' zip::zip_list(tmpf)$filename
#'
#' @export
export_gtfs <- function(gtfs,
                        path,
                        files = NULL,
                        standard_only = FALSE,
                        compression_level = 9,
                        as_dir = FALSE,
                        overwrite = TRUE,
                        quiet = TRUE) {

  gtfs_standards <- get_gtfs_standards()

  # basic input checking

  assert_class(gtfs, "gtfs")
  assert_vector(path, "character", len = 1L)
  assert_vector(files, "character", null_ok = TRUE)
  assert_vector(standard_only, "logical", len = 1L)
  assert_vector(compression_level, "numeric", len = 1L)
  assert_vector(as_dir, "logical", len = 1L)
  assert_vector(overwrite, "logical", len = 1L)
  assert_vector(quiet, "logical", len = 1L)

  if (path == tempdir()) error_tempfile_misused()

  # input checks that depend on more than one argument

  if (file.exists(path) & !overwrite) error_cannot_overwrite()
  if (!as_dir & !grepl("\\.zip$", path)) error_ext_must_be_zip()
  if (as_dir & grepl("\\.zip$", path)) error_path_must_be_dir()

  extra_files <- setdiff(files, names(gtfs_standards))
  if (standard_only & !is.null(files) & !identical(extra_files, character(0))) {
    error_non_standard_files(extra_files)
  }

  # if files is NULL then all 'gtfs' elements should be written

  if (is.null(files)) files <- names(gtfs)

  # remove '.' from 'files' if it exists ({tidytransit} may place some auxiliary
  # tables in a sub-list named '.')

  files <- setdiff(files, ".")

  # remove extra files from 'files' if 'standard_only' is set to TRUE
  # 'extra_files' is re-evaluated because 'files' might have changed in the
  # lines above

  extra_files <- setdiff(files, names(gtfs_standards))

  if (standard_only) files <- setdiff(files, extra_files)

  # throw an error if a specified file is not an element of 'gtfs'

  missing_files <- setdiff(files, names(gtfs))

  if (!identical(missing_files, character(0))) {
    error_missing_specified_file(missing_files)
  }

  # write files either to a temporary directory (if as_dir = FALSE), or to path
  # (if as_dir = TRUE)

  if (as_dir) {
    tmpd <- path
  } else {
    tmpd <- tempfile(pattern = "gtfsio")
  }

  unlink(tmpd, recursive = TRUE)
  dir.create(tmpd)

  # write files to 'tmpd'

  if (!quiet) message("Writing text files to ", tmpd)

  for (file in files) {

    filename <- paste0(file, ".txt")
    filepath <- file.path(tmpd, filename)

    if (!quiet) message("  - Writing ", filename)

    dt <- gtfs[[file]]

    # if 'standard_only' is set to TRUE, remove non-standard fields from 'dt'
    # before writing it to disk

    if (standard_only) {

      file_cols  <- names(dt)
      extra_cols <- setdiff(file_cols, names(gtfs_standards[[file]]))

      if (!identical(extra_cols, character(0))) dt <- dt[, !..extra_cols]

    }

    # print warning message if warning is raised and 'quiet' is FALSE
    withCallingHandlers(
      data.table::fwrite(dt, filepath, scipen = 999),
      warning = function(cnd) {
        if (!quiet) message("    - ", conditionMessage(cnd))
      }
    )

  }

  # zip the contents of 'tmpd' to 'path', if as_dir = FALSE
  # remove the file/directory in 'path' (an error would already have been thrown
  # if 'path' pointed to an existing file that should not be overwritten).
  # this action prevents zip::zip() from crashing R when 'path' exists, but is a
  # directory, not a file
  # related issue: https://github.com/r-lib/zip/issues/76

  if (!as_dir) {

    unlink(path, recursive = TRUE)

    filepaths <- file.path(tmpd, paste0(files, ".txt"))

    zip::zip(
      path,
      filepaths,
      compression_level = compression_level,
      mode = "cherry-pick"
    )

    if (!quiet) message("GTFS object successfully zipped to ", path)

  }

  return(invisible(gtfs))

}


# errors ------------------------------------------------------------------


#' @include gtfsio_error.R
error_tempfile_misused <- parent_function_error(
  paste0(
    "Please use 'tempfile()' instead of 'tempdir()' to designate ",
    "temporary directories."
  ),
  subclass = "tempfile_misused"
)

error_cannot_overwrite <- parent_function_error(
  paste0(
    "'path' points to an existing file/directory, ",
    "but 'overwrite' is set to FALSE."
  ),
  subclass = "cannot_overwrite_file"
)

error_ext_must_be_zip <- parent_function_error(
  paste0(
    "'path' must have '.zip' extension. ",
    "If you meant to create a directory please set 'as_dir' to TRUE."
  ),
  subclass = "ext_must_be_zip"
)

error_path_must_be_dir <- parent_function_error(
  "'path' cannot have '.zip' extension when 'as_dir' is TRUE.",
  subclass = "path_must_be_dir"
)

error_non_standard_files <- function(extra_files) {
  parent_call <- sys.call(-1)
  message <- paste0(
    "Non-standard file specified in 'files', ",
    "even though 'standard_only' is set to TRUE: ",
    paste0("'", extra_files, "'", collapse = ", ")
  )

  gtfsio_error(message, subclass = "non_standard_files", call = parent_call)
}

error_missing_specified_file <- function(missing_files) {
  parent_call <- sys.call(-1)
  message <- paste0(
    "The provided GTFS object does not contain the following ",
    "elements specified in 'files': ",
    paste0("'", missing_files, "'", collapse = ", ")
  )

  gtfsio_error(message, subclass = "missing_specified_file", call = parent_call)
}

Try the gtfsio package in your browser

Any scripts or data that you put into this service are public.

gtfsio documentation built on Oct. 20, 2023, 9:08 a.m.