« 失敗しない Rails が動かせるホスティングサービス選びと環境構築 | メイン

Adobe AIRでAjax!(その2) AIR APIを利用してWebとLocalを繋ぐマッシュアップ サンプルはてなブックマークに追加 livedoorクリップに追加 Yahoo!ブックマークに追加 del.icio.usに追加 イザ!ブックマーク ニフティクリップに追加

こんにちは、JavaScript担当の(株)アークウェブの竹村です。

前回はAdobe AIRの制作環境構築と、ユーザに配布するまでの流れを説明しました。

今回もマッシュアップサービスを利用しつつ、AIR APIをいくつか使ったサンプルを用意しました。

ホットペッパーのマッシュアップサンプル『オヒルダ!』

ホットペッパーのAPIに「ランチ」というフラグがあるのに着目し、ランチを提供しているショップリストを表示するサンプルです。ショップ検索をする際に、予め自分の住所や
指定した緯度経度などを設定できるようにしています。

オヒルダ!の構成は↓このようになります。

01organize.jpg

最初にWeb版を作ります。Web版には下記の機能を盛り込みます。

w-1. ホットペッパーAPIを通して検索結果一覧を表示する
w-2. 検索で使用する地域/ジャンルを設定できるようにする
w-3. 設定をAIRアプリ用にエクスポートできるようにする

AIR版には下記の機能を盛り込みます。

a-1. ホットペッパーAPIを通して検索結果一覧を表示する
a-2. Web版の設定をインポートできるようにする

今回利用するAIR APIは『ローカルファイル選択ダイアログの表示』と『ファイルの内容の読み取り』と『SQLiteへのアクセス』の 3点です。

完成版は↓こちらからアクセスできます。
オヒルダ! (Web版)
02image.jpg
オヒルダ! AIR版 インストーラー


今回のレジュメは↓このようになっています。

  • w-1. ホットペッパーAPIを通して検索結果一覧を表示する
  • w-2. 検索で使用する地域/ジャンルを設定できるようにする
  • w-2-1. GoogleMapsで住所から緯度/経度を取得する
  • w-2-2. GoogleMapsで円を描く
  • w-2-3. 設定内容を保存する
  • w-3. 設定をAIRアプリ用にエクスポートできるようにする
  • a-1. ホットペッパーAPIを通して検索結果一覧を表示する
  • a-2. Web版の設定をインポートできるようにする
  • a-2-1. ローカルファイル選択ダイアログを表示して選ばせる
  • a-2-2. ファイルを開いて内容を読み込む
  • a-2-3. SQLiteに設定を保存する
  • a. SQLiteから設定を読み込むには
  • オヒルダ!AIR版のソース一式ダウンロード
  • まとめ

w-1. ホットペッパーAPIを通して検索結果一覧を表示する (リスト表示)

まずはWeb版の実装を行います。
ホットペッパーAPIはJSONPアクセスが可能なので、これを使いましょう。

JSONPアクセスは、いつものように JSONscriptRequest (jsr_class.js) を利用して表示しています。

