Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add GeoInterface/Extents compat methods #22

Merged
merged 4 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
fail-fast: false
matrix:
version:
- '1.3'
- '1.6'
- '1'
- 'nightly'
os:
Expand Down
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
*.jl.cov
*.jl.*.cov
*.jl.mem
/deps/deps.jl
/deps/usr/
Manifest.toml
8 changes: 6 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ license = "MIT"
version = "0.2.0"

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

[compat]
julia = "1.3"
Aqua = "0.7"
GeoInterface = "1"
LibSpatialIndex_jll = "1.8"
julia = "1.6"

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

[targets]
test = ["Test"]
test = ["Aqua", "Test"]
65 changes: 47 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,53 +8,82 @@ LibSpatialIndex.jl is a julia wrapper around the C API of [libspatialindex](http
# Quick Guide

A new RTree with 2 dimensions can be created using this package as follows:

```julia
import LibSpatialIndex
rtree = LibSpatialIndex.RTree(2)
```

## Insertion
Items can be inserted using the `insert!` method, where

Items can be inserted using the `insert!` method, where:

```julia
LibSpatialIndex.insert!(rtree, 1, [0.,0.], [1.,1.])
LibSpatialIndex.insert!(rtree, 2, [0.,0.], [2.,2.])
LibSpatialIndex.insert!(rtree, 1, [0.0, 0.0], [1.0, 1.0])
LibSpatialIndex.insert!(rtree, 2, Extexts.Extent(X=(0.0, 2.0), (0.0, 2.0)))
```

inserts two items,

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

## Queries
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`.

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`.

### Intersection
So for instance,

So for instance:

```julia
LibSpatialIndex.intersects(rtree, [0.,0.],[1.,1.])
LibSpatialIndex.intersects(rtree, [0.0, 0.0], [1.0, 1.0])
LibSpatialIndex.intersects(rtree, Extents.extent(X=(0.0, 1.0), Y=(0.0, 1.0)))
```
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]`.

You can also perform queries on an individual point, so
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]`.

Any GeoInterface.jl or Extents.jl compatible object can be used directly instead
of defining the `Extent` manually - the extent will either be detected or calculated.


You can also perform queries on any individual GeoInterface.jl compatible point, so:

```julia
LibSpatialIndex.intersects(rtree, [1.,1.])
LibSpatialIndex.intersects(rtree, (1.0, 1.0))
```
will return the ids `[1,2]` in the `rtree` constructed earlier, and

will return the ids `[1, 2]` in the `rtree` constructed earlier, and:

```julia
LibSpatialIndex.intersects(rtree, [2.,2.])
LibSpatialIndex.intersects(rtree, [2.0, 2.0])
```
will only return the vector `[2]`, because item 1 does not contain the point `[2,2]`.

will only return the vector `[2]`, because item 1 does not contain the point `[2.0, 2.0]`.

### k Nearest Neighbors
For `knn` queries,

For `knn` queries:

```julia
LibSpatialIndex.knn(rtree, [2.,2.], 1)
LibSpatialIndex.knn(rtree, [2.0, 2.0], 1)
```

returns the vector `[2]` because the item with id `2` is closest to the point `[2.0, 2.0]`, and

```julia
sort(LibSpatialIndex.knn(rtree, [2.,2.], 2))
sort(LibSpatialIndex.knn(rtree, [2.0, 2.0], 2))
```
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

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:

```julia
sort(SI.knn(rtree, [2.,2.], 3))
sort(SI.knn(rtree, [2.0, 2.0], 3))
```
will return the vector `[1,2]`.

will return the vector `[1, 2]`.
122 changes: 98 additions & 24 deletions src/LibSpatialIndex.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module LibSpatialIndex

import GeoInterface as GI

include("capi.jl")

version() = unsafe_string(C.SIDX_Version())
Expand All @@ -11,6 +13,8 @@
end

"""
RTree(ndim::Integer; kw...)

The RTree index [guttman84] is a balanced tree structure that consists of
index nodes, leaf nodes and data.

Expand All @@ -22,9 +26,12 @@
They cannot be empty though. A `fillfactor` specifies the minimum number of
entries allowed in any node. The fill factor is usually close to `70%`.

# Options
## Arguments

`ndim`: Dimensionality of the data that will be inserted.

## Keywords

* `ndim`: Dimensionality of the data that will be inserted.
* `indextype`: one of `RT_RTree` (default), `RT_MVRTree`, or `RT_TPRTree`.
* `variant`: one of `RT_Linear`, `RT_Quadratic`, or `RT_Star` (default).
* `storage`: one of `RT_Memory` (default), `RT_Disk`, or `RT_Custom`.
Expand All @@ -39,7 +46,7 @@
* `fillfactor`: The fill factor. Default is `0.7`.
* `splitdistributionfactor`: Default is `0.4`.
* `reinsertfactor`: Default is `0.3`.

# Performance

Dataset size, data density, etc. have nothing to do with capacity and page
Expand Down Expand Up @@ -152,12 +159,22 @@
end

"""
insert!(rtree::RTree, id::Integer, minvalues::Vector{Float64}, maxvalues::Vector{Float64})
insert!(rtree::RTree, id::Integer, extent::Extent)
insert!(rtree::RTree, id::Integer, obj)

