Search by location with Geocoder

Publié le 31 janvier 2013 par Alexandre Salaun | outils

Cet article est publié sous licence CC BY-NC-SA

For some kind of applications or websites, it must be useful to search elements by location. For example, when you want to develop an online classified website. Indeed, users want to see classifieds close to their home.

In this article we will use the gem Geocoder to do that. Then, we can use GmapsForRails to display results on a map.

Prerequisites

Geocoder needs Ruby 1.8.7 or newest (or JRuby) and supports main databases (MySQL, PostgreSQL, MongoDB…). To install this gem, you just need to add the next line in your Gemfile :

gem 'geocoder'

Then, launch bundle install to complete installation.

Next, you have to add fields (in this example we use MySQL database) to the model, which will be searched : latitude and longitude (float). These fields will serve to locate each item. To set latitude and longitude add a method (geocoded_by) to get the address to localite. A callback (after_validation) must be added to the model. A method name or an attribute may be given to this method.

class MyModel < ActiveRecord::Base
  # with an attributes
  geocoded_by :address # address is an attribute of MyModel

  # or with a method
  geocoded_by :full_address # full_address is a method which take some model's attributes to get a formatted address for example

  # the callback to set longitude and latitude
  after_validation :geocode

  # the full_address method
  def full_address
    "#{address}, #{zipcode}, #{city}, #{country}"
  end
end

You can choose the most suitable method : if the address is saved in database you can just give the field’s name or if there are several attributes, use the second way and give a method name.

An initializer could be created to define some parameters like the geocoding_service, the timeout or the default unit (kilometers or miles).

# config/initializers/geocoder.rb
Geocoder.configure(
  # geocoding service
  lookup: :google,

  # geocoding service request timeout (in seconds)
  timeout: 3,

  # default units
  units: :km
)

Other parameters are available as you can see in the Github repository.

If you add geocoder fields after the creation of the objects you can use a Rake Task in order to geocode them :

rake geocode:all CLASS=MyModel

Now you have all the elements to search objects by location and we will demonstrate you how to use it.

There are different methods to search items. The first one : seek elements near to a given address.

Search methods can only be applied on geocoded objects. A scope is available to get them : geocoded. Another one exist for non geocoded objects : not_geocoded.

MyModel.geocoded     # => return objects with coordinates
MyModel.not_geocoded # => return objects without coordinates

near method

To find all objects near to a given address you can use this method like the example below :

MyModel.near("Champs de Mars, Paris", 10)
# you must give an address and a kilometers number to get all objects within 10 kilometers of the given address

This method returns an array of Geocoder::Result objects. You can get longitude, latitude or distance and all object’s attributes.

An optional parameter can be given to the method to sort results.

MyModel.near("Champs de Mars, Paris", 10, order: :distance)
# in this case the results are sorted by distance

You can create a form on your website to allow user to search elements by location like this :

<%= form_tag classifieds_path, method: :get do %>
  <%= label :location, "Search classifieds near : " %>
  <%= text_field_tag :location, params[:location] %>

  <%= label :distance, "Distance : " %>
  <%= text_field_tag :distance, params[:distance] %>

  <%= submit_tag "Search" %>
<% end %>

A controller must be created too :

class ClassifiedsController < ApplicationController
  def index
    if params[:location].present?
      @classifieds = Classified.near(params[:location], params[:distance] || 10, order: :distance)
    else
      @classifieds = Classified.all
    end
  end
end

Now users can search classifieds by location and you can display results. Users can fill location’s text field with address (city or full address) or with an array which contains longitude and latitude.

nearbys method

The nearbys method must be applied on an object not on a Class. It allows you to find other objects near the given one specifying the distance like the example below :

@nearby_classifieds = @classified.nearbys(10) # 10 is the distance parameter (in kilometers in our case)

nearbys returns an array of Geocoder::Result so you can access to the distance attribute in order to display distance between current element and the displayed one.

This method can be very helpful when you want to display similar classifieds near to the selected one for instance.

It is just an additional feature to improve results with relevance.

With our example, you can add nearby classifieds in the show view of a classified.

<%= content_tag :h1, @classified.title %>

Description :
<%= @classified.description %>

Nearby classifieds :
<ul>
  <% @classified.nearbys(10).each do |nearby_classified| %>
    <li><%= link_to nearby_classified.title, nearby_classified %></li>
  <% end %>
</ul>

With these two methods, you can allow users to search elements by location and help them to get nearby items. Others methods could be interesting in this gem to add localization features in your app.

Geocoder useful functionalities

IP Location

Geocoder add accessors to request to get your current ip location and some other locations information :

  request.ip                  # return your ip address
  request.location            # return a Geocoder::Result which corresponds to your location
  request.location.try(:city) # return the city corresponding to your ip location

I suggest you to use try to avoid some no method error troubles with Geocoder calls.

Now, you can display elements near to the location of the connected user without asking him an address or a location.

Be careful, Geocoder doesn’t succeed to locate you via your IP address when you are running your app locally. If you try to display request.location.city or others attributes, you will see that they are equal to “” and latitude and longitude are equal to 0.

Distance between elements

It is also possible to calculate distance between two elements without a search method. Use the distance_from method on a given geocoded object :

distance = @classified.distance_from([other_classified.latitude, other_classified.longitude])
# return the distance in kilometers (or miles if the geocoder initializer's default units is miles) between this two elements.

To calculate distances between elements, Geocoder have some methods to generate database requests (for each supported database type). Distances are calculated via elements longitude and latitude as you can see in example on SQL methods.

Display results

Now, you are able to search objects by location and list them. We will see how to display results on a map. The GmapsForRails gem is a good solution to do that.

After installing this gem (add to your Gemfile and launch bundle install command), you can complete the installation with this command :

rails generate gmaps4rails:install

Then, add a yield to your footer to load gmaps javascripts files :

<%= yield :scripts %>

Add the link to your gmaps stylesheet :

<%= stylesheet_link_tag 'gmaps4rails' %>

Finally you need to add a gmap field to your model :

rails g migration add_gmap_field_to_classifieds gmaps:boolean
# GmapsForRails also needs longitude and latitude fields but they are already added because Geocoder needs them

This one is set to true if the object was saved with a correct address and longitude and latitude are set. This gem set longitude and latitude when you save the object. So to avoid conflicts between GmapsForRails and Geocoder, remove the geocoder’s callback in your model (after_validation :geocode). The fields will be set by GmapsForRails.

Next, you must have to add the following lines to your model :

class MyModel < ActiveRecord::Base
  acts_as_gmappable # to allow the gem on this model

  # the address to get the coordinates when the object is saved
  def gmaps4rails_address
    "#{address}, #{zipcode}, #{city}, #{country}"
  end
end

Now, every saved objects will have longitude and latitude, can be searched with Geocoder and be displayed on a map via GmapsForRails.

To display a map with search results, you just have to convert them with the appropriate function in your controller :

class ClassifiedsController < ApplicationController
  def index
    if params[:location].present?
      @classifieds = Classified.near(params[:location], params[:distance] || 10, order: :distance)
    else
      @classifieds = Classified.all
    end

    @classifieds = @classifieds.to_gmaps4rails
  end
end

And in your view :

gmaps4rails(@classifieds)

At the end, you can see a map with your search results. If you don’t want to use a gem to see results on Gmap, you can use the Gmap API.

As you can see above, with these two gems you can easily search objects by location and display them on a map or as a list. Further information are available on the gems pages. You can adapt this case to your application.

If you use SQL for your app, have a look to Spatial Extensions. It allows you generation, storage, and analysis of geographic features in your database.

The Synbioz Team.