13. Compass

The Compass API allows a PhoneGap program to determine the device’s heading along a two-dimensional plane roughly corresponding to the surface of the earth. Many modern smartphones have a physical compass (on a chip), and the API simply queries the chip and returns an angle between 0 and 360 indicating the direction the device is pointing. A value of 0 indicates the device is pointing north, 90 indicates it is pointing east, 180 refers to south, and 270 refers to west.


Image Note

Not all smartphones have a compass. The iPhone series of devices have always had one, but RIM didn’t add one until BlackBerry 7 OS devices.


The Compass API works in a very similar manner to the Accelerometer API described in Chapter 10. Using the API, developers can manually query the device’s orientation or can set up a watch to have the API periodically report orientation to the application on a specific frequency or when the device’s orientation changes by a minimum threshold.

Getting Device Heading

To query the device’s orientation, simply call the following method:

navigator.compass.getCurrentHeading(successFunction, 
  errorFunction);

Passed to the API are the names of two functions that are called depending on whether the API is returning a result. The successFunction is called when a reading has been successfully made, and the errorFunction is called when there is an error reading the compass.

When called, the successFunction is passed the compassHeading object, which consists of the following components:

magneticHeading: The device’s current heading in degrees ranging from 0 to 359.99.

trueHeading: The device’s current heading relative to the geographic North Pole in degrees ranging from 0 to 359.99. A negative value indicates that a value could not be determined.

headingAccuracy: A value indicating the deviation, in degrees, between the magneticHeading and trueHeading values.

timestamp: The time when the heading values were measured (in milliseconds since the Unix Epoch, January 1, 1970).

The earth has two North Poles: the geographic North Pole (which is the exact, geographic top of the earth) and the magnetic North Pole (which regularly moves around because of magnetic changes in the earth’s core). You’ll have to determine which matters for your particular application. On the Android platform, the associated APIs return values only for magneticHeading, so headingAccuracy will always be zero.

Unlike the Accelerometer API, when the Compass API calls the errorFunction, it passes in a CompassError object that allows a program to understand a little bit about why the error occurred. The most useful aspect of this is that you can tell whether the compass is supported on the device.

Let’s take a look at an application that implements this API. Example 13-1 is an application that queries the compass and updates the screen with the current heading every time a button is clicked. This is not necessarily the most robust example, but it illustrates how the API works.


Example 13-1