<script type="text/javascript" src="js/jsr_class.js"></script> <script type="text/javascript" src="js/prototype.js"></script> (...) <script type="text/javascript"> // グローバル変数 var goJsr4Listing; // JSONscriptRequestオブジェクトのインスタンス var gsHotpepperAPIListingURL = 'http://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key=[APIキー]'; (...) function sendListing() { var aArea = getCheckedValues(document.form.area_select); var sAddress = $F('address'); var iLat = $F('lat'); var iLng = $F('lng'); var iRange = $F('range'); var iLunch = $F('lunch'); var aGenre = getCheckedValues(document.form.genre); var sUrl = gsHotpepperAPIListingURL; sUrl += (aArea[0] == 'address' ? '&address='+ sAddress : '&lat='+ iLat +'&lng='+ iLng) + '&range=' +iRange; sUrl += '&format=jsonp&callback=listingCallback&count=10'; sUrl += (iLunch == 1 ? '&lunch=1' : ''); sUrl += (aGenre.length > 0 ? '&genre='+ aGenre.join(',') : ''); // script タグの発行 goJsr4Listing = new JSONscriptRequest(sUrl); goJsr4Listing.buildScriptTag(); goJsr4Listing.addScriptTag(); (...) } function listingCallback(oJson) { // script タグの削除 goJsr4Listing.removeScriptTag(); // テンプレートから内容を生成して出力する var aItem = oJson['results']['shop']; if (aItem.length > 0) { makeContents($('listing'), 'listing', aItem); } else { $('listing').innerHTML = '1件もありませんでした。。'; } } // === 共通関数 ===== function makeContents(oOutput, sPath, aItem) { new Ajax.Request( getTemplate(sPath), {onComplete:function(oResponse){ makeContentsCallback(oOutput, aItem, oResponse); }} ); } function getTemplate(sPath) { switch (sPath) { case 'listing': return 'templates/listing.html'; default: alert('sPath未指定 by getTemplate'); } } function makeContentsCallback(oOutput, aItem, oResponse) { var sTemplate = oResponse.responseText; var sContents = ''; for (var i = 0 ; i < aItem.length ; i++) { sContents += sTemplate.interpolate(aItem[i]); } oOutput.innerHTML = sContents; } (...) <div id="listing-area"> <p>■一覧表示</p> <div id="listing"></div> </div>

sUrlに渡しているパラメータは直前にフォームからデータを取得してます。

JSONPで戻ってくる listingCallback関数データが1件でもあれば、内容を表示します。内容の表示方法は、前回利用したAjax.Requestを使って外部のテンプレートファイルを読み込み、内容をinterpolateメソッドで埋め込んで出力しています。

この辺は、マッシュアップの要領を得ていれば結構楽に実装できると思います。


ちなみに、一覧で出しているGoogleMapsの地図は動きません。これはGoogleMapsの『staticmap』を利用しています。

w-2. 検索で使用する地域/ジャンルを設定できるようにする

Web版の「設定フォーム」ページを作成します。
このページのポイントは 3つあります。

  • w-2-1. GoogleMapsで住所から緯度/経度を取得する
  • w-2-2. GoogleMapsで円を描く
  • w-2-3. 設定内容を保存する

w-2-1. GoogleMapsで住所から緯度/経度を取得する

こちらはローカルサーチという機能を利用します。早速具体的な使い方を説明します。

<script type="text/javascript" src="http://www.google.com/jsapi?key=[APIキー]"></script> <script src="http://www.google.com/uds/api?file=uds.js&v=0.1&key=[APIキー]" type="text/javascript" charset="utf-8"></script> <script type="text/javascript"> (...) // === マップ ===== // 大まかな住所からGlobalSearchでlatlngを取得してマップを描画する function moveTo(sPlace) { gls = new GlocalSearch(); gls.setSearchCompleteCallback(null, moveToPlaceCallback); gls.setCenterPoint(map); gls.execute(sPlace); } function moveToPlaceCallback() { if ( !gls.results || gls.results.length == 0 ) { alert('指定された住所は見つかりませんでした'); } else { var oGSearchResult = gls.results[0]; drawMap(new GLatLng(oGSearchResult.lat, oGSearchResult.lng)); } } (...) // マップを描画する function drawMap(oGLatLng) { map.clearOverlays(); var iRange = $F('range'); map.setCenter(oGLatLng, goRange2data[iRange]['size']); $('lat').value = oGLatLng.lat(); $('lng').value = oGLatLng.lng(); addPolygonToMap(oGLatLng, goRange2data[iRange]['radius']); }

moveTo関数の引数である sPlace には、フォームで入力された「マップの大まかな住所」が入ります。GolobalSearchクラスのインスタンスのexecuteにそのsPlaceを渡して検索するんですが、その前にイベントハンドラの設定をしています。

