13
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-05-21

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

ドローン操作システムを作ろう

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

#概要

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

航空機の位置情報と言えば,
 FlgithRader241
が有名です.(iOS/Android用のアプリもあります)
世界中の有人旅客機の位置情報が,地図上にリアルタイムに表示されています.
同じ様に,ドローンの現在位置も地図上にリアルタイムに表示させたいです.

今回は入門として,Leafletを使った地図表示に挑戦します.

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

#準備するもの

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

#Leafletについて

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

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

一方,leafletを含めたオープンソースの地図表示プラグインは,
OpenStreetMap3や,地理院地図4,Mapbox5など様々な地図を表示できるのが特徴です.

地理院のWebサイト6
 地理院タイルを用いたサイト構築サンプル集
には,著名な3種類のプラグイン
  Leaflet7,OpenLayers8,Cesium9
を使ったサンプルがあります.

LeafletとOpenLayersは2次元地図の表示用です.この2者の優劣は常々比較されています.
Cesiumは3次元地図の表示用です.FlightRader241の"3D View"機能にも使われています.

今回はシンプルに2次元地図のLeafletを使用します.
プログラムが軽い,プラグインが豊富,など,
カスタマイズが簡単である点に着目しました.

余談
地理院地図4は,Leafletの能力をフルに使いこなしており,
「国土地理院が本気を出した」と言われるほどの作品です.
特にドローンをやる人は,人口集中地区の確認に使ったことがあるかと思います.
 人口集中地区(DID) 平成27年

#Leafletを学ぶ

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

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

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

#地図を表示
まずは,以下のコードをコピー&ペーストするか,
または ここ を右クリック-[名前を付けて保存]します.

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を付けます.

$sudo cp hello_leaflet.html /var/www/html/

Webブラウザのアドレスバーに
http://localhost/hello_leaflet.html
と入力し,Enterしてページを表示させます.

こんな画面が表示されれば成功です.
Screenshot at 2019-05-20 17-37-42.png

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

##解説
では,hello_leaflet.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部
次に,ヘッダー部を詳しく見ていきましょう.

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という表記があることがわかります.

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

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>
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部
次はボディー部を詳しく見ていきましょう.

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>部では,**mapid**という名前が付いたブロック内に地図を表示させます.

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

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')で**mapidというidが付いたブロックに,地図ライブラリを紐付けます.
その後,.setView([35.370772,138.727305], 13);で視点を富士山頂付近に,ズーム率を13に設定しています.
JavaScriptでは,
セミコロンを打たずにピリオドで続けて書くことで,連続して関数(メソッド)を実行ことができます.
これを
メソッドチェーン**と呼びます.
これで地図を描く領域だけができました.

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

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

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

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

#MQTT機能を追加

まずは,今回作ったhello_leaflet.htmlと,
前回作ったecho.htmlを,
マージして1つの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要素に地図を作成し,視点は名南5区付近,ズームレベルは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)       // 南5区にマーカーを追加
		.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_echo.html
なので注意してください
管理者権限が必要なのでsudoを付けます.

$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

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

##Pubプログラムの実行

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

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

##実行結果

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

Screenshot at 2019-05-21 17-05-38.png

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

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

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

以下のソースをコピー&ペーストするか,
または ここ を右クリック-[名前を付けて保存]します.

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_drone.html
なので注意してください
管理者権限が必要なのでsudoを付けます.

$sudo cp hello_drone.html /var/www/html/

Webブラウザのアドレスバーに
http://localhost/hello_drone.html
と入力し,Enterしてページを表示させます.

こんな画面が表示されれば成功です.
Screenshot at 2019-05-21 18-09-14.png

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

##プログラム解説

###head部

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

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

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を裏で使っているので,先に呼び出しています.

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">の性質を決めるためのものです.
.boxsecondに関係するスタイルが書かれています.
このdiv要素は,MQTTのLogを見るコンソールです.

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要素が2個だけです.
地図表示のdivと,Log表示のdiv.(Logのdivには入れ子になっているdivがいます)
地図のdivには直接styleが書き込まれていますが,
Logのdivのstyleはhead部にありますね.

<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関数です.
名前の通り”メッセージ到着時”ですね.

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がデータ本体です.
まずはこの2つを管理しやすいように別名の変数に代入しています.

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

データ本体message.payloadStringには,データがJSON型で入っているので,連想配列型に変換しています.
JSON.parse関数を使うと,JSON->連想配列の変換ができます.

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

Python辞書型では,["キー"]を繋げて書く
drone_info["status"]["FlightMode"] = str(vehicle.mode.name)    
JavaScript連想配列では,辞書型の様に書く,または直接キーを書いてピリオドで繋げる
var mode = drone_data['status']['FlightMode']; 
var mode = drone_data.status.FlightMode; 

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

ドローン情報の取り出し
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言語と違って,文字列を+で繋ぐだけで書くことができるのは,
高級言語(C++,JavaScript,Python)の便利なところですね.

ポップアップ用のメッセージを作る
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を**メソッドチェーン**で連続実行しています.

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

##Pubプログラムの実行

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

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

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

キー入力部分
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

しかし,ドローンが移動しないと,
ずっと同じ場所にマーカーが打たれるので,変化がよくわかりません

Pubプログラムの方でキー入力し,
離陸させて,移動先ウェイポイントへ移動すれば,
こんな風に表示されます.
Screenshot at 2019-05-21 18-20-15.png

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

#おわりに

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

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

  1. flightradar24 https://www.flightradar24.com/ 2

  2. Google Maps http://maps.google.com/

  3. Open Street Map https://www.openstreetmap.org/

  4. 地理院地図 https://maps.gsi.go.jp/ 2

  5. Mapbox https://www.mapbox.com/

  6. 地理院タイルを用いたサイト構築サンプル集 https://maps.gsi.go.jp/development/sample.html

  7. Leaflet JS https://leafletjs.com/

  8. OpenLayers https://openlayers.org/

  9. Cesium https://cesiumjs.org/

  10. JavaScriptのシングルクォーテーションとダブルクォーテーション https://qiita.com/niusounds/items/f21a28e862a68a098ea7

  11. Pythonのシングルクォーテーションとダブルクォーテーションの違い https://techacademy.jp/magazine/18887

13
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?