2012年4月28日土曜日

Windows8 でのデータ保存

さてGoogle+に対してのOAuthアクセスは作成しましたが、
アクセストークン等のアプリケーションデータを保存しよーと思ったら、
WindowsPhone的なIsolatedStorageFileStreamがない事に気付きました。

そう言われるとデータベースとか、スマートフォン系の特殊なアクセスばっかりで
「設定ファイル」何かを作ってデータを永続化してないなぁと思ったので
設定ファイルを作ってみる事にした。

シリアライズ可能なオブジェクト
シリアライズ可能なクラスを作成します。
        private static Settings _settings;
        public static Settings Settings {
            get { return _settings; }
        }
私はAppにstaticなメンバを作成しておきました。 この中にアクセストークンのデータを残すようにしておきます。 ※アクセストークン(string)、リフレッシュトークン(string)、有効期限(現在時刻から足し込んだDateTime)
データの保存
データの保存は以下の通りで行いました。
        async static private Task<StorageFile> GetSettingFile()
        {
            var folder = Windows.ApplicationModel.Package.Current.InstalledLocation;
            StorageFile file = await folder.CreateFileAsync("settings.xml",CreationCollisionOption.OpenIfExists);
            return file;
        }

        async static public void SaveSetting()
        {
            var file = await GetSettingFile();
            var serial = new XmlSerializer(typeof(Settings));
            using ( var tWriter = new StringWriter() ) {
                serial.Serialize(tWriter, _settings);
                using (var stream = await file.OpenAsync(FileAccessMode.ReadWriteUnsafe))
                {
                    var outputStream = stream.GetOutputStreamAt(0);
                    using (var writer = new DataWriter(outputStream))
                    {
                        writer.WriteString(tWriter.ToString());
                        await writer.StoreAsync();
                    }
                }
            }
        }
「Windows.ApplicationModel.Package.Current.InstalledLocation」で インストールしたディレクトリを取得する事ができます。 他にドキュメント内とかでも保存できるのですが、 他の場所に保存する場合は、パーミッション(Package.appxmanifest)が必要になるようです。 ※また機会あったら書きます。 CreateFileAsync()を使用していますが、 同期的に取得するのでawaitで呼び出しています。 第2引数の「CreationCollisionOption.OpenIfExists」は存在しない場合作成してくれます。 ※これを付けないとsettings(2).xmlとかをモリモリ作ります。 あとはSettingsオブジェクトをシリアライズして保存しています。
データの取得
        async private void LoadSetting() 
        {
            var file = await GetSettingFile();
            using (var stream = await file.OpenAsync(FileAccessMode.Read))
            {
                var inputStream = stream.GetInputStreamAt(0);
                using (var reader = new DataReader(inputStream))
                {
                    uint size = (uint)stream.Size;
                    await reader.LoadAsync(size);っd
                    string data = reader.ReadString(size);
                    if (!data.Equals(""))
                    {
                        using (var textReader = new StringReader(data))
                        {
                            XmlSerializer serial = new XmlSerializer(typeof(Settings));
                            _settings = (Settings)serial.Deserialize(textReader);
                        }
                    }
                    else
                    {
                        _settings = new Settings();
                    }
                }
            }
        }
後は同様に読み込むだけですね。
保存したデータ
<?xml version="1.0" encoding="utf-16"?>
<Settings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <AccessToken>
    <accessToken>ya29.AHES6ZQ0-H7cotFnLdVAcZ5CVz280mBarXy4tUn7-_Bs-AE</accessToken>
    <refreshToken>1/x8SHm9FaHD299fdZSNNyme2m3X2Up_Ki90pcUc2Y2Iw</refreshToken>
    <expiresIn>2012-04-28T21:48:28.55985+09:00</expiresIn>
  </AccessToken>
</Settings>
こんな感じで残ってました。 ※AccessTokenクラスで保存しています。 開発中は、 「C:\Users\(ユーザ名)\Documents\Visual Studio 11\Projects\GooglePlus\GooglePlus\bin\Debug\AppX」 って位置にありました。 リリースすると 「C:\Users\(ユーザ名)\AppxLayouts\(アプリのID)」 のようです。
感想
保存方法がかなりWindowsPhoneと違いました。 現状開発中で気になっているのは Suspendingのイベントに対して、保存処理を行ったんですけど、 開発中だからか、動作せずにStaticで他の画面から呼び出している状態になってしまいました。
何か間違ってるのかな?

await async知らなかったので呼び出した時のエラー原因がわからなかったです。

2012年4月26日木曜日

Windows8でのWebViewのスクレイピング

Windows8用アプリの開発がVisualStudio11(ベータ版)により可能になりました。
iPhoneが世間を席巻している状況下でも
現在のWindowsユーザがMacへの移行をはじめるのかは怪しいところで、
※開発者の移動はかなりありましたが。
Windows8への移行により、かなりのユーザさんが、
困惑するんじゃないかなぁ~と心配しているところではあります。
※特にまだXPを使っている人

私はWindowsPhone(というよりMetro)のかわいさに負けて、
少し開発しましたが、Microsoftの底力というか、
言語設計からなる雰囲気に少しMicrosoftに惚れ直したところでして、
メインマシンをWindows8端末にして、Metro開発でもやってみよーかなーと思っていまして。

以前、WindowsPhoneで少し開発したGoogle+アプリを
Windows8に移植しようと思い立ったわけです。

で最初のOAuth部分でWebViewに関して仕様が違ったみたいなので
以前書いた「認可コードのスクレイピング(WindowsPhone)」との違いを書きたいと思います。

2点の違い
試したのは「LoadCompletedイベント」の時なので 他のイベントの時にどうかはわかりません。 以前のコードの仕様としては 「認可後のページを開いたら、HTMLを取得して認可コードを取得」 っていう感じなのですが、 ・WebViewのUriが更新されない ・WebViewのSaveToString()がサポートされてない の2点が違います。
アクセスしているUriの取得
これはすごく単純でイベント自体のUriを取得すれば良いので
private void loginView_LoadCompleted(object sender, NavigationEventArgs e)
{
            string uri = e.Uri.AbsoluteUri;
}
とする事で回避できます。 回避というか、これが当たり前かな?
HTMLの取得方法
これはトリッキーというか正式にやっていいものなのか。。。 別の方法がある気がするのですが、私的にはHTMLがどうしても欲しかったので JavaScriptを呼び出すInvokeScript()を使ってHTMLを抜き出してみました。 eval()関数でouterHTMLを取得してみました。
string html = loginView.InvokeScript("eval", new string[] { "document.documentElement.outerHTML;" });
これでHTMLが抜き出せます。
最終的には
単純に2つのプロパティ(メソッド)での取得データを切り替えただけでOKでした。
        private void loginView_LoadCompleted(object sender, NavigationEventArgs e)
        {
            string uri = e.Uri.AbsoluteUri;
            if (uri.IndexOf("/o/oauth2/approval") != -1)
            {
                string html = loginView.InvokeScript("eval", new string[] { "document.documentElement.outerHTML;" });

                int tagStart = html.IndexOf("<textarea");
                int tagStartEnd = html.IndexOf(">", tagStart);

                int tagEnd = html.IndexOf("</textarea>");

                string pin = html.Substring(tagStartEnd + 1, tagEnd - tagStartEnd - 1);
                pinText.Text = pin;
            }  
        }
