From b6d4602492209bb02d58a6f5697b2ca024a6c311 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Wed, 31 Jul 2024 14:11:23 +0200 Subject: [PATCH 01/25] add basic v1 data retrieval functions --- .gitignore | 1 + DESCRIPTION | 2 +- NAMESPACE | 4 + R/get-v1-data.R | 165 +++++++++++++++++++++++++++++ R/get.R | 15 ++- man/get_data_dir.Rd | 16 +++ man/spo_available_data_v1.Rd | 34 ++++++ man/spo_get_latest_v1_file_list.Rd | 27 +++++ man/spo_get_zones_v1.Rd | 25 +++++ man/spo_load_zones_v1.Rd | 28 +++++ 10 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 R/get-v1-data.R create mode 100644 man/get_data_dir.Rd create mode 100644 man/spo_available_data_v1.Rd create mode 100644 man/spo_get_latest_v1_file_list.Rd create mode 100644 man/spo_get_zones_v1.Rd create mode 100644 man/spo_load_zones_v1.Rd diff --git a/.gitignore b/.gitignore index fab52f0..47a82d5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ movilidad.duckdb zonificacion_distritos* *.duckdb docs +private /.quarto/ .Rproj.user diff --git a/DESCRIPTION b/DESCRIPTION index 5e02562..af2e0f9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -35,4 +35,4 @@ Imports: xml2 Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.1 +RoxygenNote: 7.3.2.9000 diff --git a/NAMESPACE b/NAMESPACE index 234c562..f637a7d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,3 +4,7 @@ export(get_latest_v2_xml) export(get_metadata) export(get_od) export(get_zones) +export(spo_available_data_v1) +export(spo_get_latest_v1_file_list) +export(spo_get_zones_v1) +export(spo_load_zones_v1) diff --git a/R/get-v1-data.R b/R/get-v1-data.R new file mode 100644 index 0000000..7d3506e --- /dev/null +++ b/R/get-v1-data.R @@ -0,0 +1,165 @@ +#' Get latest file list from the XML for MITMA open mobiltiy data v1 (2020-2021) +#' +#' @param data_dir The directory where the data is stored. Defaults to the value returned by `get_data_dir()`. +#' @param xml_url The URL of the XML file to download. Defaults to "https://opendata-movilidad.mitma.es/RSS.xml". +#' +#' @return The path to the downloaded XML file. +#' @export +#' @examples +#' if (FALSE) { +#' spo_get_latest_v1_file_list() +#' } +spo_get_latest_v1_file_list <- function( + data_dir = get_data_dir(), + xml_url = "https://opendata-movilidad.mitma.es/RSS.xml") { + if (!fs::dir_exists(data_dir)) { + fs::dir_create(data_dir) + } + + current_timestamp <- format(Sys.time(), format = "%Y-%m-01", usetz = FALSE, tz = "UTC") + current_filename <- glue::glue("{data_dir}/data_links_v1_{current_timestamp}.xml") + + message("Saving the file to: ", current_filename) + xml_requested <- curl::curl_download( + url = xml_url, + destfile = current_filename, + quiet = FALSE + ) + return(current_filename) +} + +#' Get the available v1 data list +#' +#' This function provides a table of the available data list of MITMA v1 (2020-2021), both remote and local. +#' +#' @param data_dir The directory where the data is stored. Defaults to the value returned by `get_data_dir()`. +#' @return A tibble with links, release dates of files in the data, dates of data coverage, local paths to files, and the download status. +#' \describe{ +#' \item{target_url}{\code{character}. The URL link to the data file.} +#' \item{pub_ts}{\code{POSIXct}. The timestamp of when the file was published.} +#' \item{file_extension}{\code{character}. The file extension of the data file (e.g., 'tar', 'gz').} +#' \item{data_ym}{\code{Date}. The year and month of the data coverage, if available.} +#' \item{data_ymd}{\code{Date}. The specific date of the data coverage, if available.} +#' \item{local_path}{\code{character}. The local file path where the data is stored.} +#' \item{downloaded}{\code{logical}. Indicator of whether the data file has been downloaded locally.} +#' } +#' @export +#' @examples +#' # Get the available v1 data list for the default data directory +#' if (FALSE) { +#' metadata <- spo_available_data_v1() +#' names(metadata) +#' head(metadata) +#' } +spo_available_data_v1 <- function(data_dir = get_data_dir()) { + xml_files_list <- fs::dir_ls(data_dir, type = "file", regexp = "data_links_v1") |> sort() + latest_data_links_xml_path <- utils::tail(xml_files_list, 1) + if (length(latest_data_links_xml_path) == 0) { + message("Getting latest data links xml") + latest_data_links_xml_path <- get_latest_v2_xml(data_dir = data_dir) + } else { + message("Using existing data links xml: ", latest_data_links_xml_path) + } + + x_xml <- xml2::read_xml(latest_data_links_xml_path) + + files_table <- tibble::tibble( + target_url = xml2::xml_find_all(x = x_xml, xpath = "//link") |> xml2::xml_text(), + pub_date = xml2::xml_find_all(x = x_xml, xpath = "//pubDate") |> xml2::xml_text() + ) + + files_table$pub_ts <- lubridate::dmy_hms(files_table$pub_date) + files_table$file_extension <- tools::file_ext(files_table$target_url) + files_table <- files_table[files_table$file_extension != "", ] + files_table$pub_date <- NULL + + files_table$data_ym <- lubridate::ym(stringr::str_extract(files_table$target_url, "[0-9]{4}-[0-9]{2}")) + files_table$data_ymd <- lubridate::ymd(stringr::str_extract(files_table$target_url, "[0-9]{8}")) + # order by pub_ts + files_table <- files_table[order(files_table$pub_ts, decreasing = TRUE), ] + files_table$local_path <- file.path( + data_dir, + stringr::str_replace(files_table$target_url, ".*mitma.es/", "/v1/") + ) + + files_table$local_path <- stringr::str_replace_all(files_table$local_path, "\\/\\/\\/|\\/\\/", "/") + + # change path for daily data files to be in hive-style format + files_table$local_path <- gsub("([0-9]{4})-([0-9]{2})\\/[0-9]{6}([0-9]{2})_", "year=\\1\\/month=\\2\\/day=\\3\\/", files_table$local_path) + + # now check if any of local files exist + files_table$downloaded <- fs::file_exists(files_table$local_path) + + return(files_table) +} + +#' Retrieves the zones for v1 data +#' +#' This function retrieves the zones data from the specified data directory. +#' It can retrieve either "distritos" or "municipios" zones data. +#' +#' @param data_dir The directory where the data is stored. +#' @param type The type of zones data to retrieve ("distritos" or "municipios"). +#' @return A spatial object containing the zones data. +#' @export +#' @examples +#' if (FALSE) { +#' zones <- get_zones() +#' } +spo_get_zones_v1 <- function( + data_dir = get_data_dir(), + type = "distritos") { + metadata <- spo_available_data_v1(data_dir) + regex <- glue::glue("zonificacion_{type}\\.") + sel_distritos <- stringr::str_detect(metadata$target_url, regex) + metadata_distritos <- metadata[sel_distritos, ] + dir_name <- fs::path_dir(metadata_distritos$local_path[1]) + if (!fs::dir_exists(dir_name)) { + fs::dir_create(dir_name, recurse = TRUE) + } + + downloaded_file <- curl::multi_download(metadata_distritos$target_url, destfile = metadata_distritos$local_path, resume = TRUE) + + utils::unzip(downloaded_file$destfile, + exdir = fs::path_dir(downloaded_file$destfile) + ) + + # remove artifacts (remove __MACOSX if exists) + junk_path <- paste0(fs::path_dir(downloaded_file$destfile), "/__MACOSX") + if (fs::dir_exists(junk_path)) fs::dir_delete(junk_path) + + shp_file <- fs::dir_ls(data_dir, glob = glue::glue("**{type}/*.shp"), recurse = TRUE) + + suppressWarnings({ + return(sf::read_sf(shp_file)) + }) +} + +#' Loads the fixed zones for v1 data +#' +#' This function loads the fixed zones data for the specified data directory. +#' The geometry is cheked for validity and fixed if necessary. The 'ID' column is renamed to 'id'. +#' +#' @param data_dir The directory where the data is stored. +#' @param type The type of zones data to retrieve ("distritos" or "municipios"). +#' @return A spatial object of type `sf` containing the fixed zones data. +#' \describe{ +#' \item{id}{\code{character}. The identifier of the zone.} +#' } +#' @export +#' @examples +#' if (FALSE) { +#' zones <- spo_load_zones_v1() +#' } +spo_load_zones_v1 <- function( + data_dir = get_data_dir(), + type = "distritos") { + zones <- spo_get_zones_v1(data_dir, type) + invalid_geometries <- !sf::st_is_valid(zones) + if (invalid_geometries > 0) { + fixed_zones <- sf::st_make_valid(zones[invalid_geometries, ]) + zones <- rbind(zones[!invalid_geometries, ], fixed_zones) + } + names(zones)[names(zones) == "ID"] <- "id" + return(zones) +} diff --git a/R/get.R b/R/get.R index 573b79f..067352c 100644 --- a/R/get.R +++ b/R/get.R @@ -18,7 +18,7 @@ get_latest_v2_xml = function( fs::dir_create(data_dir) } - current_filename = glue::glue("{data_dir}/data_links_{current_timestamp}.xml") + current_filename = glue::glue("{data_dir}/data_links_v2_{current_timestamp}.xml") message("Saving the file to: ", current_filename) xml_requested = curl::curl_download(url = xml_url, destfile = current_filename, quiet = FALSE) @@ -40,7 +40,7 @@ get_latest_v2_xml = function( #' head(metadata) #' } get_metadata = function(data_dir = get_data_dir()) { - xml_files_list = fs::dir_ls(data_dir, type = "file", regexp = "data_links_") |> sort() + xml_files_list = fs::dir_ls(data_dir, type = "file", regexp = "data_links_v2") |> sort() latest_data_links_xml_path = utils::tail(xml_files_list, 1) if (length(latest_data_links_xml_path) == 0) { message("Getting latest data links xml") @@ -74,8 +74,15 @@ get_metadata = function(data_dir = get_data_dir()) { return(download_dt) } +#' Get the data directory +#' +#' This function retrieves the data directory from the environment variable SPANISH_OD_DATA_DIR. +#' If the environment variable is not set, it returns the temporary directory. +#' +#' @return The data directory. +#' @keywords internal get_data_dir = function() { - data_dir_env = Sys.getenv("SPANISH_OD_DATA_DIR") + data_dir_env = fs::path_real(Sys.getenv("SPANISH_OD_DATA_DIR")) if (data_dir_env == "") { data_dir_env = tempdir() } @@ -104,7 +111,7 @@ get_zones = function( metadata_distritos = metadata[sel_distritos, ] dir_name = dirname(metadata_distritos$local_path[1]) if (!fs::dir_exists(dir_name)) { - fs::dir_create(dir_name) + fs::dir_create(dir_name, recurse = TRUE) } for (i in 1:nrow(metadata_distritos)) { if (!fs::file_exists(metadata_distritos$local_path[i])) { diff --git a/man/get_data_dir.Rd b/man/get_data_dir.Rd new file mode 100644 index 0000000..fa9bce0 --- /dev/null +++ b/man/get_data_dir.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get.R +\name{get_data_dir} +\alias{get_data_dir} +\title{Get the data directory} +\usage{ +get_data_dir() +} +\value{ +The data directory. +} +\description{ +This function retrieves the data directory from the environment variable SPANISH_OD_DATA_DIR. +If the environment variable is not set, it returns the temporary directory. +} +\keyword{internal} diff --git a/man/spo_available_data_v1.Rd b/man/spo_available_data_v1.Rd new file mode 100644 index 0000000..c3ba363 --- /dev/null +++ b/man/spo_available_data_v1.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get-v1-data.R +\name{spo_available_data_v1} +\alias{spo_available_data_v1} +\title{Get the available v1 data list} +\usage{ +spo_available_data_v1(data_dir = get_data_dir()) +} +\arguments{ +\item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{get_data_dir()}.} +} +\value{ +A tibble with links, release dates of files in the data, dates of data coverage, local paths to files, and the download status. +\describe{ +\item{target_url}{\code{character}. The URL link to the data file.} +\item{pub_ts}{\code{POSIXct}. The timestamp of when the file was published.} +\item{file_extension}{\code{character}. The file extension of the data file (e.g., 'tar', 'gz').} +\item{data_ym}{\code{Date}. The year and month of the data coverage, if available.} +\item{data_ymd}{\code{Date}. The specific date of the data coverage, if available.} +\item{local_path}{\code{character}. The local file path where the data is stored.} +\item{downloaded}{\code{logical}. Indicator of whether the data file has been downloaded locally.} +} +} +\description{ +This function provides a table of the available data list of MITMA v1 (2020-2021), both remote and local. +} +\examples{ +# Get the available v1 data list for the default data directory +if (FALSE) { + metadata <- spo_available_data_v1() + names(metadata) + head(metadata) +} +} diff --git a/man/spo_get_latest_v1_file_list.Rd b/man/spo_get_latest_v1_file_list.Rd new file mode 100644 index 0000000..4a719f3 --- /dev/null +++ b/man/spo_get_latest_v1_file_list.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get-v1-data.R +\name{spo_get_latest_v1_file_list} +\alias{spo_get_latest_v1_file_list} +\title{Get latest file list from the XML for MITMA open mobiltiy data v1 (2020-2021)} +\usage{ +spo_get_latest_v1_file_list( + data_dir = get_data_dir(), + xml_url = "https://opendata-movilidad.mitma.es/RSS.xml" +) +} +\arguments{ +\item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{get_data_dir()}.} + +\item{xml_url}{The URL of the XML file to download. Defaults to "https://opendata-movilidad.mitma.es/RSS.xml".} +} +\value{ +The path to the downloaded XML file. +} +\description{ +Get latest file list from the XML for MITMA open mobiltiy data v1 (2020-2021) +} +\examples{ +if (FALSE) { + spo_get_latest_v1_file_list() +} +} diff --git a/man/spo_get_zones_v1.Rd b/man/spo_get_zones_v1.Rd new file mode 100644 index 0000000..b50b66f --- /dev/null +++ b/man/spo_get_zones_v1.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get-v1-data.R +\name{spo_get_zones_v1} +\alias{spo_get_zones_v1} +\title{Retrieves the zones for v1 data} +\usage{ +spo_get_zones_v1(data_dir = get_data_dir(), type = "distritos") +} +\arguments{ +\item{data_dir}{The directory where the data is stored.} + +\item{type}{The type of zones data to retrieve ("distritos" or "municipios").} +} +\value{ +A spatial object containing the zones data. +} +\description{ +This function retrieves the zones data from the specified data directory. +It can retrieve either "distritos" or "municipios" zones data. +} +\examples{ +if (FALSE) { + zones <- get_zones() +} +} diff --git a/man/spo_load_zones_v1.Rd b/man/spo_load_zones_v1.Rd new file mode 100644 index 0000000..c476526 --- /dev/null +++ b/man/spo_load_zones_v1.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get-v1-data.R +\name{spo_load_zones_v1} +\alias{spo_load_zones_v1} +\title{Loads the fixed zones for v1 data} +\usage{ +spo_load_zones_v1(data_dir = get_data_dir(), type = "distritos") +} +\arguments{ +\item{data_dir}{The directory where the data is stored.} + +\item{type}{The type of zones data to retrieve ("distritos" or "municipios").} +} +\value{ +A spatial object of type \code{sf} containing the fixed zones data. +\describe{ +\item{id}{\code{character}. The identifier of the zone.} +} +} +\description{ +This function loads the fixed zones data for the specified data directory. +The geometry is cheked for validity and fixed if necessary. The 'ID' column is renamed to 'id'. +} +\examples{ +if (FALSE) { + zones <- spo_load_zones_v1() +} +} From f07eef698b52431f02293d8a22fe9bc0e113bb9c Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Mon, 5 Aug 2024 14:27:41 +0200 Subject: [PATCH 02/25] clean get v1 zones functions, revised metadata update if file older than 1 day --- NAMESPACE | 8 +- R/get-v1-data.R | 76 +++++++++++++------ R/get.R | 2 +- man/get_latest_v2_xml.Rd | 2 +- ...e_data_v1.Rd => spod_available_data_v1.Rd} | 8 +- ...ist.Rd => spod_get_latest_v1_file_list.Rd} | 8 +- ...o_get_zones_v1.Rd => spod_get_zones_v1.Rd} | 13 ++-- ...load_zones_v1.Rd => spod_load_zones_v1.Rd} | 15 ++-- 8 files changed, 83 insertions(+), 49 deletions(-) rename man/{spo_available_data_v1.Rd => spod_available_data_v1.Rd} (89%) rename man/{spo_get_latest_v1_file_list.Rd => spod_get_latest_v1_file_list.Rd} (83%) rename man/{spo_get_zones_v1.Rd => spod_get_zones_v1.Rd} (79%) rename man/{spo_load_zones_v1.Rd => spod_load_zones_v1.Rd} (79%) diff --git a/NAMESPACE b/NAMESPACE index f637a7d..83a6341 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -4,7 +4,7 @@ export(get_latest_v2_xml) export(get_metadata) export(get_od) export(get_zones) -export(spo_available_data_v1) -export(spo_get_latest_v1_file_list) -export(spo_get_zones_v1) -export(spo_load_zones_v1) +export(spod_available_data_v1) +export(spod_get_latest_v1_file_list) +export(spod_get_zones_v1) +export(spod_load_zones_v1) diff --git a/R/get-v1-data.R b/R/get-v1-data.R index 7d3506e..14285a4 100644 --- a/R/get-v1-data.R +++ b/R/get-v1-data.R @@ -7,16 +7,16 @@ #' @export #' @examples #' if (FALSE) { -#' spo_get_latest_v1_file_list() +#' spod_get_latest_v1_file_list() #' } -spo_get_latest_v1_file_list <- function( +spod_get_latest_v1_file_list <- function( data_dir = get_data_dir(), xml_url = "https://opendata-movilidad.mitma.es/RSS.xml") { if (!fs::dir_exists(data_dir)) { fs::dir_create(data_dir) } - current_timestamp <- format(Sys.time(), format = "%Y-%m-01", usetz = FALSE, tz = "UTC") + current_timestamp <- format(Sys.time(), format = "%Y-%m-%d", usetz = FALSE, tz = "UTC") current_filename <- glue::glue("{data_dir}/data_links_v1_{current_timestamp}.xml") message("Saving the file to: ", current_filename) @@ -47,20 +47,30 @@ spo_get_latest_v1_file_list <- function( #' @examples #' # Get the available v1 data list for the default data directory #' if (FALSE) { -#' metadata <- spo_available_data_v1() +#' metadata <- spod_available_data_v1() #' names(metadata) #' head(metadata) #' } -spo_available_data_v1 <- function(data_dir = get_data_dir()) { +spod_available_data_v1 <- function(data_dir = get_data_dir()) { xml_files_list <- fs::dir_ls(data_dir, type = "file", regexp = "data_links_v1") |> sort() latest_data_links_xml_path <- utils::tail(xml_files_list, 1) + + # Check if the XML file is 1 day old or older from its name + file_date <- stringr::str_extract(latest_data_links_xml_path, "[0-9]{4}-[0-9]{2}-[0-9]{2}") + + if (file_date < format(Sys.Date(), format = "%Y-%m-%d")) { + message("File list xml is 1 day old or older, getting latest data links xml") + latest_data_links_xml_path <- spod_get_latest_v1_file_list(data_dir = data_dir) + } else { + message("Using existing data links xml: ", latest_data_links_xml_path) + } + if (length(latest_data_links_xml_path) == 0) { message("Getting latest data links xml") latest_data_links_xml_path <- get_latest_v2_xml(data_dir = data_dir) - } else { - message("Using existing data links xml: ", latest_data_links_xml_path) } + x_xml <- xml2::read_xml(latest_data_links_xml_path) files_table <- tibble::tibble( @@ -106,26 +116,42 @@ spo_available_data_v1 <- function(data_dir = get_data_dir()) { #' if (FALSE) { #' zones <- get_zones() #' } -spo_get_zones_v1 <- function( - data_dir = get_data_dir(), - type = "distritos") { - metadata <- spo_available_data_v1(data_dir) +spod_get_zones_v1 <- function( + type = c("distritos", "municipios"), + data_dir = get_data_dir()) { + type <- match.arg(type) + + # check if shp files are already extracted + expected_shp_path <- fs::path(data_dir, glue::glue("v1/zonificacion-{type}/{type}_mitma.shp")) + if (fs::file_exists(expected_shp_path)) { + message(".shp file already exists in data dir: ", expected_shp_path) + return(sf::read_sf(expected_shp_path)) + } + + metadata <- spod_available_data_v1(data_dir) regex <- glue::glue("zonificacion_{type}\\.") - sel_distritos <- stringr::str_detect(metadata$target_url, regex) - metadata_distritos <- metadata[sel_distritos, ] - dir_name <- fs::path_dir(metadata_distritos$local_path[1]) + sel_zones <- stringr::str_detect(metadata$target_url, regex) + metadata_zones <- metadata[sel_zones, ] + dir_name <- fs::path_dir(metadata_zones$local_path[1]) if (!fs::dir_exists(dir_name)) { fs::dir_create(dir_name, recurse = TRUE) } - downloaded_file <- curl::multi_download(metadata_distritos$target_url, destfile = metadata_distritos$local_path, resume = TRUE) + if (!fs::file_exists(metadata_zones$local_path)) { + message("Downloading the file to: ", metadata_zones$local_path) + downloaded_file <- curl::curl_download(metadata_zones$target_url, destfile = metadata_zones$local_path, mode = "wb", quiet = FALSE) + } else { + message("File already exists: ", metadata_zones$local_path) + downloaded_file <- metadata_zones$local_path + } - utils::unzip(downloaded_file$destfile, - exdir = fs::path_dir(downloaded_file$destfile) + message("Unzipping the file: ", downloaded_file) + utils::unzip(downloaded_file, + exdir = fs::path_dir(downloaded_file) ) # remove artifacts (remove __MACOSX if exists) - junk_path <- paste0(fs::path_dir(downloaded_file$destfile), "/__MACOSX") + junk_path <- paste0(fs::path_dir(downloaded_file), "/__MACOSX") if (fs::dir_exists(junk_path)) fs::dir_delete(junk_path) shp_file <- fs::dir_ls(data_dir, glob = glue::glue("**{type}/*.shp"), recurse = TRUE) @@ -149,14 +175,16 @@ spo_get_zones_v1 <- function( #' @export #' @examples #' if (FALSE) { -#' zones <- spo_load_zones_v1() +#' zones <- spod_load_zones_v1() #' } -spo_load_zones_v1 <- function( - data_dir = get_data_dir(), - type = "distritos") { - zones <- spo_get_zones_v1(data_dir, type) +spod_load_zones_v1 <- function( + type = c("distritos", "municipios"), + data_dir = get_data_dir()) { + type <- match.arg(type) + zones <- spod_get_zones_v1(type = type, data_dir = data_dir) + invalid_geometries <- !sf::st_is_valid(zones) - if (invalid_geometries > 0) { + if (sum(invalid_geometries) > 0) { fixed_zones <- sf::st_make_valid(zones[invalid_geometries, ]) zones <- rbind(zones[!invalid_geometries, ], fixed_zones) } diff --git a/R/get.R b/R/get.R index 067352c..be8107b 100644 --- a/R/get.R +++ b/R/get.R @@ -13,7 +13,7 @@ get_latest_v2_xml = function( data_dir = get_data_dir(), xml_url = "https://movilidad-opendata.mitma.es/RSS.xml", - current_timestamp = format(Sys.time(), format = "%Y-%m-01", usetz = FALSE, tz = "UTC")) { + current_timestamp = format(Sys.time(), format = "%Y-%m-%d", usetz = FALSE, tz = "UTC")) { if (!fs::dir_exists(data_dir)) { fs::dir_create(data_dir) } diff --git a/man/get_latest_v2_xml.Rd b/man/get_latest_v2_xml.Rd index 17338b1..defbb22 100644 --- a/man/get_latest_v2_xml.Rd +++ b/man/get_latest_v2_xml.Rd @@ -7,7 +7,7 @@ get_latest_v2_xml( data_dir = get_data_dir(), xml_url = "https://movilidad-opendata.mitma.es/RSS.xml", - current_timestamp = format(Sys.time(), format = "\%Y-\%m-01", usetz = FALSE, tz = + current_timestamp = format(Sys.time(), format = "\%Y-\%m-\%d", usetz = FALSE, tz = "UTC") ) } diff --git a/man/spo_available_data_v1.Rd b/man/spod_available_data_v1.Rd similarity index 89% rename from man/spo_available_data_v1.Rd rename to man/spod_available_data_v1.Rd index c3ba363..ea77366 100644 --- a/man/spo_available_data_v1.Rd +++ b/man/spod_available_data_v1.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/get-v1-data.R -\name{spo_available_data_v1} -\alias{spo_available_data_v1} +\name{spod_available_data_v1} +\alias{spod_available_data_v1} \title{Get the available v1 data list} \usage{ -spo_available_data_v1(data_dir = get_data_dir()) +spod_available_data_v1(data_dir = get_data_dir()) } \arguments{ \item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{get_data_dir()}.} @@ -27,7 +27,7 @@ This function provides a table of the available data list of MITMA v1 (2020-2021 \examples{ # Get the available v1 data list for the default data directory if (FALSE) { - metadata <- spo_available_data_v1() + metadata <- spod_available_data_v1() names(metadata) head(metadata) } diff --git a/man/spo_get_latest_v1_file_list.Rd b/man/spod_get_latest_v1_file_list.Rd similarity index 83% rename from man/spo_get_latest_v1_file_list.Rd rename to man/spod_get_latest_v1_file_list.Rd index 4a719f3..2e17ad3 100644 --- a/man/spo_get_latest_v1_file_list.Rd +++ b/man/spod_get_latest_v1_file_list.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/get-v1-data.R -\name{spo_get_latest_v1_file_list} -\alias{spo_get_latest_v1_file_list} +\name{spod_get_latest_v1_file_list} +\alias{spod_get_latest_v1_file_list} \title{Get latest file list from the XML for MITMA open mobiltiy data v1 (2020-2021)} \usage{ -spo_get_latest_v1_file_list( +spod_get_latest_v1_file_list( data_dir = get_data_dir(), xml_url = "https://opendata-movilidad.mitma.es/RSS.xml" ) @@ -22,6 +22,6 @@ Get latest file list from the XML for MITMA open mobiltiy data v1 (2020-2021) } \examples{ if (FALSE) { - spo_get_latest_v1_file_list() + spod_get_latest_v1_file_list() } } diff --git a/man/spo_get_zones_v1.Rd b/man/spod_get_zones_v1.Rd similarity index 79% rename from man/spo_get_zones_v1.Rd rename to man/spod_get_zones_v1.Rd index b50b66f..644de9b 100644 --- a/man/spo_get_zones_v1.Rd +++ b/man/spod_get_zones_v1.Rd @@ -1,15 +1,18 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/get-v1-data.R -\name{spo_get_zones_v1} -\alias{spo_get_zones_v1} +\name{spod_get_zones_v1} +\alias{spod_get_zones_v1} \title{Retrieves the zones for v1 data} \usage{ -spo_get_zones_v1(data_dir = get_data_dir(), type = "distritos") +spod_get_zones_v1( + type = c("distritos", "municipios"), + data_dir = get_data_dir() +) } \arguments{ -\item{data_dir}{The directory where the data is stored.} - \item{type}{The type of zones data to retrieve ("distritos" or "municipios").} + +\item{data_dir}{The directory where the data is stored.} } \value{ A spatial object containing the zones data. diff --git a/man/spo_load_zones_v1.Rd b/man/spod_load_zones_v1.Rd similarity index 79% rename from man/spo_load_zones_v1.Rd rename to man/spod_load_zones_v1.Rd index c476526..d2495a2 100644 --- a/man/spo_load_zones_v1.Rd +++ b/man/spod_load_zones_v1.Rd @@ -1,15 +1,18 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/get-v1-data.R -\name{spo_load_zones_v1} -\alias{spo_load_zones_v1} +\name{spod_load_zones_v1} +\alias{spod_load_zones_v1} \title{Loads the fixed zones for v1 data} \usage{ -spo_load_zones_v1(data_dir = get_data_dir(), type = "distritos") +spod_load_zones_v1( + type = c("distritos", "municipios"), + data_dir = get_data_dir() +) } \arguments{ -\item{data_dir}{The directory where the data is stored.} - \item{type}{The type of zones data to retrieve ("distritos" or "municipios").} + +\item{data_dir}{The directory where the data is stored.} } \value{ A spatial object of type \code{sf} containing the fixed zones data. @@ -23,6 +26,6 @@ The geometry is cheked for validity and fixed if necessary. The 'ID' column is r } \examples{ if (FALSE) { - zones <- spo_load_zones_v1() + zones <- spod_load_zones_v1() } } From d8cb82bd8eb9f734de97d7bcd62c532da2d363d3 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Mon, 5 Aug 2024 18:01:11 +0200 Subject: [PATCH 03/25] cleanup get zones v1 workflow --- NAMESPACE | 5 +- R/get-v1-data.R | 77 ++++++++++--------- R/get.R | 8 +- man/spod_available_data_v1.Rd | 4 +- man/spod_clean_zones_v1.Rd | 18 +++++ man/{get_data_dir.Rd => spod_get_data_dir.Rd} | 6 +- man/spod_get_latest_v1_file_list.Rd | 4 +- man/spod_get_zones_v1.Rd | 4 +- man/spod_load_zones_v1.Rd | 31 -------- 9 files changed, 73 insertions(+), 84 deletions(-) create mode 100644 man/spod_clean_zones_v1.Rd rename man/{get_data_dir.Rd => spod_get_data_dir.Rd} (83%) delete mode 100644 man/spod_load_zones_v1.Rd diff --git a/NAMESPACE b/NAMESPACE index 8c0dc8b..9d65e4a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,10 +1,9 @@ # Generated by roxygen2: do not edit by hand export(spod_available_data_v1) -export(spod_get_latest_v1_file_list) -export(spod_get_zones_v1) -export(spod_load_zones_v1) export(spod_get) +export(spod_get_latest_v1_file_list) export(spod_get_latest_v2_xml) export(spod_get_metadata) export(spod_get_zones) +export(spod_get_zones_v1) diff --git a/R/get-v1-data.R b/R/get-v1-data.R index 14285a4..4f1e29b 100644 --- a/R/get-v1-data.R +++ b/R/get-v1-data.R @@ -1,6 +1,6 @@ #' Get latest file list from the XML for MITMA open mobiltiy data v1 (2020-2021) #' -#' @param data_dir The directory where the data is stored. Defaults to the value returned by `get_data_dir()`. +#' @param data_dir The directory where the data is stored. Defaults to the value returned by `spod_get_data_dir()`. #' @param xml_url The URL of the XML file to download. Defaults to "https://opendata-movilidad.mitma.es/RSS.xml". #' #' @return The path to the downloaded XML file. @@ -10,7 +10,7 @@ #' spod_get_latest_v1_file_list() #' } spod_get_latest_v1_file_list <- function( - data_dir = get_data_dir(), + data_dir = spod_get_data_dir(), xml_url = "https://opendata-movilidad.mitma.es/RSS.xml") { if (!fs::dir_exists(data_dir)) { fs::dir_create(data_dir) @@ -32,7 +32,7 @@ spod_get_latest_v1_file_list <- function( #' #' This function provides a table of the available data list of MITMA v1 (2020-2021), both remote and local. #' -#' @param data_dir The directory where the data is stored. Defaults to the value returned by `get_data_dir()`. +#' @param data_dir The directory where the data is stored. Defaults to the value returned by `spod_get_data_dir()`. #' @return A tibble with links, release dates of files in the data, dates of data coverage, local paths to files, and the download status. #' \describe{ #' \item{target_url}{\code{character}. The URL link to the data file.} @@ -51,7 +51,7 @@ spod_get_latest_v1_file_list <- function( #' names(metadata) #' head(metadata) #' } -spod_available_data_v1 <- function(data_dir = get_data_dir()) { +spod_available_data_v1 <- function(data_dir = spod_get_data_dir()) { xml_files_list <- fs::dir_ls(data_dir, type = "file", regexp = "data_links_v1") |> sort() latest_data_links_xml_path <- utils::tail(xml_files_list, 1) @@ -114,20 +114,22 @@ spod_available_data_v1 <- function(data_dir = get_data_dir()) { #' @export #' @examples #' if (FALSE) { -#' zones <- get_zones() +#' zones <- spod_get_zones() #' } spod_get_zones_v1 <- function( type = c("distritos", "municipios"), - data_dir = get_data_dir()) { + data_dir = spod_get_data_dir()) { type <- match.arg(type) # check if shp files are already extracted - expected_shp_path <- fs::path(data_dir, glue::glue("v1/zonificacion-{type}/{type}_mitma.shp")) - if (fs::file_exists(expected_shp_path)) { - message(".shp file already exists in data dir: ", expected_shp_path) - return(sf::read_sf(expected_shp_path)) + expected_gpkg_path <- fs::path(data_dir, glue::glue("v1/clean/zones/{type}_mitma.gpkg")) + if (fs::file_exists(expected_gpkg_path)) { + message("Loading .gpkg file that already exists in data dir: ", expected_gpkg_path) + return(sf::read_sf(expected_gpkg_path)) } + # if data is not available, download, extract, clean and save it to gpkg + metadata <- spod_available_data_v1(data_dir) regex <- glue::glue("zonificacion_{type}\\.") sel_zones <- stringr::str_detect(metadata$target_url, regex) @@ -154,35 +156,27 @@ spod_get_zones_v1 <- function( junk_path <- paste0(fs::path_dir(downloaded_file), "/__MACOSX") if (fs::dir_exists(junk_path)) fs::dir_delete(junk_path) - shp_file <- fs::dir_ls(data_dir, glob = glue::glue("**{type}/*.shp"), recurse = TRUE) + zones_path <- fs::dir_ls(data_dir, glob = glue::glue("**{type}/*.shp"), recurse = TRUE) - suppressWarnings({ - return(sf::read_sf(shp_file)) - }) -} + zones <- spod_clean_zones_v1(zones_path) + fs::dir_create(fs::path_dir(expected_gpkg_path), recurse = TRUE) + sf::st_write(zones, expected_gpkg_path, delete_dsn = TRUE, delete_layer = TRUE) -#' Loads the fixed zones for v1 data -#' -#' This function loads the fixed zones data for the specified data directory. -#' The geometry is cheked for validity and fixed if necessary. The 'ID' column is renamed to 'id'. -#' -#' @param data_dir The directory where the data is stored. -#' @param type The type of zones data to retrieve ("distritos" or "municipios"). -#' @return A spatial object of type `sf` containing the fixed zones data. -#' \describe{ -#' \item{id}{\code{character}. The identifier of the zone.} -#' } -#' @export -#' @examples -#' if (FALSE) { -#' zones <- spod_load_zones_v1() -#' } -spod_load_zones_v1 <- function( - type = c("distritos", "municipios"), - data_dir = get_data_dir()) { - type <- match.arg(type) - zones <- spod_get_zones_v1(type = type, data_dir = data_dir) + return(zones) +} +#' Fixes common issues in the zones data and cleans up variable names +#' +#' This function fixes any invalid geometries in the zones data and renames the "ID" column to "id". +#' +#' @param zones_path The path to the zones spatial data file. +#' @return A spatial object of class `sf`. +#' @keywords internal +#' +spod_clean_zones_v1 <- function(zones_path) { + suppressWarnings({ + zones <- sf::read_sf(zones_path) + }) invalid_geometries <- !sf::st_is_valid(zones) if (sum(invalid_geometries) > 0) { fixed_zones <- sf::st_make_valid(zones[invalid_geometries, ]) @@ -191,3 +185,14 @@ spod_load_zones_v1 <- function( names(zones)[names(zones) == "ID"] <- "id" return(zones) } + +#' Retrieve the origin-destination v1 data (2020-2021) +#' +#' This function retrieves the origin-destination data from the specified data directory. +#' +# spod_get_od_v2 <- function( +# date_regex = "2020" +# data_dir = get_data_dir() +# ){ + +# } \ No newline at end of file diff --git a/R/get.R b/R/get.R index 694f180..62b5eb5 100644 --- a/R/get.R +++ b/R/get.R @@ -40,9 +40,7 @@ spod_get_latest_v2_xml = function( #' head(metadata) #' } spod_get_metadata = function(data_dir = spod_get_data_dir()) { - xml_files_list = fs::dir_ls(data_dir, type = "file", regexp = "data_links_") |> sort() -get_metadata = function(data_dir = get_data_dir()) { - xml_files_list = fs::dir_ls(data_dir, type = "file", regexp = "data_links_v2") |> sort() + xml_files_list = fs::dir_ls(data_dir, type = "file", regexp = "data_links_v2") |> sort() latest_data_links_xml_path = utils::tail(xml_files_list, 1) if (length(latest_data_links_xml_path) == 0) { message("Getting latest data links xml") @@ -83,7 +81,7 @@ get_metadata = function(data_dir = get_data_dir()) { #' #' @return The data directory. #' @keywords internal -get_data_dir = function() { +spod_get_data_dir = function() { data_dir_env = fs::path_real(Sys.getenv("SPANISH_OD_DATA_DIR")) if (data_dir_env == "") { data_dir_env = tempdir() @@ -182,4 +180,4 @@ download_od = function( } } return(metadata_od$local_path) -} +} \ No newline at end of file diff --git a/man/spod_available_data_v1.Rd b/man/spod_available_data_v1.Rd index ea77366..7214e0e 100644 --- a/man/spod_available_data_v1.Rd +++ b/man/spod_available_data_v1.Rd @@ -4,10 +4,10 @@ \alias{spod_available_data_v1} \title{Get the available v1 data list} \usage{ -spod_available_data_v1(data_dir = get_data_dir()) +spod_available_data_v1(data_dir = spod_get_data_dir()) } \arguments{ -\item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{get_data_dir()}.} +\item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{spod_get_data_dir()}.} } \value{ A tibble with links, release dates of files in the data, dates of data coverage, local paths to files, and the download status. diff --git a/man/spod_clean_zones_v1.Rd b/man/spod_clean_zones_v1.Rd new file mode 100644 index 0000000..7e376f4 --- /dev/null +++ b/man/spod_clean_zones_v1.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get-v1-data.R +\name{spod_clean_zones_v1} +\alias{spod_clean_zones_v1} +\title{Fixes common issues in the zones data and cleans up variable names} +\usage{ +spod_clean_zones_v1(zones_path) +} +\arguments{ +\item{zones_path}{The path to the zones spatial data file.} +} +\value{ +A spatial object of class \code{sf}. +} +\description{ +This function fixes any invalid geometries in the zones data and renames the "ID" column to "id". +} +\keyword{internal} diff --git a/man/get_data_dir.Rd b/man/spod_get_data_dir.Rd similarity index 83% rename from man/get_data_dir.Rd rename to man/spod_get_data_dir.Rd index fa9bce0..46ccf5d 100644 --- a/man/get_data_dir.Rd +++ b/man/spod_get_data_dir.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/get.R -\name{get_data_dir} -\alias{get_data_dir} +\name{spod_get_data_dir} +\alias{spod_get_data_dir} \title{Get the data directory} \usage{ -get_data_dir() +spod_get_data_dir() } \value{ The data directory. diff --git a/man/spod_get_latest_v1_file_list.Rd b/man/spod_get_latest_v1_file_list.Rd index 2e17ad3..8af96b0 100644 --- a/man/spod_get_latest_v1_file_list.Rd +++ b/man/spod_get_latest_v1_file_list.Rd @@ -5,12 +5,12 @@ \title{Get latest file list from the XML for MITMA open mobiltiy data v1 (2020-2021)} \usage{ spod_get_latest_v1_file_list( - data_dir = get_data_dir(), + data_dir = spod_get_data_dir(), xml_url = "https://opendata-movilidad.mitma.es/RSS.xml" ) } \arguments{ -\item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{get_data_dir()}.} +\item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{spod_get_data_dir()}.} \item{xml_url}{The URL of the XML file to download. Defaults to "https://opendata-movilidad.mitma.es/RSS.xml".} } diff --git a/man/spod_get_zones_v1.Rd b/man/spod_get_zones_v1.Rd index 644de9b..d096738 100644 --- a/man/spod_get_zones_v1.Rd +++ b/man/spod_get_zones_v1.Rd @@ -6,7 +6,7 @@ \usage{ spod_get_zones_v1( type = c("distritos", "municipios"), - data_dir = get_data_dir() + data_dir = spod_get_data_dir() ) } \arguments{ @@ -23,6 +23,6 @@ It can retrieve either "distritos" or "municipios" zones data. } \examples{ if (FALSE) { - zones <- get_zones() + zones <- spod_get_zones() } } diff --git a/man/spod_load_zones_v1.Rd b/man/spod_load_zones_v1.Rd deleted file mode 100644 index d2495a2..0000000 --- a/man/spod_load_zones_v1.Rd +++ /dev/null @@ -1,31 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/get-v1-data.R -\name{spod_load_zones_v1} -\alias{spod_load_zones_v1} -\title{Loads the fixed zones for v1 data} -\usage{ -spod_load_zones_v1( - type = c("distritos", "municipios"), - data_dir = get_data_dir() -) -} -\arguments{ -\item{type}{The type of zones data to retrieve ("distritos" or "municipios").} - -\item{data_dir}{The directory where the data is stored.} -} -\value{ -A spatial object of type \code{sf} containing the fixed zones data. -\describe{ -\item{id}{\code{character}. The identifier of the zone.} -} -} -\description{ -This function loads the fixed zones data for the specified data directory. -The geometry is cheked for validity and fixed if necessary. The 'ID' column is renamed to 'id'. -} -\examples{ -if (FALSE) { - zones <- spod_load_zones_v1() -} -} From 674a7a7b033abe9659e63de9ba9b1955652ae8fb Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Mon, 5 Aug 2024 21:58:19 +0200 Subject: [PATCH 04/25] separate raw cache data and clean data --- R/get-v1-data.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/get-v1-data.R b/R/get-v1-data.R index 4f1e29b..fd7fe6a 100644 --- a/R/get-v1-data.R +++ b/R/get-v1-data.R @@ -89,7 +89,7 @@ spod_available_data_v1 <- function(data_dir = spod_get_data_dir()) { files_table <- files_table[order(files_table$pub_ts, decreasing = TRUE), ] files_table$local_path <- file.path( data_dir, - stringr::str_replace(files_table$target_url, ".*mitma.es/", "/v1/") + stringr::str_replace(files_table$target_url, ".*mitma.es/", "raw_data_cache/v1/") ) files_table$local_path <- stringr::str_replace_all(files_table$local_path, "\\/\\/\\/|\\/\\/", "/") @@ -122,7 +122,7 @@ spod_get_zones_v1 <- function( type <- match.arg(type) # check if shp files are already extracted - expected_gpkg_path <- fs::path(data_dir, glue::glue("v1/clean/zones/{type}_mitma.gpkg")) + expected_gpkg_path <- fs::path(data_dir, glue::glue("clean_data/v1//zones/{type}_mitma.gpkg")) if (fs::file_exists(expected_gpkg_path)) { message("Loading .gpkg file that already exists in data dir: ", expected_gpkg_path) return(sf::read_sf(expected_gpkg_path)) @@ -190,7 +190,7 @@ spod_clean_zones_v1 <- function(zones_path) { #' #' This function retrieves the origin-destination data from the specified data directory. #' -# spod_get_od_v2 <- function( +# spod_get_od_v1 <- function( # date_regex = "2020" # data_dir = get_data_dir() # ){ From 38bff6b7aa2ca753f091f77631a3e98445717cd6 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Mon, 5 Aug 2024 23:27:38 +0200 Subject: [PATCH 05/25] internal utils to handle different data arguments and expand date regex --- R/internal-utils.R | 100 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 R/internal-utils.R diff --git a/R/internal-utils.R b/R/internal-utils.R new file mode 100644 index 0000000..17f743c --- /dev/null +++ b/R/internal-utils.R @@ -0,0 +1,100 @@ +# Description: Internal utility functions for the MITMA package + +#' Process multiple date arguments +#' +#' This function processes the date arguments provided to various functions in the package. +#' It checks if more than one date argument is provided and returns the appropriate dates as a POSIXct vector. +#' The function ensures that the dates are in ISO format (yyyy-mm-dd) or yyyymmdd format. +#' +#' @param date_range A vector of dates in ISO format (yyyy-mm-dd) or yyyymmdd format. +#' @param dates_list A vector of dates in ISO format (yyyy-mm-dd) or yyyymmdd format. +#' @param date_regex A regular expression to match dates in the format yyyymmdd. +#' @return A POSIXct vector of dates. +#' @keywords internal +process_date_arguments <- function(date_range = NULL, + dates_list = NULL, + date_regex = NULL, + data_ver = c("v1", "v2") +) { + data_ver <- match.arg(data_ver) + + # Helper function to check if dates are in ISO format or yyyymmdd format + is_valid_date <- function(dates) { + all(sapply(dates, function(date) { + !is.na(lubridate::ymd(date)) || !is.na(as.Date(date, format = "%Y%m%d")) + })) + } + + # Convert valid dates to POSIXct + convert_to_posixct <- function(dates) { + as.POSIXct(sapply(dates, function(date) { + if (!is.na(lubridate::ymd(date))) { + lubridate::ymd(date) + } else { + as.Date(date, format = "%Y%m%d") + } + }), tz = "UTC") + } + + # Check if more than one date argument is provided + args_provided <- list( + date_range = !is.null(date_range), + dates_list = !is.null(dates_list), + date_regex = !is.null(date_regex) + ) + + # Count how many date arguments are set + args_set <- sum(unlist(args_provided)) + + # Assert that only one argument is set + assertthat::assert_that(args_set <= 1, + msg = "Only one of the date arguments (date_range, dates_list, date_regex) should be set.") + + # Determine which date argument to use and return the appropriate dates + if (!is.null(date_range)) { + assertthat::assert_that(is_valid_date(date_range), + msg = "date_range must be in ISO format (yyyy-mm-dd) or yyyymmdd format.") + return(convert_to_posixct(date_range)) + + } else if (!is.null(dates_list)) { + assertthat::assert_that(is_valid_date(dates_list), + msg = "dates_list must be in ISO format (yyyy-mm-dd) or yyyymmdd format.") + return(convert_to_posixct(dates_list)) + + } else if (!is.null(date_regex)) { + dates <- expand_dates_from_regex(date_regex, data_ver = data_ver) + return(convert_to_posixct(dates)) + } +} + +#' Function to expand dates from a regex +#' +#' This function generates a sequence of dates from a regular expression pattern. +#' based on the provided regular expression. +#' +#' @param date_regex A regular expression to match dates in the format yyyymmdd. +#' @param data_ver The version of the data to use. Defaults to "v1". Can be "v1" or "v2". +#' @return A character vector of dates matching the regex. +#' @keywords internal +expand_dates_from_regex <- function(date_regex, + data_ver = c("v1", "v2") +) { + data_ver <- match.arg(data_ver) + + if(data_ver == "v1") { + available_data <- spod_available_data_v1() + date_range <- range(available_data[grepl("maestra2.*diarios", available_data$target_url),]$data_ymd, na.rm = TRUE) + } else if(data_ver == "v2") { + available_data <- spod_get_metadata() # replace with spod_available_data_v2() when available + date_range <- range(available_data[grepl("viajes.*diarios", available_data$target_url),]$data_ymd, na.rm = TRUE) + } + start_date <- date_range[1] + end_date <- date_range[2] + + all_dates <- seq.Date(start_date, end_date, by = "day") + + # Filter dates matching the regex + matching_dates <- all_dates[grepl(date_regex, format(all_dates, "%Y%m%d"))] + + return(as.character(matching_dates)) +} From ea6706b7f284a664cbdfba21fc49b6ff3f33626b Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Mon, 5 Aug 2024 23:28:01 +0200 Subject: [PATCH 06/25] skeleton for the get v1 od function --- DESCRIPTION | 1 + R/get-v1-data.R | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 289a7a2..d258756 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -21,6 +21,7 @@ License: MIT + file LICENSE Depends: R (>= 3.5.0) Imports: + assertthat, curl, DBI, duckdb, diff --git a/R/get-v1-data.R b/R/get-v1-data.R index fd7fe6a..f70e5fa 100644 --- a/R/get-v1-data.R +++ b/R/get-v1-data.R @@ -190,9 +190,20 @@ spod_clean_zones_v1 <- function(zones_path) { #' #' This function retrieves the origin-destination data from the specified data directory. #' -# spod_get_od_v1 <- function( -# date_regex = "2020" -# data_dir = get_data_dir() -# ){ +spod_get_od_v1 <- function( + date_range = c("2020-02-14", "2020-02-15"), + dates_list = NULL, + date_regex = NULL, + data_dir = spod_get_data_dir() +){ + # Process the date arguments to get a vector of dates to use + dates_to_use <- process_date_arguments( + date_range, dates_list, date_regex, + data_ver = "v1" + ) + -# } \ No newline at end of file + message("Retrieved data for dates: ", paste(dates_to_use, collapse = ", ")) + + +} \ No newline at end of file From 8cbc1beaec5c9c55245361b3a24e74bd2f0a09dd Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 6 Aug 2024 11:57:02 +0200 Subject: [PATCH 07/25] use temp dir if SPANISH_OD_DATA_DIR is not set. also give a warning when temp used --- R/get.R | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/R/get.R b/R/get.R index 62b5eb5..c8a1028 100644 --- a/R/get.R +++ b/R/get.R @@ -82,7 +82,14 @@ spod_get_metadata = function(data_dir = spod_get_data_dir()) { #' @return The data directory. #' @keywords internal spod_get_data_dir = function() { - data_dir_env = fs::path_real(Sys.getenv("SPANISH_OD_DATA_DIR")) + data_dir_env = fs::path_real( + Sys.getenv("SPANISH_OD_DATA_DIR", + { + warning("Warning: SPANISH_OD_DATA_DIR is not set. Using the temporary directory, which is not recommended, as the data will be deleted when the session ends.\n\n To set the data directory, use Sys.setenv(SPANISH_OD_DATA_DIR = '/path/to/data').") + tempdir() # if not set, use the temp directory + } + ) + ) if (data_dir_env == "") { data_dir_env = tempdir() } From e05ef8ddd6dc4fea1dd179fb199cbfda21398012 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 6 Aug 2024 12:12:06 +0200 Subject: [PATCH 08/25] unset of sys.getenv did not work, plug back the if statement --- R/get.R | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/R/get.R b/R/get.R index c8a1028..0359b6b 100644 --- a/R/get.R +++ b/R/get.R @@ -82,18 +82,12 @@ spod_get_metadata = function(data_dir = spod_get_data_dir()) { #' @return The data directory. #' @keywords internal spod_get_data_dir = function() { - data_dir_env = fs::path_real( - Sys.getenv("SPANISH_OD_DATA_DIR", - { - warning("Warning: SPANISH_OD_DATA_DIR is not set. Using the temporary directory, which is not recommended, as the data will be deleted when the session ends.\n\n To set the data directory, use Sys.setenv(SPANISH_OD_DATA_DIR = '/path/to/data').") - tempdir() # if not set, use the temp directory - } - ) - ) - if (data_dir_env == "") { - data_dir_env = tempdir() + data_dir_env = Sys.getenv("SPANISH_OD_DATA_DIR") + if( data_dir_env == "" ) { + warning("Warning: SPANISH_OD_DATA_DIR is not set. Using the temporary directory, which is not recommended, as the data will be deleted when the session ends.\n\n To set the data directory, use Sys.setenv(SPANISH_OD_DATA_DIR = '/path/to/data').") + data_dir_env = tempdir() # if not set, use the temp directory } - return(data_dir_env) + return(fs::path_real(data_dir_env)) } #' Retrieves the zones data From a56b2fbb9334ad6d7b75ededd64ed4ef0e0f3f75 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 6 Aug 2024 12:24:13 +0200 Subject: [PATCH 09/25] more detailed warning and suggestsions on temp dir use --- R/get.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/get.R b/R/get.R index 0359b6b..56a5afe 100644 --- a/R/get.R +++ b/R/get.R @@ -84,7 +84,7 @@ spod_get_metadata = function(data_dir = spod_get_data_dir()) { spod_get_data_dir = function() { data_dir_env = Sys.getenv("SPANISH_OD_DATA_DIR") if( data_dir_env == "" ) { - warning("Warning: SPANISH_OD_DATA_DIR is not set. Using the temporary directory, which is not recommended, as the data will be deleted when the session ends.\n\n To set the data directory, use Sys.setenv(SPANISH_OD_DATA_DIR = '/path/to/data').") + warning("Warning: SPANISH_OD_DATA_DIR is not set. Using the temporary directory, which is not recommended, as the data will be deleted when the session ends.\n\n To set the data directory, use `Sys.setenv(SPANISH_OD_DATA_DIR = '/path/to/data')` or set SPANISH_OD_DATA_DIR permanently in the environment by editing the `.Renviron` file locally for current project with `usethis::edit_r_environ('project')` or `file.edit('.Renviron')` or globally for all projects with `usethis::edit_r_environ('user')` or `file.edit('~/.Renviron')`.") data_dir_env = tempdir() # if not set, use the temp directory } return(fs::path_real(data_dir_env)) From 0e0798da709d1e348b2ac776af08608d599c2767 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 6 Aug 2024 15:26:59 +0200 Subject: [PATCH 10/25] fixes to metadata retrieval, first draft of smart download function --- DESCRIPTION | 1 + NAMESPACE | 1 + R/folders.R | 9 ++ R/get-v1-data.R | 174 ++++++++++++++++++++++++++++++--- R/internal-utils.R | 110 ++++++++++++++++----- man/expand_dates_from_regex.Rd | 21 ++++ man/process_date_arguments.Rd | 29 ++++++ man/spod_available_data_v1.Rd | 5 +- man/spod_download_tables.Rd | 35 +++++++ man/spod_get_od_v1.Rd | 18 ++++ man/spod_match_data_type.Rd | 21 ++++ 11 files changed, 383 insertions(+), 41 deletions(-) create mode 100644 R/folders.R create mode 100644 man/expand_dates_from_regex.Rd create mode 100644 man/process_date_arguments.Rd create mode 100644 man/spod_download_tables.Rd create mode 100644 man/spod_get_od_v1.Rd create mode 100644 man/spod_match_data_type.Rd diff --git a/DESCRIPTION b/DESCRIPTION index d258756..464f6ae 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -30,6 +30,7 @@ Imports: lubridate, purrr, readr, + rlang (>= 1.1.0), sf, stringr, tibble, diff --git a/NAMESPACE b/NAMESPACE index 9d65e4a..0c236a6 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,6 +1,7 @@ # Generated by roxygen2: do not edit by hand export(spod_available_data_v1) +export(spod_download_tables) export(spod_get) export(spod_get_latest_v1_file_list) export(spod_get_latest_v2_xml) diff --git a/R/folders.R b/R/folders.R new file mode 100644 index 0000000..f3cfe9e --- /dev/null +++ b/R/folders.R @@ -0,0 +1,9 @@ +# change subfolder name for raw data cache here to apply globally +spod_subfolder_raw_data_cache <- function(ver = 1) { + rlang:::check_number_whole(ver) + if (!ver %in% c(1, 2)) { + stop("Invalid version number. Must be 1 or 2.") + } + base_subdir_name <- "raw_data_cache" + return(paste0(base_subdir_name, "/v", ver, "/")) +} \ No newline at end of file diff --git a/R/get-v1-data.R b/R/get-v1-data.R index f70e5fa..c9a7729 100644 --- a/R/get-v1-data.R +++ b/R/get-v1-data.R @@ -51,7 +51,10 @@ spod_get_latest_v1_file_list <- function( #' names(metadata) #' head(metadata) #' } -spod_available_data_v1 <- function(data_dir = spod_get_data_dir()) { +spod_available_data_v1 <- function(data_dir = spod_get_data_dir(), + # check_local_files (below) is FALSE by default to avoid excessive filesystem access, perhaps should be TRUE. Download functions use it to load the xml file, but we probably do not want the script to check all local cache directories every time we run a get data function. Perhaps it is better to offload this check to a separate function and have a csv file or some other way to keep track of the files that were downloaded and cached. An output of curl::multi_download() could be used for this purpose. + check_local_files = FALSE +) { xml_files_list <- fs::dir_ls(data_dir, type = "file", regexp = "data_links_v1") |> sort() latest_data_links_xml_path <- utils::tail(xml_files_list, 1) @@ -67,10 +70,9 @@ spod_available_data_v1 <- function(data_dir = spod_get_data_dir()) { if (length(latest_data_links_xml_path) == 0) { message("Getting latest data links xml") - latest_data_links_xml_path <- get_latest_v2_xml(data_dir = data_dir) + latest_data_links_xml_path <- spod_get_latest_v1_file_list(data_dir = data_dir) } - x_xml <- xml2::read_xml(latest_data_links_xml_path) files_table <- tibble::tibble( @@ -89,7 +91,7 @@ spod_available_data_v1 <- function(data_dir = spod_get_data_dir()) { files_table <- files_table[order(files_table$pub_ts, decreasing = TRUE), ] files_table$local_path <- file.path( data_dir, - stringr::str_replace(files_table$target_url, ".*mitma.es/", "raw_data_cache/v1/") + stringr::str_replace(files_table$target_url, ".*mitma.es/", spod_subfolder_raw_data_cache(ver = 1)) ) files_table$local_path <- stringr::str_replace_all(files_table$local_path, "\\/\\/\\/|\\/\\/", "/") @@ -97,6 +99,15 @@ spod_available_data_v1 <- function(data_dir = spod_get_data_dir()) { # change path for daily data files to be in hive-style format files_table$local_path <- gsub("([0-9]{4})-([0-9]{2})\\/[0-9]{6}([0-9]{2})_", "year=\\1\\/month=\\2\\/day=\\3\\/", files_table$local_path) + # fix paths for files that are in '0000-referencia' folder + files_table$local_path <- gsub("0000-referencia\\/([0-9]{4})([0-9]{2})([0-9]{2})_", "year=\\1\\/month=\\2\\/day=\\3\\/", files_table$local_path) + + # replace 2 digit month with 1 digit month + files_table$local_path <- gsub("month=0([1-9])", "month=\\1", files_table$local_path) + + # replace 2 digit day with 1 digit day + files_table$local_path <- gsub("day=0([1-9])", "day=\\1", files_table$local_path) + # now check if any of local files exist files_table$downloaded <- fs::file_exists(files_table$local_path) @@ -117,8 +128,9 @@ spod_available_data_v1 <- function(data_dir = spod_get_data_dir()) { #' zones <- spod_get_zones() #' } spod_get_zones_v1 <- function( - type = c("distritos", "municipios"), - data_dir = spod_get_data_dir()) { + type = c("distritos", "municipios"), + data_dir = spod_get_data_dir() +) { type <- match.arg(type) # check if shp files are already extracted @@ -130,7 +142,7 @@ spod_get_zones_v1 <- function( # if data is not available, download, extract, clean and save it to gpkg - metadata <- spod_available_data_v1(data_dir) + metadata <- spod_available_data_v1(data_dir, check_local_files = FALSE) regex <- glue::glue("zonificacion_{type}\\.") sel_zones <- stringr::str_detect(metadata$target_url, regex) metadata_zones <- metadata[sel_zones, ] @@ -194,16 +206,152 @@ spod_get_od_v1 <- function( date_range = c("2020-02-14", "2020-02-15"), dates_list = NULL, date_regex = NULL, + zones = c("distritos", "municipios"), + data_dir = spod_get_data_dir(), + read_fun = duckdb::tbl_file +) { + # Processing of the date arguments is performed in subsequent functions, because we would want the downloading functions to be able to use the same date arguments for flexibility. So `spod_download_tables()` will handle the date arguments.` + + zones <- match.arg(zones) + + # check the locally cached and online available data + # and download missing files if any + # get local paths of the files requested with date arguments + metadata <- spod_download_od_v1( + date_range = date_range, + dates_list = dates_list, + date_regex = date_regex, + zones = zones + ) + + # read data from cached files + + + +} + + + +spod_download_od_v1 <- function( + date_range = c("2020-02-14", "2020-02-15"), + dates_list = NULL, + date_regex = NULL, + zones = c("districts", "dist", "distr", + "municipalities", "muni", "municip"), data_dir = spod_get_data_dir() -){ - # Process the date arguments to get a vector of dates to use - dates_to_use <- process_date_arguments( - date_range, dates_list, date_regex, - data_ver = "v1" +) { + zones <- match.arg(zones) + zones <- spod_zone_names_en2es(zones) + + downloaded_files <- spod_download_tables( + date_range = date_range, + dates_list = dates_list, + date_regex = date_regex, + subdir = glue::glue("v1/maestra2-mitma-{zones}/ficheros-diarios/"), + data_dir = data_dir ) + + return(downloaded_files) +} + +#' Download the data files of specified type, zones, dates and data version +#' +#' This function downloads the data files of the specified type, zones, dates and data version. +#' @param type The type of data to download. Can be "origin-destination" (or ust "od"), or "trips_per_person" (or just "tpp") for v1 data. For v2 data "overnight_stays" (or just "os") is also available. More data types to be supported in the future. See respective codebooks for more information. [ADD CODEBOOKS!] +#' @param zones The zones for which to download the data. Can be "districts" (or "dist", "distr") or "municipalities" (or "muni", "municip") for v1 data. Additionaly, these can be "urban_areas" (GAUs) for v2 data. +#' @param date_range A character vector of dates in ISO format (YYYY-MM-DD) to download the data for. +#' @param dates_list A character vector of dates in ISO format (YYYY-MM-DD) to download the data for. Defaults to NULL. +#' @param date_regex A regular expression to match the dates of the data to download. Defaults to NULL. +#' @param ver The version of the data to use. Defaults to 1. Can be 1 or 2. +#' @param data_dir The directory where the data is stored. Defaults to the value returned by `spod_get_data_dir()` which returns the value of the environment variable `SPANISH_OD_DATA_DIR` or a temporary directory if the variable is not set. +#' +#' @export +#' @example +#'\dontrun{ +#' spod_download_tables(type = "od", zones = "districts", date_range = c("2020-02-14", "2020-02-15")) +#' spod_download_tables(type = "os", zones = "municipalities", date_range = c("2020-02-14", "2020-02-15")) +#' spod_download_tables(type = "tpp", zones = "districts", date_range = c("2020-02-14", "2020-02-15")) +#' } +spod_download_tables <- function( + type = c( + "od", "origin-destination", + "os", "overnight_stays", + "tpp", "trips_per_person"), + zones = c("districts", "dist", "distr", + "municipalities", "muni", "municip"), # add "urban_areas" for v2 data + date_range = NULL, + dates_list = NULL, + date_regex = NULL, + ver = 1, # infer version from dates? + data_dir = spod_get_data_dir() +) { + # convert english data type names to spanish words used in the default data paths + type <- match.arg(type) + type <- spod_match_data_type(type = type, ver = ver) + + # convert english zone names to spanish words used in the default data paths + zones <- match.arg(zones) + zones <- spod_zone_names_en2es(zones) - message("Retrieved data for dates: ", paste(dates_to_use, collapse = ", ")) + + # this is where the date arguments are processed + # for all the wrapper functions that call this worker function + dates_to_use <- process_date_arguments( + date_range = date_range, + dates_list = dates_list, + date_regex = date_regex, + ver = ver + ) + + # check version + # replace this argument with automatic version detection based on the dates requested? + rlang:::check_number_whole(ver) + if (!ver %in% c(1, 2)) { + stop("Invalid version number. Must be 1 or 2.") + } + + # get the available data list while checking for files already cached on disk + if( ver == 1) { + metadata <- spod_available_data_v1(data_dir = data_dir, + check_local_files = TRUE) + } else if (ver == 2) { + metadata <- spod_get_metadata(data_dir = data_dir) + # replace with spod_available_data_v2() when available, spod_get_metadata can become a wrapper with v1/v2 argument. Potentially we can even automaticaly detect the data version based on the time intervals that user requests, but this is a bit controversial, as the methodology behind v1 and v2 data generation is not the same and Nommon+MITMA do not recommend mixing those together and comparing absoloute numbers of trips. + } + # match the metadata to type, zones, version and dates + if(ver == 1){ + requested_files <- metadata[ + grepl(glue::glue("v{ver}.*{type}.*{zones}"), metadata$local_path) & + metadata$data_ymd %in% dates_to_use, + ] + } else if(ver == 2){ + requested_files <- metadata[ + grepl(glue::glue("v{ver}.*{zones}.*{type}"), metadata$local_path) & + metadata$data_ymd %in% dates_to_use, + ] + } + + files_to_download <- requested_files[!requested_files$downloaded, ] + # pre-generate target paths for the files to download + fs::dir_create( + unique(fs::path_dir(files_to_download$local_path)), + recurse = TRUE) + + # download the missing files + downloaded_files <- curl::multi_download( + urls = files_to_download$target_url, + destfiles = files_to_download$local_path, + progress = TRUE, + resume = TRUE + ) + + # set download status for downloaded files as TRUE in requested_files + requested_files$downloaded[requested_files$local_path %in% downloaded_files$destfile] <- TRUE + + message("Retrieved data for requested dates: ", paste(dates_to_use, collapse = ", ")) # this may output too many dates, shoudl be fixed when we create a flexible date argument processing function. Keeping for now. + + return(requested_files$local_path) } \ No newline at end of file diff --git a/R/internal-utils.R b/R/internal-utils.R index 17f743c..d776bd3 100644 --- a/R/internal-utils.R +++ b/R/internal-utils.R @@ -3,20 +3,26 @@ #' Process multiple date arguments #' #' This function processes the date arguments provided to various functions in the package. -#' It checks if more than one date argument is provided and returns the appropriate dates as a POSIXct vector. +#' It checks if more than one date argument is provided and returns the appropriate dates as a Dates vector. #' The function ensures that the dates are in ISO format (yyyy-mm-dd) or yyyymmdd format. #' #' @param date_range A vector of dates in ISO format (yyyy-mm-dd) or yyyymmdd format. #' @param dates_list A vector of dates in ISO format (yyyy-mm-dd) or yyyymmdd format. #' @param date_regex A regular expression to match dates in the format yyyymmdd. -#' @return A POSIXct vector of dates. +#' @return A Dates vector of dates. #' @keywords internal -process_date_arguments <- function(date_range = NULL, +process_date_arguments <- function( + date_range = NULL, dates_list = NULL, date_regex = NULL, - data_ver = c("v1", "v2") + ver = c(1, 2) ) { - data_ver <- match.arg(data_ver) + rlang:::check_number_whole(ver) + if (!ver %in% c(1, 2)) { + stop("Invalid version number. Must be 1 or 2.") + } + + # perhaps this funciton should automatically detect the type of dates passed. This will dramatically simplify the API and allow the download and get functions to have just one argument for dates that will accept any format. # Helper function to check if dates are in ISO format or yyyymmdd format is_valid_date <- function(dates) { @@ -25,16 +31,6 @@ process_date_arguments <- function(date_range = NULL, })) } - # Convert valid dates to POSIXct - convert_to_posixct <- function(dates) { - as.POSIXct(sapply(dates, function(date) { - if (!is.na(lubridate::ymd(date))) { - lubridate::ymd(date) - } else { - as.Date(date, format = "%Y%m%d") - } - }), tz = "UTC") - } # Check if more than one date argument is provided args_provided <- list( @@ -54,16 +50,16 @@ process_date_arguments <- function(date_range = NULL, if (!is.null(date_range)) { assertthat::assert_that(is_valid_date(date_range), msg = "date_range must be in ISO format (yyyy-mm-dd) or yyyymmdd format.") - return(convert_to_posixct(date_range)) + return(date_range) } else if (!is.null(dates_list)) { assertthat::assert_that(is_valid_date(dates_list), msg = "dates_list must be in ISO format (yyyy-mm-dd) or yyyymmdd format.") - return(convert_to_posixct(dates_list)) + return(dates_list) } else if (!is.null(date_regex)) { - dates <- expand_dates_from_regex(date_regex, data_ver = data_ver) - return(convert_to_posixct(dates)) + dates <- expand_dates_from_regex(date_regex, ver = ver) + return(dates) } } @@ -73,19 +69,25 @@ process_date_arguments <- function(date_range = NULL, #' based on the provided regular expression. #' #' @param date_regex A regular expression to match dates in the format yyyymmdd. -#' @param data_ver The version of the data to use. Defaults to "v1". Can be "v1" or "v2". +#' @param ver The version of the data to use. Defaults to "v1". Can be "v1" or "v2". #' @return A character vector of dates matching the regex. #' @keywords internal expand_dates_from_regex <- function(date_regex, - data_ver = c("v1", "v2") + ver = c(1, 2) ) { - data_ver <- match.arg(data_ver) + + # currently checks for date range for od data only. not all datasets may be available for all dates, so this function may need to be updated to check for the availability of the specific for the requested dates. spod_match_data_type() helper in the same file may be useful here. + + rlang:::check_number_whole(ver) + if (!ver %in% c(1, 2)) { + stop("Invalid version number. Must be 1 or 2.") + } - if(data_ver == "v1") { - available_data <- spod_available_data_v1() - date_range <- range(available_data[grepl("maestra2.*diarios", available_data$target_url),]$data_ymd, na.rm = TRUE) - } else if(data_ver == "v2") { - available_data <- spod_get_metadata() # replace with spod_available_data_v2() when available + if(ver == 1) { + available_data <- spod_available_data_v1(check_local_files = FALSE) + date_range <- range(available_data[grepl("maestra1.*diarios", available_data$target_url),]$data_ymd, na.rm = TRUE) + } else if(ver == 2) { + available_data <- spod_get_metadata() # replace with spod_available_data_v2() when available, spod_get_metadata can become a wrapper with v1/v2 argument. Potentially we can even automaticaly detect the data version based on the time intervals that user requests, but this is a bit controversial, as the methodology behind v1 and v2 data generation is not the same and Nommon+MITMA do not recommend mixing those together and comparing absoloute numbers of trips. date_range <- range(available_data[grepl("viajes.*diarios", available_data$target_url),]$data_ymd, na.rm = TRUE) } start_date <- date_range[1] @@ -98,3 +100,57 @@ expand_dates_from_regex <- function(date_regex, return(as.character(matching_dates)) } + +spod_zone_names_en2es <- function( + zones = c("districts", "dist", "distr", + "municipalities", "muni", "municip") +) { + zones <- tolower(zones) + zones <- match.arg(zones) + if(zones %in% c("districts", "dist", "distr")) { + return("distritos") + } else if(zones %in% c("municipalities", "muni", "municip")) { + return("municipios") + } +} + +#' Match data types to folders +#' @param type The type of data to match. Can be "od", "origin-destination", "os", "overnight_stays", or "tpp", "trips_per_person". +#' @param ver The version of the data to use. Defaults to 1. Can be 1 or 2. +#' @keywords internal +spod_match_data_type <- function( + type = c( + "od", "origin-destination", + "os", "overnight_stays", + "tpp", "trips_per_person"), + ver = c(1, 2) +){ + rlang:::check_number_whole(ver) + if (!ver %in% c(1, 2)) { + stop("Invalid version number. Must be 1 or 2.") + } + + type <- tolower(type) + type <- match.arg(type) + + if(ver == 1) { + if (type %in% c("od", "origin-destination")) { + return("maestra1") + } else if(type %in% c("tpp", "trips_per_person")) { + return("maestra2") + } + } + + if(ver == 2) { + if (type %in% c("od", "origin-destination")) { + return("viajes") + } else if(type %in% c("os", "overnight_stays")) { + return("pernoctaciones") + } else if(type %in% c("tpp", "trips_per_person")) { + return("personas") + } + } + + # need to add a warning here that the type is not recognized + return(NULL) +} \ No newline at end of file diff --git a/man/expand_dates_from_regex.Rd b/man/expand_dates_from_regex.Rd new file mode 100644 index 0000000..15351ea --- /dev/null +++ b/man/expand_dates_from_regex.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/internal-utils.R +\name{expand_dates_from_regex} +\alias{expand_dates_from_regex} +\title{Function to expand dates from a regex} +\usage{ +expand_dates_from_regex(date_regex, ver = c(1, 2)) +} +\arguments{ +\item{date_regex}{A regular expression to match dates in the format yyyymmdd.} + +\item{ver}{The version of the data to use. Defaults to "v1". Can be "v1" or "v2".} +} +\value{ +A character vector of dates matching the regex. +} +\description{ +This function generates a sequence of dates from a regular expression pattern. +based on the provided regular expression. +} +\keyword{internal} diff --git a/man/process_date_arguments.Rd b/man/process_date_arguments.Rd new file mode 100644 index 0000000..57a629e --- /dev/null +++ b/man/process_date_arguments.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/internal-utils.R +\name{process_date_arguments} +\alias{process_date_arguments} +\title{Process multiple date arguments} +\usage{ +process_date_arguments( + date_range = NULL, + dates_list = NULL, + date_regex = NULL, + ver = c(1, 2) +) +} +\arguments{ +\item{date_range}{A vector of dates in ISO format (yyyy-mm-dd) or yyyymmdd format.} + +\item{dates_list}{A vector of dates in ISO format (yyyy-mm-dd) or yyyymmdd format.} + +\item{date_regex}{A regular expression to match dates in the format yyyymmdd.} +} +\value{ +A Dates vector of dates. +} +\description{ +This function processes the date arguments provided to various functions in the package. +It checks if more than one date argument is provided and returns the appropriate dates as a Dates vector. +The function ensures that the dates are in ISO format (yyyy-mm-dd) or yyyymmdd format. +} +\keyword{internal} diff --git a/man/spod_available_data_v1.Rd b/man/spod_available_data_v1.Rd index 7214e0e..09c8512 100644 --- a/man/spod_available_data_v1.Rd +++ b/man/spod_available_data_v1.Rd @@ -4,7 +4,10 @@ \alias{spod_available_data_v1} \title{Get the available v1 data list} \usage{ -spod_available_data_v1(data_dir = spod_get_data_dir()) +spod_available_data_v1( + data_dir = spod_get_data_dir(), + check_local_files = FALSE +) } \arguments{ \item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{spod_get_data_dir()}.} diff --git a/man/spod_download_tables.Rd b/man/spod_download_tables.Rd new file mode 100644 index 0000000..a201c3e --- /dev/null +++ b/man/spod_download_tables.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get-v1-data.R +\name{spod_download_tables} +\alias{spod_download_tables} +\title{Download the data files of specified type, zones, dates and data version} +\usage{ +spod_download_tables( + type = c("od", "origin-destination", "os", "overnight_stays", "tpp", + "trips_per_person"), + zones = c("districts", "dist", "distr", "municipalities", "muni", "municip"), + date_range = NULL, + dates_list = NULL, + date_regex = NULL, + ver = 1, + data_dir = spod_get_data_dir() +) +} +\arguments{ +\item{type}{The type of data to download. Can be "origin-destination" (or ust "od"), or "trips_per_person" (or just "tpp") for v1 data. For v2 data "overnight_stays" (or just "os") is also available. More data types to be supported in the future. See respective codebooks for more information. \link{ADD CODEBOOKS!}} + +\item{zones}{The zones for which to download the data. Can be "districts" (or "dist", "distr") or "municipalities" (or "muni", "municip") for v1 data. Additionaly, these can be "urban_areas" (GAUs) for v2 data.} + +\item{date_range}{A character vector of dates in ISO format (YYYY-MM-DD) to download the data for.} + +\item{dates_list}{A character vector of dates in ISO format (YYYY-MM-DD) to download the data for. Defaults to NULL.} + +\item{date_regex}{A regular expression to match the dates of the data to download. Defaults to NULL.} + +\item{ver}{The version of the data to use. Defaults to 1. Can be 1 or 2.} + +\item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{spod_get_data_dir()} which returns the value of the environment variable \code{SPANISH_OD_DATA_DIR} or a temporary directory if the variable is not set.} +} +\description{ +This function downloads the data files of the specified type, zones, dates and data version. +} diff --git a/man/spod_get_od_v1.Rd b/man/spod_get_od_v1.Rd new file mode 100644 index 0000000..c608016 --- /dev/null +++ b/man/spod_get_od_v1.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get-v1-data.R +\name{spod_get_od_v1} +\alias{spod_get_od_v1} +\title{Retrieve the origin-destination v1 data (2020-2021)} +\usage{ +spod_get_od_v1( + date_range = c("2020-02-14", "2020-02-15"), + dates_list = NULL, + date_regex = NULL, + zones = c("distritos", "municipios"), + data_dir = spod_get_data_dir(), + read_fun = duckdb::tbl_file +) +} +\description{ +This function retrieves the origin-destination data from the specified data directory. +} diff --git a/man/spod_match_data_type.Rd b/man/spod_match_data_type.Rd new file mode 100644 index 0000000..76b7295 --- /dev/null +++ b/man/spod_match_data_type.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/internal-utils.R +\name{spod_match_data_type} +\alias{spod_match_data_type} +\title{Match data types to folders} +\usage{ +spod_match_data_type( + type = c("od", "origin-destination", "os", "overnight_stays", "tpp", + "trips_per_person"), + ver = c(1, 2) +) +} +\arguments{ +\item{type}{The type of data to match. Can be "od", "origin-destination", "os", "overnight_stays", or "tpp", "trips_per_person".} + +\item{ver}{The version of the data to use. Defaults to 1. Can be 1 or 2.} +} +\description{ +Match data types to folders +} +\keyword{internal} From a6d542af2a161c436cadc184e99c2f0ded6644dd Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 6 Aug 2024 16:20:40 +0200 Subject: [PATCH 11/25] update spod_download_tables to be spod_download_data, various usage examples with date arguments --- R/get-v1-data.R | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/R/get-v1-data.R b/R/get-v1-data.R index c9a7729..65a75a3 100644 --- a/R/get-v1-data.R +++ b/R/get-v1-data.R @@ -243,7 +243,7 @@ spod_download_od_v1 <- function( zones <- match.arg(zones) zones <- spod_zone_names_en2es(zones) - downloaded_files <- spod_download_tables( + downloaded_files <- spod_download_data( date_range = date_range, dates_list = dates_list, date_regex = date_regex, @@ -268,11 +268,16 @@ spod_download_od_v1 <- function( #' @export #' @example #'\dontrun{ -#' spod_download_tables(type = "od", zones = "districts", date_range = c("2020-02-14", "2020-02-15")) -#' spod_download_tables(type = "os", zones = "municipalities", date_range = c("2020-02-14", "2020-02-15")) -#' spod_download_tables(type = "tpp", zones = "districts", date_range = c("2020-02-14", "2020-02-15")) +#' # Download the origin-destination on district level for the a date range in March 2020 +#' spod_download_tables(type = "od", zones = "districts", date_range = c("2020-03-20", "2020-03-24"), ver = 1) +#' +#' # Download the origin-destination on district level for select dates in 2020 and 2021 +#' spod_download_tables(type = "od", zones = "districts", dates_list = c("2020-03-20", "2020-03-24", "2021-03-20", "2021-03-24"), ver = 1) +#' +#' # Download the origin-destination on district level using regex for a date range in March 2020 (the regex will capture the dates 2020-03-20 to 2020-03-24) +#' spod_download_tables(type = "od", zones = "districts", date_regex = "2020032[0-4]", ver = 1) #' } -spod_download_tables <- function( +spod_download_data <- function( type = c( "od", "origin-destination", "os", "overnight_stays", From e40bcef449eab22b4ebff29960e7fae6c73668aa Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Tue, 6 Aug 2024 16:25:03 +0200 Subject: [PATCH 12/25] cleanup remains of download_table --- R/get-v1-data.R | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/R/get-v1-data.R b/R/get-v1-data.R index 65a75a3..07d8e27 100644 --- a/R/get-v1-data.R +++ b/R/get-v1-data.R @@ -210,7 +210,7 @@ spod_get_od_v1 <- function( data_dir = spod_get_data_dir(), read_fun = duckdb::tbl_file ) { - # Processing of the date arguments is performed in subsequent functions, because we would want the downloading functions to be able to use the same date arguments for flexibility. So `spod_download_tables()` will handle the date arguments.` + # Processing of the date arguments is performed in subsequent functions, because we would want the downloading functions to be able to use the same date arguments for flexibility. So `spod_download_data()` will handle the date arguments.` zones <- match.arg(zones) @@ -259,7 +259,7 @@ spod_download_od_v1 <- function( #' This function downloads the data files of the specified type, zones, dates and data version. #' @param type The type of data to download. Can be "origin-destination" (or ust "od"), or "trips_per_person" (or just "tpp") for v1 data. For v2 data "overnight_stays" (or just "os") is also available. More data types to be supported in the future. See respective codebooks for more information. [ADD CODEBOOKS!] #' @param zones The zones for which to download the data. Can be "districts" (or "dist", "distr") or "municipalities" (or "muni", "municip") for v1 data. Additionaly, these can be "urban_areas" (GAUs) for v2 data. -#' @param date_range A character vector of dates in ISO format (YYYY-MM-DD) to download the data for. +#' @param date_range A character vector of dates, length 2, in ISO format (YYYY-MM-DD) to download the data for. Defaults to NULL. #' @param dates_list A character vector of dates in ISO format (YYYY-MM-DD) to download the data for. Defaults to NULL. #' @param date_regex A regular expression to match the dates of the data to download. Defaults to NULL. #' @param ver The version of the data to use. Defaults to 1. Can be 1 or 2. @@ -269,13 +269,13 @@ spod_download_od_v1 <- function( #' @example #'\dontrun{ #' # Download the origin-destination on district level for the a date range in March 2020 -#' spod_download_tables(type = "od", zones = "districts", date_range = c("2020-03-20", "2020-03-24"), ver = 1) +#' spod_download_data(type = "od", zones = "districts", date_range = c("2020-03-20", "2020-03-24"), ver = 1) #' #' # Download the origin-destination on district level for select dates in 2020 and 2021 -#' spod_download_tables(type = "od", zones = "districts", dates_list = c("2020-03-20", "2020-03-24", "2021-03-20", "2021-03-24"), ver = 1) +#' spod_download_data(type = "od", zones = "dist", dates_list = c("2020-03-20", "2020-03-24", "2021-03-20", "2021-03-24"), ver = 1) #' -#' # Download the origin-destination on district level using regex for a date range in March 2020 (the regex will capture the dates 2020-03-20 to 2020-03-24) -#' spod_download_tables(type = "od", zones = "districts", date_regex = "2020032[0-4]", ver = 1) +#' # Download the origin-destination on municipality level using regex for a date range in March 2020 (the regex will capture the dates 2020-03-20 to 2020-03-24) +#' spod_download_data(type = "od", zones = "municip", date_regex = "2020032[0-4]", ver = 1) #' } spod_download_data <- function( type = c( From e90f61e0db326c105db65cf69ac69d8ba52dd3a1 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Wed, 7 Aug 2024 14:25:59 +0200 Subject: [PATCH 13/25] revamped internal utils maily for dates and data version handling --- R/internal-utils.R | 156 ------------------------------- R/internal_utils.R | 228 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+), 156 deletions(-) delete mode 100644 R/internal-utils.R create mode 100644 R/internal_utils.R diff --git a/R/internal-utils.R b/R/internal-utils.R deleted file mode 100644 index d776bd3..0000000 --- a/R/internal-utils.R +++ /dev/null @@ -1,156 +0,0 @@ -# Description: Internal utility functions for the MITMA package - -#' Process multiple date arguments -#' -#' This function processes the date arguments provided to various functions in the package. -#' It checks if more than one date argument is provided and returns the appropriate dates as a Dates vector. -#' The function ensures that the dates are in ISO format (yyyy-mm-dd) or yyyymmdd format. -#' -#' @param date_range A vector of dates in ISO format (yyyy-mm-dd) or yyyymmdd format. -#' @param dates_list A vector of dates in ISO format (yyyy-mm-dd) or yyyymmdd format. -#' @param date_regex A regular expression to match dates in the format yyyymmdd. -#' @return A Dates vector of dates. -#' @keywords internal -process_date_arguments <- function( - date_range = NULL, - dates_list = NULL, - date_regex = NULL, - ver = c(1, 2) -) { - rlang:::check_number_whole(ver) - if (!ver %in% c(1, 2)) { - stop("Invalid version number. Must be 1 or 2.") - } - - # perhaps this funciton should automatically detect the type of dates passed. This will dramatically simplify the API and allow the download and get functions to have just one argument for dates that will accept any format. - - # Helper function to check if dates are in ISO format or yyyymmdd format - is_valid_date <- function(dates) { - all(sapply(dates, function(date) { - !is.na(lubridate::ymd(date)) || !is.na(as.Date(date, format = "%Y%m%d")) - })) - } - - - # Check if more than one date argument is provided - args_provided <- list( - date_range = !is.null(date_range), - dates_list = !is.null(dates_list), - date_regex = !is.null(date_regex) - ) - - # Count how many date arguments are set - args_set <- sum(unlist(args_provided)) - - # Assert that only one argument is set - assertthat::assert_that(args_set <= 1, - msg = "Only one of the date arguments (date_range, dates_list, date_regex) should be set.") - - # Determine which date argument to use and return the appropriate dates - if (!is.null(date_range)) { - assertthat::assert_that(is_valid_date(date_range), - msg = "date_range must be in ISO format (yyyy-mm-dd) or yyyymmdd format.") - return(date_range) - - } else if (!is.null(dates_list)) { - assertthat::assert_that(is_valid_date(dates_list), - msg = "dates_list must be in ISO format (yyyy-mm-dd) or yyyymmdd format.") - return(dates_list) - - } else if (!is.null(date_regex)) { - dates <- expand_dates_from_regex(date_regex, ver = ver) - return(dates) - } -} - -#' Function to expand dates from a regex -#' -#' This function generates a sequence of dates from a regular expression pattern. -#' based on the provided regular expression. -#' -#' @param date_regex A regular expression to match dates in the format yyyymmdd. -#' @param ver The version of the data to use. Defaults to "v1". Can be "v1" or "v2". -#' @return A character vector of dates matching the regex. -#' @keywords internal -expand_dates_from_regex <- function(date_regex, - ver = c(1, 2) -) { - - # currently checks for date range for od data only. not all datasets may be available for all dates, so this function may need to be updated to check for the availability of the specific for the requested dates. spod_match_data_type() helper in the same file may be useful here. - - rlang:::check_number_whole(ver) - if (!ver %in% c(1, 2)) { - stop("Invalid version number. Must be 1 or 2.") - } - - if(ver == 1) { - available_data <- spod_available_data_v1(check_local_files = FALSE) - date_range <- range(available_data[grepl("maestra1.*diarios", available_data$target_url),]$data_ymd, na.rm = TRUE) - } else if(ver == 2) { - available_data <- spod_get_metadata() # replace with spod_available_data_v2() when available, spod_get_metadata can become a wrapper with v1/v2 argument. Potentially we can even automaticaly detect the data version based on the time intervals that user requests, but this is a bit controversial, as the methodology behind v1 and v2 data generation is not the same and Nommon+MITMA do not recommend mixing those together and comparing absoloute numbers of trips. - date_range <- range(available_data[grepl("viajes.*diarios", available_data$target_url),]$data_ymd, na.rm = TRUE) - } - start_date <- date_range[1] - end_date <- date_range[2] - - all_dates <- seq.Date(start_date, end_date, by = "day") - - # Filter dates matching the regex - matching_dates <- all_dates[grepl(date_regex, format(all_dates, "%Y%m%d"))] - - return(as.character(matching_dates)) -} - -spod_zone_names_en2es <- function( - zones = c("districts", "dist", "distr", - "municipalities", "muni", "municip") -) { - zones <- tolower(zones) - zones <- match.arg(zones) - if(zones %in% c("districts", "dist", "distr")) { - return("distritos") - } else if(zones %in% c("municipalities", "muni", "municip")) { - return("municipios") - } -} - -#' Match data types to folders -#' @param type The type of data to match. Can be "od", "origin-destination", "os", "overnight_stays", or "tpp", "trips_per_person". -#' @param ver The version of the data to use. Defaults to 1. Can be 1 or 2. -#' @keywords internal -spod_match_data_type <- function( - type = c( - "od", "origin-destination", - "os", "overnight_stays", - "tpp", "trips_per_person"), - ver = c(1, 2) -){ - rlang:::check_number_whole(ver) - if (!ver %in% c(1, 2)) { - stop("Invalid version number. Must be 1 or 2.") - } - - type <- tolower(type) - type <- match.arg(type) - - if(ver == 1) { - if (type %in% c("od", "origin-destination")) { - return("maestra1") - } else if(type %in% c("tpp", "trips_per_person")) { - return("maestra2") - } - } - - if(ver == 2) { - if (type %in% c("od", "origin-destination")) { - return("viajes") - } else if(type %in% c("os", "overnight_stays")) { - return("pernoctaciones") - } else if(type %in% c("tpp", "trips_per_person")) { - return("personas") - } - } - - # need to add a warning here that the type is not recognized - return(NULL) -} \ No newline at end of file diff --git a/R/internal_utils.R b/R/internal_utils.R new file mode 100644 index 0000000..245c526 --- /dev/null +++ b/R/internal_utils.R @@ -0,0 +1,228 @@ +#' Convert multiple formates of date arguments to a sequence of dates +#' +#' This function processes the date arguments provided to various functions in the package. It can handle single dates and arbitratry sequences (vectors) of dates in ISO (YYYY-MM-DD) and YYYYMMDD format. It can also handle date ranges in the format 'YYYY-MM-DD_YYYY-MM-DD' (or 'YYYYMMDD_YYYYMMDD'), date ranges in named vec and regular expressions to match dates in the format `YYYYMMDD`. +#' +#' @param dates A `character` or `Date` vector of dates to process. Kindly keep in mind that v1 and v2 data follow different data collection methodologies and may not be directly comparable. Therefore, do not try to request data from both versions for the same date range. If you need to compare data from both versions, please refer to the respective codebooks and methodology documents. The v1 data covers the period from 2020-02-14 to 2021-05-09, and the v2 data covers the period from 2022-01-01 to the present until further notice. The true dates range is checked against the available data for each version on every function run. +#' +#' The possible values can be any of the following: +#' +#' * A single date in ISO (YYYY-MM-DD) or YYYYMMDD format. `character` or `Date` object. +#' +#' * A vector of dates in ISO (YYYY-MM-DD) or YYYYMMDD format. `character` or `Date` object. Can be any non-consecutive sequence of dates. +#' +#' * A date range +#' +#' * eigher a `character` or `Date` object of length 2 with clearly named elements `start` and `end` in ISO (YYYY-MM-DD) or YYYYMMDD format. E.g. `c(start = "2020-02-15", end = "2020-02-17")`; +#' +#' * or a `character` object of the form `YYYY-MM-DD_YYYY-MM-DD` or `YYYYMMDD_YYYYMMDD`. For example, `2020-02-15_2020-02-17` or `20200215_20200217`. +#' +#' * A regular expression to match dates in the format `YYYYMMDD`. `character` object. For example, `^202002` will match all dates in February 2020. +#' +#' +#' @return A character vector of dates in ISO format (YYYY-MM-DD). +#' @keywords internal +spod_dates_argument_to_dates_seq <- function(dates) { + if (is.null(dates) || (!is.character(dates) && !inherits(dates, "Date"))) { + stop("Invalid date input format. Please provide a character vector or Date object.") + } + + range_regex <- "^\\d{4}(-\\d{2}){2}_\\d{4}(-\\d{2}){2}$|^\\d{8}_\\d{8}$" + single_date_regex <- "^(\\d{4}-\\d{2}-\\d{2}|\\d{8})$" + # If dates is a vector of length one + # Check if is single date, date range, or regex pattern + if (length(dates) == 1){ + + # Check if date range + # match both YYYY-MM-DD_YYYY-MM-DD and YYYYMMDD_YYYYMMDD + if (grepl(range_regex, dates)){ + date_parts <- strsplit(dates, "_")[[1]] + date_parts <- lubridate::ymd(date_parts) + dates <- seq.Date(date_parts[1], date_parts[2], by = "day") + + # if dates does not match the date range pattern + # check if it is just a single day in YYYY-MM-DD or YYYYMMDD format + } else if(grepl(single_date_regex, dates)) { + dates <- lubridate::ymd(dates) + + # assume it is a regex pattern + } else { + dates <- spod_expand_dates_from_regex(dates) + # since spod_expand_dates_from_regex already uses the metadata to generate valid dates we can skip any checks that are required for other date formats and only check for datte overlap + if( isFALSE(spod_is_data_version_overlaps(dates)) ){ + return(dates) + } + } + + # If dates if a vector of multiple values + } else if (length(dates) > 1){ + + # Check if it is of length 2, then it may be a date range + if (length(dates) == 2 & !is.null(names(dates))) { + # if the vector is named with 'start' and 'end', we can assume it is a date range + if(all(names(dates) %in% c("start", "end"))){ + date_parts <- lubridate::ymd(dates) + dates <- seq.Date(date_parts[1], date_parts[2], by = "day") + } + } else { + # this is apparantly a sequence of dates + dates <- lubridate::ymd(dates) + } + } + + # now that we have a clean sequence of dates, we can check for overlaps between data versions + if (isFALSE(spod_is_data_version_overlaps(dates)) & + spod_infer_data_v_from_dates(dates) %in% c(1, 2) + ) { + return(dates) + } +} + + + +#' Check if specified dates span both data versions +#' +#' This function checks if the specified dates or date ranges span both v1 and v2 data versions. +#' +#' @param dates A Dates vector of dates to check. +#' @return TRUE if the dates span both data versions, FALSE otherwise. +#' @keywords internal +spod_is_data_version_overlaps <- function(dates){ + + all_dates_v1 <- spod_get_valid_dates(ver = 1) + all_dates_v2 <- spod_get_valid_dates(ver = 2) + + if (any(dates %in% all_dates_v1) && any(dates %in% all_dates_v2)) { + stop(paste0("Dates found in both v1 and v2 data. The v1 and v2 data sets may not be comparable. Please see the respective codebooks and methodology documents.\nThe valid dates range for v1 is: ", paste0(min(all_dates_v1), " to ", max(all_dates_v1)), " and for v2 is: ", paste0(min(all_dates_v2), " to ", max(all_dates_v2)))) + } + return(FALSE) +} + +spod_infer_data_v_from_dates <- function(dates) { + # in case of overlap + # will throw an error from the spod_is_data_version_overlaps + if (spod_is_data_version_overlaps(dates)) { + invisible(return(NULL)) + } + + # of no overlap, compare with date ranges + v1_dates <- spod_get_valid_dates(ver = 1) + v2_dates <- spod_get_valid_dates(ver = 2) + + if (all(dates %in% v1_dates)) { + return(1) + } else if (all(dates %in% v2_dates)) { + return(2) + } else { + # if some dates did not match stop with a message showing which dates are missing + missing_dates <- dates[!dates %in% c(v1_dates, v2_dates)] + stop(paste0("Some dates do not match the available data. The valid dates range for v1 is: ", paste0(min(v1_dates), " to ", max(v1_dates)), " and for v2 is: ", paste0(min(v2_dates), " to ", max(v2_dates), ".\nMissing dates: ", paste0(missing_dates, collapse = ", ")))) + } +} + +#' Function to expand dates from a regex +#' +#' This function generates a sequence of dates from a regular expression pattern. +#' based on the provided regular expression. +#' +#' @param date_regex A regular expression to match dates in the format yyyymmdd. +#' @return A character vector of dates matching the regex. +#' @keywords internal +spod_expand_dates_from_regex <- function(date_regex) { + + all_dates_v1 <- spod_get_valid_dates(ver = 1) + all_dates_v2 <- spod_get_valid_dates(ver = 2) + + # Filter dates matching the regex for both versions + matching_dates_v1 <- all_dates_v1[grepl(date_regex, format(all_dates_v1, "%Y%m%d"))] + matching_dates_v2 <- all_dates_v2[grepl(date_regex, format(all_dates_v2, "%Y%m%d"))] + + # if both vectors are empty, throw an error + if (length(matching_dates_v1) == 0 && length(matching_dates_v2) == 0) { + stop(paste0("No matching dates found in the available data.", + "The valid dates range for v1 is: ", paste0(min(all_dates_v1), " to ", max(all_dates_v1)), " and for v2 is: ", paste0(min(all_dates_v2), " to ", max(all_dates_v2)))) + } + # If checks above have passed, we can combine the matching dates as only one contains dates and the other is empty + matching_dates <- sort(c(matching_dates_v1, matching_dates_v2)) + + return(matching_dates) +} + + +spod_get_valid_dates <- function(ver = 1) { + rlang:::check_number_whole(ver) + if (!ver %in% c(1, 2)) { + stop("Invalid version number. Must be 1 or 2.") + } + + + if(ver == 1) { + # available_data <- spod_available_data_v1(check_local_files = FALSE, quiet = TRUE) + # all_dates <- unique(available_data[grepl("maestra1.*diarios", available_data$target_url),]$data_ymd, na.rm = TRUE) + # perahps it is worth hardcoding at lest the v1 data range as it is unlikely to change at this point + all_dates <- seq.Date(from = as.Date("2020-02-14"), to = as.Date("2021-05-09"), by = "day") + } else if (ver == 2) { + available_data <- spod_get_metadata(quiet = TRUE) # replace with spod_available_data_v2() when available + all_dates <- unique(available_data[grepl("viajes.*diarios", available_data$target_url),]$data_ymd, na.rm = TRUE) + } + + return(all_dates) +} +# currently checks for date range for od data only. not all datasets may be available for all dates, so this function may need to be updated to check for the availability of the specific for the requested dates. spod_match_data_type() helper in the same file may be useful here. + + +# replace with spod_available_data_v2() when available, spod_get_metadata can become a wrapper with v1/v2 argument. Potentially we can even automaticaly detect the data version based on the time intervals that user requests, but this is a bit controversial, as the methodology behind v1 and v2 data generation is not the same and Nommon+MITMA do not recommend mixing those together and comparing absoloute numbers of trips. + + +spod_zone_names_en2es <- function( + zones = c("districts", "dist", "distr", + "municipalities", "muni", "municip") +) { + zones <- tolower(zones) + zones <- match.arg(zones) + if(zones %in% c("districts", "dist", "distr")) { + return("distritos") + } else if(zones %in% c("municipalities", "muni", "municip")) { + return("municipios") + } +} + +#' Match data types to folders +#' @param type The type of data to match. Can be "od", "origin-destination", "os", "overnight_stays", or "tpp", "trips_per_person". +#' @param ver The version of the data to use. Defaults to 1. Can be 1 or 2. +#' @keywords internal +spod_match_data_type <- function( + type = c( + "od", "origin-destination", + "os", "overnight_stays", + "tpp", "trips_per_person"), + ver = c(1, 2) +){ + rlang:::check_number_whole(ver) + if (!ver %in% c(1, 2)) { + stop("Invalid version number. Must be 1 or 2.") + } + + type <- tolower(type) + type <- match.arg(type) + + if(ver == 1) { + if (type %in% c("od", "origin-destination")) { + return("maestra1") + } else if(type %in% c("tpp", "trips_per_person")) { + return("maestra2") + } + } + + if(ver == 2) { + if (type %in% c("od", "origin-destination")) { + return("viajes") + } else if(type %in% c("os", "overnight_stays")) { + return("pernoctaciones") + } else if(type %in% c("tpp", "trips_per_person")) { + return("personas") + } + } + + # need to add a warning here that the type is not recognized + return(NULL) +} \ No newline at end of file From 94b641b9681d4810de09b53262665ac8e12ec9bd Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Wed, 7 Aug 2024 14:26:26 +0200 Subject: [PATCH 14/25] clean multi-purpose download function --- R/download_data.R | 102 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 R/download_data.R diff --git a/R/download_data.R b/R/download_data.R new file mode 100644 index 0000000..fc8a73a --- /dev/null +++ b/R/download_data.R @@ -0,0 +1,102 @@ +#' Download the data files of specified type, zones, dates and data version +#' +#' This function downloads the data files of the specified type, zones, dates and data version. +#' @param type The type of data to download. Can be `"origin-destination"` (or ust `"od"`), or `"trips_per_person"` (or just `"tpp"`) for v1 data. For v2 data `"overnight_stays"` (or just `"os"`) is also available. More data types to be supported in the future. See respective codebooks for more information. **ADD CODEBOOKS! to the package** +#' @param zones The zones for which to download the data. Can be `"districts"` (or `"dist"`, `"distr"`) or `"municipalities"` (or `"muni"`, `"municip"`) for v1 data. Additionaly, these can be `"large_urban_areas"` (or `"lau"`) for v2 data. +#' @inheritParams spod_dates_argument_to_dates_seq +#' @param data_dir The directory where the data is stored. Defaults to the value returned by `spod_get_data_dir()` which returns the value of the environment variable `SPANISH_OD_DATA_DIR` or a temporary directory if the variable is not set. +#' @param quiet Logical. If `TRUE`, the function does not print messages to the console. Defaults to `FALSE`. +#' @param return_output Logical. If `TRUE`, the function returns a character vector of the paths to the downloaded files. If `FALSE`, the function returns `NULL`. +#' +#' @return A character vector of the paths to the downloaded files. Unless `return_output = FALSE`, in which case the function returns `NULL`. +#' +#' @export +#' @examples +#' \dontrun{ +#' # Download the origin-destination on district level for the a date range in March 2020 +#' spod_download_data(type = "od", zones = "districts", date_range = c("2020-03-20", "2020-03-24")) +#' +#' # Download the origin-destination on district level for select dates in 2020 and 2021 +#' spod_download_data(type = "od", zones = "dist", dates_list = c("2020-03-20", "2020-03-24", "2021-03-20", "2021-03-24")) +#' +#' # Download the origin-destination on municipality level using regex for a date range in March 2020 (the regex will capture the dates 2020-03-20 to 2020-03-24) +#' spod_download_data(type = "od", zones = "municip", date_regex = "2020032[0-4]) +#' } +spod_download_data <- function( + type = c( + "od", "origin-destination", + "os", "overnight_stays", + "tpp", "trips_per_person"), + zones = c("districts", "dist", "distr", + "municipalities", "muni", "municip", + "lau", "large_urban_areas"), # implement "urban_areas" for v2 data + dates = NULL, + data_dir = spod_get_data_dir(), + quiet = FALSE, + return_output = TRUE +) { + # convert english data type names to spanish words used in the default data paths + type <- match.arg(type) + type <- spod_match_data_type(type = type, ver = ver) + + # convert english zone names to spanish words used in the default data paths + zones <- match.arg(zones) + zones <- spod_zone_names_en2es(zones) + + # this is where the date arguments are processed + # for all the wrapper functions that use the spod_download_data() function the dates are also processed here + dates_to_use <- spod_dates_argument_to_dates_seq(dates = dates) + + # check version + # replace this argument with automatic version detection based on the dates requested? + ver <- spod_infer_data_v_from_dates(dates_to_use) # this leads to a second call to an internal spod_get_valid_dates() which in turn causes a second call to spod_available_data_v1() or spod_get_metadata(). This results in reading the xml files with metadata for the second time. This is not optimal and should be fixed. + if (isFALSE(quiet)) message("Data version detected from dates: ", ver) + + + + # get the available data list while checking for files already cached on disk + if( ver == 1) { + metadata <- spod_available_data_v1(data_dir = data_dir, + check_local_files = TRUE) + } else if (ver == 2) { + metadata <- spod_get_metadata(data_dir = data_dir) + # replace with spod_available_data_v2() when available, spod_get_metadata can become a wrapper with v1/v2 argument. Potentially we can even automaticaly detect the data version based on the time intervals that user requests, but this is a bit controversial, as the methodology behind v1 and v2 data generation is not the same and Nommon+MITMA do not recommend mixing those together and comparing absoloute numbers of trips. + } + + # match the metadata to type, zones, version and dates + if(ver == 1){ + requested_files <- metadata[ + grepl(glue::glue("v{ver}.*{type}.*{zones}"), metadata$local_path) & + metadata$data_ymd %in% dates_to_use, + ] + } else if(ver == 2){ + requested_files <- metadata[ + grepl(glue::glue("v{ver}.*{zones}.*{type}"), metadata$local_path) & + metadata$data_ymd %in% dates_to_use, + ] + } + + files_to_download <- requested_files[!requested_files$downloaded, ] + + # pre-generate target paths for the files to download + fs::dir_create( + unique(fs::path_dir(files_to_download$local_path)), + recurse = TRUE) + + # download the missing files + downloaded_files <- curl::multi_download( + urls = files_to_download$target_url, + destfiles = files_to_download$local_path, + progress = TRUE, + resume = TRUE + ) + + # set download status for downloaded files as TRUE in requested_files + requested_files$downloaded[requested_files$local_path %in% downloaded_files$destfile] <- TRUE + + message("Retrieved data for requested dates: ", paste(dates_to_use, collapse = ", ")) # this may output too many dates, shoudl be fixed when we create a flexible date argument processing function. Keeping for now. + + if (return_output) { + return(requested_files$local_path) + } +} \ No newline at end of file From 455119d1c10bbe6daf3498118b8c0bcc10d242fc Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Wed, 7 Aug 2024 14:27:19 +0200 Subject: [PATCH 15/25] moved download function away from v1 specific file --- R/{get-v1-data.R => get_v1_data.R} | 177 +++++------------------------ 1 file changed, 27 insertions(+), 150 deletions(-) rename R/{get-v1-data.R => get_v1_data.R} (56%) diff --git a/R/get-v1-data.R b/R/get_v1_data.R similarity index 56% rename from R/get-v1-data.R rename to R/get_v1_data.R index 07d8e27..799a93c 100644 --- a/R/get-v1-data.R +++ b/R/get_v1_data.R @@ -53,23 +53,29 @@ spod_get_latest_v1_file_list <- function( #' } spod_available_data_v1 <- function(data_dir = spod_get_data_dir(), # check_local_files (below) is FALSE by default to avoid excessive filesystem access, perhaps should be TRUE. Download functions use it to load the xml file, but we probably do not want the script to check all local cache directories every time we run a get data function. Perhaps it is better to offload this check to a separate function and have a csv file or some other way to keep track of the files that were downloaded and cached. An output of curl::multi_download() could be used for this purpose. - check_local_files = FALSE + check_local_files = FALSE, + quiet = FALSE ) { xml_files_list <- fs::dir_ls(data_dir, type = "file", regexp = "data_links_v1") |> sort() - latest_data_links_xml_path <- utils::tail(xml_files_list, 1) + if(length(xml_files_list) == 0) { + if(isFALSE(quiet)) message("No data links xml files found, getting latest data links xml") + latest_data_links_xml_path <- spod_get_latest_v1_file_list(data_dir = data_dir) + } else { + latest_data_links_xml_path <- utils::tail(xml_files_list, 1) + } # Check if the XML file is 1 day old or older from its name file_date <- stringr::str_extract(latest_data_links_xml_path, "[0-9]{4}-[0-9]{2}-[0-9]{2}") if (file_date < format(Sys.Date(), format = "%Y-%m-%d")) { - message("File list xml is 1 day old or older, getting latest data links xml") + if(isFALSE(quiet)) message("File list xml is 1 day old or older, getting latest data links xml") latest_data_links_xml_path <- spod_get_latest_v1_file_list(data_dir = data_dir) } else { - message("Using existing data links xml: ", latest_data_links_xml_path) + if(isFALSE(quiet)) message("Using existing data links xml: ", latest_data_links_xml_path) } if (length(latest_data_links_xml_path) == 0) { - message("Getting latest data links xml") + if(isFALSE(quiet)) message("Getting latest data links xml") latest_data_links_xml_path <- spod_get_latest_v1_file_list(data_dir = data_dir) } @@ -198,165 +204,36 @@ spod_clean_zones_v1 <- function(zones_path) { return(zones) } + #' Retrieve the origin-destination v1 data (2020-2021) #' -#' This function retrieves the origin-destination data from the specified data directory. +#' This function retrieves the v1 (2020-2021) origin-destination data from the specified data directory. #' -spod_get_od_v1 <- function( - date_range = c("2020-02-14", "2020-02-15"), - dates_list = NULL, - date_regex = NULL, - zones = c("distritos", "municipios"), +#' @inheritParams spod_download_data +#' @return A tibble with the origin-destination data. +spod_get_od <- function( + zones = c("districts", "dist", "distr", + "municipalities", "muni", "municip"), # add "urban_areas" for v2 data + dates = NULL, data_dir = spod_get_data_dir(), + quiet = FALSE, read_fun = duckdb::tbl_file ) { - # Processing of the date arguments is performed in subsequent functions, because we would want the downloading functions to be able to use the same date arguments for flexibility. So `spod_download_data()` will handle the date arguments.` + # Processing of the date arguments is performed in `spod_download_data()` zones <- match.arg(zones) - # check the locally cached and online available data - # and download missing files if any - # get local paths of the files requested with date arguments - metadata <- spod_download_od_v1( - date_range = date_range, - dates_list = dates_list, - date_regex = date_regex, - zones = zones - ) - - # read data from cached files - - - -} - - - -spod_download_od_v1 <- function( - date_range = c("2020-02-14", "2020-02-15"), - dates_list = NULL, - date_regex = NULL, - zones = c("districts", "dist", "distr", - "municipalities", "muni", "municip"), - data_dir = spod_get_data_dir() -) { - zones <- match.arg(zones) - zones <- spod_zone_names_en2es(zones) - + # use the spot_download_data() function to download any missing data downloaded_files <- spod_download_data( - date_range = date_range, - dates_list = dates_list, - date_regex = date_regex, - subdir = glue::glue("v1/maestra2-mitma-{zones}/ficheros-diarios/"), + type = "od", + zones = zones, + dates = dates, data_dir = data_dir ) - return(downloaded_files) -} - -#' Download the data files of specified type, zones, dates and data version -#' -#' This function downloads the data files of the specified type, zones, dates and data version. -#' @param type The type of data to download. Can be "origin-destination" (or ust "od"), or "trips_per_person" (or just "tpp") for v1 data. For v2 data "overnight_stays" (or just "os") is also available. More data types to be supported in the future. See respective codebooks for more information. [ADD CODEBOOKS!] -#' @param zones The zones for which to download the data. Can be "districts" (or "dist", "distr") or "municipalities" (or "muni", "municip") for v1 data. Additionaly, these can be "urban_areas" (GAUs) for v2 data. -#' @param date_range A character vector of dates, length 2, in ISO format (YYYY-MM-DD) to download the data for. Defaults to NULL. -#' @param dates_list A character vector of dates in ISO format (YYYY-MM-DD) to download the data for. Defaults to NULL. -#' @param date_regex A regular expression to match the dates of the data to download. Defaults to NULL. -#' @param ver The version of the data to use. Defaults to 1. Can be 1 or 2. -#' @param data_dir The directory where the data is stored. Defaults to the value returned by `spod_get_data_dir()` which returns the value of the environment variable `SPANISH_OD_DATA_DIR` or a temporary directory if the variable is not set. -#' -#' @export -#' @example -#'\dontrun{ -#' # Download the origin-destination on district level for the a date range in March 2020 -#' spod_download_data(type = "od", zones = "districts", date_range = c("2020-03-20", "2020-03-24"), ver = 1) -#' -#' # Download the origin-destination on district level for select dates in 2020 and 2021 -#' spod_download_data(type = "od", zones = "dist", dates_list = c("2020-03-20", "2020-03-24", "2021-03-20", "2021-03-24"), ver = 1) -#' -#' # Download the origin-destination on municipality level using regex for a date range in March 2020 (the regex will capture the dates 2020-03-20 to 2020-03-24) -#' spod_download_data(type = "od", zones = "municip", date_regex = "2020032[0-4]", ver = 1) -#' } -spod_download_data <- function( - type = c( - "od", "origin-destination", - "os", "overnight_stays", - "tpp", "trips_per_person"), - zones = c("districts", "dist", "distr", - "municipalities", "muni", "municip"), # add "urban_areas" for v2 data - date_range = NULL, - dates_list = NULL, - date_regex = NULL, - ver = 1, # infer version from dates? - data_dir = spod_get_data_dir() -) { - # convert english data type names to spanish words used in the default data paths - type <- match.arg(type) - type <- spod_match_data_type(type = type, ver = ver) - - # convert english zone names to spanish words used in the default data paths - zones <- match.arg(zones) - zones <- spod_zone_names_en2es(zones) - - - # this is where the date arguments are processed - # for all the wrapper functions that call this worker function - dates_to_use <- process_date_arguments( - date_range = date_range, - dates_list = dates_list, - date_regex = date_regex, - ver = ver - ) - - # check version - # replace this argument with automatic version detection based on the dates requested? - rlang:::check_number_whole(ver) - if (!ver %in% c(1, 2)) { - stop("Invalid version number. Must be 1 or 2.") - } + # read data from cached files - # get the available data list while checking for files already cached on disk - if( ver == 1) { - metadata <- spod_available_data_v1(data_dir = data_dir, - check_local_files = TRUE) - } else if (ver == 2) { - metadata <- spod_get_metadata(data_dir = data_dir) - # replace with spod_available_data_v2() when available, spod_get_metadata can become a wrapper with v1/v2 argument. Potentially we can even automaticaly detect the data version based on the time intervals that user requests, but this is a bit controversial, as the methodology behind v1 and v2 data generation is not the same and Nommon+MITMA do not recommend mixing those together and comparing absoloute numbers of trips. - } - # match the metadata to type, zones, version and dates - if(ver == 1){ - requested_files <- metadata[ - grepl(glue::glue("v{ver}.*{type}.*{zones}"), metadata$local_path) & - metadata$data_ymd %in% dates_to_use, - ] - } else if(ver == 2){ - requested_files <- metadata[ - grepl(glue::glue("v{ver}.*{zones}.*{type}"), metadata$local_path) & - metadata$data_ymd %in% dates_to_use, - ] - } - - files_to_download <- requested_files[!requested_files$downloaded, ] - # pre-generate target paths for the files to download - fs::dir_create( - unique(fs::path_dir(files_to_download$local_path)), - recurse = TRUE) - - # download the missing files - downloaded_files <- curl::multi_download( - urls = files_to_download$target_url, - destfiles = files_to_download$local_path, - progress = TRUE, - resume = TRUE - ) - - # set download status for downloaded files as TRUE in requested_files - requested_files$downloaded[requested_files$local_path %in% downloaded_files$destfile] <- TRUE - - message("Retrieved data for requested dates: ", paste(dates_to_use, collapse = ", ")) # this may output too many dates, shoudl be fixed when we create a flexible date argument processing function. Keeping for now. - - return(requested_files$local_path) -} \ No newline at end of file +} From 3f223a69afb4e36f9b543b6c4991cb7040c7726d Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Wed, 7 Aug 2024 14:27:47 +0200 Subject: [PATCH 16/25] add tests for critical date handling internal function --- DESCRIPTION | 3 ++ NAMESPACE | 2 +- tests/testthat.R | 12 +++++ tests/testthat/test-internal_utils.R | 73 ++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 tests/testthat.R create mode 100644 tests/testthat/test-internal_utils.R diff --git a/DESCRIPTION b/DESCRIPTION index 464f6ae..08fc1ba 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -38,3 +38,6 @@ Imports: Encoding: UTF-8 Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.2 +Suggests: + testthat (>= 3.0.0) +Config/testthat/edition: 3 diff --git a/NAMESPACE b/NAMESPACE index 0c236a6..f2616bc 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,7 +1,7 @@ # Generated by roxygen2: do not edit by hand export(spod_available_data_v1) -export(spod_download_tables) +export(spod_download_data) export(spod_get) export(spod_get_latest_v1_file_list) export(spod_get_latest_v2_xml) diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..fe6ac28 --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,12 @@ +# This file is part of the standard setup for testthat. +# It is recommended that you do not modify it. +# +# Where should you do additional test configuration? +# Learn more about the roles of various files in: +# * https://r-pkgs.org/testing-design.html#sec-tests-files-overview +# * https://testthat.r-lib.org/articles/special-files.html + +library(testthat) +library(spanishoddata) + +test_check("spanishoddata") diff --git a/tests/testthat/test-internal_utils.R b/tests/testthat/test-internal_utils.R new file mode 100644 index 0000000..8269630 --- /dev/null +++ b/tests/testthat/test-internal_utils.R @@ -0,0 +1,73 @@ +Sys.setenv(SPANISH_OD_DATA_DIR = tempdir()) + +test_that("single ISO date input", { + dates <- "2023-07-01" + result <- spod_dates_argument_to_dates_seq(dates) + expect_equal(result, as.Date("2023-07-01")) +}) + +test_that("single YYYYMMDD date input", { + dates <- "20230701" + result <- spod_dates_argument_to_dates_seq(dates) + expect_equal(result, as.Date("2023-07-01")) +}) + +test_that("vector of ISO dates", { + dates <- c("2023-07-01", "2023-07-03", "2023-07-05") + result <- spod_dates_argument_to_dates_seq(dates) + expect_equal(result, as.Date(c("2023-07-01", "2023-07-03", "2023-07-05"))) +}) + +test_that("vector of YYYYMMDD dates", { + dates <- c("20230701", "20230703", "20230705") + result <- spod_dates_argument_to_dates_seq(dates) + expect_equal(result, as.Date(c("2023-07-01", "2023-07-03", "2023-07-05"))) +}) + +test_that("date range in ISO format", { + dates <- "2023-07-01_2023-07-05" + result <- spod_dates_argument_to_dates_seq(dates) + expect_equal(result, seq.Date(from = as.Date("2023-07-01"), to = as.Date("2023-07-05"), by = "day")) +}) + +test_that("date range in YYYYMMDD format", { + dates <- "20230701_20230705" + result <- spod_dates_argument_to_dates_seq(dates) + expect_equal(result, seq.Date(from = as.Date("2023-07-01"), to = as.Date("2023-07-05"), by = "day")) +}) + +test_that("named vector date range in ISO format", { + dates <- c(start = "2023-07-01", end = "2023-07-05") + result <- spod_dates_argument_to_dates_seq(dates) + expect_equal(result, seq.Date(from = as.Date("2023-07-01"), to = as.Date("2023-07-05"), by = "day")) +}) + +test_that("named vector date range in YYYYMMDD format", { + dates <- c(start = "20230701", end = "20230705") + result <- spod_dates_argument_to_dates_seq(dates) + expect_equal(result, seq.Date(from = as.Date("2023-07-01"), to = as.Date("2023-07-05"), by = "day")) +}) + +test_that("regex pattern matching dates", { + dates <- "^202307" + result <- spod_dates_argument_to_dates_seq(dates) + expected_dates <- seq.Date(from = as.Date("2023-07-01"), to = as.Date("2023-07-31"), by = "day") + expect_equal(result, expected_dates) +}) + +test_that("invalid input type", { + dates <- 20230701 + expect_error(spod_dates_argument_to_dates_seq(dates), "Invalid date input format. Please provide a character vector or Date object.") +}) + +test_that("dates span both v1 and v2 data", { + dates <- c("2021-05-09", "2022-01-01") + expect_error(spod_dates_argument_to_dates_seq(dates), + "Dates found in both v1 and v2 data.") +}) + +test_that("dates that are out of availabe range of v1 data", { + dates <- c("2020-01-01", "2021-01-01") + expect_error(spod_dates_argument_to_dates_seq(dates), + "Some dates do not match the available data.") +}) From 6a37562e4c3357b003353a03653b615e473339a0 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Wed, 7 Aug 2024 14:28:51 +0200 Subject: [PATCH 17/25] quiet options and error warning messages in spod_get_dta_dir and spod_get_metadata --- R/get.R | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/R/get.R b/R/get.R index 56a5afe..465bf39 100644 --- a/R/get.R +++ b/R/get.R @@ -39,14 +39,14 @@ spod_get_latest_v2_xml = function( #' names(metadata) #' head(metadata) #' } -spod_get_metadata = function(data_dir = spod_get_data_dir()) { +spod_get_metadata = function(data_dir = spod_get_data_dir(), quiet = FALSE) { xml_files_list = fs::dir_ls(data_dir, type = "file", regexp = "data_links_v2") |> sort() latest_data_links_xml_path = utils::tail(xml_files_list, 1) if (length(latest_data_links_xml_path) == 0) { - message("Getting latest data links xml") + if(isFALSE(quiet)) message("Getting latest data links xml") latest_data_links_xml_path = spod_get_latest_v2_xml(data_dir = data_dir) } else { - message("Using existing data links xml: ", latest_data_links_xml_path) + if(isFALSE(quiet)) message("Using existing data links xml: ", latest_data_links_xml_path) } x_xml = xml2::read_xml(latest_data_links_xml_path) @@ -81,10 +81,10 @@ spod_get_metadata = function(data_dir = spod_get_data_dir()) { #' #' @return The data directory. #' @keywords internal -spod_get_data_dir = function() { +spod_get_data_dir = function(quiet = FALSE) { data_dir_env = Sys.getenv("SPANISH_OD_DATA_DIR") if( data_dir_env == "" ) { - warning("Warning: SPANISH_OD_DATA_DIR is not set. Using the temporary directory, which is not recommended, as the data will be deleted when the session ends.\n\n To set the data directory, use `Sys.setenv(SPANISH_OD_DATA_DIR = '/path/to/data')` or set SPANISH_OD_DATA_DIR permanently in the environment by editing the `.Renviron` file locally for current project with `usethis::edit_r_environ('project')` or `file.edit('.Renviron')` or globally for all projects with `usethis::edit_r_environ('user')` or `file.edit('~/.Renviron')`.") + if (isFALSE(quiet)) warning("Warning: SPANISH_OD_DATA_DIR is not set. Using the temporary directory, which is not recommended, as the data will be deleted when the session ends.\n\n To set the data directory, use `Sys.setenv(SPANISH_OD_DATA_DIR = '/path/to/data')` or set SPANISH_OD_DATA_DIR permanently in the environment by editing the `.Renviron` file locally for current project with `usethis::edit_r_environ('project')` or `file.edit('.Renviron')` or globally for all projects with `usethis::edit_r_environ('user')` or `file.edit('~/.Renviron')`.") data_dir_env = tempdir() # if not set, use the temp directory } return(fs::path_real(data_dir_env)) From 5f9091aff08f85a3547e5c6cb30788fcc470b766 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Wed, 7 Aug 2024 14:29:01 +0200 Subject: [PATCH 18/25] update docs --- man/process_date_arguments.Rd | 29 ------------ man/spod_available_data_v1.Rd | 5 +- man/spod_clean_zones_v1.Rd | 2 +- man/spod_dates_argument_to_dates_seq.Rd | 28 +++++++++++ man/spod_download_data.Rd | 46 +++++++++++++++++++ man/spod_download_tables.Rd | 35 -------------- ...gex.Rd => spod_expand_dates_from_regex.Rd} | 10 ++-- man/spod_get_data_dir.Rd | 2 +- man/spod_get_latest_v1_file_list.Rd | 2 +- man/spod_get_metadata.Rd | 2 +- man/spod_get_od.Rd | 39 ++++++++++++++++ man/spod_get_od_v1.Rd | 18 -------- man/spod_get_zones_v1.Rd | 2 +- man/spod_is_data_version_overlaps.Rd | 18 ++++++++ man/spod_match_data_type.Rd | 2 +- 15 files changed, 144 insertions(+), 96 deletions(-) delete mode 100644 man/process_date_arguments.Rd create mode 100644 man/spod_dates_argument_to_dates_seq.Rd create mode 100644 man/spod_download_data.Rd delete mode 100644 man/spod_download_tables.Rd rename man/{expand_dates_from_regex.Rd => spod_expand_dates_from_regex.Rd} (62%) create mode 100644 man/spod_get_od.Rd delete mode 100644 man/spod_get_od_v1.Rd create mode 100644 man/spod_is_data_version_overlaps.Rd diff --git a/man/process_date_arguments.Rd b/man/process_date_arguments.Rd deleted file mode 100644 index 57a629e..0000000 --- a/man/process_date_arguments.Rd +++ /dev/null @@ -1,29 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/internal-utils.R -\name{process_date_arguments} -\alias{process_date_arguments} -\title{Process multiple date arguments} -\usage{ -process_date_arguments( - date_range = NULL, - dates_list = NULL, - date_regex = NULL, - ver = c(1, 2) -) -} -\arguments{ -\item{date_range}{A vector of dates in ISO format (yyyy-mm-dd) or yyyymmdd format.} - -\item{dates_list}{A vector of dates in ISO format (yyyy-mm-dd) or yyyymmdd format.} - -\item{date_regex}{A regular expression to match dates in the format yyyymmdd.} -} -\value{ -A Dates vector of dates. -} -\description{ -This function processes the date arguments provided to various functions in the package. -It checks if more than one date argument is provided and returns the appropriate dates as a Dates vector. -The function ensures that the dates are in ISO format (yyyy-mm-dd) or yyyymmdd format. -} -\keyword{internal} diff --git a/man/spod_available_data_v1.Rd b/man/spod_available_data_v1.Rd index 09c8512..cfea9b2 100644 --- a/man/spod_available_data_v1.Rd +++ b/man/spod_available_data_v1.Rd @@ -1,12 +1,13 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/get-v1-data.R +% Please edit documentation in R/get_v1_data.R \name{spod_available_data_v1} \alias{spod_available_data_v1} \title{Get the available v1 data list} \usage{ spod_available_data_v1( data_dir = spod_get_data_dir(), - check_local_files = FALSE + check_local_files = FALSE, + quiet = FALSE ) } \arguments{ diff --git a/man/spod_clean_zones_v1.Rd b/man/spod_clean_zones_v1.Rd index 7e376f4..81fbadc 100644 --- a/man/spod_clean_zones_v1.Rd +++ b/man/spod_clean_zones_v1.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/get-v1-data.R +% Please edit documentation in R/get_v1_data.R \name{spod_clean_zones_v1} \alias{spod_clean_zones_v1} \title{Fixes common issues in the zones data and cleans up variable names} diff --git a/man/spod_dates_argument_to_dates_seq.Rd b/man/spod_dates_argument_to_dates_seq.Rd new file mode 100644 index 0000000..d9f5b21 --- /dev/null +++ b/man/spod_dates_argument_to_dates_seq.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/internal_utils.R +\name{spod_dates_argument_to_dates_seq} +\alias{spod_dates_argument_to_dates_seq} +\title{Convert multiple formates of date arguments to a sequence of dates} +\usage{ +spod_dates_argument_to_dates_seq(dates) +} +\arguments{ +\item{dates}{A \code{character} or \code{Date} vector of dates to process. Kindly keep in mind that v1 and v2 data follow different data collection methodologies and may not be directly comparable. Therefore, do not try to request data from both versions for the same date range. If you need to compare data from both versions, please refer to the respective codebooks and methodology documents. The v1 data covers the period from 2020-02-14 to 2021-05-09, and the v2 data covers the period from 2022-01-01 to the present until further notice. The true dates range is checked against the available data for each version on every function run. + +The possible values can be any of the following: +\itemize{ +\item A single date in ISO (YYYY-MM-DD) or YYYYMMDD format. \code{character} or \code{Date} object. +\item A vector of dates in ISO (YYYY-MM-DD) or YYYYMMDD format. \code{character} or \code{Date} object. Can be any non-consecutive sequence of dates. +\item A date range +\item eigher a \code{character} or \code{Date} object of length 2 with clearly named elements \code{start} and \code{end} in ISO (YYYY-MM-DD) or YYYYMMDD format. E.g. \code{c(start = "2020-02-15", end = "2020-02-17")}; +\item or a \code{character} object of the form \code{YYYY-MM-DD_YYYY-MM-DD} or \code{YYYYMMDD_YYYYMMDD}. For example, \verb{2020-02-15_2020-02-17} or \verb{20200215_20200217}. +\item A regular expression to match dates in the format \code{YYYYMMDD}. \code{character} object. For example, \verb{^202002} will match all dates in February 2020. +}} +} +\value{ +A character vector of dates in ISO format (YYYY-MM-DD). +} +\description{ +This function processes the date arguments provided to various functions in the package. It can handle single dates and arbitratry sequences (vectors) of dates in ISO (YYYY-MM-DD) and YYYYMMDD format. It can also handle date ranges in the format 'YYYY-MM-DD_YYYY-MM-DD' (or 'YYYYMMDD_YYYYMMDD'), date ranges in named vec and regular expressions to match dates in the format \code{YYYYMMDD}. +} +\keyword{internal} diff --git a/man/spod_download_data.Rd b/man/spod_download_data.Rd new file mode 100644 index 0000000..300bb59 --- /dev/null +++ b/man/spod_download_data.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/download_data.R +\name{spod_download_data} +\alias{spod_download_data} +\title{Download the data files of specified type, zones, dates and data version} +\usage{ +spod_download_data( + type = c("od", "origin-destination", "os", "overnight_stays", "tpp", + "trips_per_person"), + zones = c("districts", "dist", "distr", "municipalities", "muni", "municip", "lau", + "large_urban_areas"), + dates = NULL, + data_dir = spod_get_data_dir(), + quiet = FALSE, + return_output = TRUE +) +} +\arguments{ +\item{type}{The type of data to download. Can be \code{"origin-destination"} (or ust \code{"od"}), or \code{"trips_per_person"} (or just \code{"tpp"}) for v1 data. For v2 data \code{"overnight_stays"} (or just \code{"os"}) is also available. More data types to be supported in the future. See respective codebooks for more information. \strong{ADD CODEBOOKS! to the package}} + +\item{zones}{The zones for which to download the data. Can be \code{"districts"} (or \code{"dist"}, \code{"distr"}) or \code{"municipalities"} (or \code{"muni"}, \code{"municip"}) for v1 data. Additionaly, these can be \code{"large_urban_areas"} (or \code{"lau"}) for v2 data.} + +\item{dates}{A \code{character} or \code{Date} vector of dates to process. Kindly keep in mind that v1 and v2 data follow different data collection methodologies and may not be directly comparable. Therefore, do not try to request data from both versions for the same date range. If you need to compare data from both versions, please refer to the respective codebooks and methodology documents. The v1 data covers the period from 2020-02-14 to 2021-05-09, and the v2 data covers the period from 2022-01-01 to the present until further notice. The true dates range is checked against the available data for each version on every function run. + +The possible values can be any of the following: +\itemize{ +\item A single date in ISO (YYYY-MM-DD) or YYYYMMDD format. \code{character} or \code{Date} object. +\item A vector of dates in ISO (YYYY-MM-DD) or YYYYMMDD format. \code{character} or \code{Date} object. Can be any non-consecutive sequence of dates. +\item A date range +\item eigher a \code{character} or \code{Date} object of length 2 with clearly named elements \code{start} and \code{end} in ISO (YYYY-MM-DD) or YYYYMMDD format. E.g. \code{c(start = "2020-02-15", end = "2020-02-17")}; +\item or a \code{character} object of the form \code{YYYY-MM-DD_YYYY-MM-DD} or \code{YYYYMMDD_YYYYMMDD}. For example, \verb{2020-02-15_2020-02-17} or \verb{20200215_20200217}. +\item A regular expression to match dates in the format \code{YYYYMMDD}. \code{character} object. For example, \verb{^202002} will match all dates in February 2020. +}} + +\item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{spod_get_data_dir()} which returns the value of the environment variable \code{SPANISH_OD_DATA_DIR} or a temporary directory if the variable is not set.} + +\item{quiet}{Logical. If \code{TRUE}, the function does not print messages to the console. Defaults to \code{FALSE}.} + +\item{return_output}{Logical. If \code{TRUE}, the function returns a character vector of the paths to the downloaded files. If \code{FALSE}, the function returns \code{NULL}.} +} +\value{ +A character vector of the paths to the downloaded files. Unless \code{return_output = FALSE}, in which case the function returns \code{NULL}. +} +\description{ +This function downloads the data files of the specified type, zones, dates and data version. +} diff --git a/man/spod_download_tables.Rd b/man/spod_download_tables.Rd deleted file mode 100644 index a201c3e..0000000 --- a/man/spod_download_tables.Rd +++ /dev/null @@ -1,35 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/get-v1-data.R -\name{spod_download_tables} -\alias{spod_download_tables} -\title{Download the data files of specified type, zones, dates and data version} -\usage{ -spod_download_tables( - type = c("od", "origin-destination", "os", "overnight_stays", "tpp", - "trips_per_person"), - zones = c("districts", "dist", "distr", "municipalities", "muni", "municip"), - date_range = NULL, - dates_list = NULL, - date_regex = NULL, - ver = 1, - data_dir = spod_get_data_dir() -) -} -\arguments{ -\item{type}{The type of data to download. Can be "origin-destination" (or ust "od"), or "trips_per_person" (or just "tpp") for v1 data. For v2 data "overnight_stays" (or just "os") is also available. More data types to be supported in the future. See respective codebooks for more information. \link{ADD CODEBOOKS!}} - -\item{zones}{The zones for which to download the data. Can be "districts" (or "dist", "distr") or "municipalities" (or "muni", "municip") for v1 data. Additionaly, these can be "urban_areas" (GAUs) for v2 data.} - -\item{date_range}{A character vector of dates in ISO format (YYYY-MM-DD) to download the data for.} - -\item{dates_list}{A character vector of dates in ISO format (YYYY-MM-DD) to download the data for. Defaults to NULL.} - -\item{date_regex}{A regular expression to match the dates of the data to download. Defaults to NULL.} - -\item{ver}{The version of the data to use. Defaults to 1. Can be 1 or 2.} - -\item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{spod_get_data_dir()} which returns the value of the environment variable \code{SPANISH_OD_DATA_DIR} or a temporary directory if the variable is not set.} -} -\description{ -This function downloads the data files of the specified type, zones, dates and data version. -} diff --git a/man/expand_dates_from_regex.Rd b/man/spod_expand_dates_from_regex.Rd similarity index 62% rename from man/expand_dates_from_regex.Rd rename to man/spod_expand_dates_from_regex.Rd index 15351ea..ed24db9 100644 --- a/man/expand_dates_from_regex.Rd +++ b/man/spod_expand_dates_from_regex.Rd @@ -1,15 +1,13 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/internal-utils.R -\name{expand_dates_from_regex} -\alias{expand_dates_from_regex} +% Please edit documentation in R/internal_utils.R +\name{spod_expand_dates_from_regex} +\alias{spod_expand_dates_from_regex} \title{Function to expand dates from a regex} \usage{ -expand_dates_from_regex(date_regex, ver = c(1, 2)) +spod_expand_dates_from_regex(date_regex) } \arguments{ \item{date_regex}{A regular expression to match dates in the format yyyymmdd.} - -\item{ver}{The version of the data to use. Defaults to "v1". Can be "v1" or "v2".} } \value{ A character vector of dates matching the regex. diff --git a/man/spod_get_data_dir.Rd b/man/spod_get_data_dir.Rd index 46ccf5d..f291402 100644 --- a/man/spod_get_data_dir.Rd +++ b/man/spod_get_data_dir.Rd @@ -4,7 +4,7 @@ \alias{spod_get_data_dir} \title{Get the data directory} \usage{ -spod_get_data_dir() +spod_get_data_dir(quiet = FALSE) } \value{ The data directory. diff --git a/man/spod_get_latest_v1_file_list.Rd b/man/spod_get_latest_v1_file_list.Rd index 8af96b0..3b6a8ff 100644 --- a/man/spod_get_latest_v1_file_list.Rd +++ b/man/spod_get_latest_v1_file_list.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/get-v1-data.R +% Please edit documentation in R/get_v1_data.R \name{spod_get_latest_v1_file_list} \alias{spod_get_latest_v1_file_list} \title{Get latest file list from the XML for MITMA open mobiltiy data v1 (2020-2021)} diff --git a/man/spod_get_metadata.Rd b/man/spod_get_metadata.Rd index 93c4bef..9682340 100644 --- a/man/spod_get_metadata.Rd +++ b/man/spod_get_metadata.Rd @@ -4,7 +4,7 @@ \alias{spod_get_metadata} \title{Get the data dictionary} \usage{ -spod_get_metadata(data_dir = spod_get_data_dir()) +spod_get_metadata(data_dir = spod_get_data_dir(), quiet = FALSE) } \arguments{ \item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{spod_get_data_dir()}.} diff --git a/man/spod_get_od.Rd b/man/spod_get_od.Rd new file mode 100644 index 0000000..5313811 --- /dev/null +++ b/man/spod_get_od.Rd @@ -0,0 +1,39 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get_v1_data.R +\name{spod_get_od} +\alias{spod_get_od} +\title{Retrieve the origin-destination v1 data (2020-2021)} +\usage{ +spod_get_od( + zones = c("districts", "dist", "distr", "municipalities", "muni", "municip"), + dates = NULL, + data_dir = spod_get_data_dir(), + quiet = FALSE, + read_fun = duckdb::tbl_file +) +} +\arguments{ +\item{zones}{The zones for which to download the data. Can be \code{"districts"} (or \code{"dist"}, \code{"distr"}) or \code{"municipalities"} (or \code{"muni"}, \code{"municip"}) for v1 data. Additionaly, these can be \code{"large_urban_areas"} (or \code{"lau"}) for v2 data.} + +\item{dates}{A \code{character} or \code{Date} vector of dates to process. Kindly keep in mind that v1 and v2 data follow different data collection methodologies and may not be directly comparable. Therefore, do not try to request data from both versions for the same date range. If you need to compare data from both versions, please refer to the respective codebooks and methodology documents. The v1 data covers the period from 2020-02-14 to 2021-05-09, and the v2 data covers the period from 2022-01-01 to the present until further notice. The true dates range is checked against the available data for each version on every function run. + +The possible values can be any of the following: +\itemize{ +\item A single date in ISO (YYYY-MM-DD) or YYYYMMDD format. \code{character} or \code{Date} object. +\item A vector of dates in ISO (YYYY-MM-DD) or YYYYMMDD format. \code{character} or \code{Date} object. Can be any non-consecutive sequence of dates. +\item A date range +\item eigher a \code{character} or \code{Date} object of length 2 with clearly named elements \code{start} and \code{end} in ISO (YYYY-MM-DD) or YYYYMMDD format. E.g. \code{c(start = "2020-02-15", end = "2020-02-17")}; +\item or a \code{character} object of the form \code{YYYY-MM-DD_YYYY-MM-DD} or \code{YYYYMMDD_YYYYMMDD}. For example, \verb{2020-02-15_2020-02-17} or \verb{20200215_20200217}. +\item A regular expression to match dates in the format \code{YYYYMMDD}. \code{character} object. For example, \verb{^202002} will match all dates in February 2020. +}} + +\item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{spod_get_data_dir()} which returns the value of the environment variable \code{SPANISH_OD_DATA_DIR} or a temporary directory if the variable is not set.} + +\item{quiet}{Logical. If \code{TRUE}, the function does not print messages to the console. Defaults to \code{FALSE}.} +} +\value{ +A tibble with the origin-destination data. +} +\description{ +This function retrieves the v1 (2020-2021) origin-destination data from the specified data directory. +} diff --git a/man/spod_get_od_v1.Rd b/man/spod_get_od_v1.Rd deleted file mode 100644 index c608016..0000000 --- a/man/spod_get_od_v1.Rd +++ /dev/null @@ -1,18 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/get-v1-data.R -\name{spod_get_od_v1} -\alias{spod_get_od_v1} -\title{Retrieve the origin-destination v1 data (2020-2021)} -\usage{ -spod_get_od_v1( - date_range = c("2020-02-14", "2020-02-15"), - dates_list = NULL, - date_regex = NULL, - zones = c("distritos", "municipios"), - data_dir = spod_get_data_dir(), - read_fun = duckdb::tbl_file -) -} -\description{ -This function retrieves the origin-destination data from the specified data directory. -} diff --git a/man/spod_get_zones_v1.Rd b/man/spod_get_zones_v1.Rd index d096738..3168e9f 100644 --- a/man/spod_get_zones_v1.Rd +++ b/man/spod_get_zones_v1.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/get-v1-data.R +% Please edit documentation in R/get_v1_data.R \name{spod_get_zones_v1} \alias{spod_get_zones_v1} \title{Retrieves the zones for v1 data} diff --git a/man/spod_is_data_version_overlaps.Rd b/man/spod_is_data_version_overlaps.Rd new file mode 100644 index 0000000..59e28ca --- /dev/null +++ b/man/spod_is_data_version_overlaps.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/internal_utils.R +\name{spod_is_data_version_overlaps} +\alias{spod_is_data_version_overlaps} +\title{Check if specified dates span both data versions} +\usage{ +spod_is_data_version_overlaps(dates) +} +\arguments{ +\item{dates}{A Dates vector of dates to check.} +} +\value{ +TRUE if the dates span both data versions, FALSE otherwise. +} +\description{ +This function checks if the specified dates or date ranges span both v1 and v2 data versions. +} +\keyword{internal} diff --git a/man/spod_match_data_type.Rd b/man/spod_match_data_type.Rd index 76b7295..4bf6f37 100644 --- a/man/spod_match_data_type.Rd +++ b/man/spod_match_data_type.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/internal-utils.R +% Please edit documentation in R/internal_utils.R \name{spod_match_data_type} \alias{spod_match_data_type} \title{Match data types to folders} From 17017a3f02cb4761c3001c7605e5fce369ebcbc0 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Wed, 7 Aug 2024 14:32:41 +0200 Subject: [PATCH 19/25] ensure newlines in the end of files --- R/download_data.R | 2 +- R/folders.R | 2 +- R/get.R | 2 +- R/internal_utils.R | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/R/download_data.R b/R/download_data.R index fc8a73a..d89da3e 100644 --- a/R/download_data.R +++ b/R/download_data.R @@ -99,4 +99,4 @@ spod_download_data <- function( if (return_output) { return(requested_files$local_path) } -} \ No newline at end of file +} diff --git a/R/folders.R b/R/folders.R index f3cfe9e..795a68e 100644 --- a/R/folders.R +++ b/R/folders.R @@ -6,4 +6,4 @@ spod_subfolder_raw_data_cache <- function(ver = 1) { } base_subdir_name <- "raw_data_cache" return(paste0(base_subdir_name, "/v", ver, "/")) -} \ No newline at end of file +} diff --git a/R/get.R b/R/get.R index 465bf39..2cf533a 100644 --- a/R/get.R +++ b/R/get.R @@ -181,4 +181,4 @@ download_od = function( } } return(metadata_od$local_path) -} \ No newline at end of file +} diff --git a/R/internal_utils.R b/R/internal_utils.R index 245c526..7eca22d 100644 --- a/R/internal_utils.R +++ b/R/internal_utils.R @@ -225,4 +225,4 @@ spod_match_data_type <- function( # need to add a warning here that the type is not recognized return(NULL) -} \ No newline at end of file +} From 762174095b28b1d3b79d5417114216385d3b7a63 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Wed, 7 Aug 2024 14:46:03 +0200 Subject: [PATCH 20/25] move current_timestamp from arguments to body of get xml 2 --- R/get.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/get.R b/R/get.R index 2cf533a..ef4b6e4 100644 --- a/R/get.R +++ b/R/get.R @@ -2,7 +2,6 @@ #' #' @param data_dir The directory where the data is stored. Defaults to the value returned by `spod_get_data_dir()`. #' @param xml_url The URL of the XML file to download. Defaults to "https://movilidad-opendata.mitma.es/RSS.xml". -#' @param current_timestamp The current timestamp to keep track of the version of the remote file list. Defaults to the current date. #' #' @return The path to the downloaded XML file. #' @export @@ -12,12 +11,13 @@ #' } spod_get_latest_v2_xml = function( data_dir = spod_get_data_dir(), - xml_url = "https://movilidad-opendata.mitma.es/RSS.xml", - current_timestamp = format(Sys.time(), format = "%Y-%m-%d", usetz = FALSE, tz = "UTC")) { + xml_url = "https://movilidad-opendata.mitma.es/RSS.xml" +) { if (!fs::dir_exists(data_dir)) { fs::dir_create(data_dir) } + current_timestamp = format(Sys.time(), format = "%Y-%m-%d", usetz = FALSE, tz = "UTC") current_filename = glue::glue("{data_dir}/data_links_v2_{current_timestamp}.xml") message("Saving the file to: ", current_filename) From cee4eb6248e642f1d3e25f29e0237255f743b502 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Wed, 7 Aug 2024 15:24:10 +0200 Subject: [PATCH 21/25] rename type to zones in get_zones v1 to unify naming between get_zones and download data --- R/get_v1_data.R | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/R/get_v1_data.R b/R/get_v1_data.R index 799a93c..659af36 100644 --- a/R/get_v1_data.R +++ b/R/get_v1_data.R @@ -126,7 +126,7 @@ spod_available_data_v1 <- function(data_dir = spod_get_data_dir(), #' It can retrieve either "distritos" or "municipios" zones data. #' #' @param data_dir The directory where the data is stored. -#' @param type The type of zones data to retrieve ("distritos" or "municipios"). +#' @param zones The zones for which to download the data. Can be `"districts"` (or `"dist"`, `"distr"`) or `"municipalities"` (or `"muni"`, `"municip"`). #' @return A spatial object containing the zones data. #' @export #' @examples @@ -134,13 +134,15 @@ spod_available_data_v1 <- function(data_dir = spod_get_data_dir(), #' zones <- spod_get_zones() #' } spod_get_zones_v1 <- function( - type = c("distritos", "municipios"), + zones = c("districts", "dist", "distr", + "municipalities", "muni", "municip"), data_dir = spod_get_data_dir() ) { - type <- match.arg(type) + zones <- match.arg(zones) + zones <- spod_zone_names_en2es(zones) # check if shp files are already extracted - expected_gpkg_path <- fs::path(data_dir, glue::glue("clean_data/v1//zones/{type}_mitma.gpkg")) + expected_gpkg_path <- fs::path(data_dir, glue::glue("clean_data/v1//zones/{zones}_mitma.gpkg")) if (fs::file_exists(expected_gpkg_path)) { message("Loading .gpkg file that already exists in data dir: ", expected_gpkg_path) return(sf::read_sf(expected_gpkg_path)) @@ -149,7 +151,7 @@ spod_get_zones_v1 <- function( # if data is not available, download, extract, clean and save it to gpkg metadata <- spod_available_data_v1(data_dir, check_local_files = FALSE) - regex <- glue::glue("zonificacion_{type}\\.") + regex <- glue::glue("zonificacion_{zones}\\.") sel_zones <- stringr::str_detect(metadata$target_url, regex) metadata_zones <- metadata[sel_zones, ] dir_name <- fs::path_dir(metadata_zones$local_path[1]) @@ -174,7 +176,7 @@ spod_get_zones_v1 <- function( junk_path <- paste0(fs::path_dir(downloaded_file), "/__MACOSX") if (fs::dir_exists(junk_path)) fs::dir_delete(junk_path) - zones_path <- fs::dir_ls(data_dir, glob = glue::glue("**{type}/*.shp"), recurse = TRUE) + zones_path <- fs::dir_ls(data_dir, glob = glue::glue("**{zones}/*.shp"), recurse = TRUE) zones <- spod_clean_zones_v1(zones_path) fs::dir_create(fs::path_dir(expected_gpkg_path), recurse = TRUE) From 3e166eef5eced801218cbdd31ce548cabc1ba4fb Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Wed, 7 Aug 2024 15:37:28 +0200 Subject: [PATCH 22/25] fixes to arguments and docs to pass cmd check --- R/download_data.R | 12 ++++++++---- R/get_v1_data.R | 2 ++ man/spod_available_data_v1.Rd | 4 ++++ man/spod_download_data.Rd | 16 ++++++++++++++++ man/spod_get_latest_v2_xml.Rd | 6 +----- man/spod_get_zones_v1.Rd | 4 ++-- 6 files changed, 33 insertions(+), 11 deletions(-) diff --git a/R/download_data.R b/R/download_data.R index d89da3e..dd976a7 100644 --- a/R/download_data.R +++ b/R/download_data.R @@ -14,13 +14,17 @@ #' @examples #' \dontrun{ #' # Download the origin-destination on district level for the a date range in March 2020 -#' spod_download_data(type = "od", zones = "districts", date_range = c("2020-03-20", "2020-03-24")) +#' spod_download_data(type = "od", zones = "districts", +#' date_range = c("2020-03-20", "2020-03-24")) #' #' # Download the origin-destination on district level for select dates in 2020 and 2021 -#' spod_download_data(type = "od", zones = "dist", dates_list = c("2020-03-20", "2020-03-24", "2021-03-20", "2021-03-24")) +#' spod_download_data(type = "od", zones = "dist", +#' dates_list = c("2020-03-20", "2020-03-24", "2021-03-20", "2021-03-24")) #' -#' # Download the origin-destination on municipality level using regex for a date range in March 2020 (the regex will capture the dates 2020-03-20 to 2020-03-24) -#' spod_download_data(type = "od", zones = "municip", date_regex = "2020032[0-4]) +#' # Download the origin-destination on municipality level using regex for a date range in March 2020 +#' # (the regex will capture the dates 2020-03-20 to 2020-03-24) +#' spod_download_data(type = "od", zones = "municip", +#' date_regex = "2020032[0-4]") #' } spod_download_data <- function( type = c( diff --git a/R/get_v1_data.R b/R/get_v1_data.R index 659af36..f82a273 100644 --- a/R/get_v1_data.R +++ b/R/get_v1_data.R @@ -33,6 +33,8 @@ spod_get_latest_v1_file_list <- function( #' This function provides a table of the available data list of MITMA v1 (2020-2021), both remote and local. #' #' @param data_dir The directory where the data is stored. Defaults to the value returned by `spod_get_data_dir()`. +#' @param check_local_files Whether to check if the local files exist. Defaults to `FALSE`. +#' @param quiet Whether to suppress messages. Defaults to `FALSE`. #' @return A tibble with links, release dates of files in the data, dates of data coverage, local paths to files, and the download status. #' \describe{ #' \item{target_url}{\code{character}. The URL link to the data file.} diff --git a/man/spod_available_data_v1.Rd b/man/spod_available_data_v1.Rd index cfea9b2..74c67b9 100644 --- a/man/spod_available_data_v1.Rd +++ b/man/spod_available_data_v1.Rd @@ -12,6 +12,10 @@ spod_available_data_v1( } \arguments{ \item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{spod_get_data_dir()}.} + +\item{check_local_files}{Whether to check if the local files exist. Defaults to \code{FALSE}.} + +\item{quiet}{Whether to suppress messages. Defaults to \code{FALSE}.} } \value{ A tibble with links, release dates of files in the data, dates of data coverage, local paths to files, and the download status. diff --git a/man/spod_download_data.Rd b/man/spod_download_data.Rd index 300bb59..11ee274 100644 --- a/man/spod_download_data.Rd +++ b/man/spod_download_data.Rd @@ -44,3 +44,19 @@ A character vector of the paths to the downloaded files. Unless \code{return_out \description{ This function downloads the data files of the specified type, zones, dates and data version. } +\examples{ +\dontrun{ +# Download the origin-destination on district level for the a date range in March 2020 +spod_download_data(type = "od", zones = "districts", + date_range = c("2020-03-20", "2020-03-24")) + +# Download the origin-destination on district level for select dates in 2020 and 2021 +spod_download_data(type = "od", zones = "dist", + dates_list = c("2020-03-20", "2020-03-24", "2021-03-20", "2021-03-24")) + +# Download the origin-destination on municipality level using regex for a date range in March 2020 +# (the regex will capture the dates 2020-03-20 to 2020-03-24) +spod_download_data(type = "od", zones = "municip", + date_regex = "2020032[0-4]") +} +} diff --git a/man/spod_get_latest_v2_xml.Rd b/man/spod_get_latest_v2_xml.Rd index 7b9643e..2674530 100644 --- a/man/spod_get_latest_v2_xml.Rd +++ b/man/spod_get_latest_v2_xml.Rd @@ -6,17 +6,13 @@ \usage{ spod_get_latest_v2_xml( data_dir = spod_get_data_dir(), - xml_url = "https://movilidad-opendata.mitma.es/RSS.xml", - current_timestamp = format(Sys.time(), format = "\%Y-\%m-\%d", usetz = FALSE, tz = - "UTC") + xml_url = "https://movilidad-opendata.mitma.es/RSS.xml" ) } \arguments{ \item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{spod_get_data_dir()}.} \item{xml_url}{The URL of the XML file to download. Defaults to "https://movilidad-opendata.mitma.es/RSS.xml".} - -\item{current_timestamp}{The current timestamp to keep track of the version of the remote file list. Defaults to the current date.} } \value{ The path to the downloaded XML file. diff --git a/man/spod_get_zones_v1.Rd b/man/spod_get_zones_v1.Rd index 3168e9f..79ec9ff 100644 --- a/man/spod_get_zones_v1.Rd +++ b/man/spod_get_zones_v1.Rd @@ -5,12 +5,12 @@ \title{Retrieves the zones for v1 data} \usage{ spod_get_zones_v1( - type = c("distritos", "municipios"), + zones = c("districts", "dist", "distr", "municipalities", "muni", "municip"), data_dir = spod_get_data_dir() ) } \arguments{ -\item{type}{The type of zones data to retrieve ("distritos" or "municipios").} +\item{zones}{The zones for which to download the data. Can be \code{"districts"} (or \code{"dist"}, \code{"distr"}) or \code{"municipalities"} (or \code{"muni"}, \code{"municip"}).} \item{data_dir}{The directory where the data is stored.} } From 88c69d862762434ccc83885c05443ba2f686af9b Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Wed, 7 Aug 2024 15:41:59 +0200 Subject: [PATCH 23/25] more fixes to pass r cmd check --- DESCRIPTION | 1 - R/download_data.R | 2 +- R/get.R | 1 + R/get_v1_data.R | 2 +- man/spod_download_data.Rd | 2 +- man/spod_get_metadata.Rd | 2 ++ man/spod_get_od.Rd | 2 ++ 7 files changed, 8 insertions(+), 4 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 08fc1ba..142e58a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -21,7 +21,6 @@ License: MIT + file LICENSE Depends: R (>= 3.5.0) Imports: - assertthat, curl, DBI, duckdb, diff --git a/R/download_data.R b/R/download_data.R index dd976a7..9a9edde 100644 --- a/R/download_data.R +++ b/R/download_data.R @@ -1,4 +1,4 @@ -#' Download the data files of specified type, zones, dates and data version +#' Download the data files of specified type, zones, and dates #' #' This function downloads the data files of the specified type, zones, dates and data version. #' @param type The type of data to download. Can be `"origin-destination"` (or ust `"od"`), or `"trips_per_person"` (or just `"tpp"`) for v1 data. For v2 data `"overnight_stays"` (or just `"os"`) is also available. More data types to be supported in the future. See respective codebooks for more information. **ADD CODEBOOKS! to the package** diff --git a/R/get.R b/R/get.R index ef4b6e4..72edbb9 100644 --- a/R/get.R +++ b/R/get.R @@ -30,6 +30,7 @@ spod_get_latest_v2_xml = function( #' This function retrieves the data dictionary for the specified data directory. #' #' @param data_dir The directory where the data is stored. Defaults to the value returned by `spod_get_data_dir()`. +#' @param quiet Whether to suppress messages. Defaults to `FALSE`. #' @return The data dictionary. #' @export #' @examples diff --git a/R/get_v1_data.R b/R/get_v1_data.R index f82a273..1da532c 100644 --- a/R/get_v1_data.R +++ b/R/get_v1_data.R @@ -212,7 +212,7 @@ spod_clean_zones_v1 <- function(zones_path) { #' Retrieve the origin-destination v1 data (2020-2021) #' #' This function retrieves the v1 (2020-2021) origin-destination data from the specified data directory. -#' +#' @param read_fun The function to read the data. Defaults to `duckdb::tbl_file`. #' @inheritParams spod_download_data #' @return A tibble with the origin-destination data. spod_get_od <- function( diff --git a/man/spod_download_data.Rd b/man/spod_download_data.Rd index 11ee274..3639bc5 100644 --- a/man/spod_download_data.Rd +++ b/man/spod_download_data.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/download_data.R \name{spod_download_data} \alias{spod_download_data} -\title{Download the data files of specified type, zones, dates and data version} +\title{Download the data files of specified type, zones, and dates} \usage{ spod_download_data( type = c("od", "origin-destination", "os", "overnight_stays", "tpp", diff --git a/man/spod_get_metadata.Rd b/man/spod_get_metadata.Rd index 9682340..81e79fb 100644 --- a/man/spod_get_metadata.Rd +++ b/man/spod_get_metadata.Rd @@ -8,6 +8,8 @@ spod_get_metadata(data_dir = spod_get_data_dir(), quiet = FALSE) } \arguments{ \item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{spod_get_data_dir()}.} + +\item{quiet}{Whether to suppress messages. Defaults to \code{FALSE}.} } \value{ The data dictionary. diff --git a/man/spod_get_od.Rd b/man/spod_get_od.Rd index 5313811..939a4d7 100644 --- a/man/spod_get_od.Rd +++ b/man/spod_get_od.Rd @@ -30,6 +30,8 @@ The possible values can be any of the following: \item{data_dir}{The directory where the data is stored. Defaults to the value returned by \code{spod_get_data_dir()} which returns the value of the environment variable \code{SPANISH_OD_DATA_DIR} or a temporary directory if the variable is not set.} \item{quiet}{Logical. If \code{TRUE}, the function does not print messages to the console. Defaults to \code{FALSE}.} + +\item{read_fun}{The function to read the data. Defaults to \code{duckdb::tbl_file}.} } \value{ A tibble with the origin-destination data. From 797d8ece65e257ca22d61fb93540ff24c17ae341 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Wed, 7 Aug 2024 16:13:33 +0200 Subject: [PATCH 24/25] bundle xml files for tests to avoid data download. copy them to test dir for successful tests --- .Rbuildignore | 2 +- .gitignore | 5 ++++ R/get.R | 4 +++ inst/extdata/data_links_v1_2024-08-07.xml.gz | Bin 0 -> 26495 bytes inst/extdata/data_links_v2_2024-08-07.xml.gz | Bin 0 -> 131451 bytes tests/testthat/test-internal_utils.R | 29 ++++++++++++++++++- 6 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 inst/extdata/data_links_v1_2024-08-07.xml.gz create mode 100644 inst/extdata/data_links_v2_2024-08-07.xml.gz diff --git a/.Rbuildignore b/.Rbuildignore index d69ac4d..d7e1207 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -11,4 +11,4 @@ README.qmd ^LICENSE\.md$ ^.*\.Rproj$ ^\.Rproj\.user$ -private \ No newline at end of file +^private$ diff --git a/.gitignore b/.gitignore index 47a82d5..1fceb1f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ +# Ignore all gz files *.gz + +# Exceptions for gz files in inst/extdata +!inst/extdata/*.gz + movilidad.duckdb .Rhistory zonificacion_distritos* diff --git a/R/get.R b/R/get.R index 72edbb9..2bfe88f 100644 --- a/R/get.R +++ b/R/get.R @@ -88,6 +88,10 @@ spod_get_data_dir = function(quiet = FALSE) { if (isFALSE(quiet)) warning("Warning: SPANISH_OD_DATA_DIR is not set. Using the temporary directory, which is not recommended, as the data will be deleted when the session ends.\n\n To set the data directory, use `Sys.setenv(SPANISH_OD_DATA_DIR = '/path/to/data')` or set SPANISH_OD_DATA_DIR permanently in the environment by editing the `.Renviron` file locally for current project with `usethis::edit_r_environ('project')` or `file.edit('.Renviron')` or globally for all projects with `usethis::edit_r_environ('user')` or `file.edit('~/.Renviron')`.") data_dir_env = tempdir() # if not set, use the temp directory } + # check if dir exists and create it if it doesn't + if (!fs::dir_exists(data_dir_env)) { + fs::dir_create(data_dir_env) + } return(fs::path_real(data_dir_env)) } diff --git a/inst/extdata/data_links_v1_2024-08-07.xml.gz b/inst/extdata/data_links_v1_2024-08-07.xml.gz new file mode 100644 index 0000000000000000000000000000000000000000..1fd4912bfbe747fde413cf18ceb7935978f479f7 GIT binary patch literal 26495 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 E-;_V5P5=M^ literal 0 HcmV?d00001 diff --git a/inst/extdata/data_links_v2_2024-08-07.xml.gz b/inst/extdata/data_links_v2_2024-08-07.xml.gz new file mode 100644 index 0000000000000000000000000000000000000000..fe90e8498c4ff350a8306211ffd05340f3d07fd0 GIT binary patch literal 131451 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 S>*|3=2=5Ox=7xtE&-pJo@Fzb2 literal 0 HcmV?d00001 diff --git a/tests/testthat/test-internal_utils.R b/tests/testthat/test-internal_utils.R index 8269630..dd6ef6a 100644 --- a/tests/testthat/test-internal_utils.R +++ b/tests/testthat/test-internal_utils.R @@ -1,4 +1,28 @@ -Sys.setenv(SPANISH_OD_DATA_DIR = tempdir()) +# Prepare the testing environment using bundled xml files to avoid downloading data from the internet + +extdata_path <- system.file("extdata", package = "spanishoddata") +gz_files <- list.files(extdata_path, pattern = "data_links_.*\\.xml\\.gz", full.names = TRUE) + +if (length(gz_files) == 0) stop("No gzipped XML files found.") + +# Create a temporary directory +test_data_dir <- tempfile() +dir.create(test_data_dir, recursive = TRUE) + +current_date <- format(Sys.time(), format = "%Y-%m-%d", usetz = FALSE) + +# Copy and rename gzipped XML files to the temporary directory +for (gz_file in gz_files) { + if (grepl("v1", gz_file)) { + file.copy(gz_file, file.path(test_data_dir, paste0("data_links_v1_", current_date, ".xml.gz"))) + } else if (grepl("v2", gz_file)) { + file.copy(gz_file, file.path(test_data_dir, paste0("data_links_v2_", current_date, ".xml.gz"))) + } +} + +# Set the environment variable to the test directory +Sys.setenv(SPANISH_OD_DATA_DIR = test_data_dir) + test_that("single ISO date input", { dates <- "2023-07-01" @@ -71,3 +95,6 @@ test_that("dates that are out of availabe range of v1 data", { expect_error(spod_dates_argument_to_dates_seq(dates), "Some dates do not match the available data.") }) + +# clean up +unlink(test_data_dir, recursive = TRUE) From d1a1e52d7af89ae000e0b23dec18e1eecffd2038 Mon Sep 17 00:00:00 2001 From: Egor Kotov Date: Fri, 9 Aug 2024 12:35:54 +0200 Subject: [PATCH 25/25] move data type check after the version detection --- R/download_data.R | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/R/download_data.R b/R/download_data.R index 9a9edde..10101b3 100644 --- a/R/download_data.R +++ b/R/download_data.R @@ -39,10 +39,6 @@ spod_download_data <- function( quiet = FALSE, return_output = TRUE ) { - # convert english data type names to spanish words used in the default data paths - type <- match.arg(type) - type <- spod_match_data_type(type = type, ver = ver) - # convert english zone names to spanish words used in the default data paths zones <- match.arg(zones) zones <- spod_zone_names_en2es(zones) @@ -56,6 +52,10 @@ spod_download_data <- function( ver <- spod_infer_data_v_from_dates(dates_to_use) # this leads to a second call to an internal spod_get_valid_dates() which in turn causes a second call to spod_available_data_v1() or spod_get_metadata(). This results in reading the xml files with metadata for the second time. This is not optimal and should be fixed. if (isFALSE(quiet)) message("Data version detected from dates: ", ver) + # convert english data type names to spanish words used in the default data paths + type <- match.arg(type) + type <- spod_match_data_type(type = type, ver = ver) + # get the available data list while checking for files already cached on disk