読者です 読者をやめる 読者になる 読者になる

鷹楠日記

美術展、博物展、神社、旅行、読書、IT関連などの雑感を書いていきます。

近くの神社を教えてくれる LINE Bot を作ってみました。

鷹楠です。前回前々回と、LINE Bot を作ってきましたが、今回もまた新しい LINE Bot を作ってみました。

機能はシンプル。近くの神社を教えてくれて、参拝済みの記録をつけられる LINE Bot です。

今回は、LINE APIカルーセル型のテンプレートメッセージを使ってみました。それから、Google Apps Script で無料で簡単に使えるデータベース Google Fusion Tables も使ってみました。

きっかけ&こんなものが作りたい

  • 神社が好きで、有名どころはけっこうまわってる。
  • とはいえ、まだまだ知らない神社はいっぱい。
  • 近所や、ちょっと出かけた先で、まだ行ったことがない神社をみつけたい。暇つぶしや、ウォーキングがてら。
  • 行ったことがある神社は、何かしらの形でわかるようにしたい。
  • 社格なんかをすぐ知りたい。
  • 御朱印を管理したい。

…とまあ、常日頃からこういう想いがあって、とりあえず作れるものを作ってみました。ちなみに、最後の 2 つの機能(社格なんかの表示、御朱印の管理)は、残念ながらこの記事を書いている時点では実現できていません。今後の課題です。

こんなものができた

  • 位置情報を送信すると、そこから半径 3 km 以内の神社の情報を、最大 5 件教えてくれる。
  • 神社の情報には、「名前」「そこまでの直線距離」「住所」が含まれる。
  • 「その神社を Google 検索するためのボタン」「その神社までのルート(by Google マップ)を検索するためのボタン」もいっしょに表示される。詳細が知りたかったら、ここから検索できる。神社名だけでなく住所もいっしょに検索ワードになってるので、一意に神社を特定して検索できる。
  • 「行ったことがある」ボタンもいっしょに表示される。これを押すと、次回からこの神社の情報には「参拝済み」マークがつく。


外観を紹介します。

Bot の名前は「近くの神社」。

f:id:takagusu:20170307193911j:plain


友だち追加時のメッセージ:

f:id:takagusu:20170307193934j:plain


位置情報を送信すると、

f:id:takagusu:20170307194130j:plain


そこから半径 3 km 以内の神社の情報を、最大 5 件教えてくれます。
今回は、カルーセル型のテンプレートメッセージでメッセージを送ってます。横にスライドするやつです。最大 5 件というのは、LINE API の制限だったり…。

f:id:takagusu:20170307194014j:plain


タップできるボタンが 3 つありまして、

f:id:takagusu:20170307194458j:plain


「この神社を検索」ボタンをタップすると、「神社の名前」+「住所」で Google 検索します。(LINE 内のブラウザで開かないで欲しいんですけどね…)

f:id:takagusu:20170307194602j:plain


「ここからのルート」ボタンをタップすると、Google マップのルート検索結果を表示します。徒歩の場合のルートにしてます。

f:id:takagusu:20170307195250j:plain


最後に、「行ったことがある」ボタンついて。タップすると、内部で持ってるデータベースにユーザーと神社の ID が保存されて、次に神社が表示されたときには、「参拝済み」という文字がいっしょに表示されます。

f:id:takagusu:20170307195723j:plain

