「稚内より近いかどうか」を調べるツールを作ってみた（試作）

# 1．はじめに
**稚内より近ければ近場**
という言葉があるが、実際ある場所からどこかに行こうとしたときに「そこは稚内に行くよりも近いのか」ということを調べるツールをLINEのbotで作りたくなって実際に作ってみた。

本記事の元ネタとなる偉大な名言を残してくだった方の元ツイートは[こちら](https://twitter.com/SAIONJI_com/status/1374570249664663554)

# 2．作ったもの
例えば「出発地：東京駅」、「目的地：大阪駅」にした場合に下の画像のように出るbotを作りました。
※後述にもある通り、距離の計算は運転ルートでの距離を表示しています。
![IMG_1344.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2640942/4da5e43e-d94c-2c12-4dec-de65d084a23c.jpeg)

もし触ってみたい方は以下のボタンから友だち登録していただけると嬉しいです。
<a href="https://lin.ee/GLLY8Hk"><img src="https://scdn.line-apps.com/n/line_add_friends/btn/ja.png" alt="友だち追加" height="36" border="0"></a>

# 3．やりたいこと
今回作る上で最低限やりたいことや必要なことを要件として整理してみた。

- LINEで入力した「出発地」～「目的地」間の **移動距離** が出発地～稚内より近いかどうかを比較する 
  - 現在地を取得してやろうと思ったが、位置情報の取得が難しそうだったのとスモールスタートの観点から出発地と目的地の2つを入力するようにした
- 「稚内＝稚内駅」とする
  - あくまでも元ネタのツイートや動画を準拠とする
- 比較するのは **運転ルートでの距離** にする
  - GoogleマップのAPI上で飛行機含めた「公共交通機関」で比較するのがGoogleマップのAPIで出来なさそうだったので代替手段として採用
- 入力する順番は「出発地」「目的地」の順とし、間に改行を入れる
  - 入力方法の仕様上の定義
- もし「出発地」のみ記載されている場合、目的地から稚内駅までの距離を返す
  - 上のパラメータを踏まえた上での考慮
- 「出発地」もしくは「目的地」で「稚内駅」と入力した場合、比較しない
  - 仕様上比較する意味がないので追加
- 「出発地」と「目的地」が同じの場合、比較しない
  - 同じ出発地で検証する必要性がないので追加 

# 4．使ったもの
今回実装するにあたってつかったものは以下の通り
- LINE Developers
  -  Messaging API を使用してメッセージを取得しその結果をbotで返せるようにする
- Google Map API
  - 2点間の距離を測ってくれる「Distance Matrix API」を使用
    - 詳細は[こちら](https://developers.google.com/maps/documentation/javascript/distancematrix?hl=ja)
- Google App Script(GAS)
  - とりあえずクラウド上で動かしたいのと、サーバ立てずに動かせるからって理由で採用

※他にも乗換案内とかで検証できるAPIが幾つかあるようでしたが、問い合わせしての許諾が必要だったり住所が入ってもうまく結果を返してくれなさそうだったりして、GCPで設定するよりも時間がかかりそうだった為今回は見送った。

https://norikae.jorudan.co.jp/openapi/

https://api-info.ekispert.com/

https://api-sdk.navitime.co.jp/api/

# 5．実装
## 5-1．LINE Developersの登録
まず初めに[LINE Developers](https://developers.line.biz/ja/)でチャンネルの登録を行う。

「プロバイダーの新規作成」の新規作成後、「新規チャンネル作成」から「Messaging API」を指定し、チャンネルを作成する。
詳細は[こちらから](https://developers.line.biz/ja/docs/messaging-api/getting-started/)

作成後以下の情報をメモしておく
- チャネル基本設定->あなたのユーザーID
- Messaging API設定-> チャネルアクセストークン（長期） 

## 5-2．Google Cloud Platform（GCP）からプロジェクトを追加する
「Google Map API」を使うために、[Google Cloud Platform（GCP）](https://cloud.google.com/?hl=ja)でプロジェクトを作成してAPIを有効化し、そこにAPIキーを取得する。
新規プロジェクトを作成後、「APIとサービス->APIとサービスの有効か」から「Distance Matrix API」を選択し、有効化する。
その後、「APIとサービス->認証情報」から「Maps API Key」を選択し、API Keyをメモする。
詳細は[こちらから](https://developers.google.com/maps/documentation/distance-matrix/cloud-setup?hl=ja)

## 5-3．GASでGoogle Map APIとのI/Fする処理を作成する
GASからGoogle Map APIにリクエストを投げるのと投げた結果の値を基に「出発地から目的地までの距離」と「出発地から稚内駅までの距離」の比較をする処理を作成する。

```javascript:main.gs
const API_KEY = 'XXXXXXXXXXXXXXXXXX'; //←5-2で取得したAPI key

function main(inputMsg) {
  //改行で分割して「出発地」「目的地」を取得する。
  var input = inputMsg.split(/\r\n|\r|\n/,2);

  var origin;
  var destination;

  if(input.length == 1){
    //入力値が１個しかない場合
    if(input[0] == '稚内駅'){
      Msg = '稚内駅が入力されています。他の場所にしてください。'
      return Msg;
    }
    origin = input[0]; 
    destination = '稚内駅';
  }else{
    //入力値が２個ある場合
    if(input[0] == input[1]){
      Msg = '目的地が同じです。'
      return Msg;
    }else if (input[0] == '稚内駅'){
      Msg = '出発地が稚内駅になっています。他の出発地にしてください。'
      return Msg;
    }else if (input[1] == '稚内駅'){
      Msg = '目的地が稚内駅になっています。他の目的地にしてください。'
      return Msg;
    //一行＋改行のみの場合も考慮
    }else if(input[1] == ""){
      input.pop()
    }
    var origin = input[0]
    if(input.length == 1){
      var destination = '稚内駅';
    }else{
      var destination = '稚内駅 | '+input[1];
    }
  }

  var Msg = '';

  var URL = 'https://maps.googleapis.com/maps/api/distancematrix/json?origins='+ origin +'&destinations='+ destination +'&language=ja&key=' + API_KEY;

  URL = encodeURI(URL);

  //メッセージの情報からAPiにリクエストを投げる。
  try{
    var response = UrlFetchApp.fetch(URL);
  }catch(e){
    Msg = '失敗しました。'
    return Msg;
  }
  var responseData = JSON.parse(response.getContentText());
  try{
    var rowData = responseData['rows'][0]['elements']
  }catch(e){
    Msg = 'レスポンスデータの取得に失敗しました。'
    return Msg;
  }

  //成功しているかどうか確かめる。
  if(input.length == 1){
    if(responseData['status'] != 'OK' 
    || rowData[0]['status'] != 'OK'){
      Msg = '結果を取得できませんでした。正しく入力してください。'
      return Msg;
    }
  }else{
    if(responseData['status'] != 'OK' 
    || rowData[0]['status'] != 'OK'
    || rowData[1]['status'] != 'OK'){
      Msg = '結果を取得できませんでした。正しく入力してください。'
      return Msg;
    }
  }

  //入力値が1個の場合、稚内までの距離を返す。
  if(input.length == 1){
    var kmDistance = decodeURI(rowData[0]['distance']['text'].replace(' ',''));
    Msg =input[0]+'から稚内駅までは'+ kmDistance+'です。';
    return Msg;
  }
  //入力値が2個の場合、稚内より近いかどうかを比較する
  var DistanceA = rowData[0]['distance']['value'];
  var DistanceB = rowData[1]['distance']['value'];

  var kmDistanceA = decodeURI(rowData[0]['distance']['text'].replace(' ',''));
  var kmDistanceB = decodeURI(rowData[1]['distance']['text'].replace(' ',''));

  Msg = input[0]+'から'+input[1]+'までの距離は'+ kmDistanceB +'\n'+input[0]+'から稚内駅までは'+ kmDistanceA+'\n';

  if(DistanceA > DistanceB){
    Msg = Msg +'なので'+input[0]+'から'+input[1]+'は稚内より近いので近場ですね。'
  }else if(DistanceA == DistanceB){
    Msg = Msg + 'なので'+input[0]+'から'+input[1]+'は稚内と同じ距離なので近場ではないですね。'
  }else{
    Msg = Msg + 'なので'+input[0]+'から'+input[1]+'は稚内より遠いので近場ではないですね。'
  }
  return Msg
}
```

## 5-4．GASでLINEとのIFするソースを作成する
doPost関数内でLINEからのリクエストを取得し、「5-3」で作ったロジックを呼ぶように作成。
その結果をLINEに返信する用の関数を呼び出しLINEに送り返す。

```javascript:LINE.gs
//LINEとのI/Fをするロジック
//固定値
  var access_token = "XXXX";  //←5-1で取得したアクセストークン
  var LINE_ID = 'XXXXXX';     //←5-1で取得したユーザーID
  var url = "https://api.line.me/v2/bot/message/reply";

//LINEからのイベントを取得
function doPost(e) {
  //とんできた情報を扱いやすいように変換
  var json = e.postData.contents;
  var events = JSON.parse(json).events; 
  //とんできたイベントの種類を確認する
  events.forEach(function(event) {
    if(event.type == "message") {
      if(event.message.type == "text"){
        var userMessage = event.message.text; 
        var txt;
        txt = main(userMessage);
        if(txt.length==0){
          txt = "失敗しました。"
        }
        var reply_token = event.replyToken;
        sendLINE(reply_token,txt);
      }
    }
  });
}

/**
 * LINEにメッセージを送る関数。
 * 
 * reply_token：返信メッセージ用のトークン
 * txt：メッセージ内容
 */
function sendLINE(reply_token,txt){
  //自動返信メッセージの内容
  var message = {
    "replyToken" : reply_token,
    "messages" : [{"type": "text","text" :txt }]
  };
  //メッセージに添えなければならない情報
  var options = {
    "method" : "post",
    "headers" : {
      "Content-Type" : "application/json",
      "Authorization" : "Bearer " + access_token
    },
    "payload" : JSON.stringify(message)
  };
  //自動返信メッセージを送信する
  UrlFetchApp.fetch(url, options);
}
```

## 5-5．デプロイして、発行されたURLをwebhookに適用させる
GASの「デプロイ->新しいデプロイ->種類の選択」から「ウェブアプリ」を選択し、「アクセスできるユーザー」で全員を選択し、デプロイを実行する。
※初めてデプロイする場合には以下のように表示されるので、アクセスを承認する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2640942/db7fae6e-e59b-51db-7fb4-af82e8e082eb.png)
その後に出てきた「ウェブアプリ」の「URL」をコピーする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2640942/13db44e1-8b2b-cfbc-b176-1bf9d3b7b6b7.png)

URLをコピー後、「LINE Developers」の「Messaging API設定-> Webhook設定」の「Webhook URL」に貼りつけ、「Webhookの利用」を有効化する。

### 5-6．実際にコメントを打ってみる
とりあえず仕様上、いくつかパターンに分けて実施した結果が以下の通り。
![LINE_capture_709370963.185033.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2640942/20a8ab00-29e9-98b2-75ce-5d51aa810f86.jpeg)

# ６． 最後に
とりあえず簡単に距離だけ比較できるものを作ったが幾つか課題もある。
- 「公共交通機関」を使っての比較ができていないこと
  - Google以外の乗換案内系のAPIも調べてできるかは今後機会があれば調べてみる
- 距離で近いかどうかを比較しているのみで、所要時間は計算していないこと
  - ざっと見た感じ、Google MapのAPIでもできそうだが、他のAPI使う場合はそっちでも比較できるかは検証がいるかも
- 与那国空港等の一部の島の施設などを入れたら距離が比較できないこと
  - もしかしたら一部の島ではGoogle MapのAPI上で比較ができないかも
- 仕様上、二つの場所を入れないと比較ができないこと
  - LINEの位置情報を基に距離を算出できるのであれば今度試してみる 

次回以降作るときにはこの辺の課題は1個でも多くクリアできるものを作りたい。

# ７．参考
- 元ネタ関連
  - [元ツイートの方のYoutubeチャンネル](https://www.youtube.com/@saionjichannel)
- LINE
  - [LINE botの作成方法](https://developers.line.biz/ja/docs/messaging-api/building-bot/)
  - [受信メッセージの形式](https://developers.line.biz/ja/reference/messaging-api/)
  - [送信メッセージの形式](https://developers.line.biz/ja/docs/messaging-api/sending-messages/)
- GCP
  - [Distance Matrix APIの使用方法](https://developers.google.com/maps/documentation/distance-matrix?hl=ja)
  - [リクエストとレスポンスの形式](https://developers.google.com/maps/documentation/distance-matrix/start?hl=ja)