mongoid_location 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,13 +2,13 @@ require 'rgeo'
2
2
  require 'mongoid'
3
3
  require 'active_support/core_ext/string/inflections'
4
4
  require 'active_support/concern'
5
+ require 'mongoid_location/version'
6
+ #require 'mongoid_location/contextual/mongo'
5
7
  if Mongoid::VERSION > '3'
6
8
  require 'mongoid_location/contextual/mongo'
7
9
  else
8
10
  require 'mongoid_location/contexts/mongo'
9
11
  end
10
- require 'mongoid_location/version'
11
- #require 'mongoid_location/contextual/mongo'
12
12
  require 'mongoid_location/criteria'
13
13
  require 'mongoid_location/extensions/symbol'
14
14
  require 'mongoid_location/extensions/rgeo_spherical_point_impl'
@@ -0,0 +1,117 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc:
3
+ module Contexts #:nodoc:
4
+ class Mongo #:nodoc:
5
+
6
+ # Fetches rows from the data base sorted by distance.
7
+ # In MongoDB versions 1.7 and above it returns a distance.
8
+ # Uses all criteria chains except without, only, asc, desc, order_by
9
+ #
10
+ # @example Minimal Query
11
+ #
12
+ # Address.geo_near([70,40])
13
+ #
14
+ # @example Chained Query
15
+ #
16
+ # Address.where(:state => 'ny').geo_near([70,40])
17
+ #
18
+ # @example Calc Distances Query
19
+ #
20
+ # Address.geo_near([70,40], :max_distance => 5, :unit => 5)
21
+ #
22
+ # @param [ Array, Hash, #to_xy ] center The center of where to calculate distance from
23
+ # @param [ Hash ] opts the options to query with
24
+ # @options opts [Integer] :num The number of rows to fetch
25
+ # @options opts [Hash] :query The query to filter the rows by, accepts
26
+ # @options opts [Numeric] :distance_multiplier this is multiplied against the calculated distance
27
+ # @options opts [Numeric] :max_distance The max distance of a row that should be returned in :unit(s)
28
+ # @options opts [Numeric, :km, :k, :mi, :ft] :unit automatically sets :distance_multiplier and converts :max_distance
29
+ # @options opts [true,false] :spherical Will determine the distance either by spherical calculation or flat calculation
30
+ # @options opts [TrueClass,Array<Symbol>] :calculate Which extra fields to calculate distance for in ruby, if set to TrueClass it will calculate all spatial fields
31
+ #
32
+ # @return [ Array ] Sorted Rows
33
+ def geo_near(center, opts = {})
34
+ opts = self.options.merge(opts)
35
+ # convert point
36
+ center = center.to_xy if center.respond_to?(:to_xy)
37
+ center = [center.x, center.y] if center.respond_to?(:x)
38
+
39
+ # set default opts
40
+ opts[:skip] ||= 0
41
+
42
+ if unit = Mongoid::Location.earth_radius[opts[:unit]]
43
+ opts[:unit] = (opts[:spherical]) ? unit : unit * Mongoid::Location::RAD_PER_DEG
44
+ end
45
+
46
+ if unit = Mongoid::Location.earth_radius[opts[:distance_multiplier]]
47
+ opts[:distance_multiplier] = (opts[:spherical]) ? unit : unit * Mongoid::Location::RAD_PER_DEG
48
+ end
49
+
50
+ opts[:distance_multiplier] = opts[:unit] if opts[:unit].kind_of?(Numeric)
51
+
52
+ # setup paging.
53
+ if opts.has_key?(:page)
54
+ opts[:page] ||= 1
55
+ opts[:paginator] ||= Mongoid::Location.paginator()
56
+
57
+ if opts[:per_page].blank?
58
+ opts[:per_page] = case opts[:paginator]
59
+ when :will_paginate
60
+ @document.per_page
61
+ when :kaminari
62
+ Kaminari.config.default_per_page
63
+ else
64
+ Mongoid::Location.default_per_page
65
+ end
66
+ opts[:per_page] = opts[:per_page].to_i
67
+ end
68
+
69
+ end
70
+ opts[:query] = create_geo_near_query(center,opts)
71
+ results = klass.db.command(opts[:query])
72
+ Mongoid::Location::GeoNearResults.new(klass,results,opts)
73
+ end
74
+
75
+ private
76
+
77
+ def create_geo_near_query(center,opts)
78
+ # minimum query
79
+ query = BSON::OrderedHash.new
80
+ query[:geoNear] = klass.collection_name
81
+ query[:near] = center
82
+
83
+ # create limit and use skip
84
+ if opts[:num]
85
+ query['num'] = opts[:skip].to_i + opts[:num].to_i
86
+ elsif opts[:limit]
87
+ query['num'] = opts[:skip].to_i + opts[:limit].to_i
88
+ elsif opts[:page]
89
+ query['num'] = opts[:skip].to_i + (opts[:page].to_i * opts[:per_page].to_i)
90
+ end
91
+
92
+ # allow the use of complex werieis
93
+ if opts[:query]
94
+ query['query'] = self.criteria.where(opts[:query]).selector
95
+ elsif self.selector != {}
96
+ query['query'] = self.selector
97
+ end
98
+
99
+ if opts[:max_distance]
100
+ query['maxDistance'] = opts[:max_distance].to_f
101
+ query['maxDistance'] = query['maxDistance']/opts[:unit].to_f if opts[:unit]
102
+ end
103
+
104
+ if opts[:unique_docs]
105
+ query['uniqueDocs'] = true
106
+ end
107
+
108
+ query['spherical'] = true if opts[:spherical]
109
+
110
+ # mongodb < 1.7 returns degrees but with earth flat. in Mongodb 1.7 you can set sphere and let mongodb calculate the distance in Miles or KM
111
+ # for mongodb < 1.7 we need to run Haversine first before calculating degrees to Km or Miles. See below.
112
+ query['distanceMultiplier'] = opts[:distance_multiplier].to_f if opts[:distance_multiplier]
113
+ query
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,118 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc:
3
+ module Contextual #:nodoc:
4
+ class Mongo #:nodoc:
5
+
6
+ # Fetches rows from the data base sorted by distance.
7
+ # In MongoDB versions 1.7 and above it returns a distance.
8
+ # Uses all criteria chains except without, only, asc, desc, order_by
9
+ #
10
+ # @example Minimal Query
11
+ #
12
+ # Address.geo_near([70,40])
13
+ #
14
+ # @example Chained Query
15
+ #
16
+ # Address.where(:state => 'ny').geo_near([70,40])
17
+ #
18
+ # @example Calc Distances Query
19
+ #
20
+ # Address.geo_near([70,40], :max_distance => 5, :unit => 5)
21
+ #
22
+ # @param [ Array, Hash, #to_xy ] center The center of where to calculate distance from
23
+ # @param [ Hash ] opts the options to query with
24
+ # @options opts [Integer] :num The number of rows to fetch
25
+ # @options opts [Hash] :query The query to filter the rows by, accepts
26
+ # @options opts [Numeric] :distance_multiplier this is multiplied against the calculated distance
27
+ # @options opts [Numeric] :max_distance The max distance of a row that should be returned in :unit(s)
28
+ # @options opts [Numeric, :km, :k, :mi, :ft] :unit automatically sets :distance_multiplier and converts :max_distance
29
+ # @options opts [true,false] :spherical Will determine the distance either by spherical calculation or flat calculation
30
+ # @options opts [TrueClass,Array<Symbol>] :calculate Which extra fields to calculate distance for in ruby, if set to TrueClass it will calculate all spatial fields
31
+ #
32
+ # @return [ Array ] Sorted Rows
33
+ def geo_near(center, opts = {})
34
+ opts = self.criteria.options.merge(opts)
35
+ # convert point
36
+ center = center.to_xy if center.respond_to?(:to_xy)
37
+ center = [center.x, center.y] if center.respond_to?(:x)
38
+
39
+ # set default opts
40
+ opts[:skip] ||= 0
41
+
42
+ if unit = Mongoid::Location.earth_radius[opts[:unit]]
43
+ opts[:unit] = (opts[:spherical]) ? unit : unit * Mongoid::Location::RAD_PER_DEG
44
+ end
45
+
46
+ if unit = Mongoid::Location.earth_radius[opts[:distance_multiplier]]
47
+ opts[:distance_multiplier] = (opts[:spherical]) ? unit : unit * Mongoid::Location::RAD_PER_DEG
48
+ end
49
+
50
+ opts[:distance_multiplier] = opts[:unit] if opts[:unit].kind_of?(Numeric)
51
+
52
+ # setup paging.
53
+ if opts.has_key?(:page)
54
+ opts[:page] ||= 1
55
+ opts[:paginator] ||= Mongoid::Location.paginator()
56
+
57
+ if opts[:per_page].blank?
58
+ opts[:per_page] = case opts[:paginator]
59
+ when :will_paginate
60
+ @document.per_page
61
+ when :kaminari
62
+ Kaminari.config.default_per_page
63
+ else
64
+ Mongoid::Location.default_per_page
65
+ end
66
+ opts[:per_page] = opts[:per_page].to_i
67
+ end
68
+
69
+ end
70
+ opts[:query] = create_geo_near_query(center,opts)
71
+ results = klass.mongo_session.command(opts[:query])
72
+
73
+ Mongoid::Location::GeoNearResults.new(klass,results,opts)
74
+ end
75
+
76
+ private
77
+
78
+ def create_geo_near_query(center,opts)
79
+ # minimum query
80
+ query = {}
81
+ query[:geoNear] = klass.collection_name
82
+ query[:near] = center
83
+
84
+ # create limit and use skip
85
+ if opts[:num]
86
+ query['num'] = opts[:skip].to_i + opts[:num].to_i
87
+ elsif opts[:limit]
88
+ query['num'] = opts[:skip].to_i + opts[:limit].to_i
89
+ elsif opts[:page]
90
+ query['num'] = opts[:skip].to_i + (opts[:page].to_i * opts[:per_page].to_i)
91
+ end
92
+
93
+ # allow the use of complex werieis
94
+ if opts[:query]
95
+ query['query'] = self.criteria.where(opts[:query]).selector
96
+ elsif self.criteria.selector != {}
97
+ query['query'] = self.criteria.selector
98
+ end
99
+
100
+ if opts[:max_distance]
101
+ query['maxDistance'] = opts[:max_distance].to_f
102
+ query['maxDistance'] = query['maxDistance']/opts[:unit].to_f if opts[:unit]
103
+ end
104
+
105
+ if opts[:unique_docs]
106
+ query['uniqueDocs'] = true
107
+ end
108
+
109
+ query['spherical'] = true if opts[:spherical]
110
+
111
+ # mongodb < 1.7 returns degrees but with earth flat. in Mongodb 1.7 you can set sphere and let mongodb calculate the distance in Miles or KM
112
+ # for mongodb < 1.7 we need to run Haversine first before calculating degrees to Km or Miles. See below.
113
+ query['distanceMultiplier'] = opts[:distance_multiplier].to_f if opts[:distance_multiplier]
114
+ query
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,10 @@
1
+ require 'mongoid_location/criterion/complex'
2
+ require 'mongoid_location/criterion/near_spatial'
3
+ require 'mongoid_location/criterion/within_spatial'
4
+
5
+
6
+ module Mongoid #:nodoc:
7
+ class Criteria
8
+ delegate :geo_near, :to => :context
9
+ end
10
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc:
3
+ module Criterion #:nodoc:
4
+ # Complex criterion are used when performing operations on symbols to get
5
+ # get a shorthand syntax for where clauses.
6
+ #
7
+ # Example:
8
+ #
9
+ # <tt>{ :field => { "$lt" => "value" } }</tt>
10
+ # becomes:
11
+ # <tt> { :field.lt => "value }</tt>
12
+ class Complex
13
+
14
+ attr_accessor :key, :operator
15
+
16
+ def initialize(opts = {})
17
+ @key, @operator = opts[:key], opts[:operator]
18
+ end
19
+
20
+ def to_mongo_query v
21
+ {"$#{operator}" => v}
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+ # module Mongoid #:nodoc:
3
+ # module Criterion #:nodoc:
4
+ # module Inclusion
5
+ # def near(attributes = {})
6
+ # update_selector(attributes, "$near")
7
+ # end
8
+
9
+ # def near_sphere(attributes = {})
10
+ # update_selector(attributes, "$near")
11
+ # end
12
+ # end
13
+ # end
14
+ # end
@@ -0,0 +1,52 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc:
3
+ module Criterion #:nodoc:
4
+
5
+ # NearSpecial criterion is used when performing #near with symbols to get
6
+ # get a shorthand syntax for where clauses.
7
+ #
8
+ # @example Coninputersion of a simple to complex criterion.
9
+ # { :field => { "$nearSphere" => [20,30]}, '$maxDistance' => 5 }
10
+ # becomes:
11
+ # { :field.near_sphere => {:point => [20,30], :max => 5, :unit => :km} }
12
+ class NearSpatial < Complex
13
+
14
+ # Coninputert input to query for near or nearSphere
15
+ #
16
+ # @example
17
+ # near = NearSpatial.new(:key => :field, :operator => "near")
18
+ # near.to_mongo_query({:point => [:50,50], :max => 5, :unit => :km}) => { '$near : [50,50]' , '$maxDistance' : 5 }
19
+ #
20
+ # @param [Hash,Array] input input to coninputer to query
21
+ def to_mongo_query(input)
22
+ if input.respond_to?(:x)
23
+ {"$#{operator}" => [input.x, input.y]} #, '$maxDistance' => input[1] }
24
+ elsif input.kind_of?(Hash)
25
+ raise ':point required to make valid query' unless input[:point]
26
+ input[:point] = input[:point].to_xy if input[:point].respond_to?(:to_xy)
27
+ query = {"$#{operator}" => input[:point] }
28
+ if input[:max]
29
+ query['$maxDistance'] = input[:max].to_f
30
+
31
+ if unit = Mongoid::Location.earth_radius[input[:unit]]
32
+ unit *= Mongoid::Location::RAD_PER_DEG unless operator =~ /sphere/i
33
+ input[:unit] = unit
34
+ end
35
+
36
+ query['$maxDistance'] = query['$maxDistance']/input[:unit].to_f if input[:unit]
37
+ end
38
+ query
39
+ elsif input.kind_of? Array
40
+ if input.first.kind_of? Numeric
41
+ {"$#{operator}" => input }
42
+ else
43
+ input[0] = input[0].to_xy if input[0].respond_to?(:to_xy)
44
+ {"$#{operator}" => input[0], '$maxDistance' => input[1] }
45
+ end
46
+ end
47
+ end
48
+
49
+ end
50
+ end
51
+ end
52
+
@@ -0,0 +1,62 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc:
3
+ module Criterion #:nodoc:
4
+
5
+ # WithinSpecial criterion is used when performing #within with symbols to get
6
+ # get a shorthand syntax for where clauses.
7
+ #
8
+ # @example Conversion of a simple to complex criterion.
9
+ # { :field => { "$within" => {'$center' => [20,30]} } }
10
+ # becomes:
11
+ # { :field.within(:center) => [20,30] }
12
+ class WithinSpatial < Complex
13
+
14
+ # Convert input to query for box, polygon, center, and centerSphere
15
+ #
16
+ # @example
17
+ # within = WithinSpatial.new(opts[:key] => 'point', :operator => 'center')
18
+ # within.to_mongo_query({:point => [20,30], :max => 5, :unit => :km}) #=>
19
+ #
20
+ # @param [Hash,Array] input Variable to conver to query
21
+ def to_mongo_query(input)
22
+ if ['box','polygon'].include?(@operator)
23
+ input = input.values if input.kind_of?(Hash)
24
+ if input.respond_to?(:map)
25
+ input.map! do |v|
26
+ v.respond_to?(:to_xy) ? v.to_xy : v
27
+ end
28
+ else
29
+ input
30
+ end
31
+ elsif ['center','centerSphere'].include?(@operator)
32
+
33
+ if input.kind_of?(Hash) || input.kind_of?(ActiveSupport::OrderedHash)
34
+ raise ':point required to make valid query' unless input[:point]
35
+ input[:point] = input[:point].to_xy if input[:point].respond_to?(:to_xy)
36
+ if input[:max]
37
+ input[:max] = input[:max].to_f
38
+
39
+ if unit = Mongoid::Location.earth_radius[input[:unit]]
40
+ unit *= Mongoid::Location::RAD_PER_DEG unless operator =~ /sphere/i
41
+ input[:unit] = unit
42
+ end
43
+
44
+ input[:max] = input[:max]/input[:unit].to_f if input[:unit]
45
+
46
+ input = [input[:point],input[:max]]
47
+ else
48
+ input = input[:point]
49
+ end
50
+ end
51
+
52
+ if input.kind_of? Array
53
+ input[0] = input[0].to_xy if input[0].respond_to?(:to_xy)
54
+ end
55
+
56
+ end
57
+ {'$within' => {"$#{@operator}"=>input} }
58
+ end
59
+ end
60
+ end
61
+ end
62
+
@@ -0,0 +1,13 @@
1
+ module RGeo
2
+ module Geographic
3
+ class SphericalPointImpl
4
+ def to_a
5
+ [x, y, z]
6
+ end
7
+
8
+ def [] index
9
+ to_a[index]
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc:
3
+ module Extensions #:nodoc:
4
+ module Symbol #:nodoc:
5
+
6
+ # return a class that will accept a value to convert the query correctly for near
7
+ #
8
+ # @param [Symbol] calc This accepts :sphere
9
+ #
10
+ # @return [Criterion::NearSpatial]
11
+
12
+ def near(calc = :flat)
13
+ Criterion::NearSpatial.new(:operator => get_op('near',calc), :key => self)
14
+ end
15
+
16
+ # alias for self.near(:sphere)
17
+ #
18
+ # @return [Criterion::NearSpatial]
19
+ def near_sphere
20
+ self.near(:sphere)
21
+ end
22
+
23
+ # @param [Symbol] shape :box,:polygon,:center,:center_sphere
24
+ #
25
+ # @return [Criterion::WithinSpatial]
26
+ def within(shape)
27
+ shape = get_op(:center,:sphere) if shape == :center_sphere
28
+ Criterion::WithinSpatial.new(:operator => shape.to_s , :key => self)
29
+ end
30
+
31
+ private
32
+
33
+ def get_op operator, calc
34
+ if calc.to_sym == :sphere
35
+ "#{operator}Sphere"
36
+ else
37
+ operator.to_s
38
+ end
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+
45
+
46
+ ::Symbol.__send__(:include, Mongoid::Extensions::Symbol)
@@ -0,0 +1,16 @@
1
+ #require 'ostruct'
2
+
3
+ Mongoid::Fields.option :spatial do |model,field,options|
4
+ options = {} unless options.kind_of?(Hash)
5
+ lat_meth = options[:lat] || :lat
6
+ lng_meth = options[:lng] || :lng
7
+ model.class_eval do
8
+ self.spatial_fields ||= []
9
+ self.spatial_fields << field.name.to_sym if self.spatial_fields.kind_of? Array
10
+
11
+ define_method "distance_from_#{field.name}" do |*args|
12
+ self.distance_from(field.name, *args)
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ module Mongoid
2
+ module Location
3
+ class LineString
4
+ # See http://mongoid.org/en/mongoid/docs/upgrading.html
5
+ def mongoize
6
+ to_a
7
+ end
8
+
9
+ class << self
10
+ def demongoize(object)
11
+ RGeo::Geographic.spherical_factory.line_string *object
12
+ end
13
+ # def evolve(object)
14
+ # { "$gte" => object.first, "$lte" => object.last }
15
+ # end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Mongoid
2
+ module Location
3
+ class LineString
4
+ include Mongoid::Fields::Serializable
5
+
6
+ def self.instantiate name, options = {}
7
+ super
8
+ end
9
+
10
+ def serialize(object)
11
+ object.to_a
12
+ end
13
+
14
+ def deserialize(object)
15
+ return unless object && !object.empty?
16
+ RGeo::Geographic.spherical_factory.line_string(*object)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Mongoid
2
+ module Location
3
+ class Point
4
+ include Mongoid::Fields::Serializable
5
+
6
+ def self.instantiate name, options = {}
7
+ super
8
+ end
9
+
10
+ def serialize(object)
11
+ object.respond_to?(:x) ? [object.x, object.y] : object
12
+ end
13
+
14
+ def deserialize(object)
15
+ return unless object && !object.empty?
16
+ RGeo::Geographic.spherical_factory.point *object
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ module Mongoid
2
+ module Location
3
+ class Polygon
4
+ include Mongoid::Fields::Serializable
5
+
6
+ def self.instantiate name, options = {}
7
+ super
8
+ end
9
+
10
+ def serialize(object)
11
+ object
12
+ end
13
+
14
+ def deserialize(object)
15
+ points = object.map do |pair|
16
+ RGeo::Geographic.spherical_factory.point *pair
17
+ end
18
+ ring = RGeo::Geographic.spherical_factory.linear_ring points
19
+ RGeo::Geographic.spherical_factory.polygon ring
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,63 @@
1
+ module Mongoid
2
+ module Location
3
+ class Point
4
+ # See http://mongoid.org/en/mongoid/docs/upgrading.html
5
+
6
+ def mongoize
7
+ self.respond_to?(:x) ? [x, y] : self
8
+ # if object.respond_to? :x
9
+ # { "x" => object.x, "y" => object.y }
10
+ # else
11
+ # { "x" => object[0], "y" => object[1] }
12
+ # end
13
+ end
14
+
15
+ class << self
16
+
17
+ def demongoize(object)
18
+ return unless object && !object.empty?
19
+ RGeo::Geographic.spherical_factory.point *object
20
+ #["x"], object["y"]
21
+ end
22
+
23
+ def mongoize(object)
24
+ object.respond_to?(:x) ? [object.x, object.y] : object
25
+ end
26
+
27
+ # Converts the object that was supplied to a criteria and converts it
28
+ # into a database friendly form.
29
+ def evolve(object)
30
+ object.respond_to?(:x) ? [object.x, object.y] : object
31
+ end
32
+ end
33
+
34
+ # - self.spacial_fields ||= []
35
+ # - self.spacial_fields << field.name.to_sym if self.spacial_fields.kind_of? Array
36
+ # -
37
+ # - define_method "distance_from_#{field.name}" do |*args|
38
+ # - self.distance_from(field.name, *args)
39
+ # - end
40
+ # -
41
+ # - define_method field.name do
42
+ # - output = self[field.name] || [nil,nil]
43
+ # - output = {lng_meth => output[0], lat_meth => output[1]} unless options[:return_array]
44
+ # - return options[:class].new(output) if options[:class]
45
+ # - output
46
+ # - end
47
+ # -
48
+ # - define_method "#{field.name}=" do |arg|
49
+ # - if arg.kind_of?(Hash) && arg[lng_meth] && arg[lat_meth]
50
+ # - arg = [arg[lng_meth].to_f, arg[lat_meth].to_f]
51
+ # - elsif arg.respond_to?(:to_xy)
52
+ # - arg = arg.to_xy
53
+ # - end
54
+ # - self[field.name]=arg
55
+ # - arg = [nil,nil] if arg.nil?
56
+ # - return arg[0..1] if options[:return_array]
57
+ # - h = {lng_meth => arg[0], lat_meth => arg[1]}
58
+ # - return h if options[:class].blank?
59
+ # - options[:class].new(h)
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,25 @@
1
+ module Mongoid
2
+ module Location
3
+ class Polygon
4
+ # See http://mongoid.org/en/mongoid/docs/upgrading.html
5
+
6
+ def mongoize
7
+ self #.flatten
8
+ end
9
+
10
+ class << self
11
+ def demongoize(object)
12
+ points = object.map do |pair|
13
+ RGeo::Geographic.spherical_factory.point *pair
14
+ end
15
+ ring = RGeo::Geographic.spherical_factory.linear_ring points
16
+ RGeo::Geographic.spherical_factory.polygon ring
17
+ end
18
+ # def evolve(object)
19
+ # { "$gte" => object.first, "$lte" => object.last }
20
+ # end
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ module Mongoid #:nodoc:
2
+ module Finders
3
+ delegate :geo_near, :to => :criteria
4
+ end
5
+ end
@@ -0,0 +1,86 @@
1
+ require 'mongoid_location/location/core_ext'
2
+ require 'mongoid_location/location/geo_near_results'
3
+
4
+ module Mongoid
5
+ module Location
6
+ extend ActiveSupport::Concern
7
+
8
+ LNG_SYMBOLS = [:x, :lon, :long, :lng, :longitude]
9
+ LAT_SYMBOLS = [:y, :lat, :latitude]
10
+
11
+ EARTH_RADIUS_KM = 6371 # taken directly from mongodb
12
+
13
+ EARTH_RADIUS = {
14
+ :km => EARTH_RADIUS_KM,
15
+ :m => EARTH_RADIUS_KM*1000,
16
+ :mi => EARTH_RADIUS_KM*0.621371192, # taken directly from mongodb
17
+ :ft => EARTH_RADIUS_KM*5280*0.621371192,
18
+ :sm => EARTH_RADIUS_KM*0.53995680345572 # sea mile
19
+ }
20
+
21
+ GEO_FACTORY = RGeo::Geographic.spherical_factory
22
+ RAD_PER_DEG = Math::PI/180
23
+ mattr_accessor :lng_symbols
24
+ @@lng_symbols = LNG_SYMBOLS.dup
25
+
26
+ mattr_accessor :lat_symbols
27
+ @@lat_symbols = LAT_SYMBOLS.dup
28
+
29
+ mattr_accessor :earth_radius
30
+ @@earth_radius = EARTH_RADIUS.dup
31
+
32
+ mattr_accessor :paginator
33
+ @@paginator = :array
34
+
35
+ mattr_accessor :default_per_page
36
+ @@default_per_page = 25
37
+
38
+ # mattr_accessor :spherical_distance_formula
39
+ # @@spherical_distance_formula = :n_vector
40
+ mattr_accessor :geo_factory
41
+ @@geo_factory = GEO_FACTORY.dup
42
+
43
+ included do
44
+ attr_accessor :geo
45
+ cattr_accessor :spatial_fields, :spatial_fields_indexed
46
+ @@spatial_fields = []
47
+ @@spatial_fields_indexed = []
48
+ end
49
+
50
+ module ClassMethods #:nodoc:
51
+ # create spatial index for given field
52
+ # @param [String,Symbol] name
53
+ # @param [Hash] options options for spatial_index
54
+
55
+ def spatial_index name, options = {}
56
+ self.spatial_fields_indexed << name
57
+ if Mongoid::VERSION =~ /3.0/
58
+ index({name => '2d'}, options)
59
+ else
60
+ index [[name, '2d']], options
61
+ end
62
+ end
63
+ end
64
+
65
+ # def distance(p2, opts = {})
66
+ # p1 = self.send(key)
67
+ # Mongoid::Location.distance(p1, p2, opts)
68
+ # end
69
+
70
+ # def self.distance(p1,p2,opts = {})
71
+ # opts[:formula] ||= (opts[:spherical]) ? @@spherical_distance_formula : :pythagorean_theorem
72
+ # p1 = p1.to_lng_lat if p1.respond_to?(:to_lng_lat)
73
+ # p2 = p2.to_lng_lat if p2.respond_to?(:to_lng_lat)
74
+
75
+ # rads = Formulas.send(opts[:formula], p1, p2)
76
+
77
+ # if unit = earth_radius[opts[:unit]]
78
+ # opts[:unit] = (rads.instance_variable_get("@radian")) ? unit : unit * RAD_PER_DEG
79
+ # end
80
+
81
+ # rads *= opts[:unit].to_f if opts[:unit]
82
+ # rads
83
+ # end
84
+
85
+ end
86
+ end
@@ -0,0 +1,29 @@
1
+ class Array
2
+ def to_xy
3
+ self[0..1].map(&:to_f)
4
+ end
5
+ alias :to_lng_lat :to_xy
6
+ end
7
+
8
+ class Hash
9
+ def to_xy
10
+ raise "Hash must have at least 2 items" if self.size < 2
11
+ [to_x, to_y]
12
+ end
13
+ alias :to_lng_lat :to_xy
14
+
15
+ def to_y
16
+ v = (Mongoid::Location.lat_symbols & self.keys).first
17
+ return self[v].to_f if !v.nil? && self[v]
18
+ raise "Hash must contain #{Mongoid::Location.lat_symbols.inspect} if ruby version is less than 1.9" if RUBY_VERSION.to_f < 1.9
19
+ raise "Hash cannot contain #{Mongoid::Location.lng_symbols.inspect} as the second item if there is no #{Mongoid::Location.lat_symbols.inspect}" if Mongoid::Location.lng_symbols.index(self.keys[1])
20
+ self.values[1].to_f
21
+ end
22
+
23
+ def to_x
24
+ v = (Mongoid::Location.lng_symbols & self.keys).first
25
+ return self[v].to_f if !v.nil? && self[v]
26
+ raise "Hash cannot contain #{Mongoid::Location.lat_symbols.inspect} as the first item if there is no #{Mongoid::Location.lng_symbols.inspect}" if Mongoid::Location.lat_symbols.index(self.keys[0])
27
+ self.values[0].to_f
28
+ end
29
+ end
@@ -0,0 +1,140 @@
1
+ module Mongoid
2
+ module Location
3
+ class GeoNearResults < Array
4
+ attr_reader :stats, :document, :_original_array, :_original_opts
5
+ attr_accessor :opts
6
+
7
+ def initialize(document,results,opts = {})
8
+ raise "#{document.name} class must include Mongoid::Location::Document" unless document.respond_to?(:spatial_fields_indexed)
9
+ @document = document
10
+ @opts = opts
11
+ @_original_opts = opts.clone
12
+ @stats = results['stats'] || {}
13
+ @opts[:skip] ||= 0
14
+
15
+ @_original_array = results['results'].collect do |result|
16
+ res = Mongoid::Factory.from_db(@document, result.delete('obj'))
17
+ res.geo = {}
18
+ # camel case is awkward in ruby when using variables...
19
+ if result['dis']
20
+ res.geo[:distance] = result.delete('dis').to_f
21
+ end
22
+ result.each do |key,value|
23
+ res.geo[key.snakecase.to_sym] = value
24
+ end
25
+ # dist_options[:formula] = opts[:formula] if opts[:formula]
26
+ @opts[:calculate] = @document.spatial_fields_indexed if @document.spatial_fields_indexed.kind_of?(Array) && @opts[:calculate] == true
27
+ if @opts[:calculate]
28
+ @opts[:calculate] = [@opts[:calculate]] unless @opts[:calculate].kind_of? Array
29
+ @opts[:calculate] = @opts[:calculate].map(&:to_sym) & geo_fields
30
+ if @document.spatial_fields_indexed.kind_of?(Array) && @document.spatial_fields_indexed.size == 1
31
+ primary = @document.spatial_fields_indexed.first
32
+ end
33
+ @opts[:calculate].each do |key|
34
+ res.geo[(key.to_s+'_distance').to_sym] = res.distance_from(key,center,{:unit =>@opts[:unit] || @opts[:distance_multiplier], :spherical => @opts[:spherical]} )
35
+ res.geo[:distance] = res.geo[key] if primary && key == primary
36
+ end
37
+ end
38
+ res
39
+ end
40
+ if @opts[:page]
41
+ start = (@opts[:page]-1)*@opts[:per_page] # assuming current_page is 1 based.
42
+ @_paginated_array = @_original_array.clone
43
+ super(@_paginated_array[@opts[:skip]+start, @opts[:per_page]] || [])
44
+ else
45
+ super(@_original_array[@opts[:skip]..-1] || [])
46
+ end
47
+ end
48
+
49
+ def page(*args)
50
+ new_collection = self.clone
51
+ new_collection.page!(*args)
52
+ new_collection
53
+ end
54
+
55
+ def page!(page, options = {})
56
+ original = options.delete(:original)
57
+ self.opts.merge!(options)
58
+ self.opts[:paginator] ||= Mongoid::Location.paginator
59
+ self.opts[:page] = page
60
+ start = (self.current_page-1)*self.limit_value # assuming current_page is 1 based.
61
+
62
+ if original
63
+ @_paginated_array = @_original_array.clone
64
+ self.replace(@_paginated_array[self.opts[:skip]+start, self.limit_value] || [])
65
+ else
66
+ @_paginated_array ||= self.to_a
67
+ self.replace(@_paginated_array[self.opts[:skip]+start, self.limit_value])
68
+ end
69
+ true
70
+ end
71
+
72
+ def per(num)
73
+ self.page(current_page, :per_page => num)
74
+ end
75
+
76
+ def reset!
77
+ self.replace(@_original_array)
78
+ @opts = @_original_opts
79
+ @_paginated_array = nil
80
+ true
81
+ end
82
+
83
+ def reset
84
+ clone = self.clone
85
+ clone.reset!
86
+ clone
87
+ end
88
+
89
+ def total_entries
90
+ (@_paginated_array) ? @_paginated_array.count : @_original_array.count
91
+ end
92
+ alias_method :total_count, :total_entries
93
+
94
+ def current_page
95
+ page = (@opts[:page]) ? @opts[:page].to_i.abs : 1
96
+ (page < 1) ? 1 : page
97
+ end
98
+
99
+ def limit_value
100
+ if @opts[:per_page]
101
+ @opts[:per_page] = @opts[:per_page].to_i.abs
102
+ else
103
+ @opts[:per_page] = case self.opts[:paginator]
104
+ when :will_paginate
105
+ @document.per_page
106
+ when :kaminari
107
+ Kaminari.config.default_per_page
108
+ else
109
+ Mongoid::Location.default_per_page
110
+ end
111
+ end
112
+ end
113
+ alias_method :per_page, :limit_value
114
+
115
+ def num_pages
116
+ (total_entries && @opts[:per_page]) ? (total_entries.to_f / @opts[:per_page]).ceil : nil
117
+ end
118
+ alias_method :total_pages, :num_pages
119
+
120
+ def out_of_bounds?
121
+ self.current_page > self.total_pages
122
+ end
123
+
124
+ def offset
125
+ (self.current_page - 1) * self.per_page
126
+ end
127
+
128
+ # current_page - 1 or nil if there is no previous page
129
+ def previous_page
130
+ self.current_page > 1 ? (self.current_page - 1) : nil
131
+ end
132
+
133
+ # current_page + 1 or nil if there is no next page
134
+ def next_page
135
+ self.current_page < self.total_pages ? (self.current_page + 1) : nil
136
+ end
137
+
138
+ end
139
+ end
140
+ end
@@ -1,5 +1,5 @@
1
1
  module Mongoid
