Use Google Maps with Rails

Nouran Mahmoud
Software Developer
Tweet

Maps are the way to navigate the world. They give the ability to inspect every single location on Earth, opening the way to services like routing and soil composition. In this article, I will go through the Google Maps API (and some alternatives) and integrate it with Rails.

Tutorial Source Repository

The source code is available on a Github repository. Each feature is a separate commit, so you can follow the tutorial progress using git checkout .

Prerequisites

  • Basic knowledge of Ruby on Rails using RailsBricks.
  • Intermediate knowledge of JavaScript
  • How web maps work. This article is worth reading if you are new to the game.

Our goal is to make map integration with Rails simple. The tutorial goes through the exact steps to integrate Google Maps, covering some useful Rails gems. Finally, alternatives to Google Maps, like the popular open source libraries Leaflet.js and MapBox, will get a brief look.

Initializing the Map

In the past, Google Maps required an API key, however, this API key is no longer mandatory on V3. If you are using another version, follow these steps:

If you use Google Maps V3 just skip this part

  1. Go to the Google API console

  2. Click the APIs & auth -> APIs.

  3. Activate Google Maps JavaScript API v3 by clicking the Status button to be On.

  4. Click on credentials -> Create new key -> browser key

  5. Make sure that ACCEPT REQUESTS FROM THESE HTTP REFERERS (WEB SITES) is empty or contains your domain, as in the image below.
    Create a browser key and configure allowed referers

NOTE:
Not every aspect of the map is covered in the material below. For more detailed information, you can visit the Google Maps JavaScript documentation.

Loading the Map

At first, create the project with RailsBrick (You’re free to use any tool for building rails apps). It’s a rails app creator with basic functionality out of the box. You can get how to use it from the introductionary video here.

To initialize the map in our home page, create a div with an id of map-canvas in the home view (/app/views/home.erb). Wrap this div in another with an id of map-container, which will be used to add some styling to the map.

First: Here is the code of the home view:

<% title("Home Page") %>
<h1>Google Maps Tut</h1>
<div id="map-container">
  <div id="map-canvas"></div>
</div>

Add some CSS. Open up the file named frameworkandoverrides.css.scss, which is a part of the RailsBricks boilerplate and is used to override Bootstrap styles.

Second: Add the following styles:

#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%;
 }

As you can see in the above CSS, we set the map-container to a fixed height of 400 pixels and added some border styling. The last step to get an initial working map is to create a folder named “map” in app/assets/javascript/map and add a file named gmap.js. Now the map should look like this:

Initializing the map

NOTE:
If the map zoom controller doesn’t show properly, it’s a conflict with Bootstrap styling for images and labels. Just add the following overrides to your CSS (frameworkandoverrides.css.scss) file:

/* Bootstrap Css Map Fix*/
#map-container #map-canvas img {
  max-width: none;
}
/* Bootstrap Css Map Fix*/
#map-container #map-canvas label {
  width: auto; display:inline;
}

Drawing on the Map

Basic markers

The Google Maps API contains a marker object allowing you to easily create simple markers. The Marker object contains attributes like marker position, marker title, and the map where the marker is located.

To avoid repeating the code, create a function called createMarker with the parameters coords, map, and title:

var marker;
function createMarker(coords, map, title){
  marker = new google.maps.Marker({
    position: coords,
    map: map,
    title: title
  });
}

Custom Markers

The Marker object has an “icon” attribute which can take a path or image object. We will create two functions: one for creating an image and the one for creating a custom marker. To make the marker draggable, simply add the attribute draggable with the value true. Also, the Google Maps API supports two types of animation for the marker: DROP and BOUNCE.

Create a function named createImage which will return the image object used by our custom marker. Its size is 32×32 pixels and its origin is (0, 0).

function createImage(url){
  var image = {
    url: url,
    // This marker is 32 pixels wide by 32 pixels tall.
    size: new google.maps.Size(32, 32),
    // The origin for this image is 0,0.
    origin: new google.maps.Point(0,0),
    // The anchor for this image is the base of the flagpole at 0,32.
    anchor: new google.maps.Point(0, 32)
  };
  return image;
}

