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 withuser_id
and marked as a foreign keyaddress
(text
) – address of the place a user has visitedlatitude
andlongtitude
(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 anaddress
column, but you can use any other method. For example, if you have a separate columns calledcountry
,city
, andstreet
, 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
andlongitude
, with their type set tofloat
. 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!