R/exact_extract_helpers.R

Defines functions splice .numCells .eagerLoad .blockSize .netCDFBlockSize .isInMemory .createProgress .getValuesBlock .res .extent .cellFromRowCol .rowFromY .yFromRow .colFromX .xFromCol .isRaster .numLayers .setValues .crs .stopReading .startReading .num_expected_args .singleColumnToVector .quickDf .validateUniqueNames .validateNumericScalarOrNA .validateNumericScalar .validateFlag .areaMethod .valueWeightIndexes .isWeighted .includeWeightInColName .makeColname .resultColumns .resultColNames

Documented in .resultColumns .valueWeightIndexes

# Copyright (c) 2018-2022 ISciences, LLC.
# All rights reserved.
#
# This software is licensed under the Apache License, Version 2.0 (the "License").
# You may not use this file except in compliance with the License. You may
# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

.resultColNames <- function(...) {
  .resultColumns(...)$colname
}

#' Return column names to be used for summary operations
#'
#' @param value_names names of value raster layers
#' @param weight_names names of weighting raster layers
#' @param fun functions or names of summary operations
#' @param full_colnames return a complete column name even when there is no
#'                      ambiguity?
#' @param quantiles quantiles to use when \code{stat_names} contains \code{quantile}
#' @return character vector of column names
#' @keywords internal
.resultColumns <- function(value_names, weight_names, fun, full_colnames, quantiles=numeric(), unique_values=numeric(), colname_fun = NULL) {
  if (inherits(fun, 'standardGeneric')) {
    stat_names <- fun@generic[1]
  } else if (is.function(fun)) {
    stat_names <- 'fun'
  } else if (is.null(fun)) {
    stat_names <- ''
  } else {
    stat_names <- fun
  }

  quantile_index = which(stat_names == 'quantile')
  if (length(quantile_index) != 0) {
    stat_names <- splice(stat_names, quantile_index, rep('quantile', length(quantiles)))
  }

  for (stat in c('frac', 'weighted_frac')) {
    frac_index = which(stat_names == stat)
    if (length(frac_index) != 0) {
      stat_names <- splice(stat_names, frac_index, rep(stat, length(unique_values)))
    }
  }

  ind <- .valueWeightIndexes(length(value_names), length(weight_names))
  vn <- value_names[ind$values]
  wn <- weight_names[ind$weights]

  # determine all combinations of index and stat
  z <- expand.grid(index=seq_along(vn),
                   stat_name=stat_names, stringsAsFactors=FALSE)

  z$values <- vn[z$index]
  z$base_value <- NA_real_

  for (stat in c('frac', 'weighted_frac')) {
    ifrac <- which(z$stat_name == stat)
    z$base_value[ifrac] <- rep(unique_values, each = length(ifrac) / length(unique_values))
  }

  iquantile <- which(z$stat_name == 'quantile')
  z$base_value[iquantile] <- rep(quantiles, each = length(iquantile) / length(quantiles))

  if (is.null(wn)) {
    z$weights <- NA
  } else {
    z$weights <- wn[z$index]
  }
  z$weights[!.includeWeightInColName(z$stat_name)] <- NA

  if (is.null(colname_fun)) {
    colname_fun <- function(...) {
      .makeColname(full_colnames = full_colnames, ...)
    }
  }

  z$colname <- mapply(colname_fun,
                      fun_name = z$stat_name,
                      values = z$values,
                      weights = z$weights,
                      fun_value = z$base_value,
                      MoreArgs = list(nvalues = length(value_names),
                                      nweights = length(weight_names)),
                      USE.NAMES = FALSE)

  return(z)
}

.makeColname <- function(fun_name, values, weights, fun_value, full_colnames, nvalues, nweights) {
  # construct column names for each index, stat
  # add weight layer name only if layer is ambiguously weighted
  if (fun_name == 'quantile') {
    fun_component <- sprintf('q%02d', as.integer(100 * fun_value))
  } else if (fun_name %in% c('frac', 'weighted_frac')) {
    fun_component <- sprintf('%s_%s', fun_name, fun_value)
  } else {
    fun_component <- fun_name
  }

  ret <- fun_component
  if (full_colnames || nvalues > 1) {
    ret <- paste(ret, values, sep='.')
  }
  if ((!is.na(weights)) && ((full_colnames & nweights > 0) || nweights > 1)) {
    ret <- paste(ret, weights, sep='.')
  }

  return(ret)
}

