Using the Geolocation API

2016-04-16T01:55:00Z

The Geolocation API Specification is a W3C Recommendation that provides access to a device’s geolocation through scripting. The API provides two methods for accessing a device’s geolocation and can be used for data collection that includes positional information in the form of latitude and longitude. While easy enough to implement, the API can be a bit challenging without a solid understanding of how it works. This article explains some of the less discussed aspects of using the Geolocation API in an effort toward a better implementation.

If you want to learn more about the Geolocation API Specification, see the W3C website or there is also a tutorial located on the Mozilla Developer Network. These two sites were used in developing the information presented here.

Some Things to Know First

There is more to it than writing some JavaScript to find a device’s location. In fact, there are some potential pitfalls, that if not properly considered, may limit the success of using the API in an application.

No Guarantees

The device supplies location information to the Geolocation API. So, if accuracy is important, it is worth checking to ensure the target device has or is capable of using GPS because API allows for location information to be inferred from network signals including WiFi, cellular and other sources.

Secondly, even though most modern browsers include the Geolocation API, it is still good practice to have the script check before trying to get the device’s location (see full script in Showing the Location on a Map).

Battery

Continued use of the GPS on a device can result in increased battery consumption. Therefore, if the implementation of the Geolocation API uses GPS for extended periods, users may need to plug into a power source or intermittently recharge the device.

User Permission

The user must explicitly grant permission before the Geolocation API can access location information. This makes sense in our world of web-based tracking but users will likely need to be informed about the purpose of collecting location information and how it is handled.

Security

In addition to explicitly granting permission, the Chromium Project (to include Chrome, Opera and others) are moving to restrict the use of the Geolocation API to secure origins (HTTPS). See The Chromium Projects for more information.

Asynchronous

The Geolocation API provides location information asynchronously. Meaning that scripts requesting a device’s location will not wait for the information to be returned before continuing execution. This behavior is by design as it can take several seconds to obtain location information. The easiest way to compensate for this behavior is to use a callback to handle the position information when it becomes available.

Options

The Geolocation API stores the latest position in a cache so it can be more resource friendly. Positions in the cache are managed by a maximum age. When a cached position passes it maximum age, it is replaced by a new one during the next request. The allowable maximum age is defined in a PositionOptions{} object and passed to the method requesting the device’s location.

Objects Used in Requesting a Device’s Location

The two methods provided in the API to access a device’s location are getCurrentPosition() and watchPosition(). Both methods take a SuccessCallback() function and optionally take an ErrorCallback() function and a PositionOptions{} object. When invoked, either method will return a Position{} object containing the device’s location to the specified SuccessCallback() function. If an error occurs during the attempt, as in the case of permission denied or a timeout, the ErrorCallback() function, if specified, is passed a PositionError{} object. The PositionError{} object can be used to make decisions about whether to continue to try to get a device’s location or to inform the use about what went wrong with the current request.

Position{} Object:

The Position{} object contains the coordinates (latitude and longitude) along with the associated accuracy information and a DOM timestamp. The coordinates and the altitude are relative to the WGS84 datum (EPSG:4326). The cords{} property of the Position{} object only has to contain the coordinates and accuracy properties according to the API, but many browsers include additional properties. An full outline of the Position{} object is shown here.

Position{} Object

  Position
├─ coords
│  └─ latitude double - decimal degrees 
│  └─ longitude double - decimal degrees
│  └─ altitude double - meters
│  └─ accuracy double - meters
│  └─ altidudeAccuracy double - meters
│  └─ heading double - true north compass heading
│  └─ speed double - meters per second
└─ timestamp DOM timestamp - date and time of position

Example of the Position{} object:

{coords: {
  accuracy:80,
  altitude:null,
  altitudeAccuracy:null,
  heading:null,
  latitude:30.123456,
  longitude:-97.123456,
  speed:null},
  timestamp: 1450411162743
  }

PositionError{} Object

The PositionError{} object is passed to the specified ErrorCallback() function if an error occurs while attempting to get a device’s location. The PositionError{} object simply has an error code and an error message properties.

PositionError
├─ code - short
│    └─ 1 = PERMISSION_DENIED
│    └─ 2 = POSITION_UNAVAILABLE
│    └─ 3 = TIMEOUT
└─ message – Detailed description of the error.

