Nothing
#' CF data variable
#'
#' @description This class represents a CF data variable,
#' the object that provides access to an array of data.
#'
#' The CF data variable instance provides access to all the details that have
#' been associated with the data variable, such as axis information, grid
#' mapping parameters, etc.
#'
#' @docType class
CFVariable <- R6::R6Class("CFVariable",
inherit = CFObject,
private = list(
# A list with the axes of the variable.
.axes = NULL,
# Auxiliary coordinates reference, if the data variable uses them. Only
# applicable to CFVariable but it is here to allow referencing it in
# check_names()
.llgrid = NULL,
# The CFGridMapping object for the variable, if set.
.crs = NULL,
# A list of cell measure objects, if the variable has cell measures.
.cell_measures = NULL,
# Return the R order of dimensional axes that "receive special treatment".
# Scalar axes are not considered here.
YXZT = function() {
orient <- sapply(private$.axes, function(x) if (x$length > 1L) x$orientation)
match(c("Y", "X", "Z", "T"), orient, nomatch = 0L)
},
# Return the CF canonical order of dimensional axes that "receive special
# treatment". Scalar axes are not considered here.
XYZT = function() {
orient <- sapply(private$.axes, function(x) if (x$length > 1L) x$orientation)
match(c("X", "Y", "Z", "T"), orient, nomatch = 0L)
},
# Return the number of "dimensional" axes, i.e. axes that are associated
# with a dimension of the data of the variable. This may include axes with
# length 1 if the netCDF resource lists them as a dimension.
num_dim_axes = function() {
if (!is.null(private$.values))
length(dim(private$.values))
else if (self$has_resource)
self$ndims
else 0L
},
# Return the names of the dimensional axes.
dim_names = function() {
num <- private$num_dim_axes()
if (num > 0L)
sapply(1L:num, function(i) private$.axes[[i]]$name)
else character(0)
},
# Return the lengths of the dimensional axes.
dim = function() {
sapply(1L:private$num_dim_axes(), function(i) private$.axes[[i]]$length)
},
# Check that names passed as arguments to $subset() and $profile() are valid.
# This means that they must refer to an axis by name or orientation and
# there can be no duplication. It returns an integer vector with the order
# in which the axes are specified.
check_names = function(nm) {
axis_names <- names(private$.axes)
is_axis <- match(nm, axis_names)
if (anyDuplicated(is_axis, incomparables = NA))
stop("Duplicated axis names not allowed", call. = FALSE)
if (!any(is.na(is_axis)))
return(is_axis)
orientations <- sapply(private$.axes, function(a) a$orientation)
is_orient <- match(nm, orientations)
if (anyDuplicated(is_orient, incomparables = NA))
stop("Duplicated axis orientations not allowed", call. = FALSE)
if (!any(is.na(is_orient)))
return(is_orient)
ax_na <- which(is.na(is_axis))
is_axis[ax_na] <- is_orient[ax_na]
if (!any(is.na(is_axis)))
return(is_axis)
if (!is.null(private$.llgrid)) {
is_aux <- match(nm, private$.llgrid$grid_names)
if (anyDuplicated(is_aux, incomparables = NA))
stop("Duplicated auxiliary axis names not allowed", call. = FALSE)
if (!any(is.na(is_aux)))
return(is_aux)
ax_na <- which(is.na(is_axis))
is_axis[ax_na] <- is_aux[ax_na]
}
if (anyDuplicated(is_axis, incomparables = NA))
stop("Duplicated axis names and orientations not allowed", call. = FALSE)
if (any(is.na(is_axis)))
stop("Arguments contain names not corresponding to an axis", call. = FALSE)
is_axis
},
# Drop unwanted values in the "coordinates" attribute
drop_coordinates_attribute = function(atts, names) {
crd <- atts[atts$name == "coordinates", ]$value
if (!length(crd)) return(atts)
crds <- strsplit(crd[[1L]], " ")[[1L]]
keep <- !(crds %in% names)
if (!any(keep))
return(atts[!(atts$name == "coordinates"), ])
crds <- paste(crds[keep], collapse = " ")
atts[atts$name == "coordinates", ]$length <- nchar(crds)
atts[atts$name == "coordinates", ]$value <- crds
atts
},
# Orient private$.values in such a way that it conforms to regular R arrays:
# axis order will be Y-X-Z-T-others and Y values will go from the top to the
# bottom. Alternatively, order private$.values in the CF canonical order.
# Argument data is the array to order, argument ordering must be "R" or
# "CF". Returns a new array with an attribute "axes" that lists dimensional
# axis names in the order of the new array.
orient = function(data, ordering = "R") {
dim_names <- names(private$.axes)[seq_along(dim(data))]
if (ordering == "R")
order <- private$YXZT()
else if (ordering == "CF")
order <- private$XYZT()
else
stop("Invalid argument for ordering.", call. = FALSE)
if (sum(order) == 0L) {
warning("Cannot orient data array because axis orientation has not been set")
attr(data, "axes") <- dim_names
return(data)
}
if (all(diff(order[which(order > 0L)]) > 0L)) {
attr(data, "axes") <- dim_names
} else {
all_dims <- seq(length(dim(data)))
perm <- c(order[which(order > 0L)], all_dims[!(all_dims %in% order)])
data <- aperm(data, perm)
attr(data, "axes") <- dim_names[perm]
}
if (ordering == "R") {
# Flip Y-axis, if necessary
ynames <- dimnames(data)[[1L]]
if (length(ynames) > 1L && as.numeric(ynames[2L]) > as.numeric(ynames[1L])) {
dn <- dimnames(data)
dims <- dim(data)
dim(data) <- c(dims[1L], prod(dims[-1L]))
data <- apply(data, 2L, rev)
dim(data) <- dims
dn[[1L]] <- rev(dn[[1L]])
dimnames(data) <- dn
}
}
data
},
# Do the auxiliary grid interpolation. Argument "subset" is passed from the
# `subset()` method. Argument "ll_names" give the auxiliary longitude and
# latitude names. Return a list of useful objects to `subset()`.
auxiliary_interpolation = function(subset, ll_names) {
# This code assumes that the grid orientation of the data variable is the
# same as that of the longitude-latitude grid
ext <- private$.llgrid$extent
sel_names <- names(subset)
Xrng <- if (ll_names[1L] %in% sel_names && !is.na(subset[[ ll_names[1L] ]][1L]))
range(subset[[ ll_names[1L] ]])
else ext[1L:2L]
Yrng <- if (ll_names[2L] %in% sel_names && !is.na(subset[[ ll_names[2L] ]][1L]))
range(subset[[ ll_names[2L] ]])
else ext[3L:4L]
private$.llgrid$aoi <- aoi(Xrng[1L], Xrng[2L], Yrng[1L], Yrng[2L])
index <- private$.llgrid$grid_index()
dim_index <- dim(index)
# The below appears counter-intuitive (XY relationship to indices) but it
# works for long-lat grids that use the recommended X-Y-Z-T axis ordering.
# Report any problems to https://github.com/R-CF/ncdfCF/issues
dim_ll <- private$.llgrid$dim
xyidx <- arrayInd(index, dim_ll) # convert indices to row,column
rx <- range(xyidx[, 2L], na.rm = TRUE) # full range of columns
xyidx[, 2L] <- xyidx[, 2L] - rx[1L] + 1L # reduce columns to 1-based
cols <- rx[2L] - rx[1L] + 1L # no of columns in reduced grid
ry <- range(xyidx[, 1L], na.rm = TRUE) # full range of rows
xyidx[, 1L] <- xyidx[, 1L] - ry[1L] + 1L # reduce rows to 1-based
rows <- ry[2L] - ry[1L] + 1L # no of rows in reduced grid
index <- rows * (xyidx[, 2L] - 1L) + xyidx[, 1L] # reduced index value
# index = the index values into the reduced grid
# X,Y = start and count values for data to read
# aoi = the AOI that was used to index
# box = the dim of the original index
list(index = index, X = c(ry[1L], rows), Y = c(rx[1L], cols), aoi = private$.llgrid$aoi, box = dim_index)
},
# Internal apply/tapply method for this class. If the size of the data
# variable is below a certain threshold, read the data and process in one
# go. Otherwise processing goes per factor level. In other words, for each
# factor level the data is read from file, to which the function is applied.
# This is usually applied over the temporal domain but could be others
# as well (untested).
process_data = function(tdim, fac, fun, ...) {
if (!is.null(private$.values))
return(.process.data(private$.values, tdim, fac, fun, ...))
else if (prod(sapply(private$.axes, function(x) x$length)) < CF.options$memory_cell_limit)
# Read the whole data array because size is manageable
return(.process.data(private$read_data(), tdim, fac, fun, ...))
# If data variable is too large, go by individual factor levels
num_dims <- private$num_dim_axes()
start <- rep(1L, num_dims)
count <- rep(NA_integer_, num_dims)
lvls <- nlevels(fac)
d <- vector("list", lvls)
ndx <- as.integer(fac)
nm <- self$name
for (l in 1L:lvls) {
indices <- which(ndx == l)
dff <- diff(indices)
if (all(dff == 1L)) { # Data is contiguous per factor level
rng <- range(indices)
start[tdim] <- rng[1L]
count[tdim] <- rng[2L] - rng[1L] + 1L
values <- private$read_chunk(start, count)
} else { # Era factors have disparate indices
cutoffs <- c(0L, which(c(dff, 2L) > 1L))
values <- lapply(2L:length(cutoffs), function(i) {
start[tdim] <- indices[cutoffs[i - 1L] + 1L]
count[tdim] <- cutoffs[i] - cutoffs[i - 1L]
private$read_chunk(start, count)
})
values <- abind::abind(values, along = num_dims)
}
d[[l]] <- .process.data(values, tdim, FUN = fun, ...)
# d is a list with lvls elements, each element a list with elements for
# the number of function results, possibly 1; each element having an
# array of dimensions from private$values that are not tdim.
}
res_dim <- dim(d[[1L]][[1L]])
tdim_len <- length(d)
if (tdim_len > 1L) {
dims <- c(res_dim, tdim_len)
perm <- c(num_dims, 1L:(num_dims - 1L))
} else
dims <- res_dim
fun_len <- length(d[[1L]])
out <- if (fun_len > 1L) {
# Multiple function result values so get all arrays for every result
# value and unlist those
lapply(1:fun_len, function(r) {
x <- unlist(lapply(d, function(lvl) lvl[[r]]), recursive = FALSE, use.names = FALSE)
dim(x) <- dims
if (tdim_len > 1L)
aperm(x, perm)
else x
})
} else {
# Single function result so unlist
d <- unlist(d, recursive = TRUE, use.names = FALSE)
dim(d) <- dims
list(if (tdim_len > 1L) aperm(d, perm) else d)
}
}
),
public = list(
#' @description Create an instance of this class.
#' @param var The [NCVariable] instance upon which this CF variable is based
#' when read from a netCDF resource, or the name for the new CF variable
#' to be created.
#' @param axes List of instances of [CFAxis] to use with this variable.
#' @param values Optional. The values of the variable in an array.
#' @param start Optional. Vector of indices where to start reading data
#' along the dimensions of the array on file. The vector must be `NA` to
#' read all data, otherwise it must have agree with the dimensions of the
#' array on file. Ignored when argument `var` is not an `NCVariable`
#' instance.
#' @param count Optional. Vector of number of elements to read along each
#' dimension of the array on file. The vector must be `NA` to read to the
#' end of each dimension, otherwise its value must agree with the
#' corresponding `start` value and the dimension of the array on file.
#' Ignored when argument `var` is not an `NCVariable` instance.
#' @param attributes Optional. A `data.frame` with the attributes of the
#' object. When argument `var` is an `NCVariable` instance and this
#' argument is an empty `data.frame` (default), arguments will be read
#' from the resource.
#' @return A `CFVariable` instance.
initialize = function(var, axes, values = values, start = NA, count = NA, attributes = data.frame()) {
super$initialize(var, values = values, start = start, count = count, attributes = attributes)
private$.axes <- axes
},
#' @description Print a summary of the data variable to the console.
#' @param ... Arguments passed on to other functions. Of particular interest
#' is `width = ` to indicate a maximum width of attribute columns.
print = function(...) {
cat("<Variable>", self$name, "\n")
fullname <- self$fullname
if (fullname != self$name)
cat("Path name:", fullname, "\n")
longname <- self$attribute("long_name")
if (!is.na(longname) && longname != self$name)
cat("Long name:", longname, "\n")
if (!is.null(private$.values)) {
rng <- self$attribute("actual_range")
if (is.na(rng[1L])) {
cat("\nValues: -\n")
cat(sprintf(" NA: %d (100%%)\n", length(private$.values)))
} else {
units <- self$attribute("units")
if (is.na(units)) units <- ""
cat("\nValues: [", rng[1L], " ... ", rng[2L], "] ", units, "\n", sep = "")
NAs <- sum(is.na(private$.values))
cat(sprintf(" NA: %d (%.1f%%)\n", NAs, NAs * 100 / length(private$.values)))
}
} else
cat("\nValues: (not loaded)\n")
if (!is.null(private$.crs)) {
cat("\nCoordinate reference system:\n")
print(.slim.data.frame(private$.crs$brief(), ...), right = FALSE, row.names = FALSE)
}
cat("\nAxes:\n")
axes <- do.call(rbind, lapply(private$.axes, function(a) a$brief()))
axes <- lapply(axes, function(c) if (all(c == "")) NULL else c)
if (length(axes)) {
axes <- as.data.frame(axes[lengths(axes) > 0L])
print(.slim.data.frame(axes, ...), right = FALSE, row.names = FALSE)
} else cat(" (none)\n")
len <- length(private$.cell_measures)
if (len) {
cat("\nCell measure", if (len > 1L) "s", ": ",
paste(sapply(private$.cell_measures, function(cm) paste0(cm$name, " (", cm$measure, ")")), collapse = "; "),
"\n", sep = "")
}
if (!is.null(private$.llgrid)) {
cat("\nAuxiliary longitude-latitude grid:\n")
ll <- private$.llgrid$brief()
print(.slim.data.frame(ll, ...), right = FALSE, row.names = FALSE)
}
self$print_attributes(...)
},
#' @description Some details of the data variable.
#'
#' @return A 1-row `data.frame` with some details of the data variable.
brief = function() {
unit <- self$attribute("units")
if (is.na(unit)) unit <- ""
longname <- self$attribute("long_name")
if (is.na(longname) || longname == self$name) longname <- ""
data.frame(name = self$fullname, long_name = longname, units = unit,
data_type = private$.data_type, axes = paste(names(private$.axes), collapse = ", "))
},
#' @description The information returned by this method is very concise
#' and most useful when combined with similar information from other
#' variables.
#'
#' @return Character string with very basic variable information.
shard = function() {
self$NCvar$shard()
},
#' @description Retrieve interesting details of the data variable.
#' @return A 1-row `data.frame` with details of the data variable.
peek = function() {
out <- data.frame(id = self$id)
out$name <- self$name
out$long_name <- self$attribute("long_name")
out$standard_name <- self$attribute("standard_name")
out$units <- self$attribute("units")
out$axes <- paste(sapply(private$.axes, function(a) a$name), collapse = ", ")
out
},
#' @description Detach the various properties of this variable from an
#' underlying netCDF resource.
#' @return Self, invisibly.
detach = function() {
lapply(private$.cell_measures, function(cm) cm$detach())
if (!is.null(private$.crs)) crs$detach()
if (!is.null(private$.llgrid)) private$.llgrid$detach()
lapply(private$.axes, function(ax) ax$detach())
super$detach()
invisible(self)
},
#' @description Return the time object from the axis representing time.
#' @param want Character string with value "axis" or "time", indicating
#' what is to be returned.
#' @return If `want = "axis"` the [CFAxisTime] axis; if `want = "time"` the
#' `CFTime` instance of the axis, or `NULL` if the variable does not have a
#' "time" axis.
time = function(want = "time") {
ndx <- sapply(private$.axes, inherits, "CFAxisTime")
if (any(ndx)) {
if (want == "axis") private$.axes[[which(ndx)]]
else private$.axes[[which(ndx)]]$time
} else NULL
},
#' @description Retrieve the data in the object exactly as it was read from
#' a netCDF resource or produced by an operation.
#' @return An `array`, `matrix` or `vector` with (dim)names set.
raw = function() {
data <- private$read_data()
if (is.null(data)) return(NULL)
if (is.null(dim(data)))
names(data) <- self$dimnames
else
dimnames(data) <- self$dimnames
data
},
#' @description Retrieve the data in the object in the form of an R array,
#' with axis ordering Y-X-others and Y values going from the top down.
#' @return An `array` or `matrix` of data in R ordering, or a `vector` if
#' the data has only a single dimension.
array = function() {
data <- self$raw()
if (is.null(data)) return(NULL)
if (is.null(dim(data)))
data
else
private$orient(data, "R")
},
#' @description This method extracts a subset of values from the array of
#' the variable, with the range along each axis to extract expressed in
#' coordinate values of the domain of each axis.
#'
#' @details The range of values along each axis to be subset is expressed in
#' coordinates of the domain of the axis. Any axes for which no selection
#' is made in the `...` argument are extracted in whole. Coordinates can
#' be specified in a variety of ways that are specific to the nature of
#' the axis. For numeric axes it should (resolve to) be a vector of real
#' values. A range (e.g. `100:200`), a vector (`c(23, 46, 3, 45, 17`), a
#' sequence (`seq(from = 78, to = 100, by = 2`), all work. Note, however,
#' that only a single range is generated from the vector so these examples
#' resolve to `(100, 200)`, `(3, 46)`, and `(78, 100)`, respectively. For
#' time axes a vector of character timestamps, `POSIXct` or `Date` values
#' must be specified. As with numeric values, only the two extreme values
#' in the vector will be used.
#'
#' If the range of coordinate values for an axis in argument `...` extends
#' the valid range of the axis, the extracted data will start at the
#' beginning for smaller values and extend to the end for larger values.
#' If the values envelope the valid range the entire axis will be
#' extracted in the result. If the range of coordinate values for any axis
#' are all either smaller or larger than the valid range of the axis then
#' nothing is extracted and `NULL` is returned.
#'
#' The extracted data has the same dimensional structure as the data in
#' the variable, with degenerate dimensions dropped. The order of the axes
#' in argument `...` does not reorder the axes in the result; use the
#' `array()` method for this.
#'
#' As an example, to extract values of a variable for Australia for the
#' year 2020, where the first axis in `x` is the longitude, the second
#' axis is the latitude, both in degrees, and the third (and final) axis
#' is time, the values are extracted by `x$subset(X = c(112, 154), Y =
#' c(-9, -44), T = c("2020-01-01", "2021-01-01"))`. Note that this works
#' equally well for projected coordinate reference systems - the key is
#' that the specification in argument `...` uses the same domain of values
#' as the respective axes in `x` use.
#'
#' ## Auxiliary coordinate variables
#'
#' A special case exists for variables where the horizontal dimensions (X
#' and Y) are not in longitude and latitude coordinates but in some other
#' coordinate system. In this case the netCDF resource may have so-called
#' *auxiliary coordinate variables* for longitude and latitude that are
#' two grids with the same dimension as the horizontal axes of the data
#' variable where each pixel gives the corresponding value for the
#' longitude and latitude. If the variable has such *auxiliary coordinate
#' variables* then you can specify their names (instead of specifying the
#' names of the primary planar axes). The resolution of the grid that is
#' produced by this method is automatically calculated. If you want to
#' subset those axes then specify values in decimal degrees; if you want
#' to extract the full extent, specify `NA` for both axes.
#' @param ... One or more arguments of the form `axis = range`. The "axis"
#' part should be the name of an axis or its orientation `X`, `Y`, `Z` or
#' `T`. The "range" part is a vector of values representing coordinates
#' along the axis where to extract data. Axis designators and names are
#' case-sensitive and can be specified in any order. If values for the
#' range per axis fall outside of the extent of the axis, the range is
#' clipped to the extent of the axis.
#' @param rightmost.closed Single logical value to indicate if the upper
#' boundary of range in each axis should be included. You must use the
#' argument name when specifying this, like `rightmost.closed = TRUE`, to
#' avoid the argument being treated as an axis name.
#' @return A `CFVariable` instance, having the axes and attributes of the
#' variable, or `NULL` if one or more of the selectors in the `...`
#' argument fall entirely outside of the range of the axis. Note that
#' degenerate dimensions (having `length(.) == 1`) are dropped from the
#' data array but the corresponding axis is maintained in the result as a
#' scalar axis.
#'
#' If `self` is linked to a netCDF resource then the result will be linked
#' to the same netCDF resource as well, except when *auxiliary coordinate
#' variables* have been selected for the planar axes.
subset = function(..., rightmost.closed = FALSE) {
num_axes <- private$num_dim_axes()
if (!num_axes)
stop("Cannot subset a scalar variable", call. = FALSE)
axis_names <- names(private$.axes)
# Organize the selectors
selectors <- list(...)
if (is.list(selectors[[1L]]))
selectors <- selectors[[1L]]
sel_names <- names(selectors)
axis_order <- private$check_names(sel_names)
# Auxiliary coordinates
aux <- NULL
if (inherits(private$.llgrid, "CFAuxiliaryLongLat")) {
aux_names <- private$.llgrid$grid_names
if (any(aux_names %in% sel_names)) {
aux <- private$auxiliary_interpolation(selectors, aux_names)
sel_names <- sel_names[-which(sel_names %in% aux_names)]
ll_dimids <- private$.llgrid$dimids
aoi_bounds <- aux$aoi$bounds()
}
}
# Start and count are "local", indices relative to the dimensions of self,
# which may be a subset of the data variable on file.
start <- rep(1L, num_axes)
count <- self$dim()
ZT_dim <- vector("integer")
out_axes_dim <- list(); ax_nm_dim <- list()
out_axes_other <- list(); ax_nm_other <- list()
for (ax in 1:num_axes) {
axis <- private$.axes[[ax]]
orient <- axis$orientation
ax_dimid <- axis$dimid
# In every section, set start and count values and create a corresponding axis
if (!is.null(aux) && ax_dimid == ll_dimids[1L]) {
# start and count relative to the data variable on file
start[ax] <- aux$X[1L]
count[ax] <- aux$X[2L]
out_axis <- CFAxisLongitude$new(aux_names[1L], values = aux$aoi$dimnames[[2L]], attributes = axis$attributes)
out_axis$bounds <- aoi_bounds$lon
} else if (!is.null(aux) && ax_dimid == ll_dimids[2L]) {
# start and count relative to the data variable on file
start[ax] <- aux$Y[1L]
count[ax] <- aux$Y[2L]
out_axis <- CFAxisLatitude$new(aux_names[2L], values = aux$aoi$dimnames[[1L]], attributes = axis$attributes)
out_axis$bounds <- aoi_bounds$lat
} else { # No auxiliary long-lat coordinates
rng <- selectors[[ axis_names[ax] ]]
if (is.null(rng)) rng <- selectors[[ orient ]]
if (is.null(rng)) { # Axis not specified so take the whole axis
ZT_dim <- c(ZT_dim, axis$length)
out_axis <- axis$copy()
} else { # Subset the axis
idx <- axis$slice(rng)
if (is.null(idx)) return(NULL)
start[ax] <- idx[1L]
count[ax] <- idx[2L] - idx[1L] + 1L
ZT_dim <- c(ZT_dim, count[ax])
out_axis <- axis$subset(rng = idx)
}
}
# Collect axes for result
if (out_axis$length == 1L) {
out_axes_other <- append(out_axes_other, out_axis)
ax_nm_other <- append(ax_nm_other, axis$name)
} else {
out_axes_dim <- append(out_axes_dim, out_axis)
ax_nm_dim <- append(ax_nm_dim, axis$name)
}
}
# Get the data for the result CFVariable
d <- NULL
if (is.null(aux)) {
# Regular axes selected
if (!is.null(private$.values))
d <- private$read_chunk(start, count)
} else {
# Auxiliary grids selected, index the data
d <- private$read_chunk(start, count)
dim(d) <- dim_in <- c(aux$X[2L] * aux$Y[2L], prod(ZT_dim))
d <- d[aux$index, ]
dim(d) <- dim_out <- c(aux$box, ZT_dim)
}
d <- drop(d)
# Put the dimensional axes in one list, with original names
axes <- c(out_axes_dim, out_axes_other)
original_axis_names <- unlist(c(ax_nm_dim, ax_nm_other))
names(axes) <- original_axis_names
# If there is a parametric vertical axis, subset its terms
lapply(axes, function(ax) {
if (ax$is_parametric)
ax$subset_parametric_terms(original_axis_names, axes, start, count, aux, ZT_dim)
})
# Sanitize the attributes for the result, as required, and get a CRS
if (is.null(aux)) {
atts <- self$attributes
} else {
atts <- private$drop_coordinates_attribute(self$attributes, private$.llgrid$grid_names)
atts <- atts[!(atts$name == "grid_mapping"), ] # drop attribute: warped to lat-long
}
# Assemble the new instance
axes <- c(axes, private$.axes[-(1L:num_axes)]) # Add the scalar axes to the list
names(axes) <- sapply(axes, function(a) a$name) # New axis names
if (is.null(aux)) {
v <- CFVariable$new(private$.NCvar, values = d, axes = axes,
start = start + private$.start_count$start - 1L, count = count,
attributes = atts)
v$crs <- private$.crs
} else {
v <- CFVariable$new(self$name, values = d, axes = axes, attributes = atts)
v$crs <- NULL
v$detach()
}
v
},
#' @description Summarise the temporal domain of the data, if present, to a
#' lower resolution, using a user-supplied aggregation function.
#'
#' Attributes are copied from the input data variable or data array. Note
#' that after a summarisation the attributes may no longer be accurate.
#' This method tries to sanitise attributes but the onus is on the calling
#' code (or yourself as interactive coder). Attributes like
#' `standard_name` and `cell_methods` likely require an update in the
#' output of this method, but the appropriate new values are not known to
#' this method. Use `CFVariable$set_attribute()` on the result of this
#' method to set or update attributes as appropriate.
#' @param name Character vector with a name for each of the results that
#' `fun` returns. So if `fun` has 2 return values, this should be a vector
#' of length 2. Any missing values are assigned a default name of
#' "result_#" (with '#' being replaced with an ordinal number).
#' @param fun A function or a symbol or character string naming a function
#' that will be applied to each grouping of data. The function must return
#' an atomic value (such as `sum()` or `mean()`), or a vector of atomic
#' values (such as `range()`). Lists and other objects are not allowed and
#' will throw an error that may be cryptic as there is no way that this
#' method can assert that `fun` behaves properly so an error will pop up
#' somewhere, most probably in unexpected ways. The function may also be
#' user-defined so you could write a wrapper around a function like `lm()`
#' to return values like the intercept or any coefficients from the object
#' returned by calling that function.
#' @param period The period to summarise to. Must be one of either "day",
#' "dekad", "month", "quarter", "season", "year". A "quarter" is the
#' standard calendar quarter such as January-March, April-June, etc. A
#' "season" is a meteorological season, such as December-February,
#' March-May, etc. (any December data is from the year preceding the
#' January data). The period must be of lower resolution than the
#' resolution of the time axis.
#' @param era Optional, integer vector of years to summarise over by the
#' specified `period`. The extreme values of the years will be used. This
#' can also be a list of multiple such vectors. The elements in the list,
#' if used, should have names as these will be used to label the results.
#' @param ... Additional parameters passed on to `fun`.
#' @return A `CFVariable` object, or a list thereof with as many
#' `CFVariable` objects as `fun` returns values.
summarise = function(name, fun, period, era = NULL, ...) {
if (missing(name) || missing(period) || missing(fun))
stop("Arguments 'name', 'period' and 'fun' are required.", call. = FALSE) # nocov
if (!(period %in% c("day", "dekad", "month", "quarter", "season", "year")))
stop("Argument 'period' has invalid value.", call. = FALSE) # nocov
if (!all(.is_valid_name(name)))
stop("Not all names are valid.", call. = FALSE) # nocov
# Find the time object, create the factor
tax <- self$time("axis")
if (is.null(tax))
stop("No 'time' axis found to summarise on.", call. = FALSE) # nocov
f <- try(tax$time$factor(period, era), silent = TRUE)
if (inherits(f, "try-error"))
stop("The 'time' axis is too short to summarise on.", call. = FALSE) # nocov
if (is.factor(f)) f <- list(f)
tm <- sum(private$YXZT() > 0L) # Test which oriented axes are present, T is the last one
# Helper function for the below lapply
.make_var <- function(nm, vals, axes, atts) {
v <- CFVariable$new(nm, values = vals, axes = axes, attributes = atts)
v$crs <- self$crs
v$detach()
v
}
res <- lapply(f, function(fac) {
# Make a new time axis for the result
new_tm <- attr(fac, "CFTime")
len <- length(new_tm)
new_ax <- CFAxisTime$new(tax$name, values = new_tm, attributes = tax$attributes)
other_axes <- private$.axes[-tm]
if (len == 1L) {
axes <- c(other_axes, new_ax)
names(axes) <- c(names(other_axes), tax$name)
} else {
axes <- c(new_ax, other_axes)
names(axes) <- c(tax$name, names(other_axes))
}
if (inherits(new_tm, "CFClimatology")) {
new_ax$delete_attribute("bounds")
new_ax$set_attribute("climatology", "NC_CHAR", "climatology_bnds")
}
# Summarise
dt <- private$process_data(tm, fac, fun, ...)
# Create the output
atts <- self$attributes
aux_nm <- self$auxiliary_names
if (!is.null(aux_nm))
atts <- private$drop_coordinates_attribute(atts, aux_nm)
len <- length(dt) # Number of fun outputs
if (len == 1L)
out <- .make_var(name[1L], dt[[1L]], axes, atts)
else {
if (length(name) < len)
name <- c(name, paste0("result_", (length(name)+1L):len))
out <- lapply(1:len, function(i) .make_var(name[i], dt[[i]], axes, atts))
names(out) <- name
}
out
})
if (length(f) == 1L)
res[[1L]]
else {
names(res) <- names(f)
res
}
},
#' @description This method extracts profiles of values from the array of
#' the variable, with the location along each axis to extract expressed in
#' coordinate values of each axis.
#'
#' @details The coordinates along each axis to be sampled are expressed in
#' values of the domain of the axis. Any axes which are not passed as
#' arguments are extracted in whole to the result. If bounds are set on
#' the axis, the coordinate whose bounds envelop the requested coordinate
#' is selected. Otherwise, the coordinate along the axis closest to the
#' supplied value will be used. If the value for a specified axis falls
#' outside the valid range of that axis, `NULL` is returned.
#'
#' A typical case is to extract the temporal profile as a 1D array for a
#' given location. In this case, use arguments for the latitude and
#' longitude on an X-Y-T data variable: `profile(lat = -24, lon = 3)`.
#' Other profiling options are also possible, such as a 2D zonal
#' atmospheric profile at a given longitude for an X-Y-Z data variable:
#' `profile(lon = 34)`.
#'
#' Multiple profiles can be extracted in one call by supplying vectors for
#' the indicated axes: `profile(lat = c(-24, -23, -2), lon = c(5, 5, 6))`.
#' The vectors need not have the same length, unless `.as_table = TRUE`.
#' With unequal length vectors the result will be a `list` of `CFVariable`
#' instances with different dimensionality and/or different axes.
#'
#' ## Auxiliary coordinate variables
#'
#' A special case exists for variables where the horizontal dimensions (X
#' and Y) are not in longitude and latitude coordinates but in some other
#' coordinate system. In this case the netCDF resource may have so-called
#' *auxiliary coordinate variables*. If the variable has such *auxiliary coordinate
#' variables* then you can specify their names (instead of specifying the
#' names of the primary planar axes).
#' @param ... One or more arguments of the form `axis = location`. The
#' "axis" part should be the name of an axis or its orientation `X`, `Y`,
#' `Z` or `T`. The "location" part is a vector of values representing
#' coordinates along the axis where to profile. A profile will be
#' generated for each of the elements of the "location" vectors in all
#' arguments.
#' @param .names A character vector with names for the results. The names
#' will be used for the resulting `CFVariable` instances, or as values for
#' the "location" column of the `data.table` if argument `.as_table` is
#' `TRUE`. If the vector is shorter than the longest vector of locations
#' in the `...` argument, a name "location_#" will be used, with the #
#' replaced by the ordinal number of the vector element.
#' @param .as_table Logical to flag if the results should be `CFVariable`
#' instances (`FALSE`, default) or a single `data.table` (`TRUE`). If
#' `TRUE`, all `...` arguments must have the same number of elements, use
#' the same axes and the `data.table` package must be installed.
#' @return If `.as_table = FALSE`, a `CFVariable` instance, or a list
#' thereof with each having one profile for each of the elements in the
#' "location" vectors of argument `...` and named with the respective
#' `.names` value. If `.as_table = TRUE`, a `data.table` with a row for
#' each element along all profiles, with a ".variable" column using the
#' values from the `.names` argument.
profile = function(..., .names = NULL, .as_table = FALSE) {
num_axes <- private$num_dim_axes()
if (!num_axes)
stop("Cannot profile a scalar variable", call. = FALSE)
# Organize the selectors
selectors <- list(...)
sel_names <- names(selectors)
sel_axes <- private$check_names(sel_names)
sel_lengths <- sapply(selectors, length)
total <- max(sel_lengths)
# Convert domain values to indices
if (inherits(private$.llgrid, "CFAuxiliaryLongLat")) {
aux_names <- private$.llgrid$grid_names
if (all(aux_names %in% sel_names)) {
xy <- private$.llgrid$sample_index(selectors[[aux_names[1L]]], selectors[[aux_names[2L]]])
sel_indices <- lapply(1:length(selectors), function(x) {
if (sel_names[x] == aux_names[1L])
idx <- xy[, 1L]
else if (sel_names[x] == aux_names[2L])
idx <- xy[, 2L]
else
idx <- private$.axes[[sel_axes[x]]]$indexOf(selectors[[x]])
idx[is.na(idx)] <- 0L
idx
})
} else
xy <- NULL
} else {
xy <- NULL
sel_indices <- lapply(1:length(selectors), function(x) {
idx <- private$.axes[[sel_axes[x]]]$indexOf(selectors[[x]])
idx[is.na(idx)] <- 0L
idx
})
}
# Check .as_table
if (.as_table) {
if (!requireNamespace("data.table", quietly = TRUE))
stop("Please install package 'data.table' before calling this method", call. = FALSE)
hasNA <- any(sapply(sel_indices, function(x) any(x == 0L)))
if (!all(sel_lengths == sel_lengths[1L]) || hasNA)
stop("All ... arguments must have the same length and use same axes when `.as_table = TRUE`.", call. = FALSE)
}
# Check .names
len <- length(.names)
if (len < total) {
.names <- c(.names, sapply((len + 1L):total, function(i) paste0("location_", i)))
} else if (len > total)
.names <- .names[1L:total]
out <- vector("list", total)
for (e in 1L:total) {
indices <- sapply(sel_indices, function(x) x[e])
if (!any(indices == 0L, na.rm = TRUE)) { # All selector coordinates are in range
start <- rep(1L, num_axes)
count <- self$dim()
out_axes_dim <- list()
out_axes_other <- list()
for (ax in 1L:num_axes) {
axis <- private$.axes[[ax]]
ax_ndx <- match(ax, sel_axes)
if (!is.na(ax_ndx) && !is.na(indices[ax_ndx])) {
start[ax] <- indices[ax_ndx]
count[ax] <- 1L
orient <- axis$orientation
nm <- if (is.null(xy)) axis$name
else if (orient == "X") private$.llgrid$varLong$name
else private$.llgrid$varLat$name
x <- axis$copy_with_values(nm, selectors[[ax_ndx]][e])
out_axes_other <- append(out_axes_other, x)
} else
out_axes_dim <- append(out_axes_dim, axis$copy())
}
scalars <- self$axes[-(1L:num_axes)]
axes <- c(out_axes_dim, out_axes_other, scalars)
names(axes) <- sapply(axes, function(a) a$name)
# Read the data
d <- private$read_chunk(start, count)
d <- drop(d)
# Sanitize the attributes for the result, as required, and make CRS
if (is.null(xy)) {
atts <- self$attributes
crs <- self$crs
} else {
atts <- private$drop_coordinates_attribute(self$attributes, private$.llgrid$grid_names)
atts <- atts[!(atts$name == "grid_mapping"), ] # drop: warped to lat-long
crs <- NULL
}
# Assemble the CFVariable instance
v <- CFVariable$new(.names[e], values = d, axes = axes, attributes = atts)
v$crs <- crs
v$update_coordinates_attribute(sapply(out_axes_other, function(ax) ax$name))
out[[e]] <- if (.as_table) v$data.table(var_as_column = TRUE)
else v
}
}
if (.as_table) {
atts <- attributes(out[[1L]])$value
out <- data.table::rbindlist(out, use.names = FALSE)
data.table::setattr(out, "value", atts)
} else if (total == 1L)
out <- out[[1L]]
else
names(out) <- .names
out
},
#' @description Append the data from another `CFVariable` instance to the
#' current instance, along one of the axes. The operation will only
#' succeed if the axes other than the one to append along have the same
#' coordinates and the coordinates of the axis to append along have to be
#' monotonically increasing or decreasing after appending.
#' @param from The `CFVariable` instance to append to this data variable.
#' @param along The name of the axis to append along. This must be a single
#' character string and the named axis has to be present both in this data
#' variable and in the `CFVariable` instance in argument `from`.
#' @return `self`, invisibly, with the arrays from this data variable and
#' `from` appended.
append = function(from, along) {
# Check if the `from` variable can be appended to self
if (length(along) != 1L || !(along %in% names(private$.axes)))
stop("Argument `along` must be a single name of an existing axis.", call. = FALSE)
if (length(from$axes) != length(private$.axes))
stop("Array `from` must have the same number of axes as this data variable", call. = FALSE)
for (ax in seq_along(self$axes)) {
if (self$axes[[ax]]$name == along)
axno <- ax
else
if (!self$axes[[ax]]$identical(from$axes[[ax]]))
stop(paste("Axis", ax, "is not identical between the two data variables."), call. = FALSE)
}
# Extend the axis `along` with values from `from`
private$.axes[[axno]] <- private$.axes[[axno]]$append(from$axes[[axno]])
# Merge the data values
app <- from$raw()
private$set_values(abind::abind(private$.values, app, along = axno))
# Unlink from any netCDF resource
self$detach()
invisible(self)
},
#' @description Convert the data to a `terra::SpatRaster` (3D) or a
#' `terra::SpatRasterDataset` (4D) object. The data will be oriented to
#' North-up. The 3rd dimension in the data will become layers in the
#' resulting `SpatRaster`, any 4th dimension the data sets. The `terra`
#' package needs to be installed for this method to work.
#' @return A `terra::SpatRaster` or `terra::SpatRasterDataset` instance.
terra = function() {
if (!requireNamespace("terra", quietly = TRUE))
stop("Please install package 'terra' before using this funcionality")
# Extent
YX <- private$YXZT()[1L:2L]
if (!all(YX))
stop("Cannot create `terra` object because data does not have X and Y axes.", call. = FALSE)
Xbnds <- private$.axes[[YX[2L]]]$bounds
if (inherits(Xbnds, "CFBounds")) Xbnds <- Xbnds$range()
else {
vals <- private$.axes[[YX[2L]]]$values
halfres <- (vals[2L] - vals[1L]) * 0.5 # this assumes regular spacing
Xbnds <- c(vals[1L] - halfres, vals[length(vals)] + halfres)
}
Ybnds <- private$.axes[[YX[1L]]]$bounds
if (inherits(Ybnds, "CFBounds")) Ybnds <- Ybnds$range()
else {
vals <- private$.axes[[YX[1L]]]$values
halfres <- (vals[2L] - vals[1L]) * 0.5 # this assumes regular spacing
Ybnds <- c(vals[1L] - halfres, vals[length(vals)] + halfres)
}
if (Ybnds[1L] > Ybnds[2L]) Ybnds <- rev(Ybnds)
ext <- round(c(Xbnds, Ybnds), CF.options$digits) # Round off spurious "accuracy"
# Create the object
arr <- self$array()
numdims <- length(dim(arr))
dn <- dimnames(arr)
if (numdims == 4L) {
r <- terra::sds(arr, extent = ext)
for (d4 in seq_along(dn[[4L]]))
names(r[d4]) <- dn[[3L]]
names(r) <- dn[[4L]]
} else {
r <- terra::rast(arr, extent = ext)
if (numdims == 3L)
names(r) <- dn[[3L]]
else if (length(private$.axes) > 2L) # Use coordinate of a scalar axis
names(r) <- private$.axes[[3L]]$coordinates
}
# CRS
if (!is.null(self$crs))
crs(r) <- self$crs$wkt2(.wkt2_axis_info(self))
r
},
#' @description Retrieve the data variable in the object in the form of a
#' `data.table`. The `data.table` package needs to be installed for this
#' method to work.
#'
#' The attributes associated with this data variable will be mostly lost.
#' If present, attributes 'long_name' and 'units' are attached to the
#' `data.table` as attributes, but all others are lost.
#' @param var_as_column Logical to flag if the name of the variable should
#' become a column (`TRUE`) or be used as the name of the column with the
#' data values (`FALSE`, default). Including the name of the variable as a
#' column is useful when multiple `data.table`s are merged into one.
#' @return A `data.table` with all data points in individual rows. All axes
#' will become columns. Two attributes are added: `name` indicates the
#' long name of this data variable, `units` indicates the physical unit of
#' the data values.
data.table = function(var_as_column = FALSE) {
if (!requireNamespace("data.table", quietly = TRUE))
stop("Please install package 'data.table' before using this functionality", call. = FALSE)
.datatable.aware = TRUE
exp <- expand.grid(lapply(private$.axes, function(ax) ax$coordinates),
KEEP.OUT.ATTRS = FALSE, stringsAsFactors = FALSE)
dt <- data.table::as.data.table(exp)
if (var_as_column) {
dt[ , .variable := private$.name]
suppressWarnings(dt[ , .value := private$.values])
} else
suppressWarnings(dt[ , eval(private$.name) := private$.values])
long_name <- self$attribute("long_name")
if (is.na(long_name)) long_name <- ""
units <- self$attribute("units")
if (is.na(units)) units <- ""
data.table::setattr(dt, "value", list(name = long_name, units = units))
dt
},
#' @description Tests if the `other` object is coincident with this data
#' variable: identical axes.
#' @param other A `CFVariable` instance to compare to this data variable.
#' @return `TRUE` if the data variables are coincident, `FALSE` otherwise.
is_coincident = function(other) {
if (!inherits(other, "CFVariable")) return(FALSE)
other_axes <- other$axes
if (length(other_axes) != length(self$axes)) return(FALSE)
for (ax in seq_along(self$axes))
if (!self$axes[[ax]]$identical(other_axes[[ax]]))
return(FALSE)
TRUE
},
#' @description Add a cell measure variable to this variable.
#' @param cm An instance of [CFCellMeasure].
#' @return Self, invisibly.
add_cell_measure = function(cm) {
if (inherits(cm, "CFCellMeasure"))
private$.cell_measures <- append(private$.cell_measures, setNames(list(cm), cm$name))
},
#' @description Add an auxiliary coordinate to the appropriate axis of this
#' variable. The length of the axis must be the same as the length of the
#' auxiliary labels.
#' @param aux An instance of [CFLabel] or [CFAxis].
#' @param axis An instance of `CFAxis` that these auxiliary coordinates are
#' for.
#' @return Self, invisibly.
add_auxiliary_coordinate = function(aux, axis) {
if (inherits(aux, "CFLabel") || inherits(aux, "CFAxis")) {
axis$auxiliary <- aux
if (aux$name %in% axis$coordinate_names)
self$update_coordinates_attribute(aux$name)
}
},
#' @description Save the data object to a netCDF file.
#' @param fn The name of the netCDF file to create.
#' @param pack Logical to indicate if the data should be packed. Packing is
#' only useful for numeric data; packing is not performed on integer values.
#' Packing is always to the "NC_SHORT" data type, i.e. 16-bits per value.
#' @return Self, invisibly.
save = function(fn, pack = FALSE) {
nc <- RNetCDF::create.nc(fn, prefill = FALSE, format = "netcdf4")
if (!inherits(nc, "NetCDF"))
stop("Could not create the netCDF file. Please check that the location of the supplied file name is writable.", call. = FALSE)
# FIXME: Use `attributes` argument to set global attributes? Then must have mechanism to create attributes before saving.
# Axes and auxiliary coordinates
lbls <- unlist(sapply(private$.axes, function(ax) {ax$write(nc); ax$coordinate_names[-1L]}), use.names = FALSE)
# CRS
if (!is.null(self$crs)) {
self$crs$write(nc)
self$set_attribute("grid_mapping", "NC_CHAR", self$crs$name)
}
if (!is.null(dt <- private$read_data())) {
# Packing
pack <- pack && private$.data_type %in% c("NC_FLOAT", "NC_DOUBLE")
if (pack) {
actual_range <- self$attribute("actual_range")
self$set_attribute("add_offset", private$.data_type, (actual_range[1L] + actual_range[2L]) * 0.5)
self$set_attribute("scale_factor", private$.data_type, (actual_range[2L] - actual_range[1L]) / 65534)
self$set_attribute("missing_value", "NC_SHORT", -32767)
}
# Data variable
dt <- private$orient(dt, "CF")
axes <- attr(dt, "axes")
dim_axes <- length(axes)
private$.id <- if (dim_axes > 0L)
RNetCDF::var.def.nc(nc, self$name, if (pack) "NC_SHORT" else private$.data_type, axes)
else
RNetCDF::var.def.nc(nc, self$name, if (pack) "NC_SHORT" else private$.data_type, NA)
if (length(private$.axes) > dim_axes || length(lbls)) {
non_dim_axis_names <- sapply(private$.axes, function(ax) ax$name)[-(1L:dim_axes)]
if (length(non_dim_axis_names) > 0L)
self$set_attribute("coordinates", "NC_CHAR", paste(c(non_dim_axis_names, lbls), collapse = " "))
}
self$write_attributes(nc, self$name)
RNetCDF::var.put.nc(nc, self$name, dt, pack = pack, na.mode = 2)
if (pack)
self$delete_attribute(c("scale_factor", "add_offset", "missing_value"))
}
RNetCDF::close.nc(nc)
invisible(self)
}
),
active = list(
#' @field friendlyClassName (read-only) A nice description of the class.
friendlyClassName = function(value) {
if (missing(value))
"Data variable"
},
#' @field axes (read-only) List of instances of classes descending from
#' [CFAxis] that are the axes of the data object. If there are any scalar
#' axes, they are listed after the axes that associate with the dimensions
#' of the data. (In other words, axes `1..n` describe the `1..n` data
#' dimensions, while any axes `n+1..m` are scalar axes.)
axes = function(value) {
if (missing(value))
private$.axes
},
#' @field crs The coordinate reference system of this variable,
#' as an instance of [CFGridMapping]. If this field is `NULL`, the
#' horizontal component of the axes are in decimal degrees of longitude
#' and latitude.
crs = function(value) {
if (missing(value))
private$.crs
else if (is.null(value) || inherits(value, "CFGridMapping"))
private$.crs <- value
else
stop("Invalid CRS for a CF data variable.", call. = FALSE) # nocov
},
#' @field cell_measures (read-only) List of the [CFCellMeasure] objects of
#' this variable, if defined.
cell_measures = function(value) {
if (missing(value))
private$.cell_measures
},
#' @field dimids (read-only) Retrieve the dimension ids used by the
#' NC variable used by this variable.
dimids = function(value) {
if (missing(value))
self$NCvar$dimids
},
#' @field dimnames (read-only) Retrieve dimnames of the data variable.
dimnames = function(value) {
if (missing(value)) {
# A data variable with a single value has no axes, like in ROMS data.
if (!length(private$.axes)) return(NULL)
len <- self$ndims
dn <- lapply(1:len, function(ax) dimnames(private$.axes[[ax]]))
names(dn) <- sapply(1:len, function(ax) private$.axes[[ax]]$name)
dn
}
},
#' @field auxiliary_names (read-only) Retrieve the names of the auxiliary
#' longitude and latitude grids as a vector of two character strings, in
#' that order. If no auxiliary grids are defined, returns `NULL`.
auxiliary_names = function(value) {
if (missing(value)) {
if (is.null(private$.llgrid)) NULL
else private$.llgrid$grid_names
}
},
#' @field gridLongLat Retrieve or set the grid of longitude and latitude
#' values of every grid cell when the main variable grid has a different
#' coordinate system.
gridLongLat = function(value) {
if (missing(value))
private$.llgrid
else if (inherits(value, "CFAuxiliaryLongLat"))
if (all(value$dimids %in% sapply(private$.axes, function(x) x$dimid)))
private$.llgrid <- value
else
warning("Dimension ids of auxiliary lat-lon grid do not match those of the axis.", call. = FALSE)
else stop("Trying to set wrong object as auxiliary lat-lon grid.", call. = FALSE)
},
#' @field crs_wkt2 (read-only) Retrieve the coordinate reference system
#' description of the variable as a WKT2 string.
crs_wkt2 = function(value) {
if (missing(value)) {
if (is.null(private$.crs)) {
# If no CRS has been set, return the default GEOGCRS unless
# the axis coordinates fall out of range in which case an empty string
# is returned.
orient <- lapply(private$.axes, function(x) x$orientation)
X <- match("X", orient, nomatch = 0L)
Y <- match("Y", orient, nomatch = 0L)
if (X && Y) {
X <- private$.axes[[X]]$range()
Y <- private$.axes[[Y]]$range()
if (Y[1L] >= -90 && Y[2L] <= 90 && ((X[1L] >= 0 && X[2L] <= 360) || (X[1L] >= -180 && X[2L] <= 180)))
.wkt2_crs_geo(4326)
else ""
} else ""
} else
private$.crs$wkt2(.wkt2_axis_info(self))
}
}
)
)
# Public S3 methods ------------------------------------------------------------
#' @export
dim.CFVariable <- function(x) {
sapply(x$axes, function(z) z$NCdim$length)
}
#' @export
dimnames.CFVariable <- function(x) {
ax <- x$axes
if (length(ax)) names(ax)
else NULL
}
#' Extract data for a variable
#'
#' Extract data from a `CFVariable` instance, optionally sub-setting the
#' axes to load only data of interest.
#'
#' If all the data of the variable in `x` is to be extracted, simply use `[]`
#' (unlike with regular arrays, this is required, otherwise the details of the
#' variable are printed on the console).
#'
#' The indices into the axes to be subset can be specified in a variety of
#' ways; in practice it should (resolve to) be a vector of integers. A range
#' (e.g. `100:200`), an explicit vector (`c(23, 46, 3, 45, 17`), a sequence
#' (`seq(from = 78, to = 100, by = 2`), all work. Note, however, that only a
#' single range is generated from the vector so these examples resolve to
#' `100:200`, `3:46`, and `78:100`, respectively. It is also possible to use a
#' custom function as an argument.
#'
#' This method works with "bare" indices into the axes of the array. If
#' you want to use domain values of the axes (e.g. longitude values or
#' timestamps) to extract part of the variable array, use the
#' `CFVariable$subset()` method.
#'
#' Scalar axes should not be included in the indexing as they do not represent a
#' dimension into the data array.
#'
#' @param x An `CFVariable` instance to extract the data of.
#' @param i,j,... Expressions, one for each axis of `x`, that select a
#' number of elements along each axis. If any expressions are missing,
#' the entire axis is extracted. The values for the arguments may be an
#' integer vector or a function that returns an integer vector. The range of
#' the values in the vector will be used. See examples, below.
#' @param drop Logical, ignored. Axes are never dropped. Any degenerate
#' dimensions of the array are returned as such, with dimnames and appropriate
#' attributes set.
#'
#' @return An array with dimnames and other attributes set.
#' @export
#' @aliases bracket_select
#' @docType methods
#' @examples
#' fn <- system.file("extdata",
#' "pr_day_EC-Earth3-CC_ssp245_r1i1p1f1_gr_20230101-20231231_vncdfCF.nc",
#' package = "ncdfCF")
#' ds <- open_ncdf(fn)
#' pr <- ds[["pr"]]
#'
#' # How are the dimensions organized?
#' dimnames(pr)
#'
#' # Precipitation data for March for a single location
#' x <- pr[5, 12, 61:91]
#' str(x)
#'
#' # Summer precipitation over the full spatial extent
#' summer <- pr[, , 173:263]
#' str(summer)
"[.CFVariable" <- function(x, i, j, ..., drop = FALSE) {
numaxes <- sum(sapply(x$axes, function(ax) ax$length > 1L))
t <- vector("list", numaxes)
names(t) <- dimnames(x)[1:numaxes]
sc <- sys.call()
if (numaxes == 0L && length(sc) == 3L) { # Variable is a scalar value
start <- 1L
count <- NA_integer_
} else {
sc <- sc[-(1L:2L)] # drop [ and x
if ((length(sc) == 1L) && (identical(sc[[1L]], quote(expr = )))) {
# [] specified, read whole array
start <- rep(1L, numaxes)
count <- rep(NA_integer_, numaxes)
dnames <- lapply(x$axes, dimnames)
t <- lapply(x$axes, function(z) z$time)
} else if (length(sc) != numaxes) {
stop("Indices specified are not equal to the number of axes of the variable")
} else {
start <- vector("integer", numaxes)
count <- vector("integer", numaxes)
dnames <- vector("list", numaxes)
names(dnames) <- dimnames(x)[1:numaxes]
for (d in seq_along(sc)) {
ax <- x$axes[[d]]
tm <- ax$time
if (identical(sc[[d]], quote(expr = ))) {
# Axis not subsetted, read the whole thing
start[d] <- 1L
count[d] <- NA_integer_
dnames[[d]] <- dimnames(ax)
if (!is.null(tm)) t[[d]] <- tm
} else {
# Subset the axis
v <- eval(sc[[d]])
ex <- range(v)
start[d] <- ex[1L]
count[d] <- ex[2L] - ex[1L] + 1L
dnames[[d]] <- dimnames(ax)[seq(ex[1], ex[2])]
if (!is.null(tm)) {
idx <- tm$indexOf(ex[1L]:ex[2L], "constant")
t[[d]] <- attr(idx, "CFTime")
}
}
}
}
}
data <- x$NCvar$get_data(start, count)
# Apply dimension data and other attributes
if (length(x$axes) && length(dim(data)) == length(dnames)) { # dimensions may have been dropped automatically, e.g. NC_CHAR to character string
dimnames(data) <- dnames
ax <- sapply(x$axes, function(ax) if (ax$length > 1L) ax$orientation)
ax <- ax[lengths(ax) > 0L]
attr(data, "axis") <- ax
}
time <- t[lengths(t) > 0L]
if (length(time))
attr(data, "time") <- time
data
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.