Rails 4 Application Development HOTSHOT (2014)

Chapter 7. Creating an API Mashup - Twitter and Google Maps

Social media is an important tool these days, and with the developer APIs available for most services such as Facebook, Twitter, and Google, the possibilities are endless. There are so many applications of these APIs, especially when you do not want a user to create a new login and when you want to give your application a social twist by sharing the data from multiple social networks inside your application.

Mission briefing

In this project, we will create an application that utilizes Twitter and Google Maps API. We will use Twitter OAuth2 to authenticate the user using Twitter, and we will use Google Maps API v3 to display the friends of the user on a Google map. We will visualize the location of the user's Twitter friends using this application. As shown in the following screenshot, we will see our friends with their corresponding locations on the map:

Mission briefing

Why is it awesome?

APIs are an important part of many web applications nowadays. It not only builds a loyal developer community, thereby backing the web application, but also improves the user engagement with the application. Facebook, Twitter, and Google APIs are the most commonly used because of their extremely high user base, clean API methods, and a huge developer community to back them up. These APIs are also easy to include in the application through community-contributed interfaces. We will look at some of them while building this project.

At the end of this project, we will be able to mashup Twitter and Google map APIs and make a fun little application.

Your Hotshot objectives

While building this application, we will have to go through the following tasks:

·        Creating an application login with Twitter

·        Calling all Twitter friends

·        Getting latitude and longitude details of the user's location

·        Passing Twitter data to the Google Maps API using Rails

·        Displaying friends on the map using the Google API

·        Creating points of interest—filter users based on their location

Mission checklist

We need the following installed on the system, and we also need to sign up for the API keys before we start with our mission:

·        Ruby 1.9.3 / Ruby 2.0.0

·        Rails 4.0.0

·        MongoDB

·        Bootstrap 3.0

·        Sass

·        Devise

·        Twitter API keys

·        Google Maps API keys

·        Git

·        A tool for mock-ups

·        jQuery

Creating an application login with Twitter

In the first task, we will create a login using Twitter and allow the users to authenticate using this. We will use the omniauth gem and add some custom methods in order to handle the session. OmniAuth is a solution for authentication that uses rack via multiple third-party OAuth providers such as Google, Twitter, Facebook, and GitHub. The omniauth gem (https://github.com/intridea/omniauth) provides the rack-based methods of authentication and sessions. Individual access methods for each provider is called a strategy. Each strategy is extracted into different gems. So, if we want to implement Twitter and Facebook, we need three gems: omniauth, omniauth-twitter, and omniauth-facebook.

Prepare for lift off

Before we start the work on this project, we will have to sign up for the API keys on Twitter and Google. Log in to Twitter as a developer and create an application by navigating to https://dev.twitter.com/apps/new. The page will look like the following screenshot:

Prepare for lift off

Once we submit the form after filling in the details, it will generate an application token and an application secret for us. As a part of our application details, we need to fill a field called Callback URL. Callback is defined as the URL where Twitter sends back the session details after you log in. By design, Twitter API does not support localhost, so in order to work with the application locally, we will define the Callback URL as http://lvh.me:3000. We have seen the various ways in which this dummy domain is used in Project 4Creating a Restaurant Menu Builder.

Prepare for lift off

Engage thrusters

We will take the first steps in this task to set up the base of the application:

1.    We will install omniauth and omniauth-twitter, the Twitter strategy gem from the master branch, by adding it to the Gemfile and run bundle install, as shown in the following code:

2.  Gemfile

3.  gem 'omniauth'

4.  gem 'omniauth-twitter', :github => 'arunagw/omniauth-twitter'

tweetmap$ bundle install

5.    We will create a file called secrets.yml inside the config folder. This file should contain secret_key_base and all the secret keys to be used in the app. We will explore this feature in detail in our debriefing section. Make sure you generate a different set of keys for development and production:

6.  config/secrets.yml

7.  development:

8.  secret_key_base: APPLICATION_SECRET TOKEN

9.  twitter_consumer_key: CONSUMER_KEY

10.twitter_consumer_secret: CONSUMER_SECRET

11. 

12.test:

13.  secret_key_base: APPLICATION_SECRET

14._TOKEN

15.  twitter_consumer_key: CONSUMER_KEY

16.  twitter_consumer_secret: CONSUMER_SECRET

17. 

18.production:

19.  secret_key_base: APPLICATION_SECRET

20._TOKEN

21.  twitter_consumer_key: CONSUMER_KEY

22.  twitter_consumer_secret: CONSUMER_SECRET

23. 

24.config/initializers/omniauth.rb

25.Rails.application.config.middleware.use OmniAuth::Builder do

26.  provider :twitter, Rails.application.secrets.twitter_consumer_key, Rails.application.secrets.twitter_consumer_secret

end

27.          We will generate a model for the user. This model will hold the values for the provider (Twitter), such as the name of the user, the screen name, or the Twitter handle, oauth_token, expires_at (expiration time of oauth_token), and location of the user:

28.tweetmap$rails g model user provider:string uid:string name:string oauth_token:string oauth_secret:string oauth_expires_at:datetime avatar:string address:string

29.          Our migration looks like the following code:

30.20131123144240_create_users.rb

31.class CreateUsers < ActiveRecord::Migration

32.  def change

33.    create_table :users do |t|

34.      t.string :provider

35.      t.string :uid

36.      t.string :name

37.      t.string :oauth_token

38.      t.string :oauth_secret

39.      t.string :avatar

40.      t.string :address

41.      t.datetime :oauth_expires_at

42.      t.timestamps

43.    end

44.  end

end

45.          In our user model, we will access certain values from the Twitter API's response hash and store it in the user table we just created:

46.app/models/user.rb

47.class User < ActiveRecord::Base

48.  def self.create_with_omniauth(auth)

49.    create! do |user|

50.       user.provider = auth["provider"]

51.      user.uid = auth["uid"]

52.      user.name = auth["info"]["name"] || ""

53.      user.address = auth["info"]["location"] || ""

54.      user.avatar = auth["info"]["image"] || ""

55.      user.oauth_token = auth["credentials"]["token"] || ""

56.      user.oauth_secret = auth["credentials"]["secret"] || ""

57.    end

58.  end

end

59.          After adding it to the user model, we need a mechanism to get these values. This is possible only when we are able to start a session with Twitter.

60.          To set up and handle a Twitter session, we will need a controller for sessions called session_controller.rb. We will add methods to create and destroy the session, that is, the signup, login, and sign out options:

61.tweetmap$ rails g controller sessions

62. 

63.app/controllers/session_controller.rb

64.class SessionsController < ApplicationController

65. def create

66.    auth = request.env["omniauth.auth"]

67.    user = User.find_by_provider_and_uid(auth["provider"], auth["uid"]) || User.create_with_omniauth(auth)

68.    session[:user_id] = user.id

69.    redirect_to root_url, :notice => Logged In Successfully"

70.  end

71.  def destroy

72.    session[:user_id] = nil

73.    redirect_to root_url, :notice =>"Logged Out Successfully"

74.  end

end

75.          For the controller to work, we need to add the routes in our routes.rb file:

76.config/routes.rb

77.match "/auth/:provider/callback" =>"sessions#create", via: [:get, :post]

  match 'signout', to: 'sessions#destroy', as: 'signout', via: [:get, :p

78.          Now that we have created a session, we will have to add a method to access the user object while in the session. We will do this by creating an object called current_user in our application_controller.rb file:

79.app/controllers/application_controller.rb

80. class ApplicationController < ActionController::Base

81.  # Prevent CSRF attacks by raising an exception.

82.  # For APIs, you may want to use :null_session instead.

83.  protect_from_forgery with: :exception

84.  helper_method :current_user

85.  private

86.  def current_user

87.    @current_user ||= User.find(session[:user_id]) if session[:user_id]

88.  end

end

89.          Also, we need to create a link to log in using Twitter. In our views/layouts/application.html.erb file, we will add a Sign In with Twitter link:

90.app/views/layouts/application.html.erb

91.<div class="navbar-collapse collapse" id="navbar-main">

92.<ul class="nav navbar-nav navbar-right">

93.<% if current_user %>

94.<li>Welcome, <%= current_user.name %><%= image_tag "#{current_user.avatar}" %><%= link_to "Sign Out", signout_path %></li>

95.<% else %>

96.<li><%= link_to "Sign in with Twitter", "/auth/twitter" %></li>

97.<% end %>

98.</ul>

</div>

99.          We will now click on Sign in with Twitter and see where it takes us. Once we do this, we are presented with the Twitter login screen as shown in the following screenshot:

Engage thrusters

Objective complete – mini debriefing

This task dealt with the addition of OmniAuth to the application. OmniAuth supports all the major services such as Facebook, Twitter, and Google. In the current version of OmniAuth, that is 1.2.1, we need to add the omniauth gem and also the gem that supports the respective provider strategy. In our case, the provider strategy uses Twitter. The same user table can be used to implement Facebook and Google strategies too.

In Rails 4.1, there is a new way to handle all the API keys and secrets in a much more secure way. When you generate a new project in Rails 4.1, Rails generates a secrets.yml file for us. This is a replacement to secret_token.rb that was earlier generated insideconfig/initializers. In Rails 3.2, the parameter was called secret_token too. In Rails 4, this has been renamed to secret_key_base and moved to a completely different file. We added lines for Twitter credentials in the secrets.yml file:

twitter_consumer_key

twitter_consumer_secret

In order to access the value of the preceding field in the controller, we can directly call Rails.application.secrets followed by the name of the field:

Rails.application.secrets.twitter_consumer_key

We then created a model for user and table to store all the callback values. Twitter or any API that uses OAuth for authentication returns oauth_token and oauth_expires_at. The token is a unique string that expires after a particular time interval of idleness. This is to terminate the session when not in use and keep the token from being stolen. To save the user to the database and create a session, we ran the create_with_omniauth method with the auth hash as an argument:

def self.create_with_omniauth(auth)

We created a controller to handle sessions against provider's Twitter user ID. This method works similar to the find_or_create_by method in Rails. It looks for the presence of the user ID and provider. If found, it creates a session; otherwise, it asks for permission to allow or reject the application from the user ID.

We then set the current_user object and persisted it in the session. We finally added a method to handle the user object during the course of the session. In the following screenshot, we can see the user logged in with the Twitter credentials:

Objective complete – mini debriefing

Calling all Twitter friends

In order to get the details of a user from Twitter, we will use the interface to the Twitter API, the twitter gem. In this task, we will pull some details of the user such as the Twitter username, the Twitter handle, the location of the user, and the user's avatar. We will store this information as a part of our user table. Friends are the users that are either followed by the user or follow the user.

Engage thrusters

We will now go ahead and access the Twitter data using the Twitter API:

1.    We will first add some more columns to our user table with the following code:

2.  tweetmap$ rails g add_details_to_users address:string avatar:string

3.    The migration file that is generated looks like the following code:

4.  class AddDetailsToUsers < ActiveRecord::Migration

5.    def change

6.      add_column :users, :address, :string

7.      add_column :users, :avatar, :string

8.    end

end

9.    We will save the link to avatar of the user and the user's location.

10.          We will now add the twitter gem to the Gemfile and run bundle install, as shown in the following code:

11.Gemfile

gem 'twitter', :github => 'sferik/twitter'

12.          We will now generate a model to save the friends' data:

13.Tweetmap$ rails g model friend name:string screen_name:string location:string latitude:float longitude:float user_id:integer

14.          We will also edit the migration to add decimal precision in our latitude and longitude fields:

15.class CreateFriends < ActiveRecord::Migration

16.  def change

17.    create_table :friends do |t|

18.      t.string :name

19.      t.string :screen_name

20.      t.string :location

21.      t.integer :user_id

22.      t.float :lat, {:precision=>10, :scale=>6}

23.      t.float :lng, {:precision=>10, :scale=>6}

24.      t.timestamps

25.    end

26.  end

end

27.          We will first create a home controller with an index action:

28.tweetmap $ rails g controller home index

29.          In our home controller, we will create a client for our Twitter API. This will require the consumer key and consumer secret. Twitter supplies the OAuth token and OAuth secret as a part of the session parameters. We also need to initiate this in order to get the data related to the user's friends:

30.app/controllers/home_controller.rb

31.def fetch_friend_data

32.    client = Twitter::REST::Client.new do |config|

33.      config.consumer_key        = "Rd5s5s82FAiUD1KufnrnQ"

34.      config.consumer_secret     = "6q8LouMcq8qE1aZa5Mn5nONdwpzchrmXOIlqEYl9CU"

35.      config.access_token        = "#{current_user.oauth_token}"

36.      config.access_token_secret = "#{current_user.oauth_secret}"

37.    end

end

38.          We will make a call on the Twitter API to fetch the last 20 friends of the user who is logged in:

39.app/controllers/home_controller.rb

@friends = client.friends.take(20)

40.          We will create a class method in which the user ID, the array of the friend's location coordinates, and the friend object will be passed as arguments. This method will save the friends' data to the friends table in the database:

41.app/models/friend.rb

42.def self.get_friend_data(friend,location_value,user_id)

43.     self.where(

44.      name: friend.name,

45.      screen_name: friend.screen_name,

46.      location: friend.location,

47.      user_id: user_id).first_or_create

  end

48.          We will loop through the friends' data, geocode their location, and get the coordinates. We will save these values to the database:

49.app/controllers/home_controller.rb

50.   @friends.each do |f|

51.      location = f.location

52.     Friend.get_friend_data(f,current_user.id)

    end

53.          As you can see, we are saving the values to the database using a method called get_friend_data and passing some arguments to this. We need to define that method in our model:

54.app/controllers/home_controller.rb

Friend.get_friend_data(f,current_user.id)

55.          We will create a route and a link to run this from the home page, as shown in the following code:

56.config/routes.rb

57.get "home/fetch_friend_data"

58.app/views/home/index.html.erb

59.<div class="row">

60.<div class="col-lg-6">

61.<h2 id="type-blockquotes"><%= link_to "Fetch My Friends", home_fetch_friend_data_path, :class=>"btn btn-primary" %></h2>

62.</div>

</div>

63.          We will first log in and then click on the Fetch My Friends link to fetch our friends' data.

64.          In order to check whether the data is being saved correctly or not, we will query our friends table:

65.1.9.3-p327 :001 > Friend.first

66.  Friend Load (0.5ms)  SELECT `friends`.* FROM `friends` ORDER BY `friends`.`id` ASC LIMIT 1

67. => #<Friend id: 1, name: "John Doe", screen_name: "johndoe", location: "Christchurch, New Zealand", user_id:user_id: 1, created_at: "2013-12-07 09:12:01", updated_at: "2013-12-07 09:12:01">

Objective complete – mini debriefing

In the context of Twitter, friends are all the people a user follows. In the previous task, we made a call on the Twitter API to fetch the data related to a user and saved it in the database. We added the twitter gem to the application and initiated a client based on the Twitter credentials we signed up for, in the beginning of the project. With these in place, we will call the friends from the Twitter API. Twitter, as a part of its API, allows very limited number of calls per hour (350) and a maximum of 180 in 15 minutes. Hence, we need to be careful about how we make a call on the data. One way to call all the friends on Twitter is to call all friends as shown in the following code:

client.friends.all

The drawback of the preceding method is that we might end up exhausting our limit, quite possibly in one go, because it makes the number of requests equal to the number of friends on Twitter. An alternative way is to call a limited number of friends, as shown in the following code:

client.friends.take(20)

The preceding method issues only a single request to the Twitter API and fetches data of 20 friends in one go. This is a better way to do the same thing.

Once the friends are fetched, they need to be saved to the friends table. As we are making an API call, the API does not tell us that the records we are fetching are the same as previous API calls. To save the friends to the database, we will use the find_or_create_bymethod in Rails. The Rails 4 syntax is significantly different from its earlier versions. It is a combination of two ActiveRecord methods called chaining of queries as shown in the following code:

     self.create_with(

      name: friend.name,

      location: friend.location,

      latitude: location_value.first,

      longitude: location_value.second).find_or_create_by(

      user_id: user_id, screen_name: friend.screen_name)

  end

The find_or_create_by method looks for the user ID and screen name of the user to check whether it exists or not. If there are extra parameters that need to be checked, we can use create_with, which runs a create query in case the record is not found with the passed attributes. We finally created a route and a link to fetch the friends. The following screenshot shows the link as it would appear on the home page:

Objective complete – mini debriefing

Getting latitude and longitude details of the user's location

To map the friends of the user to the map, the most important information required is the latitude and longitude. As we saw previously, Twitter provides the location of the user, and we will geocode it to find the coordinates. We will use a Ruby gem called geocoder in order to get this.

Engage thrusters

We will now find and save the location coordinates of our user's friends:

1.    We will first add the geocoder gem to our Gemfile and run bundle install:

gem 'geocoder'

2.    In the home controller, when we're saving friends, we will find the coordinates of the location using the Geocoder.coordinates method:

3.  app/controllers/home_controller.rb

4.  @friends.each do |f|

5.    location = f.location

6.    location_value = Geocoder.coordinates("#{location}")

7.    if location_value.present?

8.       Friend.get_friend_data(f,location_value,current_user.id)

9.    end

end

We also added the location_value argument that passes the coordinates to the model.

10.          We will now modify the model to add the argument and save the location values with the other values:

11.app/models/friend.rb

12.class Friend < ActiveRecord::Base

13.  geocoded_by :location

14.  def self.get_friend_data(friend,location_value,user_id)

15.     self.create_with(

16.      name: friend.name,

17.      location: friend.location,

18.      latitude: location_value.first,

19.      longitude: location_value.second).find_or_create_by(

20.      user_id: user_id, screen_name: friend.screen_name)

21.  end

end

22.          Now, we are able to save our friend's location coordinates:

23.1.9.3-p327 :001 > Friend.first

24.  Friend Load (0.5ms)  SELECT `friends`.* FROM `friends` ORDER BY `friends`.`id` ASC LIMIT 1

25. => #<Friend id: 1, name: "John Doe", screen_name: "johndoe", location: "Christchurch, New Zealand", user_id: 1, created_at: "2013-12-07 09:12:01", updated_at: "2013-12-07 09:12:01", latitude: -43.5321, longitude: 172.636>

26.          We will also use the Geocoder.coordinates method to find the location of the user. First, we will add the migration to save our OAuth credentials:

27.$rails g migration add_omniauth_and_location_to_users

28.class AddCoordinatesToUsers < ActiveRecord::Migration

29.  def change

30.    add_column :users, :latitude, :string

31.    add_column :users, :longitude, :string

32.  end

33.end

34.app/model/user.rb

35.class User < ActiveRecord::Base

36.  def self.create_with_omniauth(auth)

37.    location = auth["info"]["location"] || ""

38.    user_location = Geocoder.coordinates("#{location}")

39.    create! do |user|

40.      user.provider = auth["provider"]

41.      user.uid = auth["uid"]

42.      user.name = auth["info"]["name"] || ""

43.      user.address = auth["info"]["location"] || ""

44.      user.avatar = auth["info"]["image"] || ""

45.      user.oauth_token = auth["credentials"]["token"] || ""

46.      user.oauth_secret = auth["credentials"]["secret"] || ""

47.      user.latitude = user_location.first

48.      user.latitude = user_location.second

49.    end

50.  end

end

Note that in order to do this step, you need to add the latitude and longitude columns to the database. The users saved will be as follows:

1.9.3-p327 :002 > User.first

  User Load (0.6ms)  SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1

 => #<User id: 1, provider: "twitter", uid: "415386785", name: "Saurabh Bhatia", OAuth_token: "415386785-URRXAJQSyyJ1FkJQt2eSyg4hpXajoAj6PxVpzUPI", OAuth_expires_at: nil, created_at: "2013-12-07 09:11:49", updated_at: "2013-12-07 09:11:49", address: "Zhubei City, Taiwan", avatar: "http://pbs.twimg.com/profile_images/3408461966/ec48...", OAuth_secret: "oIx2ddTLd19vVj8i5xNYcxX6gtqlXu6WY14RFSXywDZJD", latitude: "121.0119444", longitude: "24.8333333">

Objective complete – mini debriefing

We used a Ruby-based geocoder API gem called geocoder. After we added and bundled it, we used the Geocoder.coordinates method to fetch the coordinates of the user's location and friends' location. In order to save it, we added the latitude and longitude columns in our friends table.

Another method that we can use to fetch and save the location coordinates of a friend is shown in the following code:

geocoded_by :location

The preceding method will run on an after_save callback to fetch the coordinates of a location. The method also fires an update query to save the latitude and longitude values in the database.

We will use the location coordinates in the upcoming tasks for various uses, such as creating markers on the Google map, and the geocoder API to create points of interest in our app.

Passing Twitter data to the Google Maps API using Rails

Now, we already have the Twitter data of the user, the data of user's friends, and also their location coordinates. From here on, we need to prepare the data to be displayed on the Google map. We need to display multiple markers on the map and associate our data with the markers.

Engage thrusters

In this task, we will prepare the data for the map:

1.    We will start by creating a controller for the map. This controller will be responsible for passing the data required for the map to the Google Maps JavaScript API:

2.  tweetmap$ rails g controller map_display index

3.    The markers include four types of data: the screen name, the name, the latitude, and longitude. Before that, we will initiate a blank array:

4.  app/controllers/map_display_controller.rb

5.  class MapDisplayController < ApplicationController

6.    def index

7.      @markers = []

8.    end

end

9.    We will first find all the friends of the current user:

10.app/controllers/map_display_controller.rb

11.class MapDisplayController < ApplicationController

12.  def index

13.    @markers = []

14.    @friends = current_user.friends

15.  end

end

16.          We will loop over the friends data and call the screen_name, name, latitude, and longitude from it. With each loop iteration, we will add each record to the loop. We will also create a helper method to generate the marker data:

17.app/helper/map_display_helper.rb

18.module MapDisplayHelper

19.  def get_marker_data(screen_name, name) "<strong>Twit:</strong>#{screen_name}<br/><strong>Name:</strong> Name: #{name}"

20.  end

21.end

22. 

23.app/controllers/map_display_controller.rb

24.class MapDisplayController < ApplicationController

25.  def index

26.    @markers = []

27.    @friends = current_user.friends

28.    @friends.each do |f|

29.         marker_data = get_marker_data(f.screen_name, f.name)

30.       @markers << [marker_data, f.latitude, f.longitude]

31.    end

32.  end

end

33.          Our final data will be an array of arrays with three keys; data to be displayed in the information box of the marker, the latitude, and the longitude:

[["<strong>Twitter Handle:...ong> Name: John Doe1", -43.5321, 172.636], ["<strong>Twitter Handle:...ong> Name: John Doe2", -38.6656, 178.034], ["<strong>Twitter Handle:...g> Name: John Doe3", -37.8141, 144.963], ["<strong>Twitter Handle:.../strong> Name: John Doe4", 37.7749, -122.419], ["<strong>Twitter Handle:...rong> Name: John Doe5", 37.7141, -122.25], ["<strong>Twitter Handle:...rong> Name: John Doe6", 23.6978, 120.961], ["<strong>Twitter Handle:...> Name: John Doe7", 19.076, 72.8777], ["<strong>Twitter Handle:.../strong> Name: John Doe8", 30.2301, -93.0122], ["<strong>Twitter Handle:...me: John Doe9", 22.3964, 114.109], ["<strong>Twitter Handle:... Name: John Doe10", 40.7124, -74.0087], ["<strong>Twitter Handle:...trong> Name: John Doe11", 37.7749, -122.419], ["<strong>Twitter Handle:...g> Name: John Doe12", 52.52, 13.405], ["<strong>Twitter Handle:...e: John Doe13", 35.6528, -97.4781], ["<strong>Twitter Handle:...trong> Name: John Doe14", 39.9626, -76.7277], ["<strong>Twitter Handle:...rong> Name: John Doe15", 32.2617, 76.3068], ["<strong>Twitter Handle:...strong> Name: John Doe16", -37.8141, 144.963]]

34.          The following screenshot shows the preceding data where the Firebug extension of the Chrome browser is used. Check the location variable:

Engage thrusters

Objective complete – mini debriefing

Google Maps requires the marker data to be sent as a hash. The JavaScript reads and understands the data in a particular format. We collected the data we've stored in the database and created a hash such that it can be passed directly to Google Maps. Google Maps will treat this data as the array of markers:

[marker_data, f.latitude, f.longitude]

The first field in the array will be picked up, converted into HTML, and used for the information box. We added the HTML containing the Twitter handle and username to display the data properly in the Google Maps information box. Then, we added the latitude and longitude of the user's friend. The second and third fields are the latitude and longitude on which the marker is supposed to be pinned and centered. We will display these markers on the map now.

Displaying friends on the map using the Google API

We now have the data in the format that is ready for the Google map. We will use Google Maps v3, the JavaScript API, in order to generate the map and display the markers. We will use the gmaps4rails gem but to a very limited capacity. We could use it to generate the entire map. However, considering our scenario, the JavaScript API looks like a better choice. So, we will use the gmaps4rails gem to load the basic JavaScript of Google Maps in the asset pipeline.

Engage thrusters

In the following steps, we will create a map and display our friend's data on it using the Google Maps JavaScript API:

1.    Add the gmaps4rails gem to the Gemfile and run bundle install:

2.  Gemfile

gem 'gmaps4rails', :github =>'apneadiving/Google-Maps-for-Rails'

3.    We will then load the Google Maps v3 JavaScript in our asset pipeline:

4.  app/assets/javascripts/application.js

5.  //= require jquery

6.  //= require jquery_ujs

7.  //= require twitter/bootstrap

8.  //= require underscore

9.  //= require gmaps/google

10.//= require turbolinks

//= require_tree

Tip

Google Maps for the Rails JavaScript has been rewritten using CoffeeScript and depends on underscore.js. Hence, it is essential to load underscore.js as a dependency.

We will also load the necessary dependencies in our view file:

app/views/map_display/index.html.erb

<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>

11.          We need these two files in order to call the geometry.js and Google Maps JavaScript API and Google Maps utility to generate a marker cluster on the map.

12.          In order to call the markers data we generated in our previous task, we will initiate a variable in the JavaScript:

13.app/views/map_display/index.html.erb

14.<script>

15.    var locations = <%=raw @markers %>;

</script>

16.          Initiate a script and bind it to an element with the ID map. We will tie this to a div element. We will center the map at (0, 0), that is, at the center of the world:

17.app/views/map_display/index.html.erb

18.<script>

19.   var locations = <%=raw @markers %>;

20.   var map = new google.maps.Map(document.getElementById('map'), {

21.      zoom: 2,

22.      center: new google.maps.LatLng(0, 0),

23.      mapTypeId: google.maps.MapTypeId.ROADMAP

24.    });

</script>

25.          We will now create a marker and assign our latitude and longitude values to the marker in a loop. We will also set the content for the information window on each marker:

26.app/views/map_display/index.html.erb

27.<script>

28.var marker, i;

29.    for (i = 0; i < locations.length; i++) {

30.      marker = new google.maps.Marker({

31.        position: new google.maps.LatLng(locations[i][1], locations[i][2]),

32.        map: map

33.      });

34.      google.maps.event.addListener(marker, 'click', (function(marker, i) {

35.        return function() {

36.          infowindow.setContent(locations[i][0]);

37.          infowindow.open(map, marker);

38.        }

39.      })(marker, i));

40.    }

</script>

41.          The final script with all the dependencies looks like the following code:

42.app/views/map_display/index.html.erb

43.<script src="//maps.google.com/maps/api/js?v=3.13&sensor=false&libraries=geometry" type="text/javascript"></script>

44. 

45.<script src='//google-maps-utility-library-v3.googlecode.com/svn/tags/markerclustererplus/2.0.14/src/markerclusterer_packed.js' type='text/javascript'></script>

46.<script>

47.    var locations = <%=raw @markers %>;

48.    var map = new google.maps.Map(document.getElementById('map'), {

49.      zoom: 2,

50.      center: new google.maps.LatLng(0, 0),

51.      mapTypeId: google.maps.MapTypeId.ROADMAP

52.    });

53.    var infowindow = new google.maps.InfoWindow();

54.    var marker, i;

55.    for (i = 0; i < locations.length; i++) {

56.      marker = new google.maps.Marker({

57.        position: new google.maps.LatLng(locations[i][1], locations[i][2]),

58.        map: map

59.      });

60.      google.maps.event.addListener(marker, 'click', (function(marker, i) {

61.        return function() {

62.          infowindow.setContent(locations[i][0]);

63.          infowindow.open(map, marker);

64.        }

65.      })(marker, i));

66.    }

</script>

67.          Finally, we will display the map in the div element. We will bind the JavaScript to the div element using the ID name map:

68.app/views/map_display/index.html.erb

<div id="map" style='width: 1200px; height: 600px;'></div>

Objective complete – mini debriefing

In this task, we created the JavaScript for generating the Google map and plotting all the data in the form of markers for us. We used the gmap4rails gem to load the Google Maps JavaScript API into our asset pipeline. Google Maps for Rails is wrapped on top of the Google Maps JavaScript API. It is completely rewritten in Coffee and underscore.js. Underscore.js is a library that provides a set of specialized functional programming helper methods. Some of the methods that Google Maps for Rails uses are as follows:

·        _.extend

·        _.map

·        _.isFunction

·        _.each

·        _.isObject

Then, we defined some geometry and marker-specific JavaScript in our views. We initiated a map and associated it with an element with the map ID. Then, the loop will read the collection of marker data represented as @marker variable in our map_display_controllerand call the location coordinates from there. The locations[i][1] and locations[i][2], the second and the third element of the locations array are called as the collection is looped over:

var marker, i;

    for (i = 0; i < locations.length; i++) {

      marker = new google.maps.Marker({

        position: new google.maps.LatLng(locations[i][1], locations[i][2]),

        map: map

      });

Then, we will pass the first value of the array to the information window on the map and bind it to the click event:

google.maps.event.addListener(marker, 'click', (function(marker, i) {

        return function() {

          infowindow.setContent(locations[i][0]);

          infowindow.open(map, marker);

        }

      })(marker, i));

    }

We used the raw tag to pass the marker data to the Google Maps JavaScript. By default, Rails escapes the executable script within the objects:

<%=raw @markers %>

The raw tag is equivalent to the html_safe tag in Rails. The difference lies in how they handle the nil object. The html_safe tag gives an exception, whereas raw gives out an empty string in return. Also, in some cases raw is susceptible to an XSS attack. This can be a use case where we have a text area and the attacker inserts an executable JavaScript in it. We should avoid the use of raw in those cases. In our case, raw is handled to output the data from a hash that we build. The marker data is now being displayed in blurb on the marker as shown in the following screenshot:

Objective complete – mini debriefing

Creating points of interest – filter users based on their location

Grouping similar information on the map according to a specific criteria is called points of interest. This is a term used for markers or points on the maps that can be categorized or grouped together. We will use locations as the points of interest in our application. We will call all the locations in our system and search the friends according to it. We will use the geocoder API to do this.

Engage thrusters

We will create location-based filters for our users in this task:

1.    The geocoder gem has a method called near, which takes the location string as the parameter and runs a spatial query on the database:

2.  1.9.3p327 :001 > user = User.first

3.  1.9.3p327 :002 > user.friends.near("NY")

Engage thrusters

4.    We got six results when we searched for the term NY:

5.  1.9.3p327 :003 > user.friends.near("NY").length

6.    Friend Load (1.6ms)  SELECT friends.*, 3958.755864232 * 2 * ASIN(SQRT(POWER(SIN((40.7143528 - friends.latitude) * PI() / 180 / 2), 2) + COS(40.7143528 * PI() / 180) * COS(friends.latitude * PI() / 180) * POWER(SIN((-74.00597309999999 - friends.longitude) * PI() / 180 / 2), 2))) AS distance, CAST(DEGREES(ATAN2( RADIANS(friends.longitude - -74.00597309999999), RADIANS(friends.latitude - 40.7143528))) + 360 AS decimal) % 360 AS bearing FROM `friends`  WHERE `friends`.`user_id` = 1 AND (friends.latitude BETWEEN 40.4248892337783 AND 41.0038163662217 AND friends.longitude BETWEEN -74.38786578682475 AND -73.62408041317524 AND 3958.755864232 * 2 * ASIN(SQRT(POWER(SIN((40.7143528 - friends.latitude) * PI() / 180 / 2), 2) + COS(40.7143528 * PI() / 180) * COS(friends.latitude * PI() / 180) * POWER(SIN((-74.00597309999999 - friends.longitude) * PI() / 180 / 2), 2))) <= 20)  ORDER BY distance ASC

7.    Now, as we know that we can find people based on their locations, we will add the location of the friend as a parameter called place in our query:

8.  app/controllers/map_display_controller.rb

9.  if params[:place].present?

10.   @friends = current_user.friends.near(params[:place])

11.   @friends.each do |f|

12.   end

end

13.          So, our final method looks like the following code:

14.app/controllers/map_display_controller.rb

15.class MapDisplayController < ApplicationController

16.  include MapDisplayHelper

17.  def index

18.    @markers = []

19.    if params[:place].present?

20.      @friends = current_user.friends.near(params[:place])

21.      @friends.each do |f|

22.         marker_data = get_marker_data(f.screen_name, f.name)

23.       @markers << [marker_data, f.latitude, f.longitude]

24.      end

25.    else

26.      @friends = current_user.friends

27.      @friends.each do |f|

28.         marker_data = get_marker_data(f.screen_name, f.name)

29.       @markers << [marker_data, f.latitude, f.longitude]

30.      end

31.    end

32.  end

end

33.          In our view, we need to pass the place parameter to the controller method:

34.app/views/map_display/index.html.erb

<div class="col-lg-12"><% @friends.each do |f|%><h3 id="type-blockquotes"><%= link_to "#{f.location}", map_display_index_path(:place =>"#{f.location}")%></h3><%end %></div>

35.          The loop will generate a list of location links with the place parameter. The following screenshot displays the list of places as links:

Engage thrusters

36.          When we click on one of the preceding locations, we will get a map with the filtered values and the location values:

Engage thrusters

Objective complete – mini debriefing

In the previous task, we used the geocoder API to search our database. It extends spatial queries in the database to search the friends table according to the location coordinates. The near query first finds the coordinates of the location according to which it is searched. Then, it converts the latitude and longitude to degrees and radians in order to match it to the location coordinates of the friends. The following is an example of geo query we can run using the geocoder:

Objective complete – mini debriefing

The geocoder API provides a host of other features, such as finding the distance between two friends and the nearbys query. The nearbys query is run as follows:

Friend.last.nearbys(30)

Objective complete – mini debriefing

We can also find distance in miles between two friends. The results are as shown in the following screenshot:

Geocoder::Calculations.distance_between(friend1, friend2)

Objective complete – mini debriefing

The following screenshot shows the result of a near query when clicked on San Fransisco:

Objective complete – mini debriefing

Mission accomplished

We have successfully created a fun app that will map our friends on Twitter. We can broadly divide what we did into four parts:

·        Used OmniAuth to sign up and log in with Twitter

·        Created sessions with Twitter and maintained our user in the session

·        Used Twitter v1.1 API and the twitter gem to call the users and their friends' data

·        Used the Ruby geocoding API to find the location coordinates of each friend and used mapping to display these friends on the Google map using Google Maps v3

Hotshot challenges

We can still have a lot of fun with these APIs:

·        Display the last tweet of each user in the information window

·        Display the user avatar in the information window

·        Change the location filter to a checkbox

·        Use jQuery to send the parameters to the controller

·        Find the distance between two friends