Example of the PositionError{} object:

{code: 1, message: "User denied Geolocation"}

PositionOptions{} Object

The final object used when requesting a device’s location is the PositionOptions{}. The PositionOptions{} object is essential when attempting to get the best results possible or when managing the maximum age of cached positions. The PositionOptions{} object can have any combination of the following three properties.

PositionOptions
├─ enableHighAccuracy boolean - False may utilize geolocation providers other than the GPS
├─ timeout milliseconds - how long to try to get a location before returning an error
└─ maximumAge milliseconds - the maximum age of a cached position before attempting to get an updated location,
                             otherwise the cached position is used

Example of the PositionOptions{} object:

{maximumAge: 5000, timeout: 20000, enableHighAccuracy:  true}

Requesting a Single Position

The getCurrentPosition() method is a single request to get a device’s location. The following script defines the PositionOptions{} object, a success callback function named logPosition() and an error callback function named logError(). The last line invokes the getCurrentPosition() method from the navigator.geolocation object in the browser. Upon successfully obtaining a device’s location, the logPosition() function is passed the position PositionObject{}.

// options
// maximumAge : don’t use positions cached for more than 5 seconds
// timeout: only try for 20 seconds to get position.  Then return error
// enableHighAccuracy: get the best result possible (GPS)
var options = {"maximumAge": 5000, "timeout": 20000, "enableHighAccuracy":  true}

// success callback that is invoked when location is retrieved 
function logPosition(position) {
    console.log(position);
}

// error callback that is invoked when an error occurs 
function logError(err) {
    console.log(err);
}

navigator.geolocation.getCurrentPosition(logPosition, logError,options);

Continuously Watching the Position

The Geolocation API also allows for monitoring the current location by using the watchPosition() method. Watching a position means an updated position object is repeatedly passed to the success callback function whenever the device’s location changes. The watchPosition() method will continue passing positions until the clearWatch() method is called. The watchPosition() method takes the same arguments as the getCurrentPosition() method.

// options
// maximumAge : don’t use positions cached for more than 5 seconds
// timeout: only try for 20 seconds to get position.  Then return error
// enableHighAccuracy: get the best result possible (GPS)
var options = {"maximumAge": 5000, "timeout": 20000, "enableHighAccuracy":  true}

// success callback that is invoked when location is retrieved 
function logPosition(position) {
    console.log(position);
}

// error callback that is invoked when an error occurs 
function logError(err) {
    console.log(err);
}

// cancel the watchPosition() activity
function stop() {
    navigator.geolocation.clearWatch(watchId);
}

var watchId = navigator.geolocation.watchPosition(logPosition, logError,options);

Showing the Location on the Map

