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:
- the function to run on a successful location request
- the function to run as when the request fails
- 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.
Sometimes there will be conflict with city names… do this script will avoid such conflicts??/