从DeviceOrientation事件API计算指南针方向

5
对于智能手机的增强现实Web应用程序,我正在尝试在用户手持设备时计算指南针方向,其中屏幕处于垂直平面,屏幕顶部朝上。
我已经采用了http://dev.w3.org/geo/api/spec-source-orientation中建议的公式(请参见工作示例),并实现了以下函数:
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;
    }
}

.toDeg()是一个Number对象扩展,由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;
    };
}  

然而问题在于,即使设备(Google Galaxy Nexus)处于静止状态,计算出的指南针方位值仍会从大约-75跳变到80。这似乎发生在Google Chrome BETA和FF BETA 23中。请问是否有人看到我的方法存在错误或知道更可靠的计算指南针方位的方法?
4个回答

20

根据规范提供的实例,确定罗盘方向的步骤如下:

  • 将返回的DeviceOrientation alphabetagamma值从角度转换为弧度,作为alphaRadbetaRadgammaRad
  • 使用alphaRadbetaRadgammaRad(如下面示例代码所示),按照规范中的实例计算旋转组件rotationA(rA)和rotationB(rB)。
  • 计算compassHeading = Math.atan(rA / rB)
  • 将返回的半单位圆头转换为整个单位圆头,在[0-360)度范围内。
  • 可选地将compassHeading从弧度转换回角度。

以下是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);

你还可以查看上面提供的代码的演示

截至撰写本文时(2014年2月17日),以下浏览器当前支持:

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

其他浏览器尚未遵循设备方向校准描述在“设备方向事件规范”中说明并且/或不提供 absolute 设备方向数据值,这使得无法使用非完整数据来确定 compassHeading

* 确定垂直于设备屏幕、指向屏幕背面的矢量的水平分量的罗盘方向。


感谢您的代码,指南针在我的Galaxy Nexus和Chrome、FF上工作非常准确。 - bardu
请参考我的更为强大的跨平台答案,网址为https://dev59.com/tWQo5IYBdhLWcg3wdPAa#26275869。 - richt
2
在三星Galaxy 8(FF,Chrome)上尝试了您的2018代码,似乎无法正常工作(evt.absolute始终为false)。 - Jonny
1
Chrome最近推出了deviceorientationabsolute事件,应该使用它来获取基于世界坐标系的坐标。有关信息和状态,请参见:https://developer.mozilla.org/en-US/docs/Web/API/Window/ondeviceorientationabsolute - richt
1
我建议使用Math.atan2,因为Math.atan仅提供+-pi/2之间的角度。 - flawr
“确定指南针...垂直于设备屏幕的向量水平分量的方向”似乎是错误的,因为当设备几乎平放在桌子上时,它是不确定的(或不准确的)。 在这种情况下,您的公式得到sB = sG = 0,因此rA = rB = 0,atan(rA / rB)未定义。 当beta和gamma趋近于零时,这些公式肯定应该解析为(线性公式的)alpha。 - Adam Gawne-Cain

2

2023答案

从绝对α、β和γ计算罗盘航向的公式如下:

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

上述公式对于零贝塔和伽马(例如放在桌子上的手机)表现良好,也适用于贝塔等于90度的情况(例如竖直握持)。


现在是2023年,iOS仍然不支持绝对的DeviceOrientationEvent,而Android仍然不直接支持指南针。以下TypeScript代码展示了如何获取两种设备的指南针值。您可以通过删除冒号后的类型说明符将其转换为JavaScript。

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();
}

在测试时,我发现我的安卓设备的绝对方向(alpha、beta、gamma)非常不准确(alpha 方向误差约为 60 度)。然而,通过将设备旋转一圈,它自动校准并将误差降至约 5 度。


0

似乎从设备方向事件中获取指南针方向已被弃用,可能是出于隐私原因。您应该查看需要权限的地理位置 API。这些事件具有Coordinates.heading


1
这里的问题是设备需要移动才能从Coordinates.heading获取读数。根据您提供的链接:“如果Coordinates.speed为0,则heading为NaN。” - bloodyKnuckles

0

我也尝试了一些 DeviceOrientationEvent(可能采用您的公式...),并且不时会出现类似的问题。您可以尝试进行校准,就像这个 video 中所示。您可以在 YouTube 上搜索更多示例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接