Skip to content

Commit d6c1ffc

Browse files
committed
add SpatialTreeInterface implementation
(but no tests yet, need to add those!)
1 parent 3419121 commit d6c1ffc

File tree

3 files changed

+293
-0
lines changed

3 files changed

+293
-0
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ authors = ["Anshul Singhvi <[email protected]>", "Rafael Schouten <rafaels
44
version = "0.1.18"
55

66
[deps]
7+
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
78
AdaptivePredicates = "35492f91-a3bd-45ad-95db-fcad7dcfedb7"
89
CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298"
910
DataAPI = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a"
@@ -32,6 +33,7 @@ GeometryOpsProjExt = "Proj"
3233
GeometryOpsTGGeometryExt = "TGGeometry"
3334

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

0 commit comments

Comments
 (0)