Geocoding in Rails 5

The geocoder version used in this article is 1.4.1. Create a location model that can store the address and the corresponding latitude and longitude.

rails g model location address latitude:float longitude:float

Migrate the database.

rails db:migrate

The layout file looks like this:

<!DOCTYPE html>
<html>
  <head>
    <title>Gcode</title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <div id="container">
    <% flash.each do |name, msg| %>
      <%= content_tag :div, msg, :id => "flash_#{name}" %>
    <% end %>
    <%= yield %>
  </div>
</html>

Add the geocoder gem to Gemfile and run bundle.

gem 'geocoder'

Create a locations controller.

rails g controller locations

The locations controller is straightforward and looks like this:

class LocationsController < ApplicationController
  def index
    if params[:search].present?
      @locations = Location.near(params[:search], 50, :order => :distance)
    else
      @locations = Location.all
    end
  end

  def show
    @location = Location.find(params[:id])
  end

  def new
    @location = Location.new
  end

  def create
    @location = Location.new(allowed_params)
    if @location.save
      redirect_to @location, :notice => "Successfully created location."
    else
      render :new
    end
  end

  def edit
    @location = Location.find(params[:id])
  end

  def update
    @location = Location.find(params[:id])
    if @location.update_attributes(allowed_params)
      redirect_to @location, :notice  => "Successfully updated location."
    else
      render :edit
    end
  end

  def destroy
    @location = Location.find(params[:id])
    @location.destroy

    redirect_to locations_url, :notice => "Successfully destroyed location."
  end

  private

  def allowed_params
    params.require(:location).permit(:id, :address, :latitude, :longitude)
  end
end

The location model has the declaration to geocode the address. After validation it hits the network to fetch the values for latitude and longitude values of the location.

class Location < ApplicationRecord
  geocoded_by :address       # can also be an IP address
  after_validation :geocode  # auto-fetch coordinates
end

The location form is in locations/_form.html.erb is as follows:

<%= form_for @location do |f| %>
  <p>
    <%= f.label :address %><br />
    <%= f.text_field :address %>
  </p>
  <p>
    <%= f.label :latitude %><br />
    <%= f.text_field :latitude %>
  </p>
  <p>
    <%= f.label :longitude %><br />
    <%= f.text_field :longitude %>
  </p>
  <p><%= f.submit %></p>
<% end %>

The edit.html.erb is as shown below:

<%= render 'form' %>
<p>
  <%= link_to "Show", @location %> |
  <%= link_to "View All", locations_path %>
</p>

The index.html.erb is as follows:

<%= form_tag locations_path, :method => :get do %>
  <p>
    <%= text_field_tag :search, params[:search] %>
    <%= submit_tag "Search Near", :name => nil %>
  </p>
<% end %>

<table>
  <tr>
    <th>Address</th>
    <th>Latitude</th>
    <th>Longitude</th>
  </tr>
  <% for location in @locations %>
    <tr>
      <td><%= location.address %></td>
      <td><%= location.latitude %></td>
      <td><%= location.longitude %></td>
      <td><%= link_to "Show", location %></td>
      <td><%= link_to "Edit", edit_location_path(location) %></td>
      <td><%= link_to "Destroy", location, :confirm => 'Are you sure?', :method => :delete %></td>
    </tr>
  <% end %>
</table>

<p><%= link_to "New Location", new_location_path %></p>

The new.html.erb is as follows:

<%= render 'form' %>
<p><%= link_to "Back to List", locations_path %></p>

The show.html.erb is as follows:

<p>
  <strong>Address:</strong>
  <%= @location.address %>
</p>
<p>
  <strong>Latitude:</strong>
  <%= @location.latitude %>
</p>
<p>
  <strong>Longitude:</strong>
  <%= @location.longitude %>
</p>

<%= image_tag "http://maps.google.com/maps/api/staticmap?size=450x300&sensor=false&zoom=16&markers=#{@location.latitude}%2C#{@location.longitude}" %>

<h3>Nearby locations</h3>
<ul>
<% for location in @location.nearbys(10) %>
  <li><%= link_to location.address, location %> (<%= location.distance.round(2) %> miles)</li>
<% end %>
</ul>

<p>
  <%= link_to "Edit", edit_location_path(@location) %> |
  <%= link_to "Destroy", @location, :confirm => 'Are you sure?', :method => :delete %> |
  <%= link_to "View All", locations_path %>
</p>

Define the location resource in routes.rb:

