R/items-funs.R

Defines functions items_as_sf.STACItemCollection items_as_sf.STACItem items_as_sf items_sign.STACItemCollection items_sign.STACItem items_sign items_fields.STACItemCollection items_fields.STACItem items_fields items_reap.STACItemCollection items_reap.STACItem items_reap items_compact.STACItemCollection items_compact items_filter.STACItemCollection items_filter items_assets.STACItemCollection items_assets.STACItem items_assets items_bbox.STACItemCollection items_bbox.STACItem items_bbox items_datetime.STACItemCollection items_datetime.STACItem items_datetime items_next.STACItemCollection items_next items_fetch.STACItemCollection items_fetch items_matched.STACItemCollection items_matched.STACItem items_matched items_length.STACItemCollection items_length.STACItem items_length

Documented in items_assets items_assets.STACItem items_assets.STACItemCollection items_as_sf items_as_sf.STACItem items_as_sf.STACItemCollection items_bbox items_bbox.STACItem items_bbox.STACItemCollection items_compact items_compact.STACItemCollection items_datetime items_datetime.STACItem items_datetime.STACItemCollection items_fetch items_fetch.STACItemCollection items_fields items_fields.STACItem items_fields.STACItemCollection items_filter items_filter.STACItemCollection items_length items_length.STACItem items_length.STACItemCollection items_matched items_matched.STACItem items_matched.STACItemCollection items_next items_next.STACItemCollection items_reap items_reap.STACItem items_reap.STACItemCollection items_sign items_sign.STACItem items_sign.STACItemCollection

