Skip to content

Commit

Permalink
WIP: geometry lookup
Browse files Browse the repository at this point in the history
  • Loading branch information
asinghvi17 committed Jan 8, 2025
1 parent 9a718eb commit 5dcb1f7
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 2 deletions.
8 changes: 6 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b"
Flatten = "4c728ea3-d9ee-5c9a-9642-b6f7d7dc04fa"
GeoFormatTypes = "68eda718-8dee-11e9-39e7-89f7f65f511f"
GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f"
GeometryOps = "3251bfac-6a57-4b6d-aa61-ac1fef2975ab"
GeometryOpsCore = "05efe853-fabf-41c8-927e-7063c8b9f013"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Missings = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28"
Expand All @@ -25,6 +26,7 @@ ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca"
RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46"
SortTileRecursiveTree = "746ee33f-1797-42c2-866d-db2fce69d14d"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"

[weakdeps]
Expand Down Expand Up @@ -69,8 +71,9 @@ GeoDataFrames = "0.3"
GeoFormatTypes = "0.4"
GeoInterface = "1.0"
GeometryBasics = "0.4"
GeometryOpsCore = "0.1.1"
Makie = "0.20, 0.21"
GeometryOps = "0.1.12"
GeometryOpsCore = "0.1"
Makie = "0.20, 0.21, 0.22"
Missings = "0.4, 1"
NCDatasets = "0.13, 0.14"
OffsetArrays = "1"
Expand All @@ -83,6 +86,7 @@ Reexport = "0.2, 1.0"
SafeTestsets = "0.1"
Setfield = "0.6, 0.7, 0.8, 1"
Shapefile = "0.10, 0.11"
SortTileRecursiveTree = "0.1.1"
Statistics = "1"
StatsBase = "0.34"
Test = "1"
Expand Down
4 changes: 4 additions & 0 deletions src/Rasters.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Adapt,
FillArrays,
Flatten,
GeoInterface,
GeometryOps,
GeometryOpsCore,
OffsetArrays,
ProgressMeter,
Expand All @@ -29,6 +30,7 @@ import Adapt,
RecipesBase,
Reexport,
Setfield,
SortTileRecursiveTree,
Statistics

Reexport.@reexport using DimensionalData, GeoFormatTypes
Expand Down Expand Up @@ -76,6 +78,7 @@ export Extent, extent
const DD = DimensionalData
const DA = DiskArrays
const GI = GeoInterface
const GO = GeometryOps
const LA = Lookups

# DimensionalData documentation urls
Expand Down Expand Up @@ -108,6 +111,7 @@ include("array.jl")
include("stack.jl")
include("series.jl")
include("crs.jl")
include("geometry_lookup.jl")

const RasterStackOrArray = Union{AbstractRasterStack,AbstractRaster}
const RasterSeriesOrStack = Union{AbstractRasterSeries,AbstractRasterStack}
Expand Down
226 changes: 226 additions & 0 deletions src/geometry_lookup.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
"""
GeometryLookup(data, dims = (X(), Y()); geometrycolumn = nothing)
The other thing I'm thinking of is that we could have a feature collection in `data` so that you can persist per geom attrs
A lookup type for geometry dimensions in vector data cubes.
`GeometryLookup` provides efficient spatial indexing and lookup for geometries using an STRtree (Sort-Tile-Recursive tree).
It is used as the lookup type for geometry dimensions in vector data cubes, enabling fast spatial queries and operations.
It spans the dimensions given to it in `dims`, as well as the dimension it's wrapped in - you would construct a DimArray with a GeometryLookup
like `DimArray(data, Geometry(GeometryLookup(data, dims)))`. Here, `Geometry` is a dimension - but selectors in X and Y will also eventually work!
# Examples
```julia
using Rasters
using NaturalEarth
import GeometryOps as GO
# construct the polygon lookup
polygons = NaturalEarth.naturalearth("admin_0_countries", 110).geometry
polygon_lookup = GeometryLookup(polygons, (X(), Y()))
# create a DimArray with the polygon lookup
dv = rand(Geometry(polygon_lookup))
# select the polygon with the centroid of the 88th polygon
dv[Geometry(Contains(GO.centroid(polygons[88])))] == dv[Geometry(88)] # true
```
"""
struct GeometryLookup{T, D} <: Lookups.Lookup{T, 1}
data::Vector{T}
tree::SortTileRecursiveTree.STRtree
dims::D
end

function GeometryLookup(data, dims = (X(), Y()); geometrycolumn = nothing)
# First, retrieve the geometries - from a table, vector of geometries, etc.
geometries = _get_geometries(data, geometrycolumn)
# Check that the geometries are actually geometries
if any(!GI.isgeometry, geometries)
throw(ArgumentError("""
The collection passed in to `GeometryLookup` has some elements that are not geometries
(`GI.isgeometry(x) == false` for some `x` in `data`).
"""))
end
# Make sure there are only two dimensions
if length(dims) != 2
throw(ArgumentError("""
The `dims` argument to `GeometryLookup` must have two dimensions, but it has $(length(dims)) dimensions (`$(dims)`).
Please make sure that it has only two dimensions, like `(X(), Y())`.
"""))
end
# Build the lookup accelerator tree
tree = SortTileRecursiveTree.STRtree(geometries)
GeometryLookup(geometries, tree, dims)
end

DD.dims(l::GeometryLookup) = l.dims
DD.dims(d::DD.Dimension{<: GeometryLookup}) = val(d).dims

