Nothing
# Author: Robert J. Hijmans
# Date : April 2026
# Version 0.1
# License GPL v3
# Coerce tile extents to a list of `list(outer=, inner=)` pairs of plain
# numeric vectors (xmin, xmax, ymin, ymax). The worker reads values on
# `outer`, then the result of `fun` is cropped to `inner` before writing,
# so the per-tile output rasters never overlap and assemble cleanly with
# vrt(). For all the explicit `tiles` shapes we trust the caller (outer ==
# inner); for the auto path we expand each inner tile by `buffer` cells,
# clamped to x's extent.
.tile_apply_extents <- function(x, tiles, cores=1, buffer=0) {
pair <- function(o, i) list(outer = as.numeric(o), inner = as.numeric(i))
identity_pairs <- function(m) {
lapply(seq_len(nrow(m)), function(j) {
v <- as.numeric(m[j, ])
pair(v, v)
})
}
if (is.null(tiles)) {
# auto path: choose tiles, optionally with a read buffer
inner <- getTileExtents(x, cores=cores)
if (any(buffer > 0)) {
outer <- getTileExtents(x, cores=cores, buffer=buffer)
# clamp outer to the raster extent so window<- never gets a
# read window that falls outside x.
rext <- as.vector(ext(x)) # xmin, xmax, ymin, ymax
out <- lapply(seq_len(nrow(inner)), function(j) {
o <- as.numeric(outer[j, ])
o[1] <- max(o[1], rext[1])
o[2] <- min(o[2], rext[2])
o[3] <- max(o[3], rext[3])
o[4] <- min(o[4], rext[4])
pair(o, as.numeric(inner[j, ]))
})
return(out)
}
return(identity_pairs(inner))
}
if (inherits(tiles, "SpatExtent")) {
v <- as.vector(tiles)
return(list(pair(v, v)))
}
if (is.list(tiles)) {
if (length(tiles) == 0) {
error("tile_apply", "'tiles' is empty")
}
out <- lapply(tiles, function(t) {
if (inherits(t, "SpatExtent")) {
v <- as.vector(t); pair(v, v)
} else if (is.numeric(t) && length(t) == 4) {
v <- as.numeric(t); pair(v, v)
} else {
error("tile_apply", "list elements of 'tiles' must be a SpatExtent or a length-4 numeric (xmin, xmax, ymin, ymax)")
}
})
return(out)
}
if (is.matrix(tiles)) {
if (ncol(tiles) != 4) {
error("tile_apply", "matrix 'tiles' must have 4 columns: xmin, xmax, ymin, ymax")
}
return(identity_pairs(tiles))
}
if (inherits(tiles, "SpatRaster") || inherits(tiles, "SpatVector") || is.numeric(tiles)) {
m <- getTileExtents(x, tiles)
return(identity_pairs(m))
}
error("tile_apply", "'tiles' must be NULL/missing (auto from block size), a SpatExtent, a list of SpatExtents, a 4-column matrix of extents, a SpatRaster or SpatVector defining tile geometry, or a numeric (rows, cols)")
}
# wrap any terra object so that it can travel across worker boundaries.
# Recurses into plain lists so users can pass e.g. a list of SpatExtents.
.tile_apply_wrap <- function(v) {
if (inherits(v, c("SpatRaster", "SpatVector", "SpatRasterDataset",
"SpatRasterCollection", "SpatVectorCollection",
"SpatExtent", "SpatVectorProxy", "SpatGraticule"))) {
return(wrap(v))
}
if (is.list(v) && !is.object(v)) {
return(lapply(v, .tile_apply_wrap))
}
v
}
.tile_apply_unwrap <- function(v) {
if (inherits(v, "Packed")) {
unwrap(v)
} else if (is.list(v) && !is.object(v)) {
lapply(v, .tile_apply_unwrap)
} else {
v
}
}
# Per-tile worker for the file-backed dispatch path. Defined at file scope
# (not as a closure) so it serializes cleanly across PSOCK/forked/future
# workers without dragging tile_apply()'s local environment along. Argument
# names must NOT collide with parLapply() args (cl, X, fun, chunk.size).
# `.px` is the whole packed raster, shared across jobs; the worker windows
# it down to `.job$outer` before invoking `fun`.
.tile_apply_worker <- function(.job, .px, .ufun, .args, .wopt) {
.unw <- function(a) {
if (inherits(a, "Packed")) terra::unwrap(a)
else if (is.list(a) && !is.object(a)) lapply(a, .unw)
else a
}
x <- terra::unwrap(.px)
terra::window(x) <- terra::ext(.job$outer)
.args <- lapply(.args, .unw)
r <- do.call(.ufun, c(list(x), .args))
if (!inherits(r, "SpatRaster")) {
stop("'fun' must return a SpatRaster for every tile")
}
# crop to the inner (un-buffered) extent so per-tile outputs do not
# overlap and assemble cleanly with vrt()
if (!identical(.job$outer, .job$inner)) {
r <- terra::crop(r, terra::ext(.job$inner), snap="near")
}
terra::writeRaster(r, filename=.job$file, overwrite=TRUE, wopt=.wopt)
.job$file
}
# Per-tile worker for the in-memory dispatch path. The job carries its own
# pre-cropped, packed raster slice (`.job$px`), so the worker only receives
# the pixels it needs and does not have to set a window.
.tile_apply_worker_slice <- function(.job, .ufun, .args, .wopt) {
.unw <- function(a) {
if (inherits(a, "Packed")) terra::unwrap(a)
else if (is.list(a) && !is.object(a)) lapply(a, .unw)
else a
}
x <- terra::unwrap(.job$px)
.args <- lapply(.args, .unw)
r <- do.call(.ufun, c(list(x), .args))
if (!inherits(r, "SpatRaster")) {
stop("'fun' must return a SpatRaster for every tile")
}
if (!identical(.job$outer, .job$inner)) {
r <- terra::crop(r, terra::ext(.job$inner), snap="near")
}
terra::writeRaster(r, filename=.job$file, overwrite=TRUE, wopt=.wopt)
.job$file
}
# Detect future-related `cores` values without calling the strategy: a bare
# strategy function (e.g. future::multisession) is class "future"+"function";
# a tweaked strategy (e.g. future::multisession(workers=2)) is "tweaked"+
# "future". Both should route through future.apply.
.is_future_plan <- function(cores) {
inherits(cores, c("FutureStrategy", "tweaked")) ||
(inherits(cores, "future") && is.function(cores))
}
# Inspect `cores` and decide which dispatch path to use. Returns a named
# list with: $kind in {"seq","cluster","future"}, $ncores (int), $cl (cluster
# or NULL), $owns_cl (TRUE iff we created the cluster and must stop it).
# The future-plan setup (installing/restoring the plan) is handled in
# tile_apply() so that on.exit is attached to the right frame; here we only
# resolve the worker count.
.tile_apply_dispatch <- function(cores) {
if (inherits(cores, "cluster")) {
return(list(kind="cluster", ncores=length(cores), cl=cores, owns_cl=FALSE))
}
if (is.character(cores) && length(cores) == 1 &&
tolower(cores) %in% c("future", "future.apply")) {
if (!requireNamespace("future", quietly=TRUE) ||
!requireNamespace("future.apply", quietly=TRUE)) {
error("tile_apply", "cores='future' requires the 'future' and 'future.apply' packages")
}
return(list(kind="future", ncores=max(1L, future::nbrOfWorkers()),
cl=NULL, owns_cl=FALSE))
}
n <- max(as.integer(cores)[1], 1L)
if (n > 1L) {
return(list(kind="cluster", ncores=n, cl=NULL, owns_cl=TRUE))
}
list(kind="seq", ncores=1L, cl=NULL, owns_cl=FALSE)
}
tile_apply <- function(x, fun, cores=1, cpkgs=NULL, tiles=NULL, buffer=0, ...,
filename="", overwrite=FALSE, wopt=list(),
overlap_fun=NULL) {
if (!inherits(x, "SpatRaster")) {
error("tile_apply", "'x' must be a SpatRaster")
}
if (any(window(x))) {
error("tile_apply", "'x' already has a window set; remove it first with 'window(x) <- NULL'")
}
fun <- match.fun(fun)
# `buffer` is only honoured with auto tiles. For explicit tiles the caller
# already controls overlap (e.g. via getTileExtents(buffer=...)).
if (!is.null(tiles) && any(buffer > 0)) {
warn("tile_apply", "'buffer' is only used when 'tiles' is NULL; ignoring it")
buffer <- 0
}
# If the caller passed a future plan/strategy via cores=, install it
# now and restore on exit -- so subsequent dispatch and tile sizing see
# the new nbrOfWorkers(). After installation, the dispatch path becomes
# the regular "future" string path.
if (.is_future_plan(cores)) {
if (!requireNamespace("future", quietly=TRUE) ||
!requireNamespace("future.apply", quietly=TRUE)) {
error("tile_apply", "a future plan was passed but 'future' and 'future.apply' are not installed")
}
oplan <- future::plan(cores)
on.exit(future::plan(oplan), add=TRUE)
cores <- "future"
}
# resolve dispatch + cores up-front so the auto tile sizer knows about it
disp <- .tile_apply_dispatch(cores)
ncores <- disp$ncores
exts <- .tile_apply_extents(x, tiles, cores=ncores, buffer=buffer)
ntiles <- length(exts)
if (ntiles == 0) {
error("tile_apply", "no tiles to process")
}
# Per-call temp directory for intermediate per-tile files. Always written to disk
tdir <- tempfile(pattern="tile_apply_")
dir.create(tdir, recursive=TRUE, showWarnings=FALSE)
tile_files <- file.path(tdir, sprintf("tile_%05d.tif", seq_len(ntiles)))
keep_tiles <- FALSE # set TRUE only when the result is a VRT backed by them
on.exit({
if (!keep_tiles) unlink(tdir, recursive=TRUE)
}, add=TRUE)
# When all of x's sources are in memory, ship a per-tile slice instead
# of the whole raster. This avoids serialising values(x) once per worker
# (or once per chunk) and removes the need for window<- inside the
# worker. For file-backed sources we keep the path-based contract:
# workers parallel-read their own tile via window<-, which is strictly
# cheaper than crop()ing on the parent and pushing pixels to workers.
in_memory <- length(sources(x)) > 0 && all(sources(x) == "")
build_jobs <- function() {
if (in_memory) {
mapply(function(p, f) {
tile <- crop(x, ext(p$outer), snap="near")
list(px = wrap(tile), outer = p$outer,
inner = p$inner, file = f)
}, exts, tile_files, SIMPLIFY=FALSE)
} else {
mapply(function(p, f) list(outer=p$outer, inner=p$inner, file=f),
exts, tile_files, SIMPLIFY=FALSE)
}
}
if (disp$kind == "cluster") {
cl <- disp$cl
if (is.null(cl)) {
cl <- parallel::makeCluster(disp$ncores)
}
if (disp$owns_cl) {
on.exit(parallel::stopCluster(cl), add=TRUE)
}
# every worker needs terra
parallel::clusterCall(cl, function() {
suppressPackageStartupMessages(library(terra))
invisible(NULL)
})
if (!is.null(cpkgs)) {
cpkgs <- as.character(cpkgs)
parallel::clusterCall(cl, function(pkgs) {
for (p in pkgs) suppressPackageStartupMessages(library(p, character.only=TRUE))
invisible(NULL)
}, cpkgs)
}
# wrap any terra objects in `...` so they survive serialization
args <- lapply(list(...), .tile_apply_wrap)
jobs <- build_jobs()
if (in_memory) {
out_files <- unlist(parallel::parLapply(cl, jobs,
.tile_apply_worker_slice,
.ufun=fun, .args=args, .wopt=wopt))
} else {
# wrap x once; the workers re-open it from filenames
px <- wrap(x)
out_files <- unlist(parallel::parLapply(cl, jobs,
.tile_apply_worker,
.px=px, .ufun=fun, .args=args, .wopt=wopt))
}
} else if (disp$kind == "future") {
# Plan was already installed up-front (if the caller passed one).
# We just dispatch to future.apply::future_lapply with the same
# wrap/unwrap contract as the cluster path.
args <- lapply(list(...), .tile_apply_wrap)
jobs <- build_jobs()
fpkgs <- unique(c("terra", as.character(cpkgs)))
if (in_memory) {
out_files <- unlist(future.apply::future_lapply(jobs,
.tile_apply_worker_slice,
.ufun=fun, .args=args, .wopt=wopt,
future.packages=fpkgs, future.seed=TRUE))
} else {
px <- wrap(x)
out_files <- unlist(future.apply::future_lapply(jobs,
.tile_apply_worker,
.px=px, .ufun=fun, .args=args, .wopt=wopt,
future.packages=fpkgs, future.seed=TRUE))
}
} else {
# sequential path - same disk-streaming contract as the workers
args <- list(...)
for (i in seq_len(ntiles)) {
p <- exts[[i]]
y <- deepcopy(x)
window(y) <- ext(p$outer)
r <- do.call(fun, c(list(y), args))
if (!inherits(r, "SpatRaster")) {
error("tile_apply", "'fun' must return a SpatRaster for every tile")
}
if (!identical(p$outer, p$inner)) {
r <- crop(r, ext(p$inner), snap="near")
}
writeRaster(r, filename=tile_files[i],
overwrite=TRUE, wopt=wopt)
rm(r, y); gc(verbose=FALSE)
}
out_files <- tile_files
}
# Single tile shortcut: just promote the one tile file.
if (length(out_files) == 1) {
if (nzchar(filename)) {
ok <- file.copy(out_files, filename, overwrite=overwrite)
if (!isTRUE(ok)) {
error("tile_apply", "could not copy tile output to 'filename'")
}
return(rast(filename))
}
# to survives the on.exit cleanup
stable <- tempfile(fileext=".tif")
file.rename(out_files, stable)
return(rast(stable))
}
if (is.null(overlap_fun)) {
# VRT for non-overlapping tiles
vrtfile <- file.path(tdir, "all.vrt")
v <- vrt(out_files, filename=vrtfile, overwrite=TRUE)
if (nzchar(filename)) {
out <- writeRaster(v, filename=filename,
overwrite=overwrite, wopt=wopt)
} else {
# tile files BACK this VRT - keep them around for the session
keep_tiles <- TRUE
out <- v
}
} else {
rcoll <- sprc(lapply(out_files, rast))
out <- mosaic(rcoll, fun=overlap_fun, filename=filename, overwrite=overwrite, wopt=wopt)
}
out
}
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.