Skip to content

Commit

Permalink
nlcd functions refactoring
Browse files Browse the repository at this point in the history
- Missing geometry is fixed in process_nlcd
- calc_nlcd does not take exceptions into account
- Missing class in inst/extdata/nlcd_classes.csv is added
  • Loading branch information
Insang Song committed May 30, 2024
1 parent ee50712 commit 21658ab
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 68 deletions.
1 change: 0 additions & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ importFrom(terra,buffer)
importFrom(terra,coltab)
importFrom(terra,crop)
importFrom(terra,crs)
importFrom(terra,deepcopy)
importFrom(terra,describe)
importFrom(terra,distance)
importFrom(terra,expanse)
Expand Down
101 changes: 65 additions & 36 deletions R/calculate_covariates.R
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,15 @@ calc_koppen_geiger <-
#' @param from SpatRaster(1). Output of \code{process_nlcd()}.
#' @param locs terra::SpatVector of points geometry
#' @param locs_id character(1). Unique identifier of locations
#' @param mode character(1). One of `"exact"`
#' (using [`exactextractr::exact_extract()`])
#' or `"terra"` (using [`terra::freq()`]).
#' @param radius numeric (non-negative) giving the
#' radius of buffer around points
#' @param max_cells integer(1). Maximum number of cells to be read at once.
#' Higher values may expedite processing, but will increase memory usage.
#' Maximum possible value is `2^31 - 1`.
#' Maximum possible value is `2^31 - 1`. Only valid when
#' `mode = "exact"`.
#' See [`exactextractr::exact_extract`] for details.
#' @param geom logical(1). Should the geometry of `locs` be returned in the
#' `data.frame`? Default is `FALSE`. If `geom = TRUE` and `locs` contain
Expand All @@ -254,8 +258,13 @@ calc_koppen_geiger <-
#' coordinate reference system of the `$geometry` is the coordinate
#' reference system of `from`.
#' @param ... Placeholders.
#' @note NLCD is available in U.S. only. Users should be cautious
#' the spatial extent of the data.
#' @note NLCD is available in U.S. only. Users should be aware of
#' the spatial extent of the data. The results are different depending
#' on `mode` argument. The `"terra"` mode is less memory intensive
#' but less accurate because it counts the number of cells
#' intersecting with the buffer. The `"exact"` may be more accurate
#' but uses more memory as it will account for the partial overlap
#' with the buffer.
#' @seealso [`process_nlcd`]
#' @returns a data.frame object
#' @importFrom utils read.csv
Expand All @@ -264,7 +273,6 @@ calc_koppen_geiger <-
#' @importFrom terra project
#' @importFrom terra vect
#' @importFrom terra crs
#' @importFrom terra deepcopy
#' @importFrom terra set.crs
#' @importFrom terra buffer
#' @importFrom sf st_union
Expand All @@ -276,11 +284,14 @@ calc_koppen_geiger <-
calc_nlcd <- function(from,
locs,
locs_id = "site_id",
mode = c("exact", "terra"),
radius = 1000,
max_cells = 5e7,
geom = FALSE,
nthreads = 1L,
...) {
# check inputs
mode <- match.arg(mode)
if (!is.numeric(radius)) {
stop("radius is not a numeric.")
}
Expand All @@ -307,43 +318,61 @@ calc_nlcd <- function(from,
# select points within mainland US and reproject on nlcd crs if necessary
data_vect_b <-
terra::project(locs_vector, y = terra::crs(from))

# create circle buffers with buf_radius
bufs_pol <- terra::buffer(data_vect_b, width = radius) |>
sf::st_as_sf() |>
sf::st_geometry()
# ratio of each nlcd class per buffer
nlcd_at_bufs <-
exactextractr::exact_extract(
from,
bufs_pol,
fun = "frac",
force_df = TRUE,
progress = FALSE,
max_cells_in_memory = max_cells
)

# select only the columns of interest
bufs_pol <- terra::buffer(data_vect_b, width = radius)
cfpath <- system.file("extdata", "nlcd_classes.csv", package = "amadeus")
nlcd_classes <- utils::read.csv(cfpath)
nlcd_at_bufs <-
nlcd_at_bufs[
sort(names(nlcd_at_bufs)[
grepl(paste0("frac_(", paste(nlcd_classes$value, collapse = "|"), ")"),
names(nlcd_at_bufs))
])
]

if (mode == "fast") {
# fast mode
class_query <- "names"
# extract land cover class in each buffer
nlcd_at_bufs <-
terra::freq(
from,
zones = bufs_pol,
wide = TRUE
)
nlcd_at_bufs <- nlcd_at_bufs[, -seq(1, 2)]
nlcd_cellcnt <- nlcd_at_bufs[, seq(1, ncol(nlcd_at_bufs), 1)]
nlcd_cellcnt <- nlcd_cellcnt / rowSums(nlcd_cellcnt)
nlcd_at_bufs[, seq(1, ncol(nlcd_at_bufs), 1)] <- nlcd_cellcnt
} else {
class_query <- "value"
# ratio of each nlcd class per buffer
bufs_pol <- bufs_pol |>
sf::st_as_sf() |>
sf::st_geometry()
nlcd_at_bufs <-
exactextractr::exact_extract(
from,
bufs_pol,
fun = "frac",
force_df = TRUE,
progress = FALSE,
max_cells_in_memory = max_cells
)

# select only the columns of interest
nlcd_at_buf_names <- names(nlcd_at_bufs)
nlcd_val_cols <-
grep("^frac_", nlcd_at_buf_names)
nlcd_at_bufs <- nlcd_at_bufs[, nlcd_val_cols]
}

# change column names
nlcd_names <- names(nlcd_at_bufs)
nlcd_names <- sub(pattern = "frac_", replacement = "", x = nlcd_names)
nlcd_names <- sort(as.numeric(nlcd_names))
nlcd_names <- nlcd_classes[nlcd_classes$value %in% nlcd_names, c("class")]
new_names <- sapply(
nlcd_names,
function(x) {
sprintf("LDU_%s_0_%05d", x, radius)
}
)
nlcd_names <-
switch(
mode,
exact = sort(as.numeric(nlcd_names)),
fast = nlcd_names
)
nlcd_names <-
nlcd_classes$class[match(nlcd_names, nlcd_classes[[class_query]])]
#nlcd_classes[nlcd_classes[[class_query]] %in% nlcd_names, c("class")]
new_names <- sprintf("LDU_%s_0_%05d", nlcd_names, radius)
names(nlcd_at_bufs) <- new_names
# merge locs_df with nlcd class fractions
new_data_vect <- cbind(locs_df, as.integer(year), nlcd_at_bufs)
Expand Down Expand Up @@ -404,7 +433,7 @@ calc_ecoregion <-
locs_df <- locs_prepared[[2]]

extracted <- terra::intersect(locsp, from)
return(extracted)

# Generate field names from extracted ecoregion keys
# TODO: if we keep all-zero fields, the initial reference
# should be the ecoregion polygon, not the extracted data
Expand Down
13 changes: 9 additions & 4 deletions R/process.R
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,8 @@ process_koppen_geiger <-
#' returning a single `SpatRaster` object.
#' @param path character giving nlcd data path
#' @param year numeric giving the year of NLCD data used
#' @param extent numeric(4) or SpatExtent giving the extent of the raster
#' if `NULL` (default), the entire raster is loaded
#' @param ... Placeholders.
#' @description Reads NLCD file of selected `year`.
#' @returns a `SpatRaster` object
Expand All @@ -695,6 +697,7 @@ process_nlcd <-
function(
path = NULL,
year = 2021,
extent = NULL,
...
) {
# check inputs
Expand All @@ -717,7 +720,7 @@ process_nlcd <-
if (length(nlcd_file) == 0) {
stop("NLCD data not available for this year.")
}
nlcd <- terra::rast(nlcd_file)
nlcd <- terra::rast(nlcd_file, win = extent)
terra::metags(nlcd) <- c(year = year)
return(nlcd)
}
Expand Down Expand Up @@ -752,9 +755,11 @@ process_ecoregion <-
ecoreg <- ecoreg[, grepl("^(L2_KEY|L3_KEY)", names(ecoreg))]
ecoreg_edit_idx <- sf::st_intersects(ecoreg, poly_tukey, sparse = FALSE)
ecoreg_edit_idx <- vapply(ecoreg_edit_idx, function(x) any(x), logical(1))
ecoreg_else <- ecoreg[!ecoreg_edit_idx, ]
ecoreg_edit <- sf::st_union(ecoreg[ecoreg_edit_idx, ], poly_tukey)
ecoreg <- rbind(ecoreg_else, ecoreg_edit)
if (!all(ecoreg_edit_idx == 0)) {
ecoreg_else <- ecoreg[!ecoreg_edit_idx, ]
ecoreg_edit <- sf::st_union(ecoreg[ecoreg_edit_idx, ], poly_tukey)
ecoreg <- rbind(ecoreg_else, ecoreg_edit)
}
ecoreg$time <- paste0(
"1997 - ", data.table::year(Sys.time())
)
Expand Down
31 changes: 16 additions & 15 deletions inst/extdata/nlcd_classes.csv
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
"","value","class","names","col"
"1",0,"TUNCL","Unclassified","white"
"1",0,"TUNCL","Unclassified","#ffffff00"
"2",11,"TWATR","Open Water","#476ba1"
"3",21,"TDVOS","Developed, Open Space","#decaca"
"4",22,"TDVLO","Developed, Low Intensity","#d99482"
"5",23,"TDVMI","Developed, Medium Intensity","#ee0000"
"6",24,"TDVHI","Developed, High Intensity","#ab0000"
"7",31,"TBARN","Barren Land","#b3aea3"
"8",41,"TDFOR","Deciduous Forest","#68ab63"
"9",42,"TEFOR","Evergreen Forest","#1c6330"
"10",43,"TMFOR","Mixed Forest","#b5ca8f"
"11",52,"TSHRB","Shrub/Scrub","#ccba7d"
"12",71,"THERB","Herbaceous","#e3e3c2"
"13",81,"TPAST","Hay/Pasture","#dcd93d"
"14",82,"TPLNT","Cultivated Crops","#ab7028"
"15",90,"TWDWT","Woody Wetlands","#bad9eb"
"16",95,"THWEM","Emergent Herbaceous Wetlands","#70a3ba"
"3",12,"TSNOW","Perennial Snow/Ice","#F5F5F5"
"4",21,"TDVOS","Developed, Open Space","#decaca"
"5",22,"TDVLO","Developed, Low Intensity","#d99482"
"6",23,"TDVMI","Developed, Medium Intensity","#ee0000"
"7",24,"TDVHI","Developed, High Intensity","#ab0000"
"8",31,"TBARN","Barren Land","#b3aea3"
"9",41,"TDFOR","Deciduous Forest","#68ab63"
"10",42,"TEFOR","Evergreen Forest","#1c6330"
"11",43,"TMFOR","Mixed Forest","#b5ca8f"
"12",52,"TSHRB","Shrub/Scrub","#ccba7d"
"13",71,"THERB","Herbaceous","#e3e3c2"
"14",81,"TPAST","Hay/Pasture","#dcd93d"
"15",82,"TPLNT","Cultivated Crops","#ab7028"
"16",90,"TWDWT","Woody Wetlands","#bad9eb"
"17",95,"THWEM","Emergent Herbaceous Wetlands","#70a3ba"
18 changes: 15 additions & 3 deletions man/calc_nlcd.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion man/process_nlcd.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 24 additions & 8 deletions tests/testthat/test-calculate_covariates.R
Original file line number Diff line number Diff line change
Expand Up @@ -441,10 +441,10 @@ testthat::test_that("Check calc_nlcd works", {
withr::local_package("sf")
withr::local_options(list(sf_use_s2 = FALSE))

point_us1 <- cbind(lon = -114.7, lat = 38.9, id = 1)
point_us2 <- cbind(lon = -114, lat = 39, id = 2)
point_ak <- cbind(lon = -155.997, lat = 69.3884, id = 3) # alaska
point_fr <- cbind(lon = 2.957, lat = 43.976, id = 4) # france
point_us1 <- cbind(lon = -114.7, lat = 38.9, site_id = 1)
point_us2 <- cbind(lon = -114, lat = 39, site_id = 2)
point_ak <- cbind(lon = -155.997, lat = 69.3884, site_id = 3) # alaska
point_fr <- cbind(lon = 2.957, lat = 43.976, site_id = 4) # france
eg_data <- rbind(point_us1, point_us2, point_ak, point_fr) |>
as.data.frame() |>
terra::vect(crs = "EPSG:4326")
Expand Down Expand Up @@ -474,6 +474,22 @@ testthat::test_that("Check calc_nlcd works", {
radius = -3),
"radius has not a likely value."
)

# -- buf_radius has likely value
testthat::expect_no_error(
calc_nlcd(locs = eg_data,
from = nlcdras,
mode = "exact",
radius = 300)
)
testthat::expect_no_error(
calc_nlcd(locs = eg_data,
from = nlcdras,
mode = "terra",
radius = 300)
)


# -- year is numeric
testthat::expect_error(
process_nlcd(path = path_testdata, year = "2021"),
Expand All @@ -492,7 +508,7 @@ testthat::test_that("Check calc_nlcd works", {
)
testthat::expect_error(
calc_nlcd(locs = 12,
locs_id = "id",
locs_id = "site_id",
from = nlcdras)
)
testthat::expect_error(
Expand All @@ -519,14 +535,14 @@ testthat::test_that("Check calc_nlcd works", {
testthat::expect_no_error(
calc_nlcd(
locs = eg_data,
locs_id = "id",
locs_id = "site_id",
from = nlcdras,
radius = buf_radius
)
)
output <- calc_nlcd(
locs = eg_data,
locs_id = "id",
locs_id = "site_id",
radius = buf_radius,
from = nlcdras
)
Expand Down Expand Up @@ -556,7 +572,7 @@ testthat::test_that("Check calc_nlcd works", {
)
output_geom <- calc_nlcd(
locs = eg_data,
locs_id = "id",
locs_id = "site_id",
radius = buf_radius,
from = nlcdras,
geom = TRUE
Expand Down

0 comments on commit 21658ab

Please sign in to comment.