という感じのコードになり
って感じで認可コードも自動設定可能になりました。 あくまで見た感じだと WindowsPhoneの時とUIの部分で色々大変な事がありそうですけどね。

2012年4月8日日曜日

もうこわくない。SELinux

第7回静岡ITPro勉強会インフラ部にて
SELinux界のスーパースターSELinuxで著名な石川さん(@ishikawa84g)をお呼びして
「SELinuxハンズオン」を行いました。

部長も興奮しています。

開催日の3/31はかなりの暴風雨に見まわれ、
予定していた部長のお迎えがなくなり、私が迎えに行くことに。

SELinuxは運用まで行くとまぁ色々あるんだろうなぁと思い
泣きながらDisabledにしていました。※石川さんごめんなさい。


ハンズオンがあったお陰で、すっかりSELinuxが怖くなくなりました。
というよりSELinuxを悪者だと誤解していたようです。
資料と重複しますが、おさらいも兼ねて、ブログに残しておきます。

Fedora16を準備して行ったのですが
手元のCentOSでは結構コマンドが違いましたのでご注意を。

SELinuxの歴史
Linux Kernel2.6.x から標準オプションになっていて 実際このころから「ヒャホー2.6インスコしたぜぇー」ってやって Apache起動したら「ん?なんぞ?動かん」って感じになった記憶がある。 海外の映画とかドラマで地味に出てくる「NSA」が開発して、 最近ではSEAndroidというものも現れてきている。これもNSAらしい。 政府、金融機関等の調達基準をLinuxで満たす為に生まれたらしい。
リファレンスモニタ
システムコールが発生して、システムリソースに到達するまでに セキュリティポリシーを読み込んでそれを判定をしている仕組み。 わかりやすいので資料から拝借。
こんな感じ。 私が想像していたよりKernelに近い部分にいるんだなぁ~と感じた。
DACとMAC
DACはUNIX系OSで用いられる制御方式。 読み書き実行をオーナー、グループ、その他で制御する方式ですね。 これはご存知のように結局rootだと何でもできるわけです。 それに対してMACはセキュリティポリシーの適用してブロックする方式で、 rootだろうがポリシーに合っているかだけで判断する。 SELinuxはDACでアクセス権限を評価したのち、MACのアクセス権限を評価しています。 SELinuxを適用している場合、 rootを乗っ取られたとしてもポリシー以外の事はできなくなる。 これが一番大きいですね。 実際CVEにも載っているようなコードを 実行さえできればかなりあっさりしたコードで乗っ取りが可能なわけなので システムを乗っ取りに対して最小限の被害に抑える事ができるようになる。
SELinuxの状態
SELinuxの状態は
  • Enforcing
  • Permissive
  • Disabled
があり、Enforcingは有効、 Permissiveはログだけを出力するモード(これ重要)、Disabledは無効にします。
$ getenforce
で現在のSELinuxの状態を知る事ができます。 「/etc/selinux/config」ファイルにある 「SELINUX=」の箇所を編集して、起動時の状態を設定できます。 より詳細な情報が知りたければ「sestatus」
$ sestatus

SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
Current mode:                   permissive
Mode from config file:          enforcing
Policy version:                 26
Policy from config file:        targeted
でファイルの状態や現在の状態を知る事ができます。 状態を変更する場合は
$ setenforce 0    → Permissiveに設定
$ setenforce 1    → Enforcingに設定
で切り替えが可能になります。 Disabledにするにはファイルの編集でしかできません。
コマンドにZオプション!
これは全く知らなかった! 様々なコマンドにZオプションをつけるとそれぞれのリソースが どのようなタイプかがわかります。
$ ls -Z

-rw-r--r--. root root system_u:object_r:selinux_config_t:s0 config
・・・

$ ps -Z

