Calculate compass heading from DeviceOrientation Event API
Asked Answered
I

4

5

For an augmented reality web app for smartphones I'm trying to calculate the compass heading when the user is holding the device in their hand, with the screen in a vertical plane and the top of the screen pointing upwards.

I have taken the suggested formula from http://dev.w3.org/geo/api/spec-source-orientation (see Worked Example) and implemented the following function:

function compassHeading(alpha, beta, gamma) {
    var a1, a2, b1, b2;
    if ( beta !== 0 || gamma !== 0 ) {
        a1 = -Math.cos(alpha) * Math.sin(gamma);
        a2 = Math.sin(alpha) * Math.sin(beta) * Math.cos(gamma);
        b1 = -Math.sin(alpha) * Math.sin(gamma);
        b2 = Math.cos(alpha) * Math.sin(beta) * Math.cos(gamma);
        return Math.atan((a1 - a2) / (b1 + b2)).toDeg();
    }
    else {
        return 0;
    }
}

while .toDeg() is a Number object extension courtesy http://www.movable-type.co.uk/scripts/latlong.html

/** Converts radians to numeric (signed) degrees */
if (typeof Number.prototype.toDeg == 'undefined') {
    Number.prototype.toDeg = function() {
        return this * 180 / Math.PI;
    };
}  

However, the problem is that the calculated compass heading value jumps from about -75 to 80 even if the device (Google Galaxy Nexus) is mounted to hold a static position. This seems to happen in both Google Chrome BETA and FF BETA 23.

Does somebody see an error in my approach or know a more reliable way to calculate the compass heading?

Immobility answered 7/8, 2013 at 20:0 Comment(0)
C
20

The steps you need to determine the compass heading according to the worked example provided in the specification* are as follows:

  • Convert returned DeviceOrientation alpha, beta and gamma values from degrees to radians as alphaRad, betaRad, gammaRad.
  • Compute rotationA (rA) and rotationB (rB) components per the worked example in the specification using alphaRad, betaRad and gammaRad (as shown in the example code below).
  • Compute compassHeading = Math.atan(rA / rB).
  • Convert returned half unit circle headings to whole unit circle headings in the range [0-360) degrees.
  • Convert compassHeading from radians back to degrees (optional).

Here is the worked example from the specification implemented in JavaScript:

function compassHeading(alpha, beta, gamma) {

  // Convert degrees to radians
  var alphaRad = alpha * (Math.PI / 180);
  var betaRad = beta * (Math.PI / 180);
  var gammaRad = gamma * (Math.PI / 180);

  // Calculate equation components
  var cA = Math.cos(alphaRad);
  var sA = Math.sin(alphaRad);
  var cB = Math.cos(betaRad);
  var sB = Math.sin(betaRad);
  var cG = Math.cos(gammaRad);
  var sG = Math.sin(gammaRad);

  // Calculate A, B, C rotation components
  var rA = - cA * sG - sA * sB * cG;
  var rB = - sA * sG + cA * sB * cG;
  var rC = - cB * cG;

  // Calculate compass heading
  var compassHeading = Math.atan(rA / rB);

  // Convert from half unit circle to whole unit circle
  if(rB < 0) {
    compassHeading += Math.PI;
  }else if(rA < 0) {
    compassHeading += 2 * Math.PI;
  }

  // Convert radians to degrees
  compassHeading *= 180 / Math.PI;

  return compassHeading;

}

window.addEventListener('deviceorientation', function(evt) {

  var heading = null;

  if(evt.absolute === true && evt.alpha !== null) {
    heading = compassHeading(evt.alpha, evt.beta, evt.gamma);
  }

  // Do something with 'heading'...

}, false);

You can also view a demo of the code provided above.

As of the time of writing (17th Feb 2014) this currently works in:

  • Google Chrome for Android
  • Opera Mobile for Android
  • Firefox Beta for Android

Other browsers do not yet conform to the DeviceOrientation calibration described in the DeviceOrientation Event specification and/or do not provide absolute DeviceOrientation data values making it impossible to determine compassHeading with non-complete data.

* Determining the compass heading of the horizontal component of a vector which is orthogonal to the device's screen and pointing out of the back of the screen.

Carat answered 17/2, 2014 at 13:1 Comment(6)
thanks for the code, compass heading works very accurate on my Galaxy Nexus and Chrome, FFImmobility
See also my more robust cross-platform answer at #16049014.Carat
Tried your code 2018 on Samsung Galaxy 8 (FF, Chrome), seems not to work (evt.absolute always false)Jumper
Chrome has recently introduced deviceorientationabsolute event that should be used instead to obtain world-based coordinates. Information and status is here: developer.mozilla.org/en-US/docs/Web/API/Window/…Carat
I recommend using Math.atan2, as Math.atan ony provides angles between +-pi/2.Infrasonic
"Determine compass ... of horizontal component of vector orthogonal to device's screen" seems wrong as it is undetermined (or inaccurate) when device is (almost) flat on table. In this case your formulas get sB=sG=0, so rA=rB=0 and atan(rA/rB) is undefined. Surely, when beta and gamma tend to zero the formulas should resolve to just (linear formula of) alpha.Gloriane
G
2

2023 Answer

The formula to calculate compass heading from absolute alpha, beta and gamma is as follows:

compass = -(alpha + beta * gamma / 90);
compass -= Math.floor(compass / 360) * 360; // Wrap to range [0,360]

The above formula is well-behaved for zero beta and gamma (e.g. phone lying on table), and also works for beta=90 (e.g. held upright).


It's 2023 and iOS still doesn't support absolute DeviceOrientationEvent, and Android still doesn't support compass directly. The following TypeScript code shows how to get the compass value for both types of device. You can convert it to JavaScript by removing the type specifiers after the colons.

function startCompassListener(callback: (compass: number) => void) {
    if (!window["DeviceOrientationEvent"]) {
        console.warn("DeviceOrientation API not available");
        return;
    }
    let absoluteListener = (e: DeviceOrientationEvent) => {
        if (!e.absolute || e.alpha == null || e.beta == null || e.gamma == null)
            return;
        let compass = -(e.alpha + e.beta * e.gamma / 90);
        compass -= Math.floor(compass / 360) * 360; // Wrap into range [0,360].
        window.removeEventListener("deviceorientation", webkitListener);
        callback(compass);
    };
    let webkitListener = (e) => {
        let compass = e.webkitCompassHeading;
        if (compass!=null && !isNaN(compass)) {
            callback(compass);
            window.removeEventListener("deviceorientationabsolute", absoluteListener);
        }
    }

    function addListeners() {
        // Add both listeners, and if either succeeds then remove the other one.
        window.addEventListener("deviceorientationabsolute", absoluteListener);
        window.addEventListener("deviceorientation", webkitListener);
    }

    if (typeof (DeviceOrientationEvent["requestPermission"]) === "function") {
        DeviceOrientationEvent["requestPermission"]()
            .then(response => {
                if (response == "granted") {
                    addListeners();
                } else
                    console.warn("Permission for DeviceMotionEvent not granted");
            });
    } else
        addListeners();
}

When testing I found that my Android's absolute (alpha,beta,gamma) was very inaccurate (~60 degrees error in alpha). However, by rolling the device around it calibrated itself and got to ~5 degrees error.

Gloriane answered 20/3, 2023 at 15:27 Comment(0)
U
0

I have also played some with the DeviceOrientationEvent (might adopt you formula...) and seen similar problems from time to time. You might wanna try a calibration as in this video. You could search youtube for more examples.

Un answered 7/10, 2013 at 10:50 Comment(0)
M
0

It seems that getting compass heading from device orientation events is deprecated, perhaps for privacy reasons. You should look at the Geolocation API, which requires permissions. The events have Coordinates.heading.

Mishap answered 27/8, 2018 at 13:26 Comment(1)
The problem here is the device needs to be moving in order to get a reading from Coordinates.heading. From the link you provided: "If Coordinates.speed is 0, heading is NaN."Osset

© 2022 - 2024 — McMap. All rights reserved.