【GAS】位置情報を送ると最寄り駅＆終電を教えてくれるLINEbot

##概要
LINEbot定番の「最寄り駅Bot」を少し発展させて、ついでに終電も教えてくれるBotを作ってみた。

ここではどんなBotに仕上がったのか、作り方のポイント、コード全文など紹介。

活動時間の遅い私にとって、先日作った[近場のせんべろ酒場を教えてくれるLINEbot](https://qiita.com/YasumiYasumi/items/e6743718fa0af075ff6e)や[近場の深夜営業レストランを教えてくれるLINEbot](https://qiita.com/YasumiYasumi/items/2fc531d6dfd62f3a260a)と合わせて必需品となるのか^^;
##Bot使用の流れ
なるべくシンプルな操作にしたかったので、基本操作は位置情報を送るだけ。最寄り駅を３つと、それぞれから目的地までの遅い電車３本を教えてくれる。

ただ、目的地によって終電は変わるので事前に降車駅を設定する必要がある。文字列を送信すると、それが降車駅として設定されスプレッドシートに保存される。以降の操作ではその情報を元に終電を調べる。

再度テキストメッセージを送ることで上書きもできる。
###降車駅設定
LINEbotに駅名を送信したところ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/a74b1384-aea2-fbd0-2cfd-755d8effdef2.png)
「〇〇駅」と送った場合に「駅」が重複しないよう、文字列最後に「駅」があった場合は削っている。

スプレッドシートのB2セルに降車駅が記録された。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/eff560ca-cc20-3a04-54c4-4d64a476a9bc.png)

複数アカウントで使用して他の人の降車駅が上書きされないよう、A列にLINEのユーザーIDを記録しID毎に降車駅を保存できるようにしている（画像のIDは伏字）。
###位置情報送信
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/b63ee89f-95a3-9e5d-0cf6-9112f6d366c5.png)
最初に現在位置が表示されるが、任意の位置にも変更可能^^)b
###レスポンス
レスポンスは４カラムのカルーセルテンプレート。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/3a7ddeb5-51e4-4143-f7c0-43d2e6ea3f37.png)
１つ目の「地図カラム」で最寄り駅３つの地図と概要。「駅名（路線名）」ボタンはGoogleMapへのリンク。

２つ目～４つ目の「終電カラム」は、それぞれの駅から降車駅までの出発時刻が遅い電車３本。「発着時間＆乗換回数」ボタンをタップすると、乗換案内情報「[ジョルダン](https://www.jorudan.co.jp/)」の経路案内ページが表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/d22393ec-d95f-363b-fae3-0b13516761c5.png)
終電カラム①の１つ目のリンクをタップしたところ。２つ目なら経路２、３つ目なら経路３が直接表示される。
###問題点
レスポンスのテキスト部分でも触れているけど、駅名が存在しない場合や、同名駅がある場合は検索結果が返ってこない・・・。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/a8c662ec-4390-e0e7-edba-f2a18aa90b62.png)
一応リンクをタップすると駅名を絞り込むページに飛ぶので、改めて駅名を選択すれば終電検索できる。この場合は「赤坂」と言う駅名の候補が、東京・福岡・山梨・群馬と４駅あるためのエラー。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/d8fda6ab-8177-f490-5ac2-6e3970fed875.png)
降車駅の設定ミスは上書きすれば回避できるけど、乗車駅の同駅名エラーを回避しようとするとコードが冗長になってしまうので、とりあえず放置して運用でカバーすることにした（笑）

もし降車駅を赤坂駅にしたい場合は「赤坂（東京）」のようにジョルダン表記に合わせればＯＫ！
##使用したサービス
全て無料・・・ホントすごい時代。

###LINEmessagingAPI & GoogleAppsScript
LINEとGoogleのアカウントがあればOK。