system_u:system_r:httpd_t:s0    17718 ?        00:00:00 httpd
・・・
をー。 これでリソースがどのようなタイプになっているかがわかる。 ここについてくるタイプによってどのようにコントロールされるかが 決まってくるわけですね。
SELinuxが悪さをした(守ってくれた)場合
これが一番勉強になったというか、なるほどとそういう事かと思った。 「アプリが動かん!SELinuxをDisabled。動いたー」 という悪魔の囁きを無視し、行儀よくする方法を教わった。 ハンズオンではApacheを利用してファイルへのアクセスを試しました。 アプリが動かない!となって「SELinuxかな?」と感じたら まずsetenforceコマンドで「Permissive」状態に切り替えます。でアプリを動かしてみる。 で、アプリが動いたとなると 「SELinuxが止めてくれている。石川さんありがとう(´ω`)」 と東京の方向を向いてお辞儀をする。 でログを見ます。 ログは「/var/log/audit/audit.log」に出力されます。 ※auditが停止中は「/var/log/message」に出力されるそうです。 これがEnforcingの状態では実際にアクセスをストップさせ、 Permissiveモードの時はログだけを吐き出してくれる。 アクセスが停止される原因は「denied」と記述してあるので ログをgrepすると
$ /var/log/audit/audit.log | grep denied

type=AVC msg=audit(1333866553.014:218): avc:
denied  { open } for  pid=17721 comm="httpd" name="1.html" dev="dm-1" 
ino=2621442 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file
という感じのログが出てくる。 これの意味については資料にありますが 簡単に言うと「1.htmlは~httpd_t:s0からopenされたけど、~default_t:s0タイプだぜ」 と言ってくれてます。 Webで見ようとしたファイルがApacheからOpenできないと叫んでいます。 この場合は
$ restorecon -FRv パス
としてファイルコンテキストを設定しなおす事で Apacheからこのファイルを見れるようになるってわけです。
感想
SELinuxが原因かな?と感じたら
  • Permissiveで原因の切り分け
  • ログの確認
  • ポリシーに合わせる、再利用する、作成する(※)
  • 動作させる
って感じでやる。(※これのうちのどれかを行う この癖を身につける。これ大事。 SELinuxの特徴として、基本的には「ポリシー自体は一般的なもの」なので 例えばWebのディレクトリをデフォルトから移動したかったりした場合には ポリシーを再利用、ポリシーの作成を行なっていくわけです。(ハンズオンで行いました。 もっと他にも色々なコマンドを教わったのですが、 一応書いたところが基本的なところかなぁ~と思います。(詳しくは資料を見て 石川さんも言ってましたが、 これらは経験を重ねて行って慣れていくしかないようですね。 「開発はDisabled!」こう感じていたんですけど、 開発者はポリシー自体を意識するのはすごく大事で、 例えば、ポリシーにかなり逸脱したようなアプリを作ってしまうと SELinuxに長けてないような運用者では「Disabledで」と選択してしまいそうな気がします。 Windowsで開発して、Linuxにデプロイってやっていると こういう問題も知らないで開発しちゃいそうですね。 アプリの脆弱性を突かれてしまった場合の最後の砦なので SELinuxを大事にしてあげないとなぁ~と感じました。 意識して開発しなきゃと本当勉強になりました。石川さんありがとうございました。 これで明日からLet's Enforcing!

2012年3月17日土曜日

DropboxにRESTfulアクセス

少しDropboxにデータを送り込みたいとなって
エンジンをGoogleAppEngineで送り込む事になったので少し調べました。

SDKはiOS,Android(Java),Ruby,Pythonがあります。
今回はJavaを選択したので見ましたがHttp「s」URLConnectionを使用しており、
これがAppEngineでサポートしてないので実行環境でエラーが起こります。

ってことでRESTfulアクセスで行なってみました。

準備
まずここからアプリを登録してクライアントキーを作成します。
作成するとこんな感じ。
AccessTypeは後から変えられません。 「App folder」は専用のディレクトリのみアクセス可能なもの。 「Full Dropbox」はすべてのファイルアクセスです。 専用のディレクトリのみでOKだったんですけど、Fullにしました。
リクエストトークンの取得
https://api.dropbox.com/1/oauth/request_tokenにアクセスを行います。 で戻り値(line)は「oauth_token_secret=biv7oeyp3nvkb1g&oauth_token=ql0l5qpk8h10rjz」みたいな感じです。 buildOAuthHeader()はSDKのコードにもありますが、
    private static String buildOAuthHeader(boolean accessToken) {
        Map<String,String> map = new HashMap<String,String>();
        map.put("oauth_version","1.0");
        map.put("oauth_signature_method","PLAINTEXT");
        map.put("oauth_consumer_key",APP_KEY);

        String sig = null;
        if ( accessToken ) {
            map.put("oauth_token",TOKEN_KEY);
            sig = encode(APP_SECRET) + "&" + encode(TOKEN_SECRET);
        } else {
            sig = encode(APP_SECRET) + "&";
        }
        map.put("oauth_signature",sig);

        StringBuilder buf = new StringBuilder();
        buf.append("OAuth ");

        Iterator<Entry<String, String>> itr = map.entrySet().iterator();
        while ( itr.hasNext() ) {
            Entry<String, String> entry = itr.next();
            buf.append(entry.getKey() + "=" + entry.getValue() + ",");
        }
        String data = buf.toString();
        return data.substring(0,data.length()-1);
    }
といった感じでヘッダに設定してあげてます。
認可用のURL作成
lineにある「oauth_token」の値を使います。
        System.out.println("https://www.dropbox.com/1/oauth/authorize?oauth_token=" + argsMap.get("oauth_token"));
        System.in.read();
read()はブラウザからの入力待ちです。 ブラウザを立ち上げるとDropboxに認証してその後許可のアクションがあります。
このアクセスの際に「oauth_callback」を指定しておくとコールバックしてくれます。 操作後の挙動としては ・許可(あり):コールバックURLに戻る ・許可(なし):許可画面に遷移 ・拒否:Dropboxのhomeに遷移 って感じです。 ここで良くOAuthでの認可コードを取るようなコードを書いてましたが、 コールバックを指定したとしてもそれはDropboxにはありません。 ※ただしコールバックにはuidとoauth_tokenを付けてくれるのでユーザ特定は可能です。 ようは「認可」を行った時点でアクセストークンは取れる状態になるってわけです。
アクセストークンの取得
認可されていれば以下ができます。
        TOKEN_KEY    = argsMap.get("oauth_token");
        TOKEN_SECRET = argsMap.get("oauth_token_secret");
        url = new URL("https://api.dropbox.com/1/oauth/access_token");
        connection = (HttpURLConnection)url.openConnection();
        connection.setDoInput(true);
        connection.addRequestProperty("Authorization", buildOAuthHeader(true));
        connection.setRequestMethod("GET");

        inStream = connection.getInputStream();
        input = new BufferedReader(new InputStreamReader(inStream));

        line = input.readLine();
リクエストトークンで取ってきていた値を設定してbuildOAuthHeader()の値をtrueの方で動かします。 アクセストークンもリクエスト時と同じように 「oauth_token_secret=biv7oeyp3nvkb1g&oauth_token=ql0l5qpk8h10rjz」 という値で入ってきます。 これでアクセス可能になります。
ファイルのアップロード
使用するのはファイルのアップロードだけだったので PUTでbodyに設定してあげてアップします。
    public static void put(String name,byte[] data) throws Exception {

        URL url = new URL("https://api-content.dropbox.com/1/files_put/dropbox/" + name);
        HttpURLConnection connection = (HttpURLConnection)url.openConnection();
        connection.setDoInput(true);
        connection.setDoOutput(true);
        connection.addRequestProperty("Authorization", buildOAuthHeader(true));
        connection.setRequestMethod("PUT");

        OutputStream outputStream = connection.getOutputStream();
        outputStream.write(data);
        outputStream.close();

        InputStream inStream = connection.getInputStream();
        BufferedReader input = new BufferedReader(new InputStreamReader(inStream));
        String line = "";
        while ((line = input.readLine()) != null) {
            System.out.println(line);
        }
    }
ここでURLにある「dropbox」です。 「Full Dropbox」を選択した場合はこれでOKなのですが、 「App folder」の場合は「sandbox」を指定してあげます。 前者はフルなのでユーザのディレクトリ通りに設定していったりするのですが、 sandboxの場合、「apps/[アプリID]」が使用されて、そこのアクセスのみが可能です。 フルアクセスのViewerアプリを作らない限り、後者を選択した方が良いでしょうね。 ※ブログ書き始めてアプリを変更しました。
感想
DropboxのAPIって使うとは思ってなかったのでアクセスするとは思いませんでした。 まぁ本家が作っているクライアントが同期とかもかなり優秀なので あまりアクセスする人はいないのかもしれませんね。 ただファイルアクセスはかなり簡単にできたので、 クラウドから一時的に持ってくるツールは作りやすいかな?と感じました。 何点か気になるのは ・POSTアクセスのAPIがGETで取得できる ・PLAINTEXTを使用している ・一度認可されていると許可とかが出ない って感じですかね? ちなみに自分以外に公開するには 「My Apps」のところで「Additional users」をONにする必要があります。 製品バージョンと特に制限はないみたいに見えます。 ※開発中は5人だけっぽい感じには書いてありますけど。

2012年3月10日土曜日

Xtionで画像を作ってみる

XtionでDepthGeneratorで深度を取得できますが、
カメラ的に使うにはImageGeneratorを使用します。

今回はJavaで取得してみます。


設定値
実はここではまりました。。。
<Node type="Image" stopOnError="false" >
 <Configuration>
  <MapOutputMode xRes="640" yRes="480" FPS="30"/> 
 </Configuration>
</Node>
とConfigに記述しておきます。 画面を指定してない場合、320x240で取得できます。 なんでハマったかというと 描画する予定の画面(640x480)に対して、 320x240のデータを埋め込んでしまった為、うまく描画できないという 現象に引っかかったからです。 なので作成する画像が320x240の場合は指定しなくてもOKです。
画像データの作成
imageGen = ImageGenerator.create(context);
でインスタンスを取得。 描画処理等の永久ループの箇所に
  ImageMetaData imageMD = imageGen.getMetaData();
  ImageMap imageMap = imageMD.getData();
  ByteBuffer image = imageMap.createByteBuffer();
  while ( image.remaining() > 0 ) {
   int pos = image.position();
   byte pixel = image.get();
      imgbytes[pos] = pixel;
  }
という風にbyte[]を作成します。 imageMapにある値はwidth*height*3になります。 *3の値はRGBを表しています。 表示する際には深度の時と同じように
        DataBufferByte dataBuffer = new DataBufferByte(imgbytes, width*height*3);
        WritableRaster raster = Raster.createInterleavedRaster(dataBuffer, width, height, width * 3, 3, new int[]{0, 1, 2}, null); 
        
        ColorModel colorModel = new ComponentColorModel(
          ColorSpace.getInstance(ColorSpace.CS_sRGB), 
          new int[]{8, 8, 8}, 
          false, 
          false, 
          ComponentColorModel.OPAQUE, 
          DataBuffer.TYPE_BYTE);

        bimg = new BufferedImage(colorModel, raster, false, null);
        g.drawImage(bimg, 0, 0, null);
となります。 (gは画面上にあるpaint用のGraphicsオブジェクトです) これでカメラのように画像を出力する事が可能です。
( ー`дー´)キリッ。。。もちろん動きますよ。
例えば
私のアプリはWebSocketと連携させているので この画像データをそのままWebSocketで送ってみようと思っています。 しかしそれはXtionとはあまり関係ないので 例えばですが1枚無人の背景データを用意してみて、 人間として認識された部分を背景データと重ねて塗りつぶす。 というプレデターごっこ(おそらくある程度マージンがあるので微妙に人の形が浮かび上がる)等が できるかなぁ。。。と思っています。 Depthとかの話とか書いてないから何か微妙な説明になるかなぁ、、、 もう少し、Generator周りとかがかっこよくなったら、記述したいと思います。

2012年2月28日火曜日

認可コードのスクレイピング(WindowsPhone)

クライアントアプリでGoogleのOAuthを行うと

という風に認可コードが取得できます。 この文字列をアプリ側に送信するのですが、 スマートフォンでこの認可コードを入力するのは至難の業です。いや苦行です。 そこでコピペとなるところですが、 Googleの認可コードのページでWindowsPhoneのコピペが何故か機能しません。 私自身開発中に頑張って入力するという苦行を行なっていました。 ※リフレッシュトークンの実装後にその苦行を忘れる エミュレータはまだキーボード入力が可能なのでいいのですが 実機になるとそれもなくなり本物の苦行を強いる事になります。 しかもコードをミスって入力した場合に開発者(私)は発狂して WindowsPhoneへの恨みが重なっていきます。 んで冷静に考えるとHTMLスクレイピングして認可コードを取得すればOKだと至りました。 まぁ他のTwitterクライアント見て実現していたので「あー可能なんだ」って思ったのが この苦行への終止符だったわけです。 ※Twitterは8桁の数値なので別に苦行レベルでもない気がしますけど。 まぁそんな苦行を強いるアプリが生まれないで済むように一応書いておきます。
        private void webBrowser_LoadCompleted(object sender, System.Windows.Navigation.NavigationEventArgs e)
        {
            string uri = webBrowser.Source.AbsoluteUri;

            if (uri.IndexOf("/o/oauth2/approval") != -1)
            {
                string html = webBrowser.SaveToString();

                int tagStart = html.IndexOf("<textarea");
                int tagStartEnd = html.IndexOf(">", tagStart);

                int tagEnd = html.LastIndexOf("</textarea>");

                string pin = html.Substring(tagStartEnd + 1, tagEnd - tagStartEnd-1);
                textBox.Text =pin;
            }

        }
WebBrowserに対してLoadCompletedイベントを作成します。 WebBrowser.SourceのUriからAbsoluteUri(LocalPath、AbsolutePathでも可能)から 認可した場合の画面を判定します。 WebBrowserのSaveToString()から内部のHTMLを取得して、 あとは認可コードを書いてあるtextareaタグを解析です。 まぁ試しのコードではそのままテキストボックスに設定してますけど、 そのままアクセストークンを取得するコードに流してもOKなんでしょうけど少し悩ましいです。 気持ちの問題なのですがOAuthのような認可手順に対して、 ユーザが要求したWebページを表示せず(実際は表示する寸前)に次の画面に遷移するっていうのが 果たして正解なのかどうかっていう事です。 まぁWebアプリの場合このままコールバックになるわけだからいいんでしょうね。 エンドユーザにはOAuthという認可方法はそんなに関係ないですし。

2012年2月23日木曜日

GoogleAnalytics(v3)でアクセスする

先日ブログにも書きましたがV2でのアクセスであり、ClientLoginだったので
今度はv3でのアクセスをやってみます。今回もJavaです。
Googleの日本語の資料はいつも通り古くほぼv2を使っているので
しばらくは英語の文献の方が良いと思います。


準備
まずここからクライアントライブラリをダウンロードしてきます。 で ・google-api-client-1.6.0-beta.jar ・google-http-client-1.6.0-beta.jar ・google-oauth-client-1.6.0-beta.jar ・dependencies/gson-1.7.1.jar ・dependencies/guava-r09.jar ・dependencies/jackson-core-asl-1.9.1.jar を読み込みます。 Analytics用のライブラリはここにあります。
OAuthでのアクセスする準備
Google+のアクセス時に説明しましたが、API使用したり、クライアントID取得の為に GoogleAPIConsoleでAnalyticsをONしておく必要があります。 「Services」をクリックしてAnalyticsをON
「API Access」をクリックして、クライアントIDを発行します。 私自身はWebアプリで使いたいですけど、一旦アクセスする為に「installed applications」を選びました。 違いはWebアプリケーションの場合と違い、コールバックURLが固定値になり、 手作業で認可コードを設定するっていう手順が増えるだけです。
認可を行う
さて準備ができたので、アクセスしてみます。
  String authorizationUrl = new GoogleAuthorizationRequestUrl(CLIENT_ID, REDIRECT_URL, "https://www.googleapis.com/auth/analytics.readonly").build();
  System.out.println(authorizationUrl);
  BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
  String authorizationCode = null;
  try {
    authorizationCode = in.readLine();
  } catch (IOException ioe) {
   throw new RuntimeException("Error",ioe);
  }
GoogleAuthorizationRequestUrlに取得したクライアントIDとリダイレクトURLを設定します。 三番目の引数はスコープ(何にアクセスできるか)です。 認証用のURLを作成してコンソールに出力しreadLine()で認可コードを待ち受けます。 ブラウザを立ち上げて、コンソールに出力されたURLにアクセスすると Googleの認証画面(マルチアカウントの場合、ユーザ選択画面等もあります)が表示され 認可画面が現れて認可を行うと、認可コードを出力してくれるのでそれをコンソールに入力します。
アクセストークンの取得
コンソールに入力された認可コードを元にアクセストークンをとりにいきます。 APIコンソールで取得したIDとSecret、それと認可コードでAccessTokenResponseを生成
  NetHttpTransport httpTransport = new NetHttpTransport();
  JacksonFactory jacksonFactory = new JacksonFactory();
  AccessTokenResponse response = null;
  try {
    response = new GoogleAccessTokenRequest.GoogleAuthorizationCodeGrant(
        httpTransport, jacksonFactory, CLIENT_ID, CLIENT_SECRET, authorizationCode,
            REDIRECT_URL).execute();
  } catch (IOException ioe) {
   throw new RuntimeException("Error",ioe);
  }

