The purpose of this tutorial is to show how to create a realtime location aware visitor tracker for a web site or application. When a visitor navigates to the page on your site, or accesses your mobile web app, the visitor tracker will display the new visitor hit in realtime, displaying the page they've landed on and where each visitor is on a Google map (lat/long and locality). For the purposes of the tutorial we'll write our server code in PHP, but you could use any server side language you like.
For the realtime functionality we'll use Pusher, who I work for. Pusher is a hosted service for quickly and easily adding realtime features into web and mobile applications. The service is used for all sorts of features such as notifications, game movements, chat, live data feeds and much more, so it fits the bill nicely here too. We'll be using the Pusher JavaScript API and the Pusher PHP Library.
Here's the order in which we'll cover things:
- Get the basics out of the way
- Connect to Pusher
- Subscribe to new visitor page hits notifications
- Get the visitor's location using the HTML5 Geolocation API
- Broadcast the visitor location and page to all application visitors using Pusher
- Display the visitor location and page information on a Google map
Before we start building the realtime tracker let's get a few basics set up.
You can download a template containing all the files you'll need here. Here are the details about each file:
index.php
- an example pageconfig.example.php
- example application configuration including our Pusher app configuration- css
styles.css
- CSS rules
- js
tracker.js
- the main body of JavaScript for the trackerStyledMarker.js
- used with Google maps later in the tutorial
- pusher
new_hit.php
- called whenever a new visitor is trackedPusher.php
- the PHP Pusher library
In order to use Pusher you need to set up a Pusher application. To do this you'll need to signup for a free Pusher sandbox account. Once you've signed up you'll be taken to a page which provides you with your API access credentials. Create a file called config.php
, by renaming the existing config.example.php
file, and add the credentials to it as follows:
<?php
define('APP_ID', 'YOUR_APP_ID');
define('APP_KEY', 'YOUR_APP_KEY');
define('APP_SECRET', 'YOUR_APP_SECRET');
?>
This file is included within the index.php
and pusher/new_hit.php
files e.g.
<?php
require_once('config.php')
?>
We're nearly getting to the good stuff. The final things we want to do is expose the APP_KEY
that we have set in the config.php
file to JavaScript, include the Pusher JavaScript library and include a JavaScript file to contain our tracker logic. So, within index.php
should now contain the following:
<script>
var APP_KEY = '<?php echo(APP_KEY); ?>';
</script>
<script src="http://js.pusher.com/1.12/pusher.min.js"></script>
<script src="js/tracker.js"></script>
It's essential to be able to keep track of what's going on within your Pusher application. So, we've created a debugging resource detailing ways to help you track down and troubleshoot problems.
Now that we have the application structure in place, and you know how to troubleshoot problems, we're in a good position where we can make a start on the really interesting code which gets us connected to Pusher.
Pusher uses WebSockets for realtime bi-directional communication between a user's web browser and Pusher. First we need to establish that WebSocket connection. We mentioned earlier that we would be using the Pusher JavaScript library. By using this library we can provide additional features that WebSockets don't natively offer but are essential when building realtime web applications; like auto reconnection.
Let's get connected by adding some code to js/tracker.js
:
var pusher = new Pusher(APP_KEY);
It's as easy as that! But, let's also add some code to detect when we've connected. The Pusher JavaScript library uses events to expose new information or changes in state (we'll see more of this later). In the case of connecting we need to bind to a state_change
event on a pusher.connection
object. We do this using a bind
function:
var pusher = new Pusher(APP_KEY);
pusher.connection.bind('state_change', function(state) {
if( state.current === 'connected' ) {
alert("Yipee! We've connected!");
}
});
We won't use this functionality within the tracker so feel free to remove the pusher.connection.bind
call.
Now that we need a way of registering interest in realtime visitor event data.
You access data using Pusher by subscribing to channels. Channels offer a great way of filtering data. In our case the channel is for visitor hits so we'll call it visitor-hits
.
Now we have the channel name to subscribe to we simply need to call the subscribe
function on our Pusher
object:
var channel = pusher.subscribe('visitor-hits');
Once you are subscribed to the channel you can bind to an event on the channel, in the same way we bound to the state_change
event on the pusher.connection
object earlier. At this point we're not 100% subscribed to the channel, Pusher still needs to acknowledge the subscription. So, we can bind to a pusher:subscription_succeeded
event so that we know when we are.
var channel = pusher.subscribe('visitor-hits');
channel('pusher:subscription_succeeded', function() {
alert('subscribed!');
});
We also want to bind to a data event so we are notified whenever a new_hit
occurs:
var channel = pusher.subscribe('visitor-hits');
channel.bind('pusher:subscription_succeeded', function() {
// we'll add some code here later
});
channel.bind('new_hit', newHitReceived);
function newHitReceived() {
alert('new hit');
}
Now we've got the code in place it would be great if we could test the Pusher data integration. We can do this using the Event Creator. Simply enter the channel name, event name and, for now, some example channel data and click the Send event button.
You'll then see the alert
in the newHitReceived
function trigger in the web app.
Now we've got the basics of the Pusher basics in place, let's take a look at accessing the visitor's location. Once we've got the location data into the event we'll look at hooking into displaying the data on a Google map.
The HTML5 Geolocation API provides a way for us to get the location from a visitor's web browser. To use this functionality the visitor will need to be using a modern web browser. Although there are polyfill solutions available for older browsers, we won't use them here.
The API is available on the navigator.geolocation
object and It provides three functions:
getCurrentPosition(successCallback[, errorCallback, options])
var watchId = watchPosition(successCallback[, errorCallback, options])
clearWatch(watchId)
When you call either getCurrentPosition
or watchPosition
the browser will prompt the visitor, asking them if they wish to share their location with the web page or application.
If the visitor clicks "Allow" the successCallback
will be called. Otherwise the errorCallback
will be called. When the successCallback
is called, an object is passed to that callback. The object looks as follows:
Let's update the code in js/tracker.js
to use the getCurrentPosition
HTML5 Geolocation function once we get subscription confirmation to the visitor-hits
channel:
var pusher = new Pusher(APP_KEY);
var channel = pusher.subscribe('visitor-hits');
pusher.bind('pusher:subscription_succeeded', function() {
navigator.geolocation.getCurrentPosition(positionSuccess, positionError);
});
var visitorPosition = null;
function positionSuccess(position) {
visitorPosition = position; // store for future reference
var lat = visitorPosition.coordinates.latitude;
var lng = visitorPosition.coordinates.longitude;
// we'll use these variables later
}
function positionError(error) {
alert('position error');
}
If the visitor declines to share their location the positionError
function will be called. In this situation we could display a message to the visitor, but we'll concentrate on the success scenario.
As you can see from we can get the latitude and longitude from the web browser. But those values aren't all that readable. It would be great to have a friendly name to put to the lat/long values. Luckily the Google Maps JavaScript API comes with a Geocoder service that lets you fetch additional information about a set of lat/long coordinates.
To use the Google Maps API you first need to include the v3 JavaScript library:
<script>
var APP_KEY = '<?php echo(APP_KEY); ?>';
</script>
<script src="http://js.pusher.com/1.12/pusher.min.js"></script>
<script src="http://maps.googleapis.com/maps/api/js?sensor=true"></script>
<script src="js/tracker.js"></script>
In js/tracker.js
we can then use the Geocoder
service to find the locality of the visitor. We won't dig into the following code too much. If you are interested in how it works have a look at the Geocoder service docs. The key point is that we either get a friendly locality name for the geolocation or a value of null
:
var visitorPosition = null;
function positionSuccess(position) {
visitorPosition = position; // store for future reference
var lat = visitorPosition.coordinates.latitude;
var lng = visitorPosition.coordinates.longitude;
var location = new google.maps.LatLng(lat, lng);
var geocoder = new google.maps.Geocoder();
geocoder.geocode({location: location}, geocodeResponse);
}
In the response handler we use the findLocality
helper function to find a nicely formatted locality to use:
function geocodeResponse(response, status) {
var locality = null;
if(status == google.maps.GeocoderStatus.OK) {
locality = findLocality(response);
}
if(locality != null) {
alert(locality.formatted_address);
}
}
function findLocality(addresses) {
if(addresses.length == 0) {
return null;
}
for(var addrIndex = 0, addrCount = addresses.length; addrIndex < addrCount; ++addrIndex) {
var address = addresses[addrIndex];
for(var typeIndex = 0, typeCount = address.types.length; typeIndex < typeCount; ++typeIndex) {
if(address.types[typeIndex] == "locality") {
return address;
}
}
}
return addresses[0];
}
Now that we have the latitude and longitude, along with a friendly locality name, we are in the position to broadcast this information to any other connected visitors to ultimately be displayed in the tracker.
In order to broadcast the data we've gathered about the visitor we're going to make an AJAX request to the server and use the Pusher PHP library to trigger an event. We could do this in the web browser using client events, but by doing this via the server we also benefit from:
- Added security - we could verify and sanitise the data. Ultimately you can't trust the client.
- Persistence - we could store the data for future reference.
To keep things simple and avoid handling cross browser XMLHttpRequest
inconsistencies in older browsers I'm going to include jQuery in index.php
to use the jQuery.ajax function
.
<script src="http://code.jquery.com/jquery-1.7.2.min.js"></script>
Now we can make the AJAX call and send the lat, long and locality to the server:
function geocodeResponse(response, status) {
var locality = null;
if(status == google.maps.GeocoderStatus.OK) {
locality = findLocality(response);
}
if(locality != null) {
$.ajax({
url: 'pusher/new_hit.php',
type: 'post',
data: {
locality: locality.formatted_address,
lat: visitorPosition.coordinates.latitude,
lng: visitorPosition.coordinates.longitude
},
success: function(data, textStatus, jqXHR) {
alert('ok');
},
error: function(jqXHR, textStatus, errorThrown) {
alert('error');
}
});
}
}
Now that the JavaScript POST
s the values to the server as part of the AJAX call we can access them in our pusher/new_hit.php
file. We can also get access to the page the visitor is on using $_SERVER['HTTP_REFERER']
:
<?php
require_once('../config.php');
$data = array(
'lat' => $_POST['lat'],
'lng' => $_POST['lng'],
'locality' => $_POST['locality'],
'page' => $_SERVER['HTTP_REFERER']
);
header('Content-type: application/json');
echo( json_encode($data) );
?>
In this file we extract them from the $_POST
and put them in an associative array
called $data
. So that we can ensure things are working we'll also echo
the data back to the client, encoded as JSON (we won't use this here, but it can be handy for debugging).
You can trigger an event in just a few lines of code; require_once
the Pusher PHP library, create an instance of a Pusher
object and call trigger
on it, passing the $data
we've already created:
<?php
require_once('../config.php');
require_once('Pusher.php');
$data = array(
'lat' => $_POST['lat'],
'lng' => $_POST['lng'],
'locality' => $_POST['locality'],
'page' => $_SERVER['HTTP_REFERER']
);
$pusher = new Pusher(APP_KEY, APP_SECRET, APP_ID);
$pusher->trigger('visitor-hits', 'new_hit', $data);
header('Content-type: application/json');
echo( json_encode($data) );
?>
I'm sure you'll agree, that couldn't be easier. Of course, you should verify and sanitise your data if you want to do something similar in production.
When you load the web app now you'll get the new_hit
alert we added earlier. It's now time to create a UI and display a Google Map. You'll probably want to remove that alert now too.
The final steps involved creating the Google Map and then taking the new_hit
event data and using it to display visitors on the map.
We've already included the Google Maps JavaScript library so now we need to first define some basic HTML and CSS for the page and then create the Google Map object. The HTML couldn't be any simpler, just add a <div>
with and id
attribute with a value of map
to the <body>
:
<div id="map"></div>
We'll add CSS to make the map the full page size. Update css/styles.css
to contain the following:
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
#map {
width: 100%;
height: 100%;
}
The next thing to do is create the Google Map. To do this we need to create a new google.maps.Map
object, passing in a reference to where we want the map to appear (in our <div id="map"></div>"
element) and some options, on of which is where the map should centre. For this we'll use the visitor's location we've already got referenced through visitorPosition
. We'll store a reference to the new Map object via a map
variable as we'll use that later:
var map = null;
function createMap() {
var visitorLocation =
new google.maps.LatLng(
visitorPosition.coords.latitude,
visitorPosition.coords.longitude);
var mapOptions = {
zoom: 2,
center: visitorLocation,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
map = new google.maps.Map(document.getElementById("map"), mapOptions);
}
We also need to call this new createMap
function once the visitorPostion
is set. Let's add a call in positionSuccess
:
var visitorPosition = null;
function positionSuccess(position) {
visitorPosition = position;
var lat = visitorPosition.coords.latitude;
var lng = visitorPosition.coords.longitude;
var location = new google.maps.LatLng(lat, lng);
var geocoder = new google.maps.Geocoder();
geocoder.geocode({location: location}, function(response, status) {
geocodeResponse.call(this, response, status, location)
});
createMap();
}
We've all seen a Google Map before, but for completeness here's what you will see:
We now want to use the data that is being passed to our newHitReceived
received function and add a marker to the map to show where the visitor is. Surprisingly enough the default Marker doesn't come with the ability to easily display text so we'll use a StyledMarker library instead. To use this we just need to include our local version in index.php
:
<script src="js/StyledMarker.js"></script>
We can then use it in our function which displays the visitor location on the map.
Before we add the marker it's worth covering the format of the data
that is passed to the newHitReceived
function. If you remember back to the PHP code we passed an array
object to the trigger
function:
$data = array(
'lat' => $_POST['lat'],
'lng' => $_POST['lng'],
'locality' => $_POST['locality'],
'page' => $_SERVER['HTTP_REFERER']
);
The Pusher PHP library converts this to a JSON representation so when we receive the data in our JavaScript it looks as demonstrated in the output from the JavaScript console below:
This means we can easily access the lat
, long
and locality
of the visitor and add a marker to the map:
function newHitReceived(data) {
var latLng = new google.maps.LatLng(data.lat, data.lng);
var styleMaker = new StyledMarker({
styleIcon: new StyledIcon(
StyledIconTypes.BUBBLE,
{
color:"fff",
text:data.locality
}),
position: latLng,
map: map
});
}
If you try the tracker out now you will see visitor locations drop down, in realtime, as they arrive in the app. We'll add one final touch in the form of a popup InfoWindow which will display the visitors locality, geolocation and the page (using data.page
) that they are on when it is clicked. The full source for the function becomes:
function newHitReceived(data) {
var latLng = new google.maps.LatLng(data.lat, data.lng);
var styleMaker = new StyledMarker({
styleIcon: new StyledIcon(
StyledIconTypes.BUBBLE,
{
color:"fff",
text:data.locality
}),
position: latLng,
map: map,
animation: google.maps.Animation.DROP
});
var contentString = '' +
'<div class="visitor-info">' +
'<strong>Locality:</strong> ' + data.locality + '<br />' +
'<strong>Lat/Lng:</strong> ' + data.lat + ', ' + data.lng + '<br />' +
'<strong>Page:</strong> <a href="' + data.page + '">' + data.page.replace(/https?:\/\/[^/]*/, '') + '</a><br />' +
'</div>';
var infowindow = new google.maps.InfoWindow({content: contentString});
google.maps.event.addListener(styleMaker, 'click', function() {
infowindow.open(map, styleMaker);
});
}
And that's it! We now have a fully functional location-aware realtime visitor tracking system, displaying visitor information on a Google Map.
We've covered quite a few things in reasonable detail in this tutorial:
- Basic Pusher principles including connecting, subscribing to data, publishing data, binding to an event and using event data.
- The HTML5 Geolocation API
- Google Map basics
The tutorial has demonstrated how mashing these technologies together is not only fun, but also very powerful. As you get people visiting the application the realtime nature can really make it come alive, and give it a 'stickiness' that realtime technologies now make possible. Hopefully it's also inspired you add increased interaction and engagement to your existing applications, or to start a new project that focuses on Pusher powered realtime activity and user interaction.
Phil Leggetter is a Developer Evangelist at Pusher. He's been involved in developing and using realtime web technologies for over 10 years. His focus is to help people use these technologies to build the next generation of interactive and engaging realtime web applications.
- Blog: http://www.leggetter.co.uk
- Twitter: @leggetter
- Mugshot: