android html5 iOS javascript

Javascriptでコンパスを作ってAndroid、iPhoneが向いている方角を特定

投稿日:2020年10月9日

普段は意識しなくても、AndroidもiPhoneも自然データをデジタルに変換するセンサーの塊です。

  • 加速度センサー
  • 重力センサー
  • ジャイロセンサー
  • 地磁気センサー
  • 気圧センサー
  • 照度センサー
  • 温度センサー
  • 位置センサー
  • etcetc…

スマートフォン黎明期はネイティブアプリを作らないと取れなかったセンサー値も、現在ではW3C仕様によってブラウザAPIが追加され、Webブラウザから取得出来るようになってきました。

Webアプリの目的に沿ったセンサー値が取れればユーザ体験(UX)も上がりますよね。

例えばスマホ背面が向いている方向や、現在地が分かればポケモンGOやドラクエウォークのようなアプリもブラウザ上で作れそうです。

Android、iPhone、かつWebアプリでスマホが向いている方角を求め、お仕事Webアプリで実戦投入出来るくらいの精度を出せるのか、検証してみました。

環境

  • MacOSX mojave
  • Android9(Chrome85)
  • iOS13(safari13)

DeviceOrientation APIで本体のセンサーから値を取得

スマートフォン本体のジャイロ、地磁気センサー値をJavascriptで取得する為、W3C仕様のDeviceOrientation APIを使用します。端末の傾きを数値として取れるのでゲームにも応用されるようです。

DeviceOrientation APIはメジャーなモバイルブラウザでは全て仕様実装済み。

DeviceOrientation Eventで取れる主な値は以下の3つ(+α)。

スマホをテーブルの上に置いた時、取れる値は以下のようになります。

  • X(gamma):0
  • Y(beta):0
  • Z(alpha):方角を表す(0が北)

そのまま水平にスマホを持ち、

右に傾けるとXが正の値に、
画面奥を手前に持ち上げるとYが正の値に、
体の向きを変えるとZの値が方角(0~360)に

変化します。

今回は方角が知りたいのでメインで扱う値はZ(alpha)になります。
Z値はスマホ本体の傾きによって変化してしまう為、XとYを使用して補正します。

実装上の課題

方位を求める仕組みが大体分かったので、実装上つまづく点を先に解消していきます。

課題1:開発にはHTTPS環境が必須

AndroidはHTTPSかlocalhostでのアクセスでないとDeviceOrientationAPIが使えません(DOMに乗らないのでundefinedになる)。

また、コレ系のプログラムは現在位置を取得するGeolocation APIと組み合わせることも多く、iOSでGeolocation APIを使うにはHTTPSが必須になります。

最初からmkcert等を使ってオレオレなHTTPS開発環境を作っておいた方が、ハマリポイントが少なくなります。

(簡易なオレオレHTTPS環境構築は以下記事参照)

課題2:OS種別の判定

Android(Chrome)とiOS(Safari)でDeviceOrientation APIの実装がちょいちょい異なるので簡易に判別出来るようにしておきます。

            // 簡易OS判定
            function detectOSSimply() {
                let ret;
                if (
                    navigator.userAgent.indexOf("iPhone") > 0 ||
                    navigator.userAgent.indexOf("iPad") > 0 ||
                    navigator.userAgent.indexOf("iPod") > 0
                ) {
                    // iPad OS13以上のsafariはデフォルト「Macintosh」なので別途要対応
                    ret = "iphone";
                } else if (navigator.userAgent.indexOf("Android") > 0) {
                    ret = "android";
                } else {
                    ret = "pc";
                }

                return ret;
            }

実戦投入する際は厳密にブラウザ種判定も必要ですね。

課題3:alphaは相対値で取れるケースと絶対値で取れるケースがある

試しに以下のコードを実装してAndroid(Chrome)とiPhone(Safari)で開き、北に向けてみます。

    <script type="text/javascript">
        window.addEventListener("deviceorientation", orientation, true);

        function orientation(event) {
            let alpha = event.alpha;
            let beta = event.beta;
            let gamma = event.gamma;

            console.log(alpha + " : " + beta + " : " + gamma);
        }

        console.log("イベント登録完了");
    </script>

実行結果。
(どうでもいいけど私の家ちょっと傾いますね・・・若干ショック)

287.7 : 0.8 : -2.2

alphaは0か360付近が出て欲しい所ですが、大分ずれていて、再描画すると毎回値が変わります。

これはalphaが相対値(API起動した時点を0としてそこからの変化)で取れてしまっている為のようです。

ゲームで使用するならこれでも良いですが、方角を知りたい場合には役に立ちません。

方角を知るには相対値(本体基準系:body frame)ではなく絶対値(地球基準系:earth frame)で取る必要があります。