Next, create a function called createCustomMarker to do the actual work of creating the marker object. It takes the coords, the map object, and the title for the marker. Use the function createImage to return the correct image for our icon.

function createCustomMarker(coords,map,title){
  marker = new google.maps.Marker({
    position: coords,
    map: map,
    title: title,
    icon: createImage("/assets/icon.png")
  });
}

Infowindow

Infowindow is a tooltip for displaying content (text or images). You can add an infowindow to a marker or on a specified longitude and latitude (lon, and lat for short). The InfoWindow object takes a InfoWindowOptions object.

function createInfoWindow(text){
  var infowindow = new google.maps.InfoWindow({
    content: text
  });
  return infowindow;
}

Put the following code in an initialize() function

// add infowindow when clicking on the simple marker marker
var info = createInfoWindow("Congratulations!");
google.maps.event.addListener(marker, 'click', function() {
  info.open(map,marker);
});

This code will create an Infowindow named info and place the text “Congratulations!” in it. An event listener handles the click event on the marker to open the Infowindow.

Drawing simple and custom marker

Drawing lines

Drawing lines requires a series of coordinates to connect. The Google Maps API provides an object called Polyline for drawing lines with attributes stroke_color, weight, and opacity, along with adding icons, symbols, or animation.

Simple Line

// drawing static polyline
var lineCoordinates = [
  new google.maps.LatLng(30.055487, 31.279766),
  new google.maps.LatLng(30.223356, 31.324345),
  new google.maps.LatLng(30.345656, 31.567677),
  new google.maps.LatLng(30.565678, 31.676887)
];
createPolyline(map, lineCoordinates, lineSymbol);

var linePath;
function createPolyline(map,lineCoordinates,lineSymbol){
  linePath = new google.maps.Polyline({
    path: lineCoordinates,
    geodesic: true,
    strokeColor: '#FF0000',
    strokeOpacity: 1.0,
    strokeWeight: 2
   });
 linePath.setMap(map);
}

We define an array called lineCoordinates with the coords for the polyline. The function createPolyline actually creates the polyline, setting its path using the lineCoordinates array. The geodesic attribute is true, telling Google Maps to take care of the complicated math for us. Give it a stroke color of #FF0000, an opacity of 1, and a stroke weight of 2 to make it visible. After we have our polyline object ready, add it to the map using the setMap function.

Simple Line with Dashes.

Drawing a dashed line is simply a matter of creating the style and telling the line to use it. Below, the lineSymbol variable is added to the polyline. Notice that the lineSymbol has a path to follow and a scale of 4. The createPolyline function is modified to use lineSymbol as a repeated icon.

var lineSymbol = {
  path: 'M 0,-1 0,1',
  scale: 4,
  strokeOpacity: 1,
  strokeColor: '#393'
};

// modify the createPolyline function to contain the symbol
var linePath;
function createPolyline(map, lineCoordinates, lineSymbol){
  linePath = new google.maps.Polyline({
    path: lineCoordinates,
    geodesic: true,
    strokeColor: '#FF0000',
    strokeOpacity: 1.0,
    strokeWeight: 2,
     icons: [{ // this Array is for adding symbols to the line
      icon: lineSymbol,
      offset: '100%'
    }]
  });
  linePath.setMap(map);
}

Animated Dashes

We can even animate the dashes by adding a function called animateCircle that moves the icons along the line by changing the offset. There isn’t even a need to change the createPolyline function.

function animateCircle() {
  var count = 0;
  window.setInterval(function() {
    count = (count + 1) % 200;

    var icons = linePath.get('icons');
    icons[0].offset = (count / 2) + '%';
    linePath.set('icons', icons);
  }, 20);
}

//modify the `createPolyline` function to be like the following
var linePath;
function createPolyline(map, lineCoordinates, lineSymbol){
  linePath = new google.maps.Polyline({
    path: lineCoordinates,
    geodesic: true,
    strokeColor: '#FF0000',
    strokeOpacity: 1.0,
    strokeWeight: 2,
     icons: [{ // this Array is for adding symbols to the line
      icon: lineSymbol,
      offset: '0',
      repeat: '20px'
    }]
  });
   linePath.setMap(map);
}

