android gis html5 iOS javascript

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

投稿日:2020年10月10日

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

今回は地図上で向きを視覚化する処理をざっくり検証してみようと思います。

実装上の課題

課題1:Web地図用Javascriptライブラリの選定

GoogleMapsはAPIキーを取るのが面倒、OpenLayersは今回の検証には高機能過ぎ、ゼンリンさんのAPIは有料。

今回は無料で手軽に使えるLeaflet.jsを使うことにしました。

課題2:背景地図用GISサーバの選定

leaflet.jsのサンプルで使われているOpenStreetMapを使用します。

PostgreSQLにPostGIS拡張を入れてOpenStreetMapデータを投入して自分だけのGISサーバを作るとめっちゃ楽しいのですが、時間もコストも掛かるので今回はインターネット上のOpenStreetMapデータで地図を表示します。

(GeoServer + PostgreSQL + PostGISでオレオレGISサーバを作る記事もその内書きたいと思ってます。HDDめちゃ喰いますけどね・・・)

課題3:現在地の取得

移動に追従して地図上のマーカーを移動させたいのでgeolocation.watchPosition()を使います。

課題4:向きの表現

CSSを定義して、GoogleMapsっぽくビームで表現してみます。

            #beam {
                position: fixed;
                left: 0;
                top: 0;
                z-index: 1000;
                opacity: 0.3;
                height: 0;
                width: 20px;
                margin: 10px auto;
                border-top: 40px solid #11b;
                border-left: 30px solid transparent;
                border-right: 30px solid transparent;
                transform-origin: bottom;
            }

このCSS矩形に対して、前回の記事で求められた0~360の値をrotete関数で設定してあげることで、向いている方向に回転させます。

                let beam = document.querySelector("#beam");
                beam.style.transform = "rotate(" + degrees + "deg)";

処理するタイミングはDeviceOrientationイベントにすることでリアルタイムに向いている方向が分かるようにします。

実装開始

課題がクリアになったので例のごとく一枚っぺらの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>
        <link
            rel="stylesheet"
            href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
            integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
            crossorigin=""
        />
        <script
            src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"
            integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew=="
            crossorigin=""
        ></script>
        <style>
            body {
                margin: 0px;
                padding: 0px;
            }
            #mapid {
                height: 100vh;
            }
            #beam {
                position: fixed;
                left: 0;
                top: 0;
                z-index: 1000;
                opacity: 0.3;
                height: 0;
                width: 20px;
                margin: 10px auto;
                border-top: 40px solid #11b;
                border-left: 30px solid transparent;
                border-right: 30px solid transparent;
                transform-origin: bottom;
            }
        </style>
    </head>
    <body>
        <div id="mapid"></div>

        <input
            id="permitGeolocation"
            type="button"
            value="iOSでDeviceOrientationを許可する"
            style="position: fixed; top: 10px; left: 100px; z-index: 1000"
            onclick="permitGeolocation()"
        />

        <input
            type="text"
            id="os"
            style="position: fixed; top: 40px; left: 100px; z-index: 1000"
        />

        <input
            type="text"
            id="degree"
            style="position: fixed; top: 70px; left: 100px; z-index: 1000"
        />

        <input
            type="text"
            id="iPhone"
            style="position: fixed; top: 100px; left: 100px; z-index: 1000"
        />
        <input
            type="text"
            id="accuracy"
            style="position: fixed; top: 130px; left: 100px; z-index: 1000"
        />

        <div id="heading"></div>
        <div id="beam"></div>

        <script type="text/javascript">
            let os;
            if (
                navigator.userAgent.indexOf("iPhone") > 0 ||
                navigator.userAgent.indexOf("iPad") > 0 ||
                navigator.userAgent.indexOf("iPod") > 0
            ) {
                os = "iphone";
                console.log("iPhone");
            } else if (navigator.userAgent.indexOf("Android") > 0) {
                os = "android";
                console.log("Android");
            } else {
                os = "pc";
                console.log("PC");
            }

            document.querySelector("#os").value = os;

            let map;
            let human;

            window.addEventListener("DOMContentLoaded", init);

            if (os == "iphone") {
                window.addEventListener(
                    "deviceorientation",
                    detectDirection,
                    true
                );
            } else if (os == "android") {
                window.addEventListener(
                    "deviceorientationabsolute",
                    detectDirection,
                    true
                );
            }
            // DOM初期化
            function init() {
                // 初回に現在地緯度経度を取得
                navigator.geolocation.getCurrentPosition(
                    initMap,
                    err => {
                        alert(err.message);
                    },
                    {
                        enableHighAccuracy: true,
                        timeout: 5000,
                        maximumAge: 0
                    }
                );
            }

            // Map初期化
            function initMap(initPos) {
                // #mapidにOSMタイルマップをレンダリング
                map = L.map("mapid").setView(
                    [initPos.coords.latitude, initPos.coords.longitude],
                    17
                );

                L.tileLayer(
                    "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
                    {
                        attribution:
                            '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
                    }
                ).addTo(map);

                // 現在地緯度経度を継続取得
                let watchId = navigator.geolocation.watchPosition(
                    pos => {
                        moveMapFollowingHuman(
                            pos.coords.latitude,
                            pos.coords.longitude,
                            pos.coords.heading
                        );
                    },
                    err => {
                        window.alert(err.message);
                    },
                    {
                        enableHighAccuracy: true,
                        timeout: 5000,
                        maximumAge: 0
                    }
                );
            }

            // 現在地変更ハンドラ
            function moveMapFollowingHuman(latitude, longitude, heading) {

                // 現在地circle描画を削除
                if (human) {
                    map.removeLayer(human);
                }
                // 現在地circle描画
                human = L.circle([latitude, longitude], {
                    color: "blue",
                    fillColor: "#30f",
                    fillOpacity: 0.5,
                    radius: 10
                }).addTo(map);
                human._path.id = "human";

                // 現在地を示すエレメントの画面位置座標を取得
                var clientRect = human._path.getBoundingClientRect();

                // 画面の左端から、要素の左端までの距離
                var x = clientRect.left;

                // 画面の上端から、要素の上端までの距離
                var y = clientRect.top;

                let beam = document.querySelector("#beam");
                let h = beam.clientHeight;
                let w = beam.clientWidth;
                beam.style.top = y - 40 + "px";
                beam.style.left = x - 30 + "px";
            }

            function detectDirection(e) {
                let absolute = event.absolute;
                let alpha = event.alpha;
                let beta = event.beta;
                let gamma = event.gamma;

                let degrees;
                if (os == "iphone") {
                    degrees = e.webkitCompassHeading;
                } else {
                    degrees = compassHeading(alpha, beta, gamma);
                }
                document.querySelector("#degree").value = degrees;

                let beam = document.querySelector("#beam");
                beam.style.transform = "rotate(" + degrees + "deg)";

                let iPhone = document.querySelector("#iPhone");
                iPhone.value = e.webkitCompassHeading;

                let accuracy = document.querySelector("#accuracy");
                accuracy.value = e.webkitCompassAccuracy;
            }

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

            function permitGeolocation() {
                DeviceOrientationEvent.requestPermission()
                    .then(response => {
                        if (response === "granted") {
                            window.addEventListener(
                                "deviceorientation",
                                detectDirection
                            );
                        }
                    })
                    .catch(console.error);
            }
        </script>
    </body>
