Ruby
Article
By Ilya Bodrov-Krukowski

Geocoder: Display Maps and Find Places in Rails

By Ilya Bodrov-Krukowski

The world is big. Seriously, I’d say it’s really huge. Different countries, cities, various people, and cultures…but still, the internet connects us all and that’s really cool. I can communicate with my friends who live a thousand miles away from me.

Because the world is huge, there are many different places that you may need to keep track of within your app. Luckily, there is a great solution to help you find locations by their coordinates, addresses, or even measuring distances between places and finding places nearby. All of this location-based work is called “geocoding”. in Ruby, one geocoding solution is called Geocoder and that’s our guest today.

In this app you will learn how to

  • Integrate Geocoder into your Rails app
  • Tweak Geocoder’s settings
  • Enable geocoding to be able to fetch coordinates based on the address
  • Enable reverse geocoding to grab an address based on the coordinates
  • Measure the distance between locations
  • Add a static map to display the selected location
  • Add a dynamic map to allow users to select a desired location
  • Add the ability to find the location on the map based on coordinates

By the end of the article you will have a solid understanding of Geocoder and a chance to work with the handy Google Maps API. So, shall we start?

The source code is available at GitHub.

The working demo can be found at sitepoint-geocoder.herokuapp.com.

Preparing the App

For this demo, I’ll be using Rails 5 beta 3, but Geocoder supports both Rails 3 and 4. Create a new app called Vagabond (we’ll you don’t really have to call it that, but I find this name somewhat suitable):

$ rails new Vagabond -T

Suppose we want our users to share places that they have visited. We won’t focus on stuff like authentication, adding photos, videos etc., but you can extend this app yourself later. For now let’s add a table called places with the following fields:

  • title (string)
  • visited_by (string) – later this can be replaced with user_id and marked as a foreign key
  • address (text) – address of the place a user has visited
  • latitude and longtitude (float) – the exact coordinates of the place. The first draft of the app should fetch them automatically based on the provided address.

Create and apply the appropriate migration:

$ rails g model Place title:string address:text latitude:float longitude:float visited_by:string
$ rake db:migrate

Before moving forward, let’s add bootstrap-rubygem that integrates Bootstrap 4 into our app. I won’t list all the styling in this article, but you can refer to the source code to see the complete markup.

Gemfile

[...]
gem 'bootstrap', '~> 4.0.0.alpha3'
[...]

Run

$ bundle install

Now create a controller, a route, and some views:

places_controller.rb

class PlacesController < ApplicationController
  def index
    @places = Place.order('created_at DESC')
  end

  def new
    @place = Place.new
  end

  def create
    @place = Place.new(place_params)
    if @place.save
      flash[:success] = "Place added!"
      redirect_to root_path
    else
      render 'new'
    end
  end

  private

  def place_params
    params.require(:place).permit(:title, :address, :visited_by)
  end
end

config/routes.rb

[...]
resources :places, except: [:update, :edit, :destroy]
root 'places#index'
[...]

views/places/index.html.erb

<header><h1 class="display-4">Places</h1></header>

<%= link_to 'Add place', new_place_path, class: 'btn btn-primary btn-lg' %>

<div class="card">
  <div class="card-block">
    <ul>
      <%= render @places %>
    </ul>
  </div>
</div>

views/places/new.html.erb

<header><h1 class="display-4">Add Place</h1></header>

<%= render 'form' %>

Now the partials:

views/places/_place.html.erb

<li>
  <%= link_to place.title, place_path(place) %>
  visited by <strong><%= place.visited_by %></strong>
</li>

views/places/_form.html.erb

<%= form_for @place do |f| %>
  <fieldset class="form-group">
    <%= f.label :title %>
    <%= f.text_field :title, class: "form-control" %>
  </fieldset>

  <fieldset class="form-group">
    <%= f.label :visited_by %>
    <%= f.text_field :visited_by, class: "form-control" %>
  </fieldset>

  <fieldset class="form-group">
    <%= f.label :address, 'Address' %>
    <%= f.text_field :address, class: "form-control" %>
  </fieldset>

  <%= f.submit 'Add!', class: 'btn btn-primary' %>
<% end %>

We set up the index, new, and create actions for our controller. That’s great, but how are we going to grab coordinates based on the provided address? For that, we’ll utilize Geocoder, so proceed to the next section!