Then call the animateCircle() function after creating the polyline with createPolyline function.

User-Created Dynamic Polyline

In the following code, we add the polyline options/attributes to a variable and use it to create the polyline. This is not very different from the above code samples. Adding a click event listener to the map that adds a point to our line allows the user to draw at will.

// drawing dynamic polyline
var polyOptions = {
  strokeColor: '#000000',
  strokeOpacity: 1.0,
  strokeWeight: 3
};
poly = new google.maps.Polyline(polyOptions);
poly.setMap(map);
google.maps.event.addListener(map, 'click', addLatLng);

function addLatLng(event){
  var path = poly.getPath();
  // Because path is an MVCArray, we can simply append a new coordinate
  // and it will automatically appear.
  path.push(event.latLng);
}

Drawing simple and animated lines

Drawing Polygons

Polygons are similar to polylines in that they are drawn by a series of coordinates. A Polygon has a stroke and fill, which can be customized. We will add the coords for the polygon by hand in an array called polygonCoords and pass it to a new function named drawingPolygon. This function creates the polygon and set its paths to the coords added in the polygonCoords array. Polygons are also draggable and editable.

Simple Polygon

// drawing polygon
var polygonCoords = [
  new google.maps.LatLng(30.055487, 31.279766),
  new google.maps.LatLng(30.466465, 31.118292),
  new google.maps.LatLng(30.321384, 31.75737),
  new google.maps.LatLng(30.055487, 31.279766)
];

// Construct the polygon.
drawingPolygon(polygonCoords);

function drawingPolygon(polygonCoords) {
  var polygon = new google.maps.Polygon({
    paths: polygonCoords,
    strokeColor: '#FF00FF',
    strokeOpacity: 0.8,
    strokeWeight: 2,
    fillColor: '#FF0000',
    fillOpacity: 0.35,
    draggable:true,
    editable: true
  });
  polygon.setMap(map);
}

Drawing polygon

Drawing on the Map Using Drawing library

The Google Maps API supports a drawing library that provides a graphical interface. This GUI lets users draw polylines, polygons, circles, markers, and triangles on the map.

To load the drawing library on the map, just make the maps API URL include &libraries=drawing and start using the DrawingManager object.

The API source link should look like this:


https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=drawing

Initialize the DrawingManager object:

// trying the drawing liberary
var drawingManager = new google.maps.drawing.DrawingManager({
  drawingMode: null,
  drawingControl: true,
  drawingControlOptions: {
    position: google.maps.ControlPosition.TOP_CENTER,
    drawingModes: [
      google.maps.drawing.OverlayType.MARKER,
      google.maps.drawing.OverlayType.CIRCLE,
      google.maps.drawing.OverlayType.POLYGON,
      google.maps.drawing.OverlayType.POLYLINE,
      google.maps.drawing.OverlayType.RECTANGLE
    ]
  },
  markerOptions: {
    icon: "/assets/icon.png"
  }
});
drawingManager.setMap(map);

The DrawingManager constructor allows you to manage the map drawing tools (controls) by specifying which overlay will be rendered, its position on the map, and its initial state. Setting drawingMode to null means it will not be defaulted to a specific overlay control. It can be defaulted with the polyline overlay by changing null to google.maps.drawing.OverlayType.POLYLINE.

The second argument is drawingControl which takes true for rendering the control or false for hiding it. drawingControlOptions specifies the control position on the map. Google Maps provides various places to put its controls, such as TOP_CENTER, TOP_RIGHT, BOTTOM_LEFT, and so on.

The arguments also specify the available drawingModes, an array of available google.maps.drawing.OverlayType constants like CIRCLE, POLYLINE, POLYGONS, RECTANGLE, MARKER. You can also give each overlay specific properties, just like we did in the previous code snippets.

The last step is to set the map on drawingManager.

Drawing markers, polylines, polygons, circles and triangles by Drawing library

Adding Map Services

Geocoding and Reverse Geocoding

The Google Maps API provides a class called Geocoder for getting coordinate locations of known addresses (geocoding) and vice-versa (reverse geocoding) dynamically.

While the service no longer requires an API key, it does limit geocodes to 2,500 per day and requires that the resulting application show data with a Google Map. The returned data is either JSON or XML.

var geocoding  = new google.maps.Geocoder();
$("#submit_button_geocoding").click(function(){
  codeAddress(geocoding);
});
$("#submit_button_reverse").click(function(){
  codeLatLng(geocoding);
});

Get Coordinates by Geocoding

Here, we get the coordinates by entering an address into an input field.

function codeAddress(geocoding){
  var address = $("#search_box_geocoding").val();
  if(address.length > 0){
    geocoding.geocode({'address': address},function(results, status){
      if(status == google.maps.GeocoderStatus.OK){
        map.setCenter(results[0].geometry.location);
        var marker = new google.maps.Marker({
          map: map,
          position: results[0].geometry.location
        });
      } else {
        alert("Geocode was not successful for the following reason: " + status);
      }
    });
  } else {
    alert("Search field can't be blank");
  }
}

Geocoding

Get Address by Reverse Geocoding

In this case, we pass the latlng variable to the geocode object to generate the location (address) on the map.

function codeLatLng(geocoding) {
  var input = $('#search_box_reverse').val();
  console.log(input);

  var latlngbounds = new google.maps.LatLngBounds();
  var listener;
  var regex = /([1-9])+\.([1-9])+\,([1-9])+\.([1-9])+/g;
  if(regex.test(input)) {
    var latLngStr = input.split(",",2);
    var lat = parseFloat(latLngStr[0]);
    var lng = parseFloat(latLngStr[1]);
    var latLng = new google.maps.LatLng(lat, lng);
    geocoding.geocode({'latLng': latLng}, function(results, status) {
      if (status == google.maps.GeocoderStatus.OK) {
        if(results.length > 0) {
          //map.setZoom(11);
          var marker;
          map.setCenter(results[1].geometry.location);
          var i;
          info = createInfoWindow("");
          for(i in results){
            latlngbounds.extend(results[i].geometry.location);
              marker = new google.maps.Marker({
              map: map,
              position: results[i].geometry.location
            });

            google.maps.event.addListener(marker, 'click', (function(marker,i) {
              return function() {
                info.setContent(results[i].formatted_address);
                info.open(map,marker);
              }
            })(marker,i));
          }

          map.fitBounds(latlngbounds);
          listener = google.maps.event.addListener(map, "idle", function() {
            if (map.getZoom() > 16) map.setZoom(16);
            google.maps.event.removeListener(listener);
          });
        }
      } else {
        alert("Geocoder failed due to: " + status);
      }
    });
  } else {
    alert("Wrong lat,lng format!");
  }
}

Reverse Geocoding

Generating Directions

The Google Map API provides a great direction service for calculating routes between two or more addresses. This service can be enabled by initializing google.maps.DirectionsService, which takes no parameters but has one method called route(). This method takes two parameters: an object from google.maps.DirectionsRequest and a callback function.

The basic properties of DirectionRequest are origin, destination, and the travelMode that defines the mode of transportation. DirectionsStatus contains the repsonse status for the directions request.

To expose the resulting routes, there is DirectionsRenderer, which takes no parameters and has a method called setMap for defining the map and a method called setDirections that sets the returned response.

For more details about the directions service, read this tutorial

var directionsService = new google.maps.DirectionsService();
var directionsDisplay = new google.maps.DirectionsRenderer();

map = new google.maps.Map(document.getElementById("map-canvas"),mapOptions);

directionsDisplay.setMap(map);

var request = {
  origin: "Mansoura, Daqahlia, Egypt",
  destination: "Cairo, Egypt",
  travelMode: google.maps.DirectionsTravelMode.DRIVING
};
directionsService.route(request, function(response, status) {
  //Check if request is successful.
  if (status == google.maps.DirectionsStatus.OK) {
    console.log(status);
    directionsDisplay.setDirections(response); //Display the directions result
  }
});

Direction between Cairo and Manousoura cities

Map Controls

Google Maps provides controls on the map for handling and manipulating the map UI. These controls can be disabled, moved, or even customized with new functionality.

The available controls are:

  • Zoom control
  • Pan control
  • Scale control
  • MapType control
  • Streetview control
  • Rotate control
  • Overview map control

The default UI controls can be disabled by adding disableDefaultUI: true to the map options.

To remove one of the default controls, add it as an attribute on the MapOptions, like panControl: true, zoomControl: false.

var mapOptions = {
  center: new google.maps.LatLng(30.055487, 31.279766),
  zoom: 8,
  mapTypeId: google.maps.MapTypeId.NORMAL,
  panControl: true,
  zoomControlOptions: {
    style: google.maps.ZoomControlStyle.SMALL,
    position: google.maps.ControlPosition.LEFT_CENTER
  },
  mapTypeControlOptions: {
    style: google.maps.MapTypeControlStyle.DROPDOWN_MENU,
    mapTypeIds: [google.maps.MapTypeId.ROADMAP, "map_style"]
  },
  scaleControl: false,
  streetViewControl: true,
  overviewMapControl: true
};

Simple and Custom controls (fullscreen,zoom)

Custom Map Styles

This feature give the ability to manipulate the standard base map presentation UI.

Map presentation is composed of two characteristics: Map features lie roads, park, mountains, etc. and Styles for various map elements.

Gotcha: The style array has a limited number of characters allowed which, if exceeded, prevents the style from being applied to the map.

To create a custom map style, do the following:

First: Create the style array with two basic properties MapFeatures and stylers:

var mapstyle = [
  {
    "featureType": "administrative.locality",
    "elementType": "labels.icon",
    "stylers": [
      { "invert_lightness": true },
      { "color": "#e40952" },
      { "visibility": "on" }
    ]
  },{
    "featureType": "water",
    "elementType": "geometry.fill",
    "stylers": [
      { "visibility": "on" },
      { "hue": "#5eff00" },
      { "color": "#282744" },
      { "weight": 0.1 },
      { "saturation": -56 },
      { "lightness": 22 },
      { "gamma": 3.91 }
    ]
  }
]

Second: Set the mapTypeId in the map options:

var mapOptions = {
  center: new google.maps.LatLng(30.055487, 31.279766),
  zoom: 8,
  mapTypeId: google.maps.MapTypeId.NORMAL,
  mapTypeControlOptions: {
    style: google.maps.MapTypeControlStyle.DROPDOWN_MENU,
    mapTypeIds: [google.maps.MapTypeId.ROADMAP, "map_style"]
  }
};

Third: Instantiate the StyledMapType, providing the pre-defined mapstyle array and the map name. The name will appear in the control as a style option.

var styledMap = new google.maps.StyledMapType(mapstyle, {name: "styled map"});
map = new google.maps.Map(document.getElementById("map-canvas"), mapOptions);

Fourth: Add our customized map style to the map’s mapTypes:

map.mapTypes.set("map_style", styledMap);

Fifth: Set the mapType ID to our styled map:

map.setMapTypeId("map_style");

Giving Custom style to the map

Useful Rails Gems

Geocoder

The Geocoder gem provides geocoding, reverse gecoding, finding nearby locations, determining distances, and map services for Ruby.

NOTE:
Geocoder supports using rails3 and rails4, there’s another branch for rails2.

Geocoder is installed like any Ruby gem using gem install geocoder or adding gem "geocoder" to the Gemfile and running bundle install.

You have to add two float fields (lat, lng) to any model used to store latitude and longitude values after fetching them by street address or zip codes like so:

rails generate migration AddLatitudeAndLongitudeToModel lat:float lng:float
rake db:migrate

Also, add the following to the model to specify which service will be used (geocoder or reverse geocoder):

geocoded_by :address
# auto-fetch coordinates and the condition is for preventing fetching the same address more than once
after_validation :geocode, if: :address_changed?

This full_street_address needs to be implemented on the model to create a readable address.

NOTE:
Geocoder supports some of the popular databases like (MySQL, PostgreSQL, MongoDB).

The Geocoder also gem provides you with an easy way to swap between various geocoding providers.

GMaps4rails

GMaps4rails is an awesome gem that provides geocoding and map locations. It uses JS to generate filters when rendering markers. It also has a geocoding feature that calculates simple lat, lng values.

You can combine this gem with the Geocoder gem features, rendering the results on the map using GMaps4rails.

Installation

First: Add the following line to your Gemfile file:

gem 'gmaps4rails

Then run bundle install

Second: Add a div to hold the map:

<div style='width: 800px;'>
  <div id="map" style='width: 800px; height: 400px;'></div>
</div>

Third: Add the Google scripts to your application.html.erb layout:

<script src="//maps.google.com/maps/api/js?v=3.13&sensor=false&libraries=geometry" type="text/javascript"></script>
<script src='//google-maps-utility-library-v3.googlecode.com/svn/tags/markerclustererplus/2.0.14/src/markerclusterer_packed.js' type='text/javascript'></script>

Fourth: Require the underscore.js library too, because Gmaps4rails uses it. In your Rails application.js:

//= require underscore
//= require gmaps/google

Now, create the map as follows:

handler = Gmaps.build('Google');
handler.buildMap(
  {
    provider: {
      disableDefaultUI: true
      // here you can pass other Google Maps API options here
    },
    internal: {
      id: 'map'
    }
  },
  function() {
    markers = handler.addMarkers([
      {
        "lat": 0,
        "lng": 0,
        "picture": {
          "url": "https://addons.cdn.mozilla.net/img/uploads/addon_icons/13/13028-64.png",
          "width":  36,
          "height": 36
        },
        "infowindow": "hello!"
      }
    ]);
    handler.bounds.extendWith(markers);
    handler.fitMapToBounds();
  }
);

For more details about this gem, check this link.

Alternatives to Google Maps

Leaflet.js

Leaflet is a modern JavaScript library for embedding maps which gained its popularity from simplicity and ease of implementing markers, overlays, and manipulating various map components. Leaflet can be extended with the enormous set of available plugins. It uses a permissive BSD open-source license, so it can be added to any site without legal issues. Also, it supports multiple map providers, including OpenStreetMap, MapQuestOpen, Stamen, Esri, and OpenWeatherMap.

Installation

Download it from its official site leaflet.com. It is available as a .zip file or a fork on github.

A snip of code to illustrate Leaflet’s simplicity:

// create a map in the "map" div, set the view to a given place and zoom
var map = L.map('map').setView([51.505, -0.09], 13);

// add an OpenStreetMap tile layer
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
    attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

// add a marker in the given location, attach some popup content to it and open the popup
L.marker([51.5, -0.09]).addTo(map)
    .bindPopup('A pretty CSS3 popup. <br> Easily customizable.')
    .openPopup();

Check the Leaflet quick start for more information.

MapBox

MapBox is a beautiful tool which gives flexibility for creating maps. It has the ability to design custom maps with wonderful layers and a lot of custom features with TileMill (map design studio), a downloadabale application. Or you can create MapBox web applications with custom markers and overlays by using its JavaScript API.

Installaton

All you need is an account on MapBox. Once you’ve created some maps on MapBox, those Map IDs are used to integrate it with your web application.

This example will give you just a glance about using MapBox with JavaScript.

After creating your map on MapBox, include the Mapbox.js library on your project.

First: Initialize the map:

var map = mapbox.map('map');

Second: Set the zoom range and center zoom point of the map:

map.setZoomRange(5, 15);

map.centerzoom({
    lat: 37.871385,
    lon: -99.228516
}, 5);

Third: Add the custom layer you created on MapBox.

map.addLayer(mapbox.layer().id('YOUR-MAP-ID-HERE'));

After that, you can embed more features in the map, like markers, UI features (fullscreen, zoomer) and so on.

NOTE:
This is not an in-depth tutorial on how to use MapBox, but it exposes it as an alternative option to Google Maps.

Summary

This tutorial gives a basic understanding of how to create maps and general map concepts. Hopefully, you can now create a map to meet your needs using any map API you choose.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments