The following blog post contains material either currently found or soon to be incorporated into my new book, "Easy Active Record for Rails Developers". Among many other topics, you'll learn about model generation, migrations, validations, associations, scopes, joins, includes, forms integration, nested forms, and model testing with RSpec and FactoryGirl. The book is now available, head over to this website's home page to learn more.


Sites like ArcadeNomad would be a lot less interesting without the features that can help users find classic arcade games close to their current location. These location-based features are possible determining which arcades are situated within a certain radius of the user’s current latitudinal and longitudinal coordinates. As you might imagine, implementing such a feature is fairly complicated, involving several steps:

  • Whenever a new arcade is inserted into the database, geocode the address and store the coordinates alongside other arcade-related attributes.
  • When a user visits the site, determine the user’s latitudinal and longitudinal coordinates based on the IP address.
  • Use the Haversine formula to identify the arcades residing within a certain radius of the user.

All three of these steps are pretty involved, and not something I’d want to implement alone. Fortunately we can use a great gem and one of several third-party services to handle much of the heavy lifting for us. The gem is called Geocoder, and it was created by Alex Reisner. In this section I’ll show you how to integrate Geocoder into your Rails project and use it to plug into the Google Geocoding and FreeGeoIP to geocode street addresses and IP addresses, respectively.

Installing the Geocoder Gem

To install the Geocoder gem add the following line to your project’s Gemfile:

gem 'geocoder'

Save the changes and run bundle install to install the gem.

Geocoding Addresses

With the Geocoder gem installed we can immediately begin using it to convert street addresses into latitudinal and longitudinal coordinates. Geocoder uses the Google Geocoding API by default however it’s possible to instead use a number of other competing services, among them Yahoo BOSS and MapQuest. See the Geocoder README for more details.

To associate latitudinal and longitudinal coordinates with a model schema you’ll need to generate a new migration, adding two float columns to the desired table. In the following example I’m adding latitude and longitude fields to the locations table:

$ rails g migration AddLatitudeAndLongitudeToLocations \
> latitude:float longitude:float

After creating the migration you can add the fields to the table:

$ rake db:migrate

Keep in mind Geocoder expects the fields to be named latitude and longitude. If for some reason you need to use different naming conventions you can override them when calling the geocoded_by method within your model (more on this method in a moment). For instance the following example overrides latitude with lat and longitude with lng:

geocoded_by :address, :latitude => :lat, :longitude => :lng

Next you need to inform the model as to which fields should be supplied to the geocoder in order to determine the coordinates. For instance you might only wish to geocode according to city and state, but others might wish to use a complete street address (street, city, state and zip code) in order to obtain more accurate results. For ArcadeNomad I use the following approach:

This approach will gather the street, city, and associated state’s name fields, returning them in the format “1234 Jump Street, Dublin, Ohio”. Again you’re not required to manage all of these address-related components, and can just return the street and zip code for instance. Once these model changes are saved, the supplied address will automatically be geocoded every time a Location record is saved or updated. Of course, you can avoid unnecessary API requests by updating your model to only geocode if the address-related components are present or have changed.

Let’s work through a quick example using the Rails console:

>> l = Location.new(name: 'Barcade',
?> street: '388 Union Avenue',
?> city: 'Brooklyn',
?> state: State.find_by_abbreviation('NY'),
?> zip: '11211')
>> l.save
>> l
#<Location:0x007fa815acb8a8> {
           :id => 13,
         :name => "Barcade",
  :description => "blah",
   :created_at => Thu, 06 Nov 2014 15:48:15 UTC +00:00,
   :updated_at => Thu, 06 Nov 2014 15:48:15 UTC +00:00,
       :street => "388 Union Avenue",
         :city => "Brooklyn",
          :zip => "11211",
     :latitude => 40.7120412,
    :longitude => -73.95101339999999,
     :state_id => 33,
  :category_id => nil,
    :telephone => nil,
         :slug => "barcade",
    :published => true,
          :url => nil,
     :has_menu => false
}

As you can see, once the record was saved the Geocoder was contacted, and the coordinates 40.7120412 and -73.9510133 were returned and inserted into the database.

Geocoding User Locations

After having geocoded a few arcades, we can begin geocoding site visitor’s locations and determining whether the user is in close proximity to any of the location found in the database. Once Geocoder is installed a location method is made available to your Rails application’s request object, meaning determining the location of any HTTP request is as simple as this:

location = request.location

The return location object contains a wealth of information about the user, including the user’s country, region (state in the U.S.), city, zip code, and coordinates, among other attributes:

#<Geocoder::Result::Freegeoip:0x000000052041a8 @data = {
  "ip"=>"184.57.29.59",
  "country_code"=>"US",
  "country_name"=>"United States",
  "region_code"=>"OH",
  "region_name"=>"Ohio",
  "city"=>"Columbus",
  "zipcode"=>"43215",
  "latitude"=>39.9653,
  "longitude"=>-83.0235,
  "metro_code"=>"535",
"area_code"=>"614"},
@cache_hit=nil>

Therefore to retrieve for instance the user’s longitude you can access the returned object’s data hash:

location = request.location
longitude = location.data['longitude']

Keep in mind the FreeGeoIP service limits you to 10,000 queries per hour. If you expect significant traffic, consider downloading and hosting a copy of the FreeGeoIP database and accessing the database directly. More information is available on the FreeGeoIP website.

Calculating Visitor Proximity to Locations

OK so now we have the coordinates for several arcades and know how to retrieve a visitor’s coordinates, we can determine whether any locations are within a desired proximity of users. This is actually incredibly easy to do, because the Geocoder gem implements the Haversine formula for you. All you need to do is call the near method on a Geocoder-enabled model, passing in an array containing the user’s coordinates:

user_location = request.location

locations = Location.near(
  [user_location.data['latitude'], user_location.data['longitude']
  )

The near method will by default return objects located within a 20 mile radius of the supplied coordinates. You can change the radius by passing in a value as the second parameter:

Location.near([latitude, longitude], 10)

Other options are available, such as using kilometer instead of miles. Consult the Geocoder documentation for more details.

Comments