エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Evernote のノートのタイトルに住所を付け加える

先日、Evernote Food のサポートが終了するので… - エンジニアのソフトウェア的愛情 という記事を書きました。その最後に

同じ要領で notes[i].longitude(), notes[i].latitude() を使えば位置情報を取得できるので、住所を設定することもできると思います。

と無責任に書いたのですが、よくよく調べてみるとそこそこ難しい。技術的に難しいというよりも必要な情報を見つけるのが難しい。


と、いうわけで。位置情報から住所を取得するため四苦八苦した結果を記録しておきます。
なお、検索と試行錯誤を繰り返した結果でして、仕様の裏が取れているわけではありませんので、その点はご容赦を。


技術情報

緯度経度から住所を取得するには…

今回は Google Maps Geocoding API を利用しました。

クエリの latlng に緯度経度を指定して次の URL に GET でリクエストすると JSON 形式で結果を取得することができます。

  • http://maps.google.com/maps/api/geocode/json?latlng=緯度,経度


また language を指定すると、指定した言語表記で結果を取得することができます。


例)北緯24度26分16.75104秒(24.4379864度)、東経123度0分38.96604秒(123.0108239度)の住所

$ curl 'http://maps.google.com/maps/api/geocode/json?latlng=24.4379864,123.0108239&language=ja'
{
   "results" : [
      {
         "address_components" : [
            {
               "long_name" : "3024",
               "short_name" : "3024",
               "types" : [ "sublocality_level_4", "sublocality", "political" ]
            },
            {
               "long_name" : "与那国",
               "short_name" : "与那国",
               "types" : [ "sublocality_level_1", "sublocality", "political" ]
            },
            {
               "long_name" : "与那国町",
               "short_name" : "与那国町",
               "types" : [ "locality", "political" ]
            },
            {
               "long_name" : "八重山郡",
               "short_name" : "八重山郡",
               "types" : [ "colloquial_area", "locality", "political" ]
            },
            {
               "long_name" : "沖縄県",
               "short_name" : "沖縄県",
               "types" : [ "administrative_area_level_1", "political" ]
            },
            {
               "long_name" : "日本",
               "short_name" : "JP",
               "types" : [ "country", "political" ]
            },
            {
               "long_name" : "907-1801",
               "short_name" : "907-1801",
               "types" : [ "postal_code" ]
            }
         ],
         "formatted_address" : "日本, 〒907-1801 沖縄県八重山郡与那国町与那国3024",
         "geometry" : {
            "location" : {
               "lat" : 24.4443869,
               "lng" : 122.9841723
            },
            "location_type" : "ROOFTOP",
            "viewport" : {
               "northeast" : {
                  "lat" : 24.4457358802915,
                  "lng" : 122.9855212802915
               },
               "southwest" : {
                  "lat" : 24.44303791970849,
                  "lng" : 122.9828233197085
               }
            }
         },
         "place_id" : "ChIJOZRCOXdOZzQRyEH9ltU1-b0",
         "types" : [ "sublocality_level_4", "sublocality", "political" ]
      },

      ... 以下略
指定したURLからコンテンツを取得するには…

今回は Foundation Framework の各機能を利用しています。


取得の手順はこう。

  • 取得する URL の NSURL オブジェクトを用意する
  • NSDatadataWithContentsOfURL: メソッドでデータを取得する
  • NSStringinitWithData:encoding: メソッドで取得したデータを文字列(NSString オブジェクト)に変換する
  • NSStringcStringUsingEncoding: メソッドで C 言語の文字列(文字配列)に変換する


これをふまえて実装してみます。

#import <Foundation/Foundation.h>
#import <stdio.h>

int main(int argc, char **argv)
{
  NSURL      *url     = [NSURL URLWithString:@"http://maps.google.com/maps/api/geocode/json?latlng=24.4379864,123.0108239&language=ja"];
  NSData     *data    = [NSData dataWithContentsOfURL:url];
  NSString   *string  = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  const char *cstring = [string cStringUsingEncoding:NSUTF8StringEncoding];

  printf("%s\n", cstring);

  return 0;
}


geocoding_sample.m というファイル名で保存してコンパイルして実行します。

$ clang -o geocoding_sample geocoding_sample.m -framework Foundation
$ ./geocoding_sample
{
   "results" : [
      {
         "address_components" : [
            {
               "long_name" : "3024",
               "short_name" : "3024",
               "types" : [ "sublocality_level_4", "sublocality", "political" ]
            },
            {
               "long_name" : "与那国",
               "short_name" : "与那国",
               "types" : [ "sublocality_level_1", "sublocality", "political" ]
            },

            ... 以下略
それを JavaScript で利用するには…

今回、緯度経度から住所を取得したいのは、EvernoteJavaScript で操作して住所を追加したいというのが動機でした。
ここまで Evernote の操作には JavaScript を利用しています。
と、いうわけで。上記の住所取得操作を JavaScript に書き直します。このあたりはかなり手探りの情報です。

osascript をインタラクティヴモードで起動しつつ話を進めます。

$ osascript -i -l JavaScript
>> 


>> がプロンプトです。以下、プロンプトから始まる行はインタラクティヴモードで入力した文字列、=> から始まる行がその実行結果になります。

>> 1 + 1
=> 2
>>
クラス

NSURL, NSData, NSString といったクラスはすべて $ オブジェクトの要素として参照できます。

>> $.NSURL
=> $.NSURL
>> $.NSData
=> $.NSData
>> $.NSString
=> $.NSString
>> $
=> [function $] {
  "name":"", 
  "prototype":{"constructor":[function $]}, 
  "NSURL":$.NSURL, 
  "NSString":$.NSString, 
  "NSData":$.NSData
}
>>

少なくとも undefined などではなく、参照できていることがわかります。

メソッド

メソッドの呼び出しは、普通に JavaScript の構文の世界です。
オブジェクトに続いてドットでメソッド名をつけ、引数は括弧でくくります。

>> url = $.NSURL.URLWithString('http://maps.google.com/maps/api/geocode/json?latlng=24.4379864,123.0108239&language=ja') 
=> [id NSURL]
>> data = $.NSData.dataWithContentsOfURL(url)
=> [id _NSInlineData]


問題は initWithData:encoding: のような複数の引数を持つ場合です。
確実な仕様を見つけられなかったのですが、このような場合はセレクタを連結してキャメルケースにしたメソッドにマップされているようです。
つまり、

[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]

これがこうなります。

$.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding)

なお、NSUTF8StringEncoding のような定数も $ の要素として格納されているようです。

>> string = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding)
=> $("{\n   \"results\" : [\n      {\n         \"address_components\" : [\n            {\n               \"long_name\" : \"3024\", ... 以下略

Evernote のノートのタイトルに住所を追加する

これらをふまえて。Evernote のノートのタイトルに住所を追加するスクリプトを書いてみます。
タイトルに単に住所を付け加える処理だけでは、繰り返し実行するたびに住所が付け加えられてしまいます。
ですので、タイトルを更新した時に更新済みタグを付けておき、検索の時にそのタグが付いているノートを対象外にするようにしました。

var getAddress = function(latitude, longitude) {
  var url     = $.NSURL.URLWithString('http://maps.google.com/maps/api/geocode/json?latlng=' + latitude + ',' + longitude + '&language=ja');
  var data    = $.NSData.dataWithContentsOfURL(url);
  var string  = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding);
  var cstring = string.cStringUsingEncoding($.NSUTF8StringEncoding);
  var json    = JSON.parse(cstring)

  if(json.results.length > 0) {
    // ここでレスポンスから適当に住所を組み立ててください
    var long_names = [];
    for(var i in json.results[0].address_components) {
      long_names.unshift(json.results[0].address_components[i].long_name);
    }
    return long_names.join(' ')
  } else {
    return '';
  }
}

// Evernote オブジェクトを取得する
var evernote = Application('Evernote');

// food タグが付いていて、pinpointed タグが付いていないノートを検索する
var notes = evernote.findNotes('tag:food -tag:pinpointed');

// pinpointed タグ オブジェクトを取得する
var pinpointed_tag = evernote.tags['pinpointed']

// pinpointed タグ が存在しなければタグを作成する
if( ! pinpointed_tag.exists()) {
  pinpointed_tag = evernote.make({new: 'tag', withProperties: {name: 'pinpointed'}})
}

for(var i in notes) {
  var latitude  = notes[i].latitude();
  var longitude = notes[i].longitude();
  var address   = getAddress(latitude, longitude);

  if(address != '') {
    var title = notes[i].title();
    console.log(title);

    // タイトルに住所を付け加える
    notes[i].title = title + ' @' + address;

    // pinpointed タグを付ける
    evernote.assign(pinpointed_tag, {to: notes[i]});
  }
}


適当なファイル名で保存し、osascript コマンドで実行するとタイトルに住所が付け加えられます。


いつか読むはずっと読まない:過去からの一撃!!

「楽園通信社綺談 ビブリオテーク・リヴ」の復刻に続いて驚きの展開。「驚きの展開」とか言ったら怒られますが。

リプライズ

リプライズ

楽園通信社綺談 ビブリオテーク・リヴ

楽園通信社綺談 ビブリオテーク・リヴ

パラダイスバード (BUNCH COMICS)

パラダイスバード (BUNCH COMICS)