地図上にドローンの位置を表示してみる

#はじめに
このページは，

[ドローン操作システムを作ろう](https://qiita.com/hsgucci/items/86eedb5555b4234ee0e7)

の１ページです．
全体を見たい場合は上記ページへお戻りください．

#概要

前回，WebブラウザでMQTTをSubscribeし，ドローンの情報を表示しました．
しかし，テキストメッセージを表示するだけでは面白くありません．

航空機の位置情報と言えば，
　[FlgithRader24](https://www.flightradar24.com/)[^0]
が有名です．（iOS/Android用のアプリもあります）
世界中の有人旅客機の位置情報が，地図上にリアルタイムに表示されています．
同じ様に，ドローンの現在位置も地図上にリアルタイムに表示させたいです．

**今回は入門として，<font color="blue">Leafletを使った地図表示に挑戦</font>します．**


**余談**
本企画では「全てのシステムを自力で構築する」にこだわっています．
例えば，著名なクラウドサービスを使えば，緯度経度の情報を投げれば地図上に自動表示してくれる機能があります．
また，ドローン専用のクラウドサービス(ほとんどがDJI用ですが)も多数存在しています．
本企画は
「そんなお高いサービスを使わずとも，安いLinuxサーバー１つで作れるよ」
を目指しています．
（問題は『WebページのGUIデザイン』で，これはプロに頼むしかありません）

#準備するもの

・これまで使用したLinux PC
　　前回Webサーバーを立てたPCです．引き続き利用します．

#Leafletについて

Leafletとは，Webブラウザで地図を表示するためのJavaScriptライブラリです．

最も有名な地図サービスといえばGoogleMaps[^1]ですが，
GoogleマップをJavaScriptで使う場合にはGoogle Maps APIを使用します．
ですが，Google社の地図しか表示できません．

[^1]: Google Maps http://maps.google.com/

一方，leafletを含めたオープンソースの地図表示プラグインは，
OpenStreetMap[^2]や，地理院地図[^3]，Mapbox[^4]など様々な地図を表示できるのが特徴です．

[^2]: Open Street Map https://www.openstreetmap.org/
[^3]: 地理院地図 https://maps.gsi.go.jp/
[^4]: Mapbox https://www.mapbox.com/

地理院のWebサイト[^5]
　[地理院タイルを用いたサイト構築サンプル集](https://maps.gsi.go.jp/development/sample.html)
には，著名な３種類のプラグイン
　　Leaflet[^6]，OpenLayers[^7]，Cesium[^8]
を使ったサンプルがあります．

[^5]: 地理院タイルを用いたサイト構築サンプル集 https://maps.gsi.go.jp/development/sample.html
[^6]: Leaflet JS https://leafletjs.com/
[^7]: OpenLayers https://openlayers.org/
[^8]: Cesium https://cesiumjs.org/

LeafletとOpenLayersは２次元地図の表示用です．この２者の優劣は常々比較されています．
Cesiumは３次元地図の表示用です．FlightRader24[^0]の"3D View"機能にも使われています．

[^0]: flightradar24 https://www.flightradar24.com/

**<font color="red">今回はシンプルに２次元地図のLeafletを使用します．</font>**
プログラムが軽い，プラグインが豊富，など，
カスタマイズが簡単である点に着目しました．

**余談**
地理院地図[^3]は，Leafletの能力をフルに使いこなしており，
「国土地理院が本気を出した」と言われるほどの作品です．
特にドローンをやる人は，人口集中地区の確認に使ったことがあるかと思います．
　[人口集中地区（DID） 平成27年](http://maps.gsi.go.jp/#9/35.801108/139.595032/&base=std&ls=std%7Cdid2015&blend=0&disp=11&lcd=did2010&vs=c1j0h0k0l0u0t0z0r0s0m0f0&d=vl)

#Leafletを学ぶ

Leafletの詳しい使い方を学ぶには，谷先生のページをお薦めします．
　　**谷謙二研究室（埼玉大学教育学部人文地理学）**
　　http://ktgis.net/service/leafletlearn/index.html

　
また，様々な方がLeafletを使って作った地図ページを，
　右クリック-[ページのソースを表示]
でソースコードを見て学ぶことも重要です．
いわゆるリバースエンジニアリングです（笑
JavaScriptなので，ソースを見ることが可能なのが良い点ですね．
（JavaScript難読化ツールとか使っていなければ，の話ですが）

ステップバイステップのチュートリアルも大事ですが，
実際の利用例を見ると非常に勉強になります．


#地図を表示
まずは，以下のコードをコピー＆ペーストするか，
または [ここ](https://raw.githubusercontent.com/DCoJA/OpenUVTM/master/chapter11/hello_leaflet.html) を右クリック-[名前を付けて保存]します．

```html:hello_leaflet.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8" /><!-- 文字コードはutf-8を使用する -->
	<title>Hello Leaflet!</title><!-- タイトルバーに表示されるメッセージ -->

    <!-- 以下の2行でLeafletで使用するスタイルシート(.css)とライブラリ(.js)を読み込む -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
    <script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js" integrity="sha512-GffPMF3RvMeYyc1LWMHtK8EbPv0iNZ8/oTtHPx9/cc2ILxQ+u905qIwdpULaqDkyBKgOaB57QTMg7ztg8Jm2Og==" crossorigin=""></script>
</head>
<body>

<!-- このmapidと名付けられたdiv要素の中に地図を表示する -->
<div id="mapid" style="width: 800px; height: 600px;"></div>

<!-- <script></script>で囲まれた部分がJavaScript -->
<script>
    // mapidと名の付いたdiv要素に地図を作成し，視点は富士山頂付近，ズームレベルは13に設定
	var mymap = L.map('mapid').setView([35.370772,138.727305], 13);

    // OpenStreetMapのタイルレイヤーを作る
    var tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
        attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
        maxZoom: 19
    });
    tileLayer.addTo(mymap); // 作成したtileLayerをmymapに追加する

    // マーカーを作り，それに紐付けるポップアップも同時作成
	L.marker([35.360772,138.727305]).addTo(mymap)       // 富士山頂にマーカーを追加
		.bindPopup("<b>Hello Leaflet!</b><br/>これはポップアップです").openPopup();   // ポップアップで出すメッセージ
</script>

</body>
</html>
```

保存した`hello_leaflet.html`を，Webサーバーが利用するフォルダへコピーします．
管理者権限が必要なので`sudo`を付けます．

```bash
$sudo cp hello_leaflet.html /var/www/html/
```
Webブラウザのアドレスバーに
`http://localhost/hello_leaflet.html`
と入力し，Enterしてページを表示させます．

こんな画面が表示されれば成功です．
![Screenshot at 2019-05-20 17-37-42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256470/4fa1391a-a230-8e19-8d33-01eada9f4c80.png)

オープンストリートマップを利用した地図上に，富士山頂にマーカーが置かれています．
GoogleMapsと同様に，マウスの左ドラッグで視点の移動，スクロールボタンでズーム率が変わります．
ポップアップのＸボタンで，ポップアップ自体を消すことができますし，
マーカーをクリックすれば再度ポップアップが表示されます．

##解説
では，`hello_leaflet.html`の解説をしていきます．

まず，HTMLファイルの基本構造です．

```html:HTMLの基本構造
<!DOCTYPE html>　　HTML5であることを示す『おまじない』
<html>
  <head>　ヘッダー
　　　スタイルシートやライブラリへの接続を記述
  </head>

  <body>　ボディー
　　　画面表示部

　　　<script> スクリプト
       JavaScriptを記述
     </script>
  </body>
</html>
```
まず先頭にHTML5であることを示す`<!DOCTYPE html>`がおまじないの様に書かれています．
HTMLは`<html>〜</html>`で囲まれた部分だけが有効になるので，最初と最後に書いてあります．
ヘッダー部`<head>〜</head>`には，画面には表示されない情報を書きます．
ボディー部`<body>〜</body>`には，画面に表示されるテキストが書かれます．
ボディー部の中にJavaScriptを記述する`<script>〜</script>`があります．

**余談**
`<script>〜</script>`タグ自体は，どこにでも，例えばhead部の中にも書くことも出来ます．
しかし，地図表示の命令は，後述するbody部の`<div id="mapid" ～`が定義された後に実行されなければなりません．
したがって，必ずbody部の後に書く必要があるのです．

###head部
次に，ヘッダー部を詳しく見ていきましょう．

```html:hello_leaflet.htmlのhead部
<head>
	<meta charset="utf-8" /><!-- 文字コードはutf-8を使用する -->
	<title>Hello Leaflet!</title><!-- タイトルバーに表示されるメッセージ -->

    <!-- 以下の2行でLeafletで使用するスタイルシート(.css)とライブラリ(.js)を読み込む -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
    <script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js" integrity="sha512-GffPMF3RvMeYyc1LWMHtK8EbPv0iNZ8/oTtHPx9/cc2ILxQ+u905qIwdpULaqDkyBKgOaB57QTMg7ztg8Jm2Og==" crossorigin=""></script>
</head>
```
今回は，「文字コードの指定」「タイトルバーに表示される文字列」「Leafletのスタイルシートの読み込み」「LeafletのJavaScriptライブラリの読み込み」が書かれています．

ここはLeafletのバージョンによって記載が変わります．2019年5月20日現在ではバージョン1.5.1なので，
`leaflet@1.5.1`という表記があることがわかります．

例えば，以前は以下の様に古いバージョンで書いていました．

```html:leaflet_ver.0.7.3の表記
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css" />
<script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>
```

```html:leaflet_ver.1.3.1の表記
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css" integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js" integrity="sha512-/Nsx9X4HebavoBvEBuyp3I7od5tA0UzAxs+j83KgC8PU0kgB4XiK4Lfe4y4cgBtaRJQEIFCW+oC506aPT2L1zw==" crossorigin=""></script>
```
leafletライブラリのバージョンが上がったら，ここを変更すれば良いわけです．

###body部
次はボディー部を詳しく見ていきましょう．

```html:hello_leaflet.htmlのbody部
<body>
<!-- このmapidと名付けられたdiv要素の中に地図を表示する -->
<div id="mapid" style="width: 800px; height: 600px;"></div>

</body>
```
今回Webブラウザの画面に表示されるのは`<div></div>`のたった一行です．
`<div>`はブロック要素と呼ばれており，「中身に何か入るよ」という入れ物だけの宣言です．
`<div>`の追加情報として`id="mapid"`が定義されています．これが重要です．
後述する`<script>〜</script>`部では，**<font color="red">mapid</font>**という名前が付いたブロック内に地図を表示させます．

###script部
次はスクリプト部です．

```html:hello_leaflet.htmlのスクリプト部
<!-- <script></script>で囲まれた部分がJavaScript -->
<script>
    // mapidと名の付いたdiv要素に地図を作成し，視点は富士山頂付近，ズームレベルは13に設定
	var mymap = L.map('mapid').setView([35.370772,138.727305], 13);

    // OpenStreetMapのタイルレイヤーを作る
    var tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
        attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
        maxZoom: 19
    });
    tileLayer.addTo(mymap); // 作成したtileLayerをmymapに追加する

    // マーカーを作り，それに紐付けるポップアップも同時作成
	L.marker([35.360772,138.727305]).addTo(mymap)       // 富士山頂にマーカーを追加
		.bindPopup("<b>Hello Leaflet!</b><br/>これはポップアップです").openPopup();   // ポップアップで出すメッセージ
</script>
```
まずは`var mymap = L.map('mapid')`で**<font color="red">mapid</font>**というidが付いたブロックに，地図ライブラリを紐付けます．
その後，`.setView([35.370772,138.727305], 13);`で視点を富士山頂付近に，ズーム率を13に設定しています．
JavaScriptでは，
**セミコロンを打たずにピリオドで続けて書くことで，連続して関数(メソッド)を実行ことができます．**
これを**<font color="red">メソッドチェーン</font>**と呼びます．
これで地図を描く領域だけができました．

次に`var tileLayer = L.tileLayer(タイルURL,オプション)`でタイル表示する地図サービスを指定しています．
タイルURLには，今回オープンストリートマップを使いました．
例えば地理院地図を使いたいときには，以下のページを参考にURLを記述します．
　[地理院タイル](https://maps.gsi.go.jp/development/ichiran.html)
オプション部分には，中括弧{}内にカンマ区切りで，
会社名やライセンスを表示する`attribution:`や，
地図サーバーに応じた最大ズーム率を指定する`maxZoom:`が書かれています．
`{ attribution: '会社名やライセンス表記', maxZoom: 最大ズーム率}`
この部分に何を書くべきかは，それぞれのタイルサーバー会社で指定があるので，それに従います．

次は`tileLayer.addTo(mymap);`です．
この関数で，先程作った地図領域**mymap**に，オープンストリートマップのタイルレイヤー地図を追加しています．
leafletでは基本的に**<font color="blue">addTo(なんとか)</font>**という記載をして紐付ける機会が多いです．

以上で地図の作成は完了です．

最後にマーカーを１つだけ作成します．
`L.marker([35.360772,138.727305]).addTo(mymap)`は，
緯度35.360772,経度138.727305にマーカーを作り，**<font color="blue">addTo(mymap)</font>**でマップ上に追加しています．
その次の行で，
`.bindPopup("<b>Hello Leaflet!</b><br/>これはポップアップです")`
`.openPopup();`
と続いているので，
「マーカーにポップアップメッセージを付加(bind)する」
「そのポップアップを開く」
と連続して命令実行されます．

#MQTT機能を追加

まずは，今回作った`hello_leaflet.html`と，
前回作った`echo.html`を，
マージして１つのHTMLファイルにしてみます．

以下のソースをコピー＆ペーストするか，
または [ここ](https://raw.githubusercontent.com/DCoJA/OpenUVTM/master/chapter11/hello_leaflet_echo.html) を右クリック-[名前を付けて保存]します．

```html:hello_leaflet_echo.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8" /><!-- 文字コードはutf-8を使用する -->
	<title>Hello Leaflet with echo!</title><!-- タイトルバーに表示されるメッセージ -->

    <!-- 以下の2行でLeafletで使用するスタイルシート(.css)とライブラリ(.js)を読み込む -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
    <script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js" integrity="sha512-GffPMF3RvMeYyc1LWMHtK8EbPv0iNZ8/oTtHPx9/cc2ILxQ+u905qIwdpULaqDkyBKgOaB57QTMg7ztg8Jm2Og==" crossorigin=""></script>

    <!-- 以下の2行で，MQTT over Websocketを使うライブラリ(.js)を読み込む -->
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
    <script src="mqttws31.js" type="text/javascript"></script>

    <!-- echo.htmlにあったスタイルシートをコピー -->
    <style>
      .box {
          width: 440px;
          float: left;
          margin: 0 20px 0 20px;
      }

      .box div, .box input {
          border: 1px solid;
          -moz-border-radius: 4px;
          border-radius: 4px;
          width: 100%;
          padding: 5px;
          margin: 3px 0 10px 0;
      }

      .box div {
          border-color: grey;
          height: 300px;
          overflow: auto;
      }

      div code {
          display: block;
      }

      #first div code {
          -moz-border-radius: 2px;
          border-radius: 2px;
          border: 1px solid #eee;
          margin-bottom: 5px;
      }

      #second div {
          font-size: 0.8em;
      }
    </style>
</head>

<body>
<!-- このmapidと名付けられたdiv要素の中に地図を表示する -->
<div id="mapid" style="width: 950px; height: 400px;"></div>

<!-- エディットボックスを作る部分．echo.htmlからコピーした -->
<div id="first" class="box">
  <h2>Received</h2>
  <div></div>
  <form><input autocomplete="off" value="Type here..."></input></form>
</div>
<div id="second" class="box">
  <h2>Logs</h2>
  <div></div>
</div>

<!-- <script></script>で囲まれた部分がJavaScript -->
<script>
    // leafletのスクリプト部分

    // mapidと名の付いたdiv要素に地図を作成し，視点は名南５区付近，ズームレベルは16に設定
	var mymap = L.map('mapid').setView([34.953102,136.817690], 16);

    // OpenStreetMapのタイルレイヤーを作る
    var tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
        attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
        maxZoom: 19
    });
    tileLayer.addTo(mymap); // 作成したtileLayerをmymapに追加する

    // マーカーを作り，それに紐付けるポップアップも同時作成
	L.marker([34.952794,136.817422]).addTo(mymap)       // 南５区にマーカーを追加
		.bindPopup("<b>Hello echo!</b><br/>これはポップアップです").openPopup();   // ポップアップで出すメッセージ
</script>

<script>
    // MQTT over WebSocketのスクリプト部分
    var has_had_focus = false;
    var pipe = function(el_name, send) {
        var div  = $(el_name + ' div');
        var inp  = $(el_name + ' input');
        var form = $(el_name + ' form');

        var print = function(m, p) {
            p = (p === undefined) ? '' : JSON.stringify(p);
            div.append($("<code>").text(m + ' ' + p));
            div.scrollTop(div.scrollTop() + 10000);
        };

        if (send) {
            form.submit(function() {
                send(inp.val());
                inp.val('');
                return false;
            });
        }
        return print;
    };

    var print_first = pipe('#first', function(data) {
        message = new Paho.MQTT.Message(data);
        message.destinationName = "drone/001";
        debug("SEND ON " + message.destinationName + " PAYLOAD " + data);
        client.send(message);
    });

    var debug = pipe('#second');

    var wsbroker = location.hostname;  //mqtt websocket enabled broker
    var wsport = 15675; // port for above

    var client = new Paho.MQTT.Client(wsbroker, wsport, "/ws",
        "myclientid_" + parseInt(Math.random() * 100, 10));

    client.onConnectionLost = function (responseObject) {
        debug("CONNECTION LOST - " + responseObject.errorMessage);
    };

    client.onMessageArrived = function (message) {
        debug("RECEIVE ON " + message.destinationName + " PAYLOAD " + message.payloadString);
        print_first(message.payloadString);
    };

    var options = {
        timeout: 3,
        onSuccess: function () {
            debug("CONNECTION SUCCESS");
            client.subscribe('drone/#', {qos: 1});
        },
        onFailure: function (message) {
            debug("CONNECTION FAILURE - " + message.errorMessage);
        }
    };

    if (location.protocol == "https:") {
        options.useSSL = true;
    }

    debug("CONNECT TO " + wsbroker + ":" + wsport);
    client.connect(options);

    $('#first input').focus(function() {
        if (!has_had_focus) {
            has_had_focus = true;
            $(this).val("");
        }
    });
</script>

</body>
</html>
```
保存した`hello_leaflet_echo.html`を，Webサーバーが利用するフォルダへコピーします．
ファイル名の後ろに_echoが付いた
**hello_leaflet<font color="red">_echo</font>.html**
なので注意してください
管理者権限が必要なので`sudo`を付けます．

```bash
$sudo cp hello_leaflet_echo.html /var/www/html/
```
Webブラウザのアドレスバーに
`http://localhost/hello_leaflet_echo.html`
と入力し，Enterしてページを表示させます．

こんな画面が表示されれば成功です．
![Screenshot at 2019-05-21 17-09-06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256470/c3a857e9-3bdb-7b84-d40c-67cb1f9caa46.png)

今回は名古屋南５区飛行場に視点が来ています．
見栄えが良くなるように，地図部分の大きさを調整してあります．

##Pubプログラムの実行

前々回(dronekitの情報をMQTTで送信してみる)に作った`sitl_mqtt_pub_json.py`を起動して，
ブローカーにメッセージを投げて(Pubして)おきます．

```bash:sitlドローンの位置情報をPubするプログラム
$python sitl_mqtt_pub_json.py
```
##実行結果

Webページ上に地図が表示され，
メッセージを受信すると`echo.html`の際と同様にテキストが表示されます．

![Screenshot at 2019-05-21 17-05-38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256470/e91de732-9025-9fb3-e2ae-2674dc94abee.png)

今回は，単純に「地図表示」と「メッセージ表示」を同時に行っただけで，
残念ながら相互の連携性はありません．

#地図上にマーカーを打つ

更にプログラムを改良し，
Subしたメッセージの情報を元に，地図上にマーカーを打ってみましょう．

以下のソースをコピー＆ペーストするか，
または [ここ](https://raw.githubusercontent.com/DCoJA/OpenUVTM/master/chapter11/hello_drone.html) を右クリック-[名前を付けて保存]します．

```html:hello_drone.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8" /><!-- 文字コードはutf-8を使用する -->
	<title>Hello Drone!</title><!-- タイトルバーに表示されるメッセージ -->

    <!-- 以下の2行でLeafletで使用するスタイルシート(.css)とライブラリ(.js)を読み込む -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
    <script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js" integrity="sha512-GffPMF3RvMeYyc1LWMHtK8EbPv0iNZ8/oTtHPx9/cc2ILxQ+u905qIwdpULaqDkyBKgOaB57QTMg7ztg8Jm2Og==" crossorigin=""></script>

    <!-- 以下の2行で，MQTT over Websocketを使うライブラリ(.js)を読み込む -->
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
    <script src="mqttws31.js" type="text/javascript"></script>

    <!-- echo.htmlにあったスタイルシートをコピー -->
    <style>
      .box {
          width: 440px;
          float: left;
          margin: 0 20px 0 20px;
      }

      .box div, .box input {
          border: 1px solid;
          -moz-border-radius: 4px;
          border-radius: 4px;
          width: 100%;
          padding: 5px;
          margin: 3px 0 10px 0;
      }

      .box div {
          border-color: grey;
          height: 300px;
          overflow: auto;
      }

      div code {
          display: block;
      }

      #second div {
          font-size: 0.8em;
      }
    </style>
</head>

<body>
<!-- このmapidと名付けられたdiv要素の中に地図を表示する -->
<div id="mapid" style="width: 950px; height: 400px;"></div>

<!-- エディットボックスを作る部分．#firstの方は削除した -->
<div id="second" class="box">
  <h2>Logs</h2>
  <div></div>
</div>

<!-- <script></script>で囲まれた部分がJavaScript -->
<script>
    // leafletのスクリプト部分

    // mapidと名の付いたdiv要素に地図を作成し，視点は柏の葉キャンパス駅前付近，ズームレベルは16に設定
	var mymap = L.map('mapid').setView([35.894087,139.952447], 17);

    // OpenStreetMapのタイルレイヤーを作る
    var tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
        attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
        maxZoom: 19
    });
    tileLayer.addTo(mymap); // 作成したtileLayerをmymapに追加する

    // MQTT over WebSocketのスクリプト部分
    var has_had_focus = false;
    var pipe = function(el_name, send) {
        var div  = $(el_name + ' div');
        var inp  = $(el_name + ' input');
        var form = $(el_name + ' form');

        var print = function(m, p) {
            p = (p === undefined) ? '' : JSON.stringify(p);
            div.append($("<code>").text(m + ' ' + p));
            div.scrollTop(div.scrollTop() + 10000);
        };
        return print;
    };

    var debug = pipe('#second');

    var wsbroker = location.hostname;  //mqtt websocket enabled broker
    var wsport = 15675; // port for above

    var client = new Paho.MQTT.Client(wsbroker, wsport, "/ws", "myclientid_" + parseInt(Math.random() * 100, 10));

    client.onConnectionLost = function (responseObject) {
        debug("CONNECTION LOST - " + responseObject.errorMessage);
    };

    client.onMessageArrived = function (message) {
        debug("RECEIVE ON " + message.destinationName + " PAYLOAD " + message.payloadString);

        var drone_name = message.destinationName;   // ドローン名はトピック名とする
        var drone_data = JSON.parse( message.payloadString );   // ドローンのデータを連想配列にして格納

        var arm  = drone_data.status.Arm;           // ARM/DISARM
        var mode = drone_data.status.FlightMode;    // フライトモード
        var lat  = parseFloat( drone_data.position.latitude );  // 緯度
        var lon  = parseFloat( drone_data.position.longitude ); // 経度
        var alt  = parseFloat( drone_data.position.altitude );  // 高度
        var ang  = parseFloat( drone_data.position.heading );   // 方位

        var drone_popmessage = drone_name + '<br>';
        drone_popmessage += mode + ',' + arm + '<br>';
        drone_popmessage += lon + ',' + lat + '<br>';
        drone_popmessage += alt + '[m], ' + ang + '[deg]<br>';

        L.marker([ lat, lon]).addTo(mymap)
            .bindPopup( drone_popmessage );

    };

    var options = {
        timeout: 3,
        onSuccess: function () {
            debug("CONNECTION SUCCESS");
            client.subscribe('drone/#', {qos: 1});
        },
        onFailure: function (message) {
            debug("CONNECTION FAILURE - " + message.errorMessage);
        }
    };

    if (location.protocol == "https:") {
        options.useSSL = true;
    }

    debug("CONNECT TO " + wsbroker + ":" + wsport);
    client.connect(options);

</script>

</body>
</html>
```

保存した`hello_drone.html`を，Webサーバーが利用するフォルダへコピーします．
ファイル名は**hello_<font color="red">drone</font>.html**
なので注意してください
管理者権限が必要なので`sudo`を付けます．

```bash
$sudo cp hello_drone.html /var/www/html/
```
Webブラウザのアドレスバーに
`http://localhost/hello_drone.html`
と入力し，Enterしてページを表示させます．

こんな画面が表示されれば成功です．
![Screenshot at 2019-05-21 18-09-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256470/de526541-1983-7458-5553-66c19414f009.png)

今回は柏の葉キャンパス駅前に視点が来ています．
初期状態ではマーカーは打たれていません．
MQTTの受信メッセージ表示は，**Logs**のボックスだけにしました．

##プログラム解説

###head部

LeafletとMQTT over Websocketの混在なので，それぞれのライブラリを呼び出しています．

Leafletにはスタイルシートファイル(.css)とJavaScriptファイル(.js)が必要です．

```html:Leaflet1.5.1を使うための呼び出し
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js" integrity="sha512-GffPMF3RvMeYyc1LWMHtK8EbPv0iNZ8/oTtHPx9/cc2ILxQ+u905qIwdpULaqDkyBKgOaB57QTMg7ztg8Jm2Og==" crossorigin=""></script>
```

MQTT over Websocketのライブラリは，jQueryを裏で使っているので，先に呼び出しています．

```html:MQTTを使うための呼び出し
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
<script src="mqttws31.js" type="text/javascript"></script>
```
`<style>〜</style>`で囲まれたスタイルシートの部分は，
`<body>`に書かれているdiv要素`<div id="second" class="box">`の性質を決めるためのものです．
`.box`や`second`に関係するスタイルが書かれています．
このdiv要素は，MQTTのLogを見るコンソールです．

```css:MQTTの表示をするdiv要素のスタイル
<style>
  .box {
      width: 440px;
      float: left;
      margin: 0 20px 0 20px;
  }

  .box div, .box input {
      border: 1px solid;
      -moz-border-radius: 4px;
      border-radius: 4px;
      width: 100%;
      padding: 5px;
      margin: 3px 0 10px 0;
  }

  .box div {
      border-color: grey;
      height: 300px;
      overflow: auto;
  }

  div code {
      display: block;
  }

  #second div {
      font-size: 0.8em;
  }
</style>
```

###body部

`<script>`を除いたbody部は，div要素が２個だけです．
地図表示のdivと，Log表示のdiv．(Logのdivには入れ子になっているdivがいます)
地図のdivには直接styleが書き込まれていますが，
Logのdivのstyleはhead部にありますね．

```html
<body>html:bodyにはdivが2個だけ
<!-- このmapidと名付けられたdiv要素の中に地図を表示する -->
<div id="mapid" style="width: 950px; height: 400px;"></div>

<!-- エディットボックスを作る部分．#firstの方は削除した -->
<div id="second" class="box">
  <h2>Logs</h2>
  <div></div>
</div>
```

###script部

script部で大事なポイントは，MQTTのメッセージを受信(Subscribe)した時の処理部分です．
具体的には`client.onMessageArrived`関数です．
名前の通り”メッセージ到着時”ですね．

```javascript:MQTT受信関数

client.onMessageArrived = function (message) {
    debug("RECEIVE ON " + message.destinationName + " PAYLOAD " + message.payloadString);

    var drone_name = message.destinationName;   // ドローン名はトピック名とする
    var drone_data = JSON.parse( message.payloadString );   // ドローンのデータを連想配列にして格納

    var arm  = drone_data.status.Arm;           // ARM/DISARM
    var mode = drone_data.status.FlightMode;    // フライトモード
    var lat  = parseFloat( drone_data.position.latitude );  // 緯度
    var lon  = parseFloat( drone_data.position.longitude ); // 経度
    var alt  = parseFloat( drone_data.position.altitude );  // 高度
    var ang  = parseFloat( drone_data.position.heading );   // 方位

    // ポップアップ用のメッセージを作成． +=で追加していく
    var drone_popmessage = drone_name + '<br>';
    drone_popmessage += mode + ',' + arm + '<br>';
    drone_popmessage += lon + ',' + lat + '<br>';
    drone_popmessage += alt + '[m], ' + ang + '[deg]<br>';


    L.marker([ lat, lon]).addTo(mymap)
        .bindPopup( drone_popmessage );

};
```
引数で渡される`message`クラスの中身は，
`message.destinationName`がトピック名，
`message.payloadString`がデータ本体です．
まずはこの２つを管理しやすいように別名の変数に代入しています．

```javascript:受信メッセージをドローン名とデータ本体に分離
var drone_name = message.destinationName;   // ドローン名はトピック名とする
var drone_data = JSON.parse( message.payloadString );   // ドローンのデータを連想配列にして格納
```
データ本体`message.payloadString`には，データがJSON型で入っているので，連想配列型に変換しています．
`JSON.parse`関数を使うと，JSON->連想配列の変換ができます．

**Pythonの辞書型と，JavaScriptの連想配列型はほぼ同じもの**と思っておけば良いです．
連想配列は違う書式の書き方もできますが，考え方は同じですね．
(シングルクォート'とダブルクォート"は，どちらでも良いです[^10] [^11])

[^10]: JavaScriptのシングルクォーテーションとダブルクォーテーション https://qiita.com/niusounds/items/f21a28e862a68a098ea7
[^11]: Pythonのシングルクォーテーションとダブルクォーテーションの違い　https://techacademy.jp/magazine/18887

```python:Python辞書型では，["キー"]を繋げて書く
drone_info["status"]["FlightMode"] = str(vehicle.mode.name)    
```

```javascript:JavaScript連想配列では，辞書型の様に書く，または直接キーを書いてピリオドで繋げる
var mode = drone_data['status']['FlightMode']; 
var mode = drone_data.status.FlightMode; 
```

こうして，連想配列の中身にアクセスし，
ドローンの情報(ARM状態，フライトモード，緯度，経度，高度，方位)を取り出しています．

```javascript:ドローン情報の取り出し
var arm  = drone_data.status.Arm;           // ARM/DISARM
var mode = drone_data.status.FlightMode;    // フライトモード
var lat  = parseFloat( drone_data.position.latitude );  // 緯度
var lon  = parseFloat( drone_data.position.longitude ); // 経度
var alt  = parseFloat( drone_data.position.altitude );  // 高度
var ang  = parseFloat( drone_data.position.heading );   // 方位
```
ついでに，吹き出し(ポップアップ)に表示される文字も作ります．
改行するところには，HTML言語の`<br>`を書き込みます．
Ｃ言語と違って，文字列を+で繋ぐだけで書くことができるのは，
高級言語(C++,JavaScript,Python)の便利なところですね．

```javascript:ポップアップ用のメッセージを作る
var drone_popmessage = drone_name + '<br>';
drone_popmessage += mode + ',' + arm + '<br>';
drone_popmessage += lon + ',' + lat + '<br>';
drone_popmessage += alt + '[m], ' + ang + '[deg]<br>';
```
最後に，lat(緯度)と経度(lon)の位置にマーカーを作っています．
`maker`,`addTo`,`bindPopup`を**<font color="red">メソッドチェーン</font>**で連続実行しています．

```javascript:指定した緯度・経度にマーカーを作成する
L.marker([ lat, lon])                // マーカーをデータ的に作る
    .addTo(mymap)                    // 地図上に追加して表示
    .bindPopup( drone_popmessage );  // ポップアップを付加
```

##Pubプログラムの実行

先ほど使った`sitl_mqtt_pub_json.py`をまた使います．

```bash:sitlドローンの位置情報をPubするプログラム
$python sitl_mqtt_pub_json.py
```

このプログラムの操作方法を思い出しておきましょう．

```python:キー入力部分
if kbhit():     # 何かキーが押されるのを待つ
    key = getch()   # 1文字取得
    # keyの中身に応じて分岐
    if key=='g':                # guided
        vehicle.mode = VehicleMode( 'GUIDED' )
    elif key=='l':              # land
        vehicle.mode = VehicleMode( 'LAND' )
    elif key=='a':              # arm
        vehicle.armed = True
    elif key=='d':              # disarm
        vehicle.armed = False
    elif key=='t':              # takeoff
        vehicle.simple_takeoff(alt=10)
    elif key=='1':              # simple_goto
        # 柏の葉キャンパス交番上空30mへ
        point = LocationGlobalRelative( 35.893246, 139.954909 , 30 )
        vehicle.simple_goto(point)
    elif key=='2':              # simple_goto
        # 三井ガーデンホテル上空50mへ
        point = LocationGlobalRelative( 35.895236, 139.952468 , 50 )
        vehicle.simple_goto(point)
    elif key=='r':              # RTL
        vehicle.mode = VehicleMode( 'RTL' )
```
飛行させるときは，

1. `g`キーでフライトモードを"GUIDED"にする
2. `a`キーでARMする（10秒経過するとDISARMするので注意）
3. `t`キーで離陸する
4. `1`キーで交番の方へ移動する
5. `2`キーでホテルへ移動する

の手順で動かしてください．


##実行結果

Webページ上に地図が表示され，
メッセージを受信するとドローンがいる場所にマーカーが打たれます．
マーカーをクリックすると，現在のフライトモード，ARM/DISARM状態や緯度経度がわかります．
![Screenshot at 2019-05-21 18-22-51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256470/eda0c88d-d590-f746-7c46-ae6914e496a1.png)

しかし，ドローンが移動しないと，
**<font color="red">ずっと同じ場所にマーカーが打たれるので，変化がよくわかりません</font>**．

Pubプログラムの方でキー入力し，
離陸させて，移動先ウェイポイントへ移動すれば，
こんな風に表示されます．
![Screenshot at 2019-05-21 18-20-15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256470/53c6ac58-43e5-b131-1061-799f9b393f7f.png)

マーカーを消すにはF5キーを押すか，[ページを再読込]ボタンを押してください．

#おわりに

今回は，LeafletとMQTTを連動させて，地図上にマーカーを打ってみました．
しかし，
1秒毎にメッセージが来る度に新しいマーカーを作成しているので，
**<font color="red">永遠にリソース(メモリ)が食われていく</font>**という**ダメプログラム**です．

マーカーについては，改善の必要がありますね．
次回はマーカーについて書きたいと思います．