イベントハンドラの moveToPlaceCallback関数ではローカルサーチで得た緯度/経度の1番目を採用してマップ描画用関数に渡しています。

マップ描画用の drawMap関数では、map.setCenter でマップの位置調整をしています。

w-2-2. GoogleMapsで円を描く

GoogleMaps上に赤い円を描いている部分は、GPolygonクラスを利用しています。具体的な指定方法は下記のようにしています。

// マップを描画する function drawMap(oGLatLng) { map.clearOverlays(); var iRange = $F('range'); map.setCenter(oGLatLng, goRange2data[iRange]['size']); $('lat').value = oGLatLng.lat(); $('lng').value = oGLatLng.lng(); addPolygonToMap(oGLatLng, goRange2data[iRange]['radius']); } /** * 下記のaddPolygonToMap関数、createCircle関数は、 * http://www.ajaxtower.jp/googlemaps/gpolygon/index6.html を * 利用させていただきました. */ function addPolygonToMap(point, radius){ var latlngs = createCircle(point, radius, 32); var polygon = new GPolygon(latlngs, "#ff0000", 5, 0.5, "#0000ff", 0.1); map.addOverlay(polygon); } function createCircle(latlng, radius, level){ var point = map.fromLatLngToDivPixel(latlng); var latlngbounds = map.getBounds(); var southwest = latlngbounds.getSouthWest(); var northeast = latlngbounds.getNorthEast(); var nelat = northeast.lat(); var swlng = southwest.lng(); var lefttop = map.fromLatLngToDivPixel(new GLatLng(nelat, swlng)); var x = point.x - lefttop.x; var y = point.y - lefttop.y; var latlngs = []; for (var i = 0 ; i < level ; i++){ var px = x + radius * Math.cos(i * Math.PI/(level/ 2)); var py = y + radius * Math.sin(i * Math.PI/(level/ 2)); var latlng = map.fromContainerPixelToLatLng(new GPoint(px, py)); latlngs.push(latlng); } latlngs.push(map.fromContainerPixelToLatLng(new GPoint(x + radius, y))); return latlngs; }

上記で軽く触れた drawMap関数の addPolygonToMap関数で円を描く部分を実装しています。

こちらは下記のサイトのコードを参考にさせていただきました。

円の計算は、createCircle関数で行っています。これは元サイトのままです。createCircleではLatLngのリストを返しています。

addPolygonToMap関数の GPolygonクラスでLatLngのリストを渡して map.addOverlayで描画します。なお、再描画する際は map.clearOverlays(); を実行しておくことで以前の円を消してから新しい円を描画できます。

w-2-3. 設定内容を保存する

ページ最下部の[設定する]ボタンをクリックした時の設定内容を保存する処理は、Web版はCookieを利用します。

Cookie管理用に CookieManager というライブラリを利用しています。

<script type="text/javascript" src="js/cookiemanager.js"></script> (...) // === 保存 ===== function checkAndSave() { (...) // データのセーブ var aSettings = makeSaveSetting(); saveSetting(aSettings); alert("Cookieに保存しました。\nトップからアクセスしてみてください。"); } function saveSetting(aSettings) { // AIRかどうか判定 if (!window.runtime) { return saveSetting4web(aSettings); } else { return saveSetting4air(aSettings); } } function saveSetting4web(aSettings) { var oCookieManager = new CookieManager({shelfLife:30}); oCookieManager.setCookie("address", encodeURI(aSettings['address'])); oCookieManager.setCookie("lat", aSettings['lat']); oCookieManager.setCookie("lng", aSettings['lng']); oCookieManager.setCookie("range", aSettings['range']); oCookieManager.setCookie("genre1", aSettings['genre1']); oCookieManager.setCookie("genre2", aSettings['genre2']); oCookieManager.setCookie("genre3", aSettings['genre3']); }