2
2
  module Location
3
- VERSION = "0.3.1"
3
+ VERSION = "0.3.2"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid_location
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -153,6 +153,26 @@ files:
153
153
  - README.md
154
154
  - Rakefile
155
155
  - lib/mongoid_location.rb
156
+ - lib/mongoid_location/contexts/mongo.rb
157
+ - lib/mongoid_location/contextual/mongo.rb
158
+ - lib/mongoid_location/criteria.rb
159
+ - lib/mongoid_location/criterion/complex.rb
160
+ - lib/mongoid_location/criterion/inclusion.rb
161
+ - lib/mongoid_location/criterion/near_spatial.rb
162
+ - lib/mongoid_location/criterion/within_spatial.rb
163
+ - lib/mongoid_location/extensions/rgeo_spherical_point_impl.rb
164
+ - lib/mongoid_location/extensions/symbol.rb
165
+ - lib/mongoid_location/field_option.rb
166
+ - lib/mongoid_location/fields/line_string.rb
167
+ - lib/mongoid_location/fields/mongoid2/line_string.rb
168
+ - lib/mongoid_location/fields/mongoid2/point.rb
169
+ - lib/mongoid_location/fields/mongoid2/polygon.rb
170
+ - lib/mongoid_location/fields/point.rb
171
+ - lib/mongoid_location/fields/polygon.rb
172
+ - lib/mongoid_location/finders.rb
173
+ - lib/mongoid_location/location.rb
174
+ - lib/mongoid_location/location/core_ext.rb
175
+ - lib/mongoid_location/location/geo_near_results.rb
156
176
  - lib/mongoid_location/version.rb
157
177
  - mongoid_location.gemspec
158
178
  homepage: http://www.openxid.com