mongoid-geospatial 7.2.0 → 7.3.1

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: 4bc532f8201a1c7c0f43c63d051a073dd5658ab8ffa561ea852e14a0fcf1c243
4
- data.tar.gz: 9af8f1e4e1ef5f2514e3214f5e0bc66c0f56fbb7e9a1cbdf3fd152e89b11cbad
3
+ metadata.gz: 9437e8d6af695bcba5f39208f8219019da404fc51b863c4fb7940567183c5c9f
4
+ data.tar.gz: 2a3ca5f675cd58c204462b7122b84341c63cee8222f3372a014d1ed3096f82f0
5
5
  SHA512:
6
- metadata.gz: 154e488426770257945cd93d36138d238d5b1bf75e308b62b568ba897f9835f921faf26d50f14e71c9f3f43f0ce9be761586f228a788819a0ed7f2662274fbc3
7
- data.tar.gz: 1fbd1713b7121edc2bc8f171a5cf66f63befc5668369d1f833d0be1d0783844bb41ffdf62e618172324e15bb2cfb4e905a3cd91896ec9b7d28a46142a06b6769
6
+ metadata.gz: 82fe874fb74ab68457b2e9c56c202bd94ff6c71626832fa0a514d8ce7741282bff68d7c08601806c72e56bf4364d5173f3cdfdaca47b005c86f698f0601d9697
7
+ data.tar.gz: fb2a788d4559688612083fddb24ef77d632d0d18b58aa5f3a6216ca58d6c6220a88180db3b009f4697fda2aa0cb95a43d3ef2d11ccc6e0d8a4f8003cc320227c
data/README.md CHANGED
@@ -105,7 +105,7 @@ cafe.location.to_hsh # => { x: -74.026667, y: 40.703056 }
105
105
  Commonly used
106
106
 
107
107
  ```ruby
108
- cafe.location.to_lat_lon # => { lat: 40.703056, lon: -74.026667 }
108
+ cafe.location.to_lat_lon # => { latitude: 40.703056, longitude: -74.026667 }
109
109
  cafe.location.lat # => 40.703056
110
110
  cafe.location.lng # => -74.026667
111
111
  cafe.location.distance(other) # => km, haversine
@@ -238,10 +238,30 @@ Bar.where(:location.near_sphere => person.house)
238
238
  Bar.nearby(person.house)
239
239
  Bar.nearby(person.house, km: 30)
240
240
  Bar.within(person.house, 30) # same question, km required
241
+ Bar.nearest(person.house, 30) # the closest one, or nil
242
+ Ride.within(here, 5, field: :drop_up) # two pins? name the one you mean
241
243
  City.where(:geom.near_sphere => Mongoid::Geospatial.near_query(geom, 50))
242
244
  ```
243
245
 
244
- `within` is `$nearSphere` + `$maxDistance` in metres. Nearest first, chainable.
246
+ `within` caps by km, nearest first, chainable on either index. `$nearSphere`
247
+ speaks two dialects and **the field's index picks which**, so the km never moves:
248
+
249
+ ```
250
+ sphere: true { loc: { $nearSphere: { $geometry: {..}, $maxDistance: <metres> } } }
251
+ spatial: true { loc: { $nearSphere: [x, y], $maxDistance: <radians> } }
252
+ km / 6371
253
+ ```
254
+
255
+ Hand the server the wrong one and it answers `NoQueryExecutionPlans` — not a
256
+ wrong count, a raise.
257
+
258
+ **`within(..).first` is not the nearest one.** Mongoid's `#first` and `#last`
259
+ sort by `_id` whenever the criteria carries no sort of its own, and that `_id`
260
+ sort replaces the distance order `$near` put there. It reads as working every
261
+ time the closest document happens to be the oldest. Walk the criteria
262
+ (`.to_a.first`) or ask `nearest`. `Mongoid::Geospatial.near_selector(field, point, km,
263
+ sphere: <bool>)` is the one place both shapes are built; `near_query` is the
264
+ 2dsphere half of it, for a query you write by hand.
245
265
 
246
266
  **Don't call `#first` or `#last` on a `$near` criteria.** Mongoid sorts by
247
267
  `_id` when a criteria carries no sort of its own, and that replaces the
@@ -284,12 +304,14 @@ Place.geo_near(:location, [10, 20],
284
304
  query: { category: 'restaurant' }, # Optional: filter documents before geoNear
285
305
  limit: 10)
286
306
 
287
- # Iterate over results
288
- Place.geo_near(:location, [10, 20], spherical: true).each do |place|
289
- # 'place.distance' will be available if distanceField was 'distance' (the default)
290
- # or 'place.dist_calculated' if distanceField was 'dist.calculated'
291
- puts "#{place.name} is #{place.distance || place.dist_calculated} meters away."
307
+ # Iterate over results — HASHES, not documents. Nothing is instantiated:
308
+ # $geoNear adds fields a model has no field for.
309
+ Place.geo_near(:location, [10, 20], spherical: true).each do |doc|
310
+ puts "#{doc['name']} is #{doc['distance']} meters away."
292
311
  end
312
+
313
+ # Want models? ask for them at the call site:
314
+ Place.geo_near(:location, [10, 20]).map { |attrs| Place.instantiate(attrs) }
293
315
  ```
294
316
 
295
317
  Key features and options for `geo_near`:
@@ -10,8 +10,8 @@ module Mongoid
10
10
 
11
11
  def reset!
12
12
  # Now self.x and self.y refer to the public module accessors
13
- self.x = Mongoid::Geospatial.lng_symbols
14
- self.y = Mongoid::Geospatial.lat_symbols
13
+ self.x = Mongoid::Geospatial.lng_symbols.dup
14
+ self.y = Mongoid::Geospatial.lat_symbols.dup
15
15
  end
16
16
 
17
17
  # Initialize the configuration
@@ -35,7 +35,9 @@ module Mongoid
35
35
  alias to_lng_lat mongoize
36
36
 
37
37
  def [](args)
38
- mongoize[args]
38
+ raise ArgumentError, "Invalid point: #{inspect}" unless (pair = mongoize)
39
+
40
+ pair[args]
39
41
  end
40
42
 
41
43
  def each
@@ -49,7 +51,9 @@ module Mongoid
49
51
  # @return [Array] with [self, radius]
50
52
  #
51
53
  def radius(r = 1) # rubocop:disable Naming/MethodParameterName
52
- [mongoize, r]
54
+ return nil unless (pair = mongoize)
55
+
56
+ [pair, r]
53
57
  end
54
58
 
55
59
  #
@@ -60,7 +64,7 @@ module Mongoid
60
64
  # @return [Array] with [self, radius / earth radius]
61
65
  #
62
66
  def radius_sphere(r = 1, unit = :km) # rubocop:disable Naming/MethodParameterName
63
- radius r.to_f / Mongoid::Geospatial.earth_radius[unit]
67
+ radius r.to_f / Mongoid::Geospatial.earth_radius.fetch(unit)
64
68
  end
65
69
 
66
70
  #
@@ -135,7 +139,7 @@ module Mongoid
135
139
  #
136
140
  def distance(other, unit = :km) # rubocop:disable Metrics/AbcSize
137
141
  xy = other.is_a?(Point) ? [other.x, other.y] : self.class.mongoize(other)
138
- raise ArgumentError, "Invalid point: #{other.inspect}" unless xy
142
+ raise ArgumentError, "Invalid point: #{other.inspect}" unless xy&.size == 2
139
143
 
140
144
  dlat = (xy[1] - y) * RAD_PER_DEG
141
145
  dlon = (xy[0] - x) * RAD_PER_DEG
@@ -195,7 +199,7 @@ module Mongoid
195
199
  def from_string(str)
196
200
  return nil if str.empty?
197
201
 
198
- str.split(/,|\s/).reject(&:empty?).map(&:to_f)
202
+ from_array(str.split(/,|\s/).reject(&:empty?))
199
203
  end
200
204
 
201
205
  #
@@ -228,6 +232,9 @@ module Mongoid
228
232
  # @return (Array)
229
233
  #
230
234
  def from_hash(hsh)
235
+ coords = hsh[:coordinates] || hsh['coordinates']
236
+ return from_array(coords) if coords
237
+
231
238
  raise 'Hash must have at least 2 items' if hsh.size < 2
232
239
 
233
240
  [from_hash_x(hsh), from_hash_y(hsh)]
@@ -19,16 +19,10 @@ module Mongoid
19
19
  # @return [Array] containing 2 points
20
20
  #
21
21
  def bounding_box
22
- max_x = -Float::MAX
23
- min_x = Float::MAX
24
- max_y = -Float::MAX
25
- min_y = Float::MAX
26
- each do |point|
27
- max_y = point[1] if point[1] > max_y
28
- min_y = point[1] if point[1] < min_y
29
- max_x = point[0] if point[0] > max_x
30
- min_x = point[0] if point[0] < min_x
31
- end
22
+ return nil if empty?
23
+
24
+ min_x, max_x = map { |point| point[0] }.minmax
25
+ min_y, max_y = map { |point| point[1] }.minmax
32
26
  [[min_x, min_y], [max_x, max_y]]
33
27
  end
34
28
  alias bbox bounding_box
@@ -43,7 +37,9 @@ module Mongoid
43
37
  # @return [Array] containing 5 points
44
38
  #
45
39
  def geom_box
46
- xl, yl = bounding_box
40
+ return nil unless (box = bounding_box)
41
+
42
+ xl, yl = box
47
43
  [xl, [xl[0], yl[1]], yl, [yl[0], xl[1]], xl]
48
44
  end
49
45
 
@@ -54,7 +50,9 @@ module Mongoid
54
50
  # @return [Array] containing 1 point [x,y]
55
51
  #
56
52
  def center_point
57
- min, max = *bbox
53
+ return nil unless (box = bbox)
54
+
55
+ min, max = *box
58
56
  [(min[0] + max[0]) / 2.0, (min[1] + max[1]) / 2.0]
59
57
  end
60
58
  alias center center_point
@@ -66,7 +64,9 @@ module Mongoid
66
64
  # @return [Array] [point, r] point and radius in mongoid format
67
65
  #
68
66
  def radius(r = 1) # rubocop:disable Naming/MethodParameterName
69
- [center, r]
67
+ return nil unless (mid = center)
68
+
69
+ [mid, r]
70
70
  end
71
71
 
72
72
  #
@@ -78,7 +78,7 @@ module Mongoid
78
78
  # @return [Array]
79
79
  #
80
80
  def radius_sphere(r = 1, unit = :km) # rubocop:disable Naming/MethodParameterName
81
- radius r.to_f / Mongoid::Geospatial.earth_radius[unit]
81
+ radius r.to_f / Mongoid::Geospatial.earth_radius.fetch(unit)
82
82
  end
83
83
 
84
84
  class << self
@@ -10,7 +10,6 @@ Mongoid::Fields.option :spatial do |model, field, _options|
10
10
 
11
11
  model.class_eval do
12
12
  spatial_fields << field.name.to_sym
13
- spatial_fields_indexed << field.name.to_sym
14
13
 
15
14
  # Create 2D index
16
15
  spatial_index field.name
@@ -10,7 +10,6 @@ Mongoid::Fields.option :sphere do |model, field, _options|
10
10
 
11
11
  model.class_eval do
12
12
  spatial_fields << field.name.to_sym
13
- spatial_fields_indexed << field.name.to_sym
14
13
 
15
14
  # Create 2Dsphere index
16
15
  spherical_index field.name
@@ -3,6 +3,6 @@
3
3
  module Mongoid
4
4
  # Mongoid Geospatial version
5
5
  module Geospatial
6
- VERSION = '7.2.0'
6
+ VERSION = '7.3.1'
7
7
  end
8
8
  end
@@ -85,11 +85,39 @@ module Mongoid
85
85
  end
86
86
 
87
87
  #
88
- # Selector for `$nearSphere` with a km cap. Metres on the wire.
88
+ # A cap Mongo can measure: a positive number of kilometres.
89
+ #
90
+ def self.km!(km) # rubocop:disable Naming/MethodParameterName
91
+ raise ArgumentError, "Invalid km: #{km.inspect}" unless km.is_a?(Numeric) && km.positive?
92
+
93
+ km
94
+ end
95
+
96
+ #
97
+ # `$nearSphere` value with a km cap, GeoJSON. Metres on the wire.
98
+ # This is the 2dsphere shape — see #near_selector for the other one.
89
99
  #
90
100
  def self.near_query(geom, km) # rubocop:disable Naming/MethodParameterName
91
101
  { '$geometry' => { 'type' => 'Point', 'coordinates' => mongoize_point!(geom) },
92
- '$maxDistance' => km * 1_000 }
102
+ '$maxDistance' => km!(km) * 1_000 }
103
+ end
104
+
105
+ #
106
+ # The whole `{ field => ... }` selector for "within +km+", for either index.
107
+ #
108
+ # `$nearSphere` speaks two dialects and the index picks which:
109
+ #
110
+ # 2dsphere { loc: { $nearSphere: { $geometry: {...}, $maxDistance: <m> } } }
111
+ # 2d { loc: { $nearSphere: [x, y], $maxDistance: <rad> } }
112
+ #
113
+ # Hand the wrong one over and the server answers `NoQueryExecutionPlans`,
114
+ # not a wrong count — so ask the field which it is before building.
115
+ #
116
+ def self.near_selector(field, geom, km, sphere: true) # rubocop:disable Naming/MethodParameterName
117
+ return { field => { '$nearSphere' => near_query(geom, km) } } if sphere
118
+
119
+ { field => { '$nearSphere' => mongoize_point!(geom),
120
+ '$maxDistance' => km!(km) / EARTH_RADIUS_KM.to_f } }
93
121
  end
94
122
 
95
123
  # Methods applied to Document's class
@@ -101,7 +129,7 @@ module Mongoid
101
129
  # @param options [Hash] Additional options for the index.
102
130
  #
103
131
  def spatial_index(name, options = {})
104
- spatial_fields_indexed << name
132
+ remember_indexed(name)
105
133
  index({ name => '2d' }, options)
106
134
  end
107
135
 
@@ -112,11 +140,20 @@ module Mongoid
112
140
  # @param options [Hash] Additional options for the index.
113
141
  #
114
142
  def spherical_index(name, options = {})
115
- spatial_fields_indexed << name
143
+ remember_indexed(name)
116
144
  index({ name => '2dsphere' }, options)
117
145
  end
118
146
  alias sphere_index spherical_index
119
147
 
148
+ #
149
+ # One field, one entry, always a Symbol — a field may carry both a 2d
150
+ # and a 2dsphere index, and `spatial: true` calls this on its way in too.
151
+ #
152
+ def remember_indexed(name)
153
+ sym = name.to_sym
154
+ spatial_fields_indexed << sym unless spatial_fields_indexed.include?(sym)
155
+ end
156
+
120
157
  #
121
158
  # A Point on a 2dsphere index. Default name is `geom`.
122
159
  #
@@ -232,7 +269,7 @@ module Mongoid
232
269
  #
233
270
  # @param coordinates [Array, Mongoid::Geospatial::Point] The coordinates (e.g., [lon, lat])
234
271
  # or a Point object to find documents near to.
235
- # @param km [Numeric, nil] Optional cap in kilometres (`$maxDistance` in metres).
272
+ # @param km [Numeric, nil] Optional cap in kilometres.
236
273
  #
237
274
  # @return [Mongoid::Criteria] A criteria object for the query.
238
275
  #
@@ -240,33 +277,51 @@ module Mongoid
240
277
  # Bar.nearby([10, 20])
241
278
  # Alarm.nearby(my_point_object, km: 30)
242
279
  #
243
- def nearby(coordinates, km: nil, **_opts) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Naming/MethodParameterName
244
- if spatial_fields.empty?
245
- raise "No spatial fields defined for #{name} to use with .nearby. " \
246
- "Mark a field with 'spatial: true' or 'sphere: true'."
247
- end
280
+ def nearby(coordinates, km: nil, field: nil) # rubocop:disable Naming/MethodParameterName
281
+ pin, sphere = spatial_field(field)
282
+ return criteria.where(Mongoid::Geospatial.near_selector(pin, coordinates, km, sphere: sphere)) if km
248
283
 
249
- field_name_sym = spatial_fields.first.to_sym
250
- field_definition = fields[field_name_sym.to_s]
251
-
252
- raise "Could not find field definition for spatial field: #{field_name_sym}" unless field_definition
284
+ criteria.where(pin.send(sphere ? :near_sphere : :near) => coordinates)
285
+ end
253
286
 
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)
287
+ #
288
+ # The pin `.nearby`, `.within` and `.nearest` read, and whether it is on
289
+ # a sphere. Handed nothing, the first spatial field — a model with two
290
+ # pins (`geom :pick_up`, `geom :drop_up`) has to name the one it means.
291
+ #
292
+ # @return [Array] [field name as a Symbol, sphere?]
293
+ #
294
+ def spatial_field(field = nil)
295
+ sym = (field || spatial_fields.first)&.to_sym
296
+ unless sym && spatial_fields.include?(sym)
297
+ raise ArgumentError, "#{name} has no spatial field #{sym.inspect} — it has #{spatial_fields.inspect}. " \
298
+ "Mark one with 'spatial: true' or 'sphere: true'."
261
299
  end
300
+
301
+ [sym, fields.fetch(sym.to_s).options[:sphere] ? true : false]
262
302
  end
263
303
 
264
304
  #
265
- # Documents within +km+ of +geom+, nearest first.
266
- # `$nearSphere` + `$maxDistance` in metres.
305
+ # Documents within +km+ of +geom+, nearest first. Spherical either way:
306
+ # the field's index decides the dialect, see .near_selector.
267
307
  #
268
- def within(geom, km) # rubocop:disable Naming/MethodParameterName
269
- nearby(geom, km: km)
308
+ def within(geom, km, field: nil) # rubocop:disable Naming/MethodParameterName
309
+ nearby(geom, km: Mongoid::Geospatial.km!(km), field: field)
310
+ end
311
+
312
+ #
313
+ # The single closest document within +km+, or nil.
314
+ #
315
+ # `within(geom, km).first` NOT the nearest one
316
+ # `nearest(geom, km)` the nearest one
317
+ #
318
+ # Mongoid's #first and #last sort by `_id` when the criteria carries no
319
+ # sort of its own (contextual/mongo.rb, `view.sort || { _id: 1 }`), and
320
+ # that _id sort replaces the distance order `$near` put there. It reads
321
+ # as working every time the closest document happens to be the oldest.
322
+ #
323
+ def nearest(geom, km, field: nil) # rubocop:disable Naming/MethodParameterName
324
+ within(geom, km, field: field).limit(1).to_a.first
270
325
  end
271
326
 
272
327
  # Performs a $geoNear aggregation pipeline stage to find documents near a point,
@@ -289,17 +344,20 @@ module Mongoid
289
344
  # For spherical queries, specify distance in meters. For 2d queries, in the same units as coordinates.
290
345
  # - `:minDistance` [Numeric] The minimum distance. (MongoDB 2.6+)
291
346
  # - `:query` [Hash] Limits the results to the documents that match the query.
292
- # - `:limit` [Integer] The maximum number of documents to return (applied as a separate `$limit` pipeline stage).
293
- # - `:distanceMultiplier` [Numeric] A factor to multiply all distances returned by the query.
294
- # - `:includeLocs` [String] Specifies the name of the output field that identifies the location used to calculate the distance.
295
- # This is useful when the queried field contains multiple locations (e.g., an array of points) or complex GeoJSON
296
- # geometries (e.g., a Polygon), as it shows which specific point was used for the distance calculation.
297
- # Example: `includeLocs: 'matchedPoint'` would add a `matchedPoint` field to each output document.
298
- #
299
- # @return [Array<Mongoid::Document>] An array of instantiated Mongoid documents.
300
- # Each document will include its original fields plus any fields added by the `$geoNear` stage,
301
- # such as the field specified by `:distanceField` (e.g., `document.distance`) and `:includeLocs`.
302
- # These additional fields are accessible as dynamic attributes on the model instances.
347
+ # - `:limit` [Integer] The maximum number of documents to return
348
+ # (applied as a separate `$limit` pipeline stage).
349
+ # - `:distanceMultiplier` [Numeric] A factor to multiply all distances by.
350
+ # - `:includeLocs` [String] Output field naming WHICH location the distance
351
+ # was measured to the one that matters when the queried field holds
352
+ # several points, or a Polygon. `includeLocs: 'matchedPoint'` adds a
353
+ # `matchedPoint` field to each output document.
354
+ #
355
+ # @return [Mongo::Collection::View::Aggregation] The raw pipeline result it
356
+ # yields `BSON::Document` hashes, NOT model instances, so read a field with
357
+ # `doc['name']` and the distance with `doc['distance']` (or whatever
358
+ # `:distanceField` was set to). Nothing is instantiated: `$geoNear` adds fields
359
+ # a document does not have, and a Point field comes back as a raw pair.
360
+ # Need models? `.map { |attrs| Model.instantiate(attrs) }` at the call site.
303
361
  #
304
362
  # @raise [ArgumentError] If coordinates cannot be mongoized.
305
363
  #
@@ -312,9 +370,9 @@ module Mongoid
312
370
  # query: { category: 'restaurant' },
313
371
  # limit: 10)
314
372
  #
315
- # # Iterate over results
316
- # Place.geo_near(:location, [10, 20], spherical: true).each do |place|
317
- # puts "#{place.name} is #{place.distance} meters away." # Assumes distanceField is 'distance'
373
+ # # Iterate over results — hashes, not documents
374
+ # Place.geo_near(:location, [10, 20], spherical: true).each do |doc|
375
+ # puts "#{doc['name']} is #{doc['distance']} meters away."
318
376
  # end
319
377
  #
320
378
  def geo_near(field_name, coordinates, options = {})
@@ -363,15 +421,7 @@ module Mongoid
363
421
  # Add $limit stage if limit_value was provided
364
422
  pipeline << { '$limit' => limit_value.to_i } if limit_value
365
423
 
366
- # Execute the aggregation pipeline
367
424
  collection.aggregate(pipeline)
368
-
369
- # Don't instantiate results here.
370
- # aggregated_results = collection.aggregate(pipeline)
371
- # Map the raw Hash results from aggregation to Mongoid model instances.
372
- # Mongoid's #instantiate method correctly handles creating model objects
373
- # and assigning attributes, including dynamic ones like the distanceField.
374
- # aggregated_results.map { |attrs| instantiate(attrs) }
375
425
  end
376
426
  end
377
427
  end
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.2.0
4
+ version: 7.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Ong
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 4.0.0
19
+ version: 7.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 4.0.0
26
+ version: 7.0.0
27
27
  description: Mongoid Extension that simplifies MongoDB casting and operations on spatial
28
28
  Ruby objects.
29
29
  email: