From 9882870c73a4ef076ef08eabd49e5551737c1e23 Mon Sep 17 00:00:00 2001 From: your name Date: Sun, 3 Dec 2023 01:31:30 -0500 Subject: [PATCH 01/11] Add initial version of `arc_read()` #108 --- NAMESPACE | 2 + R/arc-read.R | 136 +++++++++++++++++++++++++++++++++++++++++ R/arc-select.R | 7 +++ man/arc_read.Rd | 72 ++++++++++++++++++++++ man/obj_check_layer.Rd | 3 + 5 files changed, 220 insertions(+) create mode 100644 R/arc-read.R create mode 100644 man/arc_read.Rd diff --git a/NAMESPACE b/NAMESPACE index f735f55..01e6879 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -12,6 +12,7 @@ export(add_features) export(add_item) export(arc_open) export(arc_raster) +export(arc_read) export(arc_select) export(clear_query) export(create_feature_server) @@ -33,3 +34,4 @@ export(xss_defaults) import(arcgisutils) importFrom(lifecycle,deprecated) importFrom(utils,head) +importFrom(vctrs,vec_as_names) diff --git a/R/arc-read.R b/R/arc-read.R new file mode 100644 index 0000000..377a6e8 --- /dev/null +++ b/R/arc-read.R @@ -0,0 +1,136 @@ +#' Read a ArcGIS FeatureLayer, Table, or ImageServer +#' +#' [arc_read()] combines the functionality of [arc_open()] with [arc_select()] +#' or [arc_raster()] to read an ArcGIS FeatureLayer, Table, or ImageServer to a +#' `sf` object or object of class `SpatRaster`. `n_max` defaults to 10000 to +#' avoid unintentionally reading an entire layer with a very large number of +#' features. +#' +#' @inheritParams arc_open +#' @param col_select Columns to select. Alternate argument for specifying fields +#' if fields is `NULL`. +#' @param col_names If `TRUE`, use the default column names for the feature. If +#' `col_names` is a character function with the same length as the number of +#' columns in the layer, the default names are replaced with the character +#' vector. If `col_names` is one less than the length of the default or if +#' `col_names` if `FALSE`, the existing sf column name is retained. If +#' `col_names` is the string "alias", names are set to match the available +#' alias names for the layer. +#' @inheritParams arc_select +#' @inheritParams arc_raster +#' @param fields Fields to return. Ignored if `col_select` is supplied. +#' @param name_repair See [vctrs::vec_as_names()] for details. +#' @param ... Additional arguments passed to [arc_select()] or [arc_raster()] +#' @examples +#' if (interactive()) { +#' url <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3" +#' +#' arc_read(url) +#' +#' img_url <- "https://landsat2.arcgis.com/arcgis/rest/services/Landsat/MS/ImageServer" +#' +#' arc_read( +#' img_url, +#' width = 1000, height = 1000, +#' xmin = -71, ymin = 43, +#' xmax = -67, ymax = 47.5, +#' bbox_crs = 4326 +#' ) +#' } +#' @export +arc_read <- function(url, + col_names = TRUE, + col_select = NULL, + n_max = getOption("arcgislayers.n_max", default = 10000), + name_repair = "unique", + crs = NULL, + ..., + fields = NULL, + token = Sys.getenv("ARCGIS_TOKEN")) { + service <- arc_open(url = url, token = token) + + crs <- crs %||% sf::st_crs(service) + + if (!obj_is_layer(service)) { + layer <- arc_raster( + x = service, + ..., + crs = crs, + token = token + ) + + return(layer) + } + + if (attr(service, "n") > n_max) { + cli::cli_alert_warning( + "{.arg n_max} is set to {n_max}, less than the {attr(service, 'n')} + features available from this service." + ) + } + + layer <- arc_select( + x = service, + fields = col_select %||% fields, + crs = crs, + n_max = n_max, + token = token, + ... + ) + + set_layer_names( + layer, + col_names = col_names, + name_repair = name_repair, + alias = service[["fields"]][["alias"]] + ) + +} + +#' @noRd +#' @importFrom vctrs vec_as_names +set_layer_names <- function(x, + col_names = NULL, + name_repair = NULL, + alias = NULL, + ...) { + layer_nm <- names(x) + nm <- layer_nm + sf_column_nm <- attributes(x)[["sf_column"]] + + if (is.character(col_names)) { + if (identical(col_names, "alias") && is.character(alias)) { + col_names <- alias + } + + nm <- col_names + } + + nm_len <- length(nm) + + if (rlang::is_false(col_names)) { + nm <- paste0("X", seq(to = nm_len)) + } + + if (inherits(x, "sf") && sf_column_nm != nm[[nm_len]]) { + layer_nm_len <- length(layer_nm) + if (length(nm) == layer_nm_len) { + x <- sf::st_set_geometry(x, nm[[length(layer_nm)]]) + } else if (length(nm) == (layer_nm_len - 1)) { + nm <- c(nm, sf_column_nm) + } + } + + if (!is.null(name_repair)) { + nm <- vctrs::vec_as_names( + names = nm, + repair = name_repair, + repair_arg = "name_repair" + ) + } + + rlang::set_names( + x, + nm = nm + ) +} diff --git a/R/arc-select.R b/R/arc-select.R index c6bec15..43a3612 100644 --- a/R/arc-select.R +++ b/R/arc-select.R @@ -249,6 +249,13 @@ obj_check_layer <- function(x, ) } +#' @rdname obj_check_layer +#' @name obj_is_layer +#' @keywords internal +obj_is_layer <- function(x) { + rlang::inherits_any(x, c("FeatureLayer", "Table")) +} + #' Check if an object inherits from a set of classes #' #' [check_inherits_any()] wraps [rlang::inherits_any()] to error if an object diff --git a/man/arc_read.Rd b/man/arc_read.Rd new file mode 100644 index 0000000..34f7d28 --- /dev/null +++ b/man/arc_read.Rd @@ -0,0 +1,72 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/arc-read.R +\name{arc_read} +\alias{arc_read} +\title{Read a ArcGIS FeatureLayer, Table, or ImageServer} +\usage{ +arc_read( + url, + col_names = TRUE, + col_select = NULL, + n_max = getOption("arcgislayers.n_max", default = 10000), + name_repair = "unique", + crs = NULL, + ..., + fields = NULL, + token = Sys.getenv("ARCGIS_TOKEN") +) +} +\arguments{ +\item{url}{The url of the remote resource. Must be of length one.} + +\item{col_names}{If \code{TRUE}, use the default column names for the feature. If +\code{col_names} is a character function with the same length as the number of +columns in the layer, the default names are replaced with the character +vector. If \code{col_names} is one less than the length of the default or if +\code{col_names} if \code{FALSE}, the existing sf column name is retained. If +\code{col_names} is the string "alias", names are set to match the available +alias names for the layer.} + +\item{col_select}{Columns to select. Alternate argument for specifying fields +if fields is \code{NULL}.} + +\item{n_max}{the maximum number of features to return. By default returns +every feature available. Unused at the moment.} + +\item{name_repair}{See \code{\link[vctrs:vec_as_names]{vctrs::vec_as_names()}} for details.} + +\item{crs}{the spatial reference to be returned. If the CRS is different than +the the CRS for the input \code{FeatureLayer}, a transformation will occur +server-side. Ignored if x is a \code{Table}.} + +\item{...}{Additional arguments passed to \code{\link[=arc_select]{arc_select()}} or \code{\link[=arc_raster]{arc_raster()}}} + +\item{fields}{Fields to return. Ignored if \code{col_select} is supplied.} + +\item{token}{your authorization token. By default checks the environment +variable \code{ARCGIS_TOKEN}. Set your token using \code{arcgisutils::set_auth_token()}.} +} +\description{ +\code{\link[=arc_read]{arc_read()}} combines the functionality of \code{\link[=arc_open]{arc_open()}} with \code{\link[=arc_select]{arc_select()}} +or \code{\link[=arc_raster]{arc_raster()}} to read an ArcGIS FeatureLayer, Table, or ImageServer to a +\code{sf} object or object of class \code{SpatRaster}. \code{n_max} defaults to 10000 to +avoid unintentionally reading an entire layer with a very large number of +features. +} +\examples{ +if (interactive()) { + url <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3" + + arc_read(url) + + img_url <- "https://landsat2.arcgis.com/arcgis/rest/services/Landsat/MS/ImageServer" + + arc_read( + img_url, + width = 1000, height = 1000, + xmin = -71, ymin = 43, + xmax = -67, ymax = 47.5, + bbox_crs = 4326 + ) +} +} diff --git a/man/obj_check_layer.Rd b/man/obj_check_layer.Rd index a18eda4..f35173b 100644 --- a/man/obj_check_layer.Rd +++ b/man/obj_check_layer.Rd @@ -2,9 +2,12 @@ % Please edit documentation in R/arc-select.R \name{obj_check_layer} \alias{obj_check_layer} +\alias{obj_is_layer} \title{Check if an object is a FeatureLayer or Table object} \usage{ obj_check_layer(x, arg = rlang::caller_arg(x), call = rlang::caller_env()) + +obj_is_layer(x) } \arguments{ \item{x}{A \code{FeatureLayer} or \code{Table} class object created with \code{\link[=arc_open]{arc_open()}}.} From 49cab96dae468d1e3757fc84b78388e8fdd3bbbe Mon Sep 17 00:00:00 2001 From: your name Date: Tue, 12 Dec 2023 21:19:41 -0500 Subject: [PATCH 02/11] Move vctrs to Suggests Also add more examples to `arc_read()` --- DESCRIPTION | 3 ++- NAMESPACE | 1 - R/arc-read.R | 22 ++++++++++++++++------ man/arc_read.Rd | 15 +++++++++++---- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 6b5592e..4611088 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -36,7 +36,8 @@ Suggests: dplyr, rmarkdown, testthat (>= 3.0.0), - tidyselect + tidyselect, + vctrs Config/testthat/edition: 3 Remotes: R-ArcGIS/arcgisutils diff --git a/NAMESPACE b/NAMESPACE index 01e6879..2236cf6 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -34,4 +34,3 @@ export(xss_defaults) import(arcgisutils) importFrom(lifecycle,deprecated) importFrom(utils,head) -importFrom(vctrs,vec_as_names) diff --git a/R/arc-read.R b/R/arc-read.R index 377a6e8..3b50b98 100644 --- a/R/arc-read.R +++ b/R/arc-read.R @@ -16,17 +16,27 @@ #' `col_names` if `FALSE`, the existing sf column name is retained. If #' `col_names` is the string "alias", names are set to match the available #' alias names for the layer. +#' @param n_max Maximum number of records to return. Defaults to 10000 or an +#' option set with `options("arcgislayers.n_max" = )` #' @inheritParams arc_select #' @inheritParams arc_raster #' @param fields Fields to return. Ignored if `col_select` is supplied. -#' @param name_repair See [vctrs::vec_as_names()] for details. -#' @param ... Additional arguments passed to [arc_select()] or [arc_raster()] +#' @param name_repair See [vctrs::vec_as_names()] for details. Set `name_repair +#' = NULL` to avoid using [vctrs::vec_as_names()]. +#' @param ... Additional arguments passed to [arc_select()] if URL is a +#' "FeatureLayer" or "Table" or [arc_raster()] if URL is an "ImageLayer". #' @examples #' if (interactive()) { #' url <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3" #' #' arc_read(url) #' +#' arc_read(url, name_repair = tolower) +#' +#' url <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/EmergencyFacilities/FeatureServer/0" +#' +#' arc_read(url, col_names = "alias") +#' #' img_url <- "https://landsat2.arcgis.com/arcgis/rest/services/Landsat/MS/ImageServer" #' #' arc_read( @@ -84,16 +94,14 @@ arc_read <- function(url, name_repair = name_repair, alias = service[["fields"]][["alias"]] ) - } #' @noRd -#' @importFrom vctrs vec_as_names set_layer_names <- function(x, col_names = NULL, name_repair = NULL, alias = NULL, - ...) { + call = rlang::caller_env()) { layer_nm <- names(x) nm <- layer_nm sf_column_nm <- attributes(x)[["sf_column"]] @@ -122,10 +130,12 @@ set_layer_names <- function(x, } if (!is.null(name_repair)) { + rlang::check_installed("vctrs", call = call) nm <- vctrs::vec_as_names( names = nm, repair = name_repair, - repair_arg = "name_repair" + repair_arg = "name_repair", + call = call ) } diff --git a/man/arc_read.Rd b/man/arc_read.Rd index 34f7d28..517df3a 100644 --- a/man/arc_read.Rd +++ b/man/arc_read.Rd @@ -30,16 +30,17 @@ alias names for the layer.} \item{col_select}{Columns to select. Alternate argument for specifying fields if fields is \code{NULL}.} -\item{n_max}{the maximum number of features to return. By default returns -every feature available. Unused at the moment.} +\item{n_max}{Maximum number of records to return. Defaults to 10000 or an +option set with \verb{options("arcgislayers.n_max" = )}} -\item{name_repair}{See \code{\link[vctrs:vec_as_names]{vctrs::vec_as_names()}} for details.} +\item{name_repair}{See \code{\link[vctrs:vec_as_names]{vctrs::vec_as_names()}} for details. Set \code{name_repair = NULL} to avoid using \code{\link[vctrs:vec_as_names]{vctrs::vec_as_names()}}.} \item{crs}{the spatial reference to be returned. If the CRS is different than the the CRS for the input \code{FeatureLayer}, a transformation will occur server-side. Ignored if x is a \code{Table}.} -\item{...}{Additional arguments passed to \code{\link[=arc_select]{arc_select()}} or \code{\link[=arc_raster]{arc_raster()}}} +\item{...}{Additional arguments passed to \code{\link[=arc_select]{arc_select()}} if URL is a +"FeatureLayer" or "Table" or \code{\link[=arc_raster]{arc_raster()}} if URL is an "ImageLayer".} \item{fields}{Fields to return. Ignored if \code{col_select} is supplied.} @@ -59,6 +60,12 @@ if (interactive()) { arc_read(url) + arc_read(url, name_repair = tolower) + + url <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/EmergencyFacilities/FeatureServer/0" + + arc_read(url, col_names = "alias") + img_url <- "https://landsat2.arcgis.com/arcgis/rest/services/Landsat/MS/ImageServer" arc_read( From af2b475514e1c2b9e54ff632d73513d267295da3 Mon Sep 17 00:00:00 2001 From: your name Date: Tue, 12 Dec 2023 21:29:10 -0500 Subject: [PATCH 03/11] Add `arc_read()` to NEWS --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 6fb9e04..a4791b9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # arcgislayers 0.1.0 (unreleased) +- Add `arc_read()` with support for `name_repair` argument using `{vctrs}` (#108) - Add `truncate_layer()` to support truncate and append workflow - Add support for opening `MapServers` - `arc_open()` with a layer that does not support `Query` sets the `n` attribute to`NA` From dbb47404248aaa747c7fd3bea91f64c16c934c9a Mon Sep 17 00:00:00 2001 From: your name Date: Tue, 12 Dec 2023 21:29:22 -0500 Subject: [PATCH 04/11] Add tests for `arc_read()` --- tests/testthat/test-arc_read.R | 51 ++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/testthat/test-arc_read.R diff --git a/tests/testthat/test-arc_read.R b/tests/testthat/test-arc_read.R new file mode 100644 index 0000000..18219f8 --- /dev/null +++ b/tests/testthat/test-arc_read.R @@ -0,0 +1,51 @@ +test_that("arc_read(): FeatureServer can be read", { + + furl <- "https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_Counties_Generalized_Boundaries/FeatureServer/0" + + layer <- arc_read(furl) + + # if any errors occur above here the test will fail + expect_true(TRUE) +}) + + +test_that("arc_read(): ImageServer can be read", { + + img_url <- "https://landsat2.arcgis.com/arcgis/rest/services/Landsat/MS/ImageServer" + + res <- arc_read( + img_url, + xmin = -71, + ymin = 43, + xmax = -67, + ymax = 47.5, + crs = 4326, + height = 100, + width = 100 + ) + + expect_s4_class(res, "SpatRaster") + expect_equal(attr(class(res), "package"), "terra") + +}) + + +test_that("arc_read(): name_repair works", { + + furl <- "https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_Counties_Generalized_Boundaries/FeatureServer/0" + + col_select <- c("NAME", "FIPS") + + layer <- arc_read(furl, col_select = col_select, name_repair = tolower) + + expect_named(layer, c("name", "fips", "geometry")) + + layer <- arc_read(furl, col_select = col_select, col_names = c("Name", "FIPS Code")) + + expect_named(layer, c("Name", "FIPS Code", "geometry")) + + expect_error( + arc_read(furl, col_select = col_select, col_names = c("Name", "Name"), name_repair = "check_unique") + ) + +}) From 2790bdf2a01ccd9895c33520034a585babeb3e3f Mon Sep 17 00:00:00 2001 From: your name Date: Thu, 14 Dec 2023 21:59:02 -0500 Subject: [PATCH 05/11] Fix conflicted NEWS + obj_check_layer.Rd --- NEWS.md | 1 + R/arc-select.R | 4 +--- man/obj_check_layer.Rd | 3 --- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/NEWS.md b/NEWS.md index a4791b9..c5dc0dc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ # arcgislayers 0.1.0 (unreleased) - Add `arc_read()` with support for `name_repair` argument using `{vctrs}` (#108) +- Add `get_layer_estimates()` to retrieve estimate info such as the number of features and the extent of the layer - Add `truncate_layer()` to support truncate and append workflow - Add support for opening `MapServers` - `arc_open()` with a layer that does not support `Query` sets the `n` attribute to`NA` diff --git a/R/arc-select.R b/R/arc-select.R index 43a3612..f380964 100644 --- a/R/arc-select.R +++ b/R/arc-select.R @@ -249,9 +249,7 @@ obj_check_layer <- function(x, ) } -#' @rdname obj_check_layer -#' @name obj_is_layer -#' @keywords internal +#' @noRd obj_is_layer <- function(x) { rlang::inherits_any(x, c("FeatureLayer", "Table")) } diff --git a/man/obj_check_layer.Rd b/man/obj_check_layer.Rd index f35173b..a18eda4 100644 --- a/man/obj_check_layer.Rd +++ b/man/obj_check_layer.Rd @@ -2,12 +2,9 @@ % Please edit documentation in R/arc-select.R \name{obj_check_layer} \alias{obj_check_layer} -\alias{obj_is_layer} \title{Check if an object is a FeatureLayer or Table object} \usage{ obj_check_layer(x, arg = rlang::caller_arg(x), call = rlang::caller_env()) - -obj_is_layer(x) } \arguments{ \item{x}{A \code{FeatureLayer} or \code{Table} class object created with \code{\link[=arc_open]{arc_open()}}.} From 25673cfba74f2b8b1838ca3d3bfb648a5e6bb77c Mon Sep 17 00:00:00 2001 From: your name Date: Sun, 24 Dec 2023 20:23:40 -0800 Subject: [PATCH 06/11] Add docs + comments to `arc_read()` per #118 feedback Also remove warning if n_max specifies fewer features than the max available --- R/arc-read.R | 74 ++++++++++++++++++++++++++----------------------- man/arc_read.Rd | 33 +++++++++++----------- 2 files changed, 57 insertions(+), 50 deletions(-) diff --git a/R/arc-read.R b/R/arc-read.R index 3b50b98..a092afb 100644 --- a/R/arc-read.R +++ b/R/arc-read.R @@ -2,27 +2,28 @@ #' #' [arc_read()] combines the functionality of [arc_open()] with [arc_select()] #' or [arc_raster()] to read an ArcGIS FeatureLayer, Table, or ImageServer to a -#' `sf` object or object of class `SpatRaster`. `n_max` defaults to 10000 to -#' avoid unintentionally reading an entire layer with a very large number of -#' features. +#' `sf` object or object of class `SpatRaster`. Optionally set, check, or modify +#' names for the returned data frame or sf object using the `col_names` and +#' `name_repair` parameters. #' #' @inheritParams arc_open -#' @param col_select Columns to select. Alternate argument for specifying fields -#' if fields is `NULL`. -#' @param col_names If `TRUE`, use the default column names for the feature. If -#' `col_names` is a character function with the same length as the number of -#' columns in the layer, the default names are replaced with the character -#' vector. If `col_names` is one less than the length of the default or if -#' `col_names` if `FALSE`, the existing sf column name is retained. If -#' `col_names` is the string "alias", names are set to match the available +#' @param col_names Default `NULL`. If `TRUE`, use the default column names for +#' the feature. If `col_names` is a character vector with the same length as +#' the number of columns in the layer, the default names are replaced with the +#' new names. If the length of `col_names` is one less than the length of the +#' default column names, the existing sf column name is retained. If +#' `col_names` is the string `"alias"`, names are set to match the available #' alias names for the layer. -#' @param n_max Maximum number of records to return. Defaults to 10000 or an -#' option set with `options("arcgislayers.n_max" = )` +#' @param col_select,fields Default `NULL`. A character vector of the field +#' names to be returned. By default, all fields are returned. `fields` is +#' ignored if `col_select` is supplied. +#' @param n_max Defaults to 10000 or an option set with +#' `options("arcgislayers.n_max" = )`. Maximum number of records +#' to return. #' @inheritParams arc_select #' @inheritParams arc_raster -#' @param fields Fields to return. Ignored if `col_select` is supplied. -#' @param name_repair See [vctrs::vec_as_names()] for details. Set `name_repair -#' = NULL` to avoid using [vctrs::vec_as_names()]. +#' @param name_repair Default `"unique"`. See [vctrs::vec_as_names()] for +#' details. If `name_repair = NULL`, names are set directly. #' @param ... Additional arguments passed to [arc_select()] if URL is a #' "FeatureLayer" or "Table" or [arc_raster()] if URL is an "ImageLayer". #' @examples @@ -48,15 +49,17 @@ #' ) #' } #' @export -arc_read <- function(url, - col_names = TRUE, - col_select = NULL, - n_max = getOption("arcgislayers.n_max", default = 10000), - name_repair = "unique", - crs = NULL, - ..., - fields = NULL, - token = Sys.getenv("ARCGIS_TOKEN")) { +arc_read <- function( + url, + col_names = TRUE, + col_select = NULL, + n_max = getOption("arcgislayers.n_max", default = 10000), + name_repair = "unique", + crs = NULL, + ..., + fields = NULL, + token = Sys.getenv("ARCGIS_TOKEN") +) { service <- arc_open(url = url, token = token) crs <- crs %||% sf::st_crs(service) @@ -72,13 +75,6 @@ arc_read <- function(url, return(layer) } - if (attr(service, "n") > n_max) { - cli::cli_alert_warning( - "{.arg n_max} is set to {n_max}, less than the {attr(service, 'n')} - features available from this service." - ) - } - layer <- arc_select( x = service, fields = col_select %||% fields, @@ -96,6 +92,8 @@ arc_read <- function(url, ) } +#' Set names for layer or table +#' #' @noRd set_layer_names <- function(x, col_names = NULL, @@ -103,11 +101,14 @@ set_layer_names <- function(x, alias = NULL, call = rlang::caller_env()) { layer_nm <- names(x) + + # Use existing names by default nm <- layer_nm - sf_column_nm <- attributes(x)[["sf_column"]] + sf_column_nm <- attr(x, "sf_column") if (is.character(col_names)) { - if (identical(col_names, "alias") && is.character(alias)) { + # Assign alias values as name if col_names = "alias" + if (identical(col_names, "alias")) { col_names <- alias } @@ -117,14 +118,19 @@ set_layer_names <- function(x, nm_len <- length(nm) if (rlang::is_false(col_names)) { + # Use X1, X2, etc. as names if col_names is FALSE nm <- paste0("X", seq(to = nm_len)) } + # If x is a sf object and sf column is not in names, check to ensure names + # work with geometry column if (inherits(x, "sf") && sf_column_nm != nm[[nm_len]]) { layer_nm_len <- length(layer_nm) if (length(nm) == layer_nm_len) { + # If same number of names as layer columns, use last name for geometry x <- sf::st_set_geometry(x, nm[[length(layer_nm)]]) } else if (length(nm) == (layer_nm_len - 1)) { + # If same number of names as layer columns, use existing geometry name nm <- c(nm, sf_column_nm) } } diff --git a/man/arc_read.Rd b/man/arc_read.Rd index 517df3a..77608aa 100644 --- a/man/arc_read.Rd +++ b/man/arc_read.Rd @@ -19,21 +19,24 @@ arc_read( \arguments{ \item{url}{The url of the remote resource. Must be of length one.} -\item{col_names}{If \code{TRUE}, use the default column names for the feature. If -\code{col_names} is a character function with the same length as the number of -columns in the layer, the default names are replaced with the character -vector. If \code{col_names} is one less than the length of the default or if -\code{col_names} if \code{FALSE}, the existing sf column name is retained. If -\code{col_names} is the string "alias", names are set to match the available +\item{col_names}{Default \code{NULL}. If \code{TRUE}, use the default column names for +the feature. If \code{col_names} is a character vector with the same length as +the number of columns in the layer, the default names are replaced with the +new names. If the length of \code{col_names} is one less than the length of the +default column names, the existing sf column name is retained. If +\code{col_names} is the string \code{"alias"}, names are set to match the available alias names for the layer.} -\item{col_select}{Columns to select. Alternate argument for specifying fields -if fields is \code{NULL}.} +\item{col_select, fields}{Default \code{NULL}. A character vector of the field +names to be returned. By default, all fields are returned. \code{fields} is +ignored if \code{col_select} is supplied.} -\item{n_max}{Maximum number of records to return. Defaults to 10000 or an -option set with \verb{options("arcgislayers.n_max" = )}} +\item{n_max}{Defaults to 10000 or an option set with +\verb{options("arcgislayers.n_max" = )}. Maximum number of records +to return.} -\item{name_repair}{See \code{\link[vctrs:vec_as_names]{vctrs::vec_as_names()}} for details. Set \code{name_repair = NULL} to avoid using \code{\link[vctrs:vec_as_names]{vctrs::vec_as_names()}}.} +\item{name_repair}{Default \code{"unique"}. See \code{\link[vctrs:vec_as_names]{vctrs::vec_as_names()}} for +details. If \code{name_repair = NULL}, names are set directly.} \item{crs}{the spatial reference to be returned. If the CRS is different than the the CRS for the input \code{FeatureLayer}, a transformation will occur @@ -42,17 +45,15 @@ server-side. Ignored if x is a \code{Table}.} \item{...}{Additional arguments passed to \code{\link[=arc_select]{arc_select()}} if URL is a "FeatureLayer" or "Table" or \code{\link[=arc_raster]{arc_raster()}} if URL is an "ImageLayer".} -\item{fields}{Fields to return. Ignored if \code{col_select} is supplied.} - \item{token}{your authorization token. By default checks the environment variable \code{ARCGIS_TOKEN}. Set your token using \code{arcgisutils::set_auth_token()}.} } \description{ \code{\link[=arc_read]{arc_read()}} combines the functionality of \code{\link[=arc_open]{arc_open()}} with \code{\link[=arc_select]{arc_select()}} or \code{\link[=arc_raster]{arc_raster()}} to read an ArcGIS FeatureLayer, Table, or ImageServer to a -\code{sf} object or object of class \code{SpatRaster}. \code{n_max} defaults to 10000 to -avoid unintentionally reading an entire layer with a very large number of -features. +\code{sf} object or object of class \code{SpatRaster}. Optionally set, check, or modify +names for the returned data frame or sf object using the \code{col_names} and +\code{name_repair} parameters. } \examples{ if (interactive()) { From 7fdec13e7be201c2e9c849ebadd93e53436c395a Mon Sep 17 00:00:00 2001 From: your name Date: Mon, 25 Dec 2023 16:19:36 -0800 Subject: [PATCH 07/11] Correct typo in default value for col_names --- R/arc-read.R | 2 +- man/arc_read.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/arc-read.R b/R/arc-read.R index a092afb..78c2ef7 100644 --- a/R/arc-read.R +++ b/R/arc-read.R @@ -7,7 +7,7 @@ #' `name_repair` parameters. #' #' @inheritParams arc_open -#' @param col_names Default `NULL`. If `TRUE`, use the default column names for +#' @param col_names Default `TRUE`. If `TRUE`, use the default column names for #' the feature. If `col_names` is a character vector with the same length as #' the number of columns in the layer, the default names are replaced with the #' new names. If the length of `col_names` is one less than the length of the diff --git a/man/arc_read.Rd b/man/arc_read.Rd index 77608aa..f889159 100644 --- a/man/arc_read.Rd +++ b/man/arc_read.Rd @@ -19,7 +19,7 @@ arc_read( \arguments{ \item{url}{The url of the remote resource. Must be of length one.} -\item{col_names}{Default \code{NULL}. If \code{TRUE}, use the default column names for +\item{col_names}{Default \code{TRUE}. If \code{TRUE}, use the default column names for the feature. If \code{col_names} is a character vector with the same length as the number of columns in the layer, the default names are replaced with the new names. If the length of \code{col_names} is one less than the length of the From a5ac75efcb4846522398cb76b6b0d53dd048546d Mon Sep 17 00:00:00 2001 From: your name Date: Mon, 25 Dec 2023 16:22:05 -0800 Subject: [PATCH 08/11] Revise `col_names` parameter definition --- R/arc-read.R | 7 +++---- man/arc_read.Rd | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/R/arc-read.R b/R/arc-read.R index 78c2ef7..212c305 100644 --- a/R/arc-read.R +++ b/R/arc-read.R @@ -10,10 +10,9 @@ #' @param col_names Default `TRUE`. If `TRUE`, use the default column names for #' the feature. If `col_names` is a character vector with the same length as #' the number of columns in the layer, the default names are replaced with the -#' new names. If the length of `col_names` is one less than the length of the -#' default column names, the existing sf column name is retained. If -#' `col_names` is the string `"alias"`, names are set to match the available -#' alias names for the layer. +#' new names. If `col_names` has one fewer name than the default column names, +#' the existing sf column name is retained. If `col_names` is the string +#' `"alias"`, names are set to match the available alias names for the layer. #' @param col_select,fields Default `NULL`. A character vector of the field #' names to be returned. By default, all fields are returned. `fields` is #' ignored if `col_select` is supplied. diff --git a/man/arc_read.Rd b/man/arc_read.Rd index f889159..6b98623 100644 --- a/man/arc_read.Rd +++ b/man/arc_read.Rd @@ -22,10 +22,9 @@ arc_read( \item{col_names}{Default \code{TRUE}. If \code{TRUE}, use the default column names for the feature. If \code{col_names} is a character vector with the same length as the number of columns in the layer, the default names are replaced with the -new names. If the length of \code{col_names} is one less than the length of the -default column names, the existing sf column name is retained. If -\code{col_names} is the string \code{"alias"}, names are set to match the available -alias names for the layer.} +new names. If \code{col_names} has one fewer name than the default column names, +the existing sf column name is retained. If \code{col_names} is the string +\code{"alias"}, names are set to match the available alias names for the layer.} \item{col_select, fields}{Default \code{NULL}. A character vector of the field names to be returned. By default, all fields are returned. \code{fields} is From 99c856188af083d614e5c7e4f378d8f4b21e01ed Mon Sep 17 00:00:00 2001 From: your name Date: Mon, 25 Dec 2023 16:22:48 -0800 Subject: [PATCH 09/11] Split up fields + col_select parameter defnitions --- R/arc-read.R | 6 +++--- man/arc_read.Rd | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/R/arc-read.R b/R/arc-read.R index 212c305..a73d6b5 100644 --- a/R/arc-read.R +++ b/R/arc-read.R @@ -13,9 +13,9 @@ #' new names. If `col_names` has one fewer name than the default column names, #' the existing sf column name is retained. If `col_names` is the string #' `"alias"`, names are set to match the available alias names for the layer. -#' @param col_select,fields Default `NULL`. A character vector of the field -#' names to be returned. By default, all fields are returned. `fields` is -#' ignored if `col_select` is supplied. +#' @param col_select Default `NULL`. A character vector of the field names to be +#' returned. By default, all fields are returned. `fields` is ignored if +#' `col_select` is supplied. #' @param n_max Defaults to 10000 or an option set with #' `options("arcgislayers.n_max" = )`. Maximum number of records #' to return. diff --git a/man/arc_read.Rd b/man/arc_read.Rd index 6b98623..9663d96 100644 --- a/man/arc_read.Rd +++ b/man/arc_read.Rd @@ -26,9 +26,9 @@ new names. If \code{col_names} has one fewer name than the default column names, the existing sf column name is retained. If \code{col_names} is the string \code{"alias"}, names are set to match the available alias names for the layer.} -\item{col_select, fields}{Default \code{NULL}. A character vector of the field -names to be returned. By default, all fields are returned. \code{fields} is -ignored if \code{col_select} is supplied.} +\item{col_select}{Default \code{NULL}. A character vector of the field names to be +returned. By default, all fields are returned. \code{fields} is ignored if +\code{col_select} is supplied.} \item{n_max}{Defaults to 10000 or an option set with \verb{options("arcgislayers.n_max" = )}. Maximum number of records @@ -44,6 +44,9 @@ server-side. Ignored if x is a \code{Table}.} \item{...}{Additional arguments passed to \code{\link[=arc_select]{arc_select()}} if URL is a "FeatureLayer" or "Table" or \code{\link[=arc_raster]{arc_raster()}} if URL is an "ImageLayer".} +\item{fields}{a character vector of the field names that you wish to be +returned. By default all fields are returned.} + \item{token}{your authorization token. By default checks the environment variable \code{ARCGIS_TOKEN}. Set your token using \code{arcgisutils::set_auth_token()}.} } From 9512445141e0278c71b2611a4c26618ed0af18fa Mon Sep 17 00:00:00 2001 From: your name Date: Mon, 25 Dec 2023 16:25:30 -0800 Subject: [PATCH 10/11] Add lifecycle badge + returns tag --- R/arc-read.R | 3 +++ man/arc_read.Rd | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/R/arc-read.R b/R/arc-read.R index a73d6b5..315631f 100644 --- a/R/arc-read.R +++ b/R/arc-read.R @@ -6,6 +6,8 @@ #' names for the returned data frame or sf object using the `col_names` and #' `name_repair` parameters. #' +#' `r lifecycle::badge("experimental")` +#' #' @inheritParams arc_open #' @param col_names Default `TRUE`. If `TRUE`, use the default column names for #' the feature. If `col_names` is a character vector with the same length as @@ -25,6 +27,7 @@ #' details. If `name_repair = NULL`, names are set directly. #' @param ... Additional arguments passed to [arc_select()] if URL is a #' "FeatureLayer" or "Table" or [arc_raster()] if URL is an "ImageLayer". +#' @returns A sf object, a data.frame, or an object of class `SpatRaster`. #' @examples #' if (interactive()) { #' url <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3" diff --git a/man/arc_read.Rd b/man/arc_read.Rd index 9663d96..8e38b00 100644 --- a/man/arc_read.Rd +++ b/man/arc_read.Rd @@ -50,6 +50,9 @@ returned. By default all fields are returned.} \item{token}{your authorization token. By default checks the environment variable \code{ARCGIS_TOKEN}. Set your token using \code{arcgisutils::set_auth_token()}.} } +\value{ +A sf object, a data.frame, or an object of class \code{SpatRaster}. +} \description{ \code{\link[=arc_read]{arc_read()}} combines the functionality of \code{\link[=arc_open]{arc_open()}} with \code{\link[=arc_select]{arc_select()}} or \code{\link[=arc_raster]{arc_raster()}} to read an ArcGIS FeatureLayer, Table, or ImageServer to a @@ -57,6 +60,9 @@ or \code{\link[=arc_raster]{arc_raster()}} to read an ArcGIS FeatureLayer, Table names for the returned data frame or sf object using the \code{col_names} and \code{name_repair} parameters. } +\details{ +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} +} \examples{ if (interactive()) { url <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3" From fb7c40cefcb9a54b772e667edb25a2d4be81f811 Mon Sep 17 00:00:00 2001 From: Josiah Parry Date: Wed, 27 Dec 2023 08:25:58 -0500 Subject: [PATCH 11/11] Remove references to field argument. Add consistent styling for object types. Adjust type checking to emit error for non-layer or image service types Skip tests on CRAN add addition tests for options, n_max, and unsupported types --- R/arc-read.R | 66 +++++++++++++++++++++------------- man/arc_read.Rd | 36 ++++++++++--------- tests/testthat/test-arc_read.R | 47 +++++++++++++++++++++--- 3 files changed, 102 insertions(+), 47 deletions(-) diff --git a/R/arc-read.R b/R/arc-read.R index 315631f..b86f4a9 100644 --- a/R/arc-read.R +++ b/R/arc-read.R @@ -16,8 +16,7 @@ #' the existing sf column name is retained. If `col_names` is the string #' `"alias"`, names are set to match the available alias names for the layer. #' @param col_select Default `NULL`. A character vector of the field names to be -#' returned. By default, all fields are returned. `fields` is ignored if -#' `col_select` is supplied. +#' returned. By default, all fields are returned. #' @param n_max Defaults to 10000 or an option set with #' `options("arcgislayers.n_max" = )`. Maximum number of records #' to return. @@ -26,30 +25,33 @@ #' @param name_repair Default `"unique"`. See [vctrs::vec_as_names()] for #' details. If `name_repair = NULL`, names are set directly. #' @param ... Additional arguments passed to [arc_select()] if URL is a -#' "FeatureLayer" or "Table" or [arc_raster()] if URL is an "ImageLayer". -#' @returns A sf object, a data.frame, or an object of class `SpatRaster`. +#' `FeatureLayer` or `Table` or [arc_raster()] if URL is an `ImageLayer`. +#' @returns An sf object, a `data.frame`, or an object of class `SpatRaster`. #' @examples -#' if (interactive()) { -#' url <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3" +#'if (interactive()) { +#' url <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3" #' -#' arc_read(url) +#' arc_read(url) #' -#' arc_read(url, name_repair = tolower) +#' # apply tolower() to column names +#' arc_read(url, name_repair = tolower) #' -#' url <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/EmergencyFacilities/FeatureServer/0" +#' url <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/EmergencyFacilities/FeatureServer/0" #' -#' arc_read(url, col_names = "alias") +#' # use field aliases as column names +#' arc_read(url, col_names = "alias") #' -#' img_url <- "https://landsat2.arcgis.com/arcgis/rest/services/Landsat/MS/ImageServer" +#' # read an ImageServer directly +#' img_url <- "https://landsat2.arcgis.com/arcgis/rest/services/Landsat/MS/ImageServer" #' -#' arc_read( -#' img_url, -#' width = 1000, height = 1000, -#' xmin = -71, ymin = 43, -#' xmax = -67, ymax = 47.5, -#' bbox_crs = 4326 -#' ) -#' } +#' arc_read( +#' img_url, +#' width = 1000, height = 1000, +#' xmin = -71, ymin = 43, +#' xmax = -67, ymax = 47.5, +#' bbox_crs = 4326 +#' ) +#'} #' @export arc_read <- function( url, @@ -66,7 +68,8 @@ arc_read <- function( crs <- crs %||% sf::st_crs(service) - if (!obj_is_layer(service)) { + # if the server is an ImageServer we use arc_raster + if (inherits(service, "ImageServer")) { layer <- arc_raster( x = service, ..., @@ -75,6 +78,17 @@ arc_read <- function( ) return(layer) + + } else if (!obj_is_layer(service)) { + # if it is not a layer we abort + # implicitly checks for Layer type and permits continuing + cli::cli_abort( + c( + "{.arg url} is not a supported type: + {.val FeatureLayer}, {.val Table}, or {.val ImageServer}", + "i" = "found {.val {class(service)}}" + ) + ) } layer <- arc_select( @@ -97,11 +111,13 @@ arc_read <- function( #' Set names for layer or table #' #' @noRd -set_layer_names <- function(x, - col_names = NULL, - name_repair = NULL, - alias = NULL, - call = rlang::caller_env()) { +set_layer_names <- function( + x, + col_names = NULL, + name_repair = NULL, + alias = NULL, + call = rlang::caller_env() +) { layer_nm <- names(x) # Use existing names by default diff --git a/man/arc_read.Rd b/man/arc_read.Rd index 8e38b00..4f93de0 100644 --- a/man/arc_read.Rd +++ b/man/arc_read.Rd @@ -27,8 +27,7 @@ the existing sf column name is retained. If \code{col_names} is the string \code{"alias"}, names are set to match the available alias names for the layer.} \item{col_select}{Default \code{NULL}. A character vector of the field names to be -returned. By default, all fields are returned. \code{fields} is ignored if -\code{col_select} is supplied.} +returned. By default, all fields are returned.} \item{n_max}{Defaults to 10000 or an option set with \verb{options("arcgislayers.n_max" = )}. Maximum number of records @@ -42,7 +41,7 @@ the the CRS for the input \code{FeatureLayer}, a transformation will occur server-side. Ignored if x is a \code{Table}.} \item{...}{Additional arguments passed to \code{\link[=arc_select]{arc_select()}} if URL is a -"FeatureLayer" or "Table" or \code{\link[=arc_raster]{arc_raster()}} if URL is an "ImageLayer".} +\code{FeatureLayer} or \code{Table} or \code{\link[=arc_raster]{arc_raster()}} if URL is an \code{ImageLayer}.} \item{fields}{a character vector of the field names that you wish to be returned. By default all fields are returned.} @@ -51,7 +50,7 @@ returned. By default all fields are returned.} variable \code{ARCGIS_TOKEN}. Set your token using \code{arcgisutils::set_auth_token()}.} } \value{ -A sf object, a data.frame, or an object of class \code{SpatRaster}. +An sf object, a \code{data.frame}, or an object of class \code{SpatRaster}. } \description{ \code{\link[=arc_read]{arc_read()}} combines the functionality of \code{\link[=arc_open]{arc_open()}} with \code{\link[=arc_select]{arc_select()}} @@ -65,24 +64,27 @@ names for the returned data frame or sf object using the \code{col_names} and } \examples{ if (interactive()) { - url <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3" + url <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3" - arc_read(url) + arc_read(url) - arc_read(url, name_repair = tolower) + # apply tolower() to column names + arc_read(url, name_repair = tolower) - url <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/EmergencyFacilities/FeatureServer/0" + url <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/EmergencyFacilities/FeatureServer/0" - arc_read(url, col_names = "alias") + # use field aliases as column names + arc_read(url, col_names = "alias") - img_url <- "https://landsat2.arcgis.com/arcgis/rest/services/Landsat/MS/ImageServer" + # read an ImageServer directly + img_url <- "https://landsat2.arcgis.com/arcgis/rest/services/Landsat/MS/ImageServer" - arc_read( - img_url, - width = 1000, height = 1000, - xmin = -71, ymin = 43, - xmax = -67, ymax = 47.5, - bbox_crs = 4326 - ) + arc_read( + img_url, + width = 1000, height = 1000, + xmin = -71, ymin = 43, + xmax = -67, ymax = 47.5, + bbox_crs = 4326 + ) } } diff --git a/tests/testthat/test-arc_read.R b/tests/testthat/test-arc_read.R index 18219f8..aea1bff 100644 --- a/tests/testthat/test-arc_read.R +++ b/tests/testthat/test-arc_read.R @@ -1,5 +1,5 @@ test_that("arc_read(): FeatureServer can be read", { - + skip_on_cran() furl <- "https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_Counties_Generalized_Boundaries/FeatureServer/0" layer <- arc_read(furl) @@ -10,7 +10,7 @@ test_that("arc_read(): FeatureServer can be read", { test_that("arc_read(): ImageServer can be read", { - + skip_on_cran() img_url <- "https://landsat2.arcgis.com/arcgis/rest/services/Landsat/MS/ImageServer" res <- arc_read( @@ -20,8 +20,8 @@ test_that("arc_read(): ImageServer can be read", { xmax = -67, ymax = 47.5, crs = 4326, - height = 100, - width = 100 + height = 50, + width = 50 ) expect_s4_class(res, "SpatRaster") @@ -31,7 +31,7 @@ test_that("arc_read(): ImageServer can be read", { test_that("arc_read(): name_repair works", { - + skip_on_cran() furl <- "https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_Counties_Generalized_Boundaries/FeatureServer/0" col_select <- c("NAME", "FIPS") @@ -47,5 +47,42 @@ test_that("arc_read(): name_repair works", { expect_error( arc_read(furl, col_select = col_select, col_names = c("Name", "Name"), name_repair = "check_unique") ) +}) + +test_that("arc_read(): n_max is correct", { + skip_on_cran() + furl <- "https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_Counties_Generalized_Boundaries/FeatureServer/0" + + expect_equal(nrow(arc_read(furl, n_max = 1)), 1L) + expect_equal(nrow(arc_read(furl, n_max = 1234)), 1234L) + +}) + + +test_that("arc_read(): n_max option is respected", { + skip_on_cran() + furl <- "https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_Counties_Generalized_Boundaries/FeatureServer/0" + + # set n_max via options + options("arcgislayers.n_max" = 1234) + + layer <- arc_read(furl) + expect_equal(nrow(layer), 1234L) +}) + +test_that("arc_read(): n_max option is ignored when n_max is set", { + skip_on_cran() + furl <- "https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_Counties_Generalized_Boundaries/FeatureServer/0" + + # set n_max via options + options("arcgislayers.n_max" = 1234) + + layer <- arc_read(furl, n_max = 321) + expect_equal(nrow(layer), 321L) +}) +test_that("arc_read(): correct error with unsupported type", { + skip_on_cran() + furl <- "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer" + expect_error(arc_read(furl), "is not a supported type") })