Skip to content

Commit 728befc

Browse files
committed
Factor out SpatialTreeInterface into separate files
Maybe this is a bad idea, single file was nice for readability. But we can keep going.
1 parent 06c76a9 commit 728befc

File tree

5 files changed

+236
-200
lines changed

5 files changed

+236
-200
lines changed

src/utils/SpatialTreeInterface/SpatialTreeInterface.jl

Lines changed: 11 additions & 200 deletions
Original file line numberDiff line numberDiff line change
@@ -10,118 +10,19 @@ import AbstractTrees
1010
export query, do_query
1111
export FlatNoTree
1212

13-
# ## Interface
14-
# Interface definition for spatial tree types.
15-
# There is no abstract supertype here since it's impossible to enforce,
16-
# but we do have a few methods that are common to all spatial tree types.
13+
# The spatial tree interface and its implementations are defined here.
14+
include("interface.jl")
15+
include("implementations.jl")
1716

18-
"""
19-
isspatialtree(tree)::Bool
20-
21-
Return true if the object is a spatial tree, false otherwise.
22-
23-
## Implementation notes
24-
25-
For type stability, if your spatial tree type is `MyTree`, you should define
26-
`isspatialtree(::Type{MyTree}) = true`, and `isspatialtree(::MyTree)` will forward
27-
to that method automatically.
28-
"""
29-
isspatialtree(::T) where T = isspatialtree(T)
30-
isspatialtree(::Type{<: Any}) = false
31-
32-
33-
"""
34-
getchild(node)
35-
36-
Return an iterator over all the children of a node.
37-
This may be materialized if necessary or available,
38-
but can also be lazy (like a generator).
39-
"""
40-
getchild(node) = AbstractTrees.children(node)
41-
42-
"""
43-
getchild(node, i)
44-
45-
Return the `i`-th child of a node.
46-
"""
47-
getchild(node, i) = getchild(node)[i]
48-
49-
"""
50-
nchild(node)
17+
# Here we have some algorithms that use the spatial tree interface.
18+
# The first file holds a single depth-first search, i.e., a single-tree query.
19+
include("depth_first_search.jl")
5120

52-
Return the number of children of a node.
53-
"""
54-
nchild(node) = length(getchild(node))
55-
56-
"""
57-
isleaf(node)
58-
59-
Return true if the node is a leaf node, i.e., there are no "children" below it.
60-
[`getchild`](@ref) should still work on leaf nodes, though, returning an iterator over the extents stored in the node - and similarly for `getnodes.`
61-
"""
62-
isleaf(node) = error("isleaf is not implemented for node type $(typeof(node))")
63-
64-
"""
65-
child_indices_extents(node)
66-
67-
Return an iterator over the indices and extents of the children of a node.
68-
69-
Each value of the iterator should take the form `(i, extent)`.
70-
71-
This can only be invoked on leaf nodes!
72-
"""
73-
function child_indices_extents(node)
74-
return zip(1:nchild(node), getchild(node))
75-
end
76-
77-
"""
78-
node_extent(node)
79-
80-
Return the extent like object of the node.
81-
Falls back to `GI.extent` by default, which falls back
82-
to `Extents.extent`.
83-
84-
Generally, defining `Extents.extent(node)` is sufficient here, and you
85-
won't need to define this
86-
87-
The reason we don't use that directly is to give users of this interface
88-
a way to define bounding boxes that are not extents, like spherical caps
89-
and other such things.
90-
"""
91-
node_extent(node) = GI.extent(node)
92-
93-
# ## Query functions
94-
# These are generic functions that work with any spatial tree type that implements the interface.
95-
96-
97-
"""
98-
do_query(f, predicate, tree)
99-
100-
Call `f(i)` for each index `i` in the tree that satisfies `predicate(extent(i))`.
101-
102-
This is generic to anything that implements the SpatialTreeInterface, particularly the methods
103-
[`isleaf`](@ref), [`getchild`](@ref), and [`child_extents`](@ref).
104-
"""
105-
function do_query(f::F, predicate::P, node::N) where {F, P, N}
106-
if isleaf(node)
107-
for (i, leaf_geometry_extent) in child_indices_extents(node)
108-
if predicate(leaf_geometry_extent)
109-
@controlflow f(i)
110-
end
111-
end
112-
else
113-
for child in getchild(node)
114-
if predicate(node_extent(child))
115-
@controlflow do_query(f, predicate, child)
116-
end
117-
end
118-
end
119-
end
120-
function do_query(predicate, node)
121-
a = Int[]
122-
do_query(Base.Fix1(push!, a), predicate, node)
123-
return a
124-
end
21+
# The second file holds a dual depth-first search, i.e., a dual-tree query.
22+
# This iterates over two trees simultaneously, and is substantially more efficient
23+
# than two separate single-tree queries since it can prune branches in tandem as it
24+
# descends into the trees.
25+
include("dual_depth_first_search.jl")
12526