#' @title Items functions
#'
#' @description
#' These functions provide support to work with
#' `STACItemCollection` and `STACItem` objects.
#'
#' \itemize{
#' \item `items_length()`: shows how many items there are in
#' the `STACItemCollection` object.
#'
#' \item `items_matched()`: shows how many items matched the
#' search criteria. It supports `search:metadata` (v0.8.0),
#' `context` (v0.9.0), and `numberMatched` (OGC WFS3 core spec).
#'
#' \item `items_fetch()`: request all STAC Items through
#' pagination.
#'
#' \item `items_next()`: fetches a new page from STAC service.
#'
#' \item `items_datetime()`: retrieves the `datetime`
#' field in `properties` from `STACItemCollection` and
#' `STACItem` objects.
#'
#' \item `items_bbox()`: retrieves the `bbox`
#' field of a `STACItemCollection` or a `STACItem` object.
#'
#' \item `item_assets()`: returns the assets name from
#' `STACItemCollection` and `STACItem` objects.
#'
#' \item `items_filter()`: selects only items that match some criteria
#'  (see details section).
#'
#' \item `items_reap()`: extract key values by traversing all items
#' in a `STACItemCollection` object.
#'
#' \item `items_fields()`: lists field names inside an item.
#'
#' \item `items_group()`: `r lifecycle::badge('deprecated')` organizes
#' items as elements of a list using some criteria.
#'
#' \item `items_sign()`: allow access assets by preparing its url.
#'
#' \item `items_as_sf()`: `r lifecycle::badge('experimental')` convert items to `sf` object.
#' }
#'
#' @param items           a `STACItemCollection` object.
#'
#' @param matched_field   a `character` vector with the path
#' where the number of items returned in the named list is located starting
#' from the initial node of the list. For example, if the information is in a
#' position `items$meta$found` of the object, it must be passed as the
#' following parameter `c("meta", "found")`.
#'
#' @param progress        a `logical` indicating if a progress bar must be
#' shown or not. Defaults to `TRUE`.
#'
#' @param simplify        `r lifecycle::badge('deprecated')` no side-effect
#'
#' @param field           a `character` with the names of the field to
#' get the subfields values.
#'
#' @param pick_fn         a `function` used to pick elements from items
#' addressed by `field` parameter.
#'
#' @param index           an `atomic` vector with values as the group index.
#'
#' @param sign_fn         a `function` that receives an item as a parameter
#' and returns an item signed.
#'
#' @param filter_fn       a `function` that receives an item that should
#' evaluate a `logical` value.
#'
#' @param ...             additional arguments. See details.
#'
#' @details
#' Ellipsis argument (`...`) appears in different items functions and
#' has distinct purposes:
#' \itemize{
#' \item `items_matched()` and `items_assets()`: ellipsis is not used.
#'
#' \item `items_fetch()` and `items_next()`: ellipsis is used to pass
#' additional `httr` options to [GET][httr::GET] or [POST][httr::POST]
#' methods, such as [add_headers][httr::add_headers] or
#' [set_cookies][httr::set_cookies].
#'
#' \item `items_fields()`: ellipsis parameter is deprecated in version
#' 0.9.2 of rstac. Please, use `field` parameter instead.
#'
#' \item `items_filter()`: ellipsis is used to pass logical
#' expressions to be evaluated against a `STACItem` field as filter criteria.
#'
#' **WARNING:** the evaluation of filter expressions changed in `rstac` 0.9.2.
#' Older versions of `rstac` used `properties` field to evaluate filter
#' expressions. Below, there is an example of how to write expressions in new
#' `rstac` version:
#' ```R
#' # expression in older version
#' items_filter(stac_obj, `eo:cloud_cover` < 10)
#' # now expressions must refer to properties explicitly
#' items_filter(stac_obj, properties$`eo:cloud_cover` < 10)
#' items_filter(stac_obj, properties[["eo:cloud_cover"]] < 10)
#' ```
#'
#' \item `items_sign()`: in the near future, ellipsis will be used to append
#' key-value pairs to the url query string of an asset.
#' }
#'
#' `items_sign()` has `sign_fn` parameter that must be a function that
#' receives as argument an item and returns a signed item. `rstac` provides
#' `sign_bdc()` and `sign_planetary_computer()` functions to access Brazil
#' Data Cube products and Microsoft Planetary Computer catalogs, respectively.
#'
#' @return
#'
#' \itemize{
#' \item `items_length()`: an `integer` value.
#'
#' \item `items_matched()`: returns an `integer` value if the STAC web server
#' does support this extension. Otherwise returns `NULL`.
#'
#' \item `items_fetch()`: a `STACItemCollection` with all matched items.
#'
#' \item `items_next()`: fetches a new page from STAC service.
#'
#' \item `items_datetime()`: a `list` of all items' datetime.
#'
#' \item `items_bbox()`: returns a `list` with all items' bounding boxes.
#'
#' \item `item_assets()`: Returns a `character` value with all assets names
#' of the all items.
#'
#' \item `items_filter()`: a `STACItemCollection` object.
#'
#' \item `items_reap()`: a `vector` if the supplied field is atomic,
#' otherwise or a `list`.
#'
#' \item `items_fields()`: a `character` vector.
#'
#' \item `items_group()`: a `list` of `STACItemCollection` objects.
#'
#' \item `items_sign()`: a `STACItemCollection` object with signed assets url.
#'
#' \item `items_as_sf()`: a `sf` object.
#'
#' }
#'
#' @examples
#' \dontrun{
#'  x <- stac("https://brazildatacube.dpi.inpe.br/stac") %>%
#'      stac_search(collections = "CB4-16D-2") %>%
#'      stac_search(limit = 500) %>%
#'      get_request()
#'
#'  x %>% items_length()
#'  x %>% items_matched()
#'  x %>% items_datetime()
#'  x %>% items_bbox()
#'  x %>% items_fetch()
#' }
#'
#' \dontrun{
#' # Defining BDC token
#' Sys.setenv("BDC_ACCESS_KEY" = "token-123")
#'
#' # STACItem object
#' stac("https://brazildatacube.dpi.inpe.br/stac/") %>%
#'     stac_search(collections = "CB4-16D-2", limit = 100,
#'         datetime = "2017-08-01/2018-03-01",
#'         bbox = c(-48.206, -14.195, -45.067, -12.272)) %>%
#'     get_request() %>% items_sign(sign_fn = sign_bdc())
#'
#' }
#'
#' \dontrun{
#' # STACItemCollection object
#' stac("https://brazildatacube.dpi.inpe.br/stac/") %>%
#'     stac_search(collections = "CB4-16D-2", limit = 100,
#'         datetime = "2017-08-01/2018-03-01",
#'         bbox = c(-48.206, -14.195, -45.067, -12.272)) %>%
#'     get_request() %>%
#'     items_filter(properties$`eo:cloud_cover` < 10)
#'
#' # Example with AWS STAC
#' stac("https://earth-search.aws.element84.com/v0") %>%
#'   stac_search(collections = "sentinel-s2-l2a-cogs",
#'               bbox = c(-48.206, -14.195, -45.067, -12.272),
#'               datetime = "2018-06-01/2018-06-30",
#'               limit = 500) %>%
#'   post_request() %>%
#'   items_filter(filter_fn = function(x) {x$properties$`eo:cloud_cover` < 10})
#' }
#'
#' \dontrun{
#' # STACItemCollection object
#' stac_item <- stac("https://brazildatacube.dpi.inpe.br/stac/") %>%
#'  stac_search(collections = "CB4-16D-2", limit = 100,
#'         datetime = "2017-08-01/2018-03-01",
#'         bbox = c(-48.206, -14.195, -45.067, -12.272)) %>%
#'  get_request() %>% items_fetch(progress = FALSE)
#'
#' stac_item %>% items_reap(field = c("properties", "datetime"))
#' }
#'
#' @name items_functions
NULL

