Electron / Leaflet / GTFS-RealTimeでシミュレーションデスクトップアプリを作ろう

# 概要
学校の研究でバスのシミュレーションソフトを作ることとなったのでその作り方をまとめようと思います。

ソースコードは[こちら](https://github.com/shunmame/bus_simulation_electron)

2023/08/26追記
記事にはしていませんが、静的GTFSをアップロードできるようにしています。
詳しくはソースコードを参照してください。


# Electronについて
こちらの記事が分かりやすかったです
[ようこそ！Electron入門](https://qiita.com/umamichi/items/6ce4f46c1458e89c4cfc)

# 開発環境
|ライブラリ等           |バージョン|
|:---------------------|:---------|
|node.js               |14.16.1   |
|npm                   |6.14.12   |
|Electron              |12.0.5    |
|leaflet               |1.7.1     |
|gtfs-realtime-bindings|0.0.6     |
|sqlite3               |5.0.2     |
|request               |2.88.2    |
|Bootstrap             |4.3.1     |

# Electronの導入
## プロジェクトの作成
まずはアプリ用のフォルダを作成して、その中で以下を実行してください。

```
npm init -y
```

## インストール
必要なライブラリをインストールします

```
npm install electron --save
```

## アプリの作成
まず`src`フォルダを作成します。
srcフォルダの中に以下のファイルを作成してください。

- index.html
- main.js
- style.css
- package.json

中身はこのようにしてください。

```index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>MY ELECTRON</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <p>Hello World</p>
</body>
</html>
```

```main.js
const { app, BrowserWindow } = require('electron')

const createWindow = () => {
    const mainWindow = new BrowserWindow(
        {
            width: 2000,
            height: 1000
        }
    )

    // 開発ツールを有効化
    mainWindow.webContents.openDevTools({ mode: "detach" });

    mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
    createWindow()
    app.on(
        'activate',
        () => !BrowserWindow.getAllWindows().length && createWindow()
    )
})

app.on(
    'window-all-closed',
    () => process.platform !== 'darwin' && app.quit()
)
```

```package.json
{
    "main": "main.js"
}
```

## 実行してみよう
ここまで入力したら以下を実行してみましょう

```
npx electron src
```
実行するとこのような画面が現れると思います。
![img1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684236/ddec1168-f4cb-124d-d746-9d0eb7a95d30.png)

# Leafletで地図を表示しよう
## インストール
必要なライブラリをインストールします。

```
npm install leaflet --save
```

## アプリの作成
`src`フォルダ内に`preload.js`を作成します。
中身を以下のようにします。

```preload.js
window.onload = () => {
    global.L = require('leaflet');

    global.map = L.map('mapid').setView([34.6650016, 133.9316551], 14);

    L.tileLayer(
        'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
    ).addTo(map)
}
```

今作成した`preload.js`を読み込むため`main.js`のように変更します。

```main.js
const createWindow = () => {
    const mainWindow = new BrowserWindow(
        {
            width: 2000,
            height: 1000,
            // ↓追加 ///////////////////////
            webPreferences: {
                preload: __dirname + '/preload.js'
            }
            ///////////////////////////////
        }
    )

    // 開発ツールを有効化
    mainWindow.webContents.openDevTools({ mode: "detach" });

    mainWindow.loadFile('index.html')
}
```

マップを表示する箇所に以下を追加します。

```index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>MY ELECTRON</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <!----- ↓追加 -------->
    <div id="mapid"></div>
    <!------------------->
</body>


</html>
```

`style.css`に以下を追加してください。
htmlとbodyと#mapidをheight: 100%にしていますが、このようにしないと地図が表示されません。
(htmlとbodyにheight: 100%が必要かは分かりません)

```style.css
html, body, #mapid {
    height: 100%;
}

body {
    margin: 0;
}
```

## 実行してみよう
ここまで入力したら以下を実行してみましょう

```
npx electron src
```
実行すると私の環境ではこのようになってしまいました。
![img2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684236/f4b893dd-9d08-c6cc-2241-3294dee67e7b.png)

なので`index.html`の`head`に以下を追加しました。

```index.html
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
    integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
    crossorigin="" />
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
    integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
    crossorigin=""></script>
```
追加してから、再度実行してください。
するとこのように表示されました。
![img3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684236/6d79fd99-2482-3005-3a8b-f5afaec9e23b.png)

# GTFS-RealTimeの位置情報を表示する
# GTFS-RealTimeとは
[こちら](https://developers.google.com/transit/gtfs-realtime?hl=ja)に書いてあります。
>GTFS リアルタイムは、公共交通機関が運行車両に関するリアルタイムの最新情報をデベロッパーに提供できるようにするためのフィードの仕様です。

## インストール
必要なライブラリをインストールします。

```
npm install gtfs-realtime-bindings --save
npm install request --save
```

## アプリの作成
まずGTFS-RealTimeのデータを取得します。
GTFS-RealTimeのデータの取り方は[こちら](https://developers.google.com/transit/gtfs-realtime/examples/nodejs-sample?hl=ja)に書いてあります。
今回は[宇野バス](http://www3.unobus.co.jp/opendata/)様のリアルタイムデータを使わせていただきます。
`preload.js`を以下のように変更します。

```preload.js
// ↓追加
var GtfsRealtimeBindings = require('gtfs-realtime-bindings');
var request = require('request');
var realtime_markers = []

window.onload = () => {
    global.L = require('leaflet');

    global.map = L.map('mapid').setView([34.6650016, 133.9316551], 14);

    L.tileLayer(
        'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
    ).addTo(map)

    show_gtfs_realtime()
}

// ↓追加
function show_gtfs_realtime() {
    const blueMarker = { icon: L.divIcon({ className: 'blue marker', iconSize: [10, 10] }) }
    var requestSettings = {
        method: 'GET',
        url: 'http://www3.unobus.co.jp/GTFS/GTFS_RT-VP.bin',
        encoding: null
    };
    request(requestSettings, function (error, response, body) {
        if (!error && response.statusCode == 200) {
            var feed = GtfsRealtimeBindings.transit_realtime.FeedMessage.decode(body);
            feed.entity.forEach(function (entity) {
                if (entity.vehicle) {
                    realtime_marker = L.marker([entity.vehicle.position.latitude, entity.vehicle.position.longitude], blueMarker).addTo(map).bindPopup("BUS")
                    realtime_markers.push(realtime_marker)
                }
            });
        }
    });
}
```

`style.css`にも以下を追加します

```style.css
.marker {
    text-align      : center;
    color           : white;
    font-size       : 16;
    border-radius   : 8px;
    box-shadow      : 8px 8px 8px rgba( 0, 0, 0, 0.4 );
}

.blue {
    background      : blue;
}
```

## 実行してみよう
ここまで入力したら以下を実行してみましょう

```
npx electron src
```
青い丸が表示されたと思います。
これはバスの現在地を表しています
![img4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684236/c8c30104-2659-6dff-392c-02c420cd6d1d.png)

# GTFS-RealTime データ更新
[宇野バス](http://www3.unobus.co.jp/opendata/)様のデータは約15秒ごとに更新されるようなのでデータ更新が行えるようにします
ipc通信というものを使用します

## ipc通信について
こちらに詳しく書かれてあります
[Electron（v.12.0.0 現在）の IPC 通信入門 - よりセキュアな方法への変遷](https://qiita.com/hibara/items/c59fb6924610fc22a9db)

## アプリの作成
`src`フォルダ内に`bus_simulation.js`を追加して、`index.html`で読み込めるようにしてください。
bodyの一番下に書いてください。

`bus_simulation.js`の中身は以下のようにしてください。

```bus_simulation.js
window.onload = function () {
    setInterval(update_gtfs_realtime, 15000);
}

function update_gtfs_realtime() {
    window.api.send("update_marker");
    console.log("update")
}
```

`preload.js`に以下のコードを追加してください。

```preload.js
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld(
    "api", {
    //rendererからの送信用//
    send: (channel, data) => {
        if (channel == "update_marker") {
            update_gtfs_realtime()
        }
        else {
            ipcRenderer.send(channel, data);
        }
    },
    //rendererでの受信用, funcはコールバック関数//
    on: (channel, func) => {
        ipcRenderer.on(channel, (event, ...args) => func(...args));
    }
});

function update_gtfs_realtime() {
    for (let i = 0; i < realtime_markers.length; i++) {
        map.removeLayer(realtime_markers[i])
    }
    realtime_markers = []
    show_gtfs_realtime()
}
```

## 実行してみよう
ここまで入力したら以下を実行してみましょう

```
npx electron src
```
15秒おきに青ピンが動いているのが確認できましたか？
Consoleにも`update`と見えていれば成功だと思います。
確認作業は昼間行うと確認しやすいです。
![img5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684236/6fa8fed0-8e0d-3ba1-8915-7990a027bca6.png)



# さいごに
今回記載したやり方は私なりのやり方です。もしほかに良い方法があれば教えていただければ幸いです。
こちらはまだ開発中なのでこれからも追記していきたいと思います。
以上読んでいただきありがとうございます。

# 参考文献
[ようこそ！Electron入門](https://qiita.com/umamichi/items/6ce4f46c1458e89c4cfc)
[日頃お世話になっているElectronのアプリ開発に入門してみる](https://qiita.com/y-tsutsu/items/179717ecbdcc27509e5a)
[Electron で地図を表示する無料で最短の道](https://qiita.com/Satachito/items/7f2a063be7f945dd426a)
[JavaScript / Node.js サンプル  |  リアルタイム乗換案内データ  |  Google Developers](https://developers.google.com/transit/gtfs-realtime/examples/nodejs-sample?hl=ja)
[Electron（v.12.0.0 現在）の IPC 通信入門 - よりセキュアな方法への変遷](https://qiita.com/hibara/items/c59fb6924610fc22a9db)