LINE ボットと LIFF で 第 3 回 東京公共交通オープンデータチャレンジで審査員特別賞をもらった話

 第 3 回 東京公共交通オープンデータチャレンジの授賞式が 2020 年 1 月 30 日に開催され、審査員特別賞をいただきました。

[第 3 回 東京公共交通オープンデータチャレンジ 結果発表](https://tokyochallenge.odpt.org/2019/award/index.html)

この記事では、今回応募した [imacoco ～I'm here～](https://tokyochallenge.odpt.org/2019/award/03.html#j) について技術的な説明と課題について説明します。

# 機能

LINE ボットと LIFF を使ったアプリで、ワンタップで以下のことが分かります。

- 山手線に乗っている場合、どの駅を出て、どの駅へ向かっているか
- それ以外の場合は自分の場所を地図で表示
![](https://tokyochallenge.odpt.org/2019/award/img/10.jpg)

ポイントは「ワンタップ」で出来ることであり、わざわざ位置情報を送る必要はありません。(あくまで LINE ボットにたどり着いてからワンタップ)

# 実装期間

相方がアイデアをまとめてくれたおかげで、実装は 2 時間くらいでした。尚、簡単に実装したかったため TypeScript を使わないという無謀をしています。

# なぜ LINE ボットにしたか

いつも通りの理由ですが、一応書いておきます。

- アプリストアに公開する必要がない
- モバイルで見る場合ブラウザより見やすい
- ユーザープロファイルが取れのでパーソナライズしやすい
- LIFF によりブラウザで出来ることはほぼできる
- LINE が好き 

では技術的な話をしていきましょう。

# 位置と進行方向の取得

今回は以下の方法で現在の位置と進行方向、および乗車中の電車情報を取得しました。

- 1 秒の間隔で 2 回位置情報を取得

```javascript
async function getLocation() {
    // Get two locations in 2 seconds
    if (navigator.geolocation) {
        m.innerHTML = language == "ja-JP" ?
            "現在位置を確認中..." :
            "Checking your location....";
        coords1 = coords2 = l.innerHTML = null;
        navigator.geolocation.getCurrentPosition((position) => { coords1 = position.coords; });
        await sleep(1000);
        navigator.geolocation.getCurrentPosition((position) => { coords2 = position.coords; });
        ...
}
```
- それぞれの位置情報から、山の手線の最寄りの駅と、駅の位置情報から進行方向を確認

```javascript
async function getStations(coords1, coords2) {
    // send location to server
    const coord = {
        latitude: coords1.latitude,
        longitude: coords1.longitude
    };

    const options = {
        method: 'POST',
        body: JSON.stringify(coord),
        headers: {
            'Content-Type': 'application/json'
        }
    }

    let stations = JSON.parse(await (await fetch('/get-stations', options)).json());
    if (stations.length < 2) {
        return { fromStation: null, toStation: null };
    }
    let fromStation;// = stations.sort((a, b) => (a.distance > b.distance ? 1 : -1))[0];
    let toStation;
    stations.some((station) => {
        station.distance2 = calcDistance({ longitude: coords2.longitude, latitude: coords2.latitude }, { longitude: station.long, latitude: station.lat }) - station.distance;
    });

    stations.some((station) => {
        if (stations[0].distance2 > 0 && station.distance2 < 0) {
            toStation = station;
            fromStation = stations[stations.indexOf(station) - 1];
            return true;
        }
        else if (stations[0].distance2 < 0 && station.distance2 > 0) {
            fromStation = station;
            toStation = stations[stations.indexOf(station) - 1];
            return true;
        }       
    });
    return { fromStation: fromStation, toStation: toStation };
}
```

- 同じ場所を走行している電車情報を取得

```javascript:クライアント側
async function getTrain(stations) {

    if (stations.fromStation == null || stations.toStation == null) {
        return {};
    }
    const options = {
        method: 'POST',
        body: JSON.stringify(stations),
        headers: {
            'Content-Type': 'application/json'
        }
    }

    let train = JSON.parse(await (await fetch('/get-train', options)).json());
    return train;
}
```
```javascript:サーバー側
app.post('/get-train', async function (req, res) {
    var fromStation = req.body.fromStation;
    let toStation = req.body.toStation;

    var response = await request.get(`${apiurl}/odpt:Train?acl:consumerKey=${key}&odpt:railway=${railway}&odpt:fromStation=${fromStation["sameAs"]}&odpt:toStation=${toStation["sameAs"]}`);
    let train = JSON.parse(response)[0];
    if (train === undefined) {
        res.json("{}");
    }
    else {
        let result = {
            trainNumber: train["odpt:trainNumber"],
            direction: train["odpt:railDirection"],
            delay: train["odpt:delay"]
        }
        res.json(JSON.stringify(result));
    }
});
```

結構アナログにデータ取得と計算しているので、もっと格好いい方法知りたいです。

# LIFF の役目

LIFF を使ったメインの目的は位置情報を取得することです。位置情報をチャットで送ることはできますが、ワンタップでは実現できないことと、2 つの位置情報が必要であったこと、および位置情報の送り方以外と知られていない問題があったため LIFF が最適解となりました。

- LIFF 起動
- 位置情報を取得してもろもろ計算
- メッセージを送付して LIFF を自動で閉じる

```javascript
if (liff.isInClient()) {
  liff.sendMessages([{
    "type": "location",
    "title": message,
    "latitude": coords2.latitude,
    "longitude": coords2.longitude
  }]);
  liff.closeWindow();
}
```
結果ユーザーは LIFF を目にしますが、操作はしません。メッセージが出て消えるだけです。動作は[ビデオ](https://www.youtube.com/watch?v=qfe6jnj-V1M&feature=emb_logo)をご覧ください。

- ユーザーの使用言語の特定

OS の言語設定に依存しますが、日本語と英語対応を行いました。Messaging API でユーザープロファイルが簡単に取得できるのは助かります。

# LINE ボットの役目

JavaScript だけであれば Web ページでも同じですが、LINE を使った理由以下の通りです。

- 地図の機能が手軽に使える
    - チャット画面に簡単な地図表示
    - タップすると本格的な地図アプリが起動する
- 情報を他の人とも簡単に共有しやすい
- ボット側から情報を送ることも容易
- グルチャの場合、誰かが「いまどこ」と発言すると、ボタンが送られてきてワンタップで知らせることができる

# リッチメニューの役目

今回は「ワンタップ」ですべてを終わらせることをテーマにしていたため、ワンタップで LIFF が起動して、自動的にすべてが終了する仕組みとして以下の機能を使いました。

- リッチメニューから直接 LIFF を起動
- ボタンテンプレートのクリックで LIFF を起動

いずれも LINE の UX の機能に助けられています。

# 課題

開発してテストしている中で以下の課題がありますが、まだ解決策はありません。

- GPS の精度が iOS の場合悪い/遅い
- LINE ボットを開くのが面倒
- 東京は電車多すぎて山手線に限定してしまった。GPS 精度とタイムラグの限界
- 現在位置を送ると乗っている電車を返す API が公開されていないため、計算が大変

今回は山手線で試しましたが、逆に都内を少し離れる電車で、駅の間隔長いものやバスの方が効果が高い気がします。

# まとめ

LINE ボットだけでは実現できないことも、LIFF を組み合わせると結構色々なことが解決します。LIFF は v2 も出ましたし今後ミニアプリもあるので、目が離せない存在になっていきそうです。

尚、第4回の東京公共交通オープンデータチャレンジが現在募集中です。是非アイデアを考えて参加してみてください。API も色々な情報が取得できるので面白いですよ。

[東京公共交通オープンデータチャレンジ](https://tokyochallenge.odpt.org/)