Integrating Geocoder

Add a new gem:

Gemfile

[...]
gem 'geocoder'
[...]

and run

$ bundle install

Starting to work with Geocoder is really simple. Go ahead and add the following line into your model:

models/place.rb

[...]
geocoded_by :address
[...]

So, what does it mean? This line equips our model with useful Geocoder methods, that, among others, can be used to retrieve coordinates based on the provided address. The usual place to do that is inside a callback:

models/place.rb

[...]
geocoded_by :address
after_validation :geocode
[...]

There are a couple of things you have to consider:

  • Your model must present a method that returns the full address – its name is passed as an argument to the geocoded method. In our case that’ll be an address column, but you can use any other method. For example, if you have a separate columns called country, city, and street, the following instance method may be introduced:

    def full_address
    [country, city, street].compact.join(‘, ‘)
    end

Then just pass its name:

geocoded_by :full_address
  • Your model must also contain two fields called latitude and longitude, with their type set to float. If your columns are called differently, just override the corresponding settings:

    geocoded_by :address, latitude: :lat, longitude: :lon

  • Geocoder supports MongoDB as well, but requires a bit different setup. Read more here and here (overriding coordinates’ names).

Having these two lines in place, coordinates will be populated automatically based on the provided address. This is possible thanks to Google Geocoding API (though Geocoder supports other options as well – we will talk about it later). What’s more, you don’t even need an API key in order for this to work.

Still, as you’ve probably guessed, the Google API has its usage limits, so we don’t want to query it if the address was unchanged or was not presented at all:

models/place.rb

[...]
after_validation :geocode, if: ->(obj){ obj.address.present? and obj.address_changed? }
[...]

Now, just add the show action for your PlacesController:

places_controller.rb

[...]
def show
  @place = Place.find(params[:id])
end
[...]

views/places/show.html.erb

<header><h1 class="display-4"><%= @place.title %></h1></header>

<p>Address: <%= @place.address %></p>
<p>Coordinates: <%= @place.latitude %> <%= @place.longitude %></p>

Boot up your server, provide an address (like “Russia, Moscow, Kremlin”) and navigate to the newly added place. The coordinates should be populated automatically. To check whether they are correct, simply paste them into the search field on this page.

Another interesting thing is that users can even provide IP addresses to detect coordinates – this does not require any changes to the code base at all. Let’s just add a small reminder:

views/places/_form.html.erb

[...]
<fieldset class="form-group">
  <%= f.label :address, 'Address' %>
  <%= f.text_field :address, class: "form-control" %>
  <small class="text-muted">You can also enter IP. Your IP is <%= request.ip %></small>
</fieldset>
[...]

If you are developing on your local machine, the IP address will be something like ::1 or localhost and obviously won’t be turned into coordinates, but you can provide any other known address (8.8.8.8 for Google).

Configuration and APIs

Geocoder supports a bunch of options. To generate a default initializer file, run this command:

$ rails generate geocoder:config

Inside this file you can set up various things: an API key to use, timeout limit, measurement units to use, and more. Also, you may change the “lookup” providers here. The default values are

:lookup => :google, # for street addresses
:ip_lookup => :freegeoip # for IP addresses

Geocoder’s docs do a great job of listing all possible providers and their usage limits,
so I won’t place them in this article.

One thing to mention is that even though you don’t require an API key to query the Google API, it’s advised to do so because you get an extended quota and also can track the usage of your app. Navigate to the console.developers.google.com, create a new project, and be sure to enable the Google Maps Geocoding API.

Next, just copy the API key and place it inside the initializer file:

config/initializers/geocoder.rb

Geocoder.configure(
  api_key: "YOUR_KEY"
)

Displaying a Static Map

One neat feature about Google Maps is the ability to add static maps (which are essentially images) into your site based on the address or coordinates. Currently, our “show” page does not look very helpful, so let’s add a small map there.

To do that, you will require an API key, so if you did not obtain it in the previous step, do so now. One thing to remember is that the Google Static Maps API has to be enabled.

Now simply tweak your view:

views/places/show.html.erb

[...]
<%= image_tag "http://maps.googleapis.com/maps/api/staticmap?center=#{@place.latitude},#{@place.longitude}&markers=#{@place.latitude},#{@place.longitude}&zoom=7&size=640x400&key=AIzaSyA4BHW3txEdqfxzdTlPwaHsYRSZbfeIcd8",
              class: 'img-fluid img-rounded', alt: "#{@place.title} on the map"%>

That’s pretty much it – no JavaScript is required. Static maps support various parameters, like addresses, labels, map styling, and more. Be sure to read the docs.

The page now looks much nicer, but what about the form? It would be much more convenient if users were able to enter not only address but coordinates, as well, by pinpointing the location on an interactive map. Proceed to the next step and let’s do it together!

--ADVERTISEMENT--

Adding Support for Coordinates

For now forget about the map – let’s simply allow users to enter coordinates instead of an address. The address itself has to be fetched based on the latitude and longitude. This requires a bit more complex configuration for Geocoder. This approach uses a technique known as “reverse geocoding”.

models/place.rb

[...]
reverse_geocoded_by :latitude, :longitude
[...]

This may sound complex, but the idea is simple – we take these two values and grab the address based on it. If your address column is named differently, provide its name like this:

reverse_geocoded_by :latitude, :longitude, :address => :full_address

Moreover, you can pass a block to this method. It is useful in scenarios when you have separate columns to store country’s and city’s name, street etc.:

reverse_geocoded_by :latitude, :longitude do |obj, results|
  if geo = results.first
    obj.city    = geo.city
    obj.zipcode = geo.postal_code
    obj.country = geo.country_code
  end
end

More information can be found here.

Now add a callback:

models/place.rb

[...]
after_validation :reverse_geocode
[...]

There are a couple of problems though:

  • We don’t want to do reverse geocoding if the coordinates were not provided or modified
  • We don’t want to perform both forward and reverse geocoding
  • We need a separate attribute to store an address provided by the user via the form

The first two issues are easy to solve – just specify the if and unless options:

models/place.rb

[...]
after_validation :geocode, if: ->(obj){ obj.address.present? and obj.address_changed? }
after_validation :reverse_geocode, unless: ->(obj) { obj.address.present? },
                   if: ->(obj){ obj.latitude.present? and obj.latitude_changed? and obj.longitude.present? and obj.longitude_changed? }
[...]

Having this in place, we will fetch coordinates if the address is provided, otherwise try to fetch the address if coordinates are set. But what about a separate attribute for an address? I don’t think we need to add another column – let’s employ a virtual attribute called raw_address instead:

models/place.rb

[...]
attr_accessor :raw_address

geocoded_by :raw_address
after_validation -> {
  self.address = self.raw_address
  geocode
}, if: ->(obj){ obj.raw_address.present? and obj.raw_address != obj.address }

after_validation :reverse_geocode, unless: ->(obj) { obj.raw_address.present? },
                 if: ->(obj){ obj.latitude.present? and obj.latitude_changed? and obj.longitude.present? and obj.longitude_changed? }
[...]

We can utilize this virtual attribute to do geocoding. Don’t forget to update the list of permitted attributes

places_controller.rb

[...]
private

def place_params
  params.require(:place).permit(:title, :raw_address, :latitude, :longitude, :visited_by)
end
[...]

and the view:

views/places/_form.html.erb

<h4>Enter either address or coordinates</h4>
<fieldset class="form-group">
  <%= f.label :raw_address, 'Address' %>
  <%= f.text_field :raw_address, class: "form-control" %>
  <small class="text-muted">You can also enter IP. Your IP is <%= request.ip %></small>
</fieldset>

<div class="form-group row">
  <div class="col-sm-1">
    <%= f.label :latitude %>
  </div>

  <div class="col-sm-3">
    <%= f.text_field :latitude, class: "form-control" %>
  </div>

  <div class="col-sm-1">
    <%= f.label :longitude %>
  </div>

  <div class="col-sm-3">
    <%= f.text_field :longitude, class: "form-control" %>
  </div>
</div>

So far so good, but without the map, the page still looks uncompleted. On to the next step!

Adding a Dynamic Map

Adding a dynamic map involves some JavaScript, so add it into your layout:

layouts/application.html.erb

<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_KEY&callback=initMap"
                                       async defer></script>

Note that the API key is mandatory (be sure to enable “Google Maps JavaScript API”). Also note the callback=initMap parameter. initMap is the function that will be called as soon as this library is loaded, so let’s place it inside the global namespace:

map.coffee

jQuery ->
  window.initMap = ->

Obviously we need a container to place a map into, so add it now:

views/places/_form.html.erb

[...]
<div class="card">
  <div class="card-block">
    <div id="map"></div>
  </div>
</div>

The function:

map.coffee

window.initMap = ->
  if $('#map').size() > 0
    map = new google.maps.Map document.getElementById('map'), {
      center: {lat: -34.397, lng: 150.644}
      zoom: 8
    }

Note that google.maps.Map requires a JS node to be passed, so this

new google.maps.Map $('#map')

will not work as $('#map') returns a wrapped jQuery set. To turn it into a JS node, you may say $('#map')[0].

center is an options that provides the initial position of the map – set the value that works for you.

Now, let’s bind a click event to our map and update the coordinate fields, accordingly.

map.coffee

lat_field = $('#place_latitude')
lng_field = $('#place_longitude')
[...]
window.initMap = ->
  map.addListener 'click', (e) ->
    updateFields e.latLng
[...]

updateFields = (latLng) ->
  lat_field.val latLng.lat()
  lng_field.val latLng.lng()

For our users’ convenience, let’s also place a marker at the clicked point. The catch here is that if you click on the map a couple of times, multiple markers will be added, so we have to clear them every time:

map.coffee

markersArray = []

window.initMap = ->
  map.addListener 'click', (e) ->
    placeMarkerAndPanTo e.latLng, map
    updateFields e.latLng

placeMarkerAndPanTo = (latLng, map) ->
  markersArray.pop().setMap(null) while(markersArray.length)
  marker = new google.maps.Marker
    position: latLng
    map: map

  map.panTo latLng
  markersArray.push marker

[...]

The idea is simple – we store the marker inside the array and remove it on the next click. Having this array, you may keep track of markers that were placed a clear them on some other condition.

It’s high time to test it out. Navigate to the new page and try clicking on the map – the coordinates should be updated properly. That’s much better!

Placing Markers Based on Coordinates

Suppose a user knows coordinates and want to find them on the map instead. This feature is easy to add. Introduce a new “Find on the map” link:

views/places/_form.html.erb

[...]
<div class="col-sm-3">
  <%= f.text_field :longitude, class: "form-control" %>
</div>

<div class="col-sm-4">
  <a href="#" id="find-on-map" class="btn btn-info btn-sm">Find on the map</a>
</div>
[...]

Now bind a click event to it that updates the map based on the provided coordinates:

map.coffee

[...]
window.initMap = ->
  $('#find-on-map').click (e) ->
    e.preventDefault()
    placeMarkerAndPanTo {
      lat: parseInt lat_field.val(), 10
      lng: parseInt lng_field.val(), 10
    }, map
[...]

We pass an object to the placeMarkerAndPanTo function that contains the user-defined latitude and longitude. Note that coordinates have to be converted to integers, otherwise an error will be raised.

Reload the page and check the result! To practice a bit more, you can try to add a similar button for the address field and introduce error handling.

Measuring Distance Between Places

The last thing we will implement today is the ability to measure the distance between added places. Create a new controller:

distances_controller.rb

class DistancesController < ApplicationController
  def new
    @places = Place.all
  end

  def create
  end
end

Add a route:

config/routes.rb

[...]
resources :distances, only: [:new, :create]
[...]

and a view:

views/distances/new.html.erb

<header><h1 class="display-4">Measure Distance</h1></header>

<%= form_tag distances_path do %>
  <fieldset class="form-group">
  <%= label_tag 'from', 'From' %>
  <%= select_tag 'from', options_from_collection_for_select(@places, :id, :title), class: "form-control" %>
</fieldset>

  <fieldset class="form-group">
  <%= label_tag 'to', 'To' %>
  <%= select_tag 'to', options_from_collection_for_select(@places, :id, :title), class: "form-control" %>
</fieldset>

  <%= submit_tag 'Go!', class: 'btn btn-primary' %>
<% end %>

Here we display two drop-downs with our places. options_from_collection_for_select is a handy method that simplifies the generation of option tags. The first argument is the collection, the second – a value to use inside the value option and the last one – the value to display for the user inside the drop-down.

