Back to search

Build a location sharing app with Ruby on Rails and the Google Maps API

  • Christian Nwamba
May 27th, 2018
To follow this tutorial, you will need Ruby and Rails installed on your machine. A basic understanding of Ruby, PostgreSQL and CoffeeScript will help you get the most out of this tutorial.

Introduction

Realtime maps have become very popular nowadays. The ability to track something or someone realtime has been incorporated into lots of apps, especially in the transportation and delivery industry. In this post, we’ll be building a realtime location sharing app using Ruby and Pusher.

Here’s a sneak-peak into what we’ll be building:

location-sharing-ruby-demo

Prerequisites

A basic understanding of Ruby, CoffeeScript and PostgreSQL will help you get the best out of this tutorial. You can check the PostgreSQL, Ruby and Rails documentation for installation steps.

Before we start building our app, let’s ensure we have Ruby and Rails installed. Run the following command in your terminal to confirm you have both Ruby and Rails installed:

    $ ruby -v      // 2.1 or above
    $ rails -v   // 4.2 or above

Pusher account setup

Since we’ll be relying on Pusher for realtime functionality, let’s head over to Pusher and create a free account.

ruby-create-pusher-account

Create a new app by selecting Channels apps on the sidebar and clicking Create Channels app button on the bottom of the sidebar:

ruby-channels-dashboard

Configure an app by providing basic information requested in the form presented. You can also choose the environment you intend to integrate with Pusher, to be provided with some boilerplate setup code:

ruby-new-pusher-app

Click the App Keys tab to retrieve your keys

ruby-app-keys

Now that we have our Pusher account, let’s setup our application.

Setting up the application

Open your terminal and run the following Rails commands to create our demo application:

    # create a new Rails application
    $ rails new pusher-locations -T --database=postgresql

Go ahead and change directory into the newly created pusher-locations folder:

    # change directory
    $ cd pusher-locations

In the root of your pusher-locations directory, open your Gemfile and add the following gems:

    # Gemfile

    gem 'bootstrap', '~> 4.1.0'
    gem 'jquery-rails'
    gem 'pusher'
    gem 'figaro'

In your terminal, ensure you are in the pusher-locations project directory and install the gems by running:

    $ bundle install

Setting up the database

To get our app up and running, we’ll create a database for it to work with. You can check out this article on how to create a Postgres database and an associated user and password.

Once you have your database details, in your database.yml file, under the development key, add the following code:

    # config/database.yml

    ...
    development:
      <<: *default
      database: pusher-locations_development // add this line if it isn't already there
      username: database_user // add this line
      password: user_password // add this line
    ...

Ensure that the username and password entered in the code above has access to the pusher-locations_development database. After that, run the following code to setup the database:

    # setup database
    $ rails db:setup

Bootstrap the application

With our database all set up, we’ll go ahead and create our models and controllers. In your terminal, while in the project’s directory, run the following code:

    # generate a trip model
    $ rails g model trip name:string uuid:string

    # generate a checkin model
    $ rails g model checkin trip:references lat:decimal lng:decimal

    # generate a trips controller with index, create and show views and actions
    $ rails g controller trips index create show

    # generate a checkins controller with a create action and view
    $ rails g controller checkins create

Next, we’ll update our trip model with its association and some methods. In your trip model file, add the following:

    # app/models/trip.rb

    class Trip < ApplicationRecord
      before_create :set_uuid
      has_many :checkins # trip model's association with the checkins model

      # a method that creates a random uuid for each trip before its created
      def set_uuid
        self.uuid = SecureRandom.uuid
      end

      # a method that generates a custom JSON output for our trip objects
      def as_json(options={})
        super(
          only: [:id, :name, :uuid],
          include: { checkins: { only: [:lat, :lng, :trip_id] } }
        )
      end
    end

We’re now ready to run our database migrations and see our new app. In your terminal, run the following code:

    # run database migrations
    $ rails db:migrate

After running migrations, start the development server on your terminal by running rails s. Visit http://localhost:3000 in your browser to see your brand new application:

youre-on-rails

Setting up a Google Maps project

We’ll be using Google Maps to render our map. This documentation will guide you through registering a project in the Google API Console and activating the Google Maps JavaScript API. Remember to grab the API key that will be generated for you after registering.

Building the homepage

Now that we have everything we need to build our app, let’s build out our homepage. We’ll set our root page to the trips controller index page and add some resource routes. In your routes file, add the following code:

    # config/routes.rb

    Rails.application.routes.draw do
      resources :trips do
        resources :checkins, only: :create
      end
      root 'trips#index'
    end

Next, we’ll require Bootstrap and add some styling. Add the following code to your application.js file, all before the last line:

    # app/assets/javascripts/application.js

    .....
    //= require jquery3 # add this line
    //= require popper # add this line
    //= require bootstrap # add this line
    //= require_tree .

Rename your application.css file to application.scss and add the following code:

    # app/assets/stylesheets/application.scss

     @import "bootstrap";
     @import url('https://fonts.googleapis.com/css?family=Dosis');
     body {
      font-family: 'Dosis', sans-serif;
     }
     #map {
      width: 100%;
      height: 42rem;
    }

In our app, we’ll be interacting with our users via a form in the header. Let’s create a partial where our header will live. We’ll render this partial on the homepage. In our layouts folder, create a _header.html.erb file and add the following markup:

    # app/views/layouts/_header.html.erb

    <header class="bg-warning">
      <nav class="navbar navbar-expand-sm navbar-light sticky-top">
      <a class="navbar-brand" href="/">Pusher Location</a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#mobile-menu" aria-controls="mobile-menu" aria-expanded="false" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="mobile-menu">
          <form class="form-inline name-form">
            <input class="form-control mr-sm-2" type="name" name="name" required placeholder="Enter your name" aria-label="name">
            <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Share Location</button>
          </form>
          <div class="share-url"></div>
        </div>
      </nav>
    </header>

Now, we’ll render our header partial and add the HTML markup for our homepage:

    # app/views/trips/index.html.erb

    <%= render 'layouts/header' %>
    <div id="map"></div>

With this, we should have a homepage that looks like this:

location-sharing-ruby-homepage

If you encounter any error related to application.html.erb, in config/boot.rb, change the ExecJS runtime from Duktape to Node.

    # config/boot.rb
    ENV['EXECJS_RUNTIME'] ='Node'

Displaying users location on a map

Once our users submit a name, we request their location, save it to the database and then render a map showing that location. We’ll check their current location every five seconds and update the map with it. To make use of the Google Maps API, we need to add the Google Maps script to the head of our application.html.erb file. We’ll also add the Pusher library script.

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

    ...
      <head>
        <title>PusherLocations</title>
        <%= csrf_meta_tags %>
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
        <script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY"></script> # add Google Maps script
        <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
        <script src="https://js.pusher.com/4.1/pusher.min.js"></script> # add Pusher script
        <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
      </head>
      ...

Ensure you add your API key to the Google Maps script.