大まかな作り方

  • 前回と同じく、LINE Bot アカウントを作り、Google Apps Script で LINE Bot 用の web アプリケーションを作成した。
  • 位置情報(緯度・経度)の近くにある神社を探すために、Yahoo!ローカルサーチAPI を利用した。この API の優れているところは、「コンビニ」「カフェ」「動物園」「歯科」などかなり具体的な施設のカテゴリ(業種コード)で検索できるところ。嬉しいことに、このカテゴリの中に「神社」があった。業種コードの一覧はここを参照(一覧の csv をダウンロードできる)。なお、利用する際はクレジット表示が必要。
  • 神社の情報を伝えるために LINE APIカルーセル型のテンプレートメッセージ を利用した。タップしたときに URL リンクを開けるように uri アクションを使い、神社に行ったことがあるという情報を受け取るために postback アクションを使った。
  • ユーザーごとの参拝済み情報(神社に行ったことがあるかどうか)は、Google Fusion Tables というデータベースで管理。Google DriveFusion Tables(試験運用) のアプリを追加して、Google API Console で有効化すると、Google Apps Script のコードから呼び出せるようになる。Google Fusion Tables は試験運用中らしく、今後どうなるかちょっと不安なところはあるけど、とりあえず気にしない。

ソースコードの紹介

GitHub に格納しています。

Google Apps Script(.gs ファイル)で実装してます。Google Apps Script を使った LINE Bot 用の基本的なコードについては、前回の記事も参照。

位置情報から、近くの神社を検索する

今回の LINE Bot で最も重要な部分です。送信された位置情報から、近くにある神社の情報を取得するための、getNearShrines() という関数を作りました。このコードでは、Yahoo!ローカルサーチAPI の仕様に従って、指定した 緯度 (latitude) と経度 (longitude) から、半径 3 km 以内にある「神社」(業種コード: 0423002)を最大 5 件、近い順に検索するための HTTP リクエストを投げています。結果は JSON 形式でもらうように指定してます。なお、事前に Yahoo! の アプリ ID を取得しておく必要があります。この Web API のレスポンスとして、神社の名前・住所・緯度・経度などを取得できます。

var YAHOO_APP_ID = 'XXXXX' // Yahoo! のアプリ ID
var YAHOO_SEARCH_URL = 'https://map.yahooapis.jp/search/local/V1/localSearch';

function getNearShrines(latitude, longitude) {
  var url = YAHOO_SEARCH_URL
          + '?appid=' + YAHOO_APP_ID
          + '&dist=3'     // 3 km 以内
          + '&gc=0424002' // 業種コード: 神社
          + '&results=5'  // 最大 5 件
          + '&lat=' + latitude
          + '&lon=' + longitude
          + '&output=json&sort=dist';
  var response = UrlFetchApp.fetch(url);
  
  var shrines = [];
  var features = JSON.parse(response.getContentText('UTF-8'))['Feature'];
  for (i = 0; i < features.length; i++) {
    var uid = features[i]['Property'].Uid;         // 場所の ID
    var name = features[i].Name;                   // 場所の名前
    var address = features[i]['Property'].Address; // 場所の住所
    var coords = features[i]['Geometry'].Coordinates.split(',');
    var shrine_longitude = coords[0];              // 経度
    var shrine_latitude = coords[1];               // 緯度

    // 神社の情報を配列に入れる処理

  }
  return shrines;
}

2 点の場所の距離を求める

Yahoo!API を使うことによって、近くの神社の緯度・経度などの情報が取れました。ほんとはこの中に、検索した場所からの距離が含まれていてほしかったのですが、どうもこの情報は含まれてない。そこで、検索した場所から、取得した神社の場所までの直線距離を、2点間距離API(これも Yahoo! 提供)を使って求めました。…まあ、ほんとうは道なり距離が知りたかったんですけどね。ぱっと探した感じだと、無料でたくさんリクエストできる API が見つかりませんでした。直線距離でも、ないよりはマシな情報かな、と。

コードは下記のような感じ。2 つの 緯度・経度 から、直線距離(地球の楕円体に合わせた距離)を求めます。パラメータ coordinates に、「coordinates=139.73091159286,35.665662327613 135.49513388889,34.701974166667」のように、2 つの位置情報をスペースで繋げて渡すみたいなんですが、当然といえば当然ながら、パーセントエンコードしないとうまく動きません(ハマりました)。Google Apps Script 標準の関数を使って、encodeURIComponent(' ') とやってます(「%20」と直に書いてももちろん OK)。最後の Math.round(distance * 10) / 10 は、小数点以下 1 桁の距離 (km) にするためです。

