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