Title: | Road Network Projection |
---|---|
Description: | Iterative least cost path and minimum spanning tree methods for projecting forest road networks. The methods connect a set of target points to an existing road network using 'igraph' <https://igraph.org> to identify least cost routes. The cost of constructing a road segment between adjacent pixels is determined by a user supplied weight raster and a weight function; options include the average of adjacent weight raster values, and a function of the elevation differences between adjacent cells that penalizes steep grades. These road network projection methods are intended for integration into R workflows and modelling frameworks used for forecasting forest change, and can be applied over multiple time-steps without rebuilding a graph at each time-step. |
Authors: | Sarah Endicott [aut, cre] , Kyle Lochhead [aut], Josie Hughes [aut], Patrick Kirby [aut], Her Majesty the Queen in Right of Canada as represented by the Minister of the Environment [cph] (Copyright holder for included functions buildSimList, getLandingsFromTarget, pathsToLines, plotRoads, projectRoads, rasterizeLine, rasterToLineSegments), Province of British Columbia [cph] (Copyright holder for included functions getGraph, lcpList, mstList, shortestPaths, getClosestRoad, buildSnapRoads) |
Maintainer: | Sarah Endicott <[email protected]> |
License: | Apache License (>= 2) |
Version: | 1.2.0.9000 |
Built: | 2024-10-25 05:18:13 UTC |
Source: | https://github.com/landscitech/roads |
From Kyle Lochhead and Tyler Muhly's CLUS road simulation example. SpatRaster
files created with the terra package must be saved with terra::wrap()
and
need to be unwrapped before they are used. prepExData()
does this.
data(CLUSexample)
data(CLUSexample)
A named list with components:
cost: a PackedSpatRaster
representing road building cost.
landings: an sf dataframe of points representing landing locations.
roads: a PackedSpatRaster
representing existing roads.
CLUSexample prepExData(CLUSexample)
CLUSexample prepExData(CLUSexample)
A list containing two rasters covering an area near Revelstoke, British
Columbia, Canada. ex_elev
is elevation data and ex_wat
is the proportion
of the cell that contains water. Both are subsets of data downloaded with the
geodata package at 30 arc seconds resolution.SpatRaster
files created with
the terra package must be saved with terra::wrap()
and need to be unwrapped
before they are used. prepExData()
does this.
data(dem_example)
data(dem_example)
A named list with components:
ex_elev: a PackedSpatRaster
of elevation.
ex_wat: a PackedSpatRaster
of proportion water.
Elevation data are primarily from Shuttle Radar Topography Mission (SRTM), specifically the hole-filled CGIAR-SRTM (90 m resolution) from https://srtm.csi.cgiar.org/.
Water data are derived from the ESA WorldCover data set at 0.3-seconds resolution. (License CC BY 4.0). See https://esa-worldcover.org/en for more information.
Zanaga, D., Van De Kerchove, R., De Keersmaecker, W., Souverijns, N., Brockmann, C., Quast, R., Wevers, J., Grosu, A., Paccini, A., Vergnaud, S., Cartus, O., Santoro, M., Fritz, S., Georgieva, I., Lesiv, M., Carter, S., Herold, M., Li, Linlin, Tsendbazar, N.E., Ramoino, F., Arino, O., 2021. ESA WorldCover 10 m 2020 v100. doi:10.5281/zenodo.5571936.
dem_example prepExData(dem_example)
dem_example prepExData(dem_example)
A demonstration set of scenarios that can be used as input to
projectRoads()
. The data contains SpatRaster
objects that
must be wrapped to be stored. To unwrap them use prepExData()
data(demoScen)
data(demoScen)
A list of sub-lists, with each sub-list representing an input scenario. The scenarios (sub-lists) each contain the following components:
scen.number: An integer value representing the scenario number (generated scenarios are numbered incrementally from 1).
road.rast: A logical PackedSpatRaster
representing existing roads. TRUE is existing road. FALSE is not existing road.
road.line: A sf object representing existing roads.
cost.rast: A PackedSpatRaster
representing the cost of developing new roads on a given cell.
landings.points: A sf object representing landings sets and landing locations within each set. The data frame includes a field named 'set' which contains integer values representing the landings set that each point belongs to
landings.stack: A PackedSpatRaster
with multiple layers representing the landings and landings sets. Each logical layer represents
one landings set. Values of TRUE are a landing in the given set. Values of FALSE are not.
landings.poly: A sf object representing a single set of polygonal landings.
projectRoads
demoScen[[1]] demoScen <- prepExData(demoScen) demoScen[[1]]
demoScen[[1]] demoScen <- prepExData(demoScen) demoScen[[1]]
Generate landing points inside polygons representing harvested area. There
are three different sampling types available: "centroid"
(default) returns
the centroid or a point inside the polygon if the
centroid is not (see sf::st_point_on_surface()
); "random"
returns a
random sample given landingDens
see
(sf::st_sample()
); "regular"
returns points on a regular grid with cell size sqrt(1/landingDens)
that intersect the polygon, or centroid if no grid points fall within the polygon.
getLandingsFromTarget(harvest, landingDens = NULL, sampleType = "centroid")
getLandingsFromTarget(harvest, landingDens = NULL, sampleType = "centroid")
harvest |
|
landingDens |
number of landings per unit area. This should be in the same units as the CRS of the harvest. Note that 0.001 points per m2 is > 1000 points per km2 so this number is usually very small for projected CRS. |
sampleType |
character. |
Note that the landingDens
is points per unit area where the unit of
area is determined by the CRS. For projected CRS this should likely be a very
small number i.e. < 0.001.
an sf simple feature collection with an ID
column and POINT
geometry
doPlots <- interactive() demoScen <- prepExData(demoScen) polys <- demoScen[[1]]$landings.poly[1:2,] # Get centroid outCent <- getLandingsFromTarget(polys) if(doPlots){ plot(sf::st_geometry(polys)) plot(outCent, col = "red", add = TRUE) } # Get random sample with density 0.1 points per unit area outRand <- getLandingsFromTarget(polys, 0.1, sampleType = "random") if(doPlots){ plot(sf::st_geometry(polys)) plot(outRand, col = "red", add = TRUE) } # Get regular sample with density 0.1 points per unit area outReg <- getLandingsFromTarget(polys, 0.1, sampleType = "regular") if(doPlots){ plot(sf::st_geometry(polys)) plot(outReg, col = "red", add = TRUE) }
doPlots <- interactive() demoScen <- prepExData(demoScen) polys <- demoScen[[1]]$landings.poly[1:2,] # Get centroid outCent <- getLandingsFromTarget(polys) if(doPlots){ plot(sf::st_geometry(polys)) plot(outCent, col = "red", add = TRUE) } # Get random sample with density 0.1 points per unit area outRand <- getLandingsFromTarget(polys, 0.1, sampleType = "random") if(doPlots){ plot(sf::st_geometry(polys)) plot(outRand, col = "red", add = TRUE) } # Get regular sample with density 0.1 points per unit area outReg <- getLandingsFromTarget(polys, 0.1, sampleType = "regular") if(doPlots){ plot(sf::st_geometry(polys)) plot(outReg, col = "red", add = TRUE) }
Method for calculating the weight of an edge between two nodes from the value
of the input raster at each of those nodes (x1
and x2
), designed for a single
DEM input. The method assumes an input weightRaster
in which:
NA
indicates a road cannot be built
Negative values are costs for crossing streams or other barriers that are crossable but expensive. Edges that link to barrier penalty (negative value) nodes are assigned the largest barrier penalty weight.
Zero values are assumed to be existing roads.
All other values are interpreted as elevation in the units of the raster map (so that a difference between two cells equal to the map resolution can be interpreted as 100% grade) This is a simplified version of the grade penalty approach taken by Anderson and Nelson (2004): The approach does not distinguish between adverse and favourable grades. Default construction cost values are from the BC interior appraisal manual. The approach ignores (unknown) grade penalties beside roads and barriers in order to avoid increased memory and computational burden associated with multiple input rasters.
gradePenaltyFn( x1, x2, hdistance, baseCost = 16178, limit = 20, penalty = 504, limitWeight = NA )
gradePenaltyFn( x1, x2, hdistance, baseCost = 16178, limit = 20, penalty = 504, limitWeight = NA )
x1 , x2
|
Number. Value of the input raster at two nodes. |
hdistance |
Number. Horizontal distance between nodes. |
baseCost |
Number. Construction cost of 0% grade road per km. |
limit |
Number. Maximum grade (%) on which roads can be built. |
penalty |
Number. Cost increase (per km) associated with each additional % increase in road grade. |
limitWeight |
Number. Value assigned to edges that exceed the grade
limit. Try setting to a high (not |
Anderson AE, Nelson J (2004) Projecting vector-based road networks with a shortest path algorithm. Canadian Journal of Forest Research 34:1444–1457. https://doi.org/10.1139/x04-030
gradePenaltyFn(0.5,0.51,1) gradePenaltyFn(0.5,0.65,1) # grade > 20% so NA gradePenaltyFn(0.5,0.75,1)
gradePenaltyFn(0.5,0.51,1) gradePenaltyFn(0.5,0.65,1) # grade > 20% so NA gradePenaltyFn(0.5,0.75,1)
Plot the results of projectRoads()
plotRoads(sim, mainTitle, subTitle = paste0("Method: ", sim$roadMethod), ...)
plotRoads(sim, mainTitle, subTitle = paste0("Method: ", sim$roadMethod), ...)
sim |
|
mainTitle |
character. A title for the plot |
subTitle |
character. A sub title for the plot, by default the |
... |
Other arguments passed to raster plot call for the |
Creates a plot using base graphics
CLUSexample <- prepExData(CLUSexample) prRes <- projectRoads(CLUSexample$landings, CLUSexample$cost, CLUSexample$roads) if(interactive()){ plotRoads(prRes, "Title") }
CLUSexample <- prepExData(CLUSexample) prRes <- projectRoads(CLUSexample$landings, CLUSexample$cost, CLUSexample$roads) if(interactive()){ plotRoads(prRes, "Title") }
Prepare example data included in the package that contain wrapped terra
objects. This applies terra::unwrap()
recursively to the list provided so
that all PackedSpatRasters
are converted to SpatRasters
.
prepExData(x)
prepExData(x)
x |
list. Contains elements some of which are packed |
The same list but with unwrapped SpatRasters
CLUSexample prepExData(CLUSexample)
CLUSexample prepExData(CLUSexample)
Project a road network that links target landings to existing roads. For all
methods except "snap"
, a weightRaster
and weightFunction
together
determine the cost to build a road between two adjacent raster cells.
projectRoads( landings = NULL, weightRaster = NULL, roads = NULL, roadMethod = "ilcp", plotRoads = FALSE, mainTitle = "", neighbourhood = "octagon", weightFunction = simpleCostFn, sim = NULL, roadsOut = NULL, roadsInWeight = TRUE, ordering = "closest", roadsConnected = FALSE, ... ) ## S4 method for signature 'ANY,ANY,ANY,ANY,ANY,ANY,ANY,ANY,missing' projectRoads( landings = NULL, weightRaster = NULL, roads = NULL, roadMethod = "ilcp", plotRoads = FALSE, mainTitle = "", neighbourhood = "octagon", weightFunction = simpleCostFn, sim = NULL, roadsOut = NULL, roadsInWeight = TRUE, ordering = "closest", roadsConnected = FALSE, ... ) ## S4 method for signature 'ANY,ANY,ANY,ANY,ANY,ANY,ANY,ANY,list' projectRoads( landings = NULL, weightRaster = NULL, roads = NULL, roadMethod = "ilcp", plotRoads = FALSE, mainTitle = "", neighbourhood = "octagon", weightFunction = simpleCostFn, sim = NULL, roadsOut = NULL, roadsInWeight = TRUE, ordering = "closest", roadsConnected = FALSE, ... )
projectRoads( landings = NULL, weightRaster = NULL, roads = NULL, roadMethod = "ilcp", plotRoads = FALSE, mainTitle = "", neighbourhood = "octagon", weightFunction = simpleCostFn, sim = NULL, roadsOut = NULL, roadsInWeight = TRUE, ordering = "closest", roadsConnected = FALSE, ... ) ## S4 method for signature 'ANY,ANY,ANY,ANY,ANY,ANY,ANY,ANY,missing' projectRoads( landings = NULL, weightRaster = NULL, roads = NULL, roadMethod = "ilcp", plotRoads = FALSE, mainTitle = "", neighbourhood = "octagon", weightFunction = simpleCostFn, sim = NULL, roadsOut = NULL, roadsInWeight = TRUE, ordering = "closest", roadsConnected = FALSE, ... ) ## S4 method for signature 'ANY,ANY,ANY,ANY,ANY,ANY,ANY,ANY,list' projectRoads( landings = NULL, weightRaster = NULL, roads = NULL, roadMethod = "ilcp", plotRoads = FALSE, mainTitle = "", neighbourhood = "octagon", weightFunction = simpleCostFn, sim = NULL, roadsOut = NULL, roadsInWeight = TRUE, ordering = "closest", roadsConnected = FALSE, ... )
landings |
sf polygons or points, |
weightRaster |
|
roads |
sf lines, |
roadMethod |
Character. Options are |
plotRoads |
Boolean. Should the resulting road network be plotted.
Default |
mainTitle |
Character. A title for the plot. |
neighbourhood |
Character. |
weightFunction |
function. Method for calculating the weight of an edge
between two nodes from the value of the |
sim |
list. Returned from a previous iteration of |
roadsOut |
Character. Either |
roadsInWeight |
Logical. If |
ordering |
character. The order in which landings are processed when
|
roadsConnected |
Logical. Are all roads fully connected? If |
... |
Optional additional arguments to |
Four road network projection methods are:
"lcp"
: The Least Cost Path method connects each landing to the closest
road with a least cost path, without reference to other landings.
"ilcp"
: The Iterative Least Cost Path method iteratively connects each
landing to the closest road with a least cost path, so that the path to
each successive landing can include roads constructed to access previous
landings. The sequence of landings is determined by ordering
and is
"closest" by default. The alternative "none" option processes landings in
the order supplied by the user.
"mst"
: The Minimum Spanning Tree method connects landings to the existing road
with a minimum spanning tree that does not require users to specify the
order in which landings are processed.
"snap"
: Connects each landing to the closest (by Euclidean distance) road without,
reference to the weights or other landings.
a list with components:
roads: the projected road network, including new and input roads.
weightRaster: the updated weightRaster
in which new and old roads have value 0.
roadMethod: the road simulation method used.
landings: the landings used in the simulation.
g: the graph that describes the cost of paths between each cell in the updated
weightRaster
. Edges between vertices connected by new roads have weight 0.
g
can be used to avoid the cost of rebuilding the graph in a simulation
with multiple time steps.
CLUSexample <- prepExData(CLUSexample) doPlots <- interactive() projectRoads(CLUSexample$landings, CLUSexample$cost, CLUSexample$roads, "lcp", plotRoads = doPlots, mainTitle = "CLUSexample") # More realistic examples that take longer to run demoScen <- prepExData(demoScen) ### using: scenario 1 / sf landings / iterative least-cost path ("ilcp") # demo scenario 1 scen <- demoScen[[1]] # landing set 1 of scenario 1: land.pnts <- scen$landings.points[scen$landings.points$set==1,] prRes <- projectRoads(land.pnts, scen$cost.rast, scen$road.line, "ilcp", plotRoads = doPlots, mainTitle = "Scen 1: SPDF-LCP") ### using: scenario 1 / `SpatRaster` landings / minimum spanning tree ("mst") # demo scenario 1 scen <- demoScen[[1]] # the RasterLayer version of landing set 1 of scenario 1: land.rLyr <- scen$landings.stack[[1]] prRes <- projectRoads(land.rLyr, scen$cost.rast, scen$road.line, "mst", plotRoads = doPlots, mainTitle = "Scen 1: Raster-MST") ### using: scenario 2 / matrix landings raster roads / snapping ("snap") # demo scenario 2 scen <- demoScen[[2]] # landing set 5 of scenario 2, as matrix: land.mat <- scen$landings.points[scen$landings.points$set==5,] |> sf::st_coordinates() prRes <- projectRoads(land.mat, scen$cost.rast, scen$road.rast, "snap", plotRoads = doPlots, mainTitle = "Scen 2: Matrix-Snap") ## using scenario 7 / Polygon landings raster / minimum spanning tree # demo scenario 7 scen <- demoScen[[7]] # rasterize polygonal landings of demo scenario 7: land.polyR <- terra::rasterize(scen$landings.poly, scen$cost.rast) prRes <- projectRoads(land.polyR, scen$cost.rast, scen$road.rast, "mst", plotRoads = doPlots, mainTitle = "Scen 7: PolyRast-MST")
CLUSexample <- prepExData(CLUSexample) doPlots <- interactive() projectRoads(CLUSexample$landings, CLUSexample$cost, CLUSexample$roads, "lcp", plotRoads = doPlots, mainTitle = "CLUSexample") # More realistic examples that take longer to run demoScen <- prepExData(demoScen) ### using: scenario 1 / sf landings / iterative least-cost path ("ilcp") # demo scenario 1 scen <- demoScen[[1]] # landing set 1 of scenario 1: land.pnts <- scen$landings.points[scen$landings.points$set==1,] prRes <- projectRoads(land.pnts, scen$cost.rast, scen$road.line, "ilcp", plotRoads = doPlots, mainTitle = "Scen 1: SPDF-LCP") ### using: scenario 1 / `SpatRaster` landings / minimum spanning tree ("mst") # demo scenario 1 scen <- demoScen[[1]] # the RasterLayer version of landing set 1 of scenario 1: land.rLyr <- scen$landings.stack[[1]] prRes <- projectRoads(land.rLyr, scen$cost.rast, scen$road.line, "mst", plotRoads = doPlots, mainTitle = "Scen 1: Raster-MST") ### using: scenario 2 / matrix landings raster roads / snapping ("snap") # demo scenario 2 scen <- demoScen[[2]] # landing set 5 of scenario 2, as matrix: land.mat <- scen$landings.points[scen$landings.points$set==5,] |> sf::st_coordinates() prRes <- projectRoads(land.mat, scen$cost.rast, scen$road.rast, "snap", plotRoads = doPlots, mainTitle = "Scen 2: Matrix-Snap") ## using scenario 7 / Polygon landings raster / minimum spanning tree # demo scenario 7 scen <- demoScen[[7]] # rasterize polygonal landings of demo scenario 7: land.polyR <- terra::rasterize(scen$landings.poly, scen$cost.rast) prRes <- projectRoads(land.polyR, scen$cost.rast, scen$road.rast, "mst", plotRoads = doPlots, mainTitle = "Scen 7: PolyRast-MST")
Converts rasters that represent lines into an sf object.
rasterToLineSegments(rast, method = "mst")
rasterToLineSegments(rast, method = "mst")
rast |
|
method |
character. Method of building lines. Options are |
For method = "nearest"
raster is first converted to points and then
lines are drawn between the nearest points. If there are two different ways
to connect the points that have the same distance both are kept which can
cause doubled lines. USE WITH CAUTION. method = "mst"
converts the
raster to points, reclassifies the raster so roads are 0 and other cells are
1 and then uses projectRoads
to connect all the points with a minimum
spanning tree. This will always connect all raster cells and is slower but
will not double lines as often. Neither method is likely to work for very
large rasters
an sf simple feature collection
CLUSexample <- prepExData(CLUSexample) # works well for very simple roads roadLine1 <- rasterToLineSegments(CLUSexample$roads) # longer running more realistic examples demoScen <- prepExData(demoScen) # mst method works well in this case roadLine2 <- rasterToLineSegments(demoScen[[1]]$road.rast) # nearest method has doubled line where the two roads meet roadLine3 <- rasterToLineSegments(demoScen[[1]]$road.rast, method = "nearest") # The mst method can also produce odd results in some cases roadLine4 <- rasterToLineSegments(demoScen[[4]]$road.rast)
CLUSexample <- prepExData(CLUSexample) # works well for very simple roads roadLine1 <- rasterToLineSegments(CLUSexample$roads) # longer running more realistic examples demoScen <- prepExData(demoScen) # mst method works well in this case roadLine2 <- rasterToLineSegments(demoScen[[1]]$road.rast) # nearest method has doubled line where the two roads meet roadLine3 <- rasterToLineSegments(demoScen[[1]]$road.rast, method = "nearest") # The mst method can also produce odd results in some cases roadLine4 <- rasterToLineSegments(demoScen[[4]]$road.rast)
Calculates the weight of an edge between two nodes as the mean value
of an input cost raster at each of those nodes (x1
and x2
).
simpleCostFn(x1, x2, hdistance)
simpleCostFn(x1, x2, hdistance)
x1 , x2
|
Number. Value of the input cost raster at two nodes. |
hdistance |
Number. Horizontal distance between the nodes - for penalizing longer diagonal edges. |
simpleCostFn(0.5,0.7,1)
simpleCostFn(0.5,0.7,1)