【ちょっとお遊び】旅客機のフライト情報をZabbixで監視(ハイジャック検知も!?) その② -世界地図に飛行状況をマッピング-

[昨日の記事](https://qiita.com/ike_dai/items/743136a87c58ed582126)ではflightrader24から旅客機のフライト情報をZabbixで監視をすることに成功しました。
今日はさらに発展で、Zabbixの標準のマップ機能を使って、フライト位置を動的にマップ上に表現したいと思います。

# 試したこと

* 世界地図を表示したZabbixマップを作成
* マップ上にある旅客機のフライト位置を緯度・経度を元にアイコンで表現
* 1分間隔で監視した位置情報をもとにマップ上のアイコンを動的に移動(Zabbix APIを使って都度更新)
* 飛行機の飛行方向に応じてアイコンの自動マッピング機能を使ってアイコンの向きを自動変更

昨日に続いて、結構Zabbixの標準機能のいろいろな要素を活用しているので、これをちょっと試してみるだけでもZabbixの機能に詳しくなれると思います。

# 世界地図を背景イメージとして登録

まず、Zabbixのマップの背景に設定する世界地図画像を準備します。
緯度・経度が等間隔で配置される画像が必要なので、世界地図の表記の中でも、***「[正距円筒図法](https://ja.wikipedia.org/wiki/%E6%AD%A3%E8%B7%9D%E5%86%86%E7%AD%92%E5%9B%B3%E6%B3%95)」***で描かれたものを入手します。

今回はこちらの地図画像をお借りしました。

* https://en.wikipedia.org/wiki/File:BlankMap-World6-Equirectangular.svg

ダウンロードした画像の余白枠を削除し、少し拡大した画像を準備します。
今回は幅:2000px、縦:1018pxです。

Zabbixの[管理]→[一般設定]→[イメージ]から背景画像を登録します。

1つ事前に確認が必要な点としては、画像の左上端のポジションが緯度・経度がいくらの地点であるかということです。
後の座標位置の計算に必要なので、だいたいのポジションを確認しておきます。今回使った画像としては、北緯90°、西経172°ぐらいの位置でした。

# アイコンの自動マッピングの定義を実施

次にマップ上に配置する飛行機画像を登録し、アイコンの自動マッピングの定義を実施します。
今回、飛行機画像は45°ずつ向きが異なる8種類の画像を準備しました。

![iconfinder_icon-plane_211875.png](https://qiita-image-store.s3.amazonaws.com/0/45851/8feff351-7572-203b-61b3-23df1c60826d.png)![airplane_2.png](https://qiita-image-store.s3.amazonaws.com/0/45851/4f6bd73a-5fea-62e8-2a83-7c5cb93117f6.png)![airplane_3.png](https://qiita-image-store.s3.amazonaws.com/0/45851/49665760-b0c6-bea9-e671-f3306eb73c30.png)![airplane_4.png](https://qiita-image-store.s3.amazonaws.com/0/45851/79b9a8ad-1edb-7840-c841-dafa1bc2181b.png)![airplane_5.png](https://qiita-image-store.s3.amazonaws.com/0/45851/13f739f5-276a-4e21-db44-44de6325e050.png)![airplane_6.png](https://qiita-image-store.s3.amazonaws.com/0/45851/d1b04410-24a8-a306-324e-3afe30157c2f.png)![airplane_7.png](https://qiita-image-store.s3.amazonaws.com/0/45851/f3e4d154-bfb5-d5aa-6817-ece63ffe194e.png)![airplane_8.png](https://qiita-image-store.s3.amazonaws.com/0/45851/1239d40e-41b7-8740-4506-a70f30d21f2e.png)

この画像を先程の背景と同じく、Zabbixの[管理]→[一般設定]→[イメージ]からアイコン登録します。
次に、[管理]→[一般設定]→[アイコンのマッピング]からホストインベントリの設定内容に合わせて適切なアイコンが自動設定されるよう登録します。

![icon_mapping.png](https://qiita-image-store.s3.amazonaws.com/0/45851/9cb82aec-fbb6-c295-1bee-40edf4d71c3e.png)

ここでポイントは、[前回の記事](https://qiita.com/ike_dai/items/743136a87c58ed582126)で記載した飛行方向の情報を登録するホストインベントリの項目(「場所」)に対して、その値と一致する画像を設定することです。
場所が「1」-22.5°〜22.5°の間なら北向きの画像になるといった具合です。

# マップを作成

それではマップを作成します。
マップは以下のように、背景画像にあわせる幅・高さで作成し、背景イメージとして最初に登録した地図画像を、アイコンの自動マッピングとして、上で設定した飛行機画像のマッピング設定を選択します。

![map_create.png](https://qiita-image-store.s3.amazonaws.com/0/45851/4fde9eb8-9cde-eb63-cf58-f5b07a5ed405.png)

# マップ上にアイコンを配置

次に、マップ上にアイコンを配置します。
座標は一旦適当な位置で構いません。タイプをホストに設定し、[前回の記事](https://qiita.com/ike_dai/items/743136a87c58ed582126)の設定で登録したホストとひもづけておきます。ポイントは「アイコンの自動選択」を有効にしておく点です。

![icon_create.png](https://qiita-image-store.s3.amazonaws.com/0/45851/574c5634-32db-c12f-1396-6ad666d904a3.png)

マップを更新して完了です。

# Zabbix APIで緯度経度状態にあわせてマップアイコン位置を更新する処理を実装

では最後に、[昨日の記事](https://qiita.com/ike_dai/items/743136a87c58ed582126)で作成したPythonスクリプトを少し修正し、監視処理のタイミングで得られた緯度経度情報を元にマップのアイコン座標をアップデートする処理を実装します。

PythonでZabbix APIを呼び出すことになるので、zabbix-apiライブラリを事前にZabbixに導入しておきます。

```
$ pip install zabbix-api
```

あとは、以下スクリプトに差し替えます。

```python
#!/usr/bin/python36

import urllib.request
import json
import sys
from zabbix_api import ZabbixAPI, ZabbixAPISubClass, ZabbixAPIException


class Flights:
    def __init__(self, flight_number):
        self.flight_number = flight_number
        self.latitude = 0.0
        self.longitude = 0.0
        self.direction = 0
        self.squawk = "0000"

    def get_info(self):
        url = "http://data-live.flightradar24.com/zones/fcgi/feed.js?adsb=1&mlat=1&faa=1&flarm=1&estimated=1&air=1&gnd=1&vehicles=1&gliders=1&array=1"
        headers = {"User-Agent": "curl/7.54.0"}
        response_body = {}
        request = urllib.request.Request(url=url, headers=headers)
        try:
            with urllib.request.urlopen(request) as response:
               response_body = json.loads(response.read().decode("utf-8"))
        except Exception as e:
            print(e)

        for flight_info in response_body['aircraft']:
            if flight_info[17] == self.flight_number:
                self.latitude = flight_info[2]
                self.longitude = flight_info[3]
                self.squawk = flight_info[7]
                direction = flight_info[4]
                if (direction >= 0 and direction < 23) or (direction >= 338):
                    self.direction = 1
                elif (direction >= 23 and direction < 68):
                    self.direction = 2
                elif (direction >= 68 and direction < 113):
                    self.direction = 3
                elif (direction >= 113 and direction < 158):
                    self.direction = 4
                elif (direction >= 158 and direction < 203):
                    self.direction = 5
                elif (direction >= 203 and direction < 248):
                    self.direction = 6
                elif (direction >= 248 and direction < 293):
                    self.direction = 7
                elif (direction >= 293 and direction < 338):
                    self.direction = 8

class FlightMap:
    def __init__(self, url="http://localhost/zabbix", username="Admin", password="zabbix"):
        self.url = url
        self.username = username
        self.password = password
        self.api = ZabbixAPI(url)
        self.mapid = "14"
        self.map_width = 2000
        self.map_height = 1018
        self.map_top = 0
        self.map_left = -172.887434

        try:
            self.api.login(username, password)
        except ZabbixAPIException as e:
            print(e)

    def _calc_position(self, latitude, longitude):
        position = {
            "x": int((self.map_width/360)*(90-self.map_top-latitude) - 10),
            "y": int((self.map_height/180)*(0-self.map_left+longitude) - 10)
        }
        return position
    def _get_map_info(self):
        result = self.api.map.get({"sysmapids": self.mapid, "selectSelements": "extend", "output": "extend"})
        self.selementid = result[0]["selements"][0]["selementid"]
        self.elements = result[0]["selements"][0]["elements"]
        self.elementtype = result[0]["selements"][0]["elementtype"]
        self.iconid_off = result[0]["selements"][0]["iconid_off"]
    def position_update(self, latitude, longitude):
        self._get_map_info()
        position = self._calc_position(latitude, longitude)
        self.api.map.update({
            "sysmapid": self.mapid,
            "selements": [{
                "selementid": self.selementid,
                "elements": self.elements,
                "elementtype": self.elementtype,
                "iconid_off": self.iconid_off,
                "x": position["x"],
                "y": position["y"],
            }]
        })

if __name__ == "__main__":
    args = sys.argv
    flight_number = ""
    if len(args) > 1:
        flight_number = sys.argv[1]
    else:
        flight_number = input("Please input flight number: ")
    flight = Flights(flight_number)
    flight.get_info()
    response = {
            "flignt_number": flight_number,
            "latitude": flight.latitude,
            "longitude": flight.longitude,
            "direction": flight.direction,
            "squawk": flight.squawk
    }
    print(json.dumps(response))
    flight_map = FlightMap("http://localhost/zabbix", "Admin", "zabbix")
    flight_map.position_update(flight.latitude, flight.longitude)
```

Zabbix APIのmap.updateを利用し、先程作成したマップ(上記例ではmapid 14)の中に配置された1つのアイコンのx座標、y座標を更新しています。
画像の幅・高さから1ピクセルあたりの移動距離が何度であるかを算出し、その値に沿って座標ポイントを決めています。 
計算式の最後のx軸、y軸ともに-10pxしているのは、飛行機アイコンが20x20の画像サイズであるため、その中心にポイントがマッピングされるように微調整しています。

# 監視結果に合わせて移動する状況を確認

このようにスクリプトを差し替えると、1分に1回監視結果が更新されるたびにマップ上のアイコンのポジションも微妙に移動していきます。
また、飛行方向が変化すると、ホストインベントリに登録されている「場所」情報が変わり、その変動に応じて画像も変更になります。

![flight_map1.png](https://qiita-image-store.s3.amazonaws.com/0/45851/5064c074-0f2d-8434-adbd-11549a1fc20e.png)

![flight_map2.png](https://qiita-image-store.s3.amazonaws.com/0/45851/e991e779-df2e-07e8-0496-6fb80f18cf43.png)

# まとめ

こんなマップの使い方はしないかとは思いますが、このようにマップはいろいろと活用できるので、例えば、ラックとその位置情報をもとに自動的に配置を位置を定義したりといったこともできると思います。