DD.order(::GeometryLookup) = Lookups.Unordered()

DD.parent(lookup::GeometryLookup) = lookup.data

DD.Dimensions.format(l::GeometryLookup, D::Type, values, axis::AbstractRange) = l

function DD.rebuild(lookup::GeometryLookup; data = lookup.data, tree = nothing, dims = lookup.dims)
new_tree = if data == lookup.data
lookup.tree
else
SortTileRecursiveTree.STRtree(data)
end
GeometryLookup(data, new_tree, dims)
end

# Return an `Int` or Vector{Bool}
Lookups.selectindices(lookup::GeometryLookup, sel::DD.DimTuple) =
selectindices(lookup, map(_val_or_nothing, sortdims(sel, dims(lookup))))
function Lookups.selectindices(lookup::GeometryLookup, sel::NamedTuple{K}) where K
dimsel = map(rebuild, map(name2dim, K), values(sel))
selectindices(lookup, dimsel)
end
Lookups.selectindices(lookup::GeometryLookup, sel::Lookups.StandardIndices) = sel
function Lookups.selectindices(lookup::GeometryLookup, sel::Tuple)
if (length(sel) == length(dims(lookup))) && all(map(s -> s isa At, sel))
i = findfirst(x -> all(map(_matches, sel, x)), lookup)
isnothing(i) && _coord_not_found_error(sel)
return i
else
return [_matches(sel, x) for x in lookup]
end
end

function _maybe_get_candidates(tree, selector_extent)
if Extents.disjoint(tree.rootnode.extent, selector_extent)
return error("""
The geometry with extent $(GI.extent(val(sel))) is outside of the extent of the geometry lookup.
The geometry lookup has extent $(GI.extent(tree.rootnode.extent))
""")
end
potential_candidates = SortTileRecursiveTree.query(tree, selector_extent)

isempty(potential_candidates) && return error("""
The geometry with extent $(GI.extent(val(sel))) does not interact with any of the geometries in the lookup.
""")

return potential_candidates
end

function Lookups.selectindices(lookup::GeometryLookup, sel::Contains)
lookup_ext = lookup.tree.rootnode.extent
sel_ext = GI.extent(val(sel))
potential_candidates = _maybe_get_candidates(lookup.tree, sel_ext)

for candidate in potential_candidates
if GO.contains(lookup.data[candidate], val(sel))
return candidate
end
end
return error("""
The geometry with extent $(GI.extent(val(sel))) is not contained by any of the geometries in the lookup.
""")
end


function Lookups.selectindices(lookup::GeometryLookup, sel::Touches)
lookup_ext = lookup.tree.rootnode.extent
sel_ext = GI.extent(val(sel))
potential_candidates = _maybe_get_candidates(lookup.tree, sel_ext)

for candidate in potential_candidates
if GO.intersects(lookup.data[candidate], val(sel))
return candidate
end
end
return error("""
The geometry with extent $(GI.extent(val(sel))) does not touch any of the geometries in the lookup.
""")
end

function Lookups.selectindices(lookup::GeometryLookup, (xs, ys)::Tuple{Union{<: At, <: Contains}, Union{<: At, <: Contains}})
xval, yval = val(xs), val(ys)

lookup_ext = lookup.tree.rootnode.extent

if lookup_ext.X[1] <= xval <= lookup_ext.X[2] && lookup_ext.Y[1] <= yval <= lookup_ext.Y[2]
potential_candidates = SortTileRecursiveTree.query(lookup.tree, (xval, yval))
if isempty(potential_candidates)
return error("""
The point ($xval, $yval) is not within any of the geometries in the lookup.
""") # no geometry intersects with it
else
for candidate in potential_candidates
if GO.contains(lookup.data[candidate], (xval, yval))
return candidate
end
end
return error("""
The point ($xval, $yval) is not within any of the geometries in the lookup.
""") # no geometry intersects with it
end
else
return error("""
The point ($xval, $yval) is outside of the extent of the geometry lookup.
""") # outside of extent / geometrylookup bbox
end
end

@inline Lookups.reducelookup(l::GeometryLookup) = NoLookup(OneTo(1))

function Lookups.show_properties(io::IO, mime, lookup::GeometryLookup)
print(io, " ")
show(IOContext(io, :inset => "", :dimcolor => 244), mime, DD.basedims(lookup))
end

# Dimension methods

@inline _reducedims(lookup::GeometryLookup, dim::DD.Dimension) =
rebuild(dim, [map(x -> zero(x), dim.val[1])])

function DD._format(dim::DD.Dimension{<:GeometryLookup}, axis::AbstractRange)
checkaxis(dim, axis)
return dim
end

# Local functions
_val_or_nothing(::Nothing) = nothing
_val_or_nothing(d::DD.Dimension) = val(d)

#=
DD.@dim Geometry
using NaturalEarth
polygons = NaturalEarth.naturalearth("admin_0_countries", 110).geometry
polygon_lookup = GeometryLookup(polygons, SortTileRecursiveTree.STRtree(polygons), (X(), Y()))
dv = rand(Geometry(polygon_lookup))
@test_throws ErrorException dv[Geometry=(X(At(1)), Y(At(2)))]
@test dv[Geometry(Contains(GO.centroid(polygons[88])))] == dv[Geometry(88)]
Geometry(Where(GO.contains(geom2)))
X(At(1)), Y(At(2)), Ti(Near(2010))
# TODO:
# metadata for crs
#
=#

0 comments on commit 5dcb1f7

Please sign in to comment.