A Geo extension for Mongoid.
- Supports Mongo DB 1.7+ sphere distance calculations
- Extra geo calculation methods such as #near_sphere etc.
- Add concept of a special “geo” field (for setting the location point)
- Configure and create geo indexes
- Calculate locations near a given point and return as criteria, sorted by distance
Added support for using GeoPoint from geo_calc to pass around location points. Also makes it easy to use GeoVectors from geo_vectors
Please see new specs use_geo_calc and use_geo_vectors in the /spec folder to see how this is used. Please help make sure the functionality works in the right way!
Thanks :)
- Fixed major bug which stored coordinate in [:lat, :lng]. This is incorrect. Coordinates must be stored with :lng as the first value in order for geospatial queries to work properly.
- Excerpt from MongoDB GeoSpatial Indexing
‘The code assumes that you are using decimal degrees in (longitude, latitude) order. This is the same order used for the GeoJSON spec.
Using (latitude, longitude) will result in very incorrect results, but is often the ordering used elsewhere, so it is good to double-check.
The names you assign to a location object (if using an object and not an array) are completely ignored, only the ordering is detected.’
- Created alias methods such as #near_sphere for #nearSphere etc. to make the DSL much more ‘rubyish’!
- Added #create_index! method (see ‘Geo index’ section below).
Post questions in the mongoid-geo group. Here I (and other uses of mongoid-geo) will
try to help out and answer any questions.
Please raise issues or suggestions for improvements in the “Issues” section on github.
I would recommend that you try to branch the project, try to fix it yourself and make a pull request.
Mongoid geo has now been integrated with Google-Maps-for-Rails, thanks to oli-g, see commit
The following summarized what geo functionality is already provided by Mongoid 2.0 (as far as I could tell, May 9th, 2011)
Address.near(:latlng => [37.761523, -122.423575, 1])
base.where(:location.within => { "$center" => [ [ 50, -40 ], 1 ] })
class Person field :location, :type => Array index [[ :location, Mongo::GEO2D ]], :min => -180, :max => 180 end # to ensure indexes are created, either: Mongoid.autocreate_indexes = true # or in the mongoid.yml autocreate_indexes: true
These are the only geo features I could find that are currently built-in for Mongoid 2.
Mongoid Geo implements some nice extra geo features:
The following briefly demonstrates all the features that mongoid-geo provides:
A new geo_index class method
Usage example:
geo_index :location
Note: For embedded documents, you must define the index in the root collection class. (davemitchell)
Calling geo_index also adds a #create_index! method to the class to enable construction of the index (on the instances/data in the DB).
class Address ... geo_index :location end Address.create_index!
I also added a nice little Array macro so you can do this:
[Address, Location].create_geo_indexes!
Objective: When setting a geo GPS location array, the setter should try to convert the value to an array of floats
The “old” manual way:
class Person field :locations, :type => Array def locations= args @locations = args.kind_of?(String) ? args.split(",").map(&:to_f) : args end end
mongoid-geo provides a new :geo
option that can be used with any Array field:
Usage example:
class Person field :location, :type => Array, :geo => true geo_index :location end p = Person.new # A Geo array can now be set via String or Strings, Hash or Object, here a few examples... # Please see geo_fields_spec.rb for more options! p.location = "45.1, -3.4" p.location = "45.1", "-3.4" p.location = {:lat => 45.1, :lng => -3.4} p.location = [{:lat => 45.1, :lng => -3.4}] p.location = {:latitude => 45.1, :longitude => -3.4} my_location = Location.new :latitude => 45.1, :longitude => -3.4 p.location = my_location # for each of the above, the following holds assert([45.1, -3.4], p.location) # also by default adds #lat and #lng convenience methods (thanks to TeuF) assert(45.1 , p.lat) assert(-3.4 , p.lng)
Customizing lat/lng attribute names:
# the #lat and #lng convenience methods can also be customized with the :lat and :lng options field :location, :type => Array, :geo => true, :lat => :latitude, :lng => :longitude assert(45.1 , p.latitude) assert(-3.4 , p.longitude) # or set the array attributes using symmetric setter convenience methods! p.latitude = 44 assert(44 , p.latitude)
Reversing lat/lng for spherical calculations
# You can also reverse the lat/lng positioning of the array storage - this is fx useful for spherical calculations Mongoid::Geo.spherical_mode do # Mongoid::Geo.lat_index.should == 1 # Mongoid::Geo.lng_index.should == 0 address.location = "23.5, -47" address.location.should == [23.5, -47].reverse end # or alternatively Mongoid::Geo.spherical = true address.location = "23.5, -47" address.location.should == [23.5, -47].reverse
class Address include Mongoid::Document extend Mongoid::Geo::Near field :location, :type => Array, :geo => true ... end # Find all addresses sorted nearest to a specific address loation nearest_addresses = Address.geo_near(another_address, :location) class Position include Mongoid::Document field :pos, :type => Array, :geo => true ... end
Find all positions sorted nearest to the address loation
nearest_positions = Position.geo_near(another_address.location, :pos)
Perform distance locations in Speherical mode inside Mongo DB (default is :plane)
nearest_positions = Position.geo_near(another_address.location, :pos, :mode => :sphere)
Other options supported are: :num, :maxDistance, :distanceMultiplier, :query
GeoNear returns each distance calculated in degrees. Use the :distanceMultiplier or :unit option to return in the unit of your choice (see unit.rb).
Set :distanceMultiplier = 6371
to get distance in KMs
Set @:distanceMultiplier = @3963.19
to get distance in Miles
You can also use the :unit option instead like this (supports :feet, :meters, :kms, :miles):
results = Address.geo_near @center.location, :location, :unit => :feet, :dist_order => :desc
The geo_near query result is returned as a Mongoid::Criteria
results.desc(:distance).map(&:distance)
Note that the :fromLocation
field, stores the location the distance was last calculated as a Hash of the GPS location point it was calculated from:
[23.5, -47].hash
This hash can be retrieved (and used for comparison?) using the fromHash
field
from = results.first.fromHash
You can also at any time get the GPS location point which the distance of any result instance was calculated from, using the @fromPoint field
from = results.first.fromPoint
You can now explicitly set/configure the Mongo DB version used. This will affect whether built-in Mongo DB distance calculation will be used or using standalone Ruby Haversine algorithm. By default the Mongo DB version is set to 1.8 (as of May 9, 2011) . See geo_near specs for more details/info on this.
Mongoid::Geo.mongo_db_version = 1.7
Find addresses near a point using spherical distance calculation
Address.near_sphere(:location => [ 72, -44 ])
base.where(:location.near_sphere => [ 72, -44 ]) # => :location => { "$nearSphere" : [ 72, -44 ] }
Find points near a given point within a maximum distance
base.where(:location.near_max => [[ 72, -44 ], 5]) # => { $near: [50, 40] , $maxDistance: 3 } base.where(:location.near_max(:sphere) => [[ 72, -44 ], 5]) # => { $nearSphere: [50, 40] , $maxDistanceSphere: 3 } base.where(:location.near_max(:sphere, :flat) => [[ 72, -44 ], 5]) # => { $nearSphere: [50, 40] , $maxDistance: 3 }
You can also use a Hash to define the near_max
places.where(:location.near_max => {:point => [ 72, -44 ], :distance => 5})
Or use an Object (which must have the methods #point
and #distance
that return the point and max distance from that point)
near_max_ = (Struct.new :point, :distance).new near_max.point = [50, 40] near_max.distance = [30,55] places.where(:location.near_max => near_max)
Note: For the points, you can also use a hash or an object with the methods/keys, either :lat, :lng
or :latitude, :longitude
Example:
center = (Struct.new :lat, :lng).new center.lat = 72 center.lng = -44 places.where(:location.within_center => [center, radius]) # OR places.where(:location.within_center => [{:lat => 72, :lng => -44}, radius])
box = [[50, 40], [30,55]] base.where(:location.within_box => box) # => locations: {"$within" : {"$box" : [[50, 40], [30,55]]} base.where(:location.within_box(:sphere) => box) # => locations: {"$within" : {"$boxSphere" : [[50, 40], [30,55]]}
You can also use a Hash to define the box
places.where(:location.within_box => {:lower_left => [50, 40], :upper_right => [30,55]}) # or mix and match places.where(:location.within_box => {:lower_left => {:lat => 50, :lng => 40}, :upper_right => [30,55] } )
Or use an object (which must have the methods #lower_left
and #upper_right
that return the points of the bounding box)
box = (Struct.new :lower_left, :upper_right).new box.lower_left = [50, 40] box.upper_right = [30, 55] places.where(:location.within_box => box)
center = [50, 40] radius = 4 places.where(:location.within_center => [center, radius]) # => places: {"$within" : {"$center" : [[50, 40], 4]} places.where(:location.within_center(:sphere) => [center, radius]) # => places: {"$within" : {"$centerSphere" : [[50, 40], 4]}
You can also use a hash to define the circle, with :center
and :radius
keys
places.where(:location.within_center => {:center => [50, 40], :radius => 4})
Or use an object (which must have the methods #center and #radius that return the center and radius of the circle))
circle = (Struct.new :center, :radius).new circle.center = [50, 40] circle.radius = 4 places.where(:location.within_center => circle)
The specs still use the old “Javascript” like method convention, such as #nearSphere
Don’t let that fool you ;)
Please feel free to contribute to the project!
I aim to deliver a complete geo package for use with Mongoid_. This gem should work nicely with geo_calc and geo_vectorsvectors that I’m also working on.
Your assistance on any of these projects will be greatly appreciated :)