MarkLogicでRaspberryPi3のセンサー情報を取り込んでみよう（8）MarkLogicでセンサーデータをビジュアル化

この記事は[MarkLogic Advent Calendar 2017](https://qiita.com/advent-calendar/2017/marklogic)の24日目です。

# はじめに
前回まではRaspberry Pi3に取り付けたセンサー情報をMQTTでパブリッシュし、サブスクライバーのMarkLogicに登録するところまで実現しました。
今回は、このデータをリアルタイムにグラフ化してみようと思います。

# 環境
サーバサイドは以下の環境を使用します。

| 環境 | バージョン |
| --- | --- |
| CentOS7 | 7.4.1708 |
| Node.js | v8.9.1 |
| npm | 5.5.1 |
| Mosca | 2.7.0 |
| MQTT.js | 2.14.0 |
| MarkLogic9 | 9.0-3 |
| MarkLogic Node.js Client API | 2.0.3 |

Raspberry Pi3側は以下の環境を使用します。

| 環境 | バージョン |
| --- | --- |
| Raspberry Pi3 Model B |  |
| RASPBIAN STRETCH WITH DESKTOP | November 2017 |
| Python3 | 3.6.3 |
| paho-mqtt | 1.3.1 |
| [GLOBALSAT BU-353S4 USB GPSレシーバー](https://www.tokyo2show.co.jp/gps/globalsat_bu353s4) | - |
| [Sense HAT](https://www.raspberrypi.org/products/sense-hat/) | 2.2.0 |

今回はWebブラウザも使います。以下のJavascriptライブラリを使用します。

| 環境 | バージョン |
| --- | --- |
| Chart.js | 2.7.1 |
| paho-mqtt | 1.0.1 |
| jQuery | 3.2.1 |

# やってみること
MarkLogicは独自のHTTPサーバを搭載しています。
管理画面やQueryConsoleといったMarkLogicの管理系の画面にアクセスしたり、REST-API用に使用したりしますが、もちろん通常のHTTPサーバと同様にHTMLを返すことも可能です。
XQueryでHTMLを返却するようにすれば、Apache等のサードパーティのHTTPサーバを用意しなくても、MarkLogic単体でWebアプリケーションを構築できます。

今回はMarkLogicのHTTPサーバを使用したWebアプリケーションを開発してみます。
[Chart.js](http://www.chartjs.org/)を使用して、ラズパイから取得したセンサーデータをグラフ化してみます。
また、MQTTを使ってセンサーデータを受信したらリアルタイムにグラフを更新するようにしてみます。

![pic01_image_01.png](https://qiita-image-store.s3.amazonaws.com/0/187299/7914a289-5a66-ecd6-6b2a-c3566dc4989a.png)

Raspberry Pi3からMQTTでMarkLogicにセンサーデータを登録する流れがこれまでの内容でした。
今回は、MarkLogicのHTTPサーバにデプロイするXQueryとChart.jsを使って、登録したセンサーデータをグラフ化します。
しかし、このままでは初回表示時からグラフが更新されないため、ブラウザ側でもMQTTでセンサーデータをサブスクライブして、リアルタイムにグラフを描画してみます。

# こんな感じです
こんな感じでリアルタイムに更新するグラフを作成してみました。

温度の折れ線グラフです。

![pic03_Graph_01.png](https://qiita-image-store.s3.amazonaws.com/0/187299/a979fb83-66da-780d-0c3e-da24aabad666.png)

続いて湿度の折れ線グラフです。

![pic04_Graph_02.png](https://qiita-image-store.s3.amazonaws.com/0/187299/43bafe16-05c0-d659-bba7-a39bca60dd0c.png)

気圧のグラフです。棒グラフも表示できます。

![pic05_Graph_03.png](https://qiita-image-store.s3.amazonaws.com/0/187299/fc40d495-41b7-06a0-f17e-54de62b03e2e.png)

このように、MarkLogicとMQTTとChart.jsを使って、センサーデータを動的にビジュアライズさせてみます。

# グラフ描画してみます
MarkLogicにデプロイするXQueryを作成します。
このXQueryはDBからデータを取得して、それを描画するHTMLを作成してブラウザに返します。

なお、XQueryファイルはMarkLogicのHTTPサーバのモジュールDBに配置します。
モジュールDBやモジュールの配置場所はMarkLogicの管理画面(8001番ポート)で設定します。以下は、管理画面のHTTPサーバの設定画面です。

![pic02_http_01.png](https://qiita-image-store.s3.amazonaws.com/0/187299/ab55103b-8af1-7a2e-0cf8-1c108b184563.png)

設定項目の「root」にモジュールを配置するディレクトリのルートを指定します。今回は"/opt/MarkLogic/Apps"を指定しています。
また、「modules」でモジュールを配置するDBを選択します。今回はOSのファイルシステム上に格納したため、「(file system)」を選択しています。

今回の設定では、ファイルシステムの"/opt/MarkLogic/Apps"ディレクトリをルートとして、その直下に"iot"ディレクトリを作成し、そこにXQueryやJavascriptファイルを格納しています。
HTTPサーバのポート番号は8103としました。

Webブラウザからアクセスする場合は、"http\://[MarkLogicサーバのIPアドレス]:8103/iot/raspi-visualize.xqy"となります。

## HTMLを返却するXQueryファイル
HTMLを返却するXQueryは以下のように作成しました。
実際にデータを取得する処理は別モジュールの"get-raspi-data.xqy"として定義しています。

グラフの種類（折れ線グラフ、棒グラフ）、データの種類（温度、湿度、気圧）を選択できるようにしています。

```xquery:raspi-visualize.xqy
xquery version "1.0-ml";

import module namespace r="get-raspi-data" at "/iot/get-raspi-data.xqy";

xdmp:set-response-content-type("text/html"),
'<!DOCTYPE html>',
<html xmlns="http://www.w3.org/1999/xhtml" lang="ja">
<head>
  <title>Raspberry Pi3 Chart</title>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.js" type="text/javascript"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.bundle.min.js"></script>
  <script src="utils.js"></script>
  <script src="drawchart.js"></script>
  <script src="raspi-mqtt.js"></script>
</head>

<body>
  <select id="graphType">
    <option value="line">折れ線グラフ</option>
    <option value="bar">棒グラフ</option>
  </select>
  <select id="dataType">
    <option value="temperature">温度</option>
    <option value="humidity">湿度</option>
    <option value="pressure">気圧</option>
  </select>
  <canvas id="Chart" height="100"></canvas>

  <!-- グラフ描画とセレクトボックス操作時の再描画 -->
  <script>
    myApps.draw({r:get-raspi-data()});

    $("#graphType").change(myApps.changeGraphType({r:get-raspi-data()}));
    $("#dataType").change(myApps.changeDataType({r:get-raspi-data()}));
  </script>
</body>
</html>
```

## データを取得するXQueryファイル
上記のXQueryの外部モジュールとして、データ取得用のXQueryを用意しました。温度、気温、湿度等を取得しています。
時刻の降順でソートし、最新の20件のみ返却するようにしています。

```xquery:get-raspi-data.xqy
xquery version "1.0-ml";
module namespace raspi = "get-raspi-data";

declare function get-raspi-data(){
let $raspiData :=
  for $i in cts:uris("", (), cts:directory-query("/mqtt/","infinity"))
    let $doc := fn:doc($i)
    let $time := $doc/data/message/date/text()
    let $lat := $doc/data/message/gps/lat/text()
    let $long := $doc/data/message/gps/long/text()
    let $temperature := $doc/data/message/senseHat/temperature/text()
    let $pressure := $doc/data/message/senseHat/pressure/text()
    let $humidity := $doc/data/message/senseHat/humidity/text()
    order by $time descending
    return
      if(($time ne 'n/a') and ($lat) and ($long) and ($temperature) and ($pressure) and ($humidity))
      then(
        let $object := json:object()
        let $_ := map:put($object,"time",$time)
        let $_ := map:put($object,"lat",$lat)
        let $_ := map:put($object,"long",$long)
        let $_ := map:put($object,"temperature",$temperature)
        let $_ := map:put($object,"pressure",$pressure)
        let $_ := map:put($object,"humidity",$humidity)
        return xdmp:to-json($object)
      )else()
  return xdmp:to-json-string(fn:reverse($raspiData[1 to 20]))
};
```

次にDBから取得したセンサーデータをChart.jsで描画するためのJavascriptを作成します。
DBから取得したデータを加工して、Chart.jsに渡すだけです。

```js:drawchart.js
var myApps = new Object();

myApps.selectedGraphType = "line";
myApps.selectedDataType = "temperature";

myApps.time = [];
myApps.temperature = [];
myApps.pressure = [];
myApps.humidity = [];

// 取得したデータをChar.jsで表示するための処理（初回表示時）
myApps.draw = (jsonData) => {
  // グラフ描画に必要なデータを保持しておく
  myApps.time = [];
  myApps.temperature = [];
  myApps.pressure = [];
  myApps.humidity = [];

  // 取得したJSONデータから描画用の変数に格納する
  for(var i = 0; i < jsonData.length; i++){
    var time = jsonData[i].time;
    var selectedData = "";

    myApps.time.push(jsonData[i].time);
    myApps.temperature.push(jsonData[i].temperature);
    myApps.pressure.push(jsonData[i].pressure);
    myApps.humidity.push(jsonData[i].humidity);
  }

  // 選択したデータの種類に応じてグラフの色を変える。
  var pointColor = "rgba(220,220,220,1)";
  var borderColor = "rgba(173,216,230,1)";
  var backgroundColor =  "rgba(173,216,230,0.5)";
  switch(myApps.selectedDataType){
    case 'temperature':
      selectedData = myApps.temperature;
      borderColor = "rgba(173,216,230,1)";
      backgroundColor =  "rgba(173,216,230,0.5)";
      break;
    case 'pressure':
      selectedData = myApps.pressure;
      borderColor = "rgba(255,182,193,1)";
      backgroundColor= "rgba(255,182,193,0.5)";
      break;
    case 'humidity':
      selectedData = myApps.humidity;
      borderColor = "rgba(255,165,0,1)";
      backgroundColor = "rgba(255,165,0,0.5)";
      break;
  }

  // Chart.js描画
  var ctx = document.getElementById("Chart");
  myApps.chart = new Chart(ctx, {
    type: myApps.selectedGraphType,
    data: {
     labels: myApps.time,
     datasets: [
       {
         borderColor: borderColor,
         backgroundColor: backgroundColor,
         data : selectedData//myApps.data
       }
     ]
    },
    options:{
      legend:{display:false},
      title:{
        display:true,
        text:'センサーデータ'},
      scales: {
        yAxes: [{
          display: true,
          scaleLabel:{
            display: true,
            labelString : myApps.selectedDataType 
          }}]}}});}

// グラフの種類を変更した場合
myApps.changeGraphType = (data) => {
  $('#graphType').change(() => {
     myApps.selectedGraphType = $('#graphType').val();
     myApps.data = data;
     myApps.draw(myApps.data);
});}

// グラフのデータを変更した場合
myApps.changeDataType = (data) => {
  $('#dataType').change(() => {
     myApps.selectedDataType= $('#dataType').val();
     myApps.data = data;
     myApps.draw(myApps.data);
});}

// 新しいデータを追加して再描画する
myApps.update = (newData) => {
  var jsonMessage = JSON.parse(newData);
  var temperature = jsonMessage.senseHat.temperature;
  var pressure = jsonMessage.senseHat.pressure;
  var humidity = jsonMessage.senseHat.humidity;
  var time = jsonMessage.gps.time;
  
  myApps.time.push(time);
  myApps.temperature.push(temperature);
  myApps.pressure.push(pressure);
  myApps.humidity.push(humidity);
  myApps.chart.update(); 
}
```

この段階で初回表示時にグラフは表示されますが、リアルタイムに更新されません。

ブラウザでもセンサーデータをMQTTでサブスクライブして、リアルタイムに更新してみます。
以下のコードはセンサーデータをサブスクライブするJavascirptです。最初に記載したXQueryのHTMLでロードしています。

```js:raspi-mqtt.js
var client = new Paho.MQTT.Client([MQTTブローカーのホスト], [ポート番号], "client1");

client.onConnectionLost = onConnectionLost;
client.onMessageArrived = onMessageArrived;

// MQTTブローカーに接続する
client.connect({userName:"subscriber",password:"subscriber", onSuccess:onConnect, onFailure:failConnect});

// センサーデータのトピックをサブスクライブする
function onConnect() {
  console.log("onConnect");
  client.subscribe("raspi/topic1");
}

function failConnect(e){
  console.log("connect fail");
  console.log(e);
}

function onConnectionLost(responseObject) {
  if (responseObject.errorCode !== 0) {
    console.log("onConnectionLost:"+responseObject.errorMessage);
  }
}

// メッセージを受信した場合の処理
function onMessageArrived(message) {
  // 新たに受信したセンサーデータをパースしてコンソール出力してみる。
  var jsonMessage = JSON.parse(message.payloadString);
  var temperature = jsonMessage.senseHat.temperature;
  var pressure = jsonMessage.senseHat.pressure;
  var humidity = jsonMessage.senseHat.humidity;
  var time = jsonMessage.gps.time;
  console.log("time:" + time + " temp:" + temperature + " pressure:" + pressure + " humidity:" + humidity);

  // グラフを再描画する
  myApps.update(message.payloadString);
}
```

以上で、動的にグラフが更新されるようになります。
初回表示はMarkLogicからデータを読み込み、更新分は動的にグラフに反映します。更新分はMarkLogicにもパブリッシュしているため、ブラウザをリロードした場合は、最新のデータをMarkLogicから読み込みます。

# 次回予告
いよいよ明日がアドベントカレンダー最終日です。今回はセンサー情報のビジュアライズを行ってみました。
最終日の明日は、これまでご紹介してきた技術を全て使って、総まとめのような内容にしたいと思います。
引き続き明日もよろしくお願い致します。