リアルタイム・ジオインフォマティクス（HERE Maps API と 時系列データCLOUDSHIP API を使って、地図上にリアルタイムデータ表示）

# はじめに

こんにちは、株式会社PROMPT-Xの滝沢です。
この度、弊社の時系列データ可視化システム[RealBoard](https://products.promptk.jp/realboard)を地図機能に対応させました。

HERE社の高精細な地図上でリアルタイムデータと時系列データを融合させることで、「いつ」「どこで」「何が」起きているのかを一目で把握できるツールとなりました。

この記事では、どのような実装を行ったのか、かいつまんで紹介させていただきます。
[CLOUDSHIP](https://www.prompt-x.jp/_files/ugd/966732_626b505de59d4f92a909a75eed53b851.pdf
)と[HERE Maps API for JavaScript](https://www.here.com/docs/bundle/maps-api-for-javascript-developer-guide/page/README.html) を用いた開発の一助となれば幸いです。
![Animation5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283248/523e0493-0c39-2ea5-3f2e-ac2506972218.gif)

## 技術スタック
・地図SDK [HERE Maps API for JavaScript](https://www.here.com/docs/bundle/maps-api-for-javascript-developer-guide/page/README.html)
・WEBサーバ Node.js
・国産時系列データベース [CLOUDSHIP](https://www.prompt-x.jp/_files/ugd/966732_626b505de59d4f92a909a75eed53b851.pdf
)
　（※ CLOUDSHIPは、時系列データベースを含むIoTサービスの構築に必要な、各種ソフトウェアのパッケージ群です。数値、文字列だけでなく画像や音声データも混在して格納。）

# やりたいこと

・画面の定義情報をJsonデータで持つ。
　　地図の中心位置や、表示したい値がどれかといった情報。
・データをリアルタイムに表示更新する。
・現在値、合計値、最大値を出力できる。
・タイムスライダーで過去のデータを閲覧できる。

# I.起動シーケンス

## I-1.画面の定義情報

地図SDKを利用するにあたって必要なもの、
地図の中心座標、地図のズームレベル、地図上のアイコンの座標が必要なります。
そしてさらに、地図上のアイコンの詳細吹き出しに何を表示するのかも必要となりました。これらのデータ構造をJson形式で考えます。
これをデータベースに持つことで、任意の画面表示を行うことができるでしょう。

Javascript
```js
{
  mapCenter: { lat: 35.62458084878175, lng: 139.7224601012724 },
  mapZoom: 18,
  mapMarkers: [
    {
      title: "24時間集計",
      iconLatLon: { lat: 35.624522652633324, lng: 139.72352016651556 },
      dataPoints: [
        {
          _comment: "トラックの24時間の合計数",
          DisplayName: "トラック（24h合計）",
          UnitName: "台",
          Digits: 0,
          Color: "#000B58",
          Icon: "ph-truck-trailer",
          PointID: "prompt-x.jp/promotion/network-camera/0001/truck",
          Select: "sum",
          GroupBy: 24 * 60 * 60,
          Type: "text",
        },
        {
          _comment: "【画像】道路（五反田）",
          DisplayName: "歩行者（24h合計）",
          PointID: "prompt-x.jp/promotion/network-camera/0001/annotated_image",
          Select: "max",
          Type: "pic",
        },
      ],
    },
    {
      title: "現場温度",
      iconLatLon: { lat: 35.62395731030394, lng: 139.72414004931187 },
      dataPoints: [
        {
          _comment: "現場温度の現在値",
          DisplayName: "現場温度",
          UnitName: "℃",
          Digits: 2,
          Color: "#882255",
          Icon: "ph-thermometer-hot",
          PointID: "prompt-x.jp/promotion/uecs/device0001/InAirTemp",
          Select: "max",
          Type: "text",
        },
      ],
    },
  ]
};
```

## I-2.データ表示エリアをマップへ配置

上記で定義したデータをもとに、HTML要素を動的に生成します。
今回は、HTMLTemplateという機能を使い、同じ要素をコピーして色やテキストを変化させて使います。

その際に、「要素を特定できるID」を設定しておきます。

HTML
```html
    <!-- 地図アイコンの情報パネル -->
    <template id="template-pnl-mapicon-info">
      <div class="csapi-data-panel">
        <div class="flexbox">
          <div class="pnl-csapi-data-left">
            <i class="ph-fill"></i>
          </div>
          <div class="pnl-csapi-data-right">
            <div class="csapi-data-timestamp">―</div>
            <div class="flexbox">
              <div class="csapi-data-title" style="width: 10rem">―</div>
              <div class="csapi-data-value" style="width: 3rem">―</div>
              <div class="csapi-data-unit" style="width: 1rem">―</div>
            </div>
          </div>
        </div>
      </div>
    </template>
```
Javascript
```js
  for (let i = 0; i < mapPanelConf.mapMarkers.length; i++) {
    const markerConf = mapPanelConf.mapMarkers[i];
    // マーカー内ポップアップ用のHTML作成
    const infoPanel = document.createElement("div");
    
    for (let j = 0; j < markerConf.dataPoints.length; j++) {
      const dataPoint = markerConf.dataPoints[j];  
      // 作成するHTMLの元となるtemplate要素を取得（この時は数値は未存在）
      const template = document.getElementById("template-pnl-mapicon-info");
      oneItem.innerHTML = template.innerHTML;
      const htmlId = `${dataPoint.PointID}-${dataPoint.Select}`;
      oneItem.id = htmlId;
      oneItem.style.opacity = 0.85;
      oneItem.style.backgroundColor = dataPoint.Color;
      infoPanel.append(oneItem);
    }
    
    const marker = new H.map.Marker(markerConf.iconLatLon);
    marker.setData(html);
    marker.addEventListener("tap", async (evt) => {
        //・・・
      }, true);
  }
```


# II.リアルタイムプロセス

## II-1.データ取得

1分毎にCLOUDSHIPを確認し、CLOUDSHIPよりデータを取得します。
複数のクエリをまとめて1度にリクエストし、まとめてレスポンスを受け取ることができます。


__クエリ例__
```
{
    "Query": [
    	// トラック台数（1日の合計値）
        {
            "PointID": "prompt-x.jp/network-camera/0003/truck",,
            "From": "2024-12-01T00:00:00.000+09:00",
            "To": "2024-12-02T00:00:00.000+09:00",
            "Select": "sum",
            "GroupBy": 24 * 60 * 60,
        },
        // 温度（最新値）
        {
            "PointID": "prompt-x.jp/airQuality/0006/Temperature",
            "From": "1970-01-01T00:00:00.000+00:00",
            "To": "2024-12-01T00:00:00.000+09:00",
            "Select":"max"
        },
        // 音量（5分間の最高値）
        {
            "PointID": "prompt-x.jp/airQuality/0006/Volume",
            "From": "2024-12-02T00:00:00.000+09:00",
            "To": "2024-12-02T00:05:00.000+09:00",
            "Select":"vmax"
        },
    ]
}
```
Javascript
```js
/** CloudShipAPIへのリクエスト */
export async function getDataCSAPI(config, to) {
  const queryObj = createQuery(config, to);

  const res = await fetch("/CSAPI/json", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(queryObj),
  });
  const result = await res.json();
  return result;
}

/** CloudShipAPIのクエリ作成 */
function createQuery(searchConf, endDate) {
  const query = {};
  query.Query = [];

  // 検索期間
  const startDate = utilDate.addDays(endDate, -1);
  const from = utilDate.toDateString(new Date(startDate));
  const to = utilDate.toDateString(new Date(endDate));

  for (let i = 0; i < searchConf.dataPoints.length; i++) {
    const pointData = searchConf.dataPoints[i];

    const oneQuery = {};
    oneQuery.PointID = pointData.PointID;
    oneQuery.From = from;
    oneQuery.To = to;
    oneQuery.Select = pointData.Select;
    oneQuery.GroupBy = pointData.GroupBy;

    query.Query.push(oneQuery);
  }
  return query;
}
```

CLOUDSHIPからのレスポンスを読み込み、
先ほど設定した「要素を特定できるID」に対して値更新を掛けます。
これで画面の表示がリアルタイムに変わるようになりました。

Javascript
```js
  // クエリ結果読み込み
  for (let i = 0; i < data.Response.length; i++) {
      const response = data.Response[i];
      const pointId = response.Query.PointID;
      const select = response.Query.Select;
      
      // 値書き換え
      const infoPanel = document.getElementById(`${pointId}-${select}`);
      const valueText = infoPanel.querySelector(".csapi-data-value");
      valueText.innerHTML = value;
  }
```



# III.過去データを閲覧したい

## III-1.日付入力

画面下部にタイムスライダーを用意します。
その値を使って、データ取得を行うようにします。
データ取得は、定期処理とは別に追加して、タイムスライダーのValueが更新された時にもイベント実行することで即座に画面データへ反映します。


HTML
```html
    <section class="section-user-control">
      <div class="pnl-user-control">
        <input type="range" id="date-slider" class="time-slider" />
        <output for="slider" id="date-slider-value">ー</output>
      </div>
    </section>
```
Javascript
```js
  const slider = document.getElementById("date-slider");

  const refreshValues = async () => {
    // 選択日付
    const selectDate = utilDate.floorHours(slider.value * 1000);
    
    for (let i = 0; i < mapPanelConf.mapMarkers.length; i++) {
      const markerConf = mapPanelConf.mapMarkers[i];
      // 地図オブジェクト更新
      refreshValueMapiconInfo(markerConf, new Date(selectDate));
    }
  };
  
  // 値変更イベントを登録
  slider.addEventListener("change", refreshValues);
```

# 完成
![Animation5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283248/523e0493-0c39-2ea5-3f2e-ac2506972218.gif)

このようなアプリケーションとなりました。
全てのソースを載せているわけではないのでそのまま実行はできませんが、
[CLOUDSHIP](https://www.prompt-x.jp/_files/ugd/966732_626b505de59d4f92a909a75eed53b851.pdf
)と[HERE Maps API for JavaScript](https://www.here.com/docs/bundle/maps-api-for-javascript-developer-guide/page/README.html) を用いた開発の一助となれば幸いです。

都市計画や環境モニタリング、防災対策などへ活用できそうでしょうか。
データを単なる数字の羅列ではなく、**場所を踏まえた生きた情報**として直感的な理解と迅速な意思決定に活用できると嬉しいですね。