Skip to content

Commit 5e70c05

Browse files
committed
Basic implementation of spatial tree interface
This is only really workable for (assumed static) trees...but very powerful abstraction. Assumes a certain structure to all spatial trees which may not be desirable. We may also want to combine trees with geometry representations in preparations - which this allows us to do but requires some acrobatics. Notably this interface does NOT help us get the actual geometries, trees are assumed to store indices to geometries. Maybe that's a bad idea? But considering featurecollections etc it seems to be the most prudent thing.
1 parent 15928df commit 5e70c05

File tree

2 files changed

+268
-0
lines changed

2 files changed

+268
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# SpatialTreeInterface.jl
2+
3+
A simple interface for spatial tree types.
4+
5+
## What is a spatial tree?
6+
7+
- 2 dimensional extents
8+
- Parent nodes encompass all leaf nodes
9+
- Leaf nodes contain references to the geometries they represent as indices (or so we assume here)
10+
11+
## Why is this useful?
12+
13+
- It allows us to write algorithms that can work with any spatial tree type, without having to know the details of the tree type.
14+
- for example, dual tree traversal / queries
15+
- It allows us to flexibly and easily swap out and use different tree types, depending on the problem at hand.
16+
17+
This is also a zero cost interface if implemented correctly! Verified implementations exist for "flat" trees like the "Natural Index" from `tg`, and "hierarchical" trees like the `STRtree` from `SortTileRecursiveTree.jl`.
18+
19+
## Interface
20+
21+
- `isspatialtree(tree)::Bool`
22+
- `isleaf(node)::Bool` - is the node a leaf node? In this context, a leaf node is a node that does not have other nodes as its children, but stores a list of indices and extents (even if implicit).
23+
- `getchild(node)` - get the children of a node. This may be materialized if necessary or available, but can also be lazy (like a generator).
24+
- `getchild(node, i)` - get the `i`-th child of a node.
25+
- `nchild(node)::Int` - the number of children of a node.
26+
- `child_indices_extents(node)` - an iterator over the indices and extents of the children of a node.
27+
28+
These are the only methods that are required to be implemented. They enable the generic query functions described below:
29+
30+
## Query functions
31+
32+
- `do_query(f, predicate, node)` - call `f(i)` for each index `i` in `node` that satisfies `predicate(extent(i))`.
33+
- `do_dual_query(f, predicate, tree1, tree2)` - call `f(i1, i2)` for each index `i1` in `tree1` and `i2` in `tree2` that satisfies `predicate(extent(i1), extent(i2))`.
34+
35+
These are both completely non-allocating, and will only call `f` for indices that satisfy the predicate.
36+
You can of course build a standard query interface on top of `do_query` if you want - that's simply:
37+
```julia
38+
a = Int[]
39+
do_query(Base.Fix1(push!, a), predicate, node)
40+
```
41+
where `predicate` might be `Base.Fix1(Extents.intersects, extent_to_query)`.
42+
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
module SpatialTreeInterface
2+
3+
import ..LoopStateMachine: @controlflow
4+
5+
import Extents
6+
import GeoInterface as GI
7+
import AbstractTrees
8+
9+
# ## Interface
10+
# Interface definition for spatial tree types.
11+
# There is no abstract supertype here since it's impossible to enforce,
12+
# but we do have a few methods that are common to all spatial tree types.
13+
14+
"""
15+
isspatialtree(tree)::Bool
16+
17+
Return true if the object is a spatial tree, false otherwise.
18+
19+
## Implementation notes
20+
21+
For type stability, if your spatial tree type is `MyTree`, you should define
22+
`isspatialtree(::Type{MyTree}) = true`, and `isspatialtree(::MyTree)` will forward
23+
to that method automatically.
24+
"""
25+
isspatialtree(::T) where T = isspatialtree(T)
26+
isspatialtree(::Type{<: Any}) = false
27+
28+
29+
"""
30+
getchild(node)
31+
32+
Return an iterator over all the children of a node.
33+
This may be materialized if necessary or available,
34+
but can also be lazy (like a generator).
35+
"""
36+
getchild(node) = AbstractTrees.children(node)
37+
38+
"""
39+
getchild(node, i)
40+
41+
Return the `i`-th child of a node.
42+
"""
43+
getchild(node, i) = getchild(node)[i]
44+
45+
"""
46+
nchild(node)
47+
48+
Return the number of children of a node.
49+
"""
50+
nchild(node) = length(getchild(node))
51+
52+
"""
53+
isleaf(node)
54+
55+
Return true if the node is a leaf node, i.e., there are no "children" below it.
56+
[`getchild`](@ref) should still work on leaf nodes, though, returning an iterator over the extents stored in the node - and similarly for `getnodes.`
57+
"""
58+
isleaf(node) = error("isleaf is not implemented for node type $(typeof(node))")
59+
60+
"""
61+
child_indices_extents(node)
62+
63+
Return an iterator over the indices and extents of the children of a node.
64+
65+
Each value of the iterator should take the form `(i, extent)`.
66+
"""
67+
function child_indices_extents(node)
68+
return zip(1:nchild(node), getchild(node))
69+
end
70+
71+
# ## Query functions
72+
# These are generic functions that work with any spatial tree type that implements the interface.
73+
74+
75+
"""
76+
do_query(f, predicate, tree)
77+
78+
Call `f(i)` for each index `i` in the tree that satisfies `predicate(extent(i))`.
79+
80+
This is generic to anything that implements the SpatialTreeInterface, particularly the methods
81+
[`isleaf`](@ref), [`getchild`](@ref), and [`child_extents`](@ref).
82+
"""
83+
function do_query(f::F, predicate::P, node::N) where {F, P, N}
84+
if isleaf(node)
85+
for (i, leaf_geometry_extent) in child_indices_extents(node)
86+
if predicate(leaf_geometry_extent)
87+
@controlflow f(i)
88+
end
89+
end
90+
else
91+
for child in getchild(node)
92+
if predicate(GI.extent(child))
93+
@controlflow do_query(f, predicate, child)
94+
end
95+
end
96+
end
97+
end
98+
99+
function do_query(predicate, node)
100+
a = Int[]
101+
do_query(Base.Fix1(push!, a), predicate, node)
102+
return a
103+
end
104+
105+
106+
"""
107+
query(tree, predicate)
108+
109+
Return a sorted list of indices of the tree that satisfy the predicate.
110+
"""
111+
function query(tree, predicate)
112+
a = Int[]
113+
do_query(Base.Fix1(push!, a), sanitize_predicate(predicate), tree)
114+
return sort!(a)
115+
end
116+
117+
118+
"""
119+
sanitize_predicate(pred)
120+
121+
Convert a predicate to a function that returns a Boolean.
122+
123+
If `pred` is an Extent, convert it to a function that returns a Boolean by intersecting with the extent.
124+
If `pred` is a geometry, convert it to an extent first, then wrap in Extents.intersects.
125+
126+
Otherwise, return the predicate unchanged.
127+
128+
129+
Users and developers may overload this function to provide custom behaviour when something is passed in.
130+
"""
131+
sanitize_predicate(pred::P) where P = sanitize_predicate(GI.trait(pred), pred)
132+
sanitize_predicate(::Nothing, pred::P) where P = pred
133+
sanitize_predicate(::GI.AbstractTrait, pred::P) where P = sanitize_predicate(GI.extent(pred))
134+
sanitize_predicate(pred::Extents.Extent) = Base.Fix1(Extents.intersects, pred)
135+
136+
137+
"""
138+
do_dual_query(f, predicate, node1, node2)
139+
140+
Call `f(i1, i2)` for each index `i1` in `node1` and `i2` in `node2` that satisfies `predicate(extent(i1), extent(i2))`.
141+
142+
This is generic to anything that implements the SpatialTreeInterface, particularly the methods
143+
[`isleaf`](@ref), [`getchild`](@ref), and [`child_extents`](@ref).
144+
"""
145+
function do_dual_query(f::F, predicate::P, node1::N1, node2::N2) where {F, P, N1, N2}
146+
if isleaf(node1) && isleaf(node2)
147+
# both nodes are leaves, so we can just iterate over the indices and extents
148+
for (i1, extent1) in child_indices_extents(node1)
149+
for (i2, extent2) in child_indices_extents(node2)
150+
if predicate(extent1, extent2)
151+
@controlflow f(i1, i2)
152+
end
153+
end
154+
end
155+
elseif isleaf(node1) # node2 is not a leaf, node1 is - recurse further into node2
156+
for child in getchild(node2)
157+
if predicate(GI.extent(node1), GI.extent(child))
158+
@controlflow do_dual_query(f, predicate, node1, child)
159+
end
160+
end
161+
elseif isleaf(node2) # node1 is not a leaf, node2 is - recurse further into node1
162+
for child in getchild(node1)
163+
if predicate(GI.extent(child), GI.extent(node2))
164+
@controlflow do_dual_query(f, predicate, child, node2)
165+
end
166+
end
167+
else # neither node is a leaf, recurse into both children
168+
for child1 in getchild(node1)
169+
for child2 in getchild(node2)
170+
if predicate(GI.extent(child1), GI.extent(child2))
171+
@controlflow do_dual_query(f, predicate, child1, child2)
172+
end
173+
end
174+
end
175+
end
176+
end
177+
178+
# Finally, here's a sample implementation of the interface for STRtrees
179+
180+
using SortTileRecursiveTree: STRtree, STRNode, STRLeafNode
181+
182+
nchild(tree::STRtree) = nchild(tree.rootnode)
183+
getchild(tree::STRtree) = getchild(tree.rootnode)
184+
getchild(tree::STRtree, i) = getchild(tree.rootnode, i)
185+
isleaf(tree::STRtree) = isleaf(tree.rootnode)
186+
child_indices_extents(tree::STRtree) = child_indices_extents(tree.rootnode)
187+
188+
189+
nchild(node::STRNode) = length(node.children)
190+
getchild(node::STRNode) = node.children
191+
getchild(node::STRNode, i) = node.children[i]
192+
isleaf(node::STRNode) = false # STRNodes are not leaves by definition
193+
194+
isleaf(node::STRLeafNode) = true
195+
child_indices_extents(node::STRLeafNode) = zip(node.indices, node.extents)
196+
197+
198+
"""
199+
FlatNoTree(iterable_of_geoms_or_extents)
200+
201+
Represents a flat collection with no tree structure, i.e., a brute force search.
202+
This is cost free, so particularly useful when you don't want to build a tree!
203+
"""
204+
struct FlatNoTree{T}
205+
geometries::T
206+
end
207+
208+
isleaf(tree::FlatNoTree) = true
209+
210+
# NOTE: use pairs instead of enumerate here, so that we can support
211+
# iterators or collections that define custom `pairs` methods.
212+
# This includes things like filtered extent lists, for example,
213+
# so we can perform extent thinning with no allocations.
214+
function child_indices_extents(tree::FlatNoTree{T}) where T
215+
# This test only applies at compile time and should be optimized away in any case.
216+
# And we can use multiple dispatch to override anyway, but it should be cost free I think.
217+
if applicable(Base.keys, T)
218+
return ((i, GI.extent(obj)) for (i, obj) in pairs(tree.geometries))
219+
else
220+
return ((i, GI.extent(obj)) for (i, obj) in enumerate(tree.geometries))
221+
end
222+
end
223+
224+
end # module SpatialTreeInterface
225+
226+
using .SpatialTreeInterface

0 commit comments

Comments
 (0)