saveSetting関数の if (!window.runtime) はAIRかどうかの判定をする際に利用できます。

肝心のCookieManagerクラスは saveSetting4web関数にあります。引数のshelfLifeは有効期限の日付指定です。Cookieの設定は setCookie、取得は getCookieです。

w-3. 設定をAIRアプリ用にエクスポートできるようにする

[エクスポート]ボタンをクリックするとダウンロードダイアログが表示されます。
この部分はPHPで動作させています。

<?php /** * ohirudaのCookieの内容をダウンロードさせる. */ if ($_POST['lat'] == '') { echo "Not a setting data.."; exit; } $sSetting = sprintf('ohiruda/%s,%s/%s/%s,%s,%s/%s/', $_POST['lat'], $_POST['lng'], $_POST['range'], $_POST['genre1'], $_POST['genre2'], $_POST['genre3'], $_POST['address'] ); header("Content-type: text/plain"); header('Content-disposition: attachment; filename=export4air.txt'); header("Content-length: " . strlen($sSetting)); echo $sSetting; ?>

a-1. ホットペッパーAPIを通して検索結果一覧を表示する

では、いよいよAIR版の作業に移ります。

まずはWeb版のHTMLをそのままコピーします。つくるぶの前回の記事に書きましたが、基本的に AIRアプリはWebと同様にHTML+JavaScriptで動作させることが可能です。よって、オヒルダWeb版のコピーがそのままAIRで動くように思うのですが、ここに落とし穴があります。

AIRではJSONPを実行するためのJSONscriptRequestが利用できません。つまり、scriptタグを動的に挿入できないようになっているようです。対応策としては、普通にprototype.jsの Ajax.Request を使用してホットペッパーAPIにアクセスします。

// === リスト表示 ===== function sendListing() { (...) var sUrl = gsHotpepperAPIListingURL; sUrl += (aArea[0] == 'address' ? '&address='+ sAddress : '&lat='+ iLat +'&lng='+ iLng) + '&range=' +iRange; // -> modify for AIR // sUrl += '&format=jsonp&callback=listingCallback&count=10'; sUrl += '&format=json&count=10'; // <- modify for AIR sUrl += (iLunch == 1 ? '&lunch=1' : ''); sUrl += (aGenre.length > 0 ? '&genre='+ aGenre.join(',') : ''); // -> modify for AIR /* // script タグの発行 goJsr4Listing = new JSONscriptRequest(sUrl); goJsr4Listing.buildScriptTag(); goJsr4Listing.addScriptTag(); */ new Ajax.Request( sUrl, {method:'get',onComplete:listingCallback} ); // <- modify for AIR // Nowloading $('listing').innerHTML = 'Nowloading...'; } function listingCallback(oJson) { // -> modify for AIR /* // script タグの削除 goJsr4Listing.removeScriptTag(); */ var oJson = eval('('+ oJson.responseText +')'); // <- modify for AIR (...)

AIRにはクロスドメインの制約がないので、外部ドメインへのアクセスが可能です。
ホットペッパーのAPIから JSON を取得するように変えて、読み込み完了のイベントハンドラ listingCallback関数でJSONをevalしています。

a-2. Web版の設定をインポートできるようにする

ついにAIR APIの登場です。
インポートには 3つのポイントがあります。

  • a-2-1. ローカルファイル選択ダイアログを表示して選ばせる
  • a-2-2. ファイルを開いて内容を読み込む
  • a-2-3. SQLiteに設定を保存する

a-2-1. ローカルファイル選択ダイアログを表示して選ばせる

[インポート]ボタンをクリックした時にダイアログを表示して、ユーザにファイルを指定してもらうには下記のようにします。

<script type="text/javascript" src="js/AIRAliases.js"></script> (...) // インポートファイル指定ダイアログ表示 function openImportDialog() { try { var oFile = air.File.desktopDirectory; var oFileFillter = new air.FileFilter("テキスト", "*.txt"); oFile.addEventListener(air.Event.SELECT, readedCallback); oFile.browseForOpen("開く", [oFileFillter]); } catch (event) { alert(event.toString()); } } function readedCallback(oEvent) { (...)

