One IT Thing

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

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

執筆者:

関連記事

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

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

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

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

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

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

mkcertとhttp-serverでHTTPS環境を作りAndroid(chrome)、iPhone(safari)から接続

簡単にパーフェクトなオレオレ証明書が作れるとgithub上で人気上昇中の「mkcert」。  GitHub  122 usersFiloSottile/mkcerthtt …

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

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

 

shingo.nakanishi
 

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

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

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