Chapter 7. Geolocation

The Geolocation API provides scripted access to geographical location information associated with the hosting device. This gives your applications the ability to locate users and track their latitude and longitude as they move about. This functionality could be used for many interesting use cases such as:

Geofencing

Give your app the ability to schedule a task to alert users the moment they enter or leave a location. You could also target ads for users within a certain city or state.

Geocoding

Combine your app with a service like the Google Maps API (Figure 7-1), and you can translate latitude and longitude coordinates into actual postal addresses.

General tracking

Track distances driven, walked, or ran.

The API itself is device agnostic; it doesn’t care how the browser determines location. The underlying mechanism to obtain the user’s actual location may be through WiFi, GPS, or by the user actually entering a zip code into the device. The API is designed to gather both “one-shot” position requests and repeated position updates. Of course, Geolocation is no different than any of the other HTML5e APIs in regard to bugs, workarounds, and differences in implementations across browsers. After a review of the basics, we’ll dive into the cross-browser nuances.

To access a user’s location, run the following JavaScript:

navigator.geolocation.getCurrentPosition(function(){//show a map});

The web browser asks the user for permission to reveal his location, as shown in Figure 7-2.

After receiving permission, the browser returns a position object with a coords attribute. This allows you to call the following properties, from which you can learn the user’s latitude, longitude, and many other data points:

position.coords.latitude //geographic coordinate in decimal degrees
position.coords.longitude //geographic coordinate in decimal degrees
position.coords.altitude //the height of the position (meters above the ellipsoid)
position.coords.accuracy //accuracy level of latitude and longitude coordinates
position.coords.altitudeAccuracy //specified in meters
position.coords.heading // direction of travel of the hosting device in degrees
position.coords.speed //the device's current velocity (meters per second)

Only the latitude, longitude, and accuracy properties are guaranteed to be available. The rest might come back null, depending on the capabilities of the user’s device and the backend positioning server that it talks to.

The getCurrentPosition() function has an optional third argument, a PositionOptions object:

navigator.geolocation.getCurrentPosition(successCallback,errorCallback, 
  { enableHighAccuracy: true, timeout: 10000, maximumAge: 6000 });

The enableHighAccuracy attribute provides a hint that the application would like to receive the best possible results. If this attribute is true, the device can support it, and the user consents, then the device will try to provide an exact location. Using this attribute may result in slower response times or increased power consumption. The user might also deny this capability, or the device might have more accurate results to provide. The intended purpose of this attribute is to allow applications to inform the implementation that they do not require high accuracy Geolocation fixes and, therefore, the implementation can avoid using Geolocation providers that consume a significant amount of power (think GPS).

The timeout property is the number of milliseconds your web application is willing to wait for a position. This timer doesn’t start counting down until after the user gives permission to share position data. You’re not timing the user; you’re timing the network.

The maximumAge attribute indicates that the application is willing to accept a cached position whose age is no greater than the specified time in milliseconds. This gives you a window of time to pull a cached location from the user device. By defining this attribute, you are saying that you’re fine with where this device was located at x milliseconds in the past.

Your web app will dictate exactly the specificity of your Geolocation needs. So keep in mind battery life and latencies on the user device when you use the above properties. If you need to track the location of the user continuously, the spec defines the watchPosition() function. It has the same structure as getCurrentPosition() and will call the successCallback whenever the device position changes:

navigator.geolocation.watchPosition(successCallback,errorCallback, 
  { enableHighAccuracy: true, timeout: 10000, maximumAge: 6000 });

Use watchPosition() with care in Mobile Safari running on iOS5. As of this writing, there is a known issue: the page will run for roughly four minutes, after which the user will receive a “JavaScript execution exceeded timeout” error. To work around the watchPosition() issue on iOS5, you can implement the following code using getCurrentPosition() with setInterval():

var geolocationID;
(function getLocation() {
     var count = 0;
     geolocationID = window.setInterval(
       function () {
            count++;
            if (count > 3) {  //when count reaches a number, reset interval
                window.clearInterval(geolocationID);
                getLocation();
            } else {
                navigator.
                geolocation.getCurrentPosition(successCallback, errorCallback, 
                 { enableHighAccuracy: true, timeout: 10000 });
            }
        },
        600000); //end setInterval;
})();

Another issue with the specific WebKit Geolocation implementation, is that accessing geolocation activates the Geolocation service, which currently blocks page caching (https://bugs.webkit.org/show_bug.cgi?id=43956). If you simply check the geolocation property, you can avoid this issue:

           function supports_geolocation() {
              try {
                return 'geolocation' in navigator &&
                navigator['geolocation'] !== null;
              } catch (e) {
                return false;
              }
            }

You can view a live demo with all implemented workarounds at http://html5e.org/example/geo.

To track a user over a set of latitude and longitude coordinates, you can use the Haversine formula. With it, your application can calculate the shortest distance over the Earth’s surface and provide an as-the-crow-flies distance between the points. The code you need is:

function calculateDistance(lat1, lon1, lat2, lon2) {
  var R = 6371; // km
  var dLat = (lat2 - lat1).toRad();
  var dLon = (lon2 - lon1).toRad();
  var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
          Math.cos(lat1.toRad()) * Math.cos(lat2.toRad()) *
          Math.sin(dLon / 2) * Math.sin(dLon / 2);
  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  var d = R * c;
  return d;
}
Number.prototype.toRad = function() {
  return this * Math.PI / 180;
}

This distance calculation, along with many others, is available at http://www.movable-type.co.uk/scripts/latlong.html under the Creative Commons Attribution 3.0 License.

The term “geocoding” generally refers to translating a human-readable address into a location on a map. The process of doing the converse, translating a location on the map into a human-readable address, is known as reverse geocoding. The following code is a simple example of how to reverse geocode coordinates returned from the Geolocation API with the Google Maps API:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
    <meta charset="utf-8">
    <title>
Google Maps JavaScript API v3 Example: Reverse Geocoding
</title>
    <link
href="https://google-developers.appspot.com/maps/documentation/
                                   javascript/examples/default.css"
rel="stylesheet">
    <script
src="https://maps.googleapis.com/maps/api/js?sensor=false">
</script>
    <script>
      var geocoder;
      var map;
      var infowindow = new google.maps.InfoWindow();
      var marker;
      function initialize() {
        geocoder = new google.maps.Geocoder();
        var latlng = new google.maps.LatLng(40.730885,-73.997383);
        var mapOptions = {
          zoom: 8,
          center: latlng,
          mapTypeId: 'roadmap'
        }
        map = new google.
        maps.Map(document.getElementById('map_canvas'), mapOptions);
      }

      function codeLatLng() {
        var input = document.getElementById('latlng').value;
        var latlngStr = input.split(',', 2);
        var lat = parseFloat(latlngStr[0]);
        var lng = parseFloat(latlngStr[1]);
        var latlng = new google.maps.LatLng(lat, lng);
        geocoder.geocode({'latLng': latlng}, function(results, status) {
          if (status == google.maps.GeocoderStatus.OK) {
            if (results[1]) {
              map.setZoom(11);
              marker = new google.maps.Marker({
                  position: latlng,
                  map: map
              });
              infowindow.setContent(results[1].formatted_address);
              infowindow.open(map, marker);
            } else {
              alert('No results found');
            }
          } else {
            alert('Geocoder failed due to: ' + status);
          }
        });
      }
    </script>
  </head>
  <body onload="initialize()">
    <div>
      <input id="latlng" type="textbox" value="40.714224,-73.961452">
    </div>
    <div>
      <input type="button" value="Reverse Geocode" onclick="codeLatLng()">
    </div>
    <div id="map_canvas" 
    style="height: 90%; top:60px; border: 1px solid black;"></div>
  </body>
</html>

When working with the Geolocation API, you should detect and wrap available Geolocation mechanisms that are available across different mobile devices. For example, you could detect Google Gears, BlackBerry, and the default Geolocation API within one JavaScript init() method. But why try to code all this yourself, when you could just use a framework? The Geolocation JavaScript frameworks are relatively small in both size and selection.