mongoid-geospatial 7.1.0 → 7.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 88d807a8e7011846e353f18fc6812fb55e578f371464c202fa726976c7fec6e6
4
- data.tar.gz: 32f5124231bfe558a9a591520c90dda408f76995faa2030e67c39bee1e9f7b23
3
+ metadata.gz: 4bc532f8201a1c7c0f43c63d051a073dd5658ab8ffa561ea852e14a0fcf1c243
4
+ data.tar.gz: 9af8f1e4e1ef5f2514e3214f5e0bc66c0f56fbb7e9a1cbdf3fd152e89b11cbad
5
5
  SHA512:
6
- metadata.gz: f8beb1650724d7b54d7a2a35d2376597144d9290360f2571da28681ef11d3e3e0792bf562f0c904f762b0abaf09d43b9f8c8f1747cb4d7753a5f187a6c6d04c5
7
- data.tar.gz: b457c28c88ab87968e76eeb00afca6e376e37cf2432df230fc88260ab44b9984c9cd11708deba53eb19fa4a19d27175d63042c5b142762707fe3bd1019107652
6
+ metadata.gz: 154e488426770257945cd93d36138d238d5b1bf75e308b62b568ba897f9835f921faf26d50f14e71c9f3f43f0ce9be761586f228a788819a0ed7f2662274fbc3
7
+ data.tar.gz: 1fbd1713b7121edc2bc8f171a5cf66f63befc5668369d1f833d0be1d0783844bb41ffdf62e618172324e15bb2cfb4e905a3cd91896ec9b7d28a46142a06b6769
data/README.md CHANGED
@@ -46,6 +46,14 @@ class Place
46
46
  end
47
47
  ```
48
48
 
49
+ A pin named `geom` on a 2dsphere index is one include:
50
+
51
+ ```ruby
52
+ include Mongoid::Geospatial::Geom # field :geom, type: Point, sphere: true
53
+ # or a named one:
54
+ geom :pick_up
55
+ ```
56
+
49
57
  Generate indexes on MongoDB via rake:
50
58
 
51
59
  ```
@@ -94,6 +102,15 @@ If you need a hash
94
102
  cafe.location.to_hsh # => { x: -74.026667, y: 40.703056 }
95
103
  ```
96
104
 
105
+ Commonly used
106
+
107
+ ```ruby
108
+ cafe.location.to_lat_lon # => { lat: 40.703056, lon: -74.026667 }
109
+ cafe.location.lat # => 40.703056
110
+ cafe.location.lng # => -74.026667
111
+ cafe.location.distance(other) # => km, haversine
112
+ ```
113
+
97
114
  If you are using GeoRuby or RGeo
98
115
 
99
116
  ```ruby
@@ -108,7 +125,7 @@ This lib uses #x and #y everywhere.
108
125
  It's shorter than lat or lng or another variation that also confuses.
109
126
  A point is a 2D mathematical notation, longitude/latitude is when you use that notation to map an sphere. In other words: all longitudes are 'xs' where not all 'xs' are longitudes.
110
127
 
111
- Distance and other geometrical calculations are delegated to the external library of your choice. More info about using RGeo or GeoRuby below. Some built in helpers for mongoid queries:
128
+ Distance is `Point#distance` (haversine, km). Projections and heavier geometry still go to RGeo or GeoRuby. Some built in helpers for mongoid queries:
112
129
 
113
130
  ```ruby
114
131
  # Returns middle point + radius
@@ -215,7 +232,21 @@ Bar.near_sphere(location: person.house)
215
232
  Bar.where(:location.near_sphere => person.house)
216
233
  ```
217
234
 
