Geocoder: Display Maps and Find Places in Rails

Share this article

Geocoder: Display Maps and Find Places in Rails

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!

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!

Frequently Asked Questions (FAQs) about Geocoder in Rails

How do I install the Geocoder gem in Rails?

To install the Geocoder gem in Rails, you need to add the gem to your Gemfile. Open your Gemfile and add the following line: gem 'geocoder'. After adding the gem, run bundle install in your terminal to install the gem. Once the gem is installed, you can start using it in your Rails application.

How do I use Geocoder to find locations in Rails?

Geocoder provides a simple and flexible way to find locations in Rails. You can use the geocoded_by method in your model to specify the attribute that should be geocoded. For example, if you have a Location model with an address attribute, you can add geocoded_by :address to your model. After adding this line, you can call Location.geocode to find the latitude and longitude of the address.

How do I display maps using Geocoder in Rails?

To display maps using Geocoder in Rails, you need to use a mapping service like Google Maps or Mapbox. You can use the latitude and longitude attributes returned by Geocoder to display the location on the map. You can also use the nearbys method provided by Geocoder to find and display nearby locations on the map.

How do I handle errors in Geocoder?

Geocoder provides several ways to handle errors. If the geocoding service is unavailable or the request fails, Geocoder will raise a Geocoder::Error. You can rescue this error in your code and handle it appropriately. You can also use the Geocoder::Configuration.always_raise option to specify which errors should be raised.

How do I test Geocoder in Rails?

Testing Geocoder in Rails can be done using Rails’ built-in testing framework or other testing libraries like RSpec. You can use the Geocoder::Testing module to set up and tear down test cases. This module provides methods like Geocoder::Testing.fake and Geocoder::Testing.unfake to control the behavior of Geocoder during testing.

How do I configure Geocoder in Rails?

Geocoder can be configured in Rails by creating an initializer file in the config/initializers directory. In this file, you can set various configuration options like the geocoding service to use, the API key for the service, the timeout for requests, and more.

How do I use Geocoder with ActiveRecord?

Geocoder integrates seamlessly with ActiveRecord. You can use the geocoded_by and reverse_geocoded_by methods in your models to specify the attributes that should be geocoded. You can also use the near and within_bounding_box methods to perform location-based queries.

How do I use Geocoder with non-ActiveRecord models?

Geocoder can be used with non-ActiveRecord models by including the Geocoder::Model::Base module in your model. This module provides the geocoded_by and reverse_geocoded_by methods, which you can use to specify the attributes that should be geocoded.

How do I use Geocoder with multiple models?

Geocoder can be used with multiple models in your Rails application. You can use the geocoded_by and reverse_geocoded_by methods in each model to specify the attributes that should be geocoded. You can also use the near and within_bounding_box methods to perform location-based queries across multiple models.

How do I use Geocoder in a Rails API?

Geocoder can be used in a Rails API in the same way as in a regular Rails application. You can use the geocoded_by and reverse_geocoded_by methods in your models, and the near and within_bounding_box methods in your controllers to perform location-based queries. You can also use the latitude and longitude attributes returned by Geocoder to return location data in your API responses.

Ilya Bodrov-KrukowskiIlya Bodrov-Krukowski
View Author

Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.

geocodingGlennGmapsRuby on Rails
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week