Geocoder allows the measuring of distance between any points on the planet – simply provide their coordinates:

distances_controller.rb

[...]
def create
  @from = Place.find_by(id: params[:from])
  @to = Place.find_by(id: params[:to])
  if @from && @to
    flash[:success] =
        "The distance between <b>#{@from.title}</b> and <b>#{@to.title}</b> is #{@from.distance_from(@to.to_coordinates)} km"
  end
  redirect_to new_distance_path
end
[...]

We find the requested places and use the distance_from method. to_coordinates transforms the record into an array of coordinates (for example, [30.1, -4.3]) – we have to use it, otherwise the calculation will result in an error.

This method relies on a flash message, so tweak layout a bit:

layouts/application.html.erb

[...]
<% flash.each do |name, msg| %>
  <%= content_tag(:div, msg.html_safe, class: "alert alert-#{name}") %>
<% end %>
[...]

By default Geocoder uses miles as the measurement units, but you can tweak the initializer file and set the units option to km (kilometers) instead.

Conclusion

Phew, that was a long one! We’ve covered many features of Geocoder: forward and reverse geocoding, tweaking options, and measuring distance. On top of that, you learned how to use various types of Google maps and work with them via the API.

Still, there are other features of Geocoder that I have not covered in this article. For example, it supports finding places near the selected location, it can provide directions while measuring distance between locations, it supports caching, and can even be used outside of Rails. If you are planning to use this great gem in your project, be sure to skim the documentation!

That’s all for today folks. Hopefully, this article was useful and interesting for you. Don’t lose your track and see you soon!

  • greengiant

    Very informative tutorial as usual Ilya. I did have an issue though. When switching between tabs in the navbar I often see this error in the console: “You have included the Google Maps API multiple times on this page. This may cause unexpected errors.”

    Also the Map often does not render unless I refresh the page. I suspect this is a turbolinks issue. Any suggestions on how to deal with this?

  • Mashab Anwar Qureshi

    Thank you so much for this great informative article

    • Ilya Bodrov

      You are welcome!

      • Mashab Anwar Qureshi

        just wondering if you could clarify, about what do you mean by
        ” so let’s place it inside the global namespace:
        map coffee
        jQuery ->
        window initMap = -> ”

        where can i find this folder? i am just a beginner.
        thank you

        • Ilya Bodrov

          Ah sure. This has nothing to do with Rails and that’s not a folder. Actually, global namespace relates to JavaScript. You see, by default when you say `var some_string = ‘123abc’` you are creating a variable in the global namespace, meaning that any other script from other file can read this variable. In many cases that’s actually bad – suppose you include someone else’s script where a variable with the same name exists. This will create bugs that might be hard to fix. Therefore, variables are usually hidden from the global namespace and made local in various ways.

          CoffeeScript, a JavaScript preprocessor that we use, hides variables automatically. It means, that if I define `initMap = -> …` then this function (define as variable) will only be visible in this file, but not inside other files. In many cases that’s okay, but not in our application. We actually do want `initMap` to be visible outside for the Google Map’s script. That’s why we say `window.initMap`. `window` is a global object that’s visible everywhere, so by attaching a variable to it we make it global. That’s pretty much it.

  • Ilya Bodrov

    If you don’t use CoffeeScript and simply stick with ordinary JS then you don’t need window. part. This window. is only needed for Coffee because it automatically wraps code in different files with immediately invoking functions to prevent global namespace pollution. Unfortunately, that’s hard to explain this stuff in a single comment, so if you need more info, you may contact me via email – I can give you a quick lection on how Coffee relates to JS and to this window. prefix. Also, here is a discussion on the topic https://stackoverflow.com/questions/27887565/self-executing-coffeescript (and many more blog posts about the topic)

  • Pepper

    I copy exactly from your place.rb file. But it does not look up the latitude and longitude. Do you have any idea why?

    geocoded_by :raw_address
    after_validation -> {
    self.address = self.raw_address
    geocode
    }, if: ->(obj){ obj.raw_address.present? and obj.raw_address != obj.address }

    Thanks for this tutorial. It is very helpful for me to learn this API.

    • Ilya Bodrov

      Maybe some other part of the app has issues? I think the gem itself has no breaking changes since I wrote this article…

Recommended
Sponsors
Get the latest in Ruby, once a week, for free.