Inserts an item into the `rtree` with given `id` and boundingbox specified
by `minvalues` and `maxvalues`, where the item lies within the interval
`[minvalues[i],maxvalues[i]]` for each axis `i` in 1, ..., `ndim`.
`[minvalues[i], maxvalues[i]]` for each axis `i` in 1, ..., `ndim`,
or similar for the `Extent` of `obj`.

If `obj` is passed it will be detected as a `GeoInterface.PointTrait`
and used as a point, or otherwise `GeoInterface.extent` will be called to
detect or calculate the objects `Extent`, falling back to `Extents.extent`.

In these cases `minvalues` and `maxvalues` are taken from the point or extent.
"""
function insert!(
rtree::RTree,
function insert!(rtree::RTree,
id::Integer,
minvalues::Vector{Float64},
maxvalues::Vector{Float64}
Expand All @@ -166,13 +183,30 @@
pointer(maxvalues), UInt32(length(minvalues)), Ptr{UInt8}(0), Cint(0)
)
end
function insert!(rtree::RTree, id::Integer, extent::GI.Extent)
insert!(rtree, id, _ext2vecs(extent)...)
end
insert!(rtree::RTree, id::Integer, extent::Nothing) = _not_point_or_ext_error()

Check warning on line 189 in src/LibSpatialIndex.jl

View check run for this annotation

Codecov / codecov/patch

src/LibSpatialIndex.jl#L189

Added line #L189 was not covered by tests
function insert!(rtree::RTree, id::Integer, obj)
insert!(rtree, id, GI.extent(obj))
end

"""
intersects(rtree::RTree, minvalues::Vector{Float64}, maxvalues::Vector{Float64})
intersects(rtree::RTree, extent::Extent)
intersects(rtree::RTree, obj)

Returns a vector of `id`s corresponding to items in `rtree` that intersects
the box specified by `minvalues` and `maxvalues`.

Each item intersects the interval `[minvalues[i],maxvalues[i]]` for each
axis `i` in 1, ..., `ndim`.
Each item intersects the interval `[minvalues[i], maxvalues[i]]` for each
axis `i` in 1, ..., `ndim`, or similar for the `Extent` of `obj`.

If `obj` is passed it will be detected as a `GeoInterface.PointTrait`
and used as a point, or otherwise `GeoInterface.extent` will be called to
detect or calculate the objects `Extent`, falling back to `Extents.extent`.

In these cases `minvalues` and `maxvalues` are taken from the point or extent.
"""
function intersects(
rtree::RTree,
Expand All @@ -187,21 +221,38 @@
_checkresult(result, "Index_Intersects_id: Failed to evaluate")
unsafe_wrap(Array, items[], nresults[])
end

"""
Returns a vector of `id`s corresponding to items in `rtree` that intersects
the coordinates specified by `point`.
"""
intersects(rtree::RTree, point::Vector{Float64}) = intersects(rtree, point, point)
function intersects(rtree::RTree, obj)
if GI.trait(obj) isa GI.PointTrait
intersects(rtree, _point2vec(obj))
else
intersects(rtree, GI.extent(obj))
end
end
function intersects(rtree::RTree, extent::GI.Extent)
intersects(rtree::RTree, _ext2vecs(extent)...)
end
intersects(rtree::RTree, ::Nothing) = _not_point_or_ext_error()

Check warning on line 235 in src/LibSpatialIndex.jl

View check run for this annotation

Codecov / codecov/patch

src/LibSpatialIndex.jl#L235

Added line #L235 was not covered by tests

"""
knn(rtree::RTree, minvalues::Vector{Float64}, maxvalues::Vector{Float64}, k::Integer)
knn(rtree::RTree, extent::Extent, k::Integer)
knn(rtree::RTree, obj, k::Integer)

Returns a vector of `id`s corresponding to the `k` items in `rtree`
that are nearest to the box specified by `minvalues` and `maxvalues`.
Each item intersects the interval `[minvalues[i], maxvalues[i]]` for each
axis `i` in 1, ..., `ndim`, or similar for the `Extent` of `obj`.

Each item intersects the interval `[minvalues[i],maxvalues[i]]` for each
axis `i` in 1, ..., `ndim`. If there are fewer than `k` items in `rtree`,
If there are fewer than `k` items in `rtree`,
it will return less than `k` items. On the other hand, if there are ties
between some of the items, it might return more than `k` items.