218
- ### nearby
235
+ ### nearby / within
236
+
237
+ ```ruby
238
+ Bar.nearby(person.house)
239
+ Bar.nearby(person.house, km: 30)
240
+ Bar.within(person.house, 30) # same question, km required
241
+ City.where(:geom.near_sphere => Mongoid::Geospatial.near_query(geom, 50))
242
+ ```
243
+
244
+ `within` is `$nearSphere` + `$maxDistance` in metres. Nearest first, chainable.
245
+
246
+ **Don't call `#first` or `#last` on a `$near` criteria.** Mongoid sorts by
247
+ `_id` when a criteria carries no sort of its own, and that replaces the
248
+ distance order Mongo put there. Walk the criteria instead — `.to_a.first` —
249
+ or use `geo_near`, which returns the distance as a field you can sort on.
219
250
 
220
251
  You can add a `spatial_scope` on your models. So you can query:
221
252
 
@@ -282,6 +313,20 @@ Bar.within_polygon(location: [[[x,y],...[x,y]]])
282
313
  Bar.within_polygon(location: street.bbox)
283
314
  ```
284
315
 
316
+ - within_circle / within_spherical_circle
317
+
318
+ Mongoid ships `within_polygon` and `within_box` and stopped there; this gem
319
+ registers the two circle shapes, which is what `radius` and `radius_sphere`
320
+ have been building arguments for all along.
321
+
322
+ ```ruby
323
+ Bar.where(:location.within_spherical_circle => cafe.location.radius_sphere(5, :km))
324
+ Bar.where(:location.within_circle => cafe.location.radius(0.05)) # degrees, flat
325
+ ```
326
+
327
+ `$centerSphere` reads radians (`radius_sphere` hands it those); `$center`
328
+ reads the coordinate system's own units — degrees on a legacy pair.
329
+
285
330
  - intersects_line
286
331
  - intersects_point
287
332
  - intersects_polygon
@@ -295,7 +340,7 @@ external library corresponding object.
295
340
 
296
341
  ### Use RGeo?
297
342
 
298
- https://github.com/dazuma/rgeo
343
+ <https://github.com/dazuma/rgeo>
299
344
 
300
345
  RGeo is a Ruby wrapper for Proj/GEOS.
301
346
  It's perfect when you need to work with complex calculations and projections.
@@ -303,7 +348,7 @@ It'll require more stuff installed to compile/work.
303
348
 
304
349
  ### Use GeoRuby?
305
350
 
306
- https://github.com/nofxx/georuby
351
+ <https://github.com/nofxx/georuby>
307
352
 
308
353
  GeoRuby is a pure Ruby Geometry Library.
309
354
  It's perfect if you want simple calculations and/or keep your stack in pure ruby.
@@ -4,9 +4,16 @@ module Mongoid
4
4
  module Geospatial
5
5
  # Point
6
6
  #
7
- class Point
7
+ class Point # rubocop:disable Metrics/ClassLength
8
8
  include Enumerable
9
+
9
10
  attr_accessor :x, :y, :z
11
+ alias lng x
12
+ alias lon x
13
+ alias lat y
14
+ alias lng= x=
15
+ alias lon= x=
16
+ alias lat= y=
10
17
 
11
18
  def initialize(lon, lat, alt = nil)
12
19
  @x = lon
@@ -25,6 +32,7 @@ module Mongoid
25
32
  end
26
33
  alias to_a mongoize
27
34
  alias to_xy mongoize
35
+ alias to_lng_lat mongoize
28
36
 
29
37
  def [](args)
30
38
  mongoize[args]
@@ -35,17 +43,6 @@ module Mongoid
35
43
  yield y
36
44
  end
37
45
 
38
- #
39
- # Point representation as a Hash
40
- # Optional param: custom keys.
41
- #
42
- # @return [Hash] with { lng_key => x, lat_key => y }
43
- #
44
- def to_hsh(xkey = :x, ykey = :y)
45
- { xkey => x, ykey => y }
46
- end
47
- alias to_hash to_hsh
48
-
49
46
  #
50
47
  # Helper for [self, radius]
51
48
  #
@@ -88,6 +85,26 @@ module Mongoid
88
85
  "#{x}, #{y}"
89
86
  end
90
87
 
88
+ #
89
+ # Point representation as a Hash
90
+ # Optional param: custom keys.
91
+ #
92
+ # @return [Hash] with { lng_key => x, lat_key => y }
93
+ #
94
+ def to_hsh(xkey = :x, ykey = :y)
95
+ { xkey => x, ykey => y }
96
+ end
97
+ alias to_hash to_hsh
98
+
99
+ #
100
+ # Point representation more commonly used
101
+ # Latitude, Longitude
102
+ #
103
+ # @return [Hash] with { latitude: y, longitude: x }
104
+ def to_lat_lon
105
+ { latitude: y, longitude: x }
106
+ end
107
+
91
108
  #
92
109
  # Point definition as GeoJSON
93
110
  #
@@ -113,32 +130,21 @@ module Mongoid
113
130
  end
114
131
 
115
132
  #
116
- # Distance calculation methods. Thinking about not using it
117
- # One needs to choose and external lib. GeoRuby or RGeo
118
- #
119
- # Return the distance between the 2D points (ie taking care
120
- # only of the x and y coordinates), assuming the points are
121
- # in projected coordinates. Euclidian distance in whatever
122
- # unit the x and y ordinates are.
123
- # def euclidian_distance(point)
124
- # Math.sqrt((point.x - x)**2 + (point.y - y)**2)
125
- # end
126
-
127
- # # Spherical distance in meters, using 'Haversine' formula.
128
- # # with a radius of 6471000m
129
- # # Assumes x is the lon and y the lat, in degrees (Changed
130
- # in version 1.1).
131
- # # The user has to make sure using this distance makes sense
132
- # (ie she should be in latlon coordinates)
133
- # def spherical_distance(point,r=6370997.0)
134
- # dlat = (point.lat - lat) * DEG2RAD / 2
135
- # dlon = (point.lon - lon) * DEG2RAD / 2
136
-
137
- # a = Math.sin(dlat)**2 + Math.cos(lat * DEG2RAD) *
138
- # Math.cos(point.lat * DEG2RAD) * Math.sin(dlon)**2
139
- # c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
140
- # r * c
141
- # end
133
+ # Crow-flies distance. Haversine, Mongo's earth radius (6371 km).
134
+ # RGeo/GeoRuby still win for projections; this is ETA and "how far".
135
+ #
136
+ def distance(other, unit = :km) # rubocop:disable Metrics/AbcSize
137
+ xy = other.is_a?(Point) ? [other.x, other.y] : self.class.mongoize(other)
138
+ raise ArgumentError, "Invalid point: #{other.inspect}" unless xy
139
+
140
+ dlat = (xy[1] - y) * RAD_PER_DEG
141
+ dlon = (xy[0] - x) * RAD_PER_DEG
142
+ a = (Math.sin(dlat / 2)**2) +
143
+ (Math.cos(y * RAD_PER_DEG) * Math.cos(xy[1] * RAD_PER_DEG) *
144
+ (Math.sin(dlon / 2)**2))
145
+ c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
146
+ Mongoid::Geospatial.earth_radius[unit] * c
147
+ end
142
148
 
143
149
  class << self
144
150
  #
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ module Geospatial
5
+ #
6
+ # The pin, named `geom`, on a 2dsphere index.
7
+ #
8
+ module Geom
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ include Mongoid::Geospatial
13
+
14
+ geom
15
+ end
16
+
17
+ def located?
18
+ geom.present?
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # `$geoWithin` with a circle. Mongoid ships `within_polygon` and `within_box`
5
+ # and stopped there — the two circle shapes left with Origin, and `Point#radius`
6
+ # / `#radius_sphere` have been building their argument ever since.
7
+ #
8
+ # Bar.where(:location.within_circle => elvis.location.radius(0.05))
9
+ # Bar.where(:location.within_spherical_circle => elvis.location.radius_sphere(5, :km))
10
+ #
11
+ # `$center` reads its radius in the coordinate system's own units (degrees, for
12
+ # a legacy pair); `$centerSphere` reads radians, which is what `radius_sphere`
13
+ # returns. Guarded: if Mongoid ever registers these, its own wins.
14
+ #
15
+ %i[within_circle within_spherical_circle].each do |name|
16
+ next if Symbol.method_defined?(name)
17
+
18
+ operator = name == :within_circle ? '$center' : '$centerSphere'
19
+ Symbol.add_key(name, :override, '$geoWithin', operator)
20
+ end
@@ -3,6 +3,6 @@
3
3
  module Mongoid