#' @rdname items_functions
#'
#' @export
items_length <- function(items) {
  UseMethod("items_length", items)
}

#' @rdname items_functions
#'
#' @export
items_length.STACItem <- function(items) {
  check_items(items)
  return(1)
}

#' @rdname items_functions
#'
#' @export
items_length.STACItemCollection <- function(items) {
  check_items(items)
  return(length(items$features))
}

#' @rdname items_functions
#'
#' @export
items_length.default <- items_length.STACItem

#' @rdname items_functions
#'
#' @export
items_matched  <- function(items, matched_field = NULL) {
  UseMethod("items_matched", items)
}

#' @rdname items_functions
#'
#' @export
items_matched.STACItem  <- function(items, matched_field = NULL) {
  check_items(items)
  return(1)
}

#' @rdname items_functions
#'
#' @export
items_matched.STACItemCollection <- function(items, matched_field = NULL) {
  check_items(items)
  matched <- NULL

  # try by the matched_field provided by user. This allow users specify a
  # non-standard field for matched items.
  if (is.character(matched_field) && matched_field %in% names(items)) {
      matched <- as.numeric(items[[matched_field]])
  }
  if (is.null(matched)) {
    if (stac_version(items) < "0.9.0")
      # STAC API < 0.9.0 extensions
      matched <- items$`search:metadata`$matched
    else
      # STAC API >= 0.9.0 extensions
      matched <- items$`context`$matched

    # try the last resort: OGC features core spec
    if (is.null(matched))
      matched <- items$numberMatched
  }
  return(matched)
}

#' @rdname items_functions
#'
#' @export
items_matched.default <- items_matched.STACItem

#' @rdname items_functions
#'
#' @export
items_fetch <- function(items, ...) {
  UseMethod("items_fetch", items)
}

#' @rdname items_functions
#'
#' @export
items_fetch.STACItemCollection <- function(items, ...,
                                           progress = TRUE,
                                           matched_field = NULL) {
  check_items(items)
  matched <- items_matched(items, matched_field)

  # verify if progress bar can be shown
  progress <- progress & (!is.null(matched) && (items_length(items) < matched))
  if (progress)
    pb <- utils::txtProgressBar(
      min = items_length(items),
      max = matched,
      style = 3
    )

  while (TRUE) {

    # check if features is complete
    if (!is.null(matched) && (items_length(items) == matched))
      break

    # protect against infinite loop
    if (!is.null(matched) && (items_length(items) > matched))
      .error(paste("Length of returned items (%s) is different",
                   "from matched items (%s)."), items_length(items), matched)

    content <- tryCatch({
      items_next(items, ...)
    },
    next_error = function(e) NULL
    )

    if (!is.null(content))
      items <- content
    else
      break

    # update progress bar
    if (progress)
      utils::setTxtProgressBar(pb, length(content))
  }

  # close progress bar
  if (progress) {
    utils::setTxtProgressBar(pb, matched)
    close(pb)
  }

  return(items)
}

