diff --git a/DESCRIPTION b/DESCRIPTION index 44e292c184d30e595190114c004182321f96bbf0..51ff5db59480603d0ce22c407baf585fe4952b8b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -12,7 +12,7 @@ BugReports: https://github.com/USGS-R/ncdfgeom/issues Imports: RNetCDF, ncmeta, sf, dplyr, methods, stars Depends: R (>= 3.5) -Suggests: testthat, knitr, rmarkdown, pkgdown, geoknife, ncdf4, jsonlite +Suggests: testthat, knitr, rmarkdown, pkgdown, geoknife, ncdf4, jsonlite, areal License: CC0 Encoding: UTF-8 VignetteBuilder: knitr diff --git a/NAMESPACE b/NAMESPACE index 6ace2e3fbead8fb518b70a3df7c881a1ce650244..bf2aed6dd492cd8b8e0d984dbaf62f3e8b648581 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +export(calculate_area_intersection_weights) export(create_cell_geometry) export(read_attribute_data) export(read_geometry) @@ -20,13 +21,17 @@ importFrom(RNetCDF,var.inq.nc) importFrom(RNetCDF,var.put.nc) importFrom(dplyr,filter) importFrom(dplyr,group_by) +importFrom(dplyr,mutate) +importFrom(dplyr,right_join) importFrom(dplyr,select) +importFrom(dplyr,ungroup) importFrom(methods,is) importFrom(ncmeta,nc_atts) importFrom(ncmeta,nc_axes) importFrom(ncmeta,nc_dim) importFrom(ncmeta,nc_meta) importFrom(sf,"st_crs<-") +importFrom(sf,st_area) importFrom(sf,st_as_sf) importFrom(sf,st_bbox) importFrom(sf,st_buffer) @@ -36,6 +41,7 @@ importFrom(sf,st_coordinates) importFrom(sf,st_crs) importFrom(sf,st_geometry) importFrom(sf,st_geometry_type) +importFrom(sf,st_intersection) importFrom(sf,st_join) importFrom(sf,st_linestring) importFrom(sf,st_multilinestring) diff --git a/R/calculate_area_intersection_weights.R b/R/calculate_area_intersection_weights.R new file mode 100644 index 0000000000000000000000000000000000000000..650d184c8a969500696a40f4c56479ef6f46036b --- /dev/null +++ b/R/calculate_area_intersection_weights.R @@ -0,0 +1,103 @@ +#' Area Weighted Intersection (areal implementation) +#' @description Returns the fractional percent of each +#' feature in x that is covered by each intersecting feature +#' in y. These can be used as the weights in an area-weighted +#' mean overlay analysis where x is the data source and area- +#' weighted means are being generated for the target, y. +#' +#' This function is a lightwieght wrapper around the functions +#' \link[areal]{aw_intersect} \link[areal]{aw_total} and \link[areal]{aw_weight} +#' from the \href{https://github.com/slu-openGIS/areal}{areal package}. +#' +#' @param x sf data.frame source features including one geometry column and one identifier column +#' @param y sf data.frame target features including one geometry column and one identifier column +#' @param allow_lonlat boolean If FALSE (the default) lon/lat target features are not allowed. +#' Intersections in lon/lat are generally not valid and problematic at the international date line. +#' @return data.frame containing fraction of each feature in x that is +#' covered by each feature in y. e.g. If a feature from x is entirely within a feature in y, +#' w will be 1. If a feature from x is 50% in one feature for y and 50% in another, there +#' will be two rows, one for each x/y pair of features with w = 0.5 in each. +#' +#' @examples +#' b1 = sf::st_polygon(list(rbind(c(-1,-1), c(1,-1), +#' c(1,1), c(-1,1), +#' c(-1,-1)))) +#' b2 = b1 + 2 +#' b3 = b1 + c(-0.2, 2) +#' b4 = b1 + c(2.2, 0) +#' b = sf::st_sfc(b1, b2, b3, b4) +#' a1 = b1 * 0.8 +#' a2 = a1 + c(1, 2) +#' a3 = a1 + c(-1, 2) +#' a = sf::st_sfc(a1,a2,a3) +#' plot(b, border = 'red') +#' plot(a, border = 'green', add = TRUE) +#' +#' sf::st_crs(b) <- sf::st_crs(a) <- sf::st_crs(5070) +#' +#' b <- sf::st_sf(b, data.frame(idb = c(1, 2, 3, 4))) +#' a <- sf::st_sf(a, data.frame(ida = c(1, 2, 3))) +#' +#' sf::st_agr(a) <- sf::st_agr(b) <- "constant" +#' +#' a_b <- calculate_area_intersection_weights(a, b) +#' b_a <- calculate_area_intersection_weights(b, a) +#' +#' @export +#' @importFrom sf st_intersection st_set_geometry st_area st_crs +#' @importFrom dplyr mutate group_by right_join select ungroup + +calculate_area_intersection_weights <- function(x, y, allow_lonlat = FALSE) { + + if(!requireNamespace("areal")) stop("areal package required for intersection weights") + + if (st_crs(x) != st_crs(y)) { + x <- st_transform(x, st_crs(y)) + } + + if(st_crs(y)$proj == "longlat" & !allow_lonlat) { + stop("Found lon/lat coordinates and allow_lonlat is FALSE.") + } + + # Standard evaluation is for chumps. + id_x <- names(x)[names(x) != attr(x, "sf_column")] + id_y <- names(y)[names(y) != attr(y, "sf_column")] + + # There is a bug in areal and this works around it. + geom_name_x <- attr(x, "sf_column") + attr(x, "sf_column") <- "geometry" + names(x)[which(names(x) == geom_name_x)] <- "geometry" + + geom_name_y <- attr(y, "sf_column") + attr(y, "sf_column") <- "geometry" + names(y)[which(names(y) == geom_name_y)] <- "geometry" + + if (length(id_x) != 1 | length(id_y) != 1) + stop("x and y must have one and only one non-geometry column") + + names(x)[names(x) == id_x] <- "varx" + names(y)[names(y) == id_y] <- "vary" + + int <- areal::aw_intersect(y, + source = x, + areaVar = "area") + int <- areal::aw_total(int, source = x, + id = "varx", + areaVar = "area", + totalVar = "totalArea", + type = "extensive", + weight = "total") + int <- areal::aw_weight(int, areaVar = "area", + totalVar = "totalArea", + areaWeight = "areaWeight") + + int <- right_join(st_set_geometry(int, NULL), st_set_geometry(x, NULL), by = "varx") + + int <- select(int, varx, vary, w = areaWeight) + + names(int) <- c(id_x, id_y, "w") + + return(dplyr::as_tibble(int)) +} + +varx <- vary <- w <- d <- poly_id <- areaWeight <- NULL diff --git a/man/calculate_area_intersection_weights.Rd b/man/calculate_area_intersection_weights.Rd new file mode 100644 index 0000000000000000000000000000000000000000..f93a1fa7049af8aba8915fc45f5b0254cfb32c28 --- /dev/null +++ b/man/calculate_area_intersection_weights.Rd @@ -0,0 +1,59 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/calculate_area_intersection_weights.R +\name{calculate_area_intersection_weights} +\alias{calculate_area_intersection_weights} +\title{Area Weighted Intersection (areal implementation)} +\usage{ +calculate_area_intersection_weights(x, y, allow_lonlat = FALSE) +} +\arguments{ +\item{x}{sf data.frame source features including one geometry column and one identifier column} + +\item{y}{sf data.frame target features including one geometry column and one identifier column} + +\item{allow_lonlat}{boolean If FALSE (the default) lon/lat target features are not allowed. +Intersections in lon/lat are generally not valid and problematic at the international date line.} +} +\value{ +data.frame containing fraction of each feature in x that is +covered by each feature in y. e.g. If a feature from x is entirely within a feature in y, +w will be 1. If a feature from x is 50% in one feature for y and 50% in another, there +will be two rows, one for each x/y pair of features with w = 0.5 in each. +} +\description{ +Returns the fractional percent of each +feature in x that is covered by each intersecting feature +in y. These can be used as the weights in an area-weighted +mean overlay analysis where x is the data source and area- +weighted means are being generated for the target, y. + +This function is a lightwieght wrapper around the functions +\link[areal]{aw_intersect} \link[areal]{aw_total} and \link[areal]{aw_weight} +from the \href{https://github.com/slu-openGIS/areal}{areal package}. +} +\examples{ +b1 = sf::st_polygon(list(rbind(c(-1,-1), c(1,-1), + c(1,1), c(-1,1), + c(-1,-1)))) +b2 = b1 + 2 +b3 = b1 + c(-0.2, 2) +b4 = b1 + c(2.2, 0) +b = sf::st_sfc(b1, b2, b3, b4) +a1 = b1 * 0.8 +a2 = a1 + c(1, 2) +a3 = a1 + c(-1, 2) +a = sf::st_sfc(a1,a2,a3) +plot(b, border = 'red') +plot(a, border = 'green', add = TRUE) + +sf::st_crs(b) <- sf::st_crs(a) <- sf::st_crs(5070) + +b <- sf::st_sf(b, data.frame(idb = c(1, 2, 3, 4))) +a <- sf::st_sf(a, data.frame(ida = c(1, 2, 3))) + +sf::st_agr(a) <- sf::st_agr(b) <- "constant" + +a_b <- calculate_area_intersection_weights(a, b) +b_a <- calculate_area_intersection_weights(b, a) + +} diff --git a/tests/testthat/test_area_weighted.R b/tests/testthat/test_area_weighted.R new file mode 100644 index 0000000000000000000000000000000000000000..98c1a250ab6e79b84822218fa92cb238d8e6bb36 --- /dev/null +++ b/tests/testthat/test_area_weighted.R @@ -0,0 +1,49 @@ +context("calculate_area_intersection_weights") + +test_that("area_intersection", { + + b1 <- sf::st_polygon(list(rbind(c(-1, -1), c(1, -1), + c(1, 1), c(-1, 1), + c(-1, -1)))) + b2 <- b1 + 2 + b3 <- b1 + c(-0.2, 2) + b4 <- b1 + c(2.2, 0) + b <- sf::st_sfc(b1, b2, b3, b4) + a1 <- b1 * 0.8 + a2 <- a1 + c(1, 2) + a3 <- a1 + c(-1, 2) + a <- sf::st_sfc(a1, a2, a3) + + sf::st_crs(b) <- sf::st_crs(a) <- sf::st_crs(5070) + + b <- sf::st_sf(b, data.frame(idb = c(1, 2, 3, 4))) + a <- sf::st_sf(a, data.frame(ida = c(1, 2, 3))) + + sf::st_agr(a) <- sf::st_agr(b) <- "constant" + + a_b <- calculate_area_intersection_weights(a, b) + b_a <- calculate_area_intersection_weights(b, a) + + expect_equal(as.numeric(a_b[1, ]), + c(1, 1, 1), info = "a1 is 100% covered by b1.") + expect_equal(as.numeric(a_b[2, ]), + c(2, 2, 0.5), info = "a2 is 50% covered by b2.") + expect_equal(as.numeric(a_b[3, ]), + c(2, 3, 0.375), info = "a2 is 37.5% covered by b3.") + expect_equal(as.numeric(a_b[4, ]), + c(3, 3, 0.625), info = "a3 is 62.5% covered by b3.") + + expect_equal(as.numeric(b_a[1, ]), + c(1, 1, 0.64), info = "b1 is 64% covered by a1") + expect_equal(as.numeric(b_a[2, ]), + c(2, 2, 0.32), info = "b2 is 32% covered by a2") + expect_equal(as.numeric(b_a[3, ]), + c(3, 2, 0.24), info = "b3 is 24% covered by a2") + expect_equal(as.numeric(b_a[4, ]), + c(3, 3, 0.4), info = "b3 is 40% covered by a3") + expect_equal(data.frame(b_a[5, ]), + data.frame(tibble::tibble(idb = 4, + ida = as.numeric(NA), + w = as.numeric(NA))), + info = "b4 is not covered") +})