Writing Hangout Apps 2 |
After learning the basics of Hangout Apps, here's a more advanced topic. We're going to learn how to use additional OAuth-Scopes and the Google APIs Javascript library to get more information into your Hangout apps.
I'm going to assume that you have read and understood my previous article because we're going to start developing from there.
The plan for this article is to develop an app which uses the Google APIs JavaScript Client to access the private Google Latitude™ data via the Latitude API and let people share their current city-based location with the other participants if they want to. The locations will be displayed using the Google Maps™ API.
You can find the full sources for the simple app we are going to build on GitHub and on Google Code.
In addition to the settings explained last time we will need to do some more things in the Google APIs console to access the other APIs we need.
In "Services" we need to enable "Google Maps API v3" to display the map and "Latitude API" to access the users location.
In "Hangouts" we need to enable additional OAuth 2.0 scopes and add "https://www.googleapis.com/auth/latitude.current.city" which allows us to request the current location of the user with city granularity. Check the Latitude API documention to see what other scopes you could request.
In the XML file we are going to load two additional scripts to access the Google Maps API and the Google APIs JavaScript Client. We will also define some HTML elements which will include the information and be used for interaction. The new parts as compared to last time are highlighted and described below.
Note: you will have to replace <YOUR_PATH> with the location where you are planning to upload the files.
<?xml version="1.0" encoding="UTF-8" ?> | |
<Module> | |
<ModulePrefs title="Hangout Demo"> | |
<Require feature="rpc"/> | |
<Require feature="views"/> | |
</ModulePrefs> | |
<Content type="html"> | |
<![CDATA[ | |
<script src="//talkgadget.google.com/hangouts/_/api/hangout.js?v=1.1"></script> | |
<script src="//www.google.com/jsapi"></script> | This includes the Google Maps API |
<link rel="stylesheet" href="<YOUR_PATH>/css/demo.css" /> | |
<h1>Hangout Location Demo</h1> | |
<div id="map"></div> | This will include the rendered map |
<div id="my_location"> | |
<p id="current_location" class="small"></p> | This will display your current location |
<button id="share" disabled="disabled">Share location</button> | Clicking will share your location with the others |
<button id="unshare" disabled="disabled">Remove location</button> | Clicking will hide your location |
</div> | |
<script src="<YOUR_PATH>/scripts/demo.js"></script> | |
<script src="//apis.google.com/js/client.js?onload=onClientReady"></script> | This loads the Google APIs JS Client. When ready this will call the onClientReady function in our demo.js script. |
]]> | |
</Content> | |
</Module> |
We're going to start with a rather empty script, defining the onClientReady function, initializing our App with some variable we will need later and waiting for the Hangouts API to be ready before doing anything else.
(function (window) { | |
"use strict"; | |
var locationApp; | |
function LocationApp() { | |
this.map = null; | map object for adding locations |
this.latlng = {}; | current location of this participants |
this.locations = {}; | shared locations of other participants |
this.api_key = "<YOUR API KEY>"; | insert your key from the API console here |
gapi.hangout.onApiReady.add(this.onApiReady.bind(this)); | waiting for API to be ready |
} | |
LocationApp.prototype.onApiReady = function (event) { | |
if (event.isApiReady === true) { | |
// ... | |
} | |
}; | |
window.onClientReady = function () { | |
locationApp = new LocationApp(); | start our app |
}; | |
}(window)); |
Once the Hangouts API is ready the first thing we are going to do is loading the Google Maps API and preparing the (empty) map.
LocationApp.prototype.onApiReady = function (event) { | |
if (event.isApiReady === true) { | |
google.load( | this loads the Google Maps Javascript API v3 |
"maps", "3", { | |
other_params: "key=" + this.api_key + "&sensor=false", | |
callback: this.mapsReady.bind(this) | this function will be called when the API is ready |
}); | |
} | |
}; | |
LocationApp.prototype.mapsReady = function () { | |
this.prepareMap(); | |
// we'll do some more stuff here soon | |
}; | |
LocationApp.prototype.prepareMap = function () { | |
var latlng, myOptions; | |
latlng = new google.maps.LatLng(0, 0); | |
myOptions = { | some options as to how the map is displayed |
zoom: 0, | see the Maps API documentation for details |
center: latlng, | |
disableDefaultUI: true, | |
zoomControl: true, | |
mapTypeId: google.maps.MapTypeId.ROADMAP | |
}; | |
this.map = new google.maps.Map( | save a reference to the map for later |
document.getElementById("map"), my Options | render the map inside our div |
); | |
}; |
After the map is prepared we're going to authenticate the Google APIs JS Client to access Latitude. The JS Client works together nicely with the Hangout, using the already existing OAuth tokens without the need for another explicit OAuth workflow for the user. That's why we already added the necessary OAuth scope in the API console.
LocationApp.prototype.mapsReady = function () { | |
var scopes; | |
this.prepareMap(); | |
scopes = [ | |
"https://www.googleapis.com/auth/plus.me", | OAuth scopes necessary for the Hangouts API |
"https://www.googleapis.com/auth/hangout.av", | . |
"https://www.googleapis.com/auth/hangout.participants", | . |
"https://www.googleapis.com/auth/latitude.current.city" | OAuth scope for accessing Latitude |
]; | |
gapi.client.setApiKey(null); | the client will automatically use the API Key |
associated with the Hangout App project | |
gapi.auth.authorize({ | this will authenticate the client using the |
client_id: null, | ClientID associated with the Hangout App project |
scope: scopes, | to access the data, the user already has |
immediate: true | granted permission to use, and will call |
}, this.handleAuthResult.bind(this)); | our function once the OAuth workflow |
}; | has finished in the background |
LocationApp.prototype.handleAuthResult = function (authResult) { | |
if (authResult) { | |
// authentication worked, we can access latitude now | |
} else { | |
document.getElementById("current_location").innerHTML = | |
"Can't determine location: not authorized"; | |
} | |
}; |
Note: after authentication you can actually use gapi.auth.getToken() to get the OAuth Access Token which you can use in any API calls, even for those that are not available in the Google APIs JS Client, provided you have requested the necessary scopes.
Once authorized we can use the Latitude API to fetch the current location for the user.
LocationApp.prototype.handleAuthResult = function (authResult) { | |
if (authResult) { | |
gapi.client.load( | This loads the necessary information to |
'latitude', 'v1', | access the Latitude API and calls our |
this.loadLocation.bind(this) | function once ready. |
); | |
} else { | |
document.getElementById("current_location").innerHTML = | |
"Can't determine location: not authorized"; | |
} | |
}; | |
LocationApp.prototype.loadLocation = function () { | |
var request; | |
request = gapi.client.latitude.currentLocation.get( | Here we create a request to get |
{"granularity": "city"} | the user's current city location. |
); | See the Latitude API documentation for details. |
request.execute(function (loc) { | Executing the request will return a JSON object |
var wp; | |
if (loc.error) { | with either an error |
document.getElementById("current_location").innerHTML = | |
"Can't determine location: " + loc.error.message; | |
} else { | |
document.getElementById("current_location").innerHTML = | or with the latest geographic coordinate |
"Current location: " + loc.latitude + " " + loc.longitude; | with latitude and longitude. |
this.latlng.lat = loc.latitude; | Saving the coordiantes |
this.latlng.lng = loc.longitude; | for later use. |
wp = new google.maps.LatLng(this.latlng.lat, this.latlng.lng); | Here we construct a Google maps marker |
this.locations[gapi.hangout.getParticipantId()] = | to display the current location |
new google.maps.Marker({position: wp, map: this.map}); | to the user. |
} | |
}.bind(this)); | |
}; |
Once we have the location we want to allow the user to share (and unshare) it with the participants of the hangout. For this we need to add some functions that interact with the shared state.
LocationApp.prototype.loadLocation = function () { | |
var request; | |
request = gapi.client.latitude.currentLocation.get( | |
{"granularity": "city"} | |
); | |
request.execute(function (loc) { | |
var wp, button; | |
if (loc.error) { | |
document.getElementById("current_location").innerHTML = | |
"Can't determine location: " + loc.error.message; | |
} else { | |
document.getElementById("current_location").innerHTML = | |
"Current location: " + loc.latitude + " " + loc.longitude; | |
this.latlng.lat = loc.latitude; | |
this.latlng.lng = loc.longitude; | |
wp = new google.maps.LatLng(this.latlng.lat, this.latlng.lng); | |
this.locations[gapi.hangout.getParticipantId()] = | |
new google.maps.Marker({position: wp, map: this.map}); | |
button = document.getElementById("share"); | Enable "Share"-button since we |
button.removeAttribute("disabled"); | have a location to share now |
button.onclick = this.shareLocation.bind(this); | and bind click event handlers |
button = document.getElementById("unshare"); | to the buttons. |
button.onclick = this.unshareLocation.bind(this); | |
} | |
}.bind(this)); | |
}; | |
LocationApp.prototype.shareLocation = function () { | |
var button; | |
button = document.getElementById("share"); | Just some DOM manipulations |
button.setAttribute("disabled", "disabled"); | to update the buttons. |
button = document.getElementById("unshare"); | |
button.removeAttribute("disabled"); | |
gapi.hangout.data.setValue( | |
gapi.hangout.getParticipantId(), | Participant ID is used as unique key |
JSON.stringify(this.latlng) | Shared state only accepts string values |
); | |
}; | |
LocationApp.prototype.unshareLocation = function () { | |
var button; | |
button = document.getElementById("unshare"); | Just some DOM manipulations |
button.setAttribute("disabled", "disabled"); | to update the buttons. |
button = document.getElementById("share"); | |
button.removeAttribute("disabled"); | |
gapi.hangout.data.clearValue( | Remove the shared value |
gapi.hangout.getParticipantId() | associated with the current user. |
); | |
}; | |
And of course since we are sharing locations we also need functions to handle shared locations by other participants. Because we need the map to display those locations we will initialize those functions after preparing the maps.
LocationApp.prototype.mapsReady = function () { | |
(...) | |
gapi.hangout.data.onStateChanged.add( | You should already know this |
this.onStateChanged.bind(this) | event handler from last time. |
); | |
this.showAllLocations(); | When we first start the app, we also |
want to display already shared locations. | |
}; | |
LocationApp.prototype.onStateChanged = function (event) { | |
var i, l, id, latlng; | |
l = event.addedKeys.length; | addedKeys includes all newly shared locations |
for (i = 0; i < l; i++) { | |
id = event.addedKeys[i].key; | The key is the unique participant ID |
of the other participant | |
latlng = JSON.parse(event.addedKeys[i].value); | Also see above in the shareLocation function |
this.addLocation(id, latlng.lat, latlng.lng); | |
} | |
l = event.removedKeys.length; | removedKeys includes the keys, i.e. IDs of |
for (i = 0; i < l; i++) { | the participants who removed their location |
id = event.removedKeys[i]; | |
this.removeLocation(id); | |
} | |
}; | |
LocationApp.prototype.addLocation = function (id, lat, lng) { | |
var latlng, info, image_url, image, participant; | |
if (id !== gapi.hangout.getParticipantId()) { | prevent the own location from being added again |
if (!this.locations[id]) { | only add really new locations |
participant = gapi.hangout.getParticipantById(id); | get profile information about the participant |
if (participant && participant.person) { | |
info = participant.person.displayName; | |
image_url = participant.person.image.url; | |
image = new google.maps.MarkerImage( | create a map marker with the profile image |
image_url, | and add it to the map |
new google.maps.Size(32, 32), | |
new google.maps.Point(0, 0), | |
new google.maps.Point(16, 16), | |
new google.maps.Size(32, 32) | |
); | |
latlng = new google.maps.LatLng(lat, lng); | |
this.locations[id] = new google.maps.Marker({ | |
position: latlng, | |
map: this.map, | |
title: info, | |
icon: image | |
}); | |
} | |
} | |
} | |
}; | |
LocationApp.prototype.removeLocation = function (id) { | |
if (id !== gapi.hangout.getParticipantId()) { | prevent the own location from being removed |
if (this.locations[id]) { | |
this.locations[id].setMap(null); | remove marker from map |
delete this.locations[id]; | delete location information |
} | |
} | |
}; |
When we first launch the app we want to display the locations that have already been shared before we joined.
LocationApp.prototype.mapsReady = function () { | |
(...) | |
gapi.hangout.data.onStateChanged.add( | |
this.onStateChanged.bind(this) | |
); | |
this.showAllLocations(); | |
}; | |
LocationApp.prototype.showAllLocations = function () { | |
var state, id, latlng; | |
state = gapi.hangout.data.getState(); | Get the currently shared locations |
for (id in state) { | |
if (state.hasOwnProperty(id)) { | |
latlng = JSON.parse(state[id]); | |
this.addLocation(id, latlng.lat, latlng.lng); | Add them to the map |
} | |
} | |
}; |
Since location information is rather sensitive information we want to make sure that the location is removed when participants leave the hangout or disable the app. For this we can add two more event handlers to events provided by the Hangouts API.
LocationApp.prototype.mapsReady = function () { | |
(...) | |
gapi.hangout.onParticipantsDisabled.add( | Called when someone disables the app |
this.removeParticipants.bind(this) | |
); | |
gapi.hangout.onParticipantsRemoved.add( | Called when someone leaves the Hangout |
this.removeParticipants.bind(this) | |
); | |
}; | |
LocationApp.prototype.removeParticipants = function (event) { | |
var i, l, remove, participants; | |
participants = | |
event.disabledParticipants || | Handling the two different events accordingly |
event.removedParticipants || []; | |
l = participants.length; | |
remove = []; | Create an array of all participant IDs |
for (i = 0; i < l; i++) { | i.e. keys to be removed |
remove.push(participants[i].id); | |
} | |
gapi.hangout.data.submitDelta({}, remove); | This will remove the locations from the shared state. |
The actual removal from the map will be done | |
afterwards via the onStateChanged event above. | |
}; |
Adding information from other Google services offers a whole lot possibilities for Hangout Apps and I'm looking forward to see what ideas you come up with.
As I already mentioned above you can find the full sources for this app on GitHub and on Google Code.
Here's approximately how the app will look like in action:
When looking at the source you might noticed an additional zoom function. This one just makes sure that all locations are always visible on the map, adjusting the zoom and map location as necessary whenever locations are added or removed.
Google+, Google Maps and Google Latitude are trademarks of Google Inc. Use of these trademarks is subject to Google Permissions.
This site is not affiliated with, sponsored by, or endorsed by Google Inc.
This work is licensed under a Creative Commons Attribution 3.0 Unported License.