resources :locations

You can now create locations by going to localhost:3000/locations.

Converting IP address to Location

Create a download model with field to store IP address.

rails g model download ip

Migrate the database.

rails db:migrate

Add the geocoding declaration to the download model.

class Download < ApplicationRecord
  geocoded_by :ip
  after_validation :geocode
end

Create a download model in rails console:

> Download.create(ip: '209.249.19.173')
   (0.1ms)  begin transaction
   (0.1ms)  rollback transaction
NoMethodError: undefined method `latitude=' for #<Download:0x007ffc5cdccd18>

This fails, the geocoder gem needs the latitude and longitude to populate the record when it geocodes the IP after validation. Drop the development database.

rails db:drop

Add the required fields for coordinates in the migration.

class CreateDownloads < ActiveRecord::Migration[5.0]
  def change
    create_table :downloads do |t|
      t.string :ip
      t.float :latitude
      t.float :longitude
      t.timestamps
    end
  end
end

Migrate the database.

rails db:migrate

Let's create a download record in the rails console.

> Download.create(ip: '209.249.19.173')
   (0.0ms)  begin transaction
Geocoding API not responding fast enough (use Geocoder.configure(:timeout => ...) to set limit).
  SQL (0.4ms)  INSERT INTO "downloads" ("ip", "created_at", "updated_at") VALUES (?, ?, ?)  [["ip", "209.249.19.173"], ["created_at", 2017-01-25 19:38:45 UTC], ["updated_at", 2017-01-25 19:38:45 UTC]]
   (2.5ms)  commit transaction
 => #<Download id: 1, ip: "209.249.19.173", latitude: nil, longitude: nil, created_at: "2017-01-25 19:38:45", updated_at: "2017-01-25 19:38:45">

This fails with 'not responding' error. Define the timeout in config/initializers/geocoder.rb:

Geocoder::Configuration.timeout = 15

You can now create a download record in the rails console.

Download.create(ip: '209.249.19.173')
   (0.1ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "downloads" ("ip", "latitude", "longitude", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["ip", "209.249.19.173"], ["latitude", 37.8381], ["longitude", -122.1026], ["created_at", 2017-01-25 19:40:03 UTC], ["updated_at", 2017-01-25 19:40:03 UTC]]
   (2.6ms)  commit transaction
 => #<Download id: 2, ip: "209.249.19.173", latitude: 37.8381, longitude: -122.1026, created_at: "2017-01-25 19:40:03", updated_at: "2017-01-25 19:40:03">

You can see that the coordinates is now populated. How do we convert the coordinates to an address? Geocoder gem provides a CLI. You can provide the coordinates to the geocode command.

$ geocode 37.8381,  -122.1026
Latitude:         37.8375726
Longitude:        -122.1023373
Full address:     Cross Path, Moraga, CA 94556, USA
City:             Moraga
State/province:   California
Postal code:      94556
Country:          United States
Google map:       http://maps.google.com/maps?q=37.8375726,-122.1023373

The address private method in this example in the examples folder shows how to convert coordinates to an address. Let's use that in the rails console.

$ rails c
Loading development environment (Rails 5.0.1)
Geocoder.address([37.8381,  -122.1026])
 => "Cross Path, Moraga, CA 94556, USA"
> Geocoder.address([37.7618242,-122.39858709999999])
 => "324 Arkansas St, San Francisco, CA 94107, USA"

You can also find the address directly without storing any coordinates:

Geocoder.search('209.249.19.173').first
 => #<Geocoder::Result::Freegeoip:0x007ff82b428d50 @data={"ip"=>"209.249.19.173", "country_code"=>"US", "country_name"=>"United States", "region_code"=>"CA", "region_name"=>"California", "city"=>"Moraga", "zip_code"=>"94556", "time_zone"=>"America/Los_Angeles", "latitude"=>37.8381, "longitude"=>-122.1026, "metro_code"=>807}, @cache_hit=nil>

I found this way by reading this test: https://github.com/alexreisner/geocoder/blob/master/test/unit/lookups/geoip2_test.rb. If you want to convert a zip code to latitude and longitude, you can use freemaptools.com

Possible Issues and Fixes

If you get the error:

Geocoding API not responding fast enough (use Geocoder.configure(:timeout => ...) to set limit).

Create config/initializers/geocoder.rb and define the timeout:

Geocoder::Configuration.timeout = 15

It will now work.


Related Articles


Create your own user feedback survey