まず、HTML+JavaScriptでAIR APIを利用するときは AIRAliases.js をインクルードしておくとかなり見やすくなります (FlexやAS3で組むコードと大差ないほどに)
例えば、File へのアクセスについていえば、↓これくらい楽です。

本来の書き方:var oFile = window.runtime.flash.filesystem.File.desctopDirectory;
AIRAliases :var oFile = air.File.desctopDirectory;

それで、ダイアログ表示は Fileクラスの browseForOpenメソッドを実行すれば開きます。その前に air.File.desktopDirectory で開く場所を指定しています。

desktopDirectoryはユーザのデスクトップ、つまりWindowsであれば↓こちらのフォルダを指しています。

C:\Documents and Settings\[ユーザ名]\デスクトップ

あとは、FileFilterクラスで *.txt のみダイアログ中に表示するように指定しています。また、ダイアログからファイルが決定された時の動作を addEventListenerで readedCallback関数をイベントハンドラとして登録しています。

a-2-2. ファイルを開いて内容を読み込む

ローカルファイル選択ダイアログをでユーザがファイルを指定した時のイベントハンドラ readedCallback関数で、ファイルの内容を取得しています。

function readedCallback(oEvent) { try { var oFile = oEvent.target; var oFileStream = new air.FileStream(); oFileStream.open(oFile, air.FileMode.READ); var sReadData = oFileStream.readMultiByte(oFile.size, "utf-8"); oFileStream.close(); } catch (event) { alert(event.toString()); } return saveImportData(sReadData); }

そのファイルの内容にアクセスするには、FileStreamクラスを利用します。openして、readMultiByteメソッドで内容を読み込み、closeしています。

a-2-3. SQLiteに設定を保存する

最後に読み込んだ内容をSQLiteに保存します。

var db = null; // データの初期化 function initSetting4air() { try { // DBに接続 db = new air.SQLConnection(); var file = air.File.applicationStorageDirectory.resolvePath("ohiruda.db"); db.open(file); // テーブルを用意 (なければ作る) var oSQLStatement = new air.SQLStatement(); oSQLStatement.sqlConnection = db; oSQLStatement.text = "CREATE TABLE IF NOT EXISTS setting" + "(address TEXT, lat REAL, lng REAL, range INTEGER," + " genre1 TEXT, genre2 TEXT, genre3 TEXT);"; oSQLStatement.execute(); } catch(error) { alert(error.toString()); } } (...) // データ書き込み (DBを使います) function saveSetting4air(aSettings) { try { // 一旦設定を削除して、登録する oSQLStatement.text = "DELETE FROM setting"; oSQLStatement.execute(); oSQLStatement.text = "INSERT INTO setting VALUES (".interpolate(aSettings) + "'#{address}','#{lat}','#{lng}','#{range}',".interpolate(aSettings) + "'#{genre1}','#{genre2}','#{genre3}');".interpolate(aSettings); oSQLStatement.execute(); } catch(error) { alert(error.toString()); } }

SQLiteはファイルにDBの内容を書き込むので、Fileクラスで書き込む先を指定しています。

applicationStorageDirectoryはWindowsであれば↓こちらのフォルダを指しています。

C:\Documents and Settings\[ユーザ名]\Application Data\[ID値]\Local Store\
※ID値はアプリケーション定義ファイルに記述したidタグの値です。

FireFoxのエクステンションを利用するとSQLiteの内容を確認できます。

FireFoxから、↑こちらのエクステンションをインストールして、ツールから「SQLite Manager」を開き、「Connect Database」から上記ディレクトリにあるohiruda.dbを選ぶと内容が確認できます。

03sqlitemanager.jpg