12627

12728
"""
@@ -155,94 +56,4 @@ sanitize_predicate(::GI.AbstractTrait, pred) = sanitize_predicate(GI.extent(pred
15556
sanitize_predicate(pred::Extents.Extent) = Base.Fix1(Extents.intersects, pred)
15657

15758

158-
"""
159-
do_dual_query(f, predicate, node1, node2)
160-
161-
Call `f(i1, i2)` for each index `i1` in `node1` and `i2` in `node2` that satisfies `predicate(extent(i1), extent(i2))`.
162-
163-
This is generic to anything that implements the SpatialTreeInterface, particularly the methods
164-
[`isleaf`](@ref), [`getchild`](@ref), and [`child_extents`](@ref).
165-
"""
166-
function do_dual_query(f::F, predicate::P, node1::N1, node2::N2) where {F, P, N1, N2}
167-
if isleaf(node1) && isleaf(node2)
168-
# both nodes are leaves, so we can just iterate over the indices and extents
169-
for (i1, extent1) in child_indices_extents(node1)
170-
for (i2, extent2) in child_indices_extents(node2)
171-
if predicate(extent1, extent2)
172-
@controlflow f(i1, i2)
173-
end
174-
end
175-
end
176-
elseif isleaf(node1) # node2 is not a leaf, node1 is - recurse further into node2
177-
for child in getchild(node2)
178-
if predicate(node_extent(node1), node_extent(child))
179-
@controlflow do_dual_query(f, predicate, node1, child)
180-
end
181-
end
182-
elseif isleaf(node2) # node1 is not a leaf, node2 is - recurse further into node1
183-
for child in getchild(node1)
184-
if predicate(node_extent(child), node_extent(node2))
185-
@controlflow do_dual_query(f, predicate, child, node2)
186-
end
187-
end
188-
else # neither node is a leaf, recurse into both children
189-
for child1 in getchild(node1)
190-
for child2 in getchild(node2)
191-
if predicate(node_extent(child1), node_extent(child2))
192-
@controlflow do_dual_query(f, predicate, child1, child2)
193-
end
194-
end
195-
end
196-
end
197-
end
198-
199-
# Finally, here's a sample implementation of the interface for STRtrees
200-
201-
using SortTileRecursiveTree: STRtree, STRNode, STRLeafNode
202-
203-
isspatialtree(::Type{<: STRtree}) = true
204-
nchild(tree::STRtree) = nchild(tree.rootnode)
205-
getchild(tree::STRtree) = getchild(tree.rootnode)
206-
getchild(tree::STRtree, i) = getchild(tree.rootnode, i)
207-
isleaf(tree::STRtree) = isleaf(tree.rootnode)
208-
child_indices_extents(tree::STRtree) = child_indices_extents(tree.rootnode)
209-
210-
isspatialtree(::Type{<: STRNode}) = true
211-
nchild(node::STRNode) = length(node.children)
212-
getchild(node::STRNode) = node.children
213-
getchild(node::STRNode, i) = node.children[i]
214-
isleaf(node::STRNode) = false # STRNodes are not leaves by definition
215-
216-
isspatialtree(::Type{<: STRLeafNode}) = true
217-
isleaf(node::STRLeafNode) = true
218-
child_indices_extents(node::STRLeafNode) = zip(node.indices, node.extents)
219-
220-
221-
"""
222-
FlatNoTree(iterable_of_geoms_or_extents)
223-
224-
Represents a flat collection with no tree structure, i.e., a brute force search.
225-
This is cost free, so particularly useful when you don't want to build a tree!
226-
"""
227-
struct FlatNoTree{T}
228-
geometries::T
229-
end
230-
231-
isspatialtree(::Type{<: FlatNoTree}) = true
232-
isleaf(tree::FlatNoTree) = true
233-
234-
# NOTE: use pairs instead of enumerate here, so that we can support
235-
# iterators or collections that define custom `pairs` methods.
236-
# This includes things like filtered extent lists, for example,
237-
# so we can perform extent thinning with no allocations.
238-
function child_indices_extents(tree::FlatNoTree{T}) where T
239-
# This test only applies at compile time and should be optimized away in any case.
240-
# And we can use multiple dispatch to override anyway, but it should be cost free I think.
241-
if applicable(Base.keys, T)
242-
return ((i, GI.extent(obj)) for (i, obj) in pairs(tree.geometries))
243-
else
244-
return ((i, GI.extent(obj)) for (i, obj) in enumerate(tree.geometries))
245-
end
246-
end
247-
24859
end # module SpatialTreeInterface
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
"""
3+
depth_first_search(f, predicate, tree)
4+
5+
Call `f(i)` for each index `i` in the tree that satisfies `predicate(extent(i))`.
6+
7+
This is generic to anything that implements the SpatialTreeInterface, particularly the methods
8+
[`isleaf`](@ref), [`getchild`](@ref), and [`child_extents`](@ref).
9+
"""
10+
function depth_first_search(f::F, predicate::P, node::N) where {F, P, N}
11+
if isleaf(node)
12+
for (i, leaf_geometry_extent) in child_indices_extents(node)
13+
if predicate(leaf_geometry_extent)
14+
@controlflow f(i)
15+
end
16+
end
17+
else
18+
for child in getchild(node)
19+
if predicate(node_extent(child))
20+
@controlflow depth_first_search(f, predicate, child)
21+
end
22+
end
23+
end
24+
end
25+
function depth_first_search(predicate, node)
26+
a = Int[]
27+
depth_first_search(Base.Fix1(push!, a), predicate, node)
28+
return a
29+
end
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""
2+
dual_depth_first_search(f, predicate, tree1, tree2)
3+
4+
Executes a dual depth-first search over two trees, descending into the children of
5+
nodes `i` and `j` when `predicate(node_extent(i), node_extent(j))` is true,
6+
and pruning that branch when `predicate(node_extent(i), node_extent(j))` is false.
7+
8+
Finally, calls `f(i1, i2)` for each leaf-level index `i1::Int` in `tree1` and `i2::Int` in `tree2`
9+
that satisfies `predicate(extent(i1), extent(i2))`.
10+
11+
Here, `f(i1::Int, i2::Int)` may be any function that takes two integers as arguments.
12+
It may optionally return an [`Action`](@ref LoopStateMachine.Action) to alter the control
13+
flow of the `Action(:full_return, true)` to return `Action(:full_return, true)` from this
14+
function and break out of the recursion.
15+
16+
This is generic to anything that implements the SpatialTreeInterface, particularly the methods
17+
[`isleaf`](@ref), [`getchild`](@ref), and [`child_indices_extents`](@ref).
18+
19+
## Examples
20+
21+
```julia
22+
using NaturalEarth,
23+
```
24+
"""
25+
function dual_depth_first_search(f::F, predicate::P, node1::N1, node2::N2) where {F, P, N1, N2}
26+
if isleaf(node1) && isleaf(node2)
27+
# both nodes are leaves, so we can just iterate over the indices and extents
28+
for (i1, extent1) in child_indices_extents(node1)
29+
for (i2, extent2) in child_indices_extents(node2)
30+
if predicate(extent1, extent2)
31+
@controlflow f(i1, i2)
32+
end
33+
end
34+
end
35+
elseif isleaf(node1) # node2 is not a leaf, node1 is - recurse further into node2
36+
for child in getchild(node2)
37+
if predicate(node_extent(node1), node_extent(child))
38+
@controlflow dual_depth_first_search(f, predicate, node1, child)
39+
end
40+
end
41+
elseif isleaf(node2) # node1 is not a leaf, node2 is - recurse further into node1
42+
for child in getchild(node1)
43+
if predicate(node_extent(child), node_extent(node2))
44+
@controlflow dual_depth_first_search(f, predicate, child, node2)
45+
end
46+
end
47+
else # neither node is a leaf, recurse into both children
48+
for child1 in getchild(node1)
49+
for child2 in getchild(node2)
50+
if predicate(node_extent(child1), node_extent(child2))
51+
@controlflow dual_depth_first_search(f, predicate, child1, child2)
52+
end
53+
end
54+
end
55+
end
56+
end
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# # Interface implementations
2+
# Below are some basic implementations of the interface,
3+
# for STRTree and a "no-tree" implementation that is a flat list of extents.
4+
5+
using SortTileRecursiveTree: STRtree, STRNode, STRLeafNode
6+
7+
# ## SortTileRecursiveTree
8+
9+
isspatialtree(::Type{<: STRtree}) = true
10+
node_extent(tree::STRtree) = node_extent(tree.rootnode)
11+
nchild(tree::STRtree) = nchild(tree.rootnode)
12+
getchild(tree::STRtree) = getchild(tree.rootnode)
13+
getchild(tree::STRtree, i) = getchild(tree.rootnode, i)
14+
isleaf(tree::STRtree) = isleaf(tree.rootnode)
15+
child_indices_extents(tree::STRtree) = child_indices_extents(tree.rootnode)
16+
17+
isspatialtree(::Type{<: STRNode}) = true
18+
node_extent(node::STRNode) = node.extent
19+
nchild(node::STRNode) = length(node.children)
20+
getchild(node::STRNode) = node.children
21+
getchild(node::STRNode, i) = node.children[i]
22+
isleaf(node::STRNode) = false # STRNodes are not leaves by definition
23+
24+
isspatialtree(::Type{<: STRLeafNode}) = true
25+
node_extent(node::STRLeafNode) = node.extent
26+
isleaf(node::STRLeafNode) = true
27+
child_indices_extents(node::STRLeafNode) = zip(node.indices, node.extents)
28+
29+
# ## FlatNoTree
30+
"""
31+
FlatNoTree(iterable_of_geoms_or_extents)
32+
33+
Represents a flat collection with no tree structure, i.e., a brute force search.
34+
This is cost free, so particularly useful when you don't want to build a tree!
35+
"""
36+
struct FlatNoTree{T}
37+
geometries::T
38+
end
39+
40+
isspatialtree(::Type{<: FlatNoTree}) = true
41+
isleaf(tree::FlatNoTree) = true
42+
43+
# NOTE: use pairs instead of enumerate here, so that we can support
44+
# iterators or collections that define custom `pairs` methods.
45+
# This includes things like filtered extent lists, for example,
46+
# so we can perform extent thinning with no allocations.
47+
function child_indices_extents(tree::FlatNoTree{T}) where T
48+
# This test only applies at compile time and should be optimized away in any case.
49+
# And we can use multiple dispatch to override anyway, but it should be cost free I think.
50+
if applicable(Base.keys, T)
51+
return ((i, GI.extent(obj)) for (i, obj) in pairs(tree.geometries))
52+
else
53+
return ((i, GI.extent(obj)) for (i, obj) in enumerate(tree.geometries))
54+
end
55+
end

0 commit comments

Comments
 (0)