Skip to content

Commit ab28913

Browse files
rafaqzvisr
andauthored
add GeoInterface/Extents compat methods (#22)
* add GeoInterface/Extents compat methods * docs tweaks * bump julia version to 1.6 * minor additions - add Manifest to .gitignore - use Aqua quality assurance - sort Project.toml - remove debugging statement --------- Co-authored-by: Martijn Visser <mgvisser@gmail.com>
1 parent 17a0727 commit ab28913

6 files changed

Lines changed: 176 additions & 48 deletions

File tree

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
fail-fast: false
1414
matrix:
1515
version:
16-
- '1.3'
16+
- '1.6'
1717
- '1'
1818
- 'nightly'
1919
os:

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
*.jl.cov
22
*.jl.*.cov
33
*.jl.mem
4-
/deps/deps.jl
5-
/deps/usr/
4+
Manifest.toml

Project.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ license = "MIT"
44
version = "0.2.0"
55

66
[deps]
7+
GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f"
78
LibSpatialIndex_jll = "00e98e2a-4326-5239-88cb-15dcbe1c18d0"
89

910
[compat]
10-
julia = "1.3"
11+
Aqua = "0.7"
12+
GeoInterface = "1"
1113
LibSpatialIndex_jll = "1.8"
14+
julia = "1.6"
1215

1316
[extras]
17+
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
1418
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1519

1620
[targets]
17-
test = ["Test"]
21+
test = ["Aqua", "Test"]

README.md

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,53 +8,82 @@ LibSpatialIndex.jl is a julia wrapper around the C API of [libspatialindex](http
88
# Quick Guide
99

1010
A new RTree with 2 dimensions can be created using this package as follows:
11+
1112
```julia
1213
import LibSpatialIndex
1314
rtree = LibSpatialIndex.RTree(2)
1415
```
1516

1617
## Insertion
17-
Items can be inserted using the `insert!` method, where
18+
19+
Items can be inserted using the `insert!` method, where:
20+
1821
```julia
19-
LibSpatialIndex.insert!(rtree, 1, [0.,0.], [1.,1.])
20-
LibSpatialIndex.insert!(rtree, 2, [0.,0.], [2.,2.])
22+
LibSpatialIndex.insert!(rtree, 1, [0.0, 0.0], [1.0, 1.0])
23+
LibSpatialIndex.insert!(rtree, 2, Extexts.Extent(X=(0.0, 2.0), (0.0, 2.0)))
2124
```
25+
2226
inserts two items,
2327

2428
- the first with `id` 1, associated with the box specified by `[xmin=0.0,ymin=0.0]` and `[xmax=1.0,ymax=1.0]`.
2529
- the second with `id` 2, associated with the box specified by `[xmin=0.0,ymin=0.0]` and `[xmax=2.0,ymax=2.0]`.
2630

2731
## Queries
28-
Thereafter, you can perform queries on the `rtree` using either (i) `intersects(rtree, minvalues, maxvalues)` for all items intersecting the box specified by `minvalues` and `maxvalues`, or (ii) `knn(rtree, minvalues, maxvalues, k)` for the `k` nearest items in `rtree` to the box specified by `minvalues` and `maxvalues`.
32+
33+
Thereafter, you can perform queries on the `rtree` using either (i) `intersects(rtree, minvalues, maxvalues)`
34+
for all items intersecting the box specified by `minvalues` and `maxvalues`, or (ii)
35+
`knn(rtree, minvalues, maxvalues, k)` for the `k` nearest items in `rtree` to the box
36+
specified by `minvalues` and `maxvalues`.
2937

3038
### Intersection
31-
So for instance,
39+
40+
So for instance:
41+
3242
```julia
33-
LibSpatialIndex.intersects(rtree, [0.,0.],[1.,1.])
43+
LibSpatialIndex.intersects(rtree, [0.0, 0.0], [1.0, 1.0])
44+
LibSpatialIndex.intersects(rtree, Extents.extent(X=(0.0, 1.0), Y=(0.0, 1.0)))
3445
```
35-
will return the vector `[1,2]` on the `rtree` constructed earlier, to indicate that items with ids `1` and `2` intersects the box specified by `[xmin=0.0,ymin=0.0]` and `[xmax=1.0,ymax=1.0]`.
3646

37-
You can also perform queries on an individual point, so
47+
will return the vector `[1, 2]` on the `rtree` constructed earlier, to indicate that items
48+
with ids `1` and `2` intersects the box specified by `[xmin=0.0, ymin=0.0]` and `[xmax=1.0, ymax=1.0]`.
49+
50+
Any GeoInterface.jl or Extents.jl compatible object can be used directly instead
51+
of defining the `Extent` manually - the extent will either be detected or calculated.
52+
53+
54+
You can also perform queries on any individual GeoInterface.jl compatible point, so:
55+
3856
```julia
39-
LibSpatialIndex.intersects(rtree, [1.,1.])
57+
LibSpatialIndex.intersects(rtree, (1.0, 1.0))
4058
```
41-
will return the ids `[1,2]` in the `rtree` constructed earlier, and
59+
60+
will return the ids `[1, 2]` in the `rtree` constructed earlier, and:
61+
4262
```julia
43-
LibSpatialIndex.intersects(rtree, [2.,2.])
63+
LibSpatialIndex.intersects(rtree, [2.0, 2.0])
4464
```
45-
will only return the vector `[2]`, because item 1 does not contain the point `[2,2]`.
65+
66+
will only return the vector `[2]`, because item 1 does not contain the point `[2.0, 2.0]`.
4667

4768
### k Nearest Neighbors
48-
For `knn` queries,
69+
70+
For `knn` queries:
71+
4972
```julia
50-
LibSpatialIndex.knn(rtree, [2.,2.], 1)
73+
LibSpatialIndex.knn(rtree, [2.0, 2.0], 1)
5174
```
75+
5276
returns the vector `[2]` because the item with id `2` is closest to the point `[2.0, 2.0]`, and
77+
5378
```julia
54-
sort(LibSpatialIndex.knn(rtree, [2.,2.], 2))
79+
sort(LibSpatialIndex.knn(rtree, [2.0, 2.0], 2))
5580
```
56-
returns the vector `[1,2]`. If the value of `k` exceeds the number of items in the `rtree`, then fewer than `k` items will be returned, so
81+
82+
returns the vector `[1, 2]`. If the value of `k` exceeds the number of items in the `rtree`,
83+
then fewer than `k` items will be returned, so:
84+
5785
```julia
58-
sort(SI.knn(rtree, [2.,2.], 3))
86+
sort(SI.knn(rtree, [2.0, 2.0], 3))
5987
```
60-
will return the vector `[1,2]`.
88+
89+
will return the vector `[1, 2]`.

src/LibSpatialIndex.jl

Lines changed: 98 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module LibSpatialIndex
22

3+
import GeoInterface as GI
4+
35
include("capi.jl")
46

57
version() = unsafe_string(C.SIDX_Version())
@@ -11,6 +13,8 @@ module LibSpatialIndex
1113
end
1214

1315
"""
16+
RTree(ndim::Integer; kw...)
17+
1418
The RTree index [guttman84] is a balanced tree structure that consists of
1519
index nodes, leaf nodes and data.
1620
@@ -22,9 +26,12 @@ module LibSpatialIndex
2226
They cannot be empty though. A `fillfactor` specifies the minimum number of
2327
entries allowed in any node. The fill factor is usually close to `70%`.
2428
25-
# Options
29+
## Arguments
30+
31+
`ndim`: Dimensionality of the data that will be inserted.
32+
33+
## Keywords
2634
27-
* `ndim`: Dimensionality of the data that will be inserted.
2835
* `indextype`: one of `RT_RTree` (default), `RT_MVRTree`, or `RT_TPRTree`.
2936
* `variant`: one of `RT_Linear`, `RT_Quadratic`, or `RT_Star` (default).
3037
* `storage`: one of `RT_Memory` (default), `RT_Disk`, or `RT_Custom`.
@@ -39,7 +46,7 @@ module LibSpatialIndex
3946
* `fillfactor`: The fill factor. Default is `0.7`.
4047
* `splitdistributionfactor`: Default is `0.4`.
4148
* `reinsertfactor`: Default is `0.3`.
42-
49+
4350
# Performance
4451
4552
Dataset size, data density, etc. have nothing to do with capacity and page
@@ -152,12 +159,22 @@ module LibSpatialIndex
152159
end
153160

154161
"""
162+
insert!(rtree::RTree, id::Integer, minvalues::Vector{Float64}, maxvalues::Vector{Float64})
163+
insert!(rtree::RTree, id::Integer, extent::Extent)
164+
insert!(rtree::RTree, id::Integer, obj)
165+
155166
Inserts an item into the `rtree` with given `id` and boundingbox specified
156167
by `minvalues` and `maxvalues`, where the item lies within the interval
157-
`[minvalues[i],maxvalues[i]]` for each axis `i` in 1, ..., `ndim`.
168+
`[minvalues[i], maxvalues[i]]` for each axis `i` in 1, ..., `ndim`,
169+
or similar for the `Extent` of `obj`.
170+
171+
If `obj` is passed it will be detected as a `GeoInterface.PointTrait`
172+
and used as a point, or otherwise `GeoInterface.extent` will be called to
173+
detect or calculate the objects `Extent`, falling back to `Extents.extent`.
174+
175+
In these cases `minvalues` and `maxvalues` are taken from the point or extent.
158176
"""
159-
function insert!(
160-
rtree::RTree,
177+
function insert!(rtree::RTree,
161178
id::Integer,
162179
minvalues::Vector{Float64},
163180
maxvalues::Vector{Float64}
@@ -166,13 +183,30 @@ module LibSpatialIndex
166183
pointer(maxvalues), UInt32(length(minvalues)), Ptr{UInt8}(0), Cint(0)
167184
)
168185
end
186+
function insert!(rtree::RTree, id::Integer, extent::GI.Extent)
187+
insert!(rtree, id, _ext2vecs(extent)...)
188+
end
189+
insert!(rtree::RTree, id::Integer, extent::Nothing) = _not_point_or_ext_error()
190+
function insert!(rtree::RTree, id::Integer, obj)
191+
insert!(rtree, id, GI.extent(obj))
192+
end
169193

170194
"""
195+
intersects(rtree::RTree, minvalues::Vector{Float64}, maxvalues::Vector{Float64})
196+
intersects(rtree::RTree, extent::Extent)
197+
intersects(rtree::RTree, obj)
198+
171199
Returns a vector of `id`s corresponding to items in `rtree` that intersects
172200
the box specified by `minvalues` and `maxvalues`.
173201
174-
Each item intersects the interval `[minvalues[i],maxvalues[i]]` for each
175-
axis `i` in 1, ..., `ndim`.
202+
Each item intersects the interval `[minvalues[i], maxvalues[i]]` for each
203+
axis `i` in 1, ..., `ndim`, or similar for the `Extent` of `obj`.
204+
205+
If `obj` is passed it will be detected as a `GeoInterface.PointTrait`
206+
and used as a point, or otherwise `GeoInterface.extent` will be called to
207+
detect or calculate the objects `Extent`, falling back to `Extents.extent`.
208+
209+
In these cases `minvalues` and `maxvalues` are taken from the point or extent.
176210
"""
177211
function intersects(
178212
rtree::RTree,
@@ -187,21 +221,38 @@ module LibSpatialIndex
187221
_checkresult(result, "Index_Intersects_id: Failed to evaluate")
188222
unsafe_wrap(Array, items[], nresults[])
189223
end
190-
191-
"""
192-
Returns a vector of `id`s corresponding to items in `rtree` that intersects
193-
the coordinates specified by `point`.
194-
"""
195224
intersects(rtree::RTree, point::Vector{Float64}) = intersects(rtree, point, point)
225+
function intersects(rtree::RTree, obj)
226+
if GI.trait(obj) isa GI.PointTrait
227+
intersects(rtree, _point2vec(obj))
228+
else
229+
intersects(rtree, GI.extent(obj))
230+
end
231+
end
232+
function intersects(rtree::RTree, extent::GI.Extent)
233+
intersects(rtree::RTree, _ext2vecs(extent)...)
234+
end
235+
intersects(rtree::RTree, ::Nothing) = _not_point_or_ext_error()
196236

197237
"""
238+
knn(rtree::RTree, minvalues::Vector{Float64}, maxvalues::Vector{Float64}, k::Integer)
239+
knn(rtree::RTree, extent::Extent, k::Integer)
240+
knn(rtree::RTree, obj, k::Integer)
241+
198242
Returns a vector of `id`s corresponding to the `k` items in `rtree`
199243
that are nearest to the box specified by `minvalues` and `maxvalues`.
244+
Each item intersects the interval `[minvalues[i], maxvalues[i]]` for each
245+
axis `i` in 1, ..., `ndim`, or similar for the `Extent` of `obj`.
200246
201-
Each item intersects the interval `[minvalues[i],maxvalues[i]]` for each
202-
axis `i` in 1, ..., `ndim`. If there are fewer than `k` items in `rtree`,
247+
If there are fewer than `k` items in `rtree`,
203248
it will return less than `k` items. On the other hand, if there are ties
204249
between some of the items, it might return more than `k` items.
250+
251+
If `obj` is passed it will be detected as a `GeoInterface.PointTrait`
252+
and used as a point, or otherwise `GeoInterface.extent` will be called to
253+
detect or calculate the objects `Extent`, falling back to `Extents.extent`.
254+
255+
In these cases `minvalues` and `maxvalues` are taken from the point or extent.
205256
"""
206257
function knn(
207258
rtree::RTree,
@@ -216,15 +267,38 @@ module LibSpatialIndex
216267
_checkresult(result, "Index_NearestNeighbors_id: Failed to evaluate")
217268
unsafe_wrap(Array, items[], nresults[])
218269
end
270+
knn(rtree::RTree, point::Vector{Float64}, k::Integer) = knn(rtree, point, point, k)
271+
knn(rtree::RTree, extent::GI.Extent, k::Integer) = knn(rtree::RTree, _ext2vecs(extent)..., k)
272+
knn(rtree::RTree, extent::Nothing, k::Integer) = _not_point_or_ext_error()
273+
function knn(rtree::RTree, obj, k::Integer)
274+
if GI.trait(obj) isa GI.PointTrait
275+
knn(rtree, _point2vec(obj), k)
276+
else
277+
knn(rtree, GI.extent(obj), k)
278+
end
279+
end
219280

220-
"""
221-
Returns a vector of `id`s corresponding to the `k` items in `rtree`
222-
that are nearest to the box specified by `minvalues` and `maxvalues`.
281+
# Utils
282+
function _ext2vecs(ex::GI.Extent)
283+
haskey(ex, :X) && haskey(ex, :Y) || throw(ArgumentError("Extent does not have X and Y keys"))
284+
285+
min, max = if haskey(ex, :Z)
286+
Float64[ex.X[1], ex.Y[1], ex.Z[1]], Float64[ex.X[2], ex.Y[2], ex.Z[1]]
287+
else
288+
Float64[ex.X[1], ex.Y[1]], Float64[ex.X[2], ex.Y[2]]
289+
end
290+
291+
return min, max
292+
end
293+
294+
function _point2vec(p)
295+
if GI.is3d(p)
296+
Float64[GI.x(p), GI.y(p), GI.z(p)]
297+
else
298+
Float64[GI.x(p), GI.y(p)]
299+
end
300+
end
301+
302+
_not_point_or_ext_error() = throw(ArgumentError("object is not a point, and does not have an extent"))
223303

224-
If there are fewer than `k` items in `rtree`, it will return less than `k`
225-
items. On the other hand, if there are ties between some of the items,
226-
it might return more than `k` items.
227-
"""
228-
knn(rtree::RTree, point::Vector{Float64}, k::Integer) = knn(rtree, point, point, k)
229-
230304
end # module

test/runtests.jl

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using LibSpatialIndex
22
using Test
3-
const SI = LibSpatialIndex
3+
import GeoInterface as GI
4+
import LibSpatialIndex as SI
5+
import Aqua
46

57
@testset "Simple Tutorial" begin
68
# based on https://github.com/libspatialindex/libspatialindex/wiki/Simple-Tutorial
@@ -51,3 +53,23 @@ end
5153
@test sort(SI.knn(rtree, [2.,2.], 1)) == [2]
5254
@test sort(SI.knn(rtree, [2.,2.], 2)) == [1,2]
5355
end
56+
57+
@testset "GeoInterface/Extents Operations" begin
58+
rtree = SI.RTree(2)
59+
result = SI.insert!(rtree, 1, GI.Extent(X=(0.0, 1.0), Y=(0.0, 1.0)))
60+
@test result == SI.C.RT_None
61+
62+
polygon = GI.Polygon([GI.LinearRing([(0.0, 0.0), (0.5, 0.0), (2.0, 0.5), (0.0, 2.0), (0.0, 0.0)])])
63+
result = SI.insert!(rtree, 2, polygon)
64+
65+
@test result == SI.C.RT_None
66+
@test SI.intersects(rtree, GI.LineString([(0.0, 0.0), (1.0, 1.0)])) == [1, 2]
67+
@test SI.intersects(rtree, GI.Point(0.0, 0.0)) == [1, 2]
68+
@test SI.intersects(rtree, (X=2.0, Y=2.0)) == [2]
69+
@test sort(SI.knn(rtree, GI.Extent(X=(2.0, 2.0), Y=(2.0, 2.0)), 1)) == [2]
70+
@test sort(SI.knn(rtree, (2.0, 2.0), 1)) == [2]
71+
end
72+
73+
@testset "Aqua" begin
74+
Aqua.test_all(LibSpatialIndex)
75+
end

0 commit comments

Comments
 (0)