JSDeferredで,面倒な非同期処理とサヨナラ

第2回 JSDeferredを用いたアプリケーション開発(その1)

この記事を読むのに必要な時間:およそ 4.5 分

各項目の内容をそれぞれループで取得し,緯度・経度情報を抜きだす

さて,ここからさらに,各記事のタイトルから緯度経度情報を抜きだすわけですが,その際使うAPIは以下のようになります。

この場合のデータ構造は以下の通りです。

リスト9

{
  "query": {
    "pages": {
      "49366": {
        "title": "賀茂別雷神社",
        "ns": 0,
        "revisions": [
          {
            "*": " .... 内容 .... "
          }
        ],
        "pageid": 49366
      },
      "458561": {
        "title": "松尾大社",
        "ns": 0,
        "revisions": [
          {
            "*": " .... 内容 .... "
          }
        ],
        "pageid": 458561
      }
  }
}

若干かえってくる構造がわかりにくいです。それはともかく,このAPI,複数のページタイトルをパイプで渡せるのはいいのですが,あまりたくさん渡しても全てキッチリは返してくれない※1ため,適当な数で分割してアクセスしていきます。このようなとき,JSDeferredだと大変簡単にアクセスしていけます。

※1
ドキュメントには記載されていないが,調べたところ35程度。

リスト10

var url = "http://ja.wikipedia.org/w/api.php?format=json&callback=?&action=query&list=categorymembers&cmlimit=500&cmtitle=" + encodeURIComponent(category);
$.getJSON(url).
next(function (data) {
  var members = data.query.categorymembers;
  return Deferred.loop({ end : members.length - 1, step : 35 }, function (n, o) {
    var q = [];
    for (var i = 0; i< o.step; i++) {
      q.push(encodeURIComponent(members[n + i].title));
    }
    return $.getJSON("http://ja.wikipedia.org/w/api.php?format=json&callback=?&action=query&prop=revisions&rvprop=content&titles=" + q.join("|")).
    next(function (res) {
      for (var key in res.query.pages) if (res.query.pages.hasOwnProperty(key)) {
        var page = res.query.pages[key];
        page.revisions[0]["*"].replace(/\{\{ウィキ座標2段度分秒\|(\d+)\|(\d+)\|(\d+)\|N\|(\d+)\|(\d+)\|(\d+)\|E/g, function (_, lat1, lat2, lat3, lng1, lng2, lng3) {
          alert([
            page.title,
            +lat1 + (+lat2 / 60) + (+lat3 / 60 / 60),
            +lng1 + (+lng2 / 60) + (+lng3 / 60 / 60)
          ]);
        });
      }
    });
  });
}).
error(function (e) {
  alert(e);
});

Deferred.loop という関数は前回もでてきましたが,これによってJSONPの非同期処理をループさせています。Deferred.loop はいろいろな引数のとりかたをできますが,例のようにすると end まで step 個ずつ要素をループさせるような処理を書くことができます。ここでは35個ずつに分割して記事をそれぞれ一括でとってくるために,このようなループ構成になっています。

ループ内部では実際にAPIに投げるクエリを構築し,JSONPリクエストを出し,$.getJSON の返すもの,すなわち Deferred オブジェクトを return しています。Deferred.loop() 関数はループ関数が Deferred オブジェクトを返した場合,その実行を待つという特徴があるため,これにより,

  1. 35ずつ
  2. 順番に
  3. 任意の個数の記事を全て

取得することができています。直接緯度経度を取得する方法は著者が確認した限りなかったため,本文中のテンプレートから正規表現で抜きだしてきます。ちなみに1つのページに複数の項目が混ざっていることもあるため,複数対応するために,置換するわけではないですが replace() を使っています。

Google Mapにプロットする

データはひとまず用意できたのでGoogle Mapにプロットしてみます。と,その前に,今まで書いた内容を関数にまとめておきます。

リスト11

function CollectLatLongFromCategory (category, callback) {
  var url = "http://ja.wikipedia.org/w/api.php?format=json&callback=?&action=query&list=categorymembers&cmlimit=500&cmtitle=" + encodeURIComponent(category);
  return $.getJSON(url).
  next(function (data) {
    var members = data.query.categorymembers;
    return loop({ end : members.length - 1, step : 15 }, function (n, o) {
      var q = [];
      for (var i = 0; i< o.step; i++) {
        q.push(encodeURIComponent(members[n + i].title));
      }
      return $.getJSON("http://ja.wikipedia.org/w/api.php?format=json&callback=?&action=query&prop=revisions&rvprop=content&titles=" + q.join("|")).
      next(function (res) {
        for (var key in res.query.pages) if (res.query.pages.hasOwnProperty(key)) {
          var page = res.query.pages[key];
          page.revisions[0]["*"].replace(/\{\{ウィキ座標2段度分秒\|(\d+)\|(\d+)\|(\d+)\|N\|(\d+)\|(\d+)\|(\d+)\|E/g, function (_, lat1, lat2, lat3, lng1, lng2, lng3) {
            callback({
              title: page.title,
              lat : +lat1 + (+lat2 / 60) + (+lat3 / 60 / 60),
              lng : +lng1 + (+lng2 / 60) + (+lng3 / 60 / 60)
            });
          });
        }
      });
    });
  }).
  error(function (e) {
    alert(e);
  });
}

リクエストが返ってくる度にマップを更新したいため,callback をとるようにしてみました。ついでに全てが終わったときからの処理を継続したいため,Deferred を返しています。

リスト12

$(function () {
  var map = ... Google Map 初期化 ...
  CollectLatLongFromCategory(category, function (obj) {
    var point  = new GLatLng(obj.lat, obj.lng);
    var marker = new GMarker(point, icon);
    map.addOverlay(marker);
  }).
  next(function () {
    alert('done');
  }).
  error(function (e) {
    alert(e);
  });
});

Google Mapの初期化などの解説は省略しましたが,特に説明する部分もなく,単にマーカーを立てているだけです。CollectLatLongFromCategory が Deferred を返すため,そのまま繋げて終わったときに done と alert させています。

さて,一通りできましたね。次回はキャッシュの仕組みを入れるのと,geolocation を例に既存APIとの繋ぎこみなどを説明したいと思います。

著者プロフィール

cho45(さとう)

はてなエンジニア。サブテカ。バックエンドからインターフェイスまで,Perl,Ruby,JavaScript,ActionScriptなどを使いつつ Scala,Ioなどを触る,言語・コード表現ヲタク。

URLhttp://www.lowreal.net/
技術ネタhttp://subtech.g.hatena.ne.jp/cho45/