# encoding: utf-8 module Mongoid #:nodoc: module Contextual #:nodoc: class Mongo #:nodoc: # Fetches rows from the data base sorted by distance. # In MongoDB versions 1.7 and above it returns a distance. # Uses all criteria chains except without, only, asc, desc, order_by # # @example Minimal Query # # Address.geo_near([70,40]) # # @example Chained Query # # Address.where(:state => 'ny').geo_near([70,40]) # # @example Calc Distances Query # # Address.geo_near([70,40], :max_distance => 5, :unit => 5) # # @param [ Array, Hash, #to_xy ] center The center of where to calculate distance from # @param [ Hash ] opts the options to query with # @options opts [Integer] :num The number of rows to fetch # @options opts [Hash] :query The query to filter the rows by, accepts # @options opts [Numeric] :distance_multiplier this is multiplied against the calculated distance # @options opts [Numeric] :max_distance The max distance of a row that should be returned in :unit(s) # @options opts [Numeric, :km, :k, :mi, :ft] :unit automatically sets :distance_multiplier and converts :max_distance # @options opts [true,false] :spherical Will determine the distance either by spherical calculation or flat calculation # @options opts [TrueClass,Array] :calculate Which extra fields to calculate distance for in ruby, if set to TrueClass it will calculate all spatial fields # # @return [ Array ] Sorted Rows def geo_near(center, opts = {}) opts = self.criteria.options.merge(opts) # convert point center = center.to_xy if center.respond_to?(:to_xy) center = [center.x, center.y] if center.respond_to?(:x) # set default opts opts[:skip] ||= 0 if unit = Mongoid::Location.earth_radius[opts[:unit]] opts[:unit] = (opts[:spherical]) ? unit : unit * Mongoid::Location::RAD_PER_DEG end if unit = Mongoid::Location.earth_radius[opts[:distance_multiplier]] opts[:distance_multiplier] = (opts[:spherical]) ? unit : unit * Mongoid::Location::RAD_PER_DEG end opts[:distance_multiplier] = opts[:unit] if opts[:unit].kind_of?(Numeric) # setup paging. if opts.has_key?(:page) opts[:page] ||= 1 opts[:paginator] ||= Mongoid::Location.paginator() if opts[:per_page].blank? opts[:per_page] = case opts[:paginator] when :will_paginate @document.per_page when :kaminari Kaminari.config.default_per_page else Mongoid::Location.default_per_page end opts[:per_page] = opts[:per_page].to_i end end opts[:query] = create_geo_near_query(center,opts) results = klass.mongo_session.command(opts[:query]) Mongoid::Location::GeoNearResults.new(klass,results,opts) end private def create_geo_near_query(center,opts) # minimum query query = {} query[:geoNear] = klass.collection_name query[:near] = center # create limit and use skip if opts[:num] query['num'] = opts[:skip].to_i + opts[:num].to_i elsif opts[:limit] query['num'] = opts[:skip].to_i + opts[:limit].to_i elsif opts[:page] query['num'] = opts[:skip].to_i + (opts[:page].to_i * opts[:per_page].to_i) end # allow the use of complex werieis if opts[:query] query['query'] = self.criteria.where(opts[:query]).selector elsif self.criteria.selector != {} query['query'] = self.criteria.selector end if opts[:max_distance] query['maxDistance'] = opts[:max_distance].to_f query['maxDistance'] = query['maxDistance']/opts[:unit].to_f if opts[:unit] end if opts[:unique_docs] query['uniqueDocs'] = true end query['spherical'] = true if opts[:spherical] # 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 # for mongodb < 1.7 we need to run Haversine first before calculating degrees to Km or Miles. See below. query['distanceMultiplier'] = opts[:distance_multiplier].to_f if opts[:distance_multiplier] query end end end end