.includeWeightInColName <- function(fun) {
  .isWeighted(fun) | fun == 'fun'
}

.isWeighted <- function(stat_name) {
  stat_name %in% c('weighted_mean', 'weighted_sum', 'weighted_frac', 'weighted_stdev', 'weighted_variance')
}

#' Compute indexes for the value and weight layers that should be
#' processed together
#'
#' @param num_values number of layers in value raster
#' @param num_weights number of layers in weighting raster
#' @return list with \code{values} and \code{weights} elements
#'         providing layer indexes
#' @keywords internal
.valueWeightIndexes <- function(num_values, num_weights) {
  if (num_weights == 0) {
    vi <- seq_len(num_values)
    wi <- NA
  } else if (num_values == num_weights) {
    # process in parallel
    vi <- seq_len(num_values)
    wi <- seq_len(num_weights)
  } else if (num_values == 1 && num_weights > 1) {
    # recycle values
    vi <- rep.int(1, num_weights)
    wi <- seq_len(num_weights)
  } else if (num_values > 1 && num_weights == 1) {
    # recycle weights
    vi <- seq_len(num_values)
    wi <- rep.int(1, num_values)
  }

  list(values = vi, weights = wi)
}

.areaMethod <- function(crs_obj) {
  if (!(is.na(crs_obj)) && crs_obj$units_gdal == 'degree') {
    return('spherical')
  } else {
    return('cartesian')
  }
}

.validateFlag <- function(value, name) {
  if(!(is.logical(value) && length(value) == 1 && !is.na(value))) {
    stop(name, ' must be TRUE or FALSE')
  }
}

.validateNumericScalar <- function(value, name) {
  if (!(is.numeric(value) && length(value) == 1 && !is.na(value))) {
    stop(name, ' must be a single numeric value')
  }
}

.validateNumericScalarOrNA <- function(value, name) {
  if (!(is.numeric(value) && length(value) == 1)) {
    stop(name, ' must be a single numeric value')
  }
}

.validateUniqueNames <- function(x) {
  nm <- names(x)
  if (!is.null(nm)) {
    if (length(nm) != length(unique(nm))) {
      stop('names of input rasters must be unique')
    }
  }
}

# faster replacement for as.data.frame when input is a named list
# with equal-length columns
# from Advanced R, sec. 24.4.2
.quickDf <- function(lst) {
  class(lst) <- 'data.frame'
  attr(lst, 'row.names') <- .set_row_names(length(lst[[1]]))
  lst
}

.singleColumnToVector <- function(df) {
  if (ncol(df) == 1) {
    df[, 1]
  } else {
    df
  }
}

# Return the number of standard (non-...) arguments in a supplied function that
# do not have a default value. This is used to fail if the summary function
# provided by the user cannot accept arguments of values and weights.
.num_expected_args <- function(fun) {
  a <- formals(args(fun))
  a <- a[names(a) != '...']
  sum(sapply(a, nchar) == 0)
}

.startReading <- function(r) {
  if(inherits(r, 'BasicRaster')) {
    return(raster::readStart(r))
  } else if (inherits(r, 'SpatRaster')) {
    terra::readStart(r)
  }

  return(r)
}

.stopReading <- function(r) {
  if(inherits(r, 'BasicRaster')) {
    return(raster::readStop(r))
  } else if (inherits(r, 'SpatRaster')) {
    terra::readStop(r)
  }

  return(r)
}

.crs <- function(r) {
  if(inherits(r, 'BasicRaster')) {
    if (utils::packageVersion('raster') < numeric_version('3.5')) {
      return(raster::crs(r))
    } else {
      return(terra::crs(r))
    }
  } else if (inherits(r, 'SpatRaster')) {
    return(terra::crs(r))
  } else {
    stop('Unknown type: ', class(r))
  }
}

.setValues <- function(r, x) {
  if(inherits(r, 'BasicRaster')) {
    if (utils::packageVersion('raster') < numeric_version('3.5')) {
      raster::values(r) <- x
    } else {
      terra::values(r) <- x
    }
  } else if (inherits(r, 'SpatRaster')) {
    raster::values(r) <- x
  } else {
    stop('Unknown type: ', class(r))
  }

  return(r)
}

.numLayers <- function(r) {
  if(inherits(r, 'BasicRaster')) {
    return(raster::nlayers(r))
  } else if (inherits(r, 'SpatRaster')) {
    return(terra::nlyr(r))
  } else {
    stop('Unknown type: ', class(r))
  }
}

.isRaster <- function(r) {
  inherits(r, 'BasicRaster') | inherits(r, 'SpatRaster')
}

.xFromCol <- function(r, col) {
  if (inherits(r, 'BasicRaster')) {
    raster::xFromCol(r, col)
  } else if (inherits(r, 'SpatRaster')) {
    terra::xFromCol(r, col)
  } else {
    stop('Unknown type: ', class(r))
  }
}

.colFromX <- function(r, x) {
  if (inherits(r, 'BasicRaster')) {
    raster::colFromX(r, x)
  } else if (inherits(r, 'SpatRaster')) {
    terra::colFromX(r, x)
  } else {
    stop('Unknown type: ', class(r))
  }
}

.yFromRow <- function(r, row) {
  if (inherits(r, 'BasicRaster')) {
    raster::yFromRow(r, row)
  } else if (inherits(r, 'SpatRaster')) {
    terra::yFromRow(r, row)
  } else {
    stop('Unknown type: ', class(r))
  }
}

.rowFromY <- function(r, y) {
  if (inherits(r, 'BasicRaster')) {
    raster::rowFromY(r, y)
  } else if (inherits(r, 'SpatRaster')) {
    terra::rowFromY(r, y)
  } else {
    stop('Unknown type: ', class(r))
  }
}

.cellFromRowCol <- function(r, row, col) {
  if (inherits(r, 'BasicRaster')) {
    raster::cellFromRowCol(r, row, col)
  } else if (inherits(r, 'SpatRaster')) {
    terra::cellFromRowCol(r, row, col)
  } else {
    stop('Unknown type: ', class(r))
  }
}

.extent <- function(r) {
  if (inherits(r, 'BasicRaster')) {
    ex <- r@extent
    c(ex@xmin, ex@ymin, ex@xmax, ex@ymax)
  } else if (inherits(r, 'SpatRaster')) {
    ex <- terra::ext(r)
    c(ex$xmin, ex$ymin, ex$xmax, ex$ymax)
  } else {
    stop('Unknown type: ', class(r))
  }
}

.res <- function(r) {
  if (inherits(r, 'BasicRaster')) {
    raster::res(r)
  } else if (inherits(r, 'SpatRaster')) {
    terra::res(r)
  } else {
    stop('Unknown type: ', class(r))
  }
}

.getValuesBlock <- function(r, row, nrows, col, ncols) {
  if (inherits(r, 'BasicRaster')) {
    raster::getValuesBlock(r, row, nrows, col, ncols, format = 'm')
  } else if (inherits(r, 'SpatRaster')) {
    terra::readValues(r, row, nrows, col, ncols, mat = TRUE)
  } else {
    stop('Unknown type: ', class(r))
  }
}

.createProgress <- function(progress, n) {
  if (progress && n > 1) {
    pb <- utils::txtProgressBar(min = 0, max = n, initial=0, style=3)
    update_progress <- function() {
      i <- 1 + utils::getTxtProgressBar(pb)
      utils::setTxtProgressBar(pb, i)
      if (i == n) {
        close(pb)
      }
    }
  } else {
    update_progress <- function() {}
  }

  return(update_progress)
}

.isInMemory <- function(r) {
  if (inherits(r, 'BasicRaster')) {
    return(raster::inMemory(r))
  } else if (inherits(r, 'SpatRaster')) {
    return(terra::inMemory(r)[1])
  } else {
    stop('Unknown type: ', class(r))
  }
}

.netCDFBlockSize <- function(fname, varname) {
  nc <- NULL
  sz <- NA

  tryCatch({
    nc <- ncdf4::nc_open(fname)
    sz <- nc$var[[varname]]$chunksizes

    dim_index <- nc$var[[varname]]$dimids + 1L
    dim_names <- sapply(dim_index, function(i) nc$dim[[i]]$name)

    if (all(is.na(sz))) {
      # file is not compressed
      sz <- rep.int(1, length(dim_names))
    }

    names(sz) <- dim_names
  }, finally = {
    if (!is.null(nc)) {
      ncdf4::nc_close(nc)
    }
  })

  # flip dimensions 1 and 2 so we return row/col
  return(sz[c(2, 1, seq_along(sz)[-(1:2)])])
}

.blockSize <- function(r) {
  # set default return value in case file is uncompressed and has
  # has no block size, or we simply can't figure it out
  ret <- c(1, 1)

  if (inherits(r, 'BasicRaster')) {
    if (r[[1]]@file@driver == 'netcdf') {
      ret <- .netCDFBlockSize(r[[1]]@file@name, attr(r[[1]]@data, 'zvar'))
    } else if (r@file@driver == 'gdal') {
      ret <- c(r@file@blockrows, r@file@blockcols)
    }
  } else if (inherits(r, 'SpatRaster')) {
    ret <- terra::fileBlocksize(r)[1, ]
  }

  unname(ret)
}

.eagerLoad <- function(r, geoms, max_cells_in_memory, message_on_fail) {
  if (is.null(r)) {
    return(NULL)
  }

  cells_required <- .numCells(r, geoms)

  if (cells_required <= max_cells_in_memory) {
    box <- sf::st_bbox(geoms)
    geom_ext <- terra::ext(box[c('xmin', 'xmax', 'ymin', 'ymax')])

    if (!inherits(r, 'SpatRaster')) {
      # current CRAN version of terra (1.4-22) does not preserve
      # names on conversion (https://github.com/rspatial/terra/issues/430)
      nm <- names(r)
      r <- terra::rast(r)
      names(r) <- nm
    }

    overlap_ext <- terra::intersect(terra::ext(r), geom_ext)
    if (is.null(overlap_ext)) {
      # Extents do not overlap, and terra::crop will throw an error
      # if we try to crop. Return the input raster as-is; nothing will be
      # read from it anyway.
      return(r)
    }

    r <- terra::crop(r, geom_ext, snap = 'out')
  } else if (message_on_fail) {
    message('Cannot preload entire working area of ', cells_required,
            ' cells with max_cells_in_memory = ', max_cells_in_memory, '.',
            ' Raster values will be read for each feature individually.')
  }

  return(r)
}

.numCells <- function(r, g) {
  if (is.null(r)) {
    return(0)
  }

  box <- sf::st_bbox(g)

  top <- .rowFromY(r, box['ymax'])
  bottom <- .rowFromY(r, box['ymin'])
  left <- .colFromX(r, box['xmin'])
  right <- .colFromX(r, box['xmax'])

  if (is.na(top) && is.na(bottom)) {
    return(0L)
  }
  if (is.na(left) && is.na(right)) {
    return(0L)
  }
  if (is.na(top)) {
    top <- 1
  }
  if (is.na(bottom)) {
    bottom <- nrow(r)
  }
  if (is.na(left)) {
    left <- 1
  }
  if (is.na(right)) {
    right <- ncol(r)
  }

  return( (bottom - top + 1) * (right - left + 1) * .numLayers(r) )
}

splice <- function(x, i, replacement) {
  c(x[seq_along(x) < i],
    replacement,
    x[seq_along(x) > i])
}

Try the exactextractr package in your browser

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

exactextractr documentation built on Sept. 20, 2023, 9:08 a.m.