Google maps in a Rails app

My Rails app has Events which have location which of course needs to be shown in a map. With the postal address of a location, its latitude and longitude can be calculated through a geocoding conversion. With them, the location can be plotted in a map.

The Google Geocoding API does just that.

For example, by entering the following URL: https://maps.googleapis.com/maps/api/geocode/json?address=800+Boulevard+Rene-Levesque+O+Montreal in a browser, a JSON string is retrieved which includes the latitude and longitude of an office building located at “800 Boulevard Rene-Levesque, Ouest” in Montreal, Canada, where one of the events took place:


{
   "results" : [
      {
...
         "geometry" : {
            "location" : {
               "lat" : 45.5009512,
               "lng" : -73.5675947
            },
...
}

However, this is one of the last steps. Let’s start with the Rails side.

Include a map in a Rails app

Before being able to use the geographical coordinates just retrieved, we need to tell Rails to include the map. This requires some JavaScript. First, I want to include a fixed map.

I added to the app/views/events/show.html.erb the following:
script

The first script brings in the necessary tools to draw the map. It can be included in the app/views/layouts/application.html.erb as follows:

in_app_erb

The second script delays the map related scripts until the page if fully loaded. Here is a good explanation.

The third section defines how the map will be displayed. The CSS portion defining these two elements is:

#map-container {
   height: 400px;
   border-radius: 16px 16px;
   border-color: #fff;
   border-style: solid;
   box-shadow: 2px 2px 10px #B1B1B1;
   margin-top: 25px;
   border-width: 7px;
 }

#map-canvas {
   height: 384px;
   width: 100%;
 }

The script to initialize the map is as follows:

# app/assets/javascripts/gmap.js

function initialize() {
    ## Initialize the map parameters
    var center = new google.maps.LatLng(45.5009512, -73.5675947)
    var mapOptions = {
        center: center,
        zoom: 16
    };
 
    ## Initialize the map object and attach it to the element with id 'map-canvas'
    var map = new google.maps.Map(document.getElementById('map-canvas'),
        mapOptions);

    ## Initialize the marker and attach it to the previously created map
    var marker = new google.maps.Marker({
        position: center,
        map: map,
        title: "Here! Ici!"
    });
}

These scripts and code snippets produce the following map with a marker centred in downtown Montreal:

map-1

The map according to the event address

Having this in place, next is to feed the geographical coordinates corresponding to the event’s address to the initialize function to display the map. However, when I changed the function to

  function initialize(lat,lon)

and

  google.maps.event.addDomListener(window, 'load', initialize(lat,lon));

I had the following error:

 Uncaught TypeError: Cannot read property 'offsetWidth' of null

After looking high and low and bearing with my inexperience with JavaScript, I found the answer here: simply move the div defining the map-canvas and map-contianer in the show.html.erb file, above the script calling the initialize function.

Latitude and longitude from a postal address

I decided to calculate the latitude and longitude only when the postal address (map_address) is created or updated. In the controller this corresponds to the update and create methods. I wrote a function and included it in app/controllers/events_controller.rb as follows:

  private
  def lat_lon(address)
    # Replace spaces by + signs, escape any other characters and
    # convert the string into a url object.
    url = URI.parse(
        'https://maps.googleapis.com/maps/api/geocode/json?address=' +
        address.tr(' ', '+')
      )
    # Make the request to retrieve the JSON string
    response = open(url).read

    # Convert the JSON string into a Hash object
    result = JSON.parse(response)
    lat = result['results'][0]['geometry']['location']['lat']
    lon = result['results'][0]['geometry']['location']['lng']
    return lat, lon
  end

The preliminary test suite worked fine, but I missed one test. As you may know, Montreal is a bilingual city and addresses are also found in French with non-ASCII characters. Browsers tend to be more lenient and flexible, and accept characters which otherwise would throw an exception in a strict conversion into a URI element. So, https://maps.googleapis.com/maps/api/geocode/json?address=800+Boulevard+René-Levesque+O+Montréal will work just fine, having the address equal to “800 Boulevard René Levesque, Montréal” as an input to the lat_lon method above, will throw an error:

URI::InvalidURIError: bad URI(is not URI?)

The solution was to use the URI method escape to transform all the unsafe characters including the space.

Final version of code

For the sake of completeness, here are the revised scripts:

# app/controllers/events_controller.rb
require 'net/http'
class EventsController < ApplicationController
  ...
  def create
    ...
    @event.lat, @event.lon = lat_lon(@event.map_address)
    ...
  end 

  ...

  def update
    ...
    @event.lat, @event.lon = lat_lon(params[:event]['map_address'])
    ...
  end

  ...

  private
  def lat_lon(address)
    # Escape any non_ASCII characters and convert the string into a URI object.
    encoded_url = URI.escape(
      'https://maps.googleapis.com/maps/api/geocode/json?address=' + address
    )
    url = URI.parse((encoded_url))

    # Make the request to retrieve the JSON string
    response = open(url).read

    # Convert the JSON string into a Hash object
    result = JSON.parse(response)

    # Extract the latitude and longitude and return them
    lat = result['results'][0]['geometry']['location']['lat']
    lon = result['results'][0]['geometry']['location']['lng']
    return lat, lon
  end
# app/assets/javascripts/gmap.js

function initialize(lat,lon) {
      var center = new google.maps.LatLng(lat, lon)
      var mapOptions = {
        center: center,
        zoom: 16
  };
  var map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);

  var marker = new google.maps.Marker({
      position: center,
      map: map,
      title: "Here! Ici!"
    });
}

script-final

Feedback

Leave any questions or comments at the bottom! I would love hearing from you!

Helpful links