The Geolocation API specifies a new navigator.geolocation object. This object has three new methods that access the geolocation capabilities of the browser and the hosting device. Since it can take an unknown amount of time to resolve the location of the device (the script will pause the first time and wait for the user to respond to the permission dialog before continuing, and then the various location methods have to be queried, each of which can take an unknown amount of time), the methods are asynchronous, and provide a way to register success and error callback functions.
■Tip You can use promises (which are well-supported in mobile browsers) to help simplify the code for
asynchronous actions. See the section on promises in Appendix A.
• navigator.geolocation.getCurrentPosition(successCallback,
errorCallback, PositionOptions): Calls either the successCallback when the location is successfully returned or the errorCallback if an error occurs. When successCallback is called, it will receive a Position object as a parameter, and when errorCallback is called it will receive a PositionError object as a parameter.
• navigator.geolocation.watchPosition(successCallback, errorCallback, PositionOptions): Immediately returns a PositionWatch identifier, and then calls the successCallback function every time the device’s position changes. Calls errorCallback if an attempt to resolve the location fails. When successCallback is called, it will receive a Position object as a parameter, and when errorCallback is called it will receive a PositionError object as a parameter.
• navigator.geolocation.clearWatch(PositionWatch): Stops a watchPosition call specified by the PositionWatch value.
In addition, the API defines three new object templates: the PositionOptions object, the Position object, and the PositionError object. The PositionOptions object provides an interface for the getCurrentPosition and watchPosition methods to fine-tune the query and results, as follows.
PositionOptions = {
// Specifies whether the query should return the most accurate location possible boolean enableHighAccuracy,
// The number of milliseconds to wait for the device to return a location number timeout,
// The number of milliseconds a cached value can be used.
number maximumAge }
The Position object defines the response that will be returned by the getCurrentPosition and watchPosition methods upon successfully resolving the location of the host device, as follows.
Position = {
// The altitude in meters above nominal sea level.
number altitude,
// The accuracy of the latitude and longitude values, in meters.
number accuracy,
// The accuracy of the altitude value, in meters.
number altitudeAccuracy,
// The current heading of the device in degrees clockwise from true north.
number heading,
// The current ground speed, in meters per second.
number speed, },
// The time when the location query was successfully created.
date timestamp }
Note that depending on the browser’s implementation of the Geolocation standard and the capabilities of the host device, the values for altitude, accuracy, altitudeAccuracy, heading, and speed may return as null.
The PositionError object defines the response that will be returned if the user refuses to allow geolocation, or if somehow the device could not resolve its location, as shown here.
PositionError = {
// The numeric code of the error (see table below).
number code,
// A human-readable error message.
string message }
Valid codes for PositionError.code are integers, as listed in Table 5-1.
Table 5-1. Valid PositionError Codes
Code Constant Description
0 UNKNOWN_ERROR The device could not resolve its location due to an unknown error.
1 PERMISSION_DENIED The application does not have permission to use the geolocation services, usually due to the user refusing permission.
2 POSITION_UNAVAILABLE The device could not resolve its location because the services are unavailable. (Typically returned when the various required radios are deactivated, as when a mobile device is in “airplane mode.”)
3 TIMEOUT The device could not resolve its location within the timeout limit specified by PositionOptions.timeout.
The simplest example of using this API is to do a simple location query and show all of the values that are returned, as demonstrated in Listing 5-1.
Listing 5-1. A Basic Query of the Geolocation API
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<h1>Geolocation Example</h1>
<div id="locationValues">
</div>
<div id="error">
</div>
<script>
/**
* The success callback function for getCurrentPosition.
* @param {Position} position The position object returned by the geolocation * services.
*/
function successCallback(position) { console.log('success')
// Get a reference to the div we're going to be manipulating.
var locationValues = document.getElementById('locationValues');
// Create a new unordered list that we can append new items to as we enumerate // the coords object.
var myUl = document.createElement('ul');
// Enumerate the properties on the position.coords object, and create a list // item for each one. Append the list item to our unordered list.
for (var geoValue in position.coords) { var newItem = document.createElement('li');
newItem.innerHTML = geoValue + ' : ' + position.coords[geoValue];
myUl.appendChild(newItem);
}
// Add the timestamp.
newItem = document.createElement('li');
newItem.innerHTML = 'timestamp : ' + position.timestamp;
myUl.appendChild(newItem);
// Enumeration complete. Append myUl to the DOM.
locationValues.appendChild(myUl);
} /**
* The error callback function for getCurrentPosition.
* @param {PositionError} error The position error object returned by the * geolocation services.
*/
function errorCallback(error) {
var myError = document.getElementById('error');
var myParagraph = document.createElement('p');
myParagraph.innerHTML = 'Error code ' + error.code + '\n' + error.message;
myError.appendChild(myParagraph);
}
// Call the geolocation services.
navigator.geolocation.getCurrentPosition(successCallback, errorCallback);
</script>
</body>
</html>
First, this example creates a success callback function that enumerates the properties of the Position object. As it does so it adds them to an unordered list that is appended to the DOM so you can see it. The error callback behaves the same way, except instead of producing a list it simply updates the contents of a paragraph.
The first time you run this example your browser should prompt you for permission to access the geolocation APIs. The first time through, deny permission, so you can see what an error condition looks like.
Figure 5-3 shows what the resulting page looks like in Chrome.
You can see that the error handler was called with an error code of 1. The actual text for the error message varies from browser to browser (Internet Explorer 11, for example, uses the error message “This site does not have permission to use the Geolocation API.”) but the error code is the same.
The Geolocation specification does not define the permission model that must be presented to the user, which is why every browser does it differently. The specification simply says,
User agents must not send location information to Web sites without the express permission of the user. User agents must acquire permission through a user interface, unless they have prearranged trust relationships with users, as described below. The user interface must include the host component of the document’s URI. Those permissions that are acquired through the user interface and that are preserved beyond the current browsing session (i.e. beyond the time when the browsing context is navigated to another URL) must be revocable and user agents must respect revoked permissions.
Some user agents will have prearranged trust relationships that do not require such user interfaces. For example, while a Web browser will present a user interface when a Web site performs a geolocation request, a VOIP telephone may not present any user interface when using location information to perform an E911 function.
As a result, how a user can grant or refuse geolocation permission, how long that decision is
remembered, and how a user can change their mind later, are all up to the browser manufacturer to decide and implement.
In Internet Explorer, for example, the user is presented with a pop-up that allows them some interesting options, as shown in Figure 5-4.
Figure 5-3. Error condition for Listing 5-1 in Chrome
If the user chooses “Allow once” or “Always allow”, the script will continue and the browser will attempt to resolve the client’s location. The option “Allow once” should probably read “Allow for this browsing session”, because the permission remains in effect until the user closes and restarts the browser. At that point, revisiting the page will reprompt the user. The option “Always allow” functions as you would expect:
once the user picks it, they will never again be prompted for permission. The option “Always deny and don’t tell me” denies permission at that point and every subsequent time the user visits that page. They are never reprompted for permission, and the only way they can undo this decision is to open the Internet Options dialog for Windows, choose the Privacy tab, and click the “Clear sites” button in the Location section—which clears all permanent permissions granted or denied to all sites.
Firefox presents a completely different interaction to the user, as shown in Figure 5-5.
Figure 5-4. Geolocation permission options in Internet Explorer 11
Figure 5-5. Geolocation permission options in Firefox 29
If the user chooses “Share Location” the script will continue and the browser will attempt to resolve the client’s location. Unlike with Internet Explorer, however, this permission is not for the current browser session but only for the current visit to the web site. Reloading the page will immediately prompt the user for permission again. The user does not have to restart the browser. The “Always Share Location” option grants permanent permission to share location, and “Never Share Location” acts as a permanent denial of permission for the page. Choosing “Not Now” or clicking on the × icon in the upper right corner of the pop-up, or clicking anywhere outside of the pop-up, will close the pop-up without either granting or denying permission and will leave your application hanging. The pop-up can be reopened by clicking the “target”
icon next to the URL, but that’s not necessarily immediately obvious. This behavior is by design; see the relevant Bugzilla bug, https://bugzilla.mozilla.org/show_bug.cgi?id=675533, for an explanation.
Only in Safari Mobile on iOS is the permission pop-up an actual modal pop-up that requires the user to respond and cannot be dismissed unless they make a choice. In all other cases, the user can ignore (and in the case of Firefox completely dismiss) the pop-up and leave your script waiting to execute a callback.
To make matters worse, time spent in this undefined state does not count toward any timeout you may have specified with PositionOption.timeout—that timer only begins running after the user has granted permission and the browser has begun trying to resolve the location.
To get around this, you need to implement a global timeout timer that starts running as soon as the script accesses the Geolocation API. If the user does grant (or deny) permission, our regular callbacks should happen and this global timer should be canceled. If the user does not grant (or deny) permission, the global timer should execute a callback that does something—for example, redirect the browser to an error page that explains to the user what they need to do to continue. Or if your application doesn’t require GPS, the global timer callback should cancel the success and error callbacks and your application can continue.
It’s easy to add such a global timer to Listing 5-1, as shown in Listing 5-2.
Listing 5-2. Registering a Global Timeout
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<h1>Geolocation Example</h1>
<div id="locationValues">
</div>
<div id="error">
</div>
<script>
// Create the variable that will hold the timer reference.
var globalTimeout = null;
/**
* The success callback function for getCurrentPosition.
* @param {Position} position The position object returned by the geolocation * services.
*/
function successCallback(position) {
// Check the state of the global timeout. If it is null, the application has // timed out and we should not continue. If it isn't null, the timeout timer // is still running, so we should cancel it and continue.
if (globalTimeout == null) { return;
} else {
clearTimeout(globalTimeout);
}
// Get a reference to the div we're going to be manipulating.
var locationValues = document.getElementById('locationValues');
// Create a new unordered list that we can append new items to as we enumerate // the coords object.
var myUl = document.createElement('ul');
// Enumerate the properties on the position.coords object, and create a list // item for each one. Append the list item to our unordered list.
for (var geoValue in position.coords) { var newItem = document.createElement('li');
newItem.innerHTML = geoValue + ' : ' + position.coords[geoValue];
myUl.appendChild(newItem);
}
// Add the timestamp.
newItem = document.createElement('li');
newItem.innerHTML = 'timestamp : ' + position.timestamp;
myUl.appendChild(newItem);
// Enumeration complete. Append myUl to the DOM.
locationValues.appendChild(myUl);
} /**
* The error callback function for getCurrentPosition.
* @param {PositionError} error The position error object returned by the * geolocation services.
*/
function errorCallback(error) {
// Check the state of the global timeout. If it is null, the application has // timed out and we should not continue. If it isn't null, the timeout timer // is still running, so we should cancel it and continue.
if (globalTimeout == null) { return;
} else {
clearTimeout(globalTimeout);
}
var myError = document.getElementById('error');
var myParagraph = document.createElement('p');
myParagraph.innerHTML = 'Error code ' + error.code + '\n' + error.message;
myError.appendChild(myParagraph);
}
/**
* The callback to execute if the whole process times out, specifically in the * situation where a user ignores the permissions pop-ups long enough.
*/
function globalTimeoutCallback() {
alert('Error: GPS permission not given, exiting application.');
globalTimeout = null;
}
// Call the geolocation services.
navigator.geolocation.getCurrentPosition(successCallback, errorCallback);
// Start the timer for the global timeout call.
globalTimeout = setTimeout(globalTimeoutCallback.bind(this), 5000);
</script>
</body>
</html>
The first thing this example does is define a globalTimeout variable, which will hold the identifier for the timer it will start when it initiates the geolocation request. Next, notice that in both the successCallback and errorCallback functions, it checks the state of the globalTimeout variable. If the variable is null, the global timeout has expired, and the code should not continue to execute those functions. If it isn’t null, the timer is still active, so the code should cancel it and continue.
Next it provides a globalTimeoutCallback function that simply alerts a message to the user. In an actual application you would want to do something more useful here—redirect the user to another page, for example. The code also sets the globalTimeout variable to null so that if either of the callbacks should get executed somehow, they will not continue past the initial global timeout check.
Finally, it sets the timer running immediately after it calls the geolocation API. The timer is set to five seconds. When you load this page, you’ll see one of the following:
• If you have permanently denied geolocation permission to the page, the
errorCallback will execute and the global timer will be canceled. No permission pop-up will be displayed.
• If you have permanently allowed geolocation permission to the page, the
successCallback will execute and the global timer will be canceled. No permission pop-up will be displayed.
• If you haven’t permanently granted or denied permission, the permission pop-up will display. You can choose to grant or deny permission before the global timeout timer expires, in which case the appropriate callback will execute and the global timer will be canceled. Or you can do nothing and wait for the global timer to expire.
When that happens, the alert message will appear.
In any case, you cannot programmatically force a permission choice for the user. They have to make their permission choice through the browser-supplied dialog.
From a user interaction standpoint, this is a somewhat unfortunate state of affairs because it means your application will cause the browser to display a notification over which you have no control. Some users might find this alarming and choose to deny permission, or even shut down the browser entirely and never return to your application. If you have been transparent with your users about how your application collects and stores geolocation information, they will be prepared for this interaction and will be more willing to grant permission, because they know what your application will be doing with the data.