If `obj` is passed it will be detected as a `GeoInterface.PointTrait`
and used as a point, or otherwise `GeoInterface.extent` will be called to
detect or calculate the objects `Extent`, falling back to `Extents.extent`.

In these cases `minvalues` and `maxvalues` are taken from the point or extent.
"""
function knn(
rtree::RTree,
Expand All @@ -216,15 +267,38 @@
_checkresult(result, "Index_NearestNeighbors_id: Failed to evaluate")
unsafe_wrap(Array, items[], nresults[])
end
knn(rtree::RTree, point::Vector{Float64}, k::Integer) = knn(rtree, point, point, k)
knn(rtree::RTree, extent::GI.Extent, k::Integer) = knn(rtree::RTree, _ext2vecs(extent)..., k)
knn(rtree::RTree, extent::Nothing, k::Integer) = _not_point_or_ext_error()

Check warning on line 272 in src/LibSpatialIndex.jl

View check run for this annotation

Codecov / codecov/patch

src/LibSpatialIndex.jl#L272

Added line #L272 was not covered by tests
function knn(rtree::RTree, obj, k::Integer)
if GI.trait(obj) isa GI.PointTrait
knn(rtree, _point2vec(obj), k)
else
knn(rtree, GI.extent(obj), k)

Check warning on line 277 in src/LibSpatialIndex.jl

View check run for this annotation

Codecov / codecov/patch

src/LibSpatialIndex.jl#L277

Added line #L277 was not covered by tests
end
end

"""
Returns a vector of `id`s corresponding to the `k` items in `rtree`
that are nearest to the box specified by `minvalues` and `maxvalues`.
# Utils
function _ext2vecs(ex::GI.Extent)
haskey(ex, :X) && haskey(ex, :Y) || throw(ArgumentError("Extent does not have X and Y keys"))

min, max = if haskey(ex, :Z)
Float64[ex.X[1], ex.Y[1], ex.Z[1]], Float64[ex.X[2], ex.Y[2], ex.Z[1]]

Check warning on line 286 in src/LibSpatialIndex.jl

View check run for this annotation

Codecov / codecov/patch

src/LibSpatialIndex.jl#L286

Added line #L286 was not covered by tests
else
Float64[ex.X[1], ex.Y[1]], Float64[ex.X[2], ex.Y[2]]
end

return min, max
end

function _point2vec(p)
if GI.is3d(p)
Float64[GI.x(p), GI.y(p), GI.z(p)]

Check warning on line 296 in src/LibSpatialIndex.jl

View check run for this annotation

Codecov / codecov/patch

src/LibSpatialIndex.jl#L296

Added line #L296 was not covered by tests
else
Float64[GI.x(p), GI.y(p)]
end
end

_not_point_or_ext_error() = throw(ArgumentError("object is not a point, and does not have an extent"))

If there are fewer than `k` items in `rtree`, it will return less than `k`
items. On the other hand, if there are ties between some of the items,
it might return more than `k` items.
"""
knn(rtree::RTree, point::Vector{Float64}, k::Integer) = knn(rtree, point, point, k)

end # module
24 changes: 23 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using LibSpatialIndex
using Test
const SI = LibSpatialIndex
import GeoInterface as GI
import LibSpatialIndex as SI
import Aqua

@testset "Simple Tutorial" begin
# based on https://github.com/libspatialindex/libspatialindex/wiki/Simple-Tutorial
Expand Down Expand Up @@ -51,3 +53,23 @@ end
@test sort(SI.knn(rtree, [2.,2.], 1)) == [2]
@test sort(SI.knn(rtree, [2.,2.], 2)) == [1,2]
end

@testset "GeoInterface/Extents Operations" begin
rtree = SI.RTree(2)
result = SI.insert!(rtree, 1, GI.Extent(X=(0.0, 1.0), Y=(0.0, 1.0)))
@test result == SI.C.RT_None

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)])])
result = SI.insert!(rtree, 2, polygon)

@test result == SI.C.RT_None
@test SI.intersects(rtree, GI.LineString([(0.0, 0.0), (1.0, 1.0)])) == [1, 2]
@test SI.intersects(rtree, GI.Point(0.0, 0.0)) == [1, 2]
@test SI.intersects(rtree, (X=2.0, Y=2.0)) == [2]
@test sort(SI.knn(rtree, GI.Extent(X=(2.0, 2.0), Y=(2.0, 2.0)), 1)) == [2]
@test sort(SI.knn(rtree, (2.0, 2.0), 1)) == [2]
end

@testset "Aqua" begin
Aqua.test_all(LibSpatialIndex)
end