Using the Geolocation API

by Rob Larsen

The following is probably the last long-form article you’ll see here for a while. I’m in full on book mode for the next couple of months, so I’m not expecting to be writing a ton here for the foreseeable future.

Anyway, the following is actually inspired by the kind of work we’re going to be doing for the book. I’m not actually doing any code-heavy writing for the book, so I wanted to get my hands dirty with this sort of content. It’s fun.

As an aside, I’ll have another (short) post about the book shortly. I’ve got a full allotment of co-authors and I’d like to give them, and the project, a little shine before I turn into a writing hermit.

And now… Geolocation


One of the most powerful aspects of mobile web app development is the ability to blur the line between the real world and the world on the screen. Allowing users to interact with physical places in novel ways is driving startups across the world and is infiltrating some of the most popular sites and applications on the web. Facebook, Foursquare, Twitter, Google+ and countless other services have built the idea of location into the core of their applications. You too can do the same in your mobile web app by taking advantage of the well-supported Geolocation API.

Whether it’s interaction with your own location based services or with a third party API, like the Google Maps API in use in this recipe, the journey begins with getting the user’s latitude and longitude.

In this article you’ll learn how to:

  • Use the W3C Geolocation API to get a user’s latitude and longitude
  • Smoothly handle devices without Geolocation support, providing a reasonable fallback for older devices
  • Use the Google Maps API to place a marker indicating the user’s location, labeled with a friendly place name

The Basics of the Geolocation API

Before we dive into the heart of the example let’s quickly look at the Geolocation API.

The elevator pitch is to the point- the W3C’s Geolocation API allows developers to retrieve the geographical location of a device. It became a Candidate Recommendation, the level at which the W3C deems features and functionality pretty much settled, in September of 2010 and already has support across a variety of devices and browsers. At the present time it’s supported all the major smartphone browsers and even on the desktop it’s supported by all major browsers except Internet Explorer 6, 7, 8 and Safari 3.2 and 4.0.

As a note, the Geolocation API was heavily influenced by the analogous functionality provided by the Google Gears plugin. This is why you often see the mothballed Google Gears plugin referenced as a fallback in many geolocation examples.

The API itself is straightforward. It provides a navigator.geolocation object which in turn provides two methods (watchPosition and getCurrentPosition) which allow the browser to query the device’s location through the use of location information servers. If you’re getting a location for use in a search or in a check-in, then getCurrentPosition is the method you want to use as it’s designed for a single location lookup. If you’re tracking a user’s location over time, then watchPosition is the way to go since it’s designed to be used over a longer period of time.

Location information is pulled from a variety of sources including IP address, device GPS, Wi-Fi and Bluetooth MAC address, RFID, or Wi-Fi connection location. The different level of precision inherent in these several methods is exposed by the API as an accuracy property.

Now that we’ve taken a look at the Geolocation API, let’s walk through our code in depth.

Getting Started with the Geolocation API

While the Geolocation API is straightforward, getting it up and running smoothly in the real world is a little bit tricky. Accounting for a successful result is one thing, making sure there’s a decent response for browsers without geolocation capability or in other instances where geolocation isn’t available is another.

Our example will touch on ways to minimize these issues and will illustrate the basics of a successful request.

Testing for the Geolocation object and Querying the User’s Location

The first thing you’ll need to do when working with Geolocation is to test whether or not it’s actually available in the browser. This is done by testing against the presence of the navigator.geolocation object. As you’ll see in the following code sample, this is a simple if…else block with a call to the navigator.getCurrentPosition() method when the object is present and a fallback when it’s not available.

The method getCurrentPosition() takes three arguments:

  1. the function to run on a successful location request
  2. the function to run as when the request fails
  3. a PositionOptions object containing other optional configuration objects.

In our case we’re passing in two named functions, success and failure, and an optional timeout of five seconds, which will keep things moving if something goes awry with the request.

Listing 1 Testing for the presence of the navigator.geolocation object

if (navigator.geolocation){  // does the geolocation object exist?             
  navigator.geolocation.getCurrentPosition( 
    success, 
    failure, 
    {timeout:5000} 
  );          
} else {
  failure();  
}      

In addition to the timeout seen in the previous example the PositionOptions object accepts two other options- enableHighAccuracy and maximumAge. enableHighAccuracy indicates that you would like receive the best possible results at the potential cost of speed or battery performance. maximumAge sets a limit, in milliseconds, for the age of a cached position object. The browser caches recent position location responses. Setting maximumAge to 0 will immediately force the browsers to try to obtain a new location.

Handling a successful geolocation request

The first function we’ll look at it is our success function. You can see it in Listin 1.2

In our example we’re using the Google Maps API to display a marker with the user’s current location.

