Skip to content

Commit 593fd1b

Browse files
authored
Merge pull request #3537 from dlebauer/clip_and_move_raster
Add function to data.land that will clip a raster file, mask it, and then write it out to a new file
2 parents 198010c + c25bbe1 commit 593fd1b

9 files changed

Lines changed: 198 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ All notable changes are kept in this file. All changes made should be added to t
33
`Unreleased`. Once a new release is made this file will be updated to create a new `Unreleased`
44
section for the next release.
55

6-
For more information about this file see also [Keep a Changelog](http://keepachangelog.com/) .
6+
For more information about this file see also [Keep a Changelog](http://keepachangelog.com/) .
7+
8+
## Unreleased
9+
10+
### Added
11+
12+
* Add function `clip_and_save_raster_file()` for subsetting rasters to match a polygon of interest (#3537).
13+
714

815
## [1.9.0] - 2025-05-25
916

docker/depends/pecan_package_dependencies.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,7 @@
681681
"withr","*","models/sipnet","Suggests",FALSE
682682
"withr","*","modules/allometry","Suggests",FALSE
683683
"withr","*","modules/data.atmosphere","Suggests",FALSE
684+
"withr","*","modules/data.land","Suggests",FALSE
684685
"xgboost","*","modules/assim.sequential","Suggests",FALSE
685686
"XML","*","base/workflow","Imports",FALSE
686687
"XML","*","models/biocro","Imports",FALSE

modules/data.land/DESCRIPTION

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ Suggests:
6767
redland,
6868
raster,
6969
reticulate,
70-
testthat (>= 1.0.2)
70+
testthat (>= 1.0.2),
71+
withr
7172
Remotes:
7273
github::ropensci/traits
7374
License: BSD_3_clause + file LICENSE

modules/data.land/NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export(Read.IC.info.BADM)
1010
export(Read_Tucson)
1111
export(Soilgrids_SoilC_prep)
1212
export(buildJAGSdata_InventoryRings)
13+
export(clip_and_save_raster_file)
1314
export(cohort2pool)
1415
export(dataone_download)
1516
export(download.SM_CDS)

modules/data.land/NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# Unreleased
2+
3+
* Add function `clip_and_save_raster_file()` for subsetting rasters to match a polygon of interest (#3537).
4+
5+
16
# PEcAn.data.land 1.8.2
27
- Removed unused parameter `machine` from put_veg_module()
38

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#' Clip and Save a Raster File
2+
#'
3+
#' Clips a raster to a polygon bounding box, optionally masks to polygon, and saves the
4+
#' output in the same format as the input.
5+
#'
6+
#' @param input_path Character. Path to the input raster file.
7+
#' @param polygon An object or file coercible to a `SpatVector` by `terra::vect()`
8+
#' (e.g., an `sf` object, a `SpatVector`, or a file path to a vector dataset).
9+
#' used for clipping and masking. Must have a valid CRS.
10+
#' @param out_path Character. Path to save the processed raster.
11+
#' @param mask Logical: Should pixels outside the polygon but inside its bounding box
12+
#' be masked out (TRUE) or included (FALSE)?
13+
#' @param overwrite Logical: Replace output file if it already exists?
14+
#' @return Invisibly, the clipped `SpatRaster` object. The raster is also saved to `out_path`.
15+
#' @export
16+
#' @author David LeBauer
17+
clip_and_save_raster_file <- function(input_path, polygon, out_path, mask = TRUE, overwrite = TRUE) {
18+
19+
# Check that input and output files have same extension
20+
# This function is not designed to convert between raster formats
21+
if (tools::file_ext(input_path) != tools::file_ext(out_path)) {
22+
PEcAn.logger::logger.error("Input and output files must have the same extension.")
23+
}
24+
25+
rast_in <- terra::rast(input_path)
26+
27+
# Coerce to SpatVector if not already
28+
if (inherits(polygon, "SpatVector")) { # NB passing a SpatVector to terra::vect() fails
29+
poly_sv <- polygon
30+
} else {
31+
poly_sv <- terra::vect(polygon)
32+
}
33+
34+
if (terra::crs(poly_sv) == "") {
35+
PEcAn.logger::logger.error("Input polygon must have CRS defined.")
36+
}
37+
38+
# Reproject polygon to raster CRS if different
39+
if (!terra::same.crs(poly_sv, rast_in)) {
40+
poly_sv <- terra::project(poly_sv, terra::crs(rast_in))
41+
}
42+
43+
rast_crop <- terra::crop(rast_in, poly_sv)
44+
45+
if (mask) {
46+
rast_to_write <- terra::mask(rast_crop, poly_sv)
47+
} else {
48+
rast_to_write <- rast_crop
49+
}
50+
51+
terra::writeRaster(
52+
rast_to_write,
53+
filename = out_path,
54+
overwrite = overwrite
55+
)
56+
57+
invisible(rast_to_write)
58+
}

modules/data.land/man/clip_and_save_raster_file.Rd

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# helper to create a small test raster
2+
make_raster <- function(outfile, crs = "EPSG:4326") {
3+
r <- terra::rast(matrix(1:16, 4, 4),
4+
extent = terra::ext(0, 4, 0, 4),
5+
crs = crs
6+
)
7+
terra::writeRaster(r, outfile, filetype = "GTiff", overwrite = TRUE)
8+
return(outfile)
9+
}
10+
11+
test_that("clip & mask works: output clipped to polygon bbox and masked", {
12+
in_r <- withr::local_tempfile(fileext = ".tif")
13+
out_f <- withr::local_tempfile(fileext = ".tif")
14+
15+
make_raster(outfile = in_r)
16+
17+
poly <- terra::as.polygons(
18+
terra::ext(1, 3, 1, 3),
19+
crs = "EPSG:4326"
20+
)
21+
22+
clip_and_save_raster_file(input_path = in_r, polygon = poly, out_path = out_f, mask = TRUE)
23+
24+
expect_true(file.exists(out_f))
25+
26+
r_out <- terra::rast(out_f)
27+
expect_equal(terra::ext(r_out), terra::ext(sf::st_bbox(poly)))
28+
29+
inside_vals <- terra::values(terra::mask(r_out, poly, inverse = FALSE))
30+
expect_true(all(!is.na(inside_vals)))
31+
32+
outside_vals <- terra::values(terra::mask(r_out, poly, inverse = TRUE))
33+
expect_true(all(is.na(outside_vals)))
34+
})
35+
36+
test_that("clip without mask retains all values within bbox", {
37+
in_r <- withr::local_tempfile(fileext = ".tif")
38+
make_raster(outfile = in_r)
39+
40+
poly <- sf::st_as_sf(
41+
sf::st_as_sfc(
42+
sf::st_bbox(c(xmin = 1, ymin = 1, xmax = 3, ymax = 3), crs = sf::st_crs(4326))
43+
)
44+
)
45+
out_f <- withr::local_tempfile(fileext = ".tif")
46+
47+
clip_and_save_raster_file(in_r, poly, out_f, mask = FALSE)
48+
r_out <- terra::rast(out_f)
49+
expect_false(any(is.na(terra::values(r_out))))
50+
})
51+
52+
test_that("preserves CRS and filetype", {
53+
in_r_path <- withr::local_tempfile(fileext = ".tif")
54+
make_raster(outfile = in_r_path, crs = "EPSG:3857")
55+
56+
spatvect_raster <- terra::rast(in_r_path)
57+
58+
poly <- sf::st_as_sf(
59+
sf::st_as_sfc(
60+
sf::st_bbox(c(xmin = 1, ymin = 1, xmax = 3, ymax = 3), crs = sf::st_crs(3857))
61+
)
62+
)
63+
out_f_path <- withr::local_tempfile(fileext = ".tif")
64+
65+
clip_and_save_raster_file(input_path = in_r_path, polygon = poly, out_path = out_f_path)
66+
r_out <- terra::rast(out_f_path)
67+
68+
expect_equal(
69+
tools::file_ext(terra::sources(r_out)[1]),
70+
tools::file_ext(terra::sources(spatvect_raster)[1])
71+
)
72+
expect_true(terra::same.crs(r_out, spatvect_raster))
73+
})

modules/data.land/tests/testthat/test-match_species_id.R

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,28 @@ test_that("Species matching works", {
2222
user = "bety",
2323
password = "bety",
2424
host = "localhost",
25-
driver = "Postgres")
26-
con <- PEcAn.DB::db.open(db_params)
25+
driver = "Postgres"
26+
)
27+
con <- tryCatch(
28+
PEcAn.DB::db.open(db_params),
29+
error = function(e) NULL
30+
)
31+
32+
skip_if(is.null(con), "No database connection available for species matching tests.")
2733

2834
test_merge(c('ACRU', 'TSCA'), 'usda', con)
2935
test_merge(c(316L, 261L), 'fia', con)
3036
test_merge(c('Acer rubrum', 'Tsuga canadensis'), 'latin_name', con)
3137

3238
test_table <- data.frame(
3339
bety_species_id = c(30L, 1419L),
34-
input_code = c('AceRub', 'TsuCan'))
40+
input_code = c('AceRub', 'TsuCan')
41+
)
3542

3643
test_merge(
3744
input_codes = test_table$input_code,
3845
format_name = 'custom',
3946
bety = con,
40-
translation_table = test_table)
47+
translation_table = test_table
48+
)
4149
})

0 commit comments

Comments
 (0)