4
4
  # Mongoid Geospatial version
5
5
  module Geospatial
6
- VERSION = '7.1.0'
6
+ VERSION = '7.2.0'
7
7
  end
8
8
  end
@@ -5,6 +5,8 @@ require 'active_support/concern' # Explicitly require for `extend ActiveSupport:
5
5
  require 'mongoid/geospatial/helpers/spatial'
6
6
  require 'mongoid/geospatial/helpers/sphere'
7
7
  require 'mongoid/geospatial/helpers/delegate'
8
+ require 'mongoid/geospatial/helpers/geom'
9
+ require 'mongoid/geospatial/keys'
8
10
 
9
11
  module Mongoid
10
12
  #
@@ -69,6 +71,27 @@ module Mongoid
69
71
  require 'mongoid/geospatial/wrappers/georuby'
70
72
  end
71
73
 
74
+ #
75
+ # A [lng, lat] pair, or nothing. `Point.mongoize` is lenient — it reads
76
+ # "nowhere" as [0.0] — and a half pair is a query Mongo cannot answer.
77
+ #
78
+ def self.mongoize_point!(geom)
79
+ coords = Point.mongoize(geom)
80
+ unless coords.is_a?(Array) && coords.size == 2 && coords.all?(Numeric)
81
+ raise ArgumentError, "Invalid coordinates: #{geom.inspect}"
82
+ end
83
+
84
+ coords
85
+ end
86
+
87
+ #
88
+ # Selector for `$nearSphere` with a km cap. Metres on the wire.
89
+ #
90
+ def self.near_query(geom, km) # rubocop:disable Naming/MethodParameterName
91
+ { '$geometry' => { 'type' => 'Point', 'coordinates' => mongoize_point!(geom) },
92
+ '$maxDistance' => km * 1_000 }
93
+ end
94
+
72
95
  # Methods applied to Document's class
