quadtree 1.0.2 → 1.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,66 +1,161 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Quadtree
2
- # Simple coordinate object to represent points in some space.
3
- class Point
4
-
5
- # @return [Float] X coordinate
6
- attr_accessor :x
7
-
8
- # @return [Float] Y coordinate
9
- attr_accessor :y
10
-
11
- # Payload attached to this {Point}.
12
- attr_accessor :data
13
-
14
- # Create a new {Point}.
15
- #
16
- # @param [Float] x X coordinate
17
- # @param [Float] y Y coordinate
18
- def initialize(x, y, data=nil)
19
- @x = x
20
- @y = y
21
- @data = data unless data.nil?
22
- end
23
-
24
- # This will calculate distance to another, given that they are both
25
- # on the same flat two dimensional plane.
26
- #
27
- # @param [Point] other the other {Point}.
28
- # @return [Float] the distance to the other {Point}.
29
- def distance_to(other)
30
- Math.sqrt((other.x - self.x) ** 2 + (other.y - self.y) ** 2)
31
- end
32
-
33
- # This will calculate distance to another point using the Haversine
34
- # formula. This means that it will treat #x as longitude and #y as
35
- # latitude!
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
- # where φ is latitude, λ is longitude, R is earth’s radius (mean
42
- # radius = 6 371 km);
43
- # note that angles need to be in radians to pass to trig functions!
44
- #
45
- # @param [Point] other the other {Point}.
46
- # @return [Float] the distance, in meters, to the other {Point}.
47
- def haversine_distance_to(other)
48
- # earth's radius
49
- r = 6371 * 1000.0
50
- # coverting degrees to radians
51
- lat1 = self.y * (Math::PI / 180)
52
- lat2 = other.y * (Math::PI / 180)
53
- dlat = (other.y - self.y) * (Math::PI / 180)
54
- dlon = (other.x - self.x) * (Math::PI / 180)
55
-
56
- # a = sin²(Δφ/2) + cos φ_1 cos φ_2 ⋅ sin²(Δλ/2)
57
- a = Math.sin(dlat / 2) * Math.sin(dlat / 2) +
58
- Math.cos(lat1) * Math.cos(lat2) *
59
- Math.sin(dlon / 2) * Math.sin(dlon / 2)
60
- # c = 2 ⋅ atan2( √a, √(1−a) )
61
- c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
62
- # d = R ⋅ c
63
- return r * c
64
- end
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
@@ -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
- def initialize(boundary)
31
- @boundary = boundary
32
- @points = []
33
- @north_west = nil
34
- @north_east = nil
35
- @south_west = nil
36
- @south_east = nil
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
- # @param [Point] point
40
- # @return [Boolean]
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 @boundary.contains_point?(point)
124
+ return false unless boundary.contains_point?(point)
43
125
 
44
126
  if points.size < NODE_CAPACITY
45
- @points << point
127
+ points << point
46
128
  return true
47
129
  end
48
130
 
49
- subdivide! if @north_west.nil?
50
- return true if @north_west.insert!(point)
51
- return true if @north_east.insert!(point)
52
- return true if @south_west.insert!(point)
53
- return true if @south_east.insert!(point)
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] range the range to search within.
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 @boundary.intersects?(range)
165
+ return points_in_range unless boundary.intersects?(range)
68
166
 
69
167
  # Check objects at this quad level
70
- @points.each do |point|
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 @north_west.nil?
173
+ return points_in_range if north_west.nil?
76
174
 
77
175
  # Otherwise, add the points from the children
78
- points_in_range += @north_west.query_range(range)
79
- points_in_range += @north_east.query_range(range)
80
- points_in_range += @south_west.query_range(range)
81
- points_in_range += @south_east.query_range(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 = @boundary.left
91
- right_edge = @boundary.right
92
- top_edge = @boundary.top
93
- bottom_edge = @boundary.bottom
94
- quad_half_dimension = @boundary.half_dimension / 2
95
-
96
- north_west_center = Quadtree::Point.new left_edge + quad_half_dimension, top_edge - quad_half_dimension
97
- north_east_center = Quadtree::Point.new right_edge - quad_half_dimension, top_edge - quad_half_dimension
98
- south_east_center = Quadtree::Point.new left_edge + quad_half_dimension, bottom_edge + quad_half_dimension
99
- south_west_center = Quadtree::Point.new right_edge - quad_half_dimension, bottom_edge + quad_half_dimension
100
-
101
- north_west_boundary = Quadtree::AxisAlignedBoundingBox.new north_west_center, quad_half_dimension
102
- north_east_boundary = Quadtree::AxisAlignedBoundingBox.new north_east_center, quad_half_dimension
103
- south_west_boundary = Quadtree::AxisAlignedBoundingBox.new south_west_center, quad_half_dimension
104
- south_east_boundary = Quadtree::AxisAlignedBoundingBox.new south_east_center, quad_half_dimension
105
-
106
- @north_west = Quadtree::Quadtree.new north_west_boundary
107
- @north_east = Quadtree::Quadtree.new north_east_boundary
108
- @south_west = Quadtree::Quadtree.new south_west_boundary
109
- @south_east = Quadtree::Quadtree.new south_east_boundary
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