geokdtree 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|