Tracking and updating the user's location (Intermediate)
While recording an individual location when the page loads is useful, in some situations you may wish to record the user's location continuously. The Geolocation API provides two functions for handling this use case—watchPosition
and clearWatch
—and in this section, we'll use them to modify our application to automatically resave the user's position when he/she moves.
Getting ready
This section doesn't require any PHP programming; the modifications are all on the front end, using JavaScript. However, ensure that you have a compatible device with a GPS receiver and mobile data capability available to test your code. Most modern smartphones should suffice; we tested with the Chrome browser on a Samsung Galaxy S2, and in Safari on an iPhone 5.
How to do it...
Perform the following steps to track and update the user's location:
Take a copy of
index.php
, and save it aslive.php
. We're going to modify it to usewatchPosition
and save multiple map points as follows:<?php // Load our common library file, and fail if it isn't present require_once('lib.php'); ?> <!doctype html> <html> <head> <title> Continuous location detector </title> <script type="text/javascript" src="//code.jquery.com/jquery-1.9.1.min.js"></script> <script type="text/javascript" src="//maps.googleapis.com/maps/api/ js?v=3.exp&sensor=true"></script> <script language="javascript">
This function is called when the Geolocation API successfully retrieves the user's location. Note that we are now saving our map points to an array:
function savePosition(point) { // Save the current latitude and longitude as properties // on the window object window.latitude = point.coords.latitude; window.longitude = point.coords.longitude; // Send the retrieved coordinates to callback.php via a POST // request, and then set the page content to "Location saved" // once this process is complete (or "We couldn't save your // location" if it failed for some reason) $.ajax({ url: 'callback.php', type: 'POST', data: { latitude: window.latitude, longitude: window.longitude }, statusCode: { 500: function() { $('#location_pane').html ('<p>We couldn\'t save your location.</p>'); } } }).done(function() { // Let the user know the location's been saved to the database $('#location_pane').html('<p>Location saved.</p>'); // Center the map on the user's current location var currentLocation = new google.maps.LatLng(window.latitude, window.longitude); window.googleMap.setCenter(currentLocation); // Create a marker at the user's current location and save it // to our array of map points window.geopath.push(new google.maps.LatLng(window.latitude, window.longitude)); }).fail(function() { $('#location_pane').html('<p>We couldn\'t save your location.</p>'); }); }
This function is called when there is a problem retrieving the user's location (but the Geolocation API is supported in his/her browser):
function errorPosition(error) { switch(error.code) { // Error code 1: permission to access the user's location // was denied case 1: $('#location_pane').html('<p>No location was retrieved.</p>'); break; // Error code 2: the user's location could not be determined case 2: $('#location_pane').html('<p>We couldn\'t find you.</p>'); break; // Error code 3: the Geolocation API timed out case 3: $('#location_pane').html('<p>We took too long trying to find your location.</p>'); break; } }
And finally, add a function to prevent automatic updating of user location:
function stopWatching() { if (navigator.geolocation) { navigator.geolocation.clearWatch (window.watchLocationID); $('#watchingButton').hide(); } }
Following this, we move onto the body of the page, incorporating a new "stop watching" button:
</script> </head> <body> <div id="location_pane"> <p> Waiting for location ... </p> </div> <p> <button onclick="stopWatching()" id="watchingButton">Stop watching position</button> </p> <div id="map_pane" style="width: 500px; height: 500px"></div> <!-- We're including the Geolocation API code at the bottom of the page so that page content will have loaded first --> <script language="javascript"> // Set initial viewing options for the map var mapOptions = { zoom: 15, mapTypeId: google.maps.MapTypeId.HYBRID }; // Initialize the map as a googleMap property on the window object window.googleMap = new google.maps.Map(document.getElementById('map_pane'), mapOptions); // Load any previous points into a JSON array, which itself is written // to the page using PHP. We're hardcoding the user ID to 1, as in // callback.php. var jsonPoints = <?=json_encode(getPreviousLocations(1));?>; window.polyLine = new google.maps.Polyline({ strokeColor: '#ff0000', strokeOpacity: 1.0, strokeWeight: 3 }); window.polyLine.setMap(window.googleMap); window.geopath = window.polyLine.getPath(); // If jsonPoints isn't empty, iterate through and create new map points // for each geolocation point if (jsonPoints.length > 0) { jsonPoints.forEach(function(point) { window.geopath.push(new google.maps.LatLng (point.latitude, point.longitude)) }); } // First, check if geolocation support is available if (navigator.geolocation) { // If Geolocation API support is available: // Attempt to get the current position, and // watch the user's location; instantiate the savePosition // function if the location was saved, or errorPosition if // it was not. Note that we don't ever want low-accuracy // location measurements in this context. window.watchLocationID = navigator.geolocation.watchPosition(savePosition, errorPosition, {enableHighAccuracy: true}); } else { // If the browser doesn't support the Geolocation API, tell the user. $('#location_pane').html ('<p>No geolocation support is available.</p>'); } </script> </body> </html>
How it works...
The Geolocation API watchPosition
method uses a very similar syntax to the getCurrentPosition
method we used earlier. Its parameters are the same:
A callback function to call on success
A callback function to call on failure
An array of options
The available options, in turn, are also the same:
enableHighAccuracy
: This is a Boolean value, It enables high accuracy mode (default: off
).timeout: This is a long value. It is the threshold beyond which the API times out (in milliseconds; the default is no limit).
maximumAge: This is a long value. It is the maximum age of a cached location that we'll accept, in milliseconds (
default: 0
).
However, rather than initiating a single check for the user's location, it instead establishes a "watch" that will load the callback functions as appropriate whenever the device has detected that the user has moved. Because you need to reference the watch process, watchPosition
returns an identifier.
The identifier is obtained when the method is called, as follows:
window.watchLocationID = navigator.geolocation.watchPosition(savePosition, errorPosition, {enableHighAccuracy: true});
Note that we've used the enableHighAccuracy
option, while not failing back to a lower-accuracy location method. That's because in a situation where you're continually retrieving the location, low-quality data will not be useful. Because the device falls back to estimating location through environmental factors, visibly erroneous data may be included in your dataset, sometimes hundreds of meters or more away from the user's actual location, making it harder to track the user's path. It's better to fail if a GPS signal cannot be found.
Sometimes, the user may wish to switch tracking off, while remaining on the page. To do this, we've created a simple button. This will trigger the clearWatch
method, which takes the watch process ID as a single parameter as follows:
navigator.geolocation.clearWatch(window.watchLocationID);
In our implementation, we've also hidden the button using the jQuery hide()
method once it has been pressed.
Finally, our savePosition
function, which is called whenever a new location is successfully received, must display the new location.
In our previous implementation, we simply created a variable for the new Marker object to display a single point. However, we also created a window.points
array to keep the Marker objects that were reloaded from the database. In order to display the full set of points that are displayed as the user moves around, we'll need to add each newly created Marker in the array too.
To do this, we can just push new Marker objects to the array as follows:
window.points.push(new google.maps.Marker({ position: currentLocation, map: window.googleMap, title: 'Detected location' }));
However, a series of individual points isn't necessarily the best way to display this data.
There's more...
Unfortunately, at the time of writing this book, there's no way to prevent the mobile device from switching its screen and GPS unit off. Consider the following screenshot from a walk I took around the Inner Sunset neighborhood in San Francisco:
Contrary to what the map might indicate, this was a continuous walk—I didn't suddenly leap two blocks in a single bound! However, while I was holding my device and ensuring that its screen didn't turn black on some of the streets (indicated by the vertical lines you can see here), on the others I chose to slip my device into my pocket, as an ordinary user might. The GPS receiver was deactivated automatically when the screen went black, in order to conserve battery life. As a result, no location points were saved for these portions of my journey.
We can further enhance our application by replacing the individual points on the map with a continuous line. This would also help smooth over any gaps in the dataset.
To achieve this, we need to replace the portions of the code that set points on the map using the Google Maps API. Google provides another element, PolyLines
, which allows you to arbitrarily add coordinates to a continuous line. I've included the complete code as livepath.php
.
In the portion of the page where we're setting up the map, just after we've loaded the jsonPoints
JSON array, we need to create a polyline. We'll make it bright red (HTML color #ff0000
) so that we can see it easily:
window.polyLine = new google.maps.Polyline({ strokeColor: '#ff0000', strokeOpacity: 1.0, strokeWeight: 3 });
Now, we'll attach it to our existing map. window.polyLine
will be the global variable that stores the polyline:
window.polyLine.setMap(window.googleMap);
And finally, we'll establish window.geopath
as the array of points that make up the polyline. We're saving that as a global variable too, for convenience. You'll see why momentarily.
window.geopath = window.polyLine.getPath();
Now, let's load our JSON array of previously saved points onto it:
// If jsonPoints isn't empty, iterate through and create new map // points for each geolocation point if (jsonPoints.length > 0) { jsonPoints.forEach(function(point) { window.geopath.push(new google.maps.LatLng ( point.latitude, point.longitude )) }); }
Finally, recall the AJAX done()
function at the top of the page, which is called once a new location has been successfully processed. We can replace the Google Maps code in this function with a simple line to add the latest coordinates to our PolyLine
:
// Create a marker at the user's current location and save it // to our array of map points window.geopath.push(new google.maps.LatLng(window.latitude, window.longitude));
Now, whenever a new location is detected, it will be added to the line rather than to the map as a new, individual point.
The Geolocation API is an important addition to any modern web developer's arsenal. Together with other new APIs, HTML 5 itself and the new breed of mobile devices, the web doesn't just allow you to build high quality network applications; it allows you to build new kinds of context-aware applications that have never been created before. Happy building.