インポートすると、上記のようなデータが 1件だけ表示されるはずです。
※英語版なので日本語でインポートした部分が文字化けしています。


DBへの接続は SQLConnectionクラスの openメソッドでできます。openメソッドは同期モードですので、DBへ接続できるまで処理が止まります。この間、ユーザは何もできません。砂時計アイコンで出ます。
(実際はローカルのファイルへのアクセスなので砂時計アイコンが出る間もなく接続できます)

一方、openAsyncメソッドという非同期モードも用意されています。こちらはDB接続中でもユーザは他の操作ができます。ただ、非同期モードはイベントハンドラを設定して内容の取得を行うので、ちょっと面倒です。

openAsyncの使いどころは、DBのサイズが結構大きくなった時や、複雑な検索をかける時になると思います。

テーブルの操作は、SQLStatementクラスのexecuteメソッドで行います。
その前に oSQLStatement.text にSQL文を入れておく必要があります。また、 oSQLStatement.sqlConnectionに接続したSQLConnectionクラスのインスタンスを入れておかないと下記のエラーが表示されてしまいます。

Error: Error #3109: Operation is not permitted when the SQLStatement.sqlConnection property is not set.

初期化処理中に、DBへの接続と「CREATE TABLE IF NOT EXISTS setting」でテーブルがなかったら用意するSQLを実行しておきます。

saveSetting4air関数で、settingテーブルの内容を一旦削除して、「INSERT INTO setting」でインポートによって取得した内容を書き込んでいます。

※英語版なので日本語が文字化けします。

a. SQLiteから設定を読み込むには

データを読み込む時は、initSetting4air関数を実行した後に下記のように取得します。

// データ読み込み (DBを使います) function loadSetting4air() { try { // 設定を読み込む oSQLStatement = new air.SQLStatement(); oSQLStatement.sqlConnection = db; oSQLStatement.text = "SELECT COUNT(*) as cnt FROM setting"; oSQLStatement.execute(); var oSQLResult = oSQLStatement.getResult(); var iCount = oSQLResult.data[0]['cnt']; oSQLStatement.text = "SELECT * FROM setting"; oSQLStatement.execute(); // 読み込んだ内容の取得 var oSQLResult = oSQLStatement.getResult(); } catch(error) { alert(error.toString()); }

データの件数と、その内容を取得するのに2回SQL文を発行して、内容を取得しました。内容の取得は SQLStatementクラスの getResultメソッドで取得できます。


あとは、AIR版のトップも同じようにSQLiteから取得したデータをもとにホットペッパーのAPIにアクセスするパラメータとして利用しています。

= ちょっとTips =
トップの一覧のGoogleMaps staticmapのアクセス先がAIR版では http://okra.ark-web.jp/~takemura/... になっています。これはGoogleMapsのAPIキーにAPIを利用するドメインの指定をする必要があるため、AIRでは直接利用できないようだったので、その回避策です。

オヒルダ!AIR版のソース一式ダウンロード

AIR版のソースを一式下記にまとめました。

まとめ

今回はHTML+JavaScriptでAIR APIを利用したサンプルを作ってみましたが、AIR API自体は大して難しいことはないと思います。

ただ、GoogleMapsがAIR上で利用できなかったり、AIR独自の挙動が見られたり、AIR APIを含むデバッグがFireBugsでできなかったりと、ちょっと開発しづらい感じはありました。

でも、ActionScript3やFlexを一から覚えるよりは、今までの知識をそのまま利用できるHTML+JavaScriptの開発は敷居が低いと思います。

Adobe AIR 1.0が出たばかりですから、これからの開発経験が後々に生きてくるでしょうし、GoogleMaps等の外部API連携もそのうちできるようになるかもしれません。


あと、AdobeはAIRの次世代リーダーを発掘するために、下記のようなコンテストを行うようです。

WebとLocalを巻き込んだAIRアプリを作って、参加されてみてはいかがでしょうか?