Skip to content

kristianmandrup/mongoid-geo

Repository files navigation

Mongoid geo

Status update: August, 2011

This project is currently dead, as I can find no time to keep it alive for the time being. The project mostly works, except for
`monogid/contexts/geo_mongo.rb`, which needs some loove ;)

The project mongoid_spacial by @ryanong has taken many of the ideas and core design of mongoid_geo, improved, reworked and optimized it and added many new features such as pagination intgration etc.

Please use the mongoid_spacial gem instead!!

Note: I have created several other “geo” projects in various states, some currently with a “refactor” branch.
Would be nice to finish those and have it all integrated some time :)

Cheers!

Running specs

The goal of this branch is to define a good specs structure and build up a good design of this ‘geo’ add-on this way.

$ bundle
$ bundle exec rspec spec/mongoid/geo/config_spec.rb

Status update: June, 2011

- A Mongoid::Geo::Config object has been added which you use to configure Mongoid::Geo, see mongoid/geo/config_spec.rb
- GeoPoints and GeoVectors can be integrated using the new #enable_extensions! method on Mongoid::Geo::Config, specs in mongoid/geo/extensions demonstrate how to use this
- Creation of Mongo query hashes for Twin- and NestedOperators have been extracted into Query classes (see mongoid/geo/queries folder)
- #geo_near now uses configuration from Mongoid::Geo::Config, such as the #distance_calculator (if Mongo server doesn’t support this) and the #radians_multiplier etc.
- extensions can be enabled using Mongoid::Geo.enabele_extension ; currently supported extensions are :geo_calc, geo_point and geo_vectors (see specs for usage examples)

Example Usage 1.0 (goal)

class Address
  field :location, :type => Array, :geo => true
end

my_home = Address.new :location => {:lat => 10, :lng => 12}.to_lng_lat
my_job = Address.new :location => [10.2, "12.1"].to_lng_lat
my_club = Address.new :location => [10.2, 12.1]
my_club.location_lat == 12.1 # true
my_club.location_lng == 10.2 # true

nearest_locations = Address.geo_near my_club
nearest_locations.first

# find near point, max distance
Address.where :location.near_max => [{:latitude => 10, :longitude => 12 }, 5] # 5 km?
Address.where :location.near_max => [my_club, 5]

Address.where(:location.within_box => [my_job, my_club])
Address.where(:location.within_center(:sphere) => [my_home, 5])

# geo_point integration (using GeoPoint class)

Mongoid::Geo::Config.enable_extension! :geo_point

p1 = "58 38 38N, 003 04 12W".geo_point # DMS format support!
my_home = Address.new :location => p1.to_lng_lat

# a GeoPoint includes some nice geo calculation methods, that are calculated relative to itself !
p1.bearing_to my_club.geo_point # => 60 deg ?
p1.distance_to my_home.geo_point # => 2341 kms ?

# geo_vectors integration

Mongoid::Geo::Config.enable_extension! :geo_vectors

my_home = Address.new :location => p1.to_lng_lat
my_home.add [2, 4].vector # add lng/lat vector
my_home.add [20, 4.km].vector # add vector of 4km at 20 degrees

Example Usage 1.1 (goal)

Would be nice with a simple API that acts as a location repository and can be implemented for any key/value store.
Would also be nice to use unit distances as Numeric macros (currently available in geo_magic gem).

Address.where(:location.within_box => [:my_job, :my_club]) # each point should be looked up in the repo using the symbol as key  
Address.where :location.near_max => [:my_home, 5000.meters] # use geo_magic distance here!

Intro

This is a Geo extension for Mongoid 2.

MongoDB Geospatial Indexing

  • Supports Mongo DB 1.7+ sphere distance calculations
  • Extra geo calculation methods such as #near_sphere etc.
  • Adds 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

You need help?

Post questions in the mongoid-geo group. Here I (and other uses of mongoid-geo) will
try to help out and answer any questions.

Suggestions, improvements and bugs?

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.

Current integrations

Mongoid geo has now been integrated with Google-Maps-for-Rails, thanks to oli-g, see commit

Mongoid 2 geo features

The following summarized what geo functionality is already provided by Mongoid 2.0 (as far as I could tell, May 9th, 2011)

Find addresses near a point

  Address.near(:latlng => [37.761523, -122.423575, 1])

Find locations within a circle

  base.where(:location.within => { "$center" => [ [ 50, -40 ], 1 ] })

Create geo-spatial index

  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:

Mongoid Geo features

The following briefly demonstrates all the features that mongoid-geo provides:

Geo index

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!

Geo option for Array GPS location field

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  

geo_near

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

Mongoid Geo extra inclusions

Find addresses near a point using spherical distance calculation

  Address.near_sphere(:location => [ 72, -44 ])

Mongoid Geo extra inflections

near_sphere

  base.where(:location.near_sphere => [ 72, -44 ])
  # => :location => { "$nearSphere" : [ 72, -44 ] }

near_max

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])

within_box

  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)

within_center

  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)

Note on the specs

The specs still use the old “Javascript” like method convention, such as #nearSphere
Don’t let that fool you ;)

Contribute

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 :)