Let’s use the whatchPosition() example to make an example HTML page that displays the current location on a Leaflet map. The demo of showing a deive's location ona a map can be seen watchPosition() can be seen here.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <title>Geolocation on Leaflet Map</title>
    <!-- Add the Leaflet stylesheet and script from the cdn -->
    <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.css" />
    <script src="http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.js"></script>
    
    <!-- using the leaflet easy button (https://github.com/CliffCloud/Leaflet.EasyButton) -->
    <link href="http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
    <link rel="stylesheet" href="easy-button.css" />
    <script src="easy-button.js"></script>
    
    <style>
      /* style for  fullscreen map */
       html, body, #map {
       height:100%;
       width:100%;
       padding:0px;
       margin:0px;
       color: #000000;
       }
       
       /* style for statusbar */
       .statusbar {
        border-left: solid 1px #a0a0a0;
        border-right: solid 1px #a0a0a0;
        border-bottom: solid 1px #a0a0a0;
        background-color: #f0f0f0;
        text-align: left;
        position:absolute;
        bottom:0;
        width:100%;
        }
        .statusbar ul {
            margin-left: 0;
            padding-left: 0;
            display: inline;
            ;
        } 
        .statusbar ul li {
            margin-left: 0;
            padding: 3px 15px;
            list-style: none;
            display: inline;
            width: 200px;
        }
        #statusbar ul li.first {
            margin-left: 0;
            border-left: none;
            list-style: none;
            display: inline;
        }
    </style>
  </head>
  <body>
    
    <!-- the leaflet map -->
    <div id="map"></div>
    
    <!-- status bar for position info -->
    <div class="statusbar">
      <ul>
          <li><span>latitude: </span><span id="lat">0</span></li>
          <li><span>longitude: </span><span id="lng">0</span></li>
          <li><span>zoom: </span><span id="zoom">0</span></li>
          <li><span>accuracy: </span><span id="accuracy">0</span></li>
      </ul>
    </div>
    
    <script>
      
      // *********** leaflet map functions ***************
      
      // create the Leaflet map
      var map = L.map('map').setView([37.78, -92.85], 4);

      // add openstreetmap tile layer as a basemap
      L.tileLayer('http://otile{s}.mqcdn.com/tiles/1.0.0/map/{z}/{x}/{y}.jpg', {
        subdomains: '1234',
        attribution: '<a href="http://www.mapquest.com/" target="_blank">© OpenStreetMap and contributors</a>| \
                      <a href="https://github.com/CliffCloud/Leaflet.EasyButton" target="_blank">EasyButton</a>'
      }).addTo(map);
      
      // update the zoom level in status bar when it changes
      map.on('zoomend', function(e) {
        document.getElementById('zoom').innerText=map.getZoom();
      });
      
      // create a circle to be drawn on the map that shows the device's location
      var circle = L.circleMarker(
          [37.78, -92.85],
          {color: '#ffffff',fillColor: '#0099FF',fillOpacity: 0.8, 'weight': 3, 'opacity':.9})
          circle.bindPopup("You are here");
      
      // the easy button to toggle watchPosition() see
      //https://github.com/CliffCloud/Leaflet.EasyButton
      L.easyButton({
        id: "toggle-watch", position: "topleft",  leafletClasses: true,  
        states:[{
          stateName: "toggle-watch-state",
          onClick: function(button, map){   // define function for easyButton to toggle watchPosition()
            toggleWatchPosition();
          },
          title: "Toggle watch position.",
          icon: "fa-location-arrow"
        }]
      }).addTo(map);
      
       // *********** geolocation api  functions ***************
       
       // initialize watchId to identify the watchPosition() activity
       var watchId = 0
       
      // set the options for use with the watchPosition() method
      var options = {"maximumAge": 5000, "timeout": 20000, "enableHighAccuracy":  true}
      
      // define the success callback for the watchPosition() method
      // fired everytime the position is updated
      function showPosition(position) {
        // move circle to current location
        circle.setLatLng([position.coords.latitude,position.coords.longitude]);
        // pan the map to the current location
        map.panTo(new L.LatLng(position.coords.latitude, position.coords.longitude));
        // update statusbar with position information
        document.getElementById("lat").innerText=parseFloat(position.coords.latitude).toFixed(6);  // get latitude from position
        document.getElementById("lng").innerText=parseFloat(position.coords.longitude).toFixed(6); // get longitude from position
        document.getElementById("accuracy").innerText=parseInt(position.coords.accuracy);          // get accuracy from position
        document.getElementById("zoom").innerText=map.getZoom();                                   // get map zoom from map
      }
      
      // define the error callback for the watchPosition() method
      // fired when attempt to get location fails
      function showError(err) {
        alert(err.message);
      }
      
      // function to toggle watchPosition() method
      function toggleWatchPosition() {
        if ("geolocation" in navigator) {                                 // check that the geolocation api is available
          if (watchId > 0) {                                              // if watch id > 0 then cancel the existing watch
            navigator.geolocation.clearWatch(watchId);                    // clear/cancel the watch
            watchId = 0;                                                  // reset watchId variable
            map.removeLayer(circle);                                      // remove the circle from the map
            document.getElementById("toggle-watch").style.color="#000000";// reset button icon to black
          } else {                                                        // if wachId == 0 then start a new watch
            watchId = navigator.geolocation.watchPosition(showPosition, showError,options); // start watchPosition() and assign watchId
            circle.addTo(map);                                            // add circle on map for position
            document.getElementById('toggle-watch').style.color="#0099FF"; // set button icon to blue
          }
        } else {
          alert("The Geolocation API is not available in your browser.");  // inform the user the api is not available in their browser
        }
      }
      
    </script>
  </body>
</html>

See a demo of the above html page and script it the here.