function getDistanceInKilloMeters(latitude1, longitude1, latitude2, longitude2) {
  var url = YAHOO_DIST_URL
          + '?appid=' + YAHOO_APP_ID
          + '&coordinates=' + longitude1 + ',' + latitude1 + encodeURIComponent(' ') + longitude2 + ',' + latitude2
          + '&output=json';
  var response = UrlFetchApp.fetch(url);
  var distance = JSON.parse(response.getContentText('UTF-8'))['Feature'][0]['Geometry'].Distance;
  return Math.round(distance * 10) / 10;
}

Google 検索の URL と Google マップのルート検索の URL

Google 検索の URL は、次のコードで生成しています。https://www.google.co.jp/search という URL に、パラメータ q を渡します。検索する文字列がマルチバイト文字の場合は、「入力文字のエンコードUTF-8 ですよ」と指定する ie=UTF-8 もつけます(説明として正確ではないかも)。

function getGoogleSearchUrl(query) {
  return 'https://www.google.co.jp/search?q=' + encodeURIComponent(query) + '&ie=UTF-8';
}


Google マップのルート検索の URL は、次のコードで生成しています。http://maps.google.com/maps という URL に、2 つの緯度経度を saddr と daddr で渡します。dirflg=w をつけると、徒歩でのルート検索になります。ただ、このへんの URL 生成は、やってみたらできた、って感じなので、そのうち動かなくなるかもしれません。…いやー、しっかし、このような URL にアクセスするだけで一瞬でルートを示してくれるって、かなり凄いことだと思います。

function getGoogleMapRouteUrl(srcLatitude, srcLongitude, destLatitude, destLongitude) {
  return 'http://maps.google.com/maps'
         + '?saddr=' + srcLatitude + ',' + srcLongitude
         + '&daddr=' + destLatitude + ',' + destLongitude
         + '&dirflg=w';
}

ルーセル型のテンプレートメッセージと、ポストバック機能

横にスライドするやつです(下記画像のようなかんじ)。LINE APIリファレンスはこちら

f:id:takagusu:20170307194014j:plain


リファレンスに書いてあるとおりに送れば実現できるのですが、リファレンスは、なんだかリンク飛ばされまくって、どこに何をいれたらいいのかわかりづらいと感じます。結果的に、下記のような JSON を送ればカルーセル型のメッセージになりました。

// ユーザーがタップするボタンになるもの. 最大 3 個まで作成可能.
var actions = [
  {
    'type': 'postback',          // ポストバックの場合は postback
    'label': '行ったことがある',
    'data': 'action=visited&uid=<場所のID>'  // ポストバックする任意の文字列
  },
  {
    'type': 'uri',               // リンクの場合は uri
    'label': 'この神社を検索',
    'uri': googleSearchUrl       // 神社の 検索 URL
  },
  {
    'type': 'uri',               // リンクの場合は uri
    'label': 'ここからのルート',
    'uri': googleMapRouteUrl     // 神社の ルート検索 URL
  }
];

// 横にスライドされる 1 単位. 最大 5 個まで作成可能.
var columns = [
  {
    'title': title,
    'text': 'ここから ' + distance + 'km ― ' + address,
    'actions': actions
  },
  
  ...

]

// ユーザーに返信するメッセージ. 最大 5 個まで作成可能.
var messages = [
  {
    'type': 'template',
    'altText': '代替のテキストメッセージ',  // 通知窓なんかにもこれが表示される
    'template': {
      'type': 'carousel', // カルーセル型のテンプレートメッセージの指定
      'columns': columns
    }
  }
];


アクションの type に postback を指定した場合、ユーザーがそれをタップすると、サーバーにポストバックが送信されます。今回、この機能を使って、ユーザーが行ったことがある神社を記録する処理に繋げてます。

送信されてきたデータを JSON に変換して(変数名 json)、json.events[0].postback.data でポストバックデータを取得できます。データの設計・使い方は自由です。この LINE Bot では、「action=visited&uid=<場所のID>」という文字列を渡して、これを受け取ってデータベースを操作するようにしています。