</html>

動作確認

HTTPSサーバにindex.htmlを配備して、iPhone8(iOS13)+ Safariで動作確認。スマホを剥けた側にビームが向くようになりました。Android + Chromeでも同じ挙動です。

まとめ

DeviceOrientaitonイベントを使い、方角が0~360の範囲で分かりさえすれば、地図上で向きを表現することはそれほど難しくない事が分かりました。

やっぱり見た目に動きがあるとテンション上がりますね。

現在地はLeafletのcircle関数で作り、ビームはCSSで作ったので、地図のズームで表示位置がおかしくなるとかの問題点もあるサンプルですが、そこはまぁ・・・実際に実戦投入する際に考えればいいかな、といった感じです(適当人間)

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

執筆者:

関連記事

ブラウザから起動したカメラの撮影画像をjavascriptで圧縮【Compressor.js】

「モバイル用Webアプリで撮影したカメラ画像のファイルサイズが大きすぎる・・・」 そんな悩みは無いですか? 昨今カメラ会社の経営が傾くほどスマホのカメラ性能が向上、それに応じて年々ファイルサイズも増大 …

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

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

Stripe+Java+Payment Request APIでApple Pay、Google Payを使ったテストWeb決済をしてみる

自分で作ったサービスを運用してチャリンチャリンしたい・・・エンジニアならこんな夢、一度は見たことがあるんじゃないでしょうか。 夢を実現する為、以前Stripeのcheckout.jsを使ったテストWe …

B2BスマホアプリをGooglePlay、AppStoreに公開することがお勧め出来ない7つの理由とその対策

商材の性質やシーンに応じてスマホアプリをGooglePlayやAppleStoreのようなストアに公開することがマイナスに働くこともあります。 商材として価値の有る電子データをお持ちの商社さんとアプリ …

開発中のWebシステム上でApple Pay決済を有効化する為のApple Developer Program設定 

WebシステムでApple Pay決済を有効にするにはApple側にその旨を認証して貰う必要が有ります。 前回、Web上でApple Payテスト課金をする為のサンドボックステスターを作成しました。 …

 

shingo.nakanishi
 

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