方角を知る為、確実に絶対値でalpha値を取るには

  • Chrome:deviceorientationabsoluteイベントを替わりに使う。
  • Safari:deviceorientationabsoluteが無い為、webkitCompassHeadingで取れる値を使う。

で対処が出来そうです。

課題4:alpha値の補正

Z(alpha)の値は端末の角度が変わると連動して変わってしまいます。角度が変わっても水平状態と同じ値を示すように、X値とY値を使って補正をかけます。

補正の為の計算方法は冒頭のW3Cドキュメントにサンプルがあるのでこれを使用します。

https://triple-underscore.github.io/deviceorientation-ja.html#worked-example

(SafariのwebkitCompassHeadingで取れる値は既にこの補正が効いている為、補正処理の実装は不要です)

課題5:iOS13はDeviceOrientationAPI使用にユーザ同意が必要

iOS12では設定アプリでの同意も必要だったので少し制限が緩くなったようです。

ユーザが操作して起動したEventDispatchThread内で以下のコードを実行し、ユーザの手で許可させることで、DeviceOrientationが使えるようになります。

                // DeviceOrientationを使用する許可ダイアログを表示
                DeviceOrientationEvent.requestPermission()
                    .then(response => {
                        // 許可がクリックされた場合
                        if (response === "granted") {
                            // deviceorientationイベントをlisten
                            window.addEventListener(
                                "deviceorientation",
                                detectDirection
                            );
                        }
                    })
                    .catch(console.error);

実装開始

課題がクリアになったので一枚っぺらのindex.htmlに実装してみます。

<!DOCTYPE html>
<html lang="ja"">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>Document</title>

        <script type="text/javascript">
            // OS識別用
            let os;

            // DOM構築完了イベントハンドラ登録
            window.addEventListener("DOMContentLoaded", init);

            // 初期化
            function init() {
                // 簡易的なOS判定
                os = detectOSSimply();
                if (os == "iphone") {
                    // safari用。DeviceOrientation APIの使用をユーザに許可して貰う
                    document.querySelector("#permit").addEventListener("click", permitDeviceOrientationForSafari);

                    window.addEventListener(
                        "deviceorientation",
                        orientation,
                        true
                    );
                } else if (os == "android") {
                    window.addEventListener(
                        "deviceorientationabsolute",
                        orientation,
                        true
                    );
                } else{
                    window.alert("PC未対応サンプル");
                }
            }


            // ジャイロスコープと地磁気をセンサーから取得
            function orientation(event) {
                let absolute = event.absolute;
                let alpha = event.alpha;
                let beta = event.beta;
                let gamma = event.gamma;

                let degrees;
                if(os == "iphone") {
                    // webkitCompasssHeading値を採用
                    degrees = event.webkitCompassHeading;

                }else{
                    // deviceorientationabsoluteイベントのalphaを補正
                    degrees = compassHeading(alpha, beta, gamma);
                }

                let direction;
                if (
                    (degrees > 337.5 && degrees < 360) ||
                    (degrees > 0 && degrees < 22.5)
                ) {
                    direction = "北";
                } else if (degrees > 22.5 && degrees < 67.5) {
                    direction = "北東";
                } else if (degrees > 67.5 && degrees < 112.5) {
                    direction = "東";
                } else if (degrees > 112.5 && degrees < 157.5) {
                    direction = "東南";
                } else if (degrees > 157.5 && degrees < 202.5) {
                    direction = "南";
                } else if (degrees > 202.5 && degrees < 247.5) {
                    direction = "南西";
                } else if (degrees > 247.5 && degrees < 292.5) {
                    direction = "西";
                } else if (degrees > 292.5 && degrees < 337.5) {
                    direction = "北西";
                }

                document.querySelector("#direction").innerHTML =
                    direction + " : " + degrees;
                document.querySelector("#absolute").innerHTML = absolute;
                document.querySelector("#alpha").innerHTML = alpha;
                document.querySelector("#beta").innerHTML = beta;
                document.querySelector("#gamma").innerHTML = gamma;
            }

            // 端末の傾き補正(Android用)
            // https://www.w3.org/TR/orientation-event/
            function compassHeading(alpha, beta, gamma) {
                var degtorad = Math.PI / 180; // Degree-to-Radian conversion

                var _x = beta ? beta * degtorad : 0; // beta value
                var _y = gamma ? gamma * degtorad : 0; // gamma value
                var _z = alpha ? alpha * degtorad : 0; // alpha value

                var cX = Math.cos(_x);
                var cY = Math.cos(_y);
                var cZ = Math.cos(_z);
                var sX = Math.sin(_x);
                var sY = Math.sin(_y);
                var sZ = Math.sin(_z);

                // Calculate Vx and Vy components
                var Vx = -cZ * sY - sZ * sX * cY;
                var Vy = -sZ * sY + cZ * sX * cY;

                // Calculate compass heading
                var compassHeading = Math.atan(Vx / Vy);

                // Convert compass heading to use whole unit circle
                if (Vy < 0) {
                    compassHeading += Math.PI;
                } else if (Vx < 0) {
                    compassHeading += 2 * Math.PI;
                }

                return compassHeading * (180 / Math.PI); // Compass Heading (in degrees)
            }

            // 簡易OS判定
            function detectOSSimply() {
                let ret;
                if (
                    navigator.userAgent.indexOf("iPhone") > 0 ||
                    navigator.userAgent.indexOf("iPad") > 0 ||
                    navigator.userAgent.indexOf("iPod") > 0
                ) {
                    // iPad OS13のsafariはデフォルト「Macintosh」なので別途要対応
                    ret = "iphone";
                } else if (navigator.userAgent.indexOf("Android") > 0) {
                    ret = "android";
                } else {
                    ret = "pc";
                }

                return ret;
            }

            // iPhone + Safariの場合はDeviceOrientation APIの使用許可をユーザに求める
            function permitDeviceOrientationForSafari() {
                DeviceOrientationEvent.requestPermission()
                    .then(response => {
                        if (response === "granted") {
                            window.addEventListener(
                                "deviceorientation",
                                detectDirection
                            );
                        }
                    })
                    .catch(console.error);
            }
        </script>
    </head>

    <body>
        <ul>
            <input type="button" id="permit" value="SafariでDeviceOrientationを許可"/>
            <li>【方角】<span id="direction"></span></li>
            <li>【absolute】<span id="absolute"></span></li>
            <li>【alpha】<span id="alpha"></span></li>
            <li>【beta】<span id="beta"></span></li>
            <li>【gamma】<span id="gamma"></span></li>
        </ul>
    </body>
</html>

動作確認

HTTPSサーバに配備してChromeとSafariで接続、北を向いてみます。

Android + Chrome

大体OK。

iPhone + Safari

「SafariでDeviceOrientationを許可」ボタンを押してDeviceOrientation.requestPermissions()を走らせ、許可します。

Androidとほぼ同じ。大丈夫そうです。

検証を終えて

Chromeではdeviceorientationabsoluteイベントで取れるalpha値を補正、SafariではDeviceOrientationEvent.webkitCompassHeading値を見ることでスマホが向いている方位を概ね知ることが出来ることが分かりました。

正直なところ、GPSが入っていない端末だとどうなるかとか、地磁気が乱れた場所、地下などで動作するか等は検証出来ていません。

精度が低いことを検知するキャリブレーションの実装検証も追々、という感じです。

お金の掛かったビジネスアプリに投入するにはまだ検証の余地は多いですが、普段使いしているAndroidやiPhoneを使い、屋外で遊びで使う分には現段階でも楽しめそうだな、といった実感が持てた検証でした。

次回は地図上で向いている方向を表現してみたいと思います。

-android, html5, iOS, javascript
-, , ,

執筆者:

関連記事

Stripe + Javaでオーソリ(与信の確保)を実装する

Stripeでチャリンチャリン、サービスを開発するエンジニアにとっては夢がありますよね。 例え自分で個人的に売るものが無かったとしても、Web決済システムを構築できるノウハウを持っておけば、公的な仕事 …

hidden.inとSoftEtherで無料のビデオ会議環境を構築する

自粛が続いて会えない人も多くなり、人との交流が恋しくなりますね。 仕事ではミーティングをオンライン化するニーズが増え、以下メジャーなパブリックサービスを使っている人も増えていると思います。 Skype …

PWAストアの実現性についての考察

(*)この記事内では「Google Play」、「App Store」、「Microsoft Store」で公開されているアプリを「ストアアプリ」と呼んでいます。 先日B2Bアプリはストアアプリにする …

IndexedDBにblob保存されたPDFファイルを外部アプリに頼らずにJavascriptで表示(前編)

目次1 はじめに2 検証環境3 Ionicプロジェクト作成4 PDFダウンロード、IndexedDB保存を実装5 実行してみる はじめに PDFをHTTPダウンロードするとHDDに保存されるか、外部ビ …

History APIを使ってIonic(3以前のSPA)でブラウザの戻るボタンやAndroidバックキーを押すと前サイトに戻ってしまう件に対応する

Ionic2や3ではまだAngular Routerを採用していなかったので、ページ遷移をしてもブラウザ履歴が積まれず、Androidのバックキーやブラウザの戻るボタンを押すとサイトに入ってくる前のペ …

 

shingo.nakanishi
 

東京在勤、1977年生まれ、IT職歴2n年、生涯技術者として楽しく生きることを目指しています。デスマに負けず健康第一。