認可コードが間違ってなかったらこれでOKのはず。 responseにアクセストークン、リフレッシュトークン等が取得されます。
Analyticsのオブジェクトを生成
取得したAccessTokenResponseを元に
GoogleAccessProtectedResource googleAccessProtectedResource = 
  new GoogleAccessProtectedResource(response.accessToken, httpTransport, jacksonFactory,CLIENT_ID, CLIENT_SECRET, response.refreshToken);
Analytics analytics = Analytics.builder(httpTransport, jacksonFactory)
         .setHttpRequestInitializer(googleAccessProtectedResource)
         .setApplicationName("test")
         .build();
というような感じでAnalyticsオブジェクトを生成します。
データを取得してみる
さてやっとアクセスですね。
  Get apiQuery;
  try {
   apiQuery = analytics
     .data()
     .ga()
     .get("ga:" + "13012476", "2011-09-01", "2011-09-30",
       "ga:visits,ga:pageviews");
  } catch (IOException e) {
   throw new RuntimeException("Error", e);
  }

  apiQuery.setSort("-ga:visits");
  apiQuery.setMaxResults(50);

  try {
   GaData data = apiQuery.execute();
   Map totalsMap = data.getTotalsForAllResults();
   for (Map.Entry entry : totalsMap.entrySet()) {
    System.out.println(entry.getKey() + " : " + entry.getValue());
   }
  } catch (IOException e) {
   throw new RuntimeException("Error", e);
  }
取得してきたanalyticsオブジェクトのdata().ga().get()をおこなってGetオブジェクトを生成します。 第1引数はProfileIdになります。v2で言っていたTableIdですね。id自体は数値などで入っている為、 「ga:」をつけてやる必要があります。 これをexecute()するとデータが取れてきます。 この取得方法だと出力は「ga:visits,ga:pageview」が取得できます。 取得できるDimensionやMetricsなどはここにあります。
ProfileのIdについて
前述した「ga:13012476」についてですが v2の時はEntries()などで比較的容易に取得できましたけど。。。
   Management management = analytics.management();

   Accounts accounts = management.accounts().list().execute();
   List<Account> accountList = accounts.getItems();
   Account account = accountList.get(0);
   String accountId = account.getId();

   Webproperties prop = management.webproperties().list(accountId)
     .execute();
   Webproperty webproperty = prop.getItems().get(0);
   String webId = webproperty.getId();

   Profiles profiles = management.profiles().list(accountId, webId)
     .execute();
   Profile profile = profiles.getItems().get(0);
   String profileId = profile.getId();

   System.out.println("accountId:" + accountId);
   System.out.println("webId:" + webId);
   System.out.println("profileId:" + profileId);
わかりやすくすべてのデータについて、itemsの1件目を取得してます。 Accountはマルチアカウント用なのかな、、、私の環境では1件でした。 WebPropertyは解析しているサイト数が出てきました。 その中からプロファイルを取得する感じですね。 少しAnalytics自体のデータ構造がわかってないのでこれで正しいのか不明です。 execute()時にAPIを発行しているはずなので、、、うーんって感じ。
感想
なんかライブラリが多すぎて少し引きました><。 実際実装する際にはライブラリではなく、RESTfulアクセスすると思います。 なぜかというと、個人(セッション)に対して何を残せば良いかがわかりにくく アクセスなどが隠蔽されているので何か使いにくいというか、、、って感じです。 ※いやこのコード位の事やるにはいいんですけどね。 まぁJSONからデータのオブジェクト作ったりするのが大変だったりもするんですけど。

GoogleAnalyticsにアクセスしてみる


そのまま勢いでv3のOAuthを作りました。

Google AnalyticsはWeb解析ソリューションです。
何も考えずにサイトにJavaSriptコードを埋め込む事でWebの訪問者などを調べる事をできます。
素晴らしいサービスであれこれ機能がありますが、
今回はAPIにJavaでアクセスしてみたいと思いました。