73
96
  module ClassMethods
74
97
  #
@@ -92,6 +115,14 @@ module Mongoid
92
115
  spatial_fields_indexed << name
93
116
  index({ name => '2dsphere' }, options)
94
117
  end
118
+ alias sphere_index spherical_index
119
+
120
+ #
121
+ # A Point on a 2dsphere index. Default name is `geom`.
122
+ #
123
+ def geom(name = :geom)
124
+ field name, type: Point, sphere: true
125
+ end
95
126
 
96
127
  #
97
128
  # # Queries
@@ -201,15 +232,15 @@ module Mongoid
201
232
  #
202
233
  # @param coordinates [Array, Mongoid::Geospatial::Point] The coordinates (e.g., [lon, lat])
203
234
  # or a Point object to find documents near to.
204
- # @param _options [Hash] Optional hash for future extensions (currently unused).
235
+ # @param km [Numeric, nil] Optional cap in kilometres (`$maxDistance` in metres).
205
236
  #
206
237
  # @return [Mongoid::Criteria] A criteria object for the query.
207
238
  #
208
239
  # Example:
209
240
  # Bar.nearby([10, 20])
210
- # Alarm.nearby(my_point_object)
241
+ # Alarm.nearby(my_point_object, km: 30)
211
242
  #
212
- def nearby(coordinates, _options = {})
243
+ def nearby(coordinates, km: nil, **_opts) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Naming/MethodParameterName
213
244
  if spatial_fields.empty?
214
245
  raise "No spatial fields defined for #{name} to use with .nearby. " \
215
246
  "Mark a field with 'spatial: true' or 'sphere: true'."
@@ -220,9 +251,22 @@ module Mongoid
220
251
 
221
252
  raise "Could not find field definition for spatial field: #{field_name_sym}" unless field_definition
222
253
 