#' @rdname items_functions
#'
#' @export
items_next <- function(items, ...) {
  UseMethod("items_next", items)
}

#' @rdname items_functions
#'
#' @export
items_next.STACItemCollection <- function(items, ...) {
  check_items(items)
  matched <- items_matched(items)

  q <- doc_query(items)
  if (is.null(q)) {
    .error("Cannot get next link URL", class = "next_error")
  }

  # get url of the next page
  next_url <- Filter(function(x) x$rel == "next", items$links)
  if (length(next_url) == 0)
    .error("Cannot get next link URL", class = "next_error")

  next_url <- next_url[[1]]

  # create a new stac object with params from the next url
  # check for body implementation in next link
  if (q$verb == "POST" && all(c("body", "method") %in% names(next_url))) {

    # TODO: check if spec can enforce that the same provided base url
    # must be used to proceed pagination.
    # For security concerns, here, the original base_url will be used in
    # subsequent requests of pagination

    # # update query base_url and verb to the returned one
    # q$base_url <- next_url$href

    # erase current parameters if merge == FALSE
    if (!is.null(next_url$merge) && !next_url$merge) {
      q$params <- list()
    }

    # get parameters
    params <- next_url$body

  } else {

    # TODO: check if spec can enforce that the same provided base url
    # must be used to proceed pagination.
    # For security concerns, here, the original base_url will be used in
    # subsequent requests of pagination

    # # update query base_url and verb to the returned one
    # q$base_url <- gsub("^([^?]+)(\\?.*)?$", "\\1", next_url$href)

    # get next link parameters from url
    params <- .querystring_decode(substring(
      gsub("^([^?]+)(\\?.*)?$", "\\2", next_url$href), 2)
    )

    # verify if query params is valid
    params <- .validate_query(params = params)
  }

  # parse params
  params <- parse_params(q, params = params)

  next_stac <- RSTACQuery(version = q$version,
                          base_url = q$base_url,
                          params = modify_list(q$params, params),
                          subclass = subclass(q))

  # call request
  if (q$verb == "GET") {

    content <- get_request(next_stac, ...)
  } else if (q$verb == "POST") {

    content <- post_request(next_stac, ..., encode = q$encode)
  } else {
    .error("Invalid HTTP method.")
  }

  # check content response
  check_subclass(content, "STACItemCollection")

  # check pagination length
  if (!is.null(q$params[["limit"]]) &&
      items_length(content) > as.numeric(q$params[["limit"]])) {
    .error("STAC invalid retrieved page length.")
  }

  # check if result length is valid
  if (!is.null(matched) && !is.null(q$params[["limit"]]) &&
      (items_length(content) != as.numeric(q$params[["limit"]])) &&
      (items_length(content) + items_length(items) != matched)) {
    .error("STAC pagination error.")
  }

  # merge features result into resulting content
  content$features <- c(items$features, content$features)

  # prepares next iteration
  items <- content

  return(items)
}

#' @rdname items_functions
#'
#' @export
items_datetime <- function(items) {
  UseMethod("items_datetime", items)
}

#' @rdname items_functions
#'
#' @export
items_datetime.STACItem <- function(items) {
  check_items(items)
  if (!"datetime" %in% names(items$properties)) {
    .error("Parameter `items` is invalid.")
  }
  return(items$properties$datetime)
}

#' @rdname items_functions
#'
#' @export
items_datetime.STACItemCollection <- function(items) {
  check_items(items)
  return(map_chr(items$features, items_datetime))
}

#' @rdname items_functions
#'
#' @export
items_datetime.default <- items_datetime.STACItem

#' @rdname items_functions
#'
#' @export
items_bbox <- function(items) {
  UseMethod("items_bbox", items)
}

#' @rdname items_functions
#'
#' @export
items_bbox.STACItem <- function(items) {
  check_items(items)
  return(items$bbox)
}

#' @rdname items_functions
#'
#' @export
items_bbox.STACItemCollection <- function(items) {
  check_items(items)
  return(items_reap(items, field = "bbox"))
}

#' @rdname items_functions
#'
#' @export
items_bbox.default <- items_bbox.STACItem

#' @rdname items_functions
#'
#' @export
items_assets <- function(items, simplify = deprecated()) {
  if (!missing(simplify)) {
    deprec_parameter(
      deprec_var = "simplify",
      deprec_version = "0.9.2",
      msg = "By default, the return will be simplified."
    )
  }
  UseMethod("items_assets", items)
}

#' @rdname items_functions
#'
#' @export
items_assets.STACItem <- function(items, simplify = deprecated()) {
  check_items(items)
  return(items_fields(items, field = "assets"))
}

#' @rdname items_functions
#'
#' @export
items_assets.STACItemCollection <- function(items, simplify = deprecated()) {
  check_items(items)
  return(sort(unique(unlist(lapply(items$features, items_assets.STACItem)))))
}

#' @rdname items_functions
#'
#' @export
items_assets.default <- items_assets.STACItem

#' @rdname items_functions
#'
#' @export
items_filter <- function(items, ..., filter_fn = NULL) {
  UseMethod("items_filter", items)
}

#' @rdname items_functions
#'
#' @export
items_filter.STACItemCollection <- function(items, ..., filter_fn = NULL) {
  check_items(items)
  exprs <- unquote(
    expr = as.list(substitute(list(...), env = environment())[-1]),
    env =  parent.frame()
  )

  if (length(exprs) > 0) {
    if (!is.null(names(exprs)))
      .error("Filter expressions cannot be named.")

    show_warning <- TRUE
    for (i in seq_along(exprs)) {
      if (show_warning && check_old_expression(items, exprs[[i]])) {
        # NOTE: this warning will be removed in next versions. We will no
        # longer support the old way of filter evaluation
        .warning(paste(
          "In version 0.9.2, rstac changed how filter expressions are",
          "evaluated. In future versions, the expression '%s' will be",
          "evaluated against each feature in items intead of `properties`",
          "field.\nSee ?items_filter for more details on how to change",
          "your expression."
        ), deparse(exprs[[i]]))
        show_warning <- FALSE
      }
      sel <- map_lgl(items$features, eval_filter_expr, expr = exprs[[i]])
    }
    items$features <- items$features[sel]
  }

  if (!is.null(filter_fn)) {
    if (check_old_fn(items, filter_fn)) {
      # NOTE: this warning will be removed in next versions. We will no
      # longer support the old way of filter evaluation
      .warning(paste(
        "In version 0.9.2, rstac changed how filter function is",
        "evaluated. In future versions, the `filter_fn` parameter will be",
        "evaluated against each feature in items instead of `properties`",
        "field.\nSee ?items_filter for more details on how to change your",
        "function."
      ))
    }
    sel <- map_lgl(items$features, eval_filter_fn, filter_fn = filter_fn)
    items$features <- items$features[sel]
  }
  return(items)
}

#' @rdname items_functions
#'
#' @export
items_compact <- function(items) {
  UseMethod("items_compact", items)
}

#' @rdname items_functions
#'
#' @export
items_compact.STACItemCollection <- function(items) {
  check_items(items)
  items_filter(items, filter_fn = has_assets)
}

#' @rdname items_functions
#'
#' @export
items_reap <- function(items, field, ..., pick_fn = identity) {
  UseMethod("items_reap", items)
}

