#' Ecological Buffer Tool
#'xxx
#' Delineates ecologically-defined buffers around target conservation areas using a resistant kernel
#'
#' @param seeds a shapefile of points, lines, or polygons designating conservation
#' targets
#' @param bandwidth maximum distance (m) to spread through cells of resistance = 1.
#' @param seedid name of numeric ID column in seeds shapefile.
#' @param result name of result polygon shapefile of ecological buffers.
#' @param resultgrid name of result kernel grid (optional). If resultgrid is
#' supplied, a graduated kernel representation. of buffers will be created, useful
#' for exploring parameterization and visualizing kernel-creation process.
#' @param resist landscape resistance grid. Optional if landcover and resist.table
#' are used to assign resistance by landcover type. Resistance values should range
#' from 1 to infinity. Use resist.mult to invert positive grids.
#' @param resist.table optional tab-delimited text file. Defines resistance value
#' for each landcover class (ranging from 1 to infinity). Columns must include class
#' (numeric landcover class) and resist (resistance value); additional columns may
#' include landcover name for reference (highly recommended) and comments. Resistance
#' ranges from 1 to infinity. If both resist and resist.table are supplied, the
#' values from the resist raster will be used for values that are absent in the
#' table.
#' @param default.resist value to use for classes not included in table, or NULL to
#' throw error if any values in landcover are missing from table. Nodata cells
#' (outside of the landscape) always get a high resistance.
#' @param landcover landcover grid. May be used to designate resistance values with
#' resist.table; also used with screen option.
#' @param barrier grid with resistance values for barriers, used as a complement to
#' resist or resist.table, the maximum resistance of resist/resist.table will be used
#' for each cell. Use to supply values for aquatic barriers (bridges, culverts, and
#' dams), with nodata for other cells.
#' @param passage grid with resistance values for cells that provide passage, reducing
#' resistance, used as a complement to resist or resist.table. The minimum of
#' resist/resist.table and passage will be used for each cell. Use to supply terrestrial
#' passage over or under roads (bridges and wildlife passage structures), with nodata
#' for other cells.
#' @param clip a polygon shapefile to clip the analysis to. Use this when developing
#' parameters and testing to speed runs up immensely.
#' @param fullextent if TRUE, produces a result grid at the full extent of the
#' landscape. This runs more slowly, so use the default of FALSE unless you have a
#' good reason not to. If TRUE, all grids are clipped to the reference grid
#' (landcover if it exists, otherwise, reference grid) to enforce alignment.
#' @param resist.mult multiplier on resistance grid, such that such that resistance
#' values that originally range from 0 to n or 1 to n will range from 1 to approximately
#' n * resist.mult after multiplying. Use a negative multiplier to invert the resistances
#' when using a positive grid, where higher values denote lower resistance. The minimum
#' and maximum resistances are needed for this procedure. They are obtained from the entire
#' raster before any clipping. If you are running on tiled data, or data that otherwise
#' don't represent the entire range of resistance values, you need to supply the overall
#' minimum and maximum as the 2nd and 3rd elements. The default multiplier is 1, thus
#' resistance values are used directly (or 1 is added if the minimum is 0).
#' @param resist.table.mult multiplier on resistances from a table. See resist.mult.
#' @param barrier.mult multiplier on barrier resistances. See resist.mult.
#' @param passage.mult multiplier on passage resistances. See resist.mult.
#' @param save.resist specify a result TIFF to write the realized resistance grid to,
#' for assessing complex combinations of resistance sources.
#' @param path base path prepended to input and result names/paths. Inputs that
#' include a complete path (starting with / or a drive letter) don't use path. This
#' option helps keep the inputs cleaner, and makes for easy switching to different
#' sets of inputs and results.
#' @param simplify if TRUE, simplify result polygons; if FALSE, polygons exactly
#' match raster.
#' @param simplify.tolerance polygon smoothing parameter used if simplify = TRUE.
#' Larger values give simpler polygons.
#' @param density build kernels for every nth cell to speed things up. By default,
#' density = 1, and resistant kernels are built from each edge cell in seeds; a
#' larger value will decrease runtime (by density2) at the cost of precision. You can
#' get away with higher values for density when using larger bandwidths.
#' @param expand distance to expand seeds (m). Use with screen to apply lines to wide
#' streams, for example. If expanded seeds overlap, the overlapping area will be
#' arbitrarily assigned the seedid of one of the seeds.
#' @param screen limit seeds to these landcover classes (requires landcover). Use
#' this if seeds are sloppy, e.g. when designating stream cores from vector data that
#' don't correspond exactly to streams in landcover, or to exclude development
#' classes from conservation target polygons.
#' @param broccoli include entire watershed above point in stream if watershed area
#' at point is <= x km2 (requires flow, accumulation, and streams grids). Used to
#' include the entirety of small watersheds.
#' @param streams stream centerline grid, used with the broccoli option.
#' @param flow flow direction grid, used with broccoli option. Grid is a standard D8
#' grid.
#' @param accumulation flow accumulation grid, used with broccoli option.
#' @param verbose set to FALSE to suppress informational chatter.
#' @param timing display overall timing message if TRUE, and also intermediate timing
#' messages if 'all' (verbose must be TRUE too).
#'
#' @details
#' The ecological buffer tool **eco.buffer** is a stand-alone R package for delineating
#' ecologically-defined buffers around target conservation areas. Targeted areas are
#' designated by point, line, or polygon shapefiles. Buffers are based on resistant
#' kernels with flexible parameters to accommodate terrestrial or aquatic settings.
#' Landscape resistance can be defined by a landcover raster and a table of classes
#' and resistance values, directly from resistance rasters, or from a combination of
#' a resistance table and rasters. Results are a polygon shapefile of ecological
#' buffers and an optional geoTIFF raster of the resistant kernels.
#'
#' The buffer tool is based on resistant kernels (Compton et. al 2007), which have
#' been used in a number of conservation applications since 2003,
#' including estimating local and regional connectivity (McGarigal et al. 2018) and
#' building terrestrial and aquatic conservation cores in Designing Sustainable
#' Landscapes/Nature's Network (McGarigal et al. 2017); they have also been used in
#' TNC's Resilient Sites for Terrestrial Conservation and Massachusetts Natural
#' Heritage's Living Waters and BioMap 2.
#'
#' @section Notes:
#' 1. Resistance values must range from 1 to infinity. The spread value starts in
#' each focal cell (all edge cells of each seed) at bandwidth / cell size. At each
#' cell, the cell's resistance x multiplier is subtracted from the spread value. For
#' example, a bandwidth of 5000 m when the cell size is 30 m gives a spread value of
#' 166.67. The spread will stop once it has passed through cells with a cumulative
#' resistance * multiplier of 166.67. Resistances greater than or equal to this value
#' will stop the spread at a single cell, thus these cells act as complete barriers.
#' 2. Raster inputs may be either Arc grids or geoTIFFs (other formats will likely
#' work).
#' 3. The seeds shapefile may be singlepart or multipart.
#' 4. The seed id field (specified with the seedid option) will be preserved in the
#' resulting buffer polygon. When result buffers overlap, the id of the seed with a
#' shortest cost-distance to each point will be used.
#' 5. When using the CAPS landcover for terrestrial cores, make sure to set the
#' resistance of classes 60 (Bridge or culvert) and 61 (Dam) to the maximum road
#' resistance, as these classes interrupt roads, thus with a lower resistance, they
#' can allow a kernel to spread across roads where they occur.
#' 6. When using the CAPS landcover for aquatic cores, you may want to use the
#' Aquatic Barriers (abarriers) grid as a secondary resistance grid using the barriers
#' option, to assign resistance to each bridge or culvert and each dam based on estimates
#' of their aquatic passability.
#' 7. If both a resistance raster (resist option) and resistance table (resist.table and
#' landcover options) are supplied, the table is used for all classes in the table, and
#' the raster is used for any classes not in the table (default.resist will be ignored).
#' 8. When testing and developing parameters, runtime will be much faster if you
#' limit seeds to a relatively small geographic area, or use the clip option select a
#' small area of the landscape.
#' 9. All input grids will be snapped and clipped to the reference grid--the landcover,
#' if supplied, or else the resistance grid.
#' 10. When using the broccoli option with a flow accumulation grid in terms of cells
#' (as is the CAPS grid), convert to km^2 with 1e6 / cellsize ^ 2. For a 30 m grid, use
#' flow accumulation / 1111 to get km^2.
#' 11. Note that when using polygons as seeds, tiny or skinny polygons that cover
#' less than half of a cell may not be properly captured when converting seeds to
#' raster. eco.buffer ensures that at least the centroid (moved inside of the
#' polygon) will be captured, but long polygons that are narrower than a cell may be
#' represented by only a single cell or a disjunct series of cells. It's always good
#' practice to use the resultgrid option to save a raster version of the result, and
#' look at cells with a value of 1.0 to see where the seed polygons ended up in the
#' raster representation.
#'
#' @section References:
#' Compton, B.W., K. McGarigal, S.A. Cushman, and L.R. Gamble. 2007. A resistant-kernel
#' model of connectivity for amphibians that breed in vernal pools. Conservation
#' Biology 21:788-799. \doi{10.1111/j.1523-1739.2007.00674.x}.
#'
#' McGarigal, K., B.W. Compton, E.B. Plunkett, W.V. DeLuca, J. Grand, E. Ene, and
#' S.D. Jackson. 2018. A landscape index of ecological integrity to inform landscape
#' conservation. Landscape Ecology 33:1029-1048. \doi{10.1007/s10980-018-0653-9}.
#'
#' McGarigal K., B.W. Compton, E.B. Plunkett, W.V. DeLuca, and J. Grand. 2017.
#' Designing sustainable landscapes: landscape conservation design. Report to the
#' North Atlantic Conservation Cooperative, US Fish and Wildlife Service, Northeast
#' Region.
#' \url{http://landeco.umass.edu/web/lcc/dsl/technical/DSL_documentation_landscape_design.pdf}
#' @section Author:
#' Bradley W. Compton <bcompton@@umass.edu>
#' @export
# C++ code that does resistant kernels. Package source is at https://github.com/ethanplunkett/gridprocess
#' @import gridprocess
# GIS processing for raster data
#' @import raster
#' @import rgdal
#' @import rgeos
#' @import sp
#' @importFrom utils read.table
#' @examples
#' ### Set up temporary directory for examples
#' require(eco.buffer)
#' dp <- paste(shortPathName(system.file('exampledata', package='eco.buffer')), '/.', sep = '')
#' dir <- tempdir()
#' if(!file.exists(dir)) dir.create(dir)
#' file.copy(dp, dir, recursive=TRUE)
#' cat('Example data and results will be in', dir)
#'
#' ### 1. terrestrial kernels (creates test1.shp and testg1.tif)
#' eco.buffer('seed_points', bandwidth = 2000, landcover = 'capsland.tif',
#' resist.table = 'resistance.txt', result = 'test1', resultgrid = 'testg1.tif', path = dir)
#'
#' ### 2. WMA poly example (creates WMAtest2.shp)
#' eco.buffer('wma_seeds', 5000, landcover = 'capsland.tif', resist = 'iei.tif', resist.mult = -30,
#' resist.table = 'resist_dev.txt', result = 'WMAtest2', path = dir)
#'
#' ### 3. stream cores (creates stream_test3.tif)
#' eco.buffer('stream_seeds', bandwidth = 3000, landcover = 'capsland.tif',
#' resist.table = 'resist_streams.txt', default.resist = 999, barrier = 'abarriers.tif',
#' barrier.mult = 100, result = 'stream_test3', simplify = FALSE, path = dir)
# B. Compton, 2 Apr 2021-31 May 2021
'eco.buffer' <- function(seeds, bandwidth, seedid = 'Id', result = NULL, resultgrid = NULL, resist = NULL,
resist.table = NULL, default.resist = NULL, landcover = '',
barrier = NULL, passage = NULL, resist.mult = 1, resist.table.mult = 1,
barrier.mult = 1, passage.mult = 1, save.resist = NULL, path = '', density = 1,
expand = NULL, screen = NULL, broccoli = NULL, flow = NULL, accumulation = NULL,
streams = NULL, clip = NULL, fullextent = FALSE, simplify = TRUE,
simplify.tolerance = 30, verbose = TRUE, timing = FALSE) {
t0 <- proc.time()[3]
if(any(timing == 'all'))
timing <- c(TRUE, TRUE)
else {
if(length(timing) == 1)
timing <- c(timing, FALSE)
}
# Process file names and check for required files
if(!is.null(path))
cat('path = ', path, '\n', sep = '')
seeds <- check.file(seeds, path, 'Seeds shapefile', verbose, ext = 'shp', require = TRUE)
clip <- check.file(clip, path, 'Clip shapefile', verbose, ext = 'shp')
resist <- check.file(resist, path, 'Resistance grid', verbose)
resist.table <- check.file(resist.table, path, 'Resistance table', verbose)
landcover <- check.file(landcover, path, 'Landcover grid', verbose)
barrier <- check.file(barrier, path, 'Barrier grid', verbose)
passage <- check.file(passage, path, 'Passage grid', verbose)
streams <- check.file(streams, path, 'Streams grid', verbose)
flow <- check.file(flow, path, 'Flow grid', verbose)
accumulation <- check.file(accumulation, path, 'Flow accumulation grid', verbose)
result <- check.file(result, path, 'Result shapefile', verbose, ext = 'shp', result = TRUE)
resultgrid <- check.file(resultgrid, path, 'Result grid', verbose, ext = 'tif', result = TRUE)
save.resist <- check.file(save.resist, path, 'Saved resistance grid', verbose, ext = 'tif', result = TRUE)
# Check file dependencies
if(!is.null(resist.table) & is.null(landcover))
stop('When supplying resist.table, you must also supply landcover')
if(is.null(resist) & is.null(resist.table))
stop('You must supply either resist or resist.table (or both)')
if(is.null(result) & is.null(resultgrid) & is.null(save.resist))
stop('You must supply result, resultgrid, or both...or at least save.result')
if(!is.null(screen) & is.null(landcover))
stop('When screen option is supplied, you must also supply landcover')
if(!is.null(broccoli) & (is.null(flow) | is.null(accumulation) | is.null(streams)))
stop('When broccoli is being used, you must supply all three of flow, accumulation, and streams')
# ------------------------ READ AND CLIP DATA ------------------------
chatter(verbose, '\nReading seeds shapefile...')
seedshape <- suppressWarnings(readOGR(dsn = add.path(path, seeds), verbose = FALSE))
if(is.na(match(seedid, names(seedshape))))
stop('Seed ID column (seedid = \'', seedid, '\') not found in shapefile ', seeds)
if((class(seedshape)[1] == 'SpatialPointsDataFrame') & density != 1)
stop('When seeds are points, denisty must be 1 (otherwise some seeds may be lost)')
if(!any(is.na(suppressWarnings(as.numeric(seedshape[[seedid]])))))
seedshape[[seedid]] <- as.numeric(seedshape[[seedid]]) # force ID to numeric
else
stop('The field referred to by seedid (currently ',seedid, ') must be numeric')
# if expand option is included, buffer seeds
if(!is.null(expand) && expand > 0) {
q <- buffer(seedshape, expand, dissolve = FALSE) # buffer seeds
names(q)[1] <- seedid
q[[seedid]] <- seedshape[[seedid]] # and transfer seedid
seedshape <- q
}
if(!is.null(clip)) {
chatter(verbose, 'Reading clip shapefile...')
clipshape <- suppressWarnings(readOGR(dsn = add.path(path, clip), verbose = FALSE))
}
# Read resistance grid, if supplied to get reference
if(!is.null(resist)) {
chatter(verbose, 'Reading resistance grid...')
rg <- raster(add.path(path, resist))
ref <- rg
}
# Read landcover grid (optional) and get reference
if(!is.null(landcover)) {
chatter(verbose, 'Reading landcover grid...')
land <- raster(add.path(path, landcover))
ref <- land
}
# Clip grids to seeds + buffer for speed-up, unless fullextent, in which case we clip to reference grid
t <- proc.time()[3]
if(!is.null(clip)){ # if clip is set, use it
chatter(verbose, 'Clipping to clip box...')
shapes <- crop(seedshape, clipshape)
ref <- crop(ref, clipshape)
}
else if(!fullextent) { # otherwise, clip to seeds + bandwidth, unless fullextent, in which case clip to full reference grid
chatter(verbose, 'Clipping grids to match ', ifelse(fullextent, 'reference grid...', 'seeds shapefile...'))
q <- extent(seedshape)
q <- q + c(-bandwidth, bandwidth, -bandwidth, bandwidth)
ref <- crop(ref, q)
}
# clip all grids for alignment, to clip, seeds + bandwidth, or fullextent, depending
if(!is.null(resist)) {
resist.minmax <- minmax(rg)
r <- as.matrix(crop(rg, ref))
}
if(!is.null(landcover))
land <- as.matrix(crop(land, ref))
# read and clip barrier grid
if(!is.null(barrier)) {
barrg <- raster(barrier)
barrier.minmax <- minmax(barrg)
barrg <- as.matrix(crop(barrg, ref))
}
# read and clip passage grid
if(!is.null(passage)) {
passg <- raster(passage)
passage.minmax <- minmax(passg)
passg <- as.matrix(crop(passage, ref))
}
# Read and clip streams, flow, and accumulation
if(!is.null(broccoli)) {
streamg <- as.matrix(crop(raster(streams), ref))
flowg <- as.matrix(crop(raster(flow), ref))
accumg <- as.matrix(crop(raster(accumulation), ref))
}
chatter(timing[2] & verbose, ' Elapsed time = ', proc.time()[3] - t, ' s')
# ------------------------ BUILD RESISTANCE GRID ------------------------
chatter(verbose, 'Getting resistance values...')
t <- proc.time()[3]
# Get resistance from grid
if(!is.null(resist))
r <- rx <- apply.mult(r, resist.mult, resist.minmax) # apply multiplier and save bonus copy to combine with table
# Get resistances from table
if(!is.null(resist.table)) {
res.table <- read.table(resist.table, sep = '\t', header = TRUE)
resist.table.minmax <- minmax(res.table$resist)
res.table$resist <- apply.mult(res.table$resist, resist.table.mult, resist.table.minmax) # apply multiplier
r <- matrix(res.table$resist[match(land, res.table$class)], dim(land)[1], dim(land)[2], byrow = FALSE)
v <- sort(unique(as.vector(land))) # look for missing classes
v <- v[!v %in% res.table$class]
if(length(v) != 0 ) # if any landcover values missing from table,
if(!is.null(resist)) { # if we have a resistance grid,
r[land %in% v] <- rx[land %in% v] # use grid values where we have nothing from table
} else # else, use default resistance or give an error
if(is.null(default.resist)) {
stop('Classes in landcover not specified in resistance table with no default.resist: ', paste(v, collapse = ', '))
} else
r[land %in% v] <- default.resist # classes not listed get default.resist
}
r[is.na(r)] <- 1e6 # NA cells (e.g., outside of landscape) always get a huge resistance
# Get barrier and passage resistances
if(!is.null(barrier))
r <- pmax(r, apply.mult(barrg, barrier.mult, barrier.minmax), na.rm = TRUE)
if(!is.null(passage))
r <- pmin(r, apply.mult(passg, passage.mult, passage.minmax), na.rm = TRUE)
chatter(timing[2] & verbose, ' Elapsed time = ', proc.time()[3] - t, ' s')
# convert vector to grid
chatter(verbose, 'Converting shapefile to grid...')
t <- proc.time()[3]
z <- rasterize(seedshape, ref, field = seedid) + 1 # add 1 to preserve zero ids
# make sure we've captured tiny polygons by converting (inside) centroids too
q <- SpatialPointsDataFrame(gPointOnSurface(seedshape, byid = TRUE), seedshape@data)
q <- rasterize(q, ref, field = seedid) + 1 # add 1 again
z[] <- pmax(as.matrix(z), as.matrix(q), na.rm = TRUE)
if(all(is.na(as.matrix(z))))
stop('Empty seeds. Perhaps you were too heavy-handed with clip?')
# if screen, set seed cells in screened-out landcover classes to naught
if(!is.null(screen)) {
z <- z * t(land) %in% screen
z[z == 0] <- NA
}
# find edge cells--we'll build kernels only from these
x <- find.edges(z)
# convert to matrix with 0s instead of NAs. Keep z for spatial reference.
x <- as.matrix(x) # edge cells
x[is.na(x)] <- 0
w <- as.matrix(z)
w[is.na(w)] <- 0
chatter(timing[2] & verbose, ' Elapsed time = ', proc.time()[3] - t, ' s')
# ------------------------ BUILD RESISTANT KERNELS ------------------------
chatter(verbose, 'Building resistant kernels...')
t <- proc.time()[3]
h <- ceiling(bandwidth / res(z)[1]) # bandwidth in terms of cells
y <- as.matrix(!is.na(z)) * h # polygon insides
c <- seq(-h, h) # window index vector
for(i in 1:dim(x)[1]) # For each row in landscape,
if(i %% density == 0) # if not skipping this row
for(j in 1:dim(x)[2]) # For each column,
if((j %% density == 0) & (x[i, j] != 0)) { # if not skipping this column and we have a seed here,
k <- i + c # take square of data to work with, possibly clipped at edges
k <- k[e1 <- (k >= 1) & (k <= dim(x)[1])]
l <- j + c
l <- l[e2 <- (l >= 1) & (l <= dim(x)[2])]
q <- rawspread(r[k, l], h, match(i, k), match(j, l))
w[k, l] <- ifelse(q > y[k, l], (q != 0) * z[i, j, 1], w[k, l]) # seed id
y[k, l] <- pmax(y[k, l], q) # kernel values
}
y <- y / max(y) # rescale kernels 0 to 1
# ------------------------ BROCCOLI ------------------------
if(!is.null(broccoli)) { # if broccoli option, build broccoli watersheds
chatter(verbose, 'Expanding broccoli kernels...')
t <- proc.time()[3]
q <- broccoli.kernel(broccoli, y, w, streamg, flowg, accumg, res(z))
y <- q[[1]] # kernel
w <- q[[2]] # seed id
chatter(timing[2] & verbose, ' Elapsed time = ', proc.time()[3] - t, ' s')
}
y[y == 0] <- NA # kernel values
w[w == 0] <- NA # seed id
w <- w - 1 # recover zero ids
# ------------------------ SAVE RESULTS ------------------------
chatter(verbose, 'Saving results...')
if(!is.null(result)) {
z[] <- w
chatter(verbose, 'Raster to poly...')
t <- proc.time()[3]
p <- rasterToPolygons(z, dissolve = TRUE) # convert to polygon
chatter(timing[2] & verbose, ' Elapsed time = ', proc.time()[3] - t, ' s')
if(simplify) {
chatter(verbose, 'Simplifying...')
t <- proc.time()[3]
p <- gSimplify(p, tol = simplify.tolerance) # smooth
chatter(timing[2] & verbose, ' Elapsed time = ', proc.time()[3] - t, ' s')
}
write.shapefile(p, result, 'result', verbose, timing[2])
}
if(!is.null(resultgrid))
write.tiff(y, z, resultgrid, 'result kernel', verbose, timing[2])
if(!is.null(save.resist))
write.tiff(r, z, save.resist, 'saved resistance', verbose, timing[2])
chatter(timing[1] & verbose, ' Total elapsed time = ', proc.time()[3] - t0, ' s')
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.