Help us understand the problem. What is going on with this article?

Leafletのマーカーを変更する

はじめに

このページは,

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

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

概要

前回は,Leafletを利用した地図を表示させ,
MQTTで受信したドローンの位置情報を元にマーカーを打ってみました.

しかし,受信するたびに新規マーカーを作ってリソースを食っていくポンコツプログラムでした.

今回はこのマーカー部分に関して改善していきます.

マーカーに画像ファイルを使う

ドローンを操作するのですから,地図上に表示されるマーカーはやはりドローンらしい物が望ましいですね.
Leafletのマーカーは,画像ファイルに簡単に替えることができます.

画像ファイルの準備

今回はこの画像を使いましょう.
え?「どこかで見たことがある」って?気のせいですよ(笑
quad_x-90.png
まずは画像ファイルをダウンロードしておきましょう.
画像を 右クリック-[名前を付けて画像を保存] するか,
このリンク を 右クリック-[名前を付けて保存] してください.

保存したquad_x-90.pngを,Webサーバーが利用するフォルダへコピーします.
管理者権限が必要なのでsudoを付けます.

$sudo cp quad_x-90.png /var/www/html/

HTMLファイルの準備

次はHTMLファイル本体です.

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

maker_icon.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" /><!-- 文字コードはutf-8を使用する -->
    <title>Change Marker Icon</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>

    <script src="MovingMarker.js" type="text/javascript"></script>
    <script src="leaflet.rotatedMarker.js" type="text/javascript"></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に追加する

    // マーカーにする画像を読み込む
    var quad_x_Icon     = L.icon({ iconUrl: 'quad_x-90.png', iconRetinaUrl: 'quad_x-90.png', iconSize: [50, 50], iconAnchor: [25, 25], popupAnchor: [0, -50] });

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

    // MQTTメッセージSubscribe時の処理
    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 mode = drone_data.status.Arm;           // ARM/DISARM
        var arm  = 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>';

        // (lat, lon)の座標にマーカーを作り,icon情報を与え,ポップアップメッセージを追加する
        L.marker( [ lat, lon],{ icon:quad_x_Icon } ).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>

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

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

解説

前回の記事で最後に作ったhello_drone.htmlからの変更点は,
2箇所だけです

1つ目は,77行目です.

マーカーに使う画像ファイルの読み込み
var quad_x_Icon = L.icon({ iconUrl: 'quad_x-90.png', iconRetinaUrl: 'quad_x-90.png', iconSize: [50, 50], iconAnchor: [25, 25], popupAnchor: [0, -50] });

L.icon関数で,マーカー(アイコン)に使う画像ファイルを読み込みます.
それぞれの要素は,
iconUrl:一般的なPCのWebブラウザで使用する画像
iconRetinaUrl:Retinaディスプレイやそれに類する高精細ディスプレイの時に使用する画像
iconSize:表示する際のアイコンの大きさを[幅:高さ]で指定する.
     画像ファイルのサイズが違っても,この大きさに自動的に拡大縮小される.
iconAnchor:画像中のどの画素が実際にその緯度経度の真上に来るかを決める項目.[x,y]で与える.
       例えば,画ビョウの画像ではピンの先の位置が与えた緯度経度になって欲しい.
       しかし,ドローン画像では,機体の中心が与えた緯度経度になって欲しい.
popupAnchor:ポップアップ(吹き出し)の表示される位置を指定する.
       マーカーの形状によって表示したい場所が違うので.

2つ目は126行目です.

マーカーの作成時のオプションを追加
L.marker( [lat, lon],{ icon:quad_x_Icon } ).addTo( mymap ).bindPopup( drone_popmessage );

hello_drone.htmlの時は
L.marker([lat,lon]).addTo(mymap).bindPopup(drone_popmessage);
でしたが,L.marker関数の引数が追加されて,{ icon:quad_x_Icon }が増えています.
これで,77行目で作ったアイコンを使う様に命令しています.

Pubプログラムの実行

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

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

実行結果

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

こんな画面が表示されれば成功です.
見事にドローンのマーカーに替わっています.
Screenshot at 2019-05-22 18-32-26.png

しかし,Pub側プログラムを操作して,
GUIDED->ARM->TakeOff->simple_goto
してみると,こんな風になってしまいます.
Screenshot at 2019-05-22 18-42-47.png

マーカーを変更しただけで,
受信するたびに新規マーカーを作ってリソースを食っていくポンコツ
な部分はまだ改善していませんので,
以前の標準マーカーの時よりも残念な画面になっています...

もっと格好良く表示したいものです.

動くマーカーを使う

マーカー画像をオリジナルの物に変更することはできました.
次は,受信するたびに新規マーカーを打つというポンコツ部分を,
1つのマーカーがリアルタイムに移動する様に変えてみたいと思います.
ついでにドローンの方位と同様にマーカーの方位も変化する様にもしてしまいます.

Leafletプラグインの追加

マーカーを動く様にできるLeaflet用のプラグインがあります.

・マーカーの移動:MovingMarkerプラグイン
          ここ を右クリック-[名前を付けて保存]
・マーカーの回転:RotatedMarkerプラグイン
          ここ を右クリック-[名前を付けて保存]

保存したMovingMarker.jsleaflet.rotatedMarker.jsを,
Webサーバーが利用するフォルダへコピーします.
管理者権限が必要なのでsudoを付けます.

$sudo cp MovingMarker.js /var/www/html/
$sudo cp leaflet.rotatedMarker.js /var/www/html/

HTMLファイルの準備

次はHTMLファイル本体です.

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

marker_moving_rotated.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" /><!-- 文字コードはutf-8を使用する -->
    <title>Moving Marker</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>

    <script src="MovingMarker.js" type="text/javascript"></script>
    <script src="leaflet.rotatedMarker.js" type="text/javascript"></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に追加する

    // マーカーにする画像を読み込む
    var quad_x_Icon     = L.icon({ iconUrl: 'quad_x-90.png', iconRetinaUrl: 'quad_x-90.png', iconSize: [50, 50], iconAnchor: [25, 25], popupAnchor: [0, -50] });

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


    // ドローン情報保存用の配列
    var drones = new Array();   // 緯度経度などのデータを保存する連想配列
    var markers = new Array();  // マーカーハンドラを保存する連想配列

    // MQTTメッセージSubscribe時の処理
    client.onMessageArrived = function (message) {
        debug("RECEIVE ON " + message.destinationName + " PAYLOAD " + message.payloadString);   //debugボックスに表示

        var drone_name = message.destinationName;   // ドローン名はトピック名とする

        var drone_data = JSON.parse( message.payloadString );   // ドローンのデータを連想配列にして格納

        var mode = drone_data.status.Arm;           // ARM/DISARM
        var arm  = 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>';

        // drone_nameで登録されている連想配列があるか? ない->新規にマーカー作成 ある->なにもしない
        if( !drones[drone_name] ) {

            // movingMarkerでマーカーを作成し,rotatedMarkerのrotationオプションをつける
            markerHandle = L.Marker.movingMarker([[lat, lon]], [], 
                            {   rotationAngle: 0,   // 回転角度
                                rotationOrigin: 'center center',    // 回転中心
                                title:drone_name,
                                contextmenu: true,
                                contextmenuItems: [{
                                    text: drone_name,
                                    index: 0
                                }, {
                                    separator: true,
                                    index: 1
                                }]
                            });
            markerHandle.bindPopup( drone_popmessage ); // ポップアップメッセージの設定
            markerHandle.options.icon = quad_x_Icon;    // マーカーアイコンをオリジナル画像に
            markerHandle.options.autostart = true;      // MovingMarker機能を自動スタート
            markerHandle.addTo( mymap );                // 地図へ追加

            markers[drone_name] = markerHandle; // マーカー用連想配列に今回作ったマーカー情報を保存
        } else {
            markerHandle = markers[drone_name]  // 保存されているマーカー情報をdrone_name(トピック名)をキーにして呼び出す
            markerHandle.moveTo([lat,lon], 1000);  // MovingMarkerの移動機能
            markerHandle.setRotationAngle(ang);    // ratatedMarkerの回転機能
            markerHandle.setPopupContent( drone_popmessage );   // メッセージ更新
        }
        drones[drone_name] = drone_data; // ドローンデータ用連想配列の情報を最新のメッセージに更新

    };

    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>

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

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

プログラム解説

head部

ヘッダー部には,新しく使うMovingMarkerrotatedMarkerのプラグインの読み込みが追加されています.

Leafletプラグインの追加
<script src="MovingMarker.js" type="text/javascript"></script>
<script src="leaflet.rotatedMarker.js" type="text/javascript"></script>

ローカルにある.jsファイルを使うので,srcに書くURLが短いですね.

body部

ボディ部に変更はありません.

script部

MQTTのメッセージ受信(Subscribe)時に毎回マーカーを作るのではなく,
1個のマーカーを動かす様に書き換えます.

まず,client.onMessageArrived関数の上に,グローバル変数の作成をします.

グローバル変数
// ドローン情報保存用の配列
var drones = new Array();   // 緯度経度などのデータを保存する連想配列
var markers = new Array();  // マーカーハンドラを保存する連想配列

今回はドローン1台しか表示しないので,グローバル変数をArray(配列)にする必要はなかったのですが,
今後は複数ドローンの表示に対応したいので,Arrayで宣言しておきます.

client.onMessageArrived関数では,メッセージからトピック名とデータを取り出し,
ドローンの各種データを取り出し,ポップアップメッセージを作成するところ,
までは上述したプログラムと同じです.

MQTTメッセージ受信時の処理は途中までは同じ
// MQTTメッセージSubscribe時の処理
client.onMessageArrived = function (message) {
    debug("RECEIVE ON " + message.destinationName + " PAYLOAD " + message.payloadString);   //debugボックスに表示

    var drone_name = message.destinationName;   // ドローン名はトピック名とする

    var drone_data = JSON.parse( message.payloadString );   // ドローンのデータを連想配列にして格納

    var mode = drone_data.status.Arm;           // ARM/DISARM
    var arm  = 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>';

このif文からがドローンを1台だけ表示するためのポイントです.

ドローンデータがまだ無いか/既にあるかで,新規マーカー作成/マーカー情報更新を分岐する
    // drone_nameで登録されている連想配列があるか? ない->新規にマーカー作成 ある->なにもしない
    if( !drones[drone_name] ) {

        // movingMarkerでマーカーを作成し,rotatedMarkerのrotationオプションをつける
        markerHandle = L.Marker.movingMarker([[lat, lon]], [], 
                        {   rotationAngle: 0,   // 回転角度
                            rotationOrigin: 'center center',    // 回転中心
                            title:drone_name,
                            contextmenu: true,
                            contextmenuItems: [{
                                text: drone_name,
                                index: 0
                            }, {
                                separator: true,
                                index: 1
                            }]
                        });
        markerHandle.bindPopup( drone_popmessage ); // ポップアップメッセージの設定
        markerHandle.options.icon = quad_x_Icon;    // マーカーアイコンをオリジナル画像に
        markerHandle.options.autostart = true;      // MovingMarker機能を自動スタート
        markerHandle.addTo( mymap );                // 地図へ追加

        markers[drone_name] = markerHandle; // マーカー用連想配列に今回作ったマーカー情報を保存
    } else {
        markerHandle = markers[drone_name]  // 保存されているマーカー情報をdrone_name(トピック名)をキーにして呼び出す
        markerHandle.moveTo([lat,lon], 1000);  // MovingMarkerの移動機能
        markerHandle.setRotationAngle(ang);    // ratatedMarkerの回転機能
        markerHandle.setPopupContent( drone_popmessage );   // メッセージ更新
    }
    drones[drone_name] = drone_data; // ドローンデータ用連想配列の情報を最新のメッセージに更新

};

グローバルdrones配列はvar drones = new Array();で定義されているので,初期状態では中身はありません.
drone_nameにはトピック名が入っていますから,このif文

drones配列の中に,キーがdrone_nameのデータがあるか?ないか?
    if( !drones[drone_name] ) {

の中のdrones[drone_name]は,
具体的にはdrones["drone/001"]というデータがあるか?という問い合わせになります.
初期状態では当然drones配列の中身はないので,結果はfalseになります.
しかし,!マークが付けてあるので,論理が反転します.
というわけでdrones["drone/001"]というデータは無いよね?という問い合わせになり,答えはtrue
概念的に書くと以下の様になります.

if(drones["drone/001"]が「ない」) {  // 真のとき
   はじめて到着したデータなので,マーカーを新しく作る
} else {  // 偽のとき
   すでにデータが有る=マーカーも有る,
   なので,マーカーは新しく作らず,既存のマーカーの情報のアップデートだけする
}

drones["drone/001"]データを作る(なので2回目の受信時は絶対にfalseになる)

マーカーの新規作成時の処理は,以下の様になります.

マーカーの新規作成とグローバル変数への登録
// movingMarkerでマーカーを作成し,rotatedMarkerのrotationオプションをつける
markerHandle = L.Marker.movingMarker([[lat, lon]], [], 
                {   rotationAngle: 0,   // 回転角度
                    rotationOrigin: 'center center',    // 回転中心
                    title:drone_name,
                    contextmenu: true,
                    contextmenuItems: [{
                        text: drone_name,
                        index: 0
                    }, {
                        separator: true,
                        index: 1
                    }]
                });
markerHandle.bindPopup( drone_popmessage ); // ポップアップメッセージの設定
markerHandle.options.icon = quad_x_Icon;    // マーカーアイコンをオリジナル画像に
markerHandle.options.autostart = true;      // MovingMarker機能を自動スタート
markerHandle.addTo( mymap );                // 地図へ追加

markers[drone_name] = markerHandle; // マーカー用連想配列に今回```
  1. L.Marker.movingMarker新規マーカーをmovingMakerで作成し,オプション設定にrotatedMarkerの条件を追加する
  2. bindPopup:マーカーにポップアップメッセージを付加
  3. options.icon:マーカーアイコンはドローンのアイコンに設定
  4. options.autostart:MovingMarkerの移動は自動で始まる
  5. addTo:地図へ追加

というマーカー作成処理を行った後で,

グローバル配列markersに,{トピック名drone_name:マーカーの管理ハンドルmarkerHandle}を追加しています.
これでメッセージの到着2回目以降は,このmarkerHandleを取り出して使用すれば,
既に存在しているマーカーを操作することが可能になります.


マーカーの情報のアップデート処理は,以下の様になります.

既存のマーカーハンドルを取り出して,更新する
markerHandle = markers[drone_name]  // 保存されているマーカー情報をdrone_name(トピック名)をキーにして呼び出す
markerHandle.moveTo([lat,lon], 1000);  // MovingMarkerの移動機能
markerHandle.setRotationAngle(ang);    // ratatedMarkerの回転機能
markerHandle.setPopupContent( drone_popmessage );   // メッセージ更新

drones["drone/001"]というデータがあれば,
当然markers["drone/001"]というデータもあるので,
まずはマーカー管理ハンドルを取り出します.markerHandle = markers[drone_name]

後は,マーカーの位置や向き,ポップアップをアップデートします
1. moveTo:MovingMarkerの関数を使って新しい緯度・経度へ移動する.移動には1000ミリ秒かける.
2. setRotationAngle:rotatedMarkerの関数を使ってマーカーの向きを変更する.
3. setPoupuContent:ポップアップメッセージを新しいものに替える


最後に,グローバル変数配列に,{トピック名drone_name:ドローン情報drone_data}を追加します.
当該トピック名のデータが無い時は新規に作成されます.ある時はデータ更新されます.
なので,メッセージ到着2回目以降はif文は常にfalseになります.

グローバル配列dronesにデータを格納する
    }
    drones[drone_name] = drone_data; // ドローンデータ用連想配列の情報を最新のメッセージに更新

Pubプログラムの実行

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

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

実行結果

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

Pub側プログラムを操作して移動させると,こんな画面が表示さるはずです.
Screenshot at 2019-05-22 19-24-55.png

ちゃんと表示されています.
しかし,実はPub側プログラムを終了させても,地図上のマーカーは最後の位置で残り続けてしまいます
「終了処理」をやっていませんので.

本来は,
一定時間メッセージが来なくなったらマーカーを削除する
という処理が必要ですね.

このあたりまで来ると,ゲームプログラミング経験のある人は,
「ネットワークゲームの様なマネージメントが必要になるんだな」
と思うでしょう.

・複数ユーザー(ドローン)の接続に対して,異なるアバター(マーカー)を表示する
・急に切断した(LANケーブルぶっこ抜き)ユーザーのアバターを速やかに削除する
・ユーザーの行動ログを取る
・NPC(疑似ドローン)を表示させる

様々な機能が考えられますが,実装が大変ですね.

おわりに

ドローンのマーカーを地図上にリアルタイムに表示させ,移動や回転できるようにもなりました.
まだまだ機能アップしたい点はありますが,
次回はMQTTでドローンの操作をやりたいと思います.

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away