Build a location sharing app with Ruby on Rails and the Google Maps API
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:
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.
Create a new app by selecting Channels apps on the sidebar and clicking Create Channels app button on the bottom of the sidebar:
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:
Click the App Keys tab to retrieve your 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:
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:
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.
Displaying the map via the share link
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:
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.
27 May 2018
by Christian Nwamba