Skip to content
julia
import SphericalSpatialTrees as SST
import GeometryOps as GO, GeoInterface as GI
import DiskArrays

using GeometryOps.UnitSpherical: GeographicFromUnitSphere, spherical_distance,
                            UnitSphereFromGeographic, SphericalCap

using Rasters, RasterDataSources, ArchGDAL, Zarr # data sources
using GeoMakie, GLMakie # visualization
"/home/runner/.julia/artifacts/RasterDataSources"

nothing # hide

Now that we have loaded all of these packages, let's start talking about functionality.

First, let's use the top-level interface to create a lazily regridded array, from a lat-long source to a DGGS target.

This dataset is a bioclimatic dataset, which is sufficiently small that you can do this quickly on your machine.

julia
ras = Raster(EarthEnv{HabitatHeterogeneity}, :cv) |> r -> replace_missing(r, NaN) .|> log
1728×696 Raster{Float64, 2}
├─────────────────────────────┴────────────────────────────────────────── dims ┐
X Projected{Float64} -180.0:0.20833333333333334:179.79166666666669 ForwardOrdered Regular Intervals{Start},
Y Projected{Float64} 84.79166666666667:-0.20833333333333334:-60.0 ReverseOrdered Regular Intervals{Start}
├──────────────────────────────────────────────────────────────────── metadata ┤
  Metadata{Rasters.GDALsource} of Dict{String, Any} with 1 entry:
  "filepath" => "/home/runner/.julia/artifacts/RasterDataSources/EarthEnv/Habit…
├────────────────────────────────────────────────────────────────────── raster ┤
  missingval: NaN
  extent: Extent(X = (-180.0, 180.00000000000003), Y = (-60.0, 85.0))
  crs: GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.25722...
└──────────────────────────────────────────────────────────────────────────────┘
     84.7917   84.5833   84.375-59.375  -59.5833  -59.7917  -60.0
 -180.0    NaN       NaN       NaN         NaN      NaN       NaN       NaN
 -179.792  NaN       NaN       NaN         NaN      NaN       NaN       NaN
    ⋮                                   ⋱                       ⋮       
  179.583  NaN       NaN       NaN         NaN      NaN       NaN       NaN
  179.792  NaN       NaN       NaN         NaN      NaN       NaN       NaN

Let's give this dataset some chunks. It's 1728x696 so 75x75 chunks give it ~ 22x11 chunks.

julia
ras_chunked = DiskArrays.mockchunks(ras, (75, 75))
1728×696 Raster{Float64, 2}
├─────────────────────────────┴────────────────────────────────────────── dims ┐
X Projected{Float64} -180.0:0.20833333333333334:179.79166666666669 ForwardOrdered Regular Intervals{Start},
Y Projected{Float64} 84.79166666666667:-0.20833333333333334:-60.0 ReverseOrdered Regular Intervals{Start}
├──────────────────────────────────────────────────────────────────── metadata ┤
  Metadata{Rasters.GDALsource} of Dict{String, Any} with 1 entry:
  "filepath" => "/home/runner/.julia/artifacts/RasterDataSources/EarthEnv/Habit…
├────────────────────────────────────────────────────────────────────── raster ┤
  missingval: NaN
  extent: Extent(X = (-180.0, 180.00000000000003), Y = (-60.0, 85.0))
  crs: GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.25722...
└──────────────────────────────────────────────────────────────────────────────┘

Now, we need to define the source and target trees. The dataset itself is on a plane (equirectangular projection), so we can use the RegularGridTree which is an implicit quadtree.

julia
source = SST.ProjectionSource(SST.RegularGridTree, ras_chunked);

We want to go to a DGGS target, so we can use the ISEACircleTree (based on the Snyder equal area projection). This is the only tree so far but more can be added as we go forward.

julia
target = SST.ProjectionTarget(SST.ISEACircleTree, 8, 2)
ProjectionSource(655360-leaf ISEACircleTree(8))

As a sidenote, this is what actually happens internally. Using the spatial tree interface, we compute the map of chunks in the source dataset that each chunk in the target dataset requires to be materialized.

