‘Geolocation’ seems to be the best dish being served today. Every web-portal, every mobile app wants to be sensitive to a persons location. Everyone wants to see information that is ‘relative’ or location sensitive. Whether its a deal portal, travel portal, social network – giving users information that is relevant to their location bring not only a personalized touch but also keeps tuned in to the portal.
So, how do we do this? In a change of writing style, I am going to ‘cook’ up this recipe!
The Ingredients
Geokit was good with Rails 2.x but it doesn’t work well with Rails3. Geocoder rocks! There is this new kid on the block too Geo Mere Lal – neat stuff but with limited browser support. My personal preference is a slice of Geocoder – it smartly adds some Rack methods on the request object. To get geo-location (via IP or router) is as simple as:
request.location
HTML5 does provide geolocation services and is also a neat way to get the coordinates (directly from the machine or connected GPS devise) but this requires user intervention to post the co-ordinates and hence not always acceptable.
MongoDB is the next ingredient in our recipe. An excellent scalable document database for storing huge amounts of data. Mongoid is the gem flavour I use MongoDB with.
The Blend
Rails is the blending mixture which holds all this together! (obviously). A simple example of a model with geospatial indexing is here:
class Business include Mongoid::Document field :name field :domain field :address field :location, type => Array index [[ :location, Mongo::GEO2D ]] before_save :set_location def set_location self.location = Geocoder.search(self.address).first.coordinates if self.address_changed? || self.new_record? end end
To taste the mixture, you need to simply do this:
lat, lng = Geocoder.search('some location').first.coordinates result = Business.near(:location => [lat, lng])
Some tips and tricks to cook this well:
- Don’t forget the index by running ‘rake db:mongoid:create_indexes’
- Remember that mongoid returns Mongoid::Criteria by default. Mongoid delays till absolutely necessary to fetch the information. (just like scopes). What could be confusing is that on the rails console, Business.all returns an array if you are using ActiveRecord (i.e. the scope is resolved) but Mongoid will still return the Criteria untill you fetch the first object.
The Garnishing
Google Maps Javascript (V3) has finally got rid of Google API keys setup! Use at will. Here is the basic JS snippet I used to get my map markers up and running:
// Somewhere earlier: http://maps.google.com/maps/api/js?sensor=false var startZoom = 9; var map; var map_markers = {}; function init_gmap(centerLatitude, centerLongitude) { var options = {zoom: startZoom, center: new google.maps.LatLng(centerLatitude,centerLongitude), zoomControl: true, panControl: true, mapTypeId: google.maps.MapTypeId.ROADMAP }; map = new google.maps.Map(document.getElementById("gmap"), options); var infoWindow = new google.maps.InfoWindow({ maxWidth: 100 }); // Ensure 'markers' array is populated with JSon Array of your objects. for(id in markers) { var loc = new google.maps.LatLng(markers[id].latitude, markers[id].longitude); var marker = new google.maps.Marker({ position: loc, map: map, name: markers[id].name, subdomain: markers[id].subdomain }); google.maps.event.addListener(marker, 'mouseover', function() { infoWindow.setContent(this.name); infoWindow.open(map, this); }); google.maps.event.addListener(marker, 'click', function() { window.location = 'http://' + this.subdomain; }); } }
Voila! Keep frozen for 2 minutes (Read: refresh screen) and you have a winner!
Adding the Twist
I found after some research that geo-spatial indexing is a standard 2-dimensional indexing. In the case of geolocation, we need a spherical mode for searching and after v1.3.3 of MongoDB, this too is supported nicely. mongoid-geo is a nice gem for making this really simple:
class Business include Mongoid::Document extend Mongoid::Geo::Near field :address field :location, :type => Array, :geo => true geo_index :location before_save :update_location def update_location self.location = Geocoder.search(self.address).first.coordinates if self.location_changed? || self.new_record? end end
Please give me feedback on my recipe — it would be more than welcome! I am a lousy cook (literally) but this could be a start of something new!
Update1
Source code for the above app is available at https://github.com/joshsoftware/mongo_geo_demo
Took me sometimes to read some the comments, any way I easily enjoyed the post. It proved to be pretty useful to me and I am certain to all the commenters right here! It’s all the time good when you can not only be informed, but also entertained!
Very useful article. Do you know if there’s a way to route users to different subdomains depending on their IP address? i.e. if my IP is from the US, then I’m routed to us.mysite.com, but if my IP is UK based, then I’m routed to uk.mysite.com and so on… Any help is much appreciated.
Riki
Well, I have not tried it (shall do so soon) but I think this is what you need to do:
location = request.location # => This is a rack request
Using Geocoder::Result ‘country’ method, you then use rack rewrite (https://github.com/jtrupiano/rack-rewrite) and rewrite / redirect the URL to your sub-domain.
Of course, ensure that your cookie settings support wild-card domain otherwise you may have a session sharing problem.
I liked your article. The ‘cooking’ metaphor made it easier to read.
Ingrediants is spelled “Ingredients” (you might want to change that).
Also, you should show other alternatives besides just MongoDB. (Does the developer HAVE to use MongoDB?)
@webmasta – Glad you liked the article and thanks for the correction. I have updated the post.
We can use other databases besides MongoDB — but what takes the cake (no pun intended here) with MongoDB is that it inherently supports geo-spatial indexing. Otherwise, there is a *huge* complex cos / radians SQL query that does this in a Relational Database. See (geocoder 1.0.2)/lib/geocoder/stores/active_record.rb: full_near_scope_options method.
Considering mongodb does this internally, it cooks quite well!
Nice writeup there, but you might want to add a bit left and right…
You could also mention that when you are heading onto a map representation with some maps API, geocoder is kinda included in gmaps4rails(which now is called GlobalMaps4Rails; was portes to other map backends too…). You could have a Map on your site with all the goodness of this gem in like 20 minutes =)
Just FYI, keep it up,
Kjellski
Sorry, I’ve forgot to add the link right away: https://github.com/apneadiving/Google-Maps-for-Rails
@Kjellski Thanks for updating this info. Its very helpful! 🙂
Hi. I have a question. Could anyone help me?
http://stackoverflow.com/questions/20109592/rails-mongoid-geo-near-sort-by-distance
Need Some help in project.
In my case I have the coordinates. And I have to fetch the Address from it.
please suggest me solution