Skip to content

Working with polygons

julia
using SpeciesDistributionToolkit
using CairoMakie
import GeoJSON

In this tutorial, we will clip a layer to a polygon (in GeoJSON format), then use the same polygon to filter GBIF records.

About coordinates

GeoJSON coordinates are expressed in WGS84. For this reason, any polygon is assumed to be in this CRS, and all operations will be done by projecting the layer coordinates to this CRS.

We provide a very lightweight tool that uses open street map to turn plain text queries into GeoJSON polygons:

julia
POL = SpeciesDistributionToolkit.openstreetmap("Yosemite National Park")
2D MultiPolygonwith 1 sub-geometries

Note that if these polygons are too big, the simplify and simplify! methods (which are not exported) can be used to reduce their complexity.

The next step is to get a layer, and so we will download the data about deciduous broadleaf trees from EarthEnv:

julia
provider = RasterData(EarthEnv, LandCover)
layer = SDMLayer(
    provider;
    layer = "Shrubs",
    SpeciesDistributionToolkit.boundingbox(POL; padding=1.0)...
)
🗺️  A 324 × 324 layer with 104976 UInt8 cells
   Projection: +proj=longlat +datum=WGS84 +no_defs

We can check that this polygon is larger than the area we want:

Code for the figure
julia
heatmap(layer; colormap = :Greens, axis = (; aspect = DataAspect()))
lines!(POL, color=:red)

We can now mask this layer according to the polygon. This uses the same mask! method we use when masking with another layer:

julia
mask!(layer, POL)
🗺️  A 324 × 324 layer with 4451 UInt8 cells
   Projection: +proj=longlat +datum=WGS84 +no_defs

Code for the figure
julia
heatmap(layer; colormap = :Greens, axis = (; aspect = DataAspect()))
lines!(POL, color=:black)

This is a much larger layer than we need! For this reason, we will trim it so that the empty areas are removed:

Code for the figure
julia
heatmap(
    trim(layer);
    colormap = :Greens,
    axis = (; aspect = DataAspect()),
)
lines!(POL, color=:black)

Let's now get some occurrences in the area defined by the layer boundingbox:

julia
sp = taxon("Ursus americanus")
presences = occurrences(
    sp,
    trim(layer),
    "occurrenceStatus" => "PRESENT",
    "limit" => 300,
)
while length(presences) < count(presences)
    occurrences!(presences)
end
Occurrences from a layer

The GBIF.occurrences method can accept a layer as its second argument, to limit to the occurrence of a species within the bounding box of this layer. If a layer is used as the sole argument, all occurrences in the bounding box are queried.

We can plot the layer and the occurrences we have retrieved so far:

Code for the figure
julia
heatmap(
    trim(layer);
    colormap = :Greens,
    axis = (; aspect = DataAspect()),
)
scatter!(presences; color = :orange, markersize = 4)
lines!(POL, color=:black)

Some of these occurrences are outside of the masked region in the layer. For this reason, we will use the non-mutating mask method on the GBIF records:

Code for the figure
julia
f, ax, plt = heatmap(
    trim(layer);
    colormap = :Greens,
    axis = (; aspect = DataAspect()),
)
scatter!(mask(presences, POL); color = :orange, markersize = 4)
lines!(POL, color=:black)
hidespines!(ax)
hidedecorations!(ax)
A note about vectors of occurrences

The reason why mask called on a GBIF result is not mutating the result is that GBIF results also store the query that was used. For this reason, it makes little sense to modify this object. The non-mutating mask returns a vector of GBIF records, which for most purposes can be used in-place of the result - this is because GBIF records are AbstractOccurrence, and vectors of these are handled correctly by the package.