julia
SST.compute_connected_chunks(source, target)
160-element Vector{Vector{Int64}}:
 [65, 66, 89, 90, 113, 114, 67, 91, 115, 92]
 [90, 114, 91, 115, 92, 116, 138, 139, 140]
 [115, 116, 138, 139, 140, 141, 163, 164]
 [139, 140, 141, 163, 164, 165, 188, 189]
 [41, 42, 65, 66, 89, 90, 43, 44, 67, 68, 91, 92]
 [66, 90, 67, 68, 69, 91, 115, 92, 93, 116]
 [91, 115, 92, 93, 116, 117, 139, 140, 141]
 [116, 117, 118, 140, 141, 164, 165, 142]
 [16, 17, 18, 40, 41, 42, 65, 66, 19, 20, 21, 43, 44, 45, 67, 68]
 [42, 66, 43, 44, 45, 46, 67, 68, 69, 91, 92, 93, 70]

 [209, 210, 233, 234, 211, 235, 212, 213, 236, 237]
 [112, 113, 114, 136, 137, 138, 160, 161, 162]
 [137, 138, 161, 162, 185, 186, 139, 163, 187]
 [161, 162, 185, 186, 209, 210, 163, 187, 188, 211]
 [186, 209, 210, 234, 187, 188, 189, 211, 235, 212, 213, 236, 237]
 [89, 90, 113, 114, 115, 137, 138, 139]
 [114, 115, 137, 138, 162, 139, 163]
 [138, 162, 186, 139, 140, 163, 164, 187, 188]
 [162, 186, 163, 164, 165, 187, 188, 189, 211, 212]

Now, we can create the lazy projected disk array.

julia
a = SST.LazyProjectedDiskArray(source, target)
256×256×10 LazyProjectedDiskArray{Float64}

You can now access some data, using this as you would use any array:

julia
a[1:10,1:10,1]
10×10 Matrix{Float64}:
 7.3815   7.04491  7.29845  7.52456  …  8.79301  8.80072  8.71997  8.69148
 7.54803  7.28893  7.00941  7.03791     9.16785  9.04958  8.76639  8.33399
 8.0523   7.43897  7.24494  6.9256      8.07465  8.97246  8.94637  8.41183
 7.40123  7.60589  7.6029   7.18007     7.4055   7.83241  8.55005  8.72013
 6.50728  7.98888  7.27656  7.04491     7.41035  8.18897  8.97094  8.71325
 6.86797  7.35756  8.0037   7.257    …  7.38956  7.64252  8.19008  8.97525
 7.02376  6.82871  7.13569  7.59085     7.13807  7.48212  7.71869  8.12976
 7.44425  6.92264  6.85013  6.58203     7.51698  7.57661  7.68064  7.85941
 7.44425  7.13966  7.0193   6.80793     7.59035  7.03351  7.29777  7.6019
 7.20564  7.50384  7.10168  7.11314     6.98008  7.49165  6.89264  7.02554

You can also materialize the array fully into memory (if the array is small enough):

julia
ac = collect(a);

Now let's visualize this! GLMakie has no problem visualizing this many polygons: First, let's get every polygon:

julia
polys = SST.index_to_polygon_lonlat.(vec(collect(eachindex(a))), (target.tree,));

Then we can just assign the correct color to the correct polygon, and plot on GeoMakie's GlobeAxis.

julia
fig, ax, p1 = poly(a; axis = (; type = GlobeAxis))
p2 = lines!(ax, a; color = :black, transparency = true, linewidth = 0.075)
p3 = meshimage!(ax, -180..180, -90..90, fill(colorant"white", 2, 2); zlevel = -100_000)
fig

Let's also see the original data:

julia
meshimage(-180..180, -90..90, reorder(ras, Y => Rasters.ForwardOrdered()); axis = (; type = GlobeAxis))

To give a better idea of what this looks like, let's use a lower level (lower resolution) DGGS:

julia
target = SST.ProjectionTarget(SST.ISEACircleTree, 3, 2)
a = SST.LazyProjectedDiskArray(source, target)
ac = mapreduce((i,j)->cat(i,j,dims=3),1:10) do n
    a[:,:,n]
end
fig, ax, plt = poly(a; strokewidth = 1, strokecolor = :black, axis = (; type = GlobeAxis));
meshimage!(ax, -180..180, -90..90, fill(colorant"white", 2, 2); zlevel = -100_000) # background plot
fig

fig2, ax2, plt2 = meshimage(-180..180, -90..90, reorder(ras, Y => Rasters.ForwardOrdered()); zlevel = 100_000, alpha = 0.5, transparency = true, axis = (; type = GlobeAxis, show_axis = false))
poly!(ax2, a; strokewidth = 1, strokecolor = :black)
meshimage!(ax2, -180..180, -90..90, fill(colorant"white", 2, 2); zlevel = -100_000) # background plot
Makie.Plot{GeoMakie.meshimage, Tuple{Makie.EndPoints{Float64}, Makie.EndPoints{Float64}, Matrix{ColorTypes.RGB{FixedPointNumbers.N0f8}}}}