<!DOCTYPE html> 
<html> 
  <head> 
    <title>Example 13-1</title> 
    <meta http-equiv="Content-type" content="text/html; 
      charset=utf-8"> 
    <meta name="viewport" id="viewport" 
      content="width=device-width, height=device-height, 
      initial-scale=1.0, maximum-scale=1.0, user-scalable=no;" 
   /> 
   <script type="text/javascript" charset="utf-8" 
     src="phonegap.js"></script> 
   <script type="text/javascript" charset="utf-8"> 

     // Heading content 
     var hc; 
     //PhoneGap Ready variable 
     var pgr = false; 
     //Has compass, assume true 
     var hasCompass = true; 

     function onBodyLoad() { 
      //alert("onBodyLoad"); 
      document.addEventListener("deviceready", onDeviceReady, 
        false); 
     } 

     function onDeviceReady() { 
      //Get a handle we'll use to adjust the heading 
      //content 
      hc = document.getElementById('headingInfo'), 
      //Set the variable that lets other parts of the program 
      //know that PhoneGap is initialized 
      pgr = true; 
     } 

     function getHeading() { 
       if (pgr == true) { 
         if  (hasCompass == true) { 
           //Clear the current heading content, 
           //just in case it takes some time to get the reading 
           hc.innerHTML = 
             "Getting heading information from compass."; 
           //get the current heading 
           navigator.compass.getCurrentHeading( 
             onHeadingSuccess, onHeadingError);
         } else { 
           alert("No compass, please stop clicking 
             the button."); 
         } 
       } else { 
         alert("Please wait. PhoneGap is not ready."); 
      } 
     } 

     function onHeadingSuccess(heading) { 
      //We received something from the API, so... 
      //first get the timestamp in a date object 
      //so we can work with it 
      var d = new Date(heading.timestamp); 


      //Then replace the page's content with the 
      //current acceleration retrieved from the API 
      hc.innerHTML = "<b>Magnetic Heading:</b> " +
        heading.magneticHeading + 
        "<br /><b>True Heading:</b> " +  heading.trueHeading + 
        "<br /><b>Heading Accuracy:</b> " +
        heading.headingAccuracy + "<br /><b>Timestamp:</b> " +
        d.toLocaleString();
      } 

      function onHeadingError(compassError) { 
        if (compassError.code == 
         CompassError.COMPASS_NOT_SUPPORTED) { 
         hc.innerHTML = "Compass not available."
         alert("Compass not supported."); 
         hasCompass == false; 
       } else if (compassError.code == 
         CompassError.COMPASS_INTERNAL_ERR) { 
         alert("Compass Internal Error"); 
       } else { 
         alert("Unknown heading error!"); 
       } 
      } 

    </script> 
  </head> 
  <body onload="onBodyLoad()"> 
   <h1>Example 13-l</h1> 
   <p>This is an Apache PhoneGap application that measures 
       device heading using the Compass API.<br /> 
   <input type="button" value="Measure Heading" 
     onclick="getHeading();"></p> 
   <p id="headingInfo">Nothing to see here (yet), clickthe
     button.</p> 
  </body> 
</html> 


The application starts by defining several variables that are used to control the application. Since the application relies upon the user clicking a button to measure the heading, the application will need to know whether PhoneGap has initialized yet, so the pgr variable is used to track status. The hasCompass variable is used to track whether the Compass API returns an error indicating that the compass is not available. These variables prevent the application from trying to do things that are not supported.

In getHeading, the application checks to make sure PhoneGap has initialized and that a previous call to getCurentHeading didn’t return an error indicating that the compass wasn’t available. When all is clear, it makes a call to getCurrentHeading to measure the device’s heading. If this is successful, the onHeadingSuccess function is called, and the application’s UI is updated with heading information. If there’s a problem, onHeadingError is called, the user is told what happens, and hasCompass is updated if needed.

The value for timestamp is converted to human readable format using the following code:

var d = new Date(heading.timestamp);
hc.innerHTML = "Timestamp: " + d.toLocaleString();

Figure 13-1 shows the application running on an Android device.

Image

Figure 13-1 Example 13-1 running on a Android device

Notice that heading accuracy is zero and the magnetic and true heading values are the same; that’s because the Android OS supports only the magnetic heading.

Watching Device Heading

For an application that relies upon heading information, manually querying the compass is inefficient. Fortunately, PhoneGap provides simple watch mechanisms that allow an application to query the compass repeatedly over a specific time interval or whenever the heading changes by more than a configurable number of degrees. The following sections describe each of these options in detail.

watchHeading

The watchHeading function allows an application to define a compass watch that fires repeatedly on a specific time interval. An application defines the watch using the following code:

var watchOptions = { frequency: 250 };
watchID = navigator.compass.watchHeading(onHeadingSuccess,
  onHeadingError, watchOptions);

When creating the watch, a program must pass in the names of two functions that are called depending on whether the heading measurement is successful. The successFunction is called when a reading has been successfully made, and the errorFunction is called when there is an error reading the compass. When called, the successFunction is passed the compassHeading object that contains information obtained from the compass. The previous section describes the compassHeading object in detail.

In this example, the code first creates a watchOptions object that defines a watch frequency of 250 milliseconds (0.25 seconds). A frequency value of 1000 would configure a watch that fired every second. Next, the code creates the watch and assigns the result of that operation in the watchID variable. The watchID is important since it allows you to later cancel the watch using the following code:

navigator.compass.clearWatch(watchID);

Let’s take a look at an application that implements this API. Example 13-2 is an application that displays a simple compass graphic and periodically (four times a second) queries the compass and rotates the compass image to show the device’s current heading.


Example 13-2

<!DOCTYPE html> 
<html> 
  <head> 
    <title>Example 13-2</title> 
    <meta http-equiv="Content-type" 
      content="text/html; charset=utf-8"> 
    <meta name="viewport" id="viewport" 
      content="width=device-width, height=device-height, 
      initial-scale=1.0, maximum-scale=1.0, user-scalable=no;" 
    /> 
    <script type="text/javascript" charset="utf-8" 
      src="jquery.js"></script> 
    <script type="text/javascript" charset="utf-8" 
      src="jQueryRotate.2.1.js"></script> 
    <script type="text/javascript" charset="utf-8" 
      src="phonegap.js"></script> 
    <script type="text/javascript" charset="utf-8"> 

      var hi, watchID; 

      function onBodyLoad() { 
       document.addEventListener("deviceready", onDeviceReady, 
         false); 
       //Get a handle to the headingInfo element of the page 
       hi = document.getElementById('headingInfo'), 
    } 
    function onDeviceReady() { 
      //Set up the watch 
      //Read the compass 4 times a second 
      var   watchOptions = { frequency: 250 }; 
      watchID = navigator.compass.watchHeading( 
        onHeadingSuccess, onHeadingError, watchOptions); 
    } 

    function onHeadingSuccess(heading) { 
      var hv = Math.round(heading.magneticHeading); 
      hi.innerHTML = "<b>Heading:</b>" + hv + "degrees"; 
      $("#compass").rotate(-hv); 
    } 

    function onHeadingError(compassError) { 
      //Remove the watch since we're having a problem 
      navigator.compass.clearWatch(watchID); 
      //clear the Heading value from the page 
      hi.innerHTML = ""; 
      //Then tell the user what happened. 
      if (compassError.code == 
        CompassError.COMPASS_NOT_SUPPORTED) { 
        alert("Compass not supported."); 
      } else if (compassError.code == 
        CompassError.COMPASS_INTERNAL_ERR) { 
        alert("Compass Internal Error"); 
      } else { 
        alert("Unknown heading error!"); 
      } 
    } 

    </script> 
  </head> 
  <body onload="onBodyLoad()"> 
    <h1>Example 13-2</h1> 
    <img src="compass.png" id="compass" align="midd1e" /><br /> 
    <p id="headingInfo"></p> 
  </body> 
</html>


Instead of muddying the example by filling these pages with the code needed to rotate the graphic, I decided to use a jQuery (www.jquery.com) plug-in called jQueryRotate (http://code.google.com/p/jqueryrotate) to take care of that aspect of the program for me. This approach dramatically simplifies the example and allows me to get right to the PhoneGapness of the application.

Looking at the code, you’ll see two <script> tags at the start of the application that load the jQuery module and the jQueryRotate plug-in.

<script type="text/javascript" charset="utf-8"
  src="jquery.js"></script>
<script type="text/javascript" charset="utf-8"
  src="jQueryRotate.2.1.js"></script>

Once those are in place, the application can rotate the graphic using the following single line of code:

$("#compass").rotate(angle);

The $() is a jQuery function that gives an application programmatic access to a particular page element, in this case an image with an ID of compass. Once it has a handle on the element, it calls the rotate function to rotate the graphic by the angle passed to the function.

With that out of the way, let’s take a look at the application.

The watch is created in the onDeviceReady function, so the application starts updating the compass as soon as PhoneGap is done initializing. As defined, it queries the compass four times a second and then updates the compass orientation accordingly. The watchID variable is defined at a global level, so it’s available to multiple parts of the application.

When the watch fires, it calls the onHeadingSuccess function and passes in the heading object defined in the previous section. There the application rounds the heading value to the nearest whole number and stores it in a variable for use later. It does that to minimize flicker as the compass adjusts itself, forcing the value to a whole number minimizes the number of changes made to the screen. Next the application updates the screen to show the numeric value for the heading and then calls the rotate function to rotate the graphic.

function onHeadingSuccess(heading) {
  var hv = Math.round(heading.magneticHeading);
  hi.innerHTML = "<b>Heading:</b>" + hv + "degrees";
  $("#compass").rotate(-hv);
}

When you look at the code, you may notice that the application converts the heading value (through the hv variable in the code) to a negative number when calling rotate. This is because while the device might be pointing in a certain direction, for the compass graphic to illustrate this accurately, it must rotate the north heading away from the horizontal axis of the device. So, as the device turns 10° to the right, the compass graphic must then rotate 10° to the left in order to still be pointing north.

If there’s an error querying the compass, the onHeadingError function is called so the application can alert the user. Passed to the function is a compassError object that includes information about the source of the error. Since we’ve had an error, the first thing the application does is cancel the watch; there’s no reason to continue to query the compass when you know it’s not working. After the watch has been canceled, the application provides some feedback to the user so they know why the application is no longer updating the compass.

Figure 13-2 shows the application running on an Android device.

Image

Figure 13-2 Example 13-2 running on an Android device

watchHeadingFilter

As useful as it is to query the compass on a time interval, sometimes an application might want to know only when the device orientation changes. To support this, the PhoneGap Compass API includes a function that can be called to define a watch that’s fired only when the heading changes by more than a configurable number of degrees. This option works in a very similar way to the previous example; the only differences are the names of the functions used to set and clear the watch and the watch options passed to the function that creates the watch.

In this case, the watch is created using the following code:

var watchOptions = { filter : 5 };
watchID = navigator.compass.watchHeadingFilter(
  onHeadingSuccess, onHeadingError, watchOptions);

The watch is created using watchHeadingFilter instead of the watchHeading function used in the previous example. The application still needs to create a watchOptions object, but instead of specifying a frequency variable, a filter is used instead. The filter variable defines the number of degrees used to filter the watch. In this case, the onHeadingSuccess function will fire whenever the heading changes by at least the value specified by the filter.

To remove the watch, call the following function and pass in the watchID being canceled:

navigator.compass.clearWatchFilter(watchID);

Unfortunately, as useful as this option is, it doesn’t work on all platforms. Today only iOS provides support for this function.

Example 13-3 shows the relevant portions of Example 13-2 updated to leverage the watchHeadingFilter function. The majority of the changes are to the onDeviceReady function where you’ll see a different watchOptions variable definition and the call to watchHeadingFilter instead of watchHeading. In the onHeadingError function, the call to clearWatch has been replaced with a call to clearWatchFilter instead. Beyond those minor changes, the application is otherwise the same as Example 13-2.


Example 13-3

function onDeviceReady() { 
  //Set up the watch to fire whenever the compass moves 5 degrees 
  var watchOptions = { filter : 5 };
  watchID = navigator.compass.watchHeadingFilter( 
    onHeadingSuccess, onHeadingError, watchOptions); 


function onHeadingSuccess(heading) { 
  var hv = Math.round(heading.magneticHeading);
  hi.innerHTML = "<b>Heading:</b>" + hv + "degrees"; 
  $("#compass").rotate(-hv); 


function onHeadingError(compassError) { 
  //Remove the watch since we're having a problem 
  navigator.compass.clearWatchFilter(watchID); 
  //clear the Heading value from the page 
  hi.innerHTML = ""; 

  //Then tell the user what happened. 
  if(compassError.code == CompassError.COMPASS_NOT_SUPPORTED) { 
    alert("Compass not supported."); 
  } else if (compassError.code == 
    CompassError.COMPASS_INTERNAL_ERR) { 
    alert("Compass Internal Error"); 
  } else { 
    alert("Unknown heading error!"); 
  } 


..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset