One IT Thing

IT業界で飯を食う為の学習系雑記

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

執筆者:

関連記事

【タップするだけ】インスタグラム、twitterのプロフィール画像を簡単に拡大表示する方法(iPhone、Android)

インスタのプロフィール画像って小さくてよく見えないですよね・・・。 インスタに限らず「何が写ってるのかスマホ画面を拡大して見たいな」という時は「設定」から「アクセシビリティ」を設定することで拡大表示機 …

キャッシュされているはずのServiceWorker資源にオフラインアクセス出来ない(Workbox + ionicons)

そのHTTPリクエストしたファイル資源、ひょっとしてURLパラメータついてたりしませんか? 目次1 事象2 原因3 対処 事象 ionic3(SPA)でWorkboxを使ったServiceWorker …

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

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

Apple PayのWeb決済を開発する為にSandboxテスターを作ってiPhoneに設定

Web上でApple Pay支払いが出来るサービスが増えていますね。 決済にApple Payが使えるシステムにしておけば、PayPalよりもエンゲージ率は高くなるはずです。 自分が開発しているWeb …

Android、iPhoneが向いている方向をWeb地図上で表現してみる(leaflet.js使用)

前回Javascriptでコンパス機能を実装して、Android、iPhoneの背面が向いている方角が数値(0~360:0が北)で分かるようになりました。  One IT ThingJava …

 

shingo.nakanishi
 

東京在勤、職歴20年越え中年ITエンジニアです。まだ開発現場で頑張っています。

19歳(1996年)から書き始めた個人日記が5,000日を超え、残りの人生は発信をして行きたいと思い、令和元日からこのサイトを開始しました。勉強と試行錯誤をしながら、自分が経験したIT関連情報を投稿しています。

私と同じく、今後IT業界で生計を立てて行きたいと考えている方や、技術共有したいけどフリーランスで孤独、といった方と一緒に成長、知識共有して行けたら楽しいな、と思っています。