And that's the basics! Now we can go a bit into the weeds of capabilities and how this works.

julia
mycoord = (11.0, 53.0)
rad = 1e-6

iseat = SST.ISEACircleTree(22)
mycircle = SphericalCap(UnitSphereFromGeographic()(mycoord), rad)
GeometryOps.UnitSpherical.SphericalCap{Float64}(UnitSphericalPoint(0.5907579861332358, 0.11483171997015439, 0.7986355100472928), 1.0e-6, 0.9999999999995)

Query

julia
r = GO.query(iseat, mycircle)
points = SST.index_to_lonlat.(r, (iseat,))
f, a, p = scatter(points; color = Makie.Cycled(2), axis = (; type = GlobeAxis, show_axis = false));
lines!(a, GeoMakie.coastlines())
f

julia
poly1 = SST.index_to_polygon_lonlat(1, iseat)

poly!(a, poly1; strokecolor = :red, strokewidth = 1)
f

julia
lo,up = CartesianIndices(SST.gridsize(iseat))[r] |> extrema
bb_isea = map(Colon(), lo.I, up.I)


ras = Raster(WorldClim{BioClim}, 5) |> r -> replace_missing(r, NaN)
c = DiskArrays.mockchunks(ras, DiskArrays.GridChunks(size(ras), (100, 100)))
lccst = SST.RegularGridTree(c)
r2 = GO.query(SST.rootnode(lccst), mycircle)
SST.index_to_lonlat.(r2, (lccst,))

lo, up = CartesianIndices(size(c)[1:2])[r2] |> extrema
bb_lccs = map(Colon(), lo.I, up.I)



d, i = SST.find_nearest(iseat, (11.0, 53.0))
SST.index_to_lonlat(i, iseat)

d, i = SST.find_nearest(lccst, (11.0, 53.0))
SST.index_to_lonlat(i, lccst)



source = SST.ProjectionSource(SST.RegularGridTree, c);
target = SST.ProjectionTarget(SST.ISEACircleTree, 3, 2);


SST.compute_connected_chunks(source, target)


a = SST.LazyProjectedDiskArray(source, target)
8×8×10 LazyProjectedDiskArray{Float64}

How do I get a polygon from a cartesian index?

julia
SST.index_to_polygon_lonlat(CartesianIndex(1,1,1), target.tree)
GeoInterface.Wrappers.Polygon{false, false}([GeoInterface.Wrappers.LineString([(90.0, 26.56505117707799), … (3) … , (90.0, 26.56505117707799)], crs = "EPSG:4326")], crs = "EPSG:4326")

Let's get every polygon...

julia
polys = SST.index_to_polygon_lonlat.(eachindex(a), (target.tree,));

This is

julia
length(polys)
640
julia
a[:, 1, :]
8×10 Matrix{Float64}:
  28.6612  NaN  NaN  NaN   37.7863  …  NaN        NaN       NaN
  30.2127  NaN  NaN  NaN   38.7557     NaN        NaN       NaN
  34.7407  NaN  NaN  NaN   41.8978     NaN        NaN       NaN
 NaN       NaN  NaN  NaN   27.984      NaN        NaN       NaN
  32.0272  NaN  NaN  NaN   36.2388     NaN        NaN       NaN
 NaN       NaN  NaN  NaN  NaN       …  NaN        NaN        -4.77825
 NaN       NaN  NaN  NaN   32.5433      -2.0865   -14.7845  -16.0435
  39.934   NaN  NaN  NaN  NaN           -9.90425  -19.6     -27.3525

Now we can materialize the lazy array...

julia
@time ac = collect(a);
  0.089171 seconds (3.03 k allocations: 5.386 MiB)

and GLMakie has no problem visualizing this!

julia
poly(vec(polys); color = vec(ac), strokewidth = 1, strokecolor = :black, axis = (; type = GlobeAxis, show_axis = false))
meshimage!(-180..180, -90..90, fill(colorant"white", 2, 2); zlevel = -100_000)
current_figure()

heatmap(c.lccs_class.data[bb_lccs...][:, :, 1])

ds = SST.create_dataset(target, "./output.zarr/", arrayname=:lccs_class)

SST.reproject!(ds.lccs_class,source,target)


This page was generated using Literate.jl.