準備
・gdata-core-1.0.jar ・gdata-analytics-2.1.jar ・google-collections-1.0.jar を読み込みます。 見つけて行く過程で「google-api-service-analytics-v3-1.3.3-beta.jar」を見つけました。 これでアクセスするのが良かったかもなのですが、 ちょっと使い方がわからなかったので後日試します。 サンプルを元におこなってみます。
まずは認証
gdataに関してOAuthやAuthSubなどの認証方式がありますが、ClientLoginで試しました。
AnalyticsService service = new AnalyticsService("applicationName");
service.setUserCredentials(username, password);
ClientLoginはOAuthなどを触った事がある人ならわかると思いますが あまり好ましいと思える認証方式ではありません。 実際文献にも「てめーで使うようなアプリにしか使うなよ」って書いてあります。 OAuthなどを行いたかったのですが、今回はアクセスしてみる事を目標にしてますので 一旦この方式でアクセスしてみます。
サイトの一覧を取得
AnalytisServiceからアカウントの情報を取得してきます。
URL feedUrl = new URL(""https://www.google.com/analytics/feeds/accounts/default""); 
AccountFeed service.getFeed(feedUrl, AccountFeed.class);
for (AccountEntry entry : accountFeed.getEntries()) {
}
AccountFeedのgetEntries()によってそのアカウントが所有しているデータを取得する事が可能です。
サイトの訪問者を見てみる
AccountEntryには「TableId」というものがあって、それによりどのサイトかを識別できるようになってます。 そのテーブルIDを元にDataQueryを作成します。
    DataQuery query = new DataQuery(new URL(DATA_URL));
    query.setIds(tableId);
    query.setStartDate("2012-01-01");
    query.setEndDate("2012-01-31");
    query.setDimensions("ga:browser");
    query.setMetrics("ga:visits,ga:bounces");
setDimensions()は何を取ってくるか? setMetrics()何を取ってくるか?です。 まぁ日付はそのままですね。 この場合、ブラウザについての訪問者を調べています。 このデータクエリに対して
query.setSort("-ga:visits");
query.setFilters("ga:browser!@Explorer");
DataFeed dataFeed = service.getFeed(query, DataFeed.class);
というようにソートやフィルタをつけてあります。 この場合訪問者数の降順、「Explorer」とついたブラウザの排他ですね。 ・・・Googleのサンプルに悪意を見た。 これで取得したDataFeedに情報が入ってますので
    for (DataEntry entry : dataFeed.getEntries()) {
      System.out.println("\tBrowser: " + entry.stringValueOf("ga:browser"));
      System.out.println("\t\tVisits: " + entry.stringValueOf("ga:visits"));
      System.out.println("\t\tBounces: " + entry.stringValueOf("ga:bounces"));
      System.out.println("\t\tBounce rate: "
          + entry.longValueOf("ga:bounces") / (double) entry.longValueOf("ga:visits"));
    }
みたいな感じで情報にアクセスできます。
感想
まぁサービスに埋め込むにはまだ色々と考えないとなぁと思いました。 ※まぁとにかく認証ですよね。 案外調べていてPHPで文献(ブログ)を出している人が多そうでした。 まぁ現状では埋め込むサイトはPHPの方が多そうですね。 Javaでアクセス解析必要なサービスって少し前なら想像できませんが 今はGoogleAppEngineもあるので、Javaでこういったものが必要なサービスも増えてくると思います。 1.6辺りからGoogleAppEngine対応されているみたいで

2012年2月5日日曜日

ブラウザベースのEPUB読み込み

ふと思いついた
達人出版会などがEPUB形式で配布しているのもあり、 タブレットではEPUBで読む機会も増えてきました。 PDFで読むのがまだ主流と思いますが、未来に行くならEPUBだ! とまぁこう思う所存でございます。※アンマオモッテナイデス 先日EPUBの圧縮方式をJavaでやりましたが、ふと思いついたんです。 ブラウザベースでEPUBを読み込んで、そのまま読めたら最高だと。 Adobeのリーダーインストールすんのか?とかいろいろ葛藤がある中、 ChromeExtensionで読めたら最高じゃない? ※クロスブラウジングしなくていいからじゃないだからね! って事で少し作成してみる事にする。
幸運なことに
先日のブログでも言いましたが、EPUBのファイルはZIP形式です。 先人によりZIP解凍してくれるライブラリがありました。 「JavaScriptでZIP解凍できるのかよ!」と驚きながらありがたやー。 ※これがあったから始めようと思ったりしたり、、、 って事で、それを使ってEPUBを解凍する事にした。 ファイルの指定はinputのfileに指定したらクライアント上で展開っていう形。
   //ファイルを変更された場合
   var fileData = document.getElementById("epubFile").files[0]
   var reader = new FileReader(); 

   //読み込みが完了したら
   reader.onload = function(evt){
          var bytes = [];
          var byteData = evt.target.result;
          //データを変換
          for (var i=0; i<byteData.length; i++)bytes[i] = byteData.charCodeAt(i) & 0xff;
          //epubファイルを解凍
          var epub = Zip.inflate(bytes);
   };
   reader.readAsBinaryString(fileData);
って感じでchangeイベント等で行えば良い。サーバが関係しないから思ったより簡単!速い! ・・・JavaScript速くなったなぁ、、、トオイメ。 Zip.inflate()によってfilesプロパティに内部のファイルにアクセスできるようになり、 files["ファイル名"]でFileクラスがもらえるので そこに生データがあったり、inflate()で解凍してくれたりする。 なので ・mimetypeの内容確認 mimetypeは非圧縮かどうかって確認まではしませんでした。 ・META-INF/container.xmlのOPF位置の確認を行う。 container.xmlに関しては文字列からDomにして確認
function changeDom(data) {
    var xmldom = new DOMParser();
    var dom = xmldom.parseFromString( data, "application/xml" );
 return dom;
}
・目次データの作成 container.xmlのopfファイル位置を取得してそれをDOMに変換して、 manifestなどを読み込んで配列データにしてファイル名を取っておく。 ここで難しいのは 圧縮ファイル名は「OEBPS/xxxx.xhtml」となっているけど、 XHTML上の指定は「xxxx.xhtml」という感じで相対パスになっているはず。 ※OPFファイルからの相対っていう仕様なのかな? OPFファイルを抜き出した時に「OEBPS」などのディレクトリ名を残しておいて XHTML内部のファイルの解析を行うときに使用できるようにしておく。
HTMLの表示、、、と問題。
目次から表示したいファイルやら何やらは取得できるのだけど、表示する際には少し工夫が必要で、 「xxxxx.html」はJavaScript上の「xxxx/xxxx.html」という名称で取得できかつ、その場には存在しない。 要は単純にリンク先は存在しない。 ・リンクをクリックされる。 ・HTMLを取得する ・それをHTML上に展開する って寸法になる。 クリックに対しては
<a href="javascript:void(0)" onclick="open('xxxx.xhtml'); return false;"></a>
見たいにやって関数をかませてやるといい。 それをinnerHTMLやらでそのまま表示。
(∩´∀`)∩ワーイ。ママ。ヒトリデデキタヨ。 ※サンプルは白鯨です。 そしてEPUBを見ていると異変に気づく。 スタイルシートあたってないわ、画像が表示できないわ、、、である。
<img src="images/xxxx.jpg">
Σ(゚д゚lll)ガーン、、、これも相対パスやないかい! Chromeのコンソールを見ると 「GET file:///C:/xxxx/EPubReader/css/stylesheet.css 」 と嘆かわしいログが出ている、、、トリニイカナイデ!
画像の埋め込み
バイナリデータが手元にあって、画像が表示できない苦悩(´・ω・`) サーバは偉大だ、、、と今更ながらに思うこの閉塞感。。。 imgタグのアクセスを擬似的にメモリデータに持ってこようとか 頭の中ではいろいろぐるぐるしたけど、 imgタグのsrcにバイナリを埋め込む方法がある事を知る。 細かいファイルとかをこうやって埋め込んでアクセス数を減らすテクがあるのか。。。 知らなかったけどこれ使える! zipやらutf8のライブラリやらjqueryやらでワラワラjsファイルが増えていってるけど クライアントのみである事を活かす為にはアクセス数など気にしない!base64.jsを追加。
マッテタヨ(∩´∀`)∩クジラタン!
またも問題発生
200k位の画像になってくると「"Maximum call stack size exceeded"」と出るのだ。 。。。。まぁスタック足りないだろうな、、、と1日置く。 原因はzipライブラリの59行目にあった。
var blob = String.fromCharCode.apply(null, this.data);
これに大きめのデータを突っ込むとアウトらしいので
  File.prototype.getBlob = function (dataArray) {
 var blob;
    try {
        blob = String.fromCharCode.apply(null,dataArray);
    } catch (err) {
     //データを分割して変換してつなげる
     var leng = dataArray.length;
     var dataLength = 100000;
     var loopNum = Math.ceil(leng / dataLength);
     var blobData = new Array();
     blobData[loopNum] = new Array();
 
     for ( var idx = 0; idx < loopNum; ++idx ) {
      var dataIdx = idx * dataLength;
      var splitData = dataArray.slice(dataIdx,dataIdx + dataLength);
      blobData[idx] = this.getBlob(splitData);
     }
     blob = blobData.join("");
    }
    return blob;
  };
って関数を追加してやった。 もちろんこれだと100000でスタックエラー出たら永久ループじゃん!こうすれば高速だ。 ってありそうだったけど、まぁ応急処置だとご勘弁を。 一応手元にある画像ではこれでエラーはなくなったけど、 多分何らかの問題はまた出てくるんだろうな。。。
公開までの道のり
・SVGファイル指定してあるとどうもダメみたい。 ・スタイルシートを当てないとダメ ・EPUBファイルがJavaScript依存とかしているとなんかもっと大変な気がしている。 ・YouTube埋め込んで合ったら、、、、大丈夫なのかな? など。まぁ公開までいけるかは微妙だけど、これで文章読む事位はできるようになる。 JavaScriptで何やってくれんねん。って感じですが、いろいろできるもんです。 ChromeStore上にEPUB系がまだないみたいなので 少し頑張ってみようかな?とは思っています。

2012年2月2日木曜日

EPUBファイルの作り方

EPUB作りたいって人がいたので少しJavaで実装してみた。

EPUBとは
EPUBとは電子書籍の形式で最近3.0が仕様確定したのは 知ってたんだけど、内容自体はZIP形式で少し特徴があり 単純にZIPしてはいけないので注意が必要。
EPUBの構成
・mimetype (非圧縮ファイル) ・META-INF/container.xml (構成ファイルを指定) ・EPUB(任意)/****.opf (構成ファイル) という構成。 opfファイルの中身やその後の構成についてはEPUBの仕様説明になるので 一応ここから割愛。
mimetypeについて
ファイルの内容自体は固定で 「application/epub+zip」を書き込むファイルを作ってやる。 特徴なのは「非圧縮で先頭」である必要があるので ZipOutputStreamを開始して最初のエントリーを以下のように非圧縮にしてやれば良い。
        String mime = "application/epub+zip";
        ZipEntry entry = new ZipEntry("mimetype");
        byte[] mimeData;
  try {
   mimeData = mime.getBytes("UTF-8");
  } catch (UnsupportedEncodingException e1) {
   throw new RuntimeException(e1);
  }
        entry.setSize(mimeData.length);
        entry.setCompressedSize(mimeData.length);
        entry.setCrc(0x2CAB616F);
        entry.setMethod(ZipEntry.STORED);

        // 出力先 Entry を設定する。
        try {
   zos.putNextEntry(entry);
         zos.write(mimeData);
         zos.closeEntry();
  } catch (IOException e) {
   e.printStackTrace();
  }

META-INF/container.xmlについて
これは単純に構成ファイルの位置指定なので
    private String OPF_PATH = "EPUB";
    private String OPF_FILE = "book.opf";

 private void createContainerXml() {
     String xml = "<?xml version=\"1.0\"?>";
     xml += "<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">";
     xml += "<rootfiles>";

     String fileName = OPF_PATH + "/" + OPF_FILE;
     xml += "<rootfile full-path=\"" + fileName + "\" media-type=\"application/oebps-package+xml\"/>";
     xml += "</rootfiles>";
     xml += "</container>";

     try {
      ZipEntry entry = new ZipEntry("META-INF/container.xml");
   put(entry,xml.getBytes("UTF-8"));
  } catch (UnsupportedEncodingException e) {
   throw new RuntimeException(e);
  }
 }

みたいな感じで固定の値を設定してあげる。 put()関数は単純にZipOutputStreamにエントリーしている関数です。
opfファイルについて
META-INF/container.xmlファイルに指定したopfファイルに EPUBが構成するようなファイルを指定していくらしい。 EPUBのサンプルとしてGoogle Codeにおいてあったのでこちらで実験した。
ZIPする時の注意点
実際の時とは違うんだけど、Windowsのファイルシステムでやっていると 「¥」でパスを設定してしまい、AdobeのViewerはそれを認識できない(EPUBの仕様?)ので それを変換する必要がある。
     entryName = entryName.replaceAll("\\\\", "/");
おなじみのですね><v それと将来的にはメモリ上でやるのでこの失敗はないと思うんだけど、 ファイルシステムを検索してEntryを作っていると 「/META-INF/container.xml」 とEntryする事もあり、ZIP自体はこれでもOKなんだけど、 「META-INF/container.xml」 としてやらないと読み込んでくれなかった。 ※これもViewerの仕様がEPUBの仕様かは不明。 一応これでサンプルのファイルを利用してZIPで固めたらOKだった。
サービス化を考えている友人へ
固めるところは技術的に見れば簡単なんだけど、 実際は「opfファイル」をサービスとして提供しなければ行けないところで 本を作っている人がこれを書きこむってのはサービス上はありえないわけで、 まぁこんな圧縮程度で止まっている場合ではないって事です。

2012年1月21日土曜日

jQueryでドラッグ&ドロップ

自分用のブログエンジンを作っていて、
要素をドラッグ&ドロップしながら章立てできるようなものにしたいなと思い
要素をどんどん追加していこうと思い、
最初draggable,droppable辺りを利用していたんだけど、
追加したい場所や、追加した要素の順番を変更したりと
sortableが必要だったので少し書いてみる。


何がしたいか?
・ドラッグして要素の追加 ・ドラッグしたものは残す ・ドラッグしたものを変更 ・ドロップする場所は順序変更が可能 これによりコンテンツを編集していく感じ
ソートの準備
まずソートしたい要素に対して
 $(".content-box").sortable({
  placeholder:'ui-state-highlight',
  connectWith: $('.content-box'),
  axis:'y',
  update : function (event,ui) {
   //順序を変更した後
  },
  receive : function (event,ui) {
   //他から要素が追加された場合
  }
 });
と行います。 placeholder:移動する場所に対してのスタイルを指定します。 これで色の変更などを行なって移動を行いやすくします。 connectWith:移動できる場所を自分自身のみにしています。 axis:yを指定して縦方向だけ移動できるようにしています。 update,receiveはイベントですが、 updateは順序を変更した場合のみに発生、receiveは他から要素が入ってきた時です。 receive発生時もupdateは発生します。 一旦はこれだけでcontent-box内の要素の順序変更ができるようになります。
ドラッグしてくる側の設定
ここが一番苦労したというか気づかなかったというか。 ドラッグする側に対してもsortableで対応しないと、ドラッグできないのではないか?と 思ってそうしてたのですが、cloneが効かなかったり、何かとやりたい事に対して うまくいかなかったのですが、まず ドラッグしたい要素に対して、ドラッグをできるようにします。
 // アイテムのドラッグ
 $('.item').draggable({
    cursor: 'move', 
    opacity: 0.7,
    connectToSortable: $('.content-box'),
          helper:'clone'
 });
cursor:カーソルの変更 opacity:移動時の透明度 connectToSortable:指定した場所にはsortableの対象として扱ってくれるので 上記で指定した「ui-state-highlight」をやってくれます。 helper:移動中の表示をコピーにしています。 これに対しては実際はfunctionも扱えるので 移動する時に表示を変えたい場合はfunctionを指定して戻り値でタグを返します。
まぁこれで大体できるんだけど、、、
私の場合はitemの部分とcontent-box内の要素が違います。 この状態で行うとitemのタグがそのまま配置されてしまうので receiveイベントの時に
    var addTag = createContentItem(itemName);
    var dropItem = $(this).children(".item");
    dropItem.after(addTag);
    dropItem.remove();
新しくタグを追加してあげて、ドロップした要素を削除しています。 当初はどちらもsortableを指定しないと要素の追加場所がわからないのではないか? と思って指定していたのですが、片方をドラッグにしてcloneを行えば良い事に気付いたので 後は追加要素を追加削除するだけでした。 connectToSortableに気づいたら非常に簡単にできました。 ただsortableでhelperにclone指定をした場合にうまく動作しなかったのが 少し解せない感じでした。

2012年1月18日水曜日

MacでXtionを動作させる

Macでの動作もしておこうと思ったので

ここ

を参考にしました。

前準備
portが1.9だと失敗したので2.0系のupdateから開始
sudo port selfupdate
sudo port install libtool
sudo port install libusb-devel +universal
かなりインストールに時間がかかりました><。
ドライバ等
本家のunstableからMacOS用の openni-bin-dev-macosx-v1.5.2.23.tar.bz2 nite-bin-macosx-v1.5.2.21.tar.bz2 sensor-bin-macosx-v5.1.0.41.tar.bz2 をダウンロードしてくる。 それぞれinstall.shが存在するのでそれらを実行。これでOKでした。 sudoが必要なものはsudoで行なってください。 通常のSampleはこれで動作するのですがJava実行だと
java.lang.UnsatisfiedLinkError: no OpenNI.jni in java.library.path
が出てしまいます。
java -Djava.library.path=/usr/lib -jar jarファイル
で動作しました。 Mac miniですが640x480だと重くてデータを飛ばせなかった><。

2012年1月9日月曜日

Xtionがやってきた。

Xtionがやってきたので、少し触ってみる。
XtionはKinectと同様にOpenNIを利用したモーションキャプチャーです。
Kinectは現状ではハードウェアが商用利用は不可ですが、Xtionは商用利用が可能です。

特に商用利用が目的ではないのですが、
USBバスで動作するのでこちらを購入したというところが強いのですが、
Kinectで行うとハック感がなくなるかな?というところで、
Xtionにしました。Xtion Pro Liveです。

OpenNIのインストール
まず付属してたDVDだと古そうだったので 本家からPackageを入れます。 古い記事だと64ビットだと動作しないみたいな話がありますが この時点では動作するようです。 Macについては後述しておきました。
どんなものを作るのか?
友人が電子書籍系をやっているのだけど「マイノリティ・リポートやろうぜっ」って感じだったので そういうものを作ろうと思った。 動かすのはiPadのようなタブレットだった。iPadにOpenNIとか非現実的な事だし、 私自身がxcodeでソレ系のアプリを作る気もないし、 とにかく相手にプッシュする方法が必要だったので、 相手がサーバでこっちがクライアントという選択肢だと、 こっちから接続しなきゃいけないし、何かと不便に感じた。 ちょうどRFCにWebSocketが載ったところだったのでこっちをサーバにしてWebSocketを実装する事にした。 Javaで動作するものを作ろうと思ったのでJettyでWebSocketサーバを作成して、 そこから何らかのクライントでWebSocketにアクセスしてもらって通信をやろうと思った。
WebSocketを作る
ここを参考に サーバサイドは作成した。 実際はChatRoom的な管理は必要ないわけだけど、 例えばHTML(WebSocketでアクセスできるもの)で資料を作って それをみんなの端末で見る~。。。なんてI/Fも想定してたりしてなかったり。
HandTrackerを発見!
はじめはサンプルの「UserTracker」を元に作り始めた。 Userを認識してSkeletonCapabilityで骨格を認識させて、 それで手の動きを見ようと考えていたんだけど、 「GestureGenerator」「HandsGenerator」が存在する事を知り ココに「HandTracker」のサンプルがあるので拝借。 ※っていうか最新のパッケージならローカルにも展開されてます。 サンプル読んだおかげで大体の動作は理解できた。 ・Contextやら読み込んで各Generatorを初期化 ・イベントに対してObserverを作成(コールバック) ・ループして ・ノードを更新してノードからデータを取得 ・何かやる みたいな流れみたい。
HandsGeneratorでジェスチャーを認識
このサンプルにGestureGeneratorに対して"Click"とあり、 これがセンサーに向かって手をpullしたり、調べた限りだと"Wave"などを認識できるようなんだけど 「これにLeftとかRightをかあれば楽?」と考えたりもしたけど この辺りを読むと サンプルの通りGestureGeneratorで認識させてからHandsで行うのが通常の使い方みたい。 サンプルは ・GestureGeneratorに"Click"を登録 ・イベント発生でHandsGeneratorのトラッキングを開始 ・Update系のイベントでそのポイントを取得して履歴に残す ・ポイントの履歴を描画 ・手の認識が終わったらGestureGeneratorの"Click"を再度登録 みたいな流れ。 Updateで履歴があるのでこれを使って
 public static SocketType getType(float modX, float modY, float modZ) {

  SocketType type = SocketType.NONE;
  if ( Math.abs(modY) < margin ) {
   if ( modX < (movement*-1) ) {
    type = SocketType.LEFT;
   } else if ( modX > movement ) {
    type = SocketType.RIGHT;
   }
  } else if ( Math.abs(modX) < margin ) {
   if ( modY < (movement*-1) ) {
    type = SocketType.DOWN;
   } else if ( modY > movement ) {
    type = SocketType.UP;
   }
  } else {
   if ( modZ < (movement*-1) ) {
    type = SocketType.ZOOMIN;
   } else if ( modZ > movement ) {
    type = SocketType.ZOOMOUT;
   }
  }
  return type;
 }
みたいな座標の移動量を計算させて、どっちに動いているかを判定 ※SocketTypeってのはオリジナルのEnumです。 あとはソケットで送るだけ。 まぁこれだと調整値によって認識とかしまくりとか認識しないとかさんざんなので もう少し何かする必要があるかな?
感想
何かライブラリ使ってもう少し何かできそうな流れだったけど、 自分で理解するのも含めて作ってみた。 サンプルを理解したところをもう少し書きたかったんだけど、 またそれは別の機会でもあれば。

2012年1月6日金曜日

undefinedという値について

引数に「undefined」って指定してあるのなんで?と言っていた人がいた。 jQueryの話だったけど、JavaScriptの癖(癌)を利用したトリックだと推測。 undefinedはグローバルスコープの変数だから、 引数に対してundefinedを代入して、ローカルスコープに持って来て ロジックによっては処理を早くしたり、 undefinedの値をローカル内で保証できる状況になるという仮説を立てたけど、 「引数間違って指定したら失敗するんじゃね?」 と思ったので試してみた。
<script type="text/javascript">
<!--

function undefinedTest1(arg1) {
 if ( arg1 === undefined ) {
  console.log("1");
 }
}

function undefinedTest2(arg1,undefined) {
 if ( arg1 === undefined ) {
  console.log("2");
 }
}

var val;
console.log("undefined");
undefinedTest1(val);
undefinedTest2(val);

console.log("変数に値を入れてみる");
undefined = true;
undefinedTest1(val);
undefinedTest2(val);

console.log("第2引数に値を入れてみる");
undefinedTest2(val,"test");

// -->
</script>
でコンソールは
undefined
1
2
変数に値を入れてみる
2
第2引数に値を入れてみる
まず、関数が正常に動作している事を確認。 グローバルスコープのundefinedに値を代入すると関数1の値は不可能になり、 間違って引数指定をすると、undefinedに値が代入されて、関数2もアウト。 まぁ十分テストされていれば第2引数の指定はないだろうから危なくはないのかな? 思ったとおりの動きだったのでひとまず満足。