Integration with SimpleSDMLayers

Below is an example using the various functions within SpatialBoundaries to estimate boundaries for (i.e. patches of) wooded areas on the Southwestern islands of the Hawaiian Islands using landcover data from the EarthEnv project [@Tuanmu2014Glo1km] as well as integrating some functionality from SimpleSDMLayers [@Dansereau2021SimJl] for easier work with the spatial nature of the input data. The SpatialBoundaries package works really well with SimpleSDMLayers, so that you can (i) apply wombling and boundaries finding to a SimpleSDMLayer object, and (ii) convert the output of a Womble object to a pair of SimpleSDMLayer corresponding to the rate and direction of change.

Because there are four different layers in the EarthEnv database that represent different types of woody cover we will use the overall mean wombling value. As the data are arranged in a matrix i.e. a lattice this example will focus on lattice wombling, however for triangulation wombling the implementation of functions and workflow would look similar with the exception that the input data would be structured differently (as three vectors of $x$, $y$, $z$) and the output data would be typed as TriangulationWomble objects.

using SpatialBoundaries
using SpeciesDistributionToolkit
using CairoMakie
import Plots # Required for radial histograms

First we can start by defining the extent of the Southwestern islands of Hawaii, which can be used to restrict the extraction of the various landcover layers from the EarthEnv database. We do the actual layers querying using SimpleSDMLayers:

hawaii = (left = -160.2, right = -154.5, bottom = 18.6, top = 22.5)
dataprovider = RasterData(EarthEnv, LandCover)
landcover_classes = SimpleSDMDatasets.layers(dataprovider)
landcover = [SimpleSDMPredictor(dataprovider; layer=class, full=true, hawaii...) for class in landcover_classes]
12-element Vector{SimpleSDMLayers.SimpleSDMPredictor{UInt8}}:
 SDM predictor → 468×684 grid with 320112 UInt8-valued cells
 SDM predictor → 468×684 grid with 320112 UInt8-valued cells
 SDM predictor → 468×684 grid with 320112 UInt8-valued cells
 SDM predictor → 468×684 grid with 320112 UInt8-valued cells
 SDM predictor → 468×684 grid with 320112 UInt8-valued cells
 SDM predictor → 468×684 grid with 320112 UInt8-valued cells
 SDM predictor → 468×684 grid with 320112 UInt8-valued cells
 SDM predictor → 468×684 grid with 320112 UInt8-valued cells
 SDM predictor → 468×684 grid with 320112 UInt8-valued cells
 SDM predictor → 468×684 grid with 320112 UInt8-valued cells
 SDM predictor → 468×684 grid with 320112 UInt8-valued cells
 SDM predictor → 468×684 grid with 320112 UInt8-valued cells

We can remove all the areas that contain 100% water from the landcover data as our question of interest is restricted to the terrestrial realm. We do this by using the "Open Water" layer to mask over each of the landcover layers individually:

ow_index = findfirst(isequal("Open Water"), landcover_classes)
not_water = landcover[ow_index] .!== 0x64
lc = [mask(not_water, layer) for layer in landcover]
12-element Vector{SimpleSDMLayers.SimpleSDMPredictor{UInt8}}:
 SDM predictor → 468×684 grid with 21764 UInt8-valued cells
 SDM predictor → 468×684 grid with 21764 UInt8-valued cells
 SDM predictor → 468×684 grid with 21764 UInt8-valued cells
 SDM predictor → 468×684 grid with 21764 UInt8-valued cells
 SDM predictor → 468×684 grid with 21764 UInt8-valued cells
 SDM predictor → 468×684 grid with 21764 UInt8-valued cells
 SDM predictor → 468×684 grid with 21764 UInt8-valued cells
 SDM predictor → 468×684 grid with 21764 UInt8-valued cells
 SDM predictor → 468×684 grid with 21764 UInt8-valued cells
 SDM predictor → 468×684 grid with 21764 UInt8-valued cells
 SDM predictor → 468×684 grid with 21764 UInt8-valued cells
 SDM predictor → 468×684 grid with 21764 UInt8-valued cells

As layers one through four of the EarthEnv data are concerned with data on woody cover (i.e. "Evergreen/Deciduous Needleleaf Trees", "Evergreen Broadleaf Trees", "Deciduous Broadleaf Trees", and "Mixed/Other Trees") we will work with only these layers. For a quick idea we of what the raw landcover data looks like we can sum these four layers and plot the total woody cover:

classes_with_trees = findall(contains.(landcover_classes, "Trees"))
tree_lc = convert(Float32, reduce(+, lc[classes_with_trees]))
heatmap(tree_lc; colormap=:linear_kbgyw_5_98_c62_n256)

Although we have previously summed the four landcover layers for the actual wombling part we will apply the wombling function to each layer before we calculate the overall mean wombling value. We will do this using broadcast, this will apply wombling in an element-wise fashion to the four different woody cover layers. This will give as a vector containing four LatticeWomble objects (since the input data was in the form of a matrix).

wombled_layers = wombling.(lc[classes_with_trees]);

As we are interested in overall woody cover for Southwestern islands we can take the wombled_layers vector and use them with the mean function to get the overall mean wombling value of the rate and direction of change for woody cover. This will 'flatten' the four wombled layers into a single LatticeWomble object.

wombled_mean = mean(wombled_layers);

From the wombled_mean object we can 'extract' the layers for both the mean rate and direction of change. For ease of plotting we will also convert these layers to SimpleSDMPredictor type objects. It is also possible to call these matrices directly from the wombled_mean object using wombled_mean.m or wombled_mean.θ for the rate and direction respectively.

rate, direction = SimpleSDMPredictor(wombled_mean)
(SDM predictor → 467×683 grid with 20759 Float64-valued cells, SDM predictor → 467×683 grid with 20759 Float64-valued cells)

Lastly we can identify candidate boundaries using the boundaries. Here we will use a thresholding value (t) of 0.1 and save these candidate boundary cells as b. Note that we are now working with a SimpleSDMResponse object and this is simply for ease of plotting.

b = similar(rate)
b.grid[boundaries(wombled_mean, 0.1; ignorezero = true)] .= 1.0
1735-element view(::Matrix{Union{Nothing, Float64}}, CartesianIndex{2}[CartesianIndex(418, 57), CartesianIndex(419, 57), CartesianIndex(418, 58), CartesianIndex(415, 59), CartesianIndex(419, 59), CartesianIndex(412, 60), CartesianIndex(415, 60), CartesianIndex(416, 60), CartesianIndex(419, 60), CartesianIndex(421, 60)  …  CartesianIndex(108, 644), CartesianIndex(109, 644), CartesianIndex(111, 644), CartesianIndex(112, 644), CartesianIndex(107, 645), CartesianIndex(108, 645), CartesianIndex(109, 645), CartesianIndex(111, 645), CartesianIndex(109, 646), CartesianIndex(110, 646)]) with eltype Union{Nothing, Float64}:
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 ⋮
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0
 1.0

We will overlay the identified boundaries (in green) over the rate of change (in levels of grey):

heatmap(rate, colormap=[:grey95, :grey5])
heatmap!(b, colormap=[:transparent, :green])
current_figure()

For this example we will plot the direction of change as radial plots to get an idea of the prominent direction of change. Here we will plot all the direction values from direction for which the rate of change is greater than zero (so as to avoid denoting directions for a slope that does not exist) as well as the direction values from only candidate cells using the same masking principle as what we did for the rate of change. It is of course also possible to forgo the radial plots and plot the direction of change in the same manner as the rate of change should one wish.

Before we plot let us create our two 'masked layers'. For all direction values for which there is a corresponding rate of change greater than zero we can use rate as a masking layer but first replace all zero values with 'nothing'. For the candidate boundary cells we can simply mask direction with b as we did for the rate of change.

direction_all = mask(replace(rate, 0 => nothing), direction)

direction_candidate = mask(b, direction)
SDM predictor → 467×683 grid with 1735 Float64-valued cells
  Latitudes	18.60833333333333 ⇢ 22.491666666666678
  Longitudes	-160.1916666666667 ⇢ -154.50833333333335

Because stephist() requires a vector of radians for plotting we must first collect the cells and convert them from degrees to radians. Then we can start by plotting the direction of change of all cells.

Plots.stephist(
         deg2rad.(values(direction_all));
         proj=:polar,
         lab="",
         c=:teal,
         nbins = 36,
         yshowaxis=false,
         normalize = false)

Followed by plotting the direction of change only for cells that are considered as candidate boundary cells.

Plots.stephist(
        deg2rad.(values(direction_candidate));
        proj=:polar,
        lab="",
        c=:red,
        nbins = 36,
        yshowaxis=false,
        normalize = false)

End

rm(joinpath(SimpleSDMLayers._layers_assets_path, "EarthEnv"); force=true, recursive=true) #hide