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 wrapper around the GADM database, which will return data as ready-to-use GeoJSON files.

julia
DEU = SpeciesDistributionToolkit.gadm("DEU")
FeatureCollection with 1 Features
More about GADM

The only other function to interact with GADM is gadmlist, which takes either a series of places, as in

julia
SpeciesDistributionToolkit.gadmlist("FRA", "Bretagne")
4-element Vector{String}:
 "Côtes-d'Armor"
 "Finistère"
 "Ille-et-Vilaine"
 "Morbihan"

or a level, to provide a list of places within a three-letter coded area at a specific depth:

julia
SpeciesDistributionToolkit.gadmlist("FRA", 3)[1:3]
3-element Vector{String}:
 "Belley"
 "Bourg-en-Bresse"
 "Gex"

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 = "Deciduous Broadleaf Trees",
    left = 2.0,
    right = 20.0,
    bottom = 45.0,
    top = 57.0,
)
SDM Layer with 3110400 UInt8 cells
	Proj string: +proj=longlat +datum=WGS84 +no_defs
	Grid size: (1440, 2160)

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

Code for the figure
julia
heatmap(layer; colormap = :linear_kbgyw_5_98_c62_n256, axis = (; aspect = DataAspect()))

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, DEU)
SDM Layer with 661690 UInt8 cells
	Proj string: +proj=longlat +datum=WGS84 +no_defs
	Grid size: (1440, 2160)

Code for the figure
julia
heatmap(layer; colormap = :linear_kbgyw_5_98_c62_n256, axis = (; aspect = DataAspect()))

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 = :linear_kbgyw_5_98_c62_n256,
    axis = (; aspect = DataAspect()),
)

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

julia
sp = taxon("Eliomys quercinus")
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 = :linear_kbgyw_5_98_c62_n256,
    axis = (; aspect = DataAspect()),
)
scatter!(presences; color = :orange, markersize = 4)

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 = :linear_kbgyw_5_98_c62_n256,
    axis = (; aspect = DataAspect()),
)
scatter!(mask(presences, DEU); color = :orange, markersize = 4)
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.