223
- query_operator = field_definition.options[:sphere] ? :near_sphere : :near
254
+ if km
255
+ # GeoJSON `$maxDistance` is metres. `$near` on a 2d index is radians.
256
+ query = Mongoid::Geospatial.near_query(coordinates, km)
257
+ criteria.where(field_name_sym.near_sphere => query)
258
+ else
259
+ query_operator = field_definition.options[:sphere] ? :near_sphere : :near
260
+ criteria.where(field_name_sym.send(query_operator) => coordinates)
261
+ end
262
+ end
224
263
 
225
- criteria.where(field_name_sym.send(query_operator) => coordinates)
264
+ #
265
+ # Documents within +km+ of +geom+, nearest first.
266
+ # `$nearSphere` + `$maxDistance` in metres.
267
+ #
268
+ def within(geom, km) # rubocop:disable Naming/MethodParameterName
269
+ nearby(geom, km: km)
226
270
  end
227
271
 
228
272
  # Performs a $geoNear aggregation pipeline stage to find documents near a point,
@@ -274,14 +318,20 @@ module Mongoid
274
318
  # end
275
319
  #
276
320
  def geo_near(field_name, coordinates, options = {})
277
- mongoized_coords = Mongoid::Geospatial::Point.mongoize(coordinates)
278
-
279
- raise ArgumentError, "Invalid coordinates provided: #{coordinates.inspect}" unless mongoized_coords
321
+ mongoized_coords = Mongoid::Geospatial.mongoize_point!(coordinates)
280
322
 
281
323
  # User-provided options. Work with a copy.
282
324
  user_options = options.dup
283
325
  limit_value = user_options.delete(:limit) # Handled by a separate pipeline stage
284
326
 
327
+ # `km:` is metres on the wire, and metres only holds for a GeoJSON
328
+ # `near` on a sphere. A legacy pair would read `maxDistance` in radians.
329
+ if (km = user_options.delete(:km))
330
+ user_options[:maxDistance] = km.to_f * 1_000
331
+ user_options[:spherical] = true
332
+ mongoized_coords = { 'type' => 'Point', 'coordinates' => mongoized_coords }
333
+ end
334
+
285
335
  # Core $geoNear parameters derived from method arguments, these are not overrideable by user_options.
286
336
  geo_near_core_params = {
287
337
  key: field_name.to_s,
@@ -297,10 +347,8 @@ module Mongoid
297
347
  # Merge user options over defaults, then ensure core parameters are set.
298
348
  geo_near_stage_options = geo_near_defaultable_params.merge(user_options).merge(geo_near_core_params)
299
349
 
300
- # Ensure :spherical is a strict boolean (true/false).
301
- # If user_options provided :spherical, it's already set. If not, the default is used.
302
- # This line ensures the final value is strictly true or false, not just truthy/falsy.
303
- geo_near_stage_options[:spherical] = !geo_near_stage_options[:spherical].nil?
350
+ # $geoNear wants a strict boolean, and honours the caller's choice.
351
+ geo_near_stage_options[:spherical] = geo_near_stage_options[:spherical] ? true : false
304
352
 
305
353
  # Note on performance:
306
354
  # $geoNear is an aggregation pipeline stage. For simple proximity queries,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid-geospatial
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.0
4
+ version: 7.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Ong
@@ -45,8 +45,10 @@ files:
45
45
  - lib/mongoid/geospatial/fields/polygon.rb
46
46
  - lib/mongoid/geospatial/geometry_field.rb
47
47
  - lib/mongoid/geospatial/helpers/delegate.rb
48
+ - lib/mongoid/geospatial/helpers/geom.rb
48
49
  - lib/mongoid/geospatial/helpers/spatial.rb
49
50
  - lib/mongoid/geospatial/helpers/sphere.rb
51
+ - lib/mongoid/geospatial/keys.rb
50
52
  - lib/mongoid/geospatial/version.rb
51
53
  - lib/mongoid/geospatial/wrappers/georuby.rb
52
54
  - lib/mongoid/geospatial/wrappers/rgeo.rb