var LINE_BOT_CHANNEL_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty('LINE_BOT_CHANNEL_ACCESS_TOKEN'); // Google Apps Script の [プロジェクトのプロパティ] > [スクリプトのプロパティ] で値を設定
var LINE_REPLY_URL = 'https://api.line.me/v2/bot/message/reply';


function doPost(e) {
  var json = JSON.parse(e.postData.contents);

  // 中略

  if ('postback' == json.events[0].type) {
    var data = json.events[0].postback.data;

    // 送信されてきたデータを使った処理
    // 今回の場合、「visited」or「unvisited」の文字列と、神社の場所 ID が送信される。

    messages = [{'type': 'text', 'text': '行ったことがある神社を更新しました'}]; 
  }

  UrlFetchApp.fetch(LINE_REPLY_URL, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + LINE_BOT_CHANNEL_ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': messages,
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}

Google Apps Script でデータベースを利用する

Google Apps Script で無料で簡単に使えるデータベース Google Fusion Tables を使って、ユーザーごとの神社参拝記録を行うための、ごく簡単なデータベースを作りました。

上の方でも少し書きましたが、Google DriveFusion Tables(試験運用) のアプリを追加して、Google API Console で有効化すると、Google Apps Script のコードから呼び出せるようになります。

まず、Google Drive で任意の場所に Fusion Tables のファイルを作成します。そしてこれを開いて、あらかじめ、カラムを定義しておきます。下記のような感じです(※これはデータがいくつか入ってるとき)。Text 型とか Date 型とか使えます。ちなみに、Location 型という特殊な型もあって、これを使うと地図上に簡単にマッピングできるらしいです(今回使ってませんが)。

f:id:takagusu:20170308221114p:plain


さて、こうして作ったデータベースに Google Apps Script からアクセスするには、下記のようにします。あんまり綺麗じゃないですが(undefined で判断してるとことか特に…)、「場所の ID」(uid) と「ユーザー ID」(userId) と「参拝の有無」(visited, 1 or 0) が引数で、指定した docId の Fusion Tables データベースに対して、select や insert、update の操作を行っています。既に「場所の ID」と「ユーザー ID」の組み合わせがある場合には、その rowId を取得して、これを使って update を実行します(rowId を指定しないと update ができないという、少し特殊なつくりになってます)。「場所の ID」と「ユーザー ID」の組み合わせがない場合には、新規に行を insert します。

// Google Apps Script の [プロジェクトのプロパティ] > [スクリプトのプロパティ] で値を設定
var GOOGLE_FUSION_TABLES_DOC_ID = PropertiesService.getScriptProperties().getProperty('GOOGLE_FUSION_TABLES_DOC_ID');

function changeVisited(uid, userId, visited) {
  var sql = "SELECT ROWID FROM " + GOOGLE_FUSION_TABLES_DOC_ID
          + " WHERE uid = '" + uid + "' and userId = '" + userId + "'";
  var result = FusionTables.Query.sqlGet(sql);
  if (typeof result.rows === 'undefined') {
    sql = "INSERT INTO " + GOOGLE_FUSION_TABLES_DOC_ID
        + " (uid, userId, visited)"
        + " VALUES ('" + uid + "', '" + userId + "', " + visited + ")";
    FusionTables.Query.sql(sql); 
  } else {
    var rowid = result.rows[0];
    sql = "UPDATE " + GOOGLE_FUSION_TABLES_DOC_ID
        + " SET visited = " + visited
        + " WHERE ROWID = '" + rowid + "'";
    FusionTables.Query.sql(sql);
  }
}

簡単なデータベースであれば、この Fusion Tables で十分ですね。


長くなってしまいましたが、とりあえずソースコードの紹介はこんな感じで。
最終的な .gs ファイルは、GitHub に格納しているので、よければ参考にしてください(あんまりいいコードじゃないですが)。

では。