Obtaining Visitor Location with the HTML5 Geolocation API


Recently, I had the opportunity to work on a local business mapping application that required a visitor's location in order to display a list of local businesses. The project was expected to support  a wide range of desktop and mobile browsers and while I was anxious to try out the HTML5 Geolocation API, I knew I'd need to fall back to IP-based geolocation. Combining both techniques turned out to be a bit tricky so I thought I'd share my approach and some code.

Privacy Concerns

Before implementing HTML5 Geolocation, be aware that it doesn't happen silently. The W3C's specification includes very specific language aimed at protecting visitor privacy:

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 you would expect, browsers implement the interface for obtaining permission differently and they offer different sets of location disclosure preferences. Some examples:

Location Tracking Prompts

Unfortunately, browsers handle the visitor response to location sharing prompts (or the lack thereof) differently as well. I found it was very important to account for these differences and thoroughly test use cases for each browser.

Geolocation Support, Timeouts, and Fallback

Geolocation is widely supported in browsers other than Internet Explorer. For a complete list, check out this table at caniuse.com. If you need to support IE6-8 and you want a fallback mechanism when HTML5 geolocation fails, IP-based geolocation is a practical alternative. There are a number of paid and free services; I chose a free (but not particularly accurate) one called geoPlugin. My project requirements called for low server load so I used geoPlugin's JavaScript web service. This way, all of my HTML5 and IP-based geolocation code could be conveniently written together in a client-side JavaScript.

The basic strategy for obtaining a visitor's location is simple:

  1. Check for Geolocation Support. None? Fallback. 
  2. Read the Visitor's Location. Failed? Timed Out? Fallback.
  3. Store the Location for Future Use.

You can check for HTML5 geolocation support in the browser like this:

// Check for HTML5 geolocation support.
if(navigator.geolocation) { /*Do Something Awesome...*/ }

Now for the tricky part. Since geolocation executes asynchronously, the navigator.geolocation.getCurrentPosition() function implements success/failure handlers and a timeout parameter so it can let you know if it was able to determine a location in a reasonable amount of time. It's constructed like this:

navigator.geolocation.getCurrentPosition(success_handler, error_handler, {timeout:ms, maximumAge:ms, enableHighAccuracy:boolean});

This function looks straightforward but there are several caveats. First, not all browsers respect the timeout parameter consistently (looking at you, Firefox). Second, setting a maximum age in hopes of getting a recently cached value doesn't work consistently and may cause no return. Finally, because of the varied implementations for user privacy preferences and dialogs, this function cannot be relied on to always return. In some cases where a visitor ignores or dismisses the location sharing prompt, your application could be left waiting indefinitely for a result. To protect against these conditions, you can write your own timeout like this:

  // Check for HTML5 geolocation support.
  if(navigator.geolocation) {

    // Start a timer to ensure we get some kind of response.
    // Make sure to clear this timer in your success and error handlers
    var location_timeout = setTimeout(function(){
      your_error_handler({'TIMEOUT':'1'})
    }, 8000);

    // Call the HTML5 geolocation feature with your handlers for success/error and an 8-second timeout.
    navigator.geolocation.getCurrentPosition(your_success_handler, your_error_handler, {timeout:8000});
  }

  // If navigator.geolocation is falsy, there's no HTML5 geolocation support.
  // Fall back to IP-based geolocation.
  else {
    yourIpFallbackFunction();
  }
}

What's Returned?

The navigator.geolocation.getCurrentPosition() function returns either a Position object or a PositionError object. The PositionError object should contain a constant with a numeric error code and an error message string you can use to determine what went wrong. Here's an example of an error handler using a switch case statement:

function your_error_handler(error) {

  // Respond to the possible error states.
  switch(error.code){
    case error.PERMISSION_DENIED:
      console.log("The user prevented this page from retrieving a location.");
      break;
    case error.POSITION_UNAVAILABLE:
      console.log("The browser was unable to determine its location: " + error.message);
      break;
    case error.TIMEOUT:
      console.log("The browser timed out before retrieving its location.");
      break;
    default:
      console.log("There was an unspecified or novel error. Nuts.");
  }

  // Clear the previously set timeout so we don't execute the error_handler twice…
  clearTimeout(location_timeout);

  // Call your IP-based geolocation function as a fallback.
  yourIpFallbackFunction();
}

 The Position object contains a timestamp and a Coordinates object that provides a minimal set of location attributes:

Coordinates Attributes in the Position Object
Name Type Example Label
latitude double 33.449708 Decimal Degrees
longitude double -112.075507 Decimal Degrees
altitude double 0 Meters
accuracy double 59 Meters
altitudeAccuracy double 0 Meters
heading double null Degrees (0° = North)
speed double null Meters per Second

Again, browser implementations vary. If you're stationary, heading and speed may be null, NaN, 0 or some combination depending on browser support. If your application just needed a raw lat/lng then you can set up your success handler like this and call it a day:

function your_success_handler(position) {

  // Clear the timeout since the success_handler has executed...
  clearTimeout(location_timeout);

  // Get the coordinates from the HTML5 geolocation API.
  var latitude = position.coords.latitude;
  var longitude = position.coords.longitude;

  // If HTML5 geolocation reports success but fails to provide coordinates...
  if (!latitude || !longitude) {
    console.log("navigator.geolocation.getCurrentPosition returned bad data.");

    // Call your IP-based geolocation function as a fallback.
    yourIpFallbackFunction();
  }
  else {
    // HTML5 geolocation success!
    // Do something with latitude and longitude
  }
}

Wait, What About an Address?

So latitude and longitude are great for adding a marker to a Google map or calling in an air strike but what if you were hoping to get something more personal, like an address? Well, the HTML5 Geolocation Specification doesn't include addresses (yet) but you can easily get one through a reverse geocoding service. Google's reverse geocoding web service is an obvious choice; just watch out for the usage limits and terms of service.

Tags

Comments