geokdtree 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +13 -0
- data/README.md +121 -0
- data/Rakefile +30 -0
- data/example/usage.rb +65 -0
- data/ext/geokdtree/distance.c +70 -0
- data/ext/geokdtree/distance.h +38 -0
- data/ext/geokdtree/extconf.rb +7 -0
- data/ext/geokdtree/kdtree.c +782 -0
- data/ext/geokdtree/kdtree.h +135 -0
- data/geokdtree.gemspec +23 -0
- data/lib/geokdtree.rb +13 -0
- data/lib/geokdtree/tree.rb +139 -0
- data/lib/geokdtree/tree_ffi.rb +92 -0
- data/lib/geokdtree/version.rb +3 -0
- data/spec/geokdtree/geokdtree_spec.rb +209 -0
- metadata +97 -0
@@ -0,0 +1,135 @@
|
|
1
|
+
/*
|
2
|
+
This file is part of ``kdtree'', a library for working with kd-trees.
|
3
|
+
Copyright (C) 2007-2011 John Tsiombikas <nuclear@member.fsf.org>
|
4
|
+
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
7
|
+
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
9
|
+
list of conditions and the following disclaimer.
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
12
|
+
and/or other materials provided with the distribution.
|
13
|
+
3. The name of the author may not be used to endorse or promote products
|
14
|
+
derived from this software without specific prior written permission.
|
15
|
+
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
17
|
+
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
18
|
+
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
19
|
+
EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
20
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
|
21
|
+
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
22
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
23
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
24
|
+
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
|
25
|
+
OF SUCH DAMAGE.
|
26
|
+
*/
|
27
|
+
|
28
|
+
/*
|
29
|
+
Feb 2013
|
30
|
+
included in the ruby-ffi-kdtree gem by Colin Surprenant <colin.surprenant@gmail.com>
|
31
|
+
original source code from https://code.google.com/p/kdtree/
|
32
|
+
using version 0.5.6 dated Nov 2011
|
33
|
+
*/
|
34
|
+
|
35
|
+
#ifndef _KDTREE_H_
|
36
|
+
#define _KDTREE_H_
|
37
|
+
|
38
|
+
#ifdef __cplusplus
|
39
|
+
extern "C" {
|
40
|
+
#endif
|
41
|
+
|
42
|
+
struct kdtree;
|
43
|
+
struct kdres;
|
44
|
+
|
45
|
+
#define GEO_UNITS_MI 0
|
46
|
+
#define GEO_UNITS_KM 1
|
47
|
+
|
48
|
+
/* create a kd-tree for "k"-dimensional data */
|
49
|
+
struct kdtree *kd_create(int k);
|
50
|
+
|
51
|
+
/* free the struct kdtree */
|
52
|
+
void kd_free(struct kdtree *tree);
|
53
|
+
|
54
|
+
/* remove all the elements from the tree */
|
55
|
+
void kd_clear(struct kdtree *tree);
|
56
|
+
|
57
|
+
/* if called with non-null 2nd argument, the function provided
|
58
|
+
* will be called on data pointers (see kd_insert) when nodes
|
59
|
+
* are to be removed from the tree.
|
60
|
+
*/
|
61
|
+
void kd_data_destructor(struct kdtree *tree, void (*destr)(void*));
|
62
|
+
|
63
|
+
/* insert a node, specifying its position, and optional data */
|
64
|
+
int kd_insert(struct kdtree *tree, const double *pos, void *data);
|
65
|
+
int kd_insert2(struct kdtree *tree, double x, double y, void *data);
|
66
|
+
int kd_insert3(struct kdtree *tree, double x, double y, double z, void *data);
|
67
|
+
|
68
|
+
/* Find the nearest node from a given point.
|
69
|
+
*
|
70
|
+
* This function returns a pointer to a result set with at most one element.
|
71
|
+
*/
|
72
|
+
struct kdres *kd_nearest(struct kdtree *tree, const double *pos);
|
73
|
+
struct kdres *kd_nearest2(struct kdtree *tree, double x, double y);
|
74
|
+
struct kdres *kd_nearest3(struct kdtree *tree, double x, double y, double z);
|
75
|
+
|
76
|
+
/* Find the N nearest nodes from a given point.
|
77
|
+
*
|
78
|
+
* This function returns a pointer to a result set, with at most N elements,
|
79
|
+
* which can be manipulated with the kd_res_* functions.
|
80
|
+
* The returned pointer can be null as an indication of an error. Otherwise
|
81
|
+
* a valid result set is always returned which may contain 0 or more elements.
|
82
|
+
* The result set must be deallocated with kd_res_free after use.
|
83
|
+
*/
|
84
|
+
/*
|
85
|
+
struct kdres *kd_nearest_n(struct kdtree *tree, const double *pos, int num);
|
86
|
+
struct kdres *kd_nearest_nf(struct kdtree *tree, const float *pos, int num);
|
87
|
+
struct kdres *kd_nearest_n3(struct kdtree *tree, double x, double y, double z);
|
88
|
+
struct kdres *kd_nearest_n3f(struct kdtree *tree, float x, float y, float z);
|
89
|
+
*/
|
90
|
+
|
91
|
+
/* Find any nearest nodes from a given point within a range.
|
92
|
+
*
|
93
|
+
* This function returns a pointer to a result set, which can be manipulated
|
94
|
+
* by the kd_res_* functions.
|
95
|
+
* The returned pointer can be null as an indication of an error. Otherwise
|
96
|
+
* a valid result set is always returned which may contain 0 or more elements.
|
97
|
+
* The result set must be deallocated with kd_res_free after use.
|
98
|
+
*/
|
99
|
+
struct kdres *kd_nearest_range(struct kdtree *tree, const double *pos, const double range);
|
100
|
+
struct kdres *kd_nearest_range2(struct kdtree *tree, double x, double y, double range);
|
101
|
+
struct kdres *kd_nearest_range3(struct kdtree *tree, double x, double y, double z, double range);
|
102
|
+
struct kdres *kd_nearest_geo_range(struct kdtree *kd, double lat, double lng, double range, int units);
|
103
|
+
|
104
|
+
/* frees a result set returned by kd_nearest_range() */
|
105
|
+
void kd_res_free(struct kdres *set);
|
106
|
+
|
107
|
+
/* returns the size of the result set (in elements) */
|
108
|
+
int kd_res_size(struct kdres *set);
|
109
|
+
|
110
|
+
/* rewinds the result set iterator */
|
111
|
+
void kd_res_rewind(struct kdres *set);
|
112
|
+
|
113
|
+
/* returns non-zero if the set iterator reached the end after the last element */
|
114
|
+
int kd_res_end(struct kdres *set);
|
115
|
+
|
116
|
+
/* advances the result set iterator, returns non-zero on success, zero if
|
117
|
+
* there are no more elements in the result set.
|
118
|
+
*/
|
119
|
+
int kd_res_next(struct kdres *set);
|
120
|
+
|
121
|
+
/* returns the data pointer (can be null) of the current result set item
|
122
|
+
* and optionally sets its position to the pointers(s) if not null.
|
123
|
+
*/
|
124
|
+
void *kd_res_item(struct kdres *set, double *pos);
|
125
|
+
void *kd_res_item3(struct kdres *set, double *x, double *y, double *z);
|
126
|
+
|
127
|
+
/* equivalent to kd_res_item(set, 0) */
|
128
|
+
void *kd_res_item_data(struct kdres *set);
|
129
|
+
|
130
|
+
|
131
|
+
#ifdef __cplusplus
|
132
|
+
}
|
133
|
+
#endif
|
134
|
+
|
135
|
+
#endif /* _KDTREE_H_ */
|
data/geokdtree.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'geokdtree/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "geokdtree"
|
8
|
+
gem.version = Geokdtree::VERSION
|
9
|
+
gem.authors = ["Colin Surprenant"]
|
10
|
+
gem.email = ["colin.surprenant@gmail.com"]
|
11
|
+
gem.description = "Ruby & JRuby gem with a fast k-d tree C implementation using FFI bindings with support for latitude/longitude and geo distance range search"
|
12
|
+
gem.summary = "Ruby & JRuby FFI gem with a fast k-d tree C implementation with geo support"
|
13
|
+
gem.homepage = "http://github.com/colinsurprenant/geokdtree"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
gem.extensions = ["ext/geokdtree/extconf.rb"]
|
20
|
+
|
21
|
+
gem.add_development_dependency "rspec"
|
22
|
+
gem.add_runtime_dependency "ffi"
|
23
|
+
end
|
data/lib/geokdtree.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'ffi'
|
2
|
+
|
3
|
+
module Geokdtree
|
4
|
+
module C
|
5
|
+
extend FFI::Library
|
6
|
+
|
7
|
+
ffi_lib File.dirname(__FILE__) + "/" + (FFI::Platform.mac? ? "geokdtree.bundle" : FFI.map_library_name("geokdtree"))
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'geokdtree/version'
|
12
|
+
require 'geokdtree/tree_ffi'
|
13
|
+
require 'geokdtree/tree'
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module Geokdtree
|
2
|
+
|
3
|
+
ResultPoint = Struct.new(:point, :data)
|
4
|
+
|
5
|
+
RADIUS_MI = 3958.760
|
6
|
+
RADIUS_KM = 6371.0
|
7
|
+
KM_PER_MI = 1.609
|
8
|
+
|
9
|
+
GEO_UNITS_MI = 0
|
10
|
+
GEO_UNITS_KM = 1
|
11
|
+
|
12
|
+
class KdtreePointer < FFI::AutoPointer
|
13
|
+
|
14
|
+
def self.release(ptr)
|
15
|
+
# ptr is the original pointer you passed in to create VectorPointer
|
16
|
+
C::kd_free(ptr)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Tree
|
21
|
+
|
22
|
+
def initialize(dimensions)
|
23
|
+
@dimensions = dimensions
|
24
|
+
@tree = KdtreePointer.new(C::kd_create(dimensions))
|
25
|
+
@data_cache = {}
|
26
|
+
@data_id = 0
|
27
|
+
end
|
28
|
+
|
29
|
+
# insert a point of n dimensions and arbritary associated object
|
30
|
+
# @param point [Array<Float>] array of n dimensions of numeric coordinates
|
31
|
+
# @param data [Object] arbritary object stored with point
|
32
|
+
# @return [Tree] self
|
33
|
+
def insert(point, data = nil)
|
34
|
+
# here we don't actually pass a pointer to our data but the object numeric key encoded into the pointer
|
35
|
+
r = case point.size
|
36
|
+
when 2 then C::kd_insert2(@tree, point[0].to_f, point[1].to_f, cache(data))
|
37
|
+
when 3 then C::kd_insert3(@tree, point[0].to_f, point[1].to_f, point[2].to_f, cache(data))
|
38
|
+
else C::kd_insert(@tree, Tree.allocate_doubles(point), cache(data))
|
39
|
+
end
|
40
|
+
raise("error on Tree#insert, code=#{r}") unless r.zero?
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
# find the given point nearest neighbor
|
45
|
+
# @param point [Array<Float>] array of n dimensions of numeric coordinates
|
46
|
+
# @return [ResultPoint] the nearest neighbor, nil if no neighbor
|
47
|
+
def nearest(point)
|
48
|
+
set = case point.size
|
49
|
+
when 2 then C::kd_nearest2(@tree, point[0].to_f, point[1].to_f)
|
50
|
+
when 3 then C::kd_nearest3(@tree, point[0].to_f, point[1].to_f, point[2].to_f)
|
51
|
+
else C::kd_nearest(@tree, Tree.allocate_doubles(point))
|
52
|
+
end
|
53
|
+
return nil if set.null?
|
54
|
+
retrieve_results(set).first
|
55
|
+
end
|
56
|
+
|
57
|
+
# find all the given point neighbors within the given Euclidean range
|
58
|
+
# @param point [Array<Float>] array of n dimensions of numeric coordinates
|
59
|
+
# @param range [Float] range units expressed as a Euclidean distance
|
60
|
+
# @return [Array<ResultPoint>] the point neighbors within range, [] if none
|
61
|
+
def nearest_range(point, range)
|
62
|
+
set = case point.size
|
63
|
+
when 2 then C::kd_nearest_range2(@tree, point[0].to_f, point[1].to_f, range.to_f)
|
64
|
+
when 3 then C::kd_nearest_range3(@tree, point[0].to_f, point[1].to_f, point[2].to_f, range.to_f)
|
65
|
+
else C::kd_nearest_range(@tree, Tree.allocate_doubles(point), range.to_f)
|
66
|
+
end
|
67
|
+
return [] if set.null?
|
68
|
+
retrieve_results(set)
|
69
|
+
end
|
70
|
+
|
71
|
+
# find all the given point neighbors within the given geo range
|
72
|
+
# @param point [Array<Float>] [latitude, longitude] array
|
73
|
+
# @param range [Float] range units in miles or kms, default in miles
|
74
|
+
# @param units [Symbol] :mi or :km, default :mi
|
75
|
+
# @return [Array<ResultPoint>] the point neighbors within range, [] if none
|
76
|
+
def nearest_geo_range(point, range, units = :mi)
|
77
|
+
raise("must have 2 dimensions for geo methods") unless @dimensions == 2
|
78
|
+
set = C::kd_nearest_geo_range(@tree, point[0].to_f, point[1].to_f, range.to_f, units == :mi ? GEO_UNITS_MI : GEO_UNITS_KM)
|
79
|
+
return [] if set.null?
|
80
|
+
retrieve_results(set)
|
81
|
+
end
|
82
|
+
|
83
|
+
# compute the Euclidean distance between 2 points of n dimensions
|
84
|
+
# @param pointa [Array<Float>] array of n dimensions of numeric coordinates
|
85
|
+
# @param pointb [Array<Float>] array of n dimensions of numeric coordinates
|
86
|
+
# @return [Float] the Euclidean distance between the 2 points
|
87
|
+
def self.distance(pointa, pointb)
|
88
|
+
case k = pointa.size
|
89
|
+
when 2 then C::euclidean_distance2(pointa[0].to_f, pointa[1].to_f, pointb[0].to_f, pointb[1].to_f)
|
90
|
+
else C::euclidean_distance(allocate_doubles(pointa), allocate_doubles(pointb), k)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# compute the geo distance between 2 points of latitude, longitude
|
95
|
+
# @param pointa [Array<Float>] [latitude, longitude] array
|
96
|
+
# @param pointb [Array<Float>] [latitude, longitude] array
|
97
|
+
# @param units [Symbol] :mi or :km, default :mi
|
98
|
+
# @return [Float] the geo distance between the 2 points
|
99
|
+
def self.geo_distance(pointa, pointb, units = :mi)
|
100
|
+
C::slc_distance2(pointa[0].to_f, pointa[1].to_f, pointb[0].to_f, pointb[1].to_f, units == :mi ? RADIUS_MI : RADIUS_KM)
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
# store data in cache hash and return pointer encoded numeric object key
|
106
|
+
# @param data [Object] arbritary object
|
107
|
+
# @return [FFI::Pointer] pointer encoded numeric object key
|
108
|
+
def cache(data)
|
109
|
+
return nil unless data
|
110
|
+
@data_cache[@data_id += 1] = data
|
111
|
+
FFI::Pointer.new(@data_id)
|
112
|
+
end
|
113
|
+
|
114
|
+
# allocate managed memory pointer for given doubles array
|
115
|
+
# @param doubles [Array<Fixnum>] array doubles numbers
|
116
|
+
# @return [FFI::MemoryPointer] pointer to allocated & copied doubles
|
117
|
+
def self.allocate_doubles(doubles)
|
118
|
+
ptr = FFI::MemoryPointer.new(:double, doubles.size)
|
119
|
+
ptr.write_array_of_double(doubles)
|
120
|
+
ptr
|
121
|
+
end
|
122
|
+
|
123
|
+
# walk a result set and transpose into result array
|
124
|
+
# @param set [FFI::Pointer] the result set from tree search functions
|
125
|
+
# @return [Array<ResultPoint>] transposed result items
|
126
|
+
def retrieve_results(set)
|
127
|
+
results = []
|
128
|
+
while C::kd_res_end(set) == 0
|
129
|
+
point_ptr = FFI::MemoryPointer.new(:double, @dimensions) # to hold point result
|
130
|
+
data_ptr = C::kd_res_item(set, point_ptr) # fetch single result item
|
131
|
+
results << ResultPoint.new(point_ptr.read_array_of_double(@dimensions), data_ptr.null? ? nil : @data_cache[data_ptr.address])
|
132
|
+
C::kd_res_next(set)
|
133
|
+
end
|
134
|
+
C::kd_res_free(set)
|
135
|
+
results
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'ffi'
|
2
|
+
|
3
|
+
module Geokdtree::C
|
4
|
+
# /* create a kd-tree for "k"-dimensional data */
|
5
|
+
# struct kdtree *kd_create(int k);
|
6
|
+
attach_function "kd_create", "kd_create", [:int], :pointer
|
7
|
+
|
8
|
+
# /* free the struct kdtree */
|
9
|
+
# void kd_free(struct kdtree *tree);
|
10
|
+
attach_function "kd_free", "kd_free", [:pointer], :void
|
11
|
+
|
12
|
+
# /* remove all the elements from the tree */
|
13
|
+
# void kd_clear(struct kdtree *tree);
|
14
|
+
attach_function "kd_clear", "kd_clear", [:pointer], :void
|
15
|
+
|
16
|
+
# /* insert a node, specifying its position, and optional data */
|
17
|
+
# int kd_insert(struct kdtree *tree, const double *pos, void *data);
|
18
|
+
attach_function "kd_insert", "kd_insert", [:pointer, :pointer, :pointer], :int
|
19
|
+
|
20
|
+
# int kd_insert2(struct kdtree *tree, double x, double y, void *data);
|
21
|
+
attach_function "kd_insert2", "kd_insert2", [:pointer, :double, :double, :pointer], :int
|
22
|
+
|
23
|
+
# int kd_insert3(struct kdtree *tree, double x, double y, double z, void *data);
|
24
|
+
attach_function "kd_insert3", "kd_insert3", [:pointer, :double, :double, :double, :pointer], :int
|
25
|
+
|
26
|
+
# /* Find the nearest node from a given point.
|
27
|
+
# *
|
28
|
+
# * This function returns a pointer to a result set with at most one element.
|
29
|
+
# */
|
30
|
+
# struct kdres *kd_nearest(struct kdtree *tree, const double *pos);
|
31
|
+
attach_function "kd_nearest", "kd_nearest", [:pointer, :pointer], :pointer
|
32
|
+
|
33
|
+
# struct kdres *kd_nearest2(struct kdtree *tree, double x, double y);
|
34
|
+
attach_function "kd_nearest2", "kd_nearest2", [:pointer, :double, :double], :pointer
|
35
|
+
|
36
|
+
# struct kdres *kd_nearest2(struct kdtree *tree, double x, double y, double z);
|
37
|
+
attach_function "kd_nearest3", "kd_nearest3", [:pointer, :double, :double, :double], :pointer
|
38
|
+
|
39
|
+
# /* Find any nearest nodes from a given point within a range.
|
40
|
+
# *
|
41
|
+
# * This function returns a pointer to a result set, which can be manipulated
|
42
|
+
# * by the kd_res_* functions.
|
43
|
+
# * The returned pointer can be null as an indication of an error. Otherwise
|
44
|
+
# * a valid result set is always returned which may contain 0 or more elements.
|
45
|
+
# * The result set must be deallocated with kd_res_free after use.
|
46
|
+
# */
|
47
|
+
# struct kdres *kd_nearest_range(struct kdtree *tree, const double *pos, double range);
|
48
|
+
attach_function "kd_nearest_range", "kd_nearest_range", [:pointer, :pointer, :double], :pointer
|
49
|
+
|
50
|
+
# struct kdres *kd_nearest_range2(struct kdtree *tree, double x, double y, double range);
|
51
|
+
attach_function "kd_nearest_range2", "kd_nearest_range2", [:pointer, :double, :double, :double], :pointer
|
52
|
+
|
53
|
+
# struct kdres *kd_nearest_range3(struct kdtree *tree, double x, double y, double z, double range);
|
54
|
+
attach_function "kd_nearest_range3", "kd_nearest_range3", [:pointer, :double, :double, :double, :double], :pointer
|
55
|
+
|
56
|
+
# struct kdres *kd_nearest_geo_range(struct kdtree *kd, double lat, double lng, double range, int units)
|
57
|
+
attach_function "kd_nearest_geo_range", "kd_nearest_geo_range", [:pointer, :double, :double, :double, :int], :pointer
|
58
|
+
|
59
|
+
# void *kd_res_item(struct kdres *set, double *pos);
|
60
|
+
attach_function "kd_res_item", "kd_res_item", [:pointer, :pointer], :pointer
|
61
|
+
|
62
|
+
# /* frees a result set returned by kd_nearest_range() */
|
63
|
+
# void kd_res_free(struct kdres *set);
|
64
|
+
attach_function "kd_res_free", "kd_res_free", [:pointer], :void
|
65
|
+
|
66
|
+
# /* advances the result set iterator, returns non-zero on success, zero if
|
67
|
+
# * there are no more elements in the result set.
|
68
|
+
# */
|
69
|
+
# int kd_res_next(struct kdres *set);
|
70
|
+
attach_function "kd_res_next", "kd_res_next", [:pointer], :int
|
71
|
+
|
72
|
+
# /* returns non-zero if the set iterator reached the end after the last element */
|
73
|
+
# int kd_res_end(struct kdres *set);
|
74
|
+
attach_function "kd_res_end", "kd_res_end", [:pointer], :int
|
75
|
+
|
76
|
+
# /* return the Euclidean distance from two points in k dimensions */
|
77
|
+
# double euclidean_distance( double *a, double *b, int k );
|
78
|
+
attach_function "euclidean_distance", "euclidean_distance", [:pointer, :pointer, :int], :double
|
79
|
+
|
80
|
+
# /* return the Euclidean distance from two points in two dimensions */
|
81
|
+
# double euclidean_distance2(double ax, double ay, double bx, double by);
|
82
|
+
attach_function "euclidean_distance2", "euclidean_distance2", [:double, :double, :double, :double], :double
|
83
|
+
|
84
|
+
# /* return the geo distance from two lat/lng points using the spherical law of cosines */
|
85
|
+
# double slc_distance(double *a, double *b, double radius) {
|
86
|
+
attach_function "slc_distance", "slc_distance", [:pointer, :pointer, :double], :double
|
87
|
+
|
88
|
+
# /* return the geo distance from two lat/lng points using the spherical law of cosines */
|
89
|
+
# double slc_distance2(double lat1, double lng1, double lat2, double lng2, double radius) {
|
90
|
+
attach_function "slc_distance2", "slc_distance2", [:double, :double, :double, :double, :double], :double
|
91
|
+
|
92
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'geokdtree'
|
3
|
+
|
4
|
+
describe Geokdtree::Tree do
|
5
|
+
|
6
|
+
before do
|
7
|
+
@points = {
|
8
|
+
:top_left => [-1, -1],
|
9
|
+
:mid_upper_left => [-0.5, -0.5],
|
10
|
+
:top_right => [1, -1],
|
11
|
+
:mid_upper_right => [0.5, -0.5],
|
12
|
+
:center => [0, 0],
|
13
|
+
:mid_lower_left => [-0.5, 0.5],
|
14
|
+
:bottom_left => [-1, 1],
|
15
|
+
:mid_lower_right => [0.5, 0.5],
|
16
|
+
:bottom_right => [1, 1],
|
17
|
+
}
|
18
|
+
|
19
|
+
@places = {
|
20
|
+
:seattle => [47.6, -122.3],
|
21
|
+
:portland => [45.5, -122.8],
|
22
|
+
:new_york => [40.7, -74.0],
|
23
|
+
:los_angeles => [34.1, -118.2],
|
24
|
+
:montreal => [45.50, -73.55],
|
25
|
+
}
|
26
|
+
@insert_places = [:seattle, :portland, :new_york]
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should initialize" do
|
30
|
+
tree = Geokdtree::Tree.new(2)
|
31
|
+
tree.should_not be_nil
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should find neighbors in a 2D tree without data" do
|
35
|
+
# this spec inspired by https://github.com/sdeming/ffi-kdtree/blob/master/spec/find_nearest_spec.rb
|
36
|
+
tree = Geokdtree::Tree.new(2)
|
37
|
+
@points.each{|k, p| tree.insert(p)}
|
38
|
+
|
39
|
+
result = tree.nearest([0, 0])
|
40
|
+
result.point.should == [0, 0]
|
41
|
+
result.data.should be_nil
|
42
|
+
|
43
|
+
result = tree.nearest([0.3, 0.3])
|
44
|
+
result.point.should == [0.5, 0.5]
|
45
|
+
result.data.should be_nil
|
46
|
+
|
47
|
+
result = tree.nearest([-0.3, -0.3])
|
48
|
+
result.point.should == [-0.5, -0.5]
|
49
|
+
result.data.should be_nil
|
50
|
+
|
51
|
+
result = tree.nearest([0.1,0.1])
|
52
|
+
result.point.should == [0, 0]
|
53
|
+
result.data.should be_nil
|
54
|
+
|
55
|
+
results = tree.nearest_range([0, 0], 2)
|
56
|
+
results.size.should == 9
|
57
|
+
|
58
|
+
results = tree.nearest_range([0, 0], 1)
|
59
|
+
results.size.should == 5 # center + mids
|
60
|
+
|
61
|
+
tree.nearest_range([10, 10], 1).should == []
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should find neighbors in a 2D tree using data" do
|
65
|
+
# this spec inspired by https://github.com/sdeming/ffi-kdtree/blob/master/spec/find_nearest_spec.rb
|
66
|
+
tree = Geokdtree::Tree.new(2)
|
67
|
+
|
68
|
+
@points.each{|k, p| tree.insert(p, k)}
|
69
|
+
|
70
|
+
result = tree.nearest([0, 0])
|
71
|
+
result.point.should == [0, 0]
|
72
|
+
result.data.should == :center
|
73
|
+
|
74
|
+
result = tree.nearest([0.3, 0.3])
|
75
|
+
result.point.should == [0.5, 0.5]
|
76
|
+
result.data.should == :mid_lower_right
|
77
|
+
|
78
|
+
result = tree.nearest([-0.3, -0.3])
|
79
|
+
result.point.should == [-0.5, -0.5]
|
80
|
+
result.data.should == :mid_upper_left
|
81
|
+
|
82
|
+
result = tree.nearest([0.1,0.1])
|
83
|
+
result.point.should == [0, 0]
|
84
|
+
result.data.should == :center
|
85
|
+
|
86
|
+
results = tree.nearest_range([0, 0], 2)
|
87
|
+
results.size.should == 9
|
88
|
+
results.map(&:data).sort.should == @points.to_a.map(&:first).sort
|
89
|
+
|
90
|
+
results = tree.nearest_range([0, 0], 1)
|
91
|
+
results.map(&:data).sort.should == [:mid_upper_left, :mid_upper_right, :center, :mid_lower_left, :mid_lower_right].sort
|
92
|
+
|
93
|
+
results = tree.nearest_range([10, 10], 1).should == []
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should not find any neighbor in an empty tree" do
|
97
|
+
tree = Geokdtree::Tree.new(2)
|
98
|
+
tree.nearest([0, 0]).should be_nil
|
99
|
+
tree = Geokdtree::Tree.new(3)
|
100
|
+
tree.nearest([0, 0, 0]).should be_nil
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should find nearest by latlong" do
|
104
|
+
tree = Geokdtree::Tree.new(2)
|
105
|
+
@insert_places.each{|p| tree.insert(@places[p], p)}
|
106
|
+
|
107
|
+
tree.nearest(@places[:los_angeles]).data.should == :portland
|
108
|
+
tree.nearest(@places[:montreal]).data.should == :new_york
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should find nearest geo range by latlong" do
|
112
|
+
tree = Geokdtree::Tree.new(2)
|
113
|
+
@insert_places.each{|p| tree.insert(@places[p], p)}
|
114
|
+
|
115
|
+
tree.nearest_geo_range(@places[:los_angeles], 0).size.should == 0
|
116
|
+
tree.nearest_geo_range(@places[:los_angeles], 800).size.should == 0
|
117
|
+
|
118
|
+
tree.nearest_geo_range(@places[:los_angeles], 900).size.should == 1
|
119
|
+
tree.nearest_geo_range(@places[:los_angeles], 900).first.data.should == :portland
|
120
|
+
tree.nearest_geo_range(@places[:los_angeles], 900, :mi).size.should == 1
|
121
|
+
tree.nearest_geo_range(@places[:los_angeles], 900, :mi).first.data.should == :portland
|
122
|
+
|
123
|
+
tree.nearest_geo_range(@places[:los_angeles], 900, :km).size.should == 0
|
124
|
+
tree.nearest_geo_range(@places[:los_angeles], 1448, :km).size.should == 1
|
125
|
+
tree.nearest_geo_range(@places[:los_angeles], 1448, :km).first.data.should == :portland
|
126
|
+
|
127
|
+
tree.nearest_geo_range(@places[:los_angeles], 957).size.should == 2
|
128
|
+
tree.nearest_geo_range(@places[:los_angeles], 957).map(&:data).sort.should == [:portland, :seattle].sort
|
129
|
+
tree.nearest_geo_range(@places[:los_angeles], 957, :mi).size.should == 2
|
130
|
+
tree.nearest_geo_range(@places[:los_angeles], 957, :mi).map(&:data).sort.should == [:portland, :seattle].sort
|
131
|
+
tree.nearest_geo_range(@places[:los_angeles], 1540, :km).size.should == 2
|
132
|
+
tree.nearest_geo_range(@places[:los_angeles], 1540, :km).map(&:data).sort.should == [:portland, :seattle].sort
|
133
|
+
|
134
|
+
tree.nearest_geo_range(@places[:los_angeles], 2500).size.should == 3
|
135
|
+
tree.nearest_geo_range(@places[:los_angeles], 2500).map(&:data).sort.should == [:portland, :seattle, :new_york].sort
|
136
|
+
tree.nearest_geo_range(@places[:los_angeles], 2500, :mi).size.should == 3
|
137
|
+
tree.nearest_geo_range(@places[:los_angeles], 2500, :mi).map(&:data).sort.should == [:portland, :seattle, :new_york].sort
|
138
|
+
tree.nearest_geo_range(@places[:los_angeles], 4023, :km).size.should == 3
|
139
|
+
tree.nearest_geo_range(@places[:los_angeles], 4023, :km).map(&:data).sort.should == [:portland, :seattle, :new_york].sort
|
140
|
+
|
141
|
+
tree.insert(@places[:los_angeles], :los_angeles)
|
142
|
+
|
143
|
+
tree.nearest_geo_range(@places[:los_angeles], 0).size.should == 1
|
144
|
+
tree.nearest_geo_range(@places[:los_angeles], 0).first.data.should == :los_angeles
|
145
|
+
tree.nearest_geo_range(@places[:los_angeles], 0, :mi).size.should == 1
|
146
|
+
tree.nearest_geo_range(@places[:los_angeles], 0, :mi).first.data.should == :los_angeles
|
147
|
+
tree.nearest_geo_range(@places[:los_angeles], 0, :km).size.should == 1
|
148
|
+
tree.nearest_geo_range(@places[:los_angeles], 0, :km).first.data.should == :los_angeles
|
149
|
+
|
150
|
+
tree.nearest_geo_range(@places[:montreal], 900).size.should == 1
|
151
|
+
tree.nearest_geo_range(@places[:montreal], 900).first.data.should == :new_york
|
152
|
+
tree.nearest_geo_range(@places[:montreal], 900, :mi).size.should == 1
|
153
|
+
tree.nearest_geo_range(@places[:montreal], 900, :mi).first.data.should == :new_york
|
154
|
+
tree.nearest_geo_range(@places[:montreal], 1448, :km).size.should == 1
|
155
|
+
tree.nearest_geo_range(@places[:montreal], 1448, :km).first.data.should == :new_york
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should find nearest using random points" do
|
159
|
+
tree = Geokdtree::Tree.new(2)
|
160
|
+
random_points = (0...1000).map{|i| [[rand_coord, rand_coord], i]}
|
161
|
+
random_points.each{|p| tree.insert(p.first, p.last)}
|
162
|
+
|
163
|
+
100.times do
|
164
|
+
random_point = [rand_coord, rand_coord]
|
165
|
+
|
166
|
+
# tree search
|
167
|
+
result = tree.nearest(random_point)
|
168
|
+
kd_nearest_point = random_points[result.data].first
|
169
|
+
|
170
|
+
# sort search
|
171
|
+
sort_nearest_point = random_points.sort_by{|i| Geokdtree::Tree.distance(i.first, random_point)}.first.first
|
172
|
+
|
173
|
+
Geokdtree::Tree.distance(sort_nearest_point, random_point).should == Geokdtree::Tree.distance(kd_nearest_point, random_point)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should compute geo distance" do
|
178
|
+
Geokdtree::Tree.geo_distance(@places[:los_angeles], @places[:portland]).round(0).should == 824
|
179
|
+
Geokdtree::Tree.geo_distance(@places[:los_angeles], @places[:seattle]).round(0).should == 957
|
180
|
+
Geokdtree::Tree.geo_distance(@places[:los_angeles], @places[:new_york]).round(0).should == 2442
|
181
|
+
Geokdtree::Tree.geo_distance(@places[:montreal], @places[:new_york]).round(0).should == 332
|
182
|
+
|
183
|
+
Geokdtree::Tree.geo_distance(@places[:los_angeles], @places[:portland], :mi).round(0).should == 824
|
184
|
+
Geokdtree::Tree.geo_distance(@places[:los_angeles], @places[:seattle], :mi).round(0).should == 957
|
185
|
+
Geokdtree::Tree.geo_distance(@places[:los_angeles], @places[:new_york], :mi).round(0).should == 2442
|
186
|
+
Geokdtree::Tree.geo_distance(@places[:montreal], @places[:new_york],:mi).round(0).should == 332
|
187
|
+
|
188
|
+
Geokdtree::Tree.geo_distance(@places[:los_angeles], @places[:portland], :km).round(0).should == 1327
|
189
|
+
Geokdtree::Tree.geo_distance(@places[:los_angeles], @places[:seattle], :km).round(0).should == 1540
|
190
|
+
Geokdtree::Tree.geo_distance(@places[:los_angeles], @places[:new_york], :km).round(0).should == 3931
|
191
|
+
Geokdtree::Tree.geo_distance(@places[:montreal], @places[:new_york], :km).round(0).should == 535
|
192
|
+
end
|
193
|
+
|
194
|
+
it "should compute distance" do
|
195
|
+
Geokdtree::Tree.distance([-1, 1], [1, 1]).should == 2
|
196
|
+
Geokdtree::Tree.distance([-1, 1], [-1, -1]).should == 2
|
197
|
+
Geokdtree::Tree.distance([1, 1], [1, 0]).should == 1
|
198
|
+
Geokdtree::Tree.distance([0, 0], [-1, 0]).should == 1
|
199
|
+
Geokdtree::Tree.distance([0, 0], [0, -1]).should == 1
|
200
|
+
Geokdtree::Tree.distance([-0.5, -0.5], [0.5, -0.5]).should == 1
|
201
|
+
Geokdtree::Tree.distance([-0.5, -0.5], [0, -0.5]).should == 0.5
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
def rand_coord
|
207
|
+
rand(0) * 10 - 5
|
208
|
+
end
|
209
|
+
|