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.
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.
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.
<!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.
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.
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.
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.
<!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.
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.
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!");
}
}