mongoid-geospatial 7.1.0 → 7.3.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 +4 -4
- data/README.md +76 -9
- data/lib/mongoid/geospatial/config/point.rb +2 -2
- data/lib/mongoid/geospatial/fields/point.rb +55 -42
- data/lib/mongoid/geospatial/geometry_field.rb +14 -14
- data/lib/mongoid/geospatial/helpers/geom.rb +22 -0
- data/lib/mongoid/geospatial/helpers/spatial.rb +0 -1
- data/lib/mongoid/geospatial/helpers/sphere.rb +0 -1
- data/lib/mongoid/geospatial/keys.rb +20 -0
- data/lib/mongoid/geospatial/version.rb +1 -1
- data/lib/mongoid/geospatial.rb +133 -36
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 59aa6c29ce76d2edc2c6ba8b30492c713e552b31ad728abe8a43743464576681
|
|
4
|
+
data.tar.gz: d62f1d2814b6e0faa93f1d5d2fc16371d30d37e97d72e5eada5a647bae6e9aab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa331e0ce250edf23904a8390442a6cdf831f98d89110f3e5c803b801f80660087444839b1607ac868a31b9aa2dfdeb17d0910294dc83727a281e0fa875b3eff
|
|
7
|
+
data.tar.gz: 1c0b907249ce2ac189bb8f00551dede0b15c59d9c72f218de23c4c746f370060953d065c77c79cb61c209a81607003cfcfdea197ca6893dcc81521b9a5f258c3
|
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 # => { latitude: 40.703056, longitude: -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
|
|
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,41 @@ 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
|
+
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
|
|
243
|
+
City.where(:geom.near_sphere => Mongoid::Geospatial.near_query(geom, 50))
|
|
244
|
+
```
|
|
245
|
+
|
|
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.
|
|
265
|
+
|
|
266
|
+
**Don't call `#first` or `#last` on a `$near` criteria.** Mongoid sorts by
|
|
267
|
+
`_id` when a criteria carries no sort of its own, and that replaces the
|
|
268
|
+
distance order Mongo put there. Walk the criteria instead — `.to_a.first` —
|
|
269
|
+
or use `geo_near`, which returns the distance as a field you can sort on.
|
|
219
270
|
|
|
220
271
|
You can add a `spatial_scope` on your models. So you can query:
|
|
221
272
|
|
|
@@ -253,12 +304,14 @@ Place.geo_near(:location, [10, 20],
|
|
|
253
304
|
query: { category: 'restaurant' }, # Optional: filter documents before geoNear
|
|
254
305
|
limit: 10)
|
|
255
306
|
|
|
256
|
-
# Iterate over results
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
#
|
|
260
|
-
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."
|
|
261
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) }
|
|
262
315
|
```
|
|
263
316
|
|
|
264
317
|
Key features and options for `geo_near`:
|
|
@@ -282,6 +335,20 @@ Bar.within_polygon(location: [[[x,y],...[x,y]]])
|
|
|
282
335
|
Bar.within_polygon(location: street.bbox)
|
|
283
336
|
```
|
|
284
337
|
|
|
338
|
+
- within_circle / within_spherical_circle
|
|
339
|
+
|
|
340
|
+
Mongoid ships `within_polygon` and `within_box` and stopped there; this gem
|
|
341
|
+
registers the two circle shapes, which is what `radius` and `radius_sphere`
|
|
342
|
+
have been building arguments for all along.
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
Bar.where(:location.within_spherical_circle => cafe.location.radius_sphere(5, :km))
|
|
346
|
+
Bar.where(:location.within_circle => cafe.location.radius(0.05)) # degrees, flat
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
`$centerSphere` reads radians (`radius_sphere` hands it those); `$center`
|
|
350
|
+
reads the coordinate system's own units — degrees on a legacy pair.
|
|
351
|
+
|
|
285
352
|
- intersects_line
|
|
286
353
|
- intersects_point
|
|
287
354
|
- intersects_polygon
|
|
@@ -295,7 +362,7 @@ external library corresponding object.
|
|
|
295
362
|
|
|
296
363
|
### Use RGeo?
|
|
297
364
|
|
|
298
|
-
https://github.com/dazuma/rgeo
|
|
365
|
+
<https://github.com/dazuma/rgeo>
|
|
299
366
|
|
|
300
367
|
RGeo is a Ruby wrapper for Proj/GEOS.
|
|
301
368
|
It's perfect when you need to work with complex calculations and projections.
|
|
@@ -303,7 +370,7 @@ It'll require more stuff installed to compile/work.
|
|
|
303
370
|
|
|
304
371
|
### Use GeoRuby?
|
|
305
372
|
|
|
306
|
-
https://github.com/nofxx/georuby
|
|
373
|
+
<https://github.com/nofxx/georuby>
|
|
307
374
|
|
|
308
375
|
GeoRuby is a pure Ruby Geometry Library.
|
|
309
376
|
It's perfect if you want simple calculations and/or keep your stack in pure ruby.
|
|
@@ -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
|
|
@@ -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,9 +32,12 @@ 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
|
-
mongoize
|
|
38
|
+
raise ArgumentError, "Invalid point: #{inspect}" unless (pair = mongoize)
|
|
39
|
+
|
|
40
|
+
pair[args]
|
|
31
41
|
end
|
|
32
42
|
|
|
33
43
|
def each
|
|
@@ -35,24 +45,15 @@ module Mongoid
|
|
|
35
45
|
yield y
|
|
36
46
|
end
|
|
37
47
|
|
|
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
48
|
#
|
|
50
49
|
# Helper for [self, radius]
|
|
51
50
|
#
|
|
52
51
|
# @return [Array] with [self, radius]
|
|
53
52
|
#
|
|
54
53
|
def radius(r = 1) # rubocop:disable Naming/MethodParameterName
|
|
55
|
-
|
|
54
|
+
return nil unless (pair = mongoize)
|
|
55
|
+
|
|
56
|
+
[pair, r]
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
#
|
|
@@ -63,7 +64,7 @@ module Mongoid
|
|
|
63
64
|
# @return [Array] with [self, radius / earth radius]
|
|
64
65
|
#
|
|
65
66
|
def radius_sphere(r = 1, unit = :km) # rubocop:disable Naming/MethodParameterName
|
|
66
|
-
radius r.to_f / Mongoid::Geospatial.earth_radius
|
|
67
|
+
radius r.to_f / Mongoid::Geospatial.earth_radius.fetch(unit)
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
#
|
|
@@ -88,6 +89,26 @@ module Mongoid
|
|
|
88
89
|
"#{x}, #{y}"
|
|
89
90
|
end
|
|
90
91
|
|
|
92
|
+
#
|
|
93
|
+
# Point representation as a Hash
|
|
94
|
+
# Optional param: custom keys.
|
|
95
|
+
#
|
|
96
|
+
# @return [Hash] with { lng_key => x, lat_key => y }
|
|
97
|
+
#
|
|
98
|
+
def to_hsh(xkey = :x, ykey = :y)
|
|
99
|
+
{ xkey => x, ykey => y }
|
|
100
|
+
end
|
|
101
|
+
alias to_hash to_hsh
|
|
102
|
+
|
|
103
|
+
#
|
|
104
|
+
# Point representation more commonly used
|
|
105
|
+
# Latitude, Longitude
|
|
106
|
+
#
|
|
107
|
+
# @return [Hash] with { latitude: y, longitude: x }
|
|
108
|
+
def to_lat_lon
|
|
109
|
+
{ latitude: y, longitude: x }
|
|
110
|
+
end
|
|
111
|
+
|
|
91
112
|
#
|
|
92
113
|
# Point definition as GeoJSON
|
|
93
114
|
#
|
|
@@ -113,32 +134,21 @@ module Mongoid
|
|
|
113
134
|
end
|
|
114
135
|
|
|
115
136
|
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
137
|
+
# Crow-flies distance. Haversine, Mongo's earth radius (6371 km).
|
|
138
|
+
# RGeo/GeoRuby still win for projections; this is ETA and "how far".
|
|
139
|
+
#
|
|
140
|
+
def distance(other, unit = :km) # rubocop:disable Metrics/AbcSize
|
|
141
|
+
xy = other.is_a?(Point) ? [other.x, other.y] : self.class.mongoize(other)
|
|
142
|
+
raise ArgumentError, "Invalid point: #{other.inspect}" unless xy&.size == 2
|
|
143
|
+
|
|
144
|
+
dlat = (xy[1] - y) * RAD_PER_DEG
|
|
145
|
+
dlon = (xy[0] - x) * RAD_PER_DEG
|
|
146
|
+
a = (Math.sin(dlat / 2)**2) +
|
|
147
|
+
(Math.cos(y * RAD_PER_DEG) * Math.cos(xy[1] * RAD_PER_DEG) *
|
|
148
|
+
(Math.sin(dlon / 2)**2))
|
|
149
|
+
c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
|
150
|
+
Mongoid::Geospatial.earth_radius[unit] * c
|
|
151
|
+
end
|
|
142
152
|
|
|
143
153
|
class << self
|
|
144
154
|
#
|
|
@@ -189,7 +199,7 @@ module Mongoid
|
|
|
189
199
|
def from_string(str)
|
|
190
200
|
return nil if str.empty?
|
|
191
201
|
|
|
192
|
-
str.split(/,|\s/).reject(&:empty?)
|
|
202
|
+
from_array(str.split(/,|\s/).reject(&:empty?))
|
|
193
203
|
end
|
|
194
204
|
|
|
195
205
|
#
|
|
@@ -222,6 +232,9 @@ module Mongoid
|
|
|
222
232
|
# @return (Array)
|
|
223
233
|
#
|
|
224
234
|
def from_hash(hsh)
|
|
235
|
+
coords = hsh[:coordinates] || hsh['coordinates']
|
|
236
|
+
return from_array(coords) if coords
|
|
237
|
+
|
|
225
238
|
raise 'Hash must have at least 2 items' if hsh.size < 2
|
|
226
239
|
|
|
227
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
min_y =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
81
|
+
radius r.to_f / Mongoid::Geospatial.earth_radius.fetch(unit)
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
class << self
|
|
@@ -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
|
data/lib/mongoid/geospatial.rb
CHANGED
|
@@ -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,55 @@ 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
|
+
# 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.
|
|
99
|
+
#
|
|
100
|
+
def self.near_query(geom, km) # rubocop:disable Naming/MethodParameterName
|
|
101
|
+
{ '$geometry' => { 'type' => 'Point', 'coordinates' => mongoize_point!(geom) },
|
|
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 } }
|
|
121
|
+
end
|
|
122
|
+
|
|
72
123
|
# Methods applied to Document's class
|
|
73
124
|
module ClassMethods
|
|
74
125
|
#
|
|
@@ -78,7 +129,7 @@ module Mongoid
|
|
|
78
129
|
# @param options [Hash] Additional options for the index.
|
|
79
130
|
#
|
|
80
131
|
def spatial_index(name, options = {})
|
|
81
|
-
|
|
132
|
+
remember_indexed(name)
|
|
82
133
|
index({ name => '2d' }, options)
|
|
83
134
|
end
|
|
84
135
|
|
|
@@ -89,9 +140,26 @@ module Mongoid
|
|
|
89
140
|
# @param options [Hash] Additional options for the index.
|
|
90
141
|
#
|
|
91
142
|
def spherical_index(name, options = {})
|
|
92
|
-
|
|
143
|
+
remember_indexed(name)
|
|
93
144
|
index({ name => '2dsphere' }, options)
|
|
94
145
|
end
|
|
146
|
+
alias sphere_index spherical_index
|
|
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
|
+
|
|
157
|
+
#
|
|
158
|
+
# A Point on a 2dsphere index. Default name is `geom`.
|
|
159
|
+
#
|
|
160
|
+
def geom(name = :geom)
|
|
161
|
+
field name, type: Point, sphere: true
|
|
162
|
+
end
|
|
95
163
|
|
|
96
164
|
#
|
|
97
165
|
# # Queries
|
|
@@ -201,28 +269,59 @@ module Mongoid
|
|
|
201
269
|
#
|
|
202
270
|
# @param coordinates [Array, Mongoid::Geospatial::Point] The coordinates (e.g., [lon, lat])
|
|
203
271
|
# or a Point object to find documents near to.
|
|
204
|
-
# @param
|
|
272
|
+
# @param km [Numeric, nil] Optional cap in kilometres.
|
|
205
273
|
#
|
|
206
274
|
# @return [Mongoid::Criteria] A criteria object for the query.
|
|
207
275
|
#
|
|
208
276
|
# Example:
|
|
209
277
|
# Bar.nearby([10, 20])
|
|
210
|
-
# Alarm.nearby(my_point_object)
|
|
278
|
+
# Alarm.nearby(my_point_object, km: 30)
|
|
211
279
|
#
|
|
212
|
-
def nearby(coordinates,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
"Mark a field with 'spatial: true' or 'sphere: true'."
|
|
216
|
-
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
|
|
217
283
|
|
|
218
|
-
|
|
219
|
-
|
|
284
|
+
criteria.where(pin.send(sphere ? :near_sphere : :near) => coordinates)
|
|
285
|
+
end
|
|
220
286
|
|
|
221
|
-
|
|
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'."
|
|
299
|
+
end
|
|
222
300
|
|
|
223
|
-
|
|
301
|
+
[sym, fields.fetch(sym.to_s).options[:sphere] ? true : false]
|
|
302
|
+
end
|
|
224
303
|
|
|
225
|
-
|
|
304
|
+
#
|
|
305
|
+
# Documents within +km+ of +geom+, nearest first. Spherical either way:
|
|
306
|
+
# the field's index decides the dialect, see .near_selector.
|
|
307
|
+
#
|
|
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
|
|
226
325
|
end
|
|
227
326
|
|
|
228
327
|
# Performs a $geoNear aggregation pipeline stage to find documents near a point,
|
|
@@ -252,10 +351,12 @@ module Mongoid
|
|
|
252
351
|
# geometries (e.g., a Polygon), as it shows which specific point was used for the distance calculation.
|
|
253
352
|
# Example: `includeLocs: 'matchedPoint'` would add a `matchedPoint` field to each output document.
|
|
254
353
|
#
|
|
255
|
-
# @return [
|
|
256
|
-
#
|
|
257
|
-
#
|
|
258
|
-
#
|
|
354
|
+
# @return [Mongo::Collection::View::Aggregation] The raw pipeline result — it
|
|
355
|
+
# yields `BSON::Document` hashes, NOT model instances, so read a field with
|
|
356
|
+
# `doc['name']` and the distance with `doc['distance']` (or whatever
|
|
357
|
+
# `:distanceField` was set to). Nothing is instantiated: `$geoNear` adds fields
|
|
358
|
+
# a document does not have, and a Point field comes back as a raw pair.
|
|
359
|
+
# Need models? `.map { |attrs| Model.instantiate(attrs) }` at the call site.
|
|
259
360
|
#
|
|
260
361
|
# @raise [ArgumentError] If coordinates cannot be mongoized.
|
|
261
362
|
#
|
|
@@ -268,20 +369,26 @@ module Mongoid
|
|
|
268
369
|
# query: { category: 'restaurant' },
|
|
269
370
|
# limit: 10)
|
|
270
371
|
#
|
|
271
|
-
# # Iterate over results
|
|
272
|
-
# Place.geo_near(:location, [10, 20], spherical: true).each do |
|
|
273
|
-
# puts "#{
|
|
372
|
+
# # Iterate over results — hashes, not documents
|
|
373
|
+
# Place.geo_near(:location, [10, 20], spherical: true).each do |doc|
|
|
374
|
+
# puts "#{doc['name']} is #{doc['distance']} meters away."
|
|
274
375
|
# end
|
|
275
376
|
#
|
|
276
377
|
def geo_near(field_name, coordinates, options = {})
|
|
277
|
-
mongoized_coords = Mongoid::Geospatial
|
|
278
|
-
|
|
279
|
-
raise ArgumentError, "Invalid coordinates provided: #{coordinates.inspect}" unless mongoized_coords
|
|
378
|
+
mongoized_coords = Mongoid::Geospatial.mongoize_point!(coordinates)
|
|
280
379
|
|
|
281
380
|
# User-provided options. Work with a copy.
|
|
282
381
|
user_options = options.dup
|
|
283
382
|
limit_value = user_options.delete(:limit) # Handled by a separate pipeline stage
|
|
284
383
|
|
|
384
|
+
# `km:` is metres on the wire, and metres only holds for a GeoJSON
|
|
385
|
+
# `near` on a sphere. A legacy pair would read `maxDistance` in radians.
|
|
386
|
+
if (km = user_options.delete(:km))
|
|
387
|
+
user_options[:maxDistance] = km.to_f * 1_000
|
|
388
|
+
user_options[:spherical] = true
|
|
389
|
+
mongoized_coords = { 'type' => 'Point', 'coordinates' => mongoized_coords }
|
|
390
|
+
end
|
|
391
|
+
|
|
285
392
|
# Core $geoNear parameters derived from method arguments, these are not overrideable by user_options.
|
|
286
393
|
geo_near_core_params = {
|
|
287
394
|
key: field_name.to_s,
|
|
@@ -297,10 +404,8 @@ module Mongoid
|
|
|
297
404
|
# Merge user options over defaults, then ensure core parameters are set.
|
|
298
405
|
geo_near_stage_options = geo_near_defaultable_params.merge(user_options).merge(geo_near_core_params)
|
|
299
406
|
|
|
300
|
-
#
|
|
301
|
-
|
|
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?
|
|
407
|
+
# $geoNear wants a strict boolean, and honours the caller's choice.
|
|
408
|
+
geo_near_stage_options[:spherical] = geo_near_stage_options[:spherical] ? true : false
|
|
304
409
|
|
|
305
410
|
# Note on performance:
|
|
306
411
|
# $geoNear is an aggregation pipeline stage. For simple proximity queries,
|
|
@@ -315,15 +420,7 @@ module Mongoid
|
|
|
315
420
|
# Add $limit stage if limit_value was provided
|
|
316
421
|
pipeline << { '$limit' => limit_value.to_i } if limit_value
|
|
317
422
|
|
|
318
|
-
# Execute the aggregation pipeline
|
|
319
423
|
collection.aggregate(pipeline)
|
|
320
|
-
|
|
321
|
-
# Don't instantiate results here.
|
|
322
|
-
# aggregated_results = collection.aggregate(pipeline)
|
|
323
|
-
# Map the raw Hash results from aggregation to Mongoid model instances.
|
|
324
|
-
# Mongoid's #instantiate method correctly handles creating model objects
|
|
325
|
-
# and assigning attributes, including dynamic ones like the distanceField.
|
|
326
|
-
# aggregated_results.map { |attrs| instantiate(attrs) }
|
|
327
424
|
end
|
|
328
425
|
end
|
|
329
426
|
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.
|
|
4
|
+
version: 7.3.0
|
|
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:
|
|
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:
|
|
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:
|
|
@@ -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
|