Now, in your trips.coffee file, add the following code:

    # app/assets/javascripts/trips.coffee

    $(document).ready =>
      tripId = ''
      startingPoint = {}
      # function for converting coordinates from strings to numbers
      makeNum = (arr) ->
        arr.forEach (arr) ->
          arr.lat = Number(arr.lat)
          arr.lng = Number(arr.lng)
          return
        arr

      # function for creating a new trip
      saveTrip = (positionData) ->
        token = $('meta[name="csrf-token"]').attr('content')
        $.ajax
          url: '/trips'
          type: 'post'
          beforeSend: (xhr) ->
            xhr.setRequestHeader 'X-CSRF-Token', token
            return
          data: positionData
          success: (response) ->
            tripId = response.id
            url = """#{window.location.protocol}//#{window.location.host}/trips/#{response.uuid}"""
            initMap()
            $('.name-form').addClass('collapse')
            $('.share-url').append """
              <h6 class="m-0 text-center">Hello <strong>#{response.name}</strong>, here's your location sharing link: <a href="#{url}">#{url}</a></h6>
            """
            getCurrentLocation()
            return
        return

      # function for getting the user's location at the begining of the trip
      getLocation = (name) ->
        if navigator.geolocation
          navigator.geolocation.getCurrentPosition (position) ->
            coord = position.coords
            timestamp = position.timestamp
            data =
              lat: coord.latitude,
              lng: coord.longitude,
              name: name
            startingPoint = data
            saveTrip data
        return

      # function for rendering the map
      initMap = ->
        center = 
          lat: startingPoint.lat
          lng: startingPoint.lng
        map = new (google.maps.Map)(document.getElementById('map'),
          zoom: 18
          center: center)
        marker = new (google.maps.Marker)(
          position: center
          map: map)
        return

      # function for updating the map with the user's current location
      updateMap = (checkin) ->
        lastCheckin = checkin[checkin.length - 1]
        center = 
          lat: startingPoint.lat
          lng: startingPoint.lng
        map = new (google.maps.Map)(document.getElementById('map'),
          zoom: 18
          center: center)
        marker = new (google.maps.Marker)(
          position: lastCheckin
          map: map)
        flightPath = new (google.maps.Polyline)(
          path: checkin
          strokeColor: '#FF0000'
          strokeOpacity: 1.0
          strokeWeight: 2)
        flightPath.setMap map
        setTimeout(getCurrentLocation, 5000)
        return

      # function for updating the database with the user's current location
      updateCurrentLocation = (tripData, id) ->
        token = $('meta[name="csrf-token"]').attr('content')
        $.ajax
          url: "/trips/#{id}/checkins"
          type: 'post'
          beforeSend: (xhr) ->
            xhr.setRequestHeader 'X-CSRF-Token', token
            return
          data: tripData
          success: (response) ->
            return
        return

      # function for finding the user's current location
      getCurrentLocation = ->
        navigator.geolocation.getCurrentPosition (position) ->
          data =
            lat: position.coords.latitude,
            lng: position.coords.longitude
          updateCurrentLocation(data, tripId)
        return

      # run this block of code if we're on the homepage
      unless location.pathname.startsWith('/trips')
        # when a user submits their name, get their name and call the function to get their location
        $('.name-form').on 'submit', (event) ->
          event.preventDefault()
          formData = $(this).serialize()
          name = formData.split('=')[1]
          data = getLocation(name)
          return

In the code above, we get our user’s name and call the getLocation function. The getLocation function gets the user’s location and saves it to the database.

If the user’s location is saved successfully, we render the link for sharing their location on the header, render the map on the page by calling the initMap function and then call the getCurrentLocation function to monitor their current location and update the map.

Also, add the following code to your trips and checkins controllers respectively:

    # app/controllers/trips_controller.rb

    class TripsController < ApplicationController
      def index
      end

      # function for creating a new trip
      def create
        @trip = Trip.new(trip_params)
        @trip.checkins.build(lat: params[:lat], lng: params[:lng])
        render json: @trip.as_json if @trip.save
      end

      # function for showing a trip
      def show
        @trip = Trip.find_by(uuid: params[:id])
      end

      private
        def trip_params
          params.permit(:name)
        end
    end
    # app/controllers/checkins_controller.rb

    class CheckinsController < ApplicationController
      def create
        @checkin = Checkin.new(checkin_params)
        render json: @checkin.as_json(only: [:lat, :lng, :trip_id]) if @checkin.save
      end

      private
        def checkin_params
          params.permit(:trip_id, :lat, :lng)
        end
    end

If you have followed the tutorial up to this point, if you refresh the homepage, you should be able to enter your name and see your location in a map on the page. Remember to allow the page to access your location.

When we share the link with other users, we want them to see the user’s current location on a map. The share link contains the UUID for that current trip. When the page loads, we attach the longitude and latitude data to a hidden field and use it to render the user’s location on a page. Add the following code to the show.html.erb file:

    # app/views/trips/show.html.erb

    <div id="map"></div>
    <%= hidden_field_tag 'lat', @trip.checkins[0][:lat]  %> # hidden field holding the latitude information
    <%= hidden_field_tag 'lng', @trip.checkins[0][:lng]  %># hidden field holding the longitude information

Add the following code to the trips.coffee file:

    # app/assets/javascripts/trips.coffee

      ......
      # run this code if we're on the trips page
      if location.pathname.startsWith('/trips')
        showLat = $('#lat').val() # get the user's latitude from the hidden field
        showLng = $('#lng').val() # get the user's longitude from the hidden field
        data =
          lat: Number(showLat),
          lng: Number(showLng)
        startingPoint = data
        initMap()

In the code above, when we’re on the trips page, we get the longitude and latitude from the hidden input field. We then call the initMap function to render the map on the page.

With this, we should be able to view the user’s location via the share link.

Realtime location sharing with Pusher

Now that other users can view user’s location on the map, it’s time for us to update the user’s location in realtime. To achieve this, every time a user’s current location is updated, we publish it and on the frontend of our app, we update the map with the new coordinates.

First, let’s initialize our Pusher client. In the config/initializers folder, create a pusher.rb file and add the following code:

    # config/initializers/pusher.rb

    require 'pusher'
    Pusher.app_id = ENV["PUSHER_APP_ID"]
    Pusher.key = ENV["PUSHER_KEY"]
    Pusher.secret = ENV["PUSHER_SECRET"]
    Pusher.cluster = ENV["PUSHER_CLUSTER"]
    Pusher.logger = Rails.logger
    Pusher.encrypted = true

Next, run figaro install in your terminal. It will generate an application.yml file. In the application.yml file add your Pusher keys:

    # config/application.yml

    PUSHER_APP_ID: 'xxxxxx'
    PUSHER_KEY: 'xxxxxxxxxxxxxxxxx'
    PUSHER_SECRET: 'xxxxxxxxxxxxxx'
    PUSHER_CLUSTER: 'xx'

With our Pusher client set up, in our checkin model, we’ll add an after create callback to publish a user’s coordinates after they’re saved. Add the following code in the checkin model:

    # app/models/checkin.rb

    class Checkin < ApplicationRecord
      belongs_to :trip
      after_create :notify_pusher

      # method to publish a user's current location
      def notify_pusher
        Pusher.trigger('location', 'new', self.trip.as_json)
      end
    end

Updating the UI

Now that our server is publishing coordinate updates, we’ll grab them on the client side and update the map with it. Lets rename our trips.coffee file to trips.coffee.erb and replace the code there with the following:

    # app/assets/javascripts/trips.coffee.erb

    $(document).ready =>
      tripId = ''
      startingPoint = {}
      isOwner = false
      map = null

      <%# function for converting coordinates to numbers %>
      makeNum = (arr) ->
        arr.forEach (arr) ->
          arr.lat = Number(arr.lat)
          arr.lng = Number(arr.lng)
          return
        arr

      <%# function for creating a new trip %>
      saveTrip = (positionData) ->
        isOwner = true
        token = $('meta[name="csrf-token"]').attr('content')
        $.ajax
          url: '/trips'
          type: 'post'
          beforeSend: (xhr) ->
            xhr.setRequestHeader 'X-CSRF-Token', token
            return
          data: positionData
          success: (response) ->
            tripId = response.id
            url = """#{window.location.protocol}//#{window.location.host}/trips/#{response.uuid}"""
            initMap()
            $('.name-form').addClass('collapse')
            $('.share-url').append """
              <h6 class="m-0 text-center">Hello <strong>#{response.name}</strong>, here's your location sharing link: <a href="#{url}">#{url}</a></h6>
            """
            getCurrentLocation()
            return
        return

      <%# function for getting the user's location at the begining of the trip %>
      getLocation = (name) ->
        if navigator.geolocation
          navigator.geolocation.getCurrentPosition (position) ->
            coord = position.coords
            timestamp = position.timestamp
            data =
              lat: coord.latitude,
              lng: coord.longitude,
              name: name
            startingPoint = data
            saveTrip data
        return

      <%# function for rendering the map %>
      initMap = ->
        center = 
          lat: startingPoint.lat
          lng: startingPoint.lng
        map = new (google.maps.Map)(document.getElementById('map'),
          zoom: 18
          center: center)
        marker = new (google.maps.Marker)(
          position: center
          map: map)
        return

      <%# function for updating the map with the user's current location %>
      updateMap = (checkin) ->
        console.log checkin
        lastCheckin = checkin[checkin.length - 1]
        center = 
          lat: startingPoint.lat
          lng: startingPoint.lng
        map = new (google.maps.Map)(document.getElementById('map'),
          zoom: 18
          center: center)
        marker = new (google.maps.Marker)(
          position: lastCheckin
          map: map)
        flightPath = new (google.maps.Polyline)(
          path: checkin
          strokeColor: '#FF0000'
          strokeOpacity: 1.0
          strokeWeight: 2)
        flightPath.setMap map
        if isOwner
          setTimeout(getCurrentLocation, 5000)
        return

      <%# function for updating the database with the user's current location %>
      updateCurrentLocation = (tripData, id) ->
        token = $('meta[name="csrf-token"]').attr('content')
        $.ajax
          url: "/trips/#{id}/checkins"
          type: 'post'
          beforeSend: (xhr) ->
            xhr.setRequestHeader 'X-CSRF-Token', token
            return
          data: tripData
          success: (response) ->
            return
        return

      <%# function for finding the user's current location %>
      getCurrentLocation = ->
        navigator.geolocation.getCurrentPosition (position) ->
          data =
            lat: position.coords.latitude,
            lng: position.coords.longitude
          updateCurrentLocation(data, tripId)
        return

      <%# run this block of code if we're on the homepage %>
      unless location.pathname.startsWith('/trips')
        <%# when a user submits their name, get their name and call the function to get their location %>
        $('.name-form').on 'submit', (event) ->
          event.preventDefault()
          formData = $(this).serialize()
          name = formData.split('=')[1]
          data = getLocation(name)
          return

      <%# run this code if we're on the trips page %>
      if location.pathname.startsWith('/trips')
        showLat = $('#lat').val()
        showLng = $('#lng').val()
        data =
          lat: Number(showLat),
          lng: Number(showLng)
        startingPoint = data
        initMap()

      <%# subscribe Pusher client %>
      pusher = new Pusher('<%= ENV["PUSHER_KEY"] %>',
        cluster: '<%= ENV["PUSHER_CLUSTER"] %>'
        encrypted: true)
      channel = pusher.subscribe('location')
      channel.bind 'new', (data) ->
        updateMap makeNum(data.checkins)
        return
      return

In the code above, we subscribed our Pusher client to the location channel and listened for the new event. Once those events are emitted, we get the coordinates and update the map with it.

Bringing it all together

Restart the development server if it is currently running. Visit http://localhost:3000 in two separate browser tabs to test the realtime location sharing app.

In order to see the marker move realtime(without going for a walk), you’ll have to send events to the location channel. The easiest way to do this is by using the event creator on the Pusher Debug Console. Here is a sample data format that can be used to trigger an update:

    {
      "id": "1",
        "name": "John",
          "checkins": [
            { "lat": "6.5542937", "lng": "3.3665464999999997" },
            { "lat": "6.5545393", "lng": "3.3667686" },
            { "lat": "6.5550349", "lng": "3.3667605" },
            { "lat": "6.5554759", "lng": "3.3667485" }
          ]
    }

Here is an image of how the event would look like on the Pusher event creator:

location-sharing-ruby-pusher-events-dashboard

Conclusion

In this post, we have successfully created a realtime location sharing app. I hope you found this tutorial helpful and would love to apply the knowledge gained here to easily set up your own application using Pusher.

You can find the source code for the demo app on GitHub.

  • Channels

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 28 Scrutton Street, London EC2A 4RP.