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サーバを作る記事もその内書きたいと思ってます。shapeデータのDB保存で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
-,

執筆者:

関連記事

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

目次1 はじめに2 ビューワ機能を提供してくれるnpmモジュールを選別する3 実装開始3.1 ng2-pdfjs-viewerを使えるようにする3.2 PDFビューワコンポーネントを作る3.3 Hom …

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

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

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

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

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

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

ionic3(SPA)のServiceWorkerライブラリをsw-toolboxからWorkboxに切り替える

昨今は閲覧速度向上やオフラインでも使える利便性が求められ、Webアプリ開発とPWAはセットで考えられるようになってきましたね。 PWAは「ServiceWorker API」を使ってキャッシュする資源 …

 

shingo.nakanishi
 

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