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.
@@ -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