「LINEbot」「GAS」でググると導入方法を分かりやすく紹介しているサイトが多く、やることは一緒なのでココでは割愛。
###最寄り駅検索API
[HeartRails Express](http://express.heartrails.com/api.html)と言う、路線、駅名データ等の地理情報を提供しているAPIを使用。この中の「最寄駅情報取得 API」を使って位置情報から最寄り駅を取得する。下記URLがサイトのサンプル。

http://express.heartrails.com/api/json?method=getStations&x=135.0&y=35.0

`x=経度` `y=緯度`を変えれば任意の場所の最寄り駅情報を取得できる。距離が近い順に３駅分、以下の内容がJSON形式で返ってくる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/cca68f54-fc86-80d2-0f3c-1c08529447a4.png)
これで無料、登録不要と、なんだか恐縮してしまうサービス<(_ _)>ｱｻﾞｯｽ
###ジョルダン
みんな大好き乗換案内サービスの老舗「[ジョルダン](https://www.jorudan.co.jp/)」。

こちらもAPIを提供しているが、今回はAPIではなく「出発駅」「到着駅」「終電」の３つの条件から検索URLを生成して、その検索結果ページのソースから必要な情報を取り出す。

```
https://www.jorudan.co.jp/norikae/cgi/nori.cgi?rf=top&eki1={出発駅}&eki2={到着駅}&Cway=3&S=検索
```

`Cway=3`で終電絞り込み。`{出発駅}` `{到着駅}`は文字列ベタ打ちでOK^^ 普通に検索すると出発日時やらの情報もURLに入って長くなるけど、どうやら無くても大丈夫そうだったので省略。

検索結果のソースを表示させると、70行目あたりに丁度良いのを発見Σ((○ﾟ∀ﾟσ)ﾟ+o｡ﾐﾂｹﾀ!

```html
var NR_data = {

1:{ 'kno':1, 'min':35, 'tm_a':202101030059, 'tm_d':202101030024, 'c':1, 'total':{ 'ic':346, 'kip':350 ,'tk':0}},
0:{ 'kno':2, 'min':34, 'tm_a':202101030040, 'tm_d':202101030006, 'c':1, 'total':{ 'ic':272, 'kip':280 ,'tk':0}},
2:{ 'kno':3, 'min':40, 'tm_a':202101030021, 'tm_d':202101022341, 'c':2, 'total':{ 'ic':307, 'kip':310 ,'tk':0}}
};
```
経路毎の乗車時間、到着時間、出発時間、乗換回数、乗車料金などが並んでいる。これをスクレイピングして使うことにする。

検索URLの後ろにチョイ足しすると「経路２」なども直接表示させられる。PC用ページだとURLの後ろに`#bR2`を付け、スマホ用ページだと`#id_nr_routeBlock_2`となる。今回はスマホ用でURLを生成している。
###ライブラリ
長いページソースから必要部分をスクレイピングするのに、自分でコードを書かずに「TextPicker」と言うライブラリを活用。めっちゃ便利(*ﾟ▽ﾟ)ﾉ”

プロジェクトID：Ms1_ywyxDUyXZlf1HE1E2_ydpxDUCDjPE

①TextPicker.open('ソース');
`ソース`を格納。

②TextPicker.pickUp('文字列１','文字列２');
ソース内の`文字列１`から`文字列２`の間を切り抜く。

③TextPicker.skipTo('文字列３');
ソース内の`文字列３`まで飛ばす。以降、`文字列３`の前は検索対象から外れる。

①②③の操作を繰り返す。
###いらすとや
毎度毎度、アイコンやカラムの画像は「[いらすとや](https://www.irasutoya.com/)」様の素材を拝借(シ_ _)シ ﾊﾊｧｰ
##コード
コードを一気に見るならコチラ。ダラダラと書き連ねてしまい、コードは`function doPost(event){}`一本^^;

<details><summary>コード全文</summary><div>

```javascript
function doPost(event) {

  const sheet = SpreadsheetApp.openById('シートID').getSheetByName('シート名');
  var cells   = sheet.getDataRange().getValues();
  var lastRow = sheet.getLastRow();
  
  const TOKEN = 'LINE Messaging APIのアクセストークン';
  const reply = "https://api.line.me/v2/bot/message/reply";
  var headers = {"Content-Type": "application/json; charset=UTF-8",
                 "Authorization": "Bearer " + TOKEN,};
  
  var json = JSON.parse(event.postData.contents);
  var replyToken  = json.events[0].replyToken;
  var messageType = json.events[0].message.type;
  var userId      = json.events[0].source.userId;

  if(!cells[0][0]){ 
    sheet.getRange('A1').setValue(userId);
    var row = 1;
  }
  
  for(var i=0; i<lastRow; i++){

    if(cells[i][0]!==userId && i+1===lastRow){
      var row = i+2;
      sheet.getRange(row, 1).setValue(userId);
    }
    if(cells[i][0]===userId){
      var row = i+1;
      break;
    }
  }
  
  if(messageType==='location'){
  
    var exitSt = sheet.getRange(row, 2).getValue();
    
    if(!exitSt){
      var payload = JSON.stringify({
          "replyToken": replyToken,
          "messages": [{
            "type": "text",
            "text": '到着駅を設定してください'
          }]
      });
      UrlFetchApp.fetch(reply, {
          "method": "post",
          "headers": headers,
          "payload": payload
      });
    return;
    }
    
    var irastUrl = [];
    irastUrl[0] = 'https://4.bp.blogspot.com/-993E8FTMZds/U9zsXft5wXI/AAAAAAAAkcg/yjF2qgfDIDk/s1600/number_1.png';
    irastUrl[1] = 'https://1.bp.blogspot.com/-a0AMrvLUJmM/U9zsXbwKokI/AAAAAAAAkck/UPrQbQ1R50E/s1600/number_2.png';
    irastUrl[2] = 'https://2.bp.blogspot.com/-wsph92D4YYQ/U9zsYIuFt3I/AAAAAAAAkc0/bdRK56v8Rfg/s1600/number_3.png';
    
    var lat = json.events[0].message.latitude;
    var lng = json.events[0].message.longitude;
    
    var stUrl  = 'http://express.heartrails.com/api/json?method=getStations&x=' + lng + '&y=' + lat;
    var stRes  = UrlFetchApp.fetch(stUrl);
    var stJson = JSON.parse(stRes.getContentText());
    
    var st = [];
    
    st[0] = stJson.response.station[0].name;
    st[1] = stJson.response.station[1].name;
    st[2] = stJson.response.station[2].name;
    
    var line = [];
    
    line[0] = stJson.response.station[0].line;
    line[1] = stJson.response.station[1].line;
    line[2] = stJson.response.station[2].line;
    
    var lat0 = stJson.response.station[0].y;
    var lat1 = stJson.response.station[1].y;
    var lat2 = stJson.response.station[2].y;
    var lng0 = stJson.response.station[0].x;
    var lng1 = stJson.response.station[1].x;
    var lng2 = stJson.response.station[2].x;
  
    var distance0 = stJson.response.station[0].distance;
    var distance1 = stJson.response.station[1].distance;
    var distance2 = stJson.response.station[2].distance;
    var farDis = Number(distance2.slice(0, -1));
    
    var text = '① ' + st[0] + '(' + distance0 + ')\n② ' + st[1] + '(' + distance1 + ')\n③ ' + st[2] + '(' + distance2 + ')';

    var lavel0 = st[0] + '(' + line[0] + ')';
    var lavel1 = st[1] + '(' + line[1] + ')';
    var lavel2 = st[2] + '(' + line[2] + ')';

    var st0Url = 'https://maps.google.com/maps?q=' + lat0 + ',' + lng0;
    var st1Url = 'https://maps.google.com/maps?q=' + lat1 + ',' + lng1;
    var st2Url = 'https://maps.google.com/maps?q=' + lat2 + ',' + lng2;
    
    var zoom = 10;
  
    switch(true){
    
      case farDis >= 10000:
      break;
    
      case farDis >= 5000:
      zoom = zoom + 1;
      break;
    
      case farDis >= 2000:
      zoom = zoom + 2;
      break;
    
      case farDis >= 1000:
      zoom = zoom + 3;
      break;
    
      case farDis >= 500:
      zoom = zoom + 4;
      break;
    
      default:
      zoom = zoom + 5;
      break;
  
    }
  
    var latC = (lat0 + lat1 + lat2) / 3;
    var lngC = (lng0 + lng1 + lng2) / 3;
  
    var map = Maps.newStaticMap()
                  .setLanguage("ja")
                  .setSize(250,250)
                  .setZoom(zoom)
                  .setCenter(latC,lngC);
                
    map.setMarkerStyle(Maps.StaticMap.MarkerSize.MID, Maps.StaticMap.Color.RED, 1).addMarker(lat0, lng0);
    map.setMarkerStyle(Maps.StaticMap.MarkerSize.MID, Maps.StaticMap.Color.RED, 2).addMarker(lat1, lng1);
    map.setMarkerStyle(Maps.StaticMap.MarkerSize.MID, Maps.StaticMap.Color.RED, 3).addMarker(lat2, lng2);
  
    var mapBlob = map.getBlob().getAs('image/png').setName('map.png');
    var folder = DriveApp.getFolderById('フォルダID');
    folder.createFile(mapBlob);
  
    var mapId  = DriveApp.getFilesByName('map.png').next().getId();
    var mapUrl = 'https://drive.google.com/uc?id=' + mapId
    
    var columns = [];
    
    var column = {
                    "thumbnailImageUrl": mapUrl,
                    "imageBackgroundColor": "#FFFFFF",
                    "title": "最寄り駅の地図を見る",
                    "text": text,
                    "actions": [
                      {
                        "type": "uri",
                        "label": lavel0,
                        "uri": st0Url
                      },
                      {
                        "type": "uri",
                        "label": lavel1,
                        "uri": st1Url
                      },
                      {
                        "type": "uri",
                        "label": lavel2,
                        "uri": st2Url
                      }
                    ]
                  }
                  
    columns[0] = column;
    
    for(var i=0; i<3; i++){
    
      var jorUrl = 'https://www.jorudan.co.jp/norikae/cgi/nori.cgi?rf=top&eki1=' + st[i] + '&eki2=' + exitSt + '&Cway=3&S=検索';
      var data = UrlFetchApp.fetch(jorUrl).getContentText();
      
      TextPicker.open(data);
      
      var actions = [];
      
      for(var j=1; j<4; j++){
      
        var skipWord = "'kno':" + j;
        TextPicker.skipTo(skipWord);
        
        var arr = TextPicker.pickUp("'tm_a':",",");
        var dep = TextPicker.pickUp("'tm_d':",",");
        
        arr = arr.slice(-4);
        arr = arr.slice(0, 2) + ':' + arr.slice(2);
        
        dep = dep.slice(-4);
        dep = dep.slice(0, 2) + ':' + dep.slice(2);
        
        var trans = TextPicker.pickUp("'c':",",");
        
        var label = dep + '発 ' + arr + '着 乗換' + trans + '回';
        var bRUrl = jorUrl + '#id_nr_routeBlock_' + j;
        
        var action = { 
                        "type": "uri", 
                        "label": label, 
                        "uri": bRUrl 
                      }
        
        actions[j-1] = action;
        
      }
    
      var title = st[i] + ' → ' + exitSt + 'の終電';
      var text  = '【出発時刻が遅い順】\n駅がないor同名駅があるとエラー\nリンク先で検索し直してください';
    
      var column = { 
                      "thumbnailImageUrl": irastUrl[i],
                      "imageBackgroundColor": "#FFFFFF",
                      "title": title,
                      "text": text,
                      "actions": actions
                    }
                    
      columns[i+1] = column;
    
    }
    
    var payload = JSON.stringify({
        "replyToken": replyToken,
        "messages": [{
          "type": "template",
          "altText": "終電情報",
          "template": {
            "type": "carousel",
            "columns": columns,
            "imageAspectRatio": "square",
            "imageSize": "cover"
          }
        }]
    });
    UrlFetchApp.fetch(reply, {
        "method": "post",
        "headers": headers,
        "payload": payload
    });
  
    var delData = DriveApp.getFilesByName('map.png').next().setTrashed(true);
  
  }else if(messageType==='text'){
  
    var userMessage = json.events[0].message.text;
    if(userMessage.slice(-1)==='駅'){userMessage = userMessage.slice(0, -1);}
    
    sheet.getRange(row, 2).setValue(userMessage);
    
    var botMessage = '目的地を「' + userMessage + '駅」に設定しました';
    
    var payload = JSON.stringify({
        "replyToken": replyToken,
        "messages": [{
          "type": "template",
          "altText": "位置情報ボタン",
          "template": {
            "type": "buttons",
            "text": botMessage,
            "actions": [
              {
                "type": "uri",
                "label": "位置情報を送る",
                "uri": "https://line.me/R/nv/location/"
              }
            ]
          }
        }]
    });
    UrlFetchApp.fetch(reply, {
        "method": "post",
        "headers": headers,
        "payload": payload
    });
    
  }else{
  
    var botMessage = '位置情報を送ってね。\n最寄り駅からの終電を調べるよ^^)b';
  
    var payload = JSON.stringify({
        "replyToken": replyToken,
        "messages": [{
          "type": "template",
          "altText": "位置情報ボタン",
          "template": {
            "type": "buttons",
            "text": botMessage,
            "actions": [
              {
                "type": "uri",
                "label": "位置情報を送る",
                "uri": "https://line.me/R/nv/location/"
              }
            ]
          }
        }]
    });
    UrlFetchApp.fetch(reply, {
        "method": "post",
        "headers": headers,
        "payload": payload
    });
  }
}
```
</div></details>

固有の記述は「シートID」「シート名」「フォルダID」「APIトークン」だけ。それ以外はコピペで同じ動作をするはず。以下、部分ごとに説明。

長い・・・分ければよかったかも。
###最初の共通部分①
コード開始＆初期設定

```javascript
function doPost(event) {

  //シート セル 最終行を取得
  const sheet = SpreadsheetApp.openById('シートID').getSheetByName('シート名');
  var cells   = sheet.getDataRange().getValues();
  var lastRow = sheet.getLastRow();
   
  //APIトークン Reply用HTTPリクエストを設定
  const TOKEN = 'LINE Messaging APIのアクセストークン';
  const reply = "https://api.line.me/v2/bot/message/reply";
  var headers = {"Content-Type": "application/json; charset=UTF-8",
                 "Authorization": "Bearer " + TOKEN,};
```
紐付いているスプレッドシートのデータを取得。APIトークンなど設定。メッセージ送信時のHTTPリクエストやヘッダーも共通なので、ついでに設定しておく。
###最初の共通部分②
メッセージイベントから必要情報を取得＆ユーザー判定。

```javascript
  //メッセージイベントの返信用トークン メッセージタイプ ユーザーIDを取得
  var json = JSON.parse(event.postData.contents);
  var replyToken  = json.events[0].replyToken;
  var messageType = json.events[0].message.type;
  var userId      = json.events[0].source.userId;

  //既存ユーザーゼロの場合 A1セルにID登録
  if(!cells[0][0]){ 
    sheet.getRange('A1').setValue(userId);
    var row = 1;
  }

  //登録判定 初回ならID登録
  for(var i=0; i<lastRow; i++){
    if(cells[i][0]!==userId && i+1===lastRow){
      var row = i+2;
      sheet.getRange(row, 1).setValue(userId);
    }
    if(cells[i][0]===userId){
      var row = i+1;
      break;
    }
  }
```
ユーザーIDがA列にあれば、その行番号を`row`に保存。無ければ最終行の下にユーザーIDを記録して、その行番号を`row`に保存。

※既存ユーザーが誰もいない場合A1セルにユーザーIDを記録する操作をしている。これが無いと一番最初にBotが動作しないが、最初以外は使われる事のない部分。
###イベントによる分岐①
```javascript
  //メッセージタイプが位置情報の場合
  if(messageType==='location'){ 
   
    //到着駅を取得
    var exitSt = sheet.getRange(row, 2).getValue(); 
```
メッセージタイプが位置情報だった場合、まずはB列に記録されているユーザー毎の到着駅を取得する。
###到着駅が未設定の場合
```javascript
    //到着駅未定の場合 メッセージを送信して終了
    if(!exitSt){ 
      var payload = JSON.stringify({ 
          "replyToken": replyToken, 
          "messages": [{ 
            "type": "text", 
            "text": '到着駅を設定してください' 
          }] 
      }); 
      UrlFetchApp.fetch(reply, { 
        "method": "post",
        "headers": headers,
        "payload": payload
      }); 
    return; 
    } 
```
到着駅を設定する前に位置情報を送った場合、「到着駅を設定してください」とメッセージを返す。LINEbotは1つのイベントに対して1つしかリアクション出来ないので、`return`しなくてもその後LINEbot的な動作は起こらない。けど気持ち悪いので終了させておく。
###いらすとや画像を設定
```javascript
    //いらすとやイラストURLを設定
    var irastUrl = []; 
    irastUrl[0] = 'https://4.bp.blogspot.com/-993E8FTMZds/U9zsXft5wXI/AAAAAAAAkcg/yjF2qgfDIDk/s1600/number_1.png'; 
    irastUrl[1] = 'https://1.bp.blogspot.com/-a0AMrvLUJmM/U9zsXbwKokI/AAAAAAAAkck/UPrQbQ1R50E/s1600/number_2.png'; 
    irastUrl[2] = 'https://2.bp.blogspot.com/-wsph92D4YYQ/U9zsYIuFt3I/AAAAAAAAkc0/bdRK56v8Rfg/s1600/number_3.png'; 
```
レスポンスのカラム２～４に使う画像。後ほどfor文繰り返しの中で順番に使えるよう、配列に格納。
###最寄り駅APIから必要情報を取得
```javascript
    //メッセージイベントから緯度経度を取得
    var lat = json.events[0].message.latitude; 
    var lng = json.events[0].message.longitude; 
     
    //API「HeartRails Express」で緯度経度から最寄り駅を取得
    var stUrl  = 'http://express.heartrails.com/api/json?method=getStations&x=' + lng + '&y=' + lat; 
    var stRes  = UrlFetchApp.fetch(stUrl); 
    var stJson = JSON.parse(stRes.getContentText()); 
     
    //最寄り駅の駅名を３つ取得
    var st = []; 
    st[0] = stJson.response.station[0].name; 
    st[1] = stJson.response.station[1].name; 
    st[2] = stJson.response.station[2].name; 
     
    //最寄り駅の路線名を３つ取得
    var line = []; 
    line[0] = stJson.response.station[0].line; 
    line[1] = stJson.response.station[1].line; 
    line[2] = stJson.response.station[2].line; 
     
    //各駅の緯度経度を取得
    var lat0 = stJson.response.station[0].y; 
    var lat1 = stJson.response.station[1].y; 
    var lat2 = stJson.response.station[2].y; 
    var lng0 = stJson.response.station[0].x; 
    var lng1 = stJson.response.station[1].x; 
    var lng2 = stJson.response.station[2].x; 
   
    //各駅までの距離を取得
    var distance0 = stJson.response.station[0].distance;
    var distance1 = stJson.response.station[1].distance;
    var distance2 = stJson.response.station[2].distance;

    //最も遠い駅までの距離の単位「ｍ」を除き別にしておく 
    var farDis = Number(distance2.slice(0, -1));
```
メッセージイベントから緯度経度を取得し、APIリクエストを生成。最寄り駅３つの「駅名」「路線名」「緯度経度」「位置情報からの距離」を取得。駅名と路線名はfor文中で使いたいので配列に格納。

最も遠い駅までの距離（単位なし）を別で使うので仕込んでおく。
###取得した情報を地図カラム用に加工
この部分。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/45359e0f-032c-e721-2bce-78540744a6f1.png)

```javascript 
    //地図カラムに表示させるテキストを作成
    var text = '① ' + st[0] + '(' + distance0 + ')\n② ' + st[1] + '(' + distance1 + ')\n③ ' + st[2] + '(' + distance2 + ')'; 

    //地図カラムのアクションラベル用「駅名（路線名）」を作成 
    var lavel0 = st[0] + '(' + line[0] + ')'; 
    var lavel1 = st[1] + '(' + line[1] + ')'; 
    var lavel2 = st[2] + '(' + line[2] + ')';

    //地図カラムのアクションボタン用に各駅のGoogleMapURLを生成
    var st0Url = 'https://maps.google.com/maps?q=' + lat0 + ',' + lng0; 
    var st1Url = 'https://maps.google.com/maps?q=' + lat1 + ',' + lng1; 
    var st2Url = 'https://maps.google.com/maps?q=' + lat2 + ',' + lng2; 
```
GoogleMap上の駅の位置は、取得した駅の緯度経度から生成。
###地図カラムの画像作成
この部分。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/b6541efc-a2e4-975f-b43b-13309eecb72a.png)
これのせいで動作がとても遅い！

```javascript
    //地図作成用 初期zoomを10に設定
    var zoom = 10;

    //最も遠い駅までの距離に応じてzoomを変更
    switch(true){ 
     
      case farDis >= 10000: 
      break; 
     
      case farDis >= 5000: 
      zoom = zoom + 1; 
      break; 
     
      case farDis >= 2000: 
      zoom = zoom + 2; 
      break; 
     
      case farDis >= 1000: 
      zoom = zoom + 3; 
      break; 
     
      case farDis >= 500: 
      zoom = zoom + 4; 
      break; 
     
      default: 
      zoom = zoom + 5; 
      break; 
   
    } 

    //地図の中心地は３つの駅の緯度経度の平均
    var latC = (lat0 + lat1 + lat2) / 3; 
    var lngC = (lng0 + lng1 + lng2) / 3; 

    //地図オブジェクト作成
    var map = Maps.newStaticMap() 
                  .setLanguage("ja") 
                  .setSize(250,250) 
                  .setZoom(zoom) 
                  .setCenter(latC,lngC); 

    //最寄り駅のマーカーを近い順にプロット
    map.setMarkerStyle(Maps.StaticMap.MarkerSize.MID, Maps.StaticMap.Color.RED, 1).addMarker(lat0, lng0);
    map.setMarkerStyle(Maps.StaticMap.MarkerSize.MID, Maps.StaticMap.Color.RED, 2).addMarker(lat1, lng1);
    map.setMarkerStyle(Maps.StaticMap.MarkerSize.MID, Maps.StaticMap.Color.RED, 3).addMarker(lat2, lng2);

    //地図オブジェクトを画像としてドライブに出力
    var mapBlob = map.getBlob().getAs('image/png').setName('map.png'); 
    var folder = DriveApp.getFolderById('フォルダID'); 
    folder.createFile(mapBlob); 

    //画像共有用URLを生成
    var mapId  = DriveApp.getFilesByName('map.png').next().getId(); 
    var mapUrl = 'https://drive.google.com/uc?id=' + mapId 
```
最も遠い駅までの距離に応じて地図の縮尺を変えている。だいたい上手く表示されるけど、たまにマーカーが見切れるので調整必要かも^^; 地図の中心は、現在位置にするより見切れにくくなるので３つの駅の中心にしている。

地図オブジェクトを画像としてドライブに出力して、共有URLを取得する。

地図の扱いについて：[【GAS】GoogleAppsScriptで地図を扱うTips【GoogleMap】](https://qiita.com/YasumiYasumi/items/d772bfa5f77201009d94)

LINEbotに表示させるために、画像の共有設定を「誰でも閲覧可」にする必要がある。共有設定を変更するメソッドもあるけど、フォルダの共有設定を誰でも閲覧可にしておけば勝手に同じ設定なる。画像のURLそのものだとLINEに表示されないので、画像のIDを取得して共有用のURLに加工する。
###地図カラム作成
```javascript
    //カラムを格納する配列を宣言
    var columns = [];

    //地図カラムを作成
    var column = { 
                    "thumbnailImageUrl": mapUrl, 
                    "imageBackgroundColor": "#FFFFFF", 
                    "title": "最寄り駅の地図を見る", 
                    "text": text, 
                    "actions": [ 
                      { 
                        "type": "uri", 
                        "label": lavel0, 
                        "uri": st0Url 
                      }, 
                      { 
                        "type": "uri", 
                        "label": lavel1, 
                        "uri": st1Url 
                      }, 
                      { 
                        "type": "uri", 
                        "label": lavel2, 
                        "uri": st2Url 
                      } 
                    ] 
                  } 
                  
    //地図カラムを格納 
    columns[0] = column; 
```
事前に作成した地図画像をサムネイルに設定。アクションオブジェクトとして、３つの最寄り駅の駅名＆路線名をラベル、駅の位置のGoogleMpaURLをURIとして設定した。

カルーセルテンプレートはカラムを最大10個まで使用できる。JSONの記述は`"columns": [{カラム},{カラム},{カラム},...]`となるので、上記のように配列`columns[]`に格納していけば、`"columns": columns`として使用できる。

参考：[LINE Messaging APIリファレンス【テンプレートメッセージ】](https://developers.line.biz/ja/reference/messaging-api/#template-messages)
###最寄り駅３つの終電カラム作成
コレ！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/18425abf-b3a0-9c71-37e1-f6d8e0374d80.png)

```javascript
    //繰り返しで終電カラム①～③を作成
    for(var i=0; i<3; i++){ 

      //ジョルダンで「最寄り駅から到着駅までの終電」をを調べるURLを生成 ソースを取得
      var jorUrl = 'https://www.jorudan.co.jp/norikae/cgi/nori.cgi?rf=top&eki1=' + st[i] + '&eki2=' + exitSt + '&Cway=3&S=検索'; 
      var data = UrlFetchApp.fetch(jorUrl).getContentText(); 

      //テキストピッカーでスクレイピング開始 
      TextPicker.open(data); 

      //アクションオブジェクトを格納する配列を宣言 
      var actions = []; 

      //繰り返しでアクションオブジェクト①～③を作成 
      for(var j=1; j<4; j++){ 

        //経路 j の行までスキップ
        var skipWord = "'kno':" + j; 
        TextPicker.skipTo(skipWord);

        //到着時間部分と出発時間部分を取得 
        var arr = TextPicker.pickUp("'tm_a':",","); 
        var dep = TextPicker.pickUp("'tm_d':",","); 
         
        //到着時間部分を「00:00」に加工
        arr = arr.slice(-4); 
        arr = arr.slice(0, 2) + ':' + arr.slice(2); 
         
        //出発時間部分を「00:00」に加工
        dep = dep.slice(-4); 
        dep = dep.slice(0, 2) + ':' + dep.slice(2); 
         
        //乗換回数を取得
        var trans = TextPicker.pickUp("'c':",","); 
         
        //取得した情報をラベル用に加工
        var label = dep + '発 ' + arr + '着 乗換' + trans + '回';

        //アクションUri用に経路 j のURLを生成
        var bRUrl = jorUrl + '#id_nr_routeBlock_' + j; 
         
        //アクションを作成
        var action = {  
                        "type": "uri",  
                        "label": label,  
                        "uri": bRUrl  
                      } 
         
        //アクションを格納
        actions[j-1] = action; 
      } 

      //駅名からカラム i のタイトルとテキストを作成
      var title = st[i] + ' → ' + exitSt + 'の終電'; 
      var text  = '【出発時刻が遅い順】\n駅がないor同名駅があるとエラー\nリンク先で検索し直してください';

      //カラムを作成（カラム i に繰り返し i 中で作ったアクション①～③が入る）
      var column = {  
                      "thumbnailImageUrl": irastUrl[i], 
                      "imageBackgroundColor": "#FFFFFF", 
                      "title": title, 
                      "text": text, 
                      "actions": actions 
                    }
      //カラムを格納
      columns[i+1] = column; 
    } 
```
終電カラム①～③（`column[1]`～`column[3]`）を作って、配列`columns[]`に格納していく。各カラムに、地図カラムと同様にアクションオブジェクトを３つ設定。

配列`actions[]`にアクションオブジェクト`action{}`を格納している。カラムの繰り返しと理屈は同じ。ちょっと分かりにくいけど、終電カラム①～③の繰り返し中に、アクション①～③の繰り返しが入れ子になっている。

アクションオブジェクトは乗車時間が遅い順に「発着時間＆乗換回数」をラベル、該当する経路のジョルダンページをURIとして設定している。
###メッセージ送信
```javascript
    //作成したカラムでメッセージを作成 送信
    var payload = JSON.stringify({ 
        "replyToken": replyToken, 
        "messages": [{ 
          "type": "template", 
          "altText": "終電情報", 
          "template": { 
            "type": "carousel", 
            "columns": columns, 
            "imageAspectRatio": "square", 
            "imageSize": "cover" 
          } 
        }] 
    }); 
    UrlFetchApp.fetch(reply, { 
        "method": "post",
        "headers": headers,
        "payload": payload
    }); 

    //ドライブから地図画像を削除
    var delData = DriveApp.getFilesByName('map.png').next().setTrashed(true); 
```
やっと送信(;´・｀)>ﾌｩ

送信してしまえば不要となるので、作成した地図画像は削除。
###イベントによる分岐②
```javascript
  //メッセージタイプが文字列の場合
  }else if(messageType==='text'){ 

    //メッセージを取得 最後の文字が「駅」なら削る
    var userMessage = json.events[0].message.text;
    if(userMessage.slice(-1)==='駅'){userMessage = userMessage.slice(0, -1);}
     
    //メッセージをシートに保存
    sheet.getRange(row, 2).setValue(userMessage); 
     
    //文字列を到着駅に設定したメッセージを作成 送信
    var botMessage = '目的地を「' + userMessage + '駅」に設定しました';
    var payload = JSON.stringify({ 
        "replyToken": replyToken, 
        "messages": [{ 
          "type": "template", 
          "altText": "位置情報ボタン", 
          "template": { 
            "type": "buttons", 
            "text": botMessage, 
            "actions": [ 
              { 
                "type": "uri", 
                "label": "位置情報を送る", 
                "uri": "https://line.me/R/nv/location/" 
              } 
            ] 
          } 
        }] 
    }); 
    UrlFetchApp.fetch(reply, { 
        "method": "post",
        "headers": headers,
        "payload": payload
    }); 
```
イベントオブジェクトが文字列だった場合、その文字列を到着駅としてB列に保存。「〇〇駅」だった場合は文字列から「駅」を削る。目的地を設定したメッセージを送信。メッセージには位置情報を開くボタン付き。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/f202b42b-cb0c-76a0-f69b-cdb0af06b335.png)
参考：[LINE Messaging APIリファレンス【URLスキーム】](https://developers.line.biz/ja/docs/messaging-api/using-line-url-scheme/#line%E3%81%A6%E3%82%99%E5%88%A9%E7%94%A8%E3%81%A6%E3%82%99%E3%81%8D%E3%82%8Burl%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%A0)
###イベントによる分岐③
```javascript
  //イベントオブジェクトが位置情報でも文字情報でもなかった場合
  }else{ 
   
    //位置情報を求めるメッセージを作成 送信（位置情報を求めるボタン付き）
    var botMessage = '位置情報を送ってね。\n最寄り駅からの終電を調べるよ^^)b'; 
    var payload = JSON.stringify({ 
        "replyToken": replyToken, 
        "messages": [{ 
          "type": "template", 
          "altText": "位置情報ボタン", 
          "template": { 
            "type": "buttons", 
            "text": botMessage, 
            "actions": [ 
              { 
                "type": "uri", 
                "label": "位置情報を送る", 
                "uri": "https://line.me/R/nv/location/" 
              } 
            ] 
          } 
        }] 
    }); 
    UrlFetchApp.fetch(reply, { 
        "method": "post",
        "headers": headers,
        "payload": payload
    }); 
  } 
}
```
画像や音声などが送られてきた場合、位置情報を求めるメッセージを送信。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/708687/26667540-85bf-82ce-4f51-9d460fb08368.png)
到着駅を設定した時のメッセージと同様、位置情報を開くボタン付き。
##検討
降車駅なんてだいたい一緒だし、一度設定した後は位置情報を送るだけで「なる遅電車」を３路線×３本教えてくれるってのは秀逸！

と思ったけど、同名駅エラーとか遅い動作とか、改善点も多い。何気にジョルダンの経路１と経路２が被ることなんかもあって、イマイチなレスポンスになることも。

同名駅エラーの対処は、エラーの場合に最寄り駅APIから住所を取得して`（都道府県）`を追加するとか、いっそのこと数も限られてるし同名駅を全て記述して回避してみようかなとか考えたけど、最寄り３駅全て同名駅ってこともないだろうと横着して放置している。

やっぱり経路検索については検索URL生成とかではなく、ちゃんとしたAPIを使用した方が精度も利便性も高そう。

駅の位置のGoogleMapURL生成だけなら早いから、地図画像作成にもたつくようなら省略しても良いかも？

カラムのタイトルは、40文字を超えると文字数制限でエラーになる。調べてみたら最長駅名が17文字みたいなので、今のところ駅名の文字数によるエラーは想定しなくて良さそう。

他、思わぬエラーやバグがあるような気がしてならない（笑）見つけたら追記するってことで、取り合えず投稿 ..._〆(・∀・＠)
###おしまい