quadtree 1.0.2 → 1.0.8
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +28 -0
- data/.rubocop_todo.yml +76 -0
- data/CODE_OF_CONDUCT.md +74 -74
- data/Gemfile +6 -6
- data/LICENSE.txt +21 -21
- data/README.md +7 -2
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/lib/quadtree.rb +9 -6
- data/lib/quadtree/axis_aligned_bounding_box.rb +91 -18
- data/lib/quadtree/point.rb +155 -60
- data/lib/quadtree/quadtree.rb +156 -50
- data/lib/quadtree/unknown_type_error.rb +8 -0
- data/lib/quadtree/version.rb +5 -1
- data/quadtree.gemspec +50 -22
- metadata +75 -22
- data/.editorconfig +0 -12
- data/.gitignore +0 -208
- data/.rspec +0 -3
- data/.travis.yml +0 -5
- data/bin/console +0 -7
- data/bin/setup +0 -8
- data/bitbucket-pipelines.yml +0 -17
data/lib/quadtree/point.rb
CHANGED
@@ -1,66 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Quadtree
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
4
|
+
# Simple coordinate object to represent points in some space.
|
5
|
+
class Point
|
6
|
+
# The X coordinate of this instance.
|
7
|
+
# @return [Float, Integer] X coordinate.
|
8
|
+
attr_accessor :x
|
9
|
+
|
10
|
+
# The Y coordinate of this instance.
|
11
|
+
# @return [Float, Integer] Y coordinate.
|
12
|
+
attr_accessor :y
|
13
|
+
|
14
|
+
# Optional payload attached to this instance.
|
15
|
+
# @return [Object] payload attached to this instance.
|
16
|
+
attr_accessor :data
|
17
|
+
|
18
|
+
# @param x [Float, Integer] X coordinate.
|
19
|
+
# @param y [Float, Integer] Y coordinate.
|
20
|
+
# @param data [Object] payload payload attached to this instance
|
21
|
+
# (optional).
|
22
|
+
# @raise [UnknownTypeError] if one or more input parameters (+x+ and +y+)
|
23
|
+
# has the wrong type.
|
24
|
+
def initialize(x, y, data = nil)
|
25
|
+
self.x = get_typed_numeric(x)
|
26
|
+
self.y = get_typed_numeric(y)
|
27
|
+
|
28
|
+
self.data = data unless data.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Create a Hash for this {Point}.
|
33
|
+
#
|
34
|
+
# @return [Hash] Hash representation of this {Point}.
|
35
|
+
#
|
36
|
+
def to_h
|
37
|
+
{
|
38
|
+
'x': x,
|
39
|
+
'y': y,
|
40
|
+
'data': process_data(data)
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Create a Hash for this {Point}.
|
46
|
+
#
|
47
|
+
# @return [Hash] Hash representation of this {Point}.
|
48
|
+
#
|
49
|
+
def to_hash
|
50
|
+
to_h
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Create a JSON String representation of this {Point}.
|
55
|
+
#
|
56
|
+
# @return [String] JSON String of this {Point}.
|
57
|
+
#
|
58
|
+
def to_json(*_args)
|
59
|
+
require 'json'
|
60
|
+
to_h.to_json
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# Create a String for this {Point}.
|
65
|
+
#
|
66
|
+
# @return [String] String representation of this {Point}.
|
67
|
+
#
|
68
|
+
def to_s
|
69
|
+
to_h.to_s
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
# Construct a {Quadtree::Point} from a JSON String.
|
74
|
+
#
|
75
|
+
# @param [String] json_data input JSON String.
|
76
|
+
#
|
77
|
+
# @return [Quadtree::Point] the {Quadtree::Point} contained in the JSON String.
|
78
|
+
#
|
79
|
+
def self.from_json(json_data)
|
80
|
+
new(json_data['x'], json_data['y'], json_data['data'])
|
81
|
+
end
|
82
|
+
|
83
|
+
# This will calculate distance to another {Point}, given that they are
|
84
|
+
# both in the same 2D space.
|
85
|
+
#
|
86
|
+
# @param other [Point] the other {Point}.
|
87
|
+
# @return [Float] the distance to the other {Point}.
|
88
|
+
def distance_to(other)
|
89
|
+
Math.sqrt((other.x - x)**2 + (other.y - y)**2)
|
90
|
+
end
|
91
|
+
|
92
|
+
# This will calculate distance to another {Point} using the Haversine
|
93
|
+
# formula. This means that it will treat {#x} as longitude and {#y} as
|
94
|
+
# latitude!
|
95
|
+
#
|
96
|
+
# a = sin²(Δφ/2) + cos φ_1 ⋅ cos φ_2 ⋅ sin²(Δλ/2)
|
97
|
+
#
|
98
|
+
# c = 2 ⋅ atan2( √a, √(1−a) )
|
99
|
+
#
|
100
|
+
# d = R ⋅ c
|
101
|
+
#
|
102
|
+
# where φ is latitude, λ is longitude, R is earth’s radius (mean
|
103
|
+
# radius = 6 371 km);
|
104
|
+
# note that angles need to be in radians to pass to trig functions!
|
105
|
+
#
|
106
|
+
# @param other [Point] the other {Point}.
|
107
|
+
# @return [Float] the distance, in meters, to the other {Point}.
|
108
|
+
def haversine_distance_to(other)
|
109
|
+
lat1 = y * (Math::PI / 180.0)
|
110
|
+
lat2 = other.y * (Math::PI / 180.0)
|
111
|
+
dlat = (other.y - y) * (Math::PI / 180.0)
|
112
|
+
dlon = (other.x - x) * (Math::PI / 180.0)
|
113
|
+
|
37
114
|
# a = sin²(Δφ/2) + cos φ_1 ⋅ cos φ_2 ⋅ sin²(Δλ/2)
|
115
|
+
a = calculate_haversine_a(lat1, lat2, dlat, dlon)
|
38
116
|
# c = 2 ⋅ atan2( √a, √(1−a) )
|
117
|
+
c = 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
39
118
|
# d = R ⋅ c
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
119
|
+
6371 * 1000.0 * c
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def calculate_haversine_a(lat1, lat2, dlat, dlon)
|
125
|
+
Math.sin(dlat / 2.0) * Math.sin(dlat / 2.0) +
|
126
|
+
Math.cos(lat1) * Math.cos(lat2) *
|
127
|
+
Math.sin(dlon / 2.0) * Math.sin(dlon / 2.0)
|
128
|
+
end
|
129
|
+
|
130
|
+
def process_data(data)
|
131
|
+
data.nil? || data.is_a?(Array) || data.is_a?(String) || data.is_a?(Numeric) ? data : data.to_h
|
132
|
+
end
|
133
|
+
|
134
|
+
def get_typed_numeric(any_input)
|
135
|
+
# Try integer first since float will parse integers too
|
136
|
+
return get_integer(any_input) unless get_integer(any_input).nil?
|
137
|
+
# Try Float next
|
138
|
+
return get_float(any_input) unless get_float(any_input).nil?
|
139
|
+
|
140
|
+
raise UnknownTypeError, "Unknown type for parameter: #{any_input.class}"
|
141
|
+
end
|
142
|
+
|
143
|
+
def get_integer(any_input)
|
144
|
+
return Integer(any_input) if any_input.is_a? String
|
145
|
+
return any_input if any_input.is_a? Integer
|
146
|
+
|
147
|
+
nil
|
148
|
+
rescue StandardError
|
149
|
+
nil
|
150
|
+
end
|
151
|
+
|
152
|
+
def get_float(any_input)
|
153
|
+
return Float(any_input) if any_input.is_a? String
|
154
|
+
return any_input if any_input.is_a? Float
|
155
|
+
|
156
|
+
nil
|
157
|
+
rescue StandardError
|
158
|
+
nil
|
65
159
|
end
|
160
|
+
end
|
66
161
|
end
|
data/lib/quadtree/quadtree.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Quadtree
|
2
|
-
# A Quadtree
|
4
|
+
# A Quadtree.
|
3
5
|
class Quadtree
|
4
|
-
|
5
6
|
# Arbitrary constant to indicate how many elements can be stored in this
|
6
7
|
# quad tree node.
|
7
|
-
# @return [Integer]
|
8
|
+
# @return [Integer] number of {Point}s this {Quadtree} can hold.
|
8
9
|
NODE_CAPACITY = 4
|
9
10
|
|
10
11
|
# Axis-aligned bounding box stored as a center with half-dimensions to
|
@@ -18,98 +19,203 @@ module Quadtree
|
|
18
19
|
|
19
20
|
# Children
|
20
21
|
|
22
|
+
# North west corner of this quad.
|
21
23
|
# @return [Quadtree]
|
22
24
|
attr_accessor :north_west
|
25
|
+
|
26
|
+
# North east corner of this quad.
|
23
27
|
# @return [Quadtree]
|
24
28
|
attr_accessor :north_east
|
29
|
+
|
30
|
+
# South west corner of this quad.
|
25
31
|
# @return [Quadtree]
|
26
32
|
attr_accessor :south_west
|
33
|
+
|
34
|
+
# South east corner of this quad.
|
27
35
|
# @return [Quadtree]
|
28
36
|
attr_accessor :south_east
|
29
37
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
38
|
+
#
|
39
|
+
# Create a new {Quadtree}.
|
40
|
+
#
|
41
|
+
# @param [AxisAlignedBoundingBox] boundary the boundary for this {Quadtree}.
|
42
|
+
# @param [Array<Point>] points any initial {Point}s.
|
43
|
+
# @param [Quadtree] north_west northwestern child {Quadtree}
|
44
|
+
# @param [Quadtree] north_east northeastern child {Quadtree}
|
45
|
+
# @param [Quadtree] south_west southwestern child {Quadtree}
|
46
|
+
# @param [Quadtree] south_east southestern child {Quadtree}
|
47
|
+
#
|
48
|
+
def initialize(boundary, points = [], north_west = nil, north_east = nil, south_west = nil, south_east = nil)
|
49
|
+
self.boundary = boundary
|
50
|
+
self.points = points
|
51
|
+
self.north_west = north_west
|
52
|
+
self.north_east = north_east
|
53
|
+
self.south_west = south_west
|
54
|
+
self.south_east = south_east
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Create a Hash from this {Quadtree}.
|
59
|
+
#
|
60
|
+
# @return [Hash] Hash representation.
|
61
|
+
#
|
62
|
+
def to_h
|
63
|
+
{
|
64
|
+
'boundary': boundary.to_h,
|
65
|
+
'points': points.map(&:to_h),
|
66
|
+
'north_west': north_west.nil? ? nil : north_west.to_h,
|
67
|
+
'north_east': north_east.nil? ? nil : north_east.to_h,
|
68
|
+
'south_west': south_west.nil? ? nil : south_west.to_h,
|
69
|
+
'south_east': south_east.nil? ? nil : south_east.to_h
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Create a Hash from this {Quadtree}.
|
75
|
+
#
|
76
|
+
# @return [Hash] Hash representation of this {Quadtree}.
|
77
|
+
#
|
78
|
+
def to_hash
|
79
|
+
to_h
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Create a JSON String representation of this {Quadtree}.
|
84
|
+
#
|
85
|
+
# @return [String] JSON String of this {Quadtree}.
|
86
|
+
#
|
87
|
+
def to_json(*_args)
|
88
|
+
require 'json'
|
89
|
+
to_h.to_json
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Create a String for this {Quadtree}.
|
94
|
+
#
|
95
|
+
# @return [String] String representation of this {Quadtree}.
|
96
|
+
#
|
97
|
+
def to_s
|
98
|
+
to_h.to_s
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Construct a Quadtree from a JSON String.
|
103
|
+
#
|
104
|
+
# @param [String] json_data input JSON String.
|
105
|
+
#
|
106
|
+
# @return [Quadtree] the {Quadtree} contained in the JSON String.
|
107
|
+
#
|
108
|
+
def self.from_json(json_data)
|
109
|
+
new(
|
110
|
+
AxisAlignedBoundingBox.from_json(json_data['boundary']),
|
111
|
+
json_data['points'].map { |point_data| Point.from_json(point_data) },
|
112
|
+
json_data['north_west'].nil? ? nil : Quadtree.from_json(json_data['north_west']),
|
113
|
+
json_data['north_east'].nil? ? nil : Quadtree.from_json(json_data['north_east']),
|
114
|
+
json_data['south_west'].nil? ? nil : Quadtree.from_json(json_data['south_west']),
|
115
|
+
json_data['south_east'].nil? ? nil : Quadtree.from_json(json_data['south_east'])
|
116
|
+
)
|
37
117
|
end
|
38
118
|
|
39
|
-
#
|
40
|
-
#
|
119
|
+
# Insert a {Point} in this {Quadtree}.
|
120
|
+
#
|
121
|
+
# @param point [Point] the point to insert.
|
122
|
+
# @return [Boolean] +true+ on success, +false+ otherwise.
|
41
123
|
def insert!(point)
|
42
|
-
return false unless
|
124
|
+
return false unless boundary.contains_point?(point)
|
43
125
|
|
44
126
|
if points.size < NODE_CAPACITY
|
45
|
-
|
127
|
+
points << point
|
46
128
|
return true
|
47
129
|
end
|
48
130
|
|
49
|
-
subdivide! if
|
50
|
-
return true if
|
51
|
-
return true if
|
52
|
-
return true if
|
53
|
-
return true if
|
131
|
+
subdivide! if north_west.nil?
|
132
|
+
return true if north_west.insert!(point)
|
133
|
+
return true if north_east.insert!(point)
|
134
|
+
return true if south_west.insert!(point)
|
135
|
+
return true if south_east.insert!(point)
|
54
136
|
|
55
137
|
false
|
56
138
|
end
|
57
139
|
|
140
|
+
# Return the size of this instance, the number of {Point}s stored in this
|
141
|
+
# {Quadtree}.
|
142
|
+
#
|
143
|
+
# @return [Integer] the size of this instance.
|
144
|
+
def size
|
145
|
+
count = 0
|
146
|
+
count += points.size
|
147
|
+
unless north_west.nil?
|
148
|
+
count += north_west.size
|
149
|
+
count += north_east.size
|
150
|
+
count += south_west.size
|
151
|
+
count += south_east.size
|
152
|
+
end
|
153
|
+
count
|
154
|
+
end
|
155
|
+
|
58
156
|
# Finds all points contained within a range.
|
59
157
|
#
|
60
|
-
# @param [AxisAlignedBoundingBox]
|
61
|
-
# @return [Array<Point>]
|
158
|
+
# @param range [AxisAlignedBoundingBox] the range to search within.
|
159
|
+
# @return [Array<Point>] all {Point}s in given range.
|
62
160
|
def query_range(range)
|
63
161
|
# Prepare an array of results
|
64
162
|
points_in_range = []
|
65
163
|
|
66
164
|
# Automatically abort if the range does not intersect this quad
|
67
|
-
return points_in_range unless
|
165
|
+
return points_in_range unless boundary.intersects?(range)
|
68
166
|
|
69
167
|
# Check objects at this quad level
|
70
|
-
|
168
|
+
points.each do |point|
|
71
169
|
points_in_range << point if range.contains_point?(point)
|
72
170
|
end
|
73
171
|
|
74
172
|
# Terminate here, if there are no children
|
75
|
-
return points_in_range if
|
173
|
+
return points_in_range if north_west.nil?
|
76
174
|
|
77
175
|
# Otherwise, add the points from the children
|
78
|
-
points_in_range +=
|
79
|
-
points_in_range +=
|
80
|
-
points_in_range +=
|
81
|
-
points_in_range +=
|
176
|
+
points_in_range += north_west.query_range(range)
|
177
|
+
points_in_range += north_east.query_range(range)
|
178
|
+
points_in_range += south_west.query_range(range)
|
179
|
+
points_in_range += south_east.query_range(range)
|
82
180
|
|
83
181
|
points_in_range
|
84
182
|
end
|
85
183
|
|
86
184
|
private
|
87
185
|
|
88
|
-
# @return [Boolean]
|
186
|
+
# @return [Boolean] +true+ on success, +false+ otherwise.
|
89
187
|
def subdivide!
|
90
|
-
left_edge =
|
91
|
-
right_edge =
|
92
|
-
top_edge =
|
93
|
-
bottom_edge =
|
94
|
-
quad_half_dimension =
|
95
|
-
|
96
|
-
north_west_center =
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
188
|
+
left_edge = boundary.left
|
189
|
+
right_edge = boundary.right
|
190
|
+
top_edge = boundary.top
|
191
|
+
bottom_edge = boundary.bottom
|
192
|
+
quad_half_dimension = boundary.half_dimension / 2
|
193
|
+
|
194
|
+
north_west_center = Point.new(left_edge + quad_half_dimension,
|
195
|
+
top_edge - quad_half_dimension)
|
196
|
+
north_east_center = Point.new(right_edge - quad_half_dimension,
|
197
|
+
top_edge - quad_half_dimension)
|
198
|
+
south_east_center = Point.new(left_edge + quad_half_dimension,
|
199
|
+
bottom_edge + quad_half_dimension)
|
200
|
+
south_west_center = Point.new(right_edge - quad_half_dimension,
|
201
|
+
bottom_edge + quad_half_dimension)
|
202
|
+
|
203
|
+
north_west_boundary = AxisAlignedBoundingBox.new(north_west_center,
|
204
|
+
quad_half_dimension)
|
205
|
+
north_east_boundary = AxisAlignedBoundingBox.new(north_east_center,
|
206
|
+
quad_half_dimension)
|
207
|
+
south_west_boundary = AxisAlignedBoundingBox.new(south_west_center,
|
208
|
+
quad_half_dimension)
|
209
|
+
south_east_boundary = AxisAlignedBoundingBox.new(south_east_center,
|
210
|
+
quad_half_dimension)
|
211
|
+
|
212
|
+
self.north_west = Quadtree.new north_west_boundary
|
213
|
+
self.north_east = Quadtree.new north_east_boundary
|
214
|
+
self.south_west = Quadtree.new south_west_boundary
|
215
|
+
self.south_east = Quadtree.new south_east_boundary
|
110
216
|
|
111
217
|
true
|
112
|
-
rescue
|
218
|
+
rescue StandardError
|
113
219
|
false
|
114
220
|
end
|
115
221
|
end
|