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
-, , ,

執筆者:

関連記事

UserAgent判定JSライブラリ「UAParser.js」と「Platform.js」の比較

(私はUAParser.jsを使っています) ネットを探すとUA文字列を解析してブラウザ判定をするコードが一杯出てきます。 でもUA解析プログラムを自前で作ってシステムに組み込むとなると、新しいブラウ …

Javascript(暗号化JSライブラリ「Forge」)とp12ファイルで署名値を作成、Javaで検証する

前回、送信データの改ざんを検知する為、簡易的なセキュリティトークンであるPKCS#12形式のファイルを作成しました。 One IT Thing  10 Pockets開発用のPKCS#12フ …

StripeとJavaで単発Web決済を一通り流してみる

会社仕事でも個人ビジネスでも、商品やサービスの対価として利用者に課金方法を提供したい時があるかと思います。 最もメジャーな決済方法であるクレジットカードで課金してもらう仕組みを導入したいところですが、 …

Chrome75に実装された「Web Share API Level 2」を使ってみた

(2019/06現在、個人的な感想としては実戦投入はまだ早い印象でした) Webアプリにシェア機能を付けたい時があります。OSネイティブAPIを呼べないWebアプリではsharer.jsやremote …

adb devicesコマンドでAndroid端末を認識しない

目次1 事象2 原因3 解決 事象 USB接続するAndroidによって以下のエラーが出たりします。 C:\src\ionic\awsomeapp>adb devices List of dev …

 

shingo.nakanishi
 

東京在勤、1977年生まれ、IT職歴2n年、生涯現役技術者を目指しています。健康第一。