quadtree 1.0.7 → 1.0.8

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