The function accepts a single data argument. This argument is automatically passed into the function by the geolocation API. This is object is defined in the specification to contain two properties coords and timestamp. timestamp is, as expected, a timestamp indicating the age of the position information. For this example we’re most interested in the coords object which contains a latitude/longitude pair indicating the user’s position.

Moving on from the single argument, you’ll see several Google Maps specific variables.

The first, GM,represents a simple technique to speed up JavaScript. By creating a local representation of the google.maps object we save lookups to the global space. In general, local variables are faster. This is especially important with mobile which devices don’t have the fastest JavaScript engines.

Every little bit helps.

The most important piece, from a geolocation perspective, is the use of two properties, data.coords.latitude and data.coords.longitude, to build a new Google Maps LatLng object. The LatLng object is a core component of Google Maps. At its core it’s a latitude/longitude pair enhanced with methods and properties used throughout the API. To create one in our example you simply pass it the two properties of the data.coords object. We store that in our position variable.

We now have our user’s location, ready to place on the map.

The next section we’re using the Google Maps Geocoder to get a friendly label for the user’s location. Geocoding works in two ways. Normal geocoding means you pass the service an address string and it will return a series of geographical results. In our case we’re doing reverse geocoding, which means we pass the service a latitude/longitude pair and the service returns whatever it knows about the location.

Listing 2 Successfully handling a geolocation request

var success = function( data ){ //the data object, passed into success
  var GM = google.maps,
      mapOptions = {
        zoom: 12,
        center:  defaultPosition,
        mapTypeId:  GM.MapTypeId.ROADMAP
      },
      map = new GM.Map(  document.getElementById('map'), mapOptions),
      position = new GM.LatLng( 
        data.coords.latitude, //accessing the coords property
        data.coords.longitude 
      ),      
      niceAddress = "Your location",
      geocoder = new GM.Geocoder();
      geocoder.geocode( { 'latLng' : position  }, 
        function( results, status ) {
          if ( status == GM.GeocoderStatus.OK ) {
            if (results[0]) {
              niceAddress = results[0].formatted_address;
            }
          } 
          var infowindow = new GM.InfoWindow({
            map: map,
            position: position,
            content: niceAddress
          });
        });
      map.setCenter(position);
    }

Handling a Geolocation Failure

Our failure function handles two negative situations. If the user doesn’t have a geolocation enabled browser or if there’s an error in the geolocation lookup, this function is ready to step in and save the day. The failure function can be seen in Listing 3

You’ll see the setup is similar to the success function with a Google Maps object being created with some smart defaults.

The major difference is in the way we get the latitude and longitude for the map. Instead of getting the coordinates from a geolocation response we create a simple form to allow the user to enter their location. Inside the formResponse function we use then use the Google Maps Geocoding service to get a latitude and longitude pair corresponding to the location in the form submission.

Additionally we use the geolocation error response, if it exists, to build out a slightly more useful error message. If a browser supports geolocation and has some issue with the location request it should return an error response as the single argument to the provided callback function.

Listing 3 The failure Function

var failure = function( error ){ //The potential error response
  var  GM = google.maps,
       mapOptions = {
         zoom: 12,
         center:  defaultPosition,
         mapTypeId:  GM.MapTypeId.ROADMAP
       },
       map = new GM.Map(  document.getElementById('map'), mapOptions),
       formResponse = function(e){
         var geocoder = new GM.Geocoder(),
             position = defaultPosition,
             niceAddress =  "Sorry We Couldn't Find Your Location";
         geocoder.geocode(
           { 'address':  document.getElementById("location").value }, 
           function( results, status ) {
             if ( status ==  GM.GeocoderStatus.OK ) {
               if (results[0]) {
                 niceAddress =  results[0].formatted_address;
                 position = new GM.LatLng( 
                   results[0].geometry.location.lat(),
                   results[0].geometry.location.lng() 
                 )
               }
             } 
           var options = {
             map : map,
             position :  position,
             content :  niceAddress
            },
           infowindow = new  google.maps.InfoWindow(options);
           map.setCenter(options.position);
           document.getElementById("geocode").style.display="none";
         }
       )
     return false;
   }
   var  fallback = document.createElement("form");
   fallback.id="geocode";
   if ( error ) { 
     switch(error.code) {//Error Handling based on error.code   
      //HANDLE ERRORS//
   }      
 }  
 fallback.innerHTML  = "<label for='location'>Eneter Your Location" + 
  "<input  type='text' id='location' /></label><input type='submit'  />";
 fallback.onsubmit  = formResponse;
 document.getElementById("main").appendChild(  fallback );
};

The error response contains a code object indicating the type of error and a human readable message string that’s defined to be used in debugging or for error logs. There are four potential values for the error.code. These can be seen in Table 1:

Table 1 Possible error response codes

Code

Name

Definition

0

UNKNOWN_ERROR

The location lookup failed due to an undefined error

1

PERMISSION_DENIED

The location lookup failed because the application does not have permission to use the Geolocation API.

2

POSITION_UNAVAILABLE

The position of the device could not be determined.

3

TIMEOUT

The location lookup took longer than the length of time defined in

Putting it all together

Listing 4 shows the completed function. In it we’ve wrapped our two methods into a larger function called loadMap. Doing so allows us to streamline the code by creating a single set of defaults for the map and to encapsulate all of the functionality under a single function so as to keep the global namespace as neat as possible.

Listing 4 The Completed Function

var loadMap = function(){
  var GM = google.maps,
      defaultPosition = new GM.LatLng(42, -71),
      mapOptions = {
      zoom: 12,
      center: defaultPosition,
      mapTypeId: GM.MapTypeId.ROADMAP},
    map = new GM.Map(
      document.getElementById('map'),
      mapOptions
    ),
    success = function( data ){
      var position = new GM.LatLng(
        data.coords.latitude,
        data.coords.longitude
      ),
         niceAddress = 'Your location',
         geocoder = new GM.Geocoder();
      geocoder.geocode({
        'latLng': position },
         function( results, status ) {
           if ( status == GM.GeocoderStatus.OK ){
             if (results[0]) {
               niceAddress = results[0].formatted_address;
             }
           }
           var infowindow = new GM.InfoWindow({
             map: map,
             position: position,
             content: niceAddress
           });
         }
       );
      map.setCenter( position );
    },
    failure = function( error ){
      var formResponse = function(){
        var geocoder = new GM.Geocoder(),
            position = defaultPosition,
            niceAddress = 'Sorry We Couldn't Find Your Location';
        geocoder.geocode({
     'address':document.getElementById('location').value
    },
          function( results, status ) {
            if ( status == GM.GeocoderStatus.OK ){
              if (results[0]) {
                niceAddress = results[0].formatted_address;
                position = new GM.LatLng(
                  results[0].geometry.location.lat(),
                  results[0].geometry.location.lng()
                )
               }
             }
             var options = {
                map: map,
                position: position,
                content: niceAddress
              },
              infowindow = new google.maps.InfoWindow(options);
              map.setCenter(options.position);
              document.getElementById('geocode').style.display='none';
           }
         )
       return false;
     }
     var fallback = document.createElement('form');
     fallback.id='geocode';
      if ( error ) {
       switch(error.code) {
         case error.PERMISSION_DENIED:
     	    fallback.innerHTML += "<p>You chose not share geolocation data. Please, use the form below. </p>" ;  
         break;  
	 case error.POSITION_UNAVAILABLE:
            fallback.innerHTML += "<p>Sorry, we couldn't determine your location. Please, use the form below. </p>" ;
         break;  
         case error.TIMEOUT: 
            fallback.innerHTML += "<p>Sorry, the location request time out. Please, use the form below. </p>" ;
         break;  
         default: 
            fallback.innerHTML += "<p>Sorry, there was an error. Please use the form below. </p>" ;
         break;
       }  	
    }
    fallback.innerHTML += "<label for='location'>Eneter Your Location <input type='text' id='location' /></label><input type='submit' />";
    fallback.onsubmit = formResponse;
    document.getElementById("main").appendChild( fallback );
  };
  if (navigator.geolocation){
    navigator.geolocation.getCurrentPosition( success, failure, {timeout:5000} ) ;
  } else {
    failure();
  }
}

Summary

With that we’ve walked through the basics of the geolocation API. While our example utilizes the Google Maps API, any exploration of location based services can be based on the same pattern. Use the navigator.geolocation object where available, and then design in a simple fallback for non-supporting browsers and devices.

code used in the geolocation sample

Recent Reading (JS Natives Duke it Out, Regexp in jQuery, Performance, a New Image Format?)

by Rob Larsen

Sorry, it’s been a while. I’ve been busy at work, I’ve been wringing every last bit out of summer on my bike, and I’ve spent a lot of my free time on my upcoming CSS presentation, so I haven’t been posting as much as I would like. Fall is here. Which means I should have more time for writing. That’s cool.

Anyway, to break the ice here are a few articles that have recently caught my attention.
Read the rest of this entry »

Required Reading: Yahoo Research on Mobile Cache Size

by Rob Larsen

With more and more people embracing the idea of producing HTML5 mobile web apps, research like this is becoming vital. There are a lot of gotchas in this article, especially if you’ve been ignoring this space.

You should read the whole article, but I just want to point out a pair of findings before I send you on your way. One has been a known issue for a while (it’s a ySlow rule, after all,) but it’s still worth pointing out. The other… wow… my emphasis.

Results varied wildly across the three most recent versions of iOS. Astonishingly, Mobile Safari on iOS 3.1.3 did not cache components of any size, despite having an apparently unlimited page cache size. This is troubling since it means iOS 3.1.3 users are likely getting a suboptimal browsing experience, especially if they aren’t using wifi. The unlimited page cache size does little good here, since it only comes into play for back/forward navigations. This behavior is a significant change from what others observed in previous iOS releases and I can’t imagine any good reason for it, so I suspect this may be a bug.

Mobile Safari on iOS 3.2 (which is only available on the iPad) does not exhibit this bug. Its 25.6KB component limit and ~281.6KB total cache limit are better than nothing, but they still seem paltry compared to the other devices tested. Uniquely among iOS devices, the iPad appears to limit the size of pages in the page cache to 25.6KB, the same as its component size limit.

Read the rest of the article:

Mobile Browser Cache Limits: Android, iOS, and webOS

Performance Tip: When Linking to JavaScript on the Google Ajax Library CDN, Use a Specific Version Number

by Rob Larsen

I’ve seen this a few times over the past few months:


<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>

What that basically says to Google is “give me the latest 1.* branch version of jQuery and make sure it’s minified.”
Read the rest of this entry »

Browser Size, a Simple But Useful Tool From Google

by Rob Larsen

Browser Size does just what the name implies. Enter a URL, hit “go” and you’ll see just what percentage of the internet will see what without scrolling. Scrolling isn’t quite the bugbear it once was, but for certain types of pages immediate impact is still very important (campaign landing pages are a good example,) so having this as an easily accessible tool is really nice. Sure, designers have these guides in Photoshop and developers can resize the window with the Web Developer Toolbar, but this handy web-based tool is just the kind of thing to use in a meeting.

For an example of the output, here’s this site put through the wringer:
Read the rest of this entry »

Of Interest- the Google Chrome Operating System

by Rob Larsen

Between Palm letting me code directly on a killer handheld device, HTML5 starting to show up in real browsers and now Google saying things like the following [my emphasis], it’s a damn fine time to be coding on the front end:

Google Chrome OS will run on both x86 as well as ARM chips and we are working with multiple OEMs to bring a number of netbooks to market next year. The software architecture is simple — Google Chrome running within a new windowing system on top of a Linux kernel. For application developers, the web is the platform. All web-based applications will automatically work and new applications can be written using your favorite web technologies. And of course, these apps will run not only on Google Chrome OS, but on any standards-based browser on Windows, Mac and Linux thereby giving developers the largest user base of any platform.

This is also interesting for me personally, as I’m a big fan of my Dell Mini 9 Inspiron and I’d be tempted to dual boot it- just to double my geeky pleasure.

Recent Reading (HTML5, CSS Fonts, JavaScript, rel=nofollow, Google Analytics)

by Rob Larsen

  1. New elements in HTML 5

    An older (2007) article, but still pretty interesting. I’ve been slowing working my way through some HTML5 stuff. This was part of that effort. If, like me, you’re fascinated by where the web is going*, you should definitely check the article out.

  2. Event Tracking Guide

    With event tracking open to everyone we’ve been going nuts with them at work. I’ve also been going nuts with them at home.

    I’m basically smothered in event tracking.

    I’m glad to have it. It saves me from doing non-standard stuff (faking page views) and it saves our analytics analyst the time of having to filter out fake page views from the data.

    Yay Google.

  3. Whiteboard Friday – How Do We Plug the Nofollow Leak?

    This isn’t a read, it’s a video :) But, if you’ve got any interest in SEO, then this discussion of the recent change in the way Google treats rel=nofollow and the way PageRank flows is a must-view.

    SEOmoz Whiteboard Friday – How Do We Plug the Nofollow Leak? from Scott Willoughby on Vimeo.

  4. 8 fonts you probably don’t use in css, but should

    While I’ve been messing with “real” fonts on the web using Cufon, it’s still a hell of a lot easier to just use old school fonts. The referenced article does a great job of pointing out some neglected fonts that might come in handy on a project.

  5. Event delegation in JavaScript

    Attaching an event handler to the document and then delegating based on the target has been on my mind a lot recently. I like the idea and it’s actually come up in two code reviews this week as an alternative to traditional event handling.

    Serendipitous then, that Nicholas Zakas decided to write it up so that I have something fresh to point to when I reference the technique.

*I’m also fascinated by where the web is and where the web has been, so I’m not really picky on those fronts.