#' Replace a specific value(s) in a GRaster
#'
#' @description This function replaces one or more user-specified values in a raster with other values. See [classify()] for replacing ranges of values.
#'
#' @param x A `GRaster`.
#'
#' @param from,to Vectors of numeric or character values. The value(s) in `from` will be replaced with the value(s) in `to`. They must be the same length, or, if you supply a single value for `to`, then all values in `from` will be converted to the same value of `to`. Numeric/integer or character vectors can be used:
#' * `from` and `to` are numeric or integer vectors: Values in `from` will be replaced by their corresponding value in `to`.
#' * `from` and `to` are character vectors: You can use a character vector for `from` and `to`. In this case, the input raster must be a factor (categorical) raster or an integer raster. If `from` is a character vector, these levels(categories) will be replaced by the levels in `to.` This can add levels to a `GRaster` `to` has labels that do not match any existing labels in `from`.
#' * `from` is a character vector and `to` is an integer vector: Cells in `x` that correspond to the given label will have their values replaced by the corresponding value in `to`, and be matched the the corresponding label. If no label corresponds to the new value, a new level will be created. The input must be a categorical raster.
#' * `from` is an integer vector and `to` is a character vector: Cells in `x` that have a value in `from` will be replaced by values that match the labels in `to`. If the input raster does not have a value that corresponds to the given label in `y`, then a new value will be created.
# * You can include `NA` in either `from` or `to` to replace `NA` values or to assign `NA` values.
#'
#' @param others `NULL` (default), `NA`, numeric, or a character:
#' * `NULL` (default): Values that do not appear in `from` will be unchanged.
#' * `NA`: Values that do not appear in `from` will be set to `NA`.
#' * Character: Cells in `x` that do not appear in `to` will be assigned to this level. In this case, `x` must be a categorical (factor) raster.
#'
#' @param warn Logical: If `TRUE` (default), display a warning when new levels are created.
#'
#' @returns A `GRaster`.
#'
#' @seealso [terra::subst()], [classify()]
#'
#' @example man/examples/ex_subst.r
#'
#' @aliases subst
#' @rdname subst
#' @exportMethod subst
methods::setMethod(
f = "subst",
signature = c(x = "GRaster"),
function(x, from, to, others = NULL, warn = TRUE) {
if (length(from) != length(to) & length(to) != 1L) stop("Arguments `from` and `to` must have the same length.")
if (is.character(from) & !all(is.factor(x))) stop("You can only use a character vector for `from` if all\n layers in `x` are categorical rasters.")
if (is.character(to) & any(datatype(x, "GRASS") != "CELL")) stop("You can only use a character vector for `to` if all\n layers in `x` are categorical or integer.")
if (is.character(others) & !all(is.factor(x))) stop("You can only use a character for `others` if all\n layers in `x` are categorical.")
if (length(from) > 1L & length(to) == 1L) to <- rep(to, length(from))
.locationRestore(x)
.region(x)
levs <- cats(x)
nLayers <- nlyr(x)
srcs <- .makeSourceName("subst_r_mapcalc", "raster", n = nLayers)
for (i in seq_len(nLayers)) {
if (is.factor(x)[i]) {
thisLev <- levs[[i]]
ac <- activeCat(x, layer = i)
# `from` is a character vector
if (is.character(from)) {
thisFrom <- thisLev[[1L]][match(from, thisLev[[ac + 1L]])]
if (any(is.na(thisFrom) & !is.na(from))) {
stop("Argument `from` contains one or more values that do not appear in the GRaster's levels table.\n These will be added ")
}
# `from` is numeric/logical
} else {
thisFrom <- from
thisFrom <- as.integer(thisFrom)
}
if (any(is.character(to))) {
thisTo <- thisLev[[1L]][match(to, thisLev[[ac + 1L]])]
# create new levels
if (any(is.na(thisTo) & !is.na(to))) {
if (warn) warning("Not all labels in `to` are found in `x`. New levels will be created.")
topVal <- max(.maxVal(x)[i], thisLev[[ac]])
nNew <- sum(is.na(unique(thisTo)) & !is.na(unique(to)))
newVals <- 1L:nNew + topVal
thisTo[is.na(thisTo) & !is.na(to)] <- newVals
thisTo <- as.integer(thisTo)
# create table to `rbind` to levels table
newLev <- .addLevels(ac = ac, thisLev = thisLev, thisTo = thisTo, labels = to)
# add new levels to table
thisLev <- rbind(thisLev, newLev)
}
# `to` is integer/numeric
} else if (!is.character(to)) {
thisTo <- to
if (any(omnibus::countDecDigits(thisTo) > 0L)) stop("When using `subst()` on a categorical rasters, the\n values in `to` must be integers or character values.")
thisTo <- as.integer(thisTo)
# create table to `rbind` to levels table
newLev <- .addLevels(ac = ac, thisLev = thisLev, thisTo = thisTo, labels = NULL)
# add new levels to table
thisLev <- rbind(thisLev, newLev)
}
# # remove levels that were re-assigned
# removes <- thisFrom[!(thisFrom %in% thisTo)]
# removes <- removes[!is.na(removes)]
# if (length(removes) > 0L) levs[[i]] <- levs[[i]][!(levs[[i]][[1L]] %in% removes)]
# remove duplicates
levs[[i]] <- thisLev[!duplicated(thisLev[ , c(1L, ac + 1L), with = FALSE])]
} else {
# raster is not a factor raster
thisFrom <- from
thisTo <- to
}
ex <- paste0(srcs[i], " = ")
for (j in seq_along(thisFrom)) {
if (is.na(thisFrom[j]) & is.na(thisTo[j])) {
ex <- paste0(ex, "if(isnull(", sources(x)[i], "),null(),")
} else if (is.na(thisFrom[j]) & !is.na(thisTo[j])) {
ex <- paste0(ex, "if(isnull(", sources(x)[i], "),", thisTo[j], ",")
} else if (!is.na(thisFrom[j]) & is.na(thisTo[j])) {
ex <- paste0(ex, "if(", sources(x)[i], "==", thisFrom[j], ",null(),")
} else {
ex <- paste0(ex, "if(", sources(x)[i], "==", thisFrom[j], ",", thisTo[j], ",")
}
}
if (is.null(others)) {
ex <- paste0(ex, sources(x)[i])
} else if (is.na(others)) {
ex <- "null()"
} else if (is.character(others) & is.factor(x)[i]) {
# get new value for `others`
thisOthers <- levs[[i]][levs[[i]][[ac + 1]] == others, 1L]
thisOthers <- thisOthers[[1L]]
if (length(thisOthers) == 0L) {
# add new level
if (warn) warning("the value in `others` is not found among existing level labels. A new level will be created.")
newVal <- max(0L, .maxVal(x)[i], max(thisTo, na.rm = TRUE)) + 1L
newVal <- as.integer(newVal)
newLabel <- others
newLev <- .addLevels(ac = ac, thisLev = thisLev, thisTo = newVal, labels = others)
levs[[i]] <- rbind(levs[[i]], newLev)
ex <- paste0(ex, newVal)
} else {
ex <- paste0(ex, thisOthers)
}
} else if (is.character(others) & !is.factor(x)[i]) {
stop("Cannot add a level to a non-factor GRaster. To make a GRaster categorical, use `levels<-` or `categories`.")
} else {
ex <- paste0(ex, others)
}
ex <- paste0(ex, paste(rep(")", length(thisFrom)), collapse = ""))
rgrass::execGRASS("r.mapcalc", expression = ex, flags = c(.quiet(), "overwrite"))
}
.makeGRaster(srcs, names = names(x), levels = levs)
}
)
#' Creates a new data.table with same structure as the existing levels table
#'
#' @param ac Integer: Active category index (increment by 1 to get the actual column).
#' @param thisLev `data.table`: The existing levels table, to be used as a template.
#' @param thisTo Integer(s): The new values to add (integers).
#' @param `NULL` or character: The new label(s) to add (`NULL` if they already exist in `thisLev`, or character if not).
#'
#' @returns A `data.table`.
#'
#' @noRd
.addLevels <- function(ac, thisLev, thisTo, labels = NULL) {
# create table to `rbind` to levels table
for (j in 1L:ncol(thisLev)) {
if (j == 1L) {
newLev <- data.table::data.table(DUMMY__ = thisTo)
} else {
if (j == ac + 1L) {
if (!is.null(labels)) {
thisLabels <- labels
} else {
thisLabels <- thisLev[[j]][match(thisTo, thisLev[[1L]])]
}
add <- data.table::data.table(DUMMY__ = thisLabels)
} else {
add <- data.table::data.table(DUMMY__ = NA)
}
newLev <- cbind(newLev, add)
}
names(newLev)[j] <- names(thisLev)[j]
}
newLev <- newLev[!duplicated(newLev)]
newLev <- newLev[!is.na(newLev[[1L]])]
newLev
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.