本文へ移動
BenriWorks Lab

開発記事公開日

スマホのコンパス方位をWebで取得する

屋外でスマートフォンを手にかざし、画面のコンパス表示と実際の風景を見比べている手元

地デジエリアマップ(旧ARコンパス)の開発で、iOSとAndroidの方位センサーの違いを吸収し、安定した方位表示を実現した実装を紹介します

スマートフォンが向いている方角をWebページで取得する。deviceorientationイベントを購読してalphaを読めば終わりに見えますし、最初はそのつもりでした。ところがこの期待は実機で崩れます。iOSとAndroidで届く値の意味が違い、同じAndroidでも端末によって方位の基準が定まらないのです。この記事では、AR表示付きのコンパスアプリを開発したときに、この差異を吸収して安定した方位表示に至った実装を記録します。

alphaが方位にならない理由

スマートフォンの姿勢は、deviceorientationイベントのalpha、beta、gammaという三つの角度で届きます。alphaは端末の水平回転なので、これが北基準なら方位はもう手に入ったも同然です。

ただし仕様には逃げ道があります。イベントのabsoluteフラグが真なら地球座標系を基準にした値、偽なら基準は実装に任されています。「alphaを読めば北からの角度になる」という期待は、absoluteが真のときにしか成り立ちません。

このアプリで確認した範囲では、二大モバイル環境の挙動は次のように分かれていました。

  • iOSのSafariでは、deviceorientationのalphaは絶対方位を意味しない。その代わり、独自プロパティのwebkitCompassHeadingが磁北からの方位を返す
  • AndroidのChromeでは、deviceorientationabsoluteという別のイベントで、地磁気と加速度をセンサー融合した絶対姿勢が届く。このイベントが届かない場合、頼れるのは基準の定まらない相対のdeviceorientationだけになる

つまり方位の出どころは、webkit、absolute、relativeの三系統です。実装では、どのイベントから来た値も「方位、出どころ、精度」の一つのサンプル型に正規化してから扱うことにしました。

iOSのwebkitCompassHeadingと精度値

三系統のうち、iOSは一番素直でした。イベントにwebkitCompassHeadingが数値で入っていれば、正規化してそのまま磁方位として使えます。

if (typeof webkitCompassHeading === 'number' && !Number.isNaN(webkitCompassHeading)) {
  return {
    heading: normalizeAngle(webkitCompassHeading),
    source: 'webkit',
    accuracy:
      typeof webkitCompassAccuracy === 'number' && !Number.isNaN(webkitCompassAccuracy)
        ? webkitCompassAccuracy
        : null
  };
}

見どころはむしろ、対になるwebkitCompassAccuracyのほうです。これは方位の誤差を度で表した値で、このアプリで確認した挙動では、負値が未キャリブレーション状態を意味します。実装では誤差15°以内を高品質、50°以内を中品質、それを超える値と負値を低品質、値が欠けているときは中品質として扱い、後述するキャリブレーション誘導の判定材料にしています。

Androidの絶対方位と相対フォールバック

Androidにはコンパス値の専用プロパティがありません。では何を聞けば方位になるのでしょうか。答えはイベントの二段構えでした。deviceorientationabsoluteとdeviceorientationの両方を購読し、絶対イベントが一度でも届いたら、以後は相対イベントを無視します。

const fallbackListener = (event: DeviceOrientationEvent) => {
  if (useAbsoluteEvent) return;
  handleOrientation(event, false);
};

window.addEventListener('deviceorientationabsolute', absoluteListener);
window.addEventListener('deviceorientation', fallbackListener);

絶対イベントの値は地磁気と加速度をセンサー側で融合済みなので、高品質として信頼します。一方、相対イベントしか届かない端末では、方位の基準がどこを向いているのか保証されません。それでも表示は動かせるので値としては受け入れつつ、品質は低として扱います。

チルト補正付きの方位再計算

webkit以外の二系統には、もう一つ罠があります。alphaが方位として使えるのは端末が水平に近いときで、ARのように端末を立てたり空にかざしたりすると、alpha単独の値は画面の向いている方位から離れていきます。そこで実装では、beta、gammaを含めた回転行列から水平面での北向き成分だけを取り出し、atan2で方位角に戻しています。

const rA = -cA * sG - sA * sB * cG;
const rB = -sA * sG + cA * sB * cG;

const headingRadians = Math.atan2(rA, rB);

ここでcAとsAはalphaの余弦と正弦、cGとsGはgammaの余弦と正弦、sBはbetaの正弦です。回転行列のうち方位の計算に必要な成分だけを組み立てています。

もう一つ、画面の回転も方位をずらします。センサーは端末本体に固定されているので、画面表示を横向きに回転させた分だけ、表示上の方位がずれてしまいます。実装ではscreen.orientation.angleを読み、その角度を方位から引いて補正します(この値が取れない古い環境ではwindow.orientationに頼ります)。

ジッタ除去とキャリブレーション誘導

ここまでで方位は取れました。ところが実機で針を回すと、端末を静止させているのに表示が細かく震え続けます。センサーの生の値が、イベントごとに小さく揺れているからです。

実装では二つの数値で揺れを抑えています。一つめはデッドバンドで、直前の値から0.4°未満の変化は無視します。表示の分解能が1°なので、それ未満の揺れを追いかけても意味がありません。二つめはスムージング係数0.25で、変化が閾値を超えたときも目標へ一度に飛ばさず、差分の25%ずつ追従させます。

このとき差分の計算には注意が要ります。359°から1°への変化を素直に引き算すると−358°になり、針が逆回りにほぼ一回転してしまいます。実装では0°と360°の境界をまたぐ最短角度差(この例では+2°)を求めてから平滑化しています。

それでも精度が落ちる状況はあります。品質が低のまま3秒続いたときは、「端末を8の字を描くように動かしてください」というキャリブレーション誘導を表示します。また、許可を得てから5秒間方位が一度も届かない場合は、この端末では方位センサーが利用できない旨の案内に切り替えます。

磁方位と真方位のずれ

最後に残るのが、コンパスが返す方位は磁方位であって、地図の北(真北)とは一致しないという問題です。このずれは磁気偏角と呼ばれ、日本国内では真北に対して西に数度ずれます。

このアプリでは、国土地理院の磁気図(2020.0年値)を北緯37°、東経138°を基準に線形近似した一次式で偏角を推定しています。西偏 = 7.94 + 0.3125 × (緯度 − 37) − 0.1127 × (経度 − 138)で、誤差はおおむね±0.5°程度です。真方位は磁方位から西偏を引いて求めます。アンテナの方向合わせの目安という用途には、この近似で足りると判断しました。

まとめ

「deviceorientationイベントを取れば方位が出る」という最初の期待は、半分だけ正しいものでした。イベントは確かに方位の材料を運んできますが、その意味はiOSの独自プロパティ、Androidの絶対イベント、基準不定の相対イベントの三系統に割れていて、さらにチルト補正、画面回転の補正、平滑化、偏角の補正を重ねてようやく「北を指す針」になります。

この一連の実装は「地デジエリアマップ」で動いています。ここに書いた挙動はこのアプリで確認した範囲のもので、すべての端末で同じ動きになる保証はありません。手元の端末では別の癖が見つかるかもしれませんが、その癖を確かめるところからが、方位センサーを扱う開発の本番だと思っています。

関連アプリ

こんな記事も読まれています