#' @rdname items_functions
#'
#' @export
items_reap.STACItem <- function(items, field, ..., pick_fn = identity) {
  check_items(items)
  dots <- list(...)
  if (length(dots) > 0) {
    deprec_parameter(
      deprec_var = "...",
      deprec_version = "0.9.2",
      msg = "Please, use `field` parameter instead."
    )
    field = c(field, unlist(dots, use.names = FALSE))
  }
  values <- apply_deeply(items, i = field, fn = pick_fn)
  return(values)
}

#' @rdname items_functions
#'
#' @export
items_reap.STACItemCollection <- function(items,
                                          field, ...,
                                          pick_fn = identity) {
  check_items(items)
  if (items_length(items) == 0) return(NULL)
  dots <- list(...)
  if (length(dots) > 0) {
    deprec_parameter(
      deprec_var = "...",
      deprec_version = "0.9.2",
      msg = "Please, use `field` parameter instead."
    )
    field = c(field, unlist(dots, use.names = FALSE))
  }
  val <- lapply(items$features, items_reap.STACItem, field = field,
                pick_fn = pick_fn)
  if (is.null(names(val)) &&
      all(vapply(val, function(x) is.atomic(x) && length(x) == 1, logical(1))))
    return(unlist(val))
  return(val)
}

#' @rdname items_functions
#'
#' @export
items_reap.default <- items_reap.STACItem

#' @rdname items_functions
#'
#' @export
items_fields <- function(items, field = NULL, ...) {
  UseMethod("items_fields", items)
}

#' @rdname items_functions
#'
#' @export
items_fields.STACItem <- function(items, field = NULL, ...) {
  check_items(items)
  dots <- list(...)
  if (length(dots) > 0) {
    deprec_parameter(
      deprec_var = "...",
      deprec_version = "0.9.2",
      msg = "Please, use `field` parameter instead."
    )
    field = c(field, unlist(dots, use.names = FALSE))
  }
  if (length(field) == 0) {
    fields <- names(items)
  } else {
    fields <- unique(unlist(apply_deeply(
      items, i = field, fn = names
    ), use.names = FALSE))
  }
  return(sort(fields))
}

#' @rdname items_functions
#'
#' @export
items_fields.STACItemCollection <- function(items, field = NULL, ...) {
  check_items(items)
  dots <- list(...)
  if (length(dots) > 0) {
    deprec_parameter(
      deprec_var = "...",
      deprec_version = "0.9.2",
      msg = "Please, use `field` parameter instead."
    )
    field = c(field, unlist(dots, use.names = FALSE))
  }
  if (items_length(items) == 0)
    return(NULL)

  fields <- lapply(items$features, items_fields.STACItem, field = field)

  return(sort(unique(unlist(unname(fields)))))
}

#' @rdname items_functions
#'
#' @export
items_fields.default <- items_fields.STACItem

#' @rdname items_functions
#'
#' @export
items_sign <- function(items, sign_fn) {
  UseMethod("items_sign", items)
}

#' @rdname items_functions
#'
#' @export
items_sign.STACItem <- function(items, sign_fn) {
  check_items(items)
  return(sign_fn(items))
}

#' @rdname items_functions
#'
#' @export
items_sign.STACItemCollection <- function(items, sign_fn) {
  check_items(items)
  return(foreach_item(items, sign_fn))
}

#' @rdname items_functions
#'
#' @export
items_sign.default <- items_sign.STACItem

#' @rdname items_functions
#'
#' @export
items_as_sf <- function(items) {
  UseMethod("items_as_sf", items)
}

#' @rdname items_functions
#'
#' @export
items_as_sf.STACItem <- function(items) {
  check_items(items)
  geojsonsf::geojson_sf(to_json(items))
}

#' @rdname items_functions
#'
#' @export
items_as_sf.STACItemCollection <- function(items) {
  check_items(items)
  geojsonsf::geojson_sf(to_json(items))
}

Try the rstac package in your browser

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

rstac documentation built on Oct. 18, 2023, 1:15 a.m.