Home > develop > Java Archive

Java Archive

Hadoop Streamingを利用してJavaScriptでMap Reduce

久々のBlog更新、というわけでリハビリがてらJavaScriptで軽く遊んでみたいと思います。

いま、巷で流行ってるMapReduceのオープンソース実装Hadoopは「Hadoop Streaming」という標準入出力でデータのやりとりができる仕組みを使って、 Hadoopの実装言語であるJavaにとらわれず、RubyやPerlなど他の言語でもMap+Reduceの処理ができることが1つのウリになっています。 で、僕たちwebエンジニアはみんなJavaScript大好きなので、「JavaScriptでもMap Reduceやりたい!」という流れになるのは必然です。 そこで、試行錯誤でいろいろ試してみると割とさっくり出来たのでそのメモを残しておきたいと思います。

環境の整備

Mac OSX上のVMWare FusionにCentOSの仮想マシンを2台立ち上げて、環境セットアップしました。以下のような手順で環境整備しました。

仮想HWの設定

仮想マシンのイメージファイルですが、僕は毎回thoughtpoliceのサイトのものを利用しています。ここからCentOS 5.3のイメージを落としてFusionでロード。

1つハマったのが、同じイメージをコピーして複数台起動すると、MACアドレスがかぶって同時に複数台の仮想マシンがNWに接続できない点。なので、仮想マシンを最初に起動させる前にMACアドレスの設定をしておくことをおすすめします。

MACアドレスは、vmxファイルのuuid.bios値をもとに自動生成されるようで、この値を最初から全仮想マシンでバラバラにしておけばOKです。と、いっても有効な値の範囲があるので、最後の1byte値だけズラしておけばOKだと思います。たとえば、上記サイトからイメージを取得した場合、uuid.bios値の最後は「7d」になっているので、僕はこの値を「7c」「7b」なんかにズラしておきました。(Fusion上から正規の方法?で、マシンをクローンする方法がよくわからなかったので、こういう強引な方法を取っています。綺麗な方法をご存知の方いらしたらぜひ教えてください!)

ネットワーク設定

/etc/hostsを設定して2台のマシンの名前解決。細かな話ですけど、ホスト名には「_(アンダーバー)」は使えないのに要注意。最初「hdp_01」みたいなホスト名にしていて、実行時エラーが出てドはまってたときに全然気づかなかった。

  • 192.168.1.15 hdp-01
  • 192.168.1.16 hdp-02

SSHの設定

マシンはそれぞれ自分自身(localhopst)に対して、パスフレーズなしでSSHでログインできるように公開鍵を設定しておきます。

  • ssh-keygen -t rsa -P ""
  • cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
  • chmod 600 ~/.ssh/authorized_keys

また、MasterのマシンからSlaveのマシンに対しても同様の設定をしておきます。今回はMasterはhdp-01, Slaveはhdp-02という設定です。hdp-01上で、次の設定をします。

  • cp ~/.ssh/id_rsa.pub ~/.ssh/id_rsa_master.pub
  • scp ~/.ssh/id_rsa_master.pub hdp-02:/home/katsuma/.ssh/
  • chmod 600 ~/.ssh/authorized_keys

また、hdp-02上で、次の設定をします。

  • cat ~/.ssh/id_rsa_master.pub >> ~/.ssh/authorized_keys

これで、Masterからlocalhost, およびSlaveに対してパスフレーズなしでログインできることを確認しておきます。

セキュリティの設定

後でハマるのが面倒なので、iptablesやSELinuxは切っておきます。必要であれば適宜設定してください。

  • sudo /etc/init.d/iptables off
  • sudo /etc/sysconfig iptables off
  • sudo vi /etc/sysconfig/selinux
    • SELINUX=disabled に変更

Shellの設定

.bashrcなんかに以下の設定をしておくと便利です。僕はzsh派なので、.zshrcに記述しました。

# hadoop
alias cdh='cd /home/katsuma/hadoop/latest/'
alias dls='/home/katsuma/hadoop/latest/bin/hadoop dfs -ls'       # ls
alias drm='/home/katsuma/hadoop/latest/bin/hadoop dfs -rm'       # rm
alias dcat='/home/katsuma/hadoop/latest/bin/hadoop dfs -cat'     # cat
alias drmr='/home/katsuma/hadoop/latest/bin/hadoop dfs -rmr'     # rm -r
alias dmkdir='/home/katsuma/hadoop/latest/bin/hadoop dfs -mkdir' # mkdir
alias dput='/home/katsuma/hadoop/latest/bin/hadoop dfs -put'     # HDFS に転送
alias dget='/home/katsuma/hadoop/latest/bin/hadoop dfs -get'     # HDFS から転送
alias dcpfl='/home/katsuma/hadoop/latest/bin/hadoop dfs -copyFromLocal'
alias dcptl='/home/katsuma/hadoop/latest/bin/hadoop dfs -copyToLocal'

Javaのインストール

Sunのサイトに行き、 「Java SE Development Kit (JDK)」を選択して、インストーラをダウンロード。その後、以下の手順でインストールできます。

chmod +x jdk-6u7-linux-i586-rpm.bin
sudo ./jdk-6u7-linux-i586-rpm.bin

Hadoopの整備

Hadoopのインストール

Hadoopは今日時点で最新の0.20.0を利用しました。インストール場所はどこでもいいのですが、僕はホームディレクトリ直下に専用のディレクトリ掘って、いろんなバージョン試せるようにこんな感じで設置してます。ここから最新版をDLが可能です。

  • cd path/to/hadoop-0.20.0.tar.gz
  • tar zxvf hadoop-0.20.0.tar.gz
  • mv hadoop-0.20.0 ~/hadoop/
  • ln -s hadoop-0.20.0 latest

他のバージョンを試したくなったら~/hadoop/以下に展開して、シンボリックリンクを付け直すとOKですね。

Hadoopの設定

特に凝ったことはしていません。全ノードともに同じ設定である必要があるので、Masterで設定しちゃって、それをrsyncで他のSlaveと同期をとるのがよいと思います。

conf/hadoop-env.sh
export JAVA_HOME=/usr/java/latest
conf/core-site.xml

<configuration>
  <property>
    <name>hadoop.tmp.dir</name>
    <value>/home/${user.name}/hadoop/latest/tmp</value>
  </property>
  <property>
    <name>fs.default.name</name>
    <value>hdfs://hdp-01:9000</value>
  </property>
</configuration>

conf/hdfs-site.xml

<configuration>
  <property>
    <name>dfs.replication</name>
    <value>1</value>
  </property>
</configuration>

conf/mapred-site.xml

<configuration>
  <property>
    <name>mapred.job.tracker</name>
    <value>hdp-01:9001</value>
  </property>
</configuration>

conf/masters

      hdp-01

conf/slaves

      hdp-02

ここまで設定できたら、masterのイメージをrsyncしておきましょう。

cd ~/hadoop/
rsync -r hadoop-0.20.0/ hdp-02:/home/katsuma/hadoop/hadoop-0.20.0

SpiderMonkeyの導入

さて、やっとHadoopの設定が終わったので、次にJavaScriptの処理系の導入です。 Hadoop Streamingは前述の通り、標準入出力の仕掛けを使って実現されているので、さすがにブラウザの処理系をそのまま利用することができません。

そこで、FirefoxのJavaScriptのエンジンであるSpiderMonkeyを利用することにします。 SpiderMonkeyはファイルを入力としても処理できるし、irbのように対話型シェルとしても利用できるJavaScriptの処理系です。また、ブラウザを利用しないので、標準出力関数print、標準入力関数readlineが実装されてあるので、今回はこれを利用すればうまくいきそうです。

では、SpiderMonkeyを各ノードに導入します。これも最新版を導入します。あらかじめ、make, gccあたりをyumで入れておきましょう。

参考:SpiderMonkeyのインストール

  • wget http://ftp.mozilla.org/pub/mozilla.org/js/js-1.8.0-rc1.tar.gz
  • tar zxf js-1.8.0-rc1.tar.gz
  • cd js/src
  • BUILD_OPT=1 make -f Makefile.ref
  • sudo install -m 755 Linux_All_OPT.OBJ/js /usr/local/bin

Map, Reduce処理のJavaScriptを記述する。

では、Map, Reduceそれぞれの処理をJavaScriptで書きます。今回はサンプルとしてよくある、ワードカウントの処理を行ってみます。

map.js

map.jsは標準入力された文章を半角スペースごとに分けて、CSVの形式に整形します。JavaScript1.7相当の機能が利用できるので、Array.prototype.forEachが利用できることもポイントです。

#!/usr/local/bin/js

var line="";
while ((line = readline())!= null){
        var words = line.split(" ");
        words.forEach(function(w){
                print(w + "," + 1);
        });
}

reduce.js

reduce.jsは、map処理されたCSV形式の入力に対して、counterオブジェクトで各単語をカウントしていきます。

#!/usr/local/bin/js

var counter = {};
var line = "";
while ((line = readline()) != null) {
        var words = line.split(",");
        var word = words[0]
        if(!counter[word]) counter[word] = 1;
        else counter[word]++;
}

for(var k in counter){
        print(k + ":" + counter[k]);
}

これらのJavaScriptファイルをhadoop/latest/script/あたりに保存しておき、全ノードで同期させておきます。

Hadoopの起動

最初にHDFSをフォーマットしておきます。Masterで次の処理を行います。

cdh
./bin/hadoop namenode -format

MasterでHadoopを起動します。

bin/start-all.sh

MapReduce用の適当な入力ファイルを作成します。こんな内容のファイルを$HADOOP_HOME/input/file1に作成します。

we are the world we change the world

このファイルをHDFSに転送します。dputは bin/hadoop dfs -putのエイリアスです。

dput input/file1 in/count

転送されてあるかどうかは、dls(bin/hadoop dfs -ls)で確認できます。

katsuma@hdp-01 ~/hadoop/latest
$ dls
Found 1 items
drwxr-xr-x   - katsuma supergroup          0 2009-07-31 02:14 /user/katsuma/in

katsuma@hdp-01 ~/hadoop/latest
$ dls in
Found 1 items
drwxr-xr-x   - katsuma supergroup          0 2009-07-31 02:56 /user/katsuma/in/count

入力用ファイルの存在が確認できたので、これでやっとMapReduce処理ができます。

./bin/hadoop jar ./contrib/streaming/hadoop-0.20.0-streaming.jar -input in/count -output out/count -mapper "js /home/katsuma/hadoop/latest/script/map.js" -reducer "js /home/katsuma/hadoop/latest/script/reduce.js"

すると、ゆったりですけど処理が進んでいきます。

packageJobJar: [/home/katsuma/hadoop/latest/tmp/hadoop-unjar4327209233542314881/] [] /tmp/streamjob4726696067913490494.jar tmpDir=null
09/07/31 10:45:05 INFO mapred.FileInputFormat: Total input paths to process : 1
09/07/31 10:45:06 INFO streaming.StreamJob: getLocalDirs(): [/home/katsuma/hadoop/latest/tmp/mapred/local]
09/07/31 10:45:06 INFO streaming.StreamJob: Running job: job_200907310237_0013
09/07/31 10:45:06 INFO streaming.StreamJob: To kill this job, run:
09/07/31 10:45:06 INFO streaming.StreamJob: /home/katsuma/hadoop/latest/bin/../bin/hadoop job  -Dmapred.job.tracker=hdp-01:9001 -kill job_200907310237_0013
09/07/31 10:45:06 INFO streaming.StreamJob: Tracking URL: http://hdp-01:50030/jobdetails.jsp?jobid=job_200907310237_0013
09/07/31 10:45:07 INFO streaming.StreamJob:  map 0%  reduce 0%
09/07/31 10:45:23 INFO streaming.StreamJob:  map 50%  reduce 0%
09/07/31 10:45:45 INFO streaming.StreamJob:  map 50%  reduce 17%
09/07/31 10:48:49 INFO streaming.StreamJob:  map 100%  reduce 17%
09/07/31 10:49:10 INFO streaming.StreamJob:  map 100%  reduce 100%
09/07/31 10:49:15 INFO streaming.StreamJob: Job complete: job_200907310237_0013
09/07/31 10:49:15 INFO streaming.StreamJob: Output: out/count

HDFS上のout/count/以下に結果が格納されたファイルができているので確認してみましょう。

katsuma@hdp-01 ~/hadoop/latest
$ dls out/count
Found 2 items
drwxr-xr-x   - katsuma supergroup          0 2009-07-31 10:45 /user/katsuma/out/count/_logs
-rw-r--r--   1 katsuma supergroup         43 2009-07-31 10:48 /user/katsuma/out/count/part-00000

katsuma@hdp-01 ~/hadoop/latest
$ dcat out/count/part-00000
are:1
change:1
the:2
we:2
world:2

まとめ

SpiderMonkeyのような処理系を用意することで、JavaScriptでもHadoopを使ってMapReduceできることが確認できました。標準入出力さえサポートされてあれば、理屈的にはどんな言語でもMapReduceできるので、MapReduceには興味あるけどJavaということで敬遠していた方は、ぜひいろんな言語で試していただければと思います。

開発のTips

なんだかんだ言って、最初開発にかなり苦労しました。と、いうのもシンタックスエラー以外は、実行しないとどうなるかよくわからないものなので、エラーで落ちたときにどうデバッグすればいいか悩みました。 ただ、よく考えれば、「標準入力→Map処理→Mapの標準出力→Reduceの入力→Reduceの標準出力」という流れになるので、たとえば今回のJavaScript実装の場合、次のように実行することでHadoopを介さなくとも動作確認は可能です。

cat input/file1 | js script/map.js | js script/reduce.js

まずは、このように手元でパイプでつないで結果を調べてみる、というのが手かと思います。実際は手元でうまく動いてもHadoop上でRuntimeエラーが起きる場合も多いので、そのときはlogディレクトリ以下にできるログファイルを調べるのがいいと思います。

また、エラーが発生するとJavaの例外のStackStraceが表示されるので、敬遠せずにそこからHadoopのソースを直接追いかけるのは何だかんだで早い解決法でした。コメントが割と充実しているので、そこからStreaming用のコードのバグを辿るのも難しくはないと思います。

Tokyo-Joggingの実行方法のチュートリアルビデオ

  • 2008年10月 1日 09:52
  • Java

まともな説明付けないままGoogle CodeにuploadしたTokyo-Joggingですが、地味に「動かし方わかんない!」メールが届いてきてるのでreadmeをやっと書いて、実行までの一連の流れをビデオにまとめてみました。多分、文章読むより動いてるの見たほうがわかりやすい(はず)。しっかしiMovieて楽ですねー。


How to run Tokyo-Jogging from katsuma on Vimeo.

あと、Tokyo-Joggingについては実は国内からもいろいろな問い合わせをいただいていて、もう少しするとここでも報告できそうです。

Javaで手軽にORマッピング ActiveObjects

  • 2008年9月23日 02:44
  • Java

CakePHPで作成したアプリケーションに対して、Javaアプリケーションから簡単なDB操作(実際はselectだけできればOK)をする必要があって、生のSQLを書くか、それともいい機会だしORマッパのライブラリを勉強がてら使ってみるか、、なんて悩んでいました。保守性を考えるとやっぱり生SQL案は無しで、ORマッパを使うことにしたのですが、Hibernateはどうもリッチすぎる印象があってなかなかやる気が進みませんでした。大規模アプリ開発ならまだしも、今回は上で書いた通り、最低限の参照だけできればそれで事足りる状況だったので、そのためだけにわざわざ設定用のXMLを書くのは気が引けていたのです。

3分で始められるORマッパ

3分間クッキングじゃないですけど、それくらい手軽に操作できるものをずっと探し続けていたら、ActiveObjectsなるプロジェクトに辿り着きました。

名前から分かるように、RailsのActiveRecordを強く意識して作られたもののようです。特徴としては、

  • 規約を重視(Convention Over Configuration)
  • XMLの設定ファイルは使わない
  • DDL作成機能 (migration)

が、あります。とにかく「できるだけすぐに使える」ものを意識、というわけですね。

とにかく動くものを早く作って、そこからチューニングをしていくことを求められることはやはり多いので、ActiveObjectsのこの姿勢はすごく好きです。

使い方

では、使い方について。今回は上で書いたとおり、CakePHPですでに作っているRDBに対してのアクセス、についてまとめたいと思います。

用意するもの

ActiveObjectsを利用するにあたって、用意するものは次の2つです。

  • activeobjects-x.x.x.jar
  • mysql-connector-x.x.x.jar

activeobjects-x.x.x.jarは公式サイトのDownloadの箇所から入手可能です。2008/09/23時点での最新バージョンは0.8.3のようです。

mysql-connector-x.x.x.jarはMySQLとの接続に対するドライバになるのですが、これはMySQLの公式サイトから入手可能です。2008/09/23時点での最新バージョンは5.1のようです。(僕は実際はもう少し古いバージョンのものを利用していますが、最新のものを利用することで特に問題はないと思います)

これらのファイルを入手して、クラスパスが通っているところに設置します。

EntityManagerの作成

まず、DBに接続するためのEntityManagerを作成します。これは接続情報をまとめたオブジェクトになります。

import net.java.ao.EntityManager;
...
EntityManager manager = new EntityManager("jdbc:mysql://" + db_host + "/" + db_database, db_login, db_password);

db_host, db_database, db_login, db_passwordはそれぞれ接続するRDBサーバのネットワークパス、データベース、接続ユーザ、接続パスワードになります。ここで接続に失敗するとSQLExceptionが発生するので、その際は接続ユーザ情報を見直してみましょう。

Entityの作成

次に、テーブルに対して1-1対応するEntityを作成します。Entityは各フィールドのgetter,setterメソッドを定義したinterfaceを作成するだけでOKで、implementsの部分は勝手に行ってくれます。すごく便利!(実際は自分でもimplementsすることもできるような)

たとえば次のようなテーブルUsersが存在するとします。

+-------------+------------------+------+-----+---------+----------------+
| Field       | Type             | Null | Key | Default | Extra          |
+-------------+------------------+------+-----+---------+----------------+
| id          | int(10) unsigned | NO   | PRI | NULL    | auto_increment | 
| name        | varchar(50)      | NO   |     |         |                | 
| email       | varchar(255)     | NO   | UNI |         |                | 
| password    | varchar(255)     | NO   |     |         |                | 
| created     | datetime         | YES  |     | NULL    |                | 
| modified    | datetime         | YES  |     | NULL    |                | 
+-------------+------------------+------+-----+---------+----------------+

ここでのUsers Entityは次のようになります。

import net.java.ao.Entity;

public interface Users extends Entity{
	
	public int getId();
	public void setId(int id);
	
	public String getName();
	public void setName(String name);
	
	public String getEmail();
	public void setEmail(String email);
	
	public String getPassword();
	public void setPassword(String password);
		
	public String getCreated();
	public void setCreated(String datetime);
	
	public String getModified();
	public void setModified(String datetime);
}

これだけ。簡単!

ここでは全カラムのgetter/setterのインターフェースを定義したけど、実際は利用するカラムのgetter/setterを用意するだけでいいと思います。あとこのgetter/setterのjavaファイルを作成するジェネレータ作っちゃうのもいいかもですね。

接続+実行

SQLの実行は先に作ったEntityManager経由で行います。たとえばUsersテーブルからid=10のユーザ情報を取得するにはこんなかんじです。

EntityManager manager = new EntityManager("jdbc:mysql://" + db_host + "/" + db_database, db_login, db_password);
Users[] users = manager.find(Users.class, Query.select().where("id = ?",10));

見たまんまなので使い方は超簡単なことがわかると思います。EntityManagerは当然使い回しまくるものなので、メインなクラスのコンストラクタあたりで作って、それを使い回すのがよさそう。

あと、データの挿入も同じ感じで書けて、たとえば

Users user = manager.create(Users.class);
user.setName("Jane");
user.setEmail("jane@hoge.com");
user.save();

こんな感じで書けちゃいます。

テーブル名とEntity名に注意

Cake使ってるとテーブル名は名詞の複数形にして、Modelのクラス名はその単数形にするのがルールになっています。なのでたとえば上の例だとModelで作成するときはUsersクラスではなく、Userクラスになります。

ActiveObjectsの場合、interfaceを作成するときにテーブル名とinterface名は同じにしておく必要があるようです。なので、テーブルで複数形の名前を利用している際は、interface名も同じように複数形にしないとEntityManagerのRDBに接続の際に例外が吐かれてしまいます。これについては正直ちょっと納得いかない制約でもあるのですが、うまく回避というか、対応付けるテーブル名を明示的に指定する方法をご存知の方がいらしゃいましたらぜひ教えていただければと思います。

一レコードだけselectするメソッドが無い?

APIをざっと眺めていて気づいた点としてCakePHPにおけるfind("last")のような1レコードだけselectするようなメソッドはどうも用意されていないようで、find系のメソッドは全部配列で取得されて、つまり複数レコードが取得されることが期待されるような作りになっているようです。

上の例でもあるようなPRY KEYの値をもとにレコードをもってくるときには1レコードだけ持ってこれるメソッドが用意されてあったほうがよさそうなのですが、そこは少し残念な点ではあります。

とは言え

まだまだ完璧に使い勝手がよい、とも言い切れない点も正直あるのですが、それでも下準備が多いHibernateと比較するとサクっとORマッピングを導入することができるActiveObjectsは十分に魅力的なライブラリではないでしょうか。ぜひ一度利用の検討を考えてもいいものだと思います。

Wii fitのジョギングゲームのようにGoogleストリートビュー上をジョギングしよう

  • 2008年9月 7日 22:46
  • Java

前回の「バランスWiiボードでGoogleストリートビューを操作するJSONP API」のネタのつづき。バランスボードでできるのだったらWiiリモコン(Wiimote)でもできるよね、というわけで、今回はWiimoteの加速度情報をJSONPで取ってきて、同じようにGoogleストリートビューを操作しています。

Wii fitのジョギングゲームのようにポケットにリモコンを入れてサクサク部屋の中で走ります。曲がるときは十字キーで曲がります。で、動いている様子はこんなかんじ。(初めてiMovieで編集作業してみた!)


Try to run on the google street view like a jogging game of wii fit from katsuma on Vimeo.

動作原理は前回と同じで、

  1. Wiimoteの加速度変化をJavaのサーバが取得
  2. localhost上のWebサーバ(Jetty)に加速度情報を通達
  3. リモートホストのサーバからJSONPで加速度を取得
  4. Google Map APIを呼び出し

の流れです。

ただ、それだけだと全く前回と同じなので、緯度経度情報の変化から走行距離と、その経路情報をリアルタイムに表示させてみました。右上の数字が走行距離、左上のGoogle Mapが経路情報になっています。この2つを表示させたら結構それっぽくなったんじゃないかなーと思います。

このあたりの処理はGoogle Map API使いまくりなわけですが、走行距離に関してはなかなか楽に求められないか調べまくった結果、GPointオブジェクトにdistanceFromメソッドなるものがあるみたいで、これを使うことで簡単に求められました。ソースコード的にはこのへん。

_TJ.prevPoint = _TJ.currentPoint;
_TJ.currentPoint = _TJ.marker.getPoint(); 
_TJ.distanceHistory += _TJ.currentPoint.distanceFrom(_TJ.prevPoint) /1000;

よくある2点間の距離を求めるWebサービスなんかはこれ使ってんじゃないかな。今回は、ジョギングするごとにマーカーを道の上に打っていき、その2点間の距離を加算しています。(なので、曲がり角にも対応)。また、あわせてマップ上に走った場所に経路情報としてラインを引いているので「ここは走ったけどここは走ってない」なログを残すことも可能です。

これ、うまくやると同じ時間帯で(Googleストリートビュー上の)近所を走っている人がいたらリアルタイムに表示させたりすると、ゲーム性も高まってなかなか面白いものができるんじゃないかなーと思っています。

コードは一式テスト的な意味もこめてGoogle codeでホスティングさせてみました。(流行のGitとやらに挑戦してみたかったけど、よくわかんなかったので断念)

ビルド方法なんかはGoogle codeのWikiに簡単に書いてますが、Windowsユーザの人はBluetoothスタック(デバイスドライバ)をWIDOMMのものを利用しないと、おそらくうまく動かないと思います。Macユーザの人は、デフォルトのBluetoothスタックでOKです。このあたり、Windowsはちょっと面倒。

あとはlib/*.jarにclasspath通したらビルドできるはず。実行したらWiimoteの裏フタをあけて「SYNC」ボタンを押すと、"Detected your wiimote" なメッセージを表示して、同期がとれるはずです。同期がとれたらWebページにアクセス。

上の通り、ノリでtokyo-jogging.comなドメインまで取ったので、できたらWebサービスの形でまとめることができたら、と思っています。まずはもうちょっと使いやすい形にまとめることですね。

バランスWiiボードでGoogleストリートビューを操作するJSONP API

  • 2008年8月20日 21:43
  • Java

先週高校の友達が家に泊まりにきたとき、バランスWiiボードを見て「これでGoogleマップ操作できたら面白そうじゃない?」とぽろっと言ったのをきっかけに「あれ、それできそうだぞ」と思ったので勢いで作ってみました。

動作としては直感的なものになっていて、足踏みするとどんどん進んでいって、左右に重心傾けると向きが変わって前後に重心を傾けるとズームが変わります。百聞は一見にしかずで、映像見てもらったほうが分かりやすいかと思います。


Google Street View by Wii Balance Board from katsuma on Vimeo.

構成

全体の構成としては次のもので成り立っています。

  1. バランスWiiボード
  2. Bluetoothレシーバ(iMac)
  3. 信号解析モジュール(Java)
  4. ローカルWebサーバ(Jetty)
  5. Javascript(JSONP)
  6. Webブラウザ(出力)

バランスWiiボードとBluetoothレシーバ

言わずもがな例のWii Fitのあれ。まずBluetoothデバイスとしてMacに繋ぐ許可を与える必要があるのですが(ペアリング)、ペアリングするためにはRVL Enablerを使えば何も考えずに簡単に検知できます。(via WiiリモコンとFirefoxをjavascriptでつなげるWiiRemoCom Firefox3対応版)RVL Enablerを起動して、バランスWiiボードを近づけて「Search」ボタンをクリックすれば認識してくれるはず。

Wii balance board

MacはいいけどWindowsはどうなんだ?という話になるのですが、おそらくUSB接続なんかのレシーバを用意すれば認識するはず?です。WindowsでWiiリモコンを扱う話もいろいろ出ているので特に問題はなくペアリングまでできるかと思います。

信号解析モジュール(Java)

ここが今回の一番のメイン。

最初はyappoさんが作られたPlusenをもとにどうにかしようと思っていたのですが、Cocoa-PerlブリッジのCamelBonesがLeopardでうまく動かなかったりビルドするものがやたら多かったり、そもそも僕はPerlが苦手だったりと山があまりにも多すぎたので断念。

で、他の方法を探していたらJavaでWiiリモコン、バランスWIiボードの信号を解析してラップしてくれるナイスなモジュールのWIiRemoteJなんてものを見つけたので、これを利用しました。実際はさらに下のレイヤーであるJNIでBluetoothの信号を受信するためのJSR-082 (Java Bluetooth API) 実装ライブラリも必要になります。今回はWindows XP, Windows Vista, MacOS X, Linuxと幅広いプラットホームに対応しているBlueCoveを利用しました。

これらの使い方も簡単で、Eclipseのビルドパスに入れておくだけでOKで、複雑なことは何もありません。WiiRemoteJのパッケージを解凍するとサンプルプログラムのWRLImpl.javaがあるので、これをビルド、実行するとWiiリモコンの加速度センサのx, y, z軸方向の変化の様子を見ることができます。

WiiremoteJ sample program

バランスWiiボードを操作するときもサンプルのWiiリモコンを扱うのと基本的に大きく変わりません。BalanceBoardListenerをimplementsするclassを用意しておいて、

BalanceBoar balanceBoard = WiiRemoteJ.findBalanceBoard();
balanceBoard.addBalanceBoardListener(this);//thisは自身のクラス

などとしておきます。addBalanceBoardListenerすると、BalanceBoardからのいろんなイベントが起こるので、そのリスナbuttonInputReceived, combinedInputReceived, disconnected, massInputReceivedを実装します。今回は重心の動きを知りたいので、実際はmassInputReceived(BBMassEvent evt)だけまじめに実装すればOK。BBMassEventは、バランスWiiボードを4分割(左上、右上、左下、右下)したときにそれぞれの領域でどれくらいの力が加わったか、のイベントとなります。こんな感じで取得できます。

double topLeft = evt.getMass(MassConstants.TOP, MassConstants.LEFT);
double topRight = evt.getMass(MassConstants.TOP,  MassConstants.RIGHT);
double bottomLeft = evt.getMass(MassConstants.BOTTOM, MassConstants.LEFT);
double bottomRight = evt.getMass(MassConstants.BOTTOM,  MassConstants.RIGHT);

ただ、実際は上下左右の4方向で取得できたほうが都合がいいので、これを補正します。試行錯誤の結果、単純にこんな感じでよさそう。

double top = evt.getMass(MassConstants.TOP, (int)Math.floor((MassConstants.LEFT + MassConstants.RIGHT)/2));
double right = evt.getMass((int)Math.floor(MassConstants.TOP + MassConstants.BOTTOM),  MassConstants.RIGHT);
double BOTTOM = evt.getMass(MassConstants.BOTTOM, (int)Math.floor((MassConstants.LEFT + MassConstants.RIGHT)/2));
double left = evt.getMass((int)Math.floor(MassConstants.TOP + MassConstants.BOTTOM),  MassConstants.LEFT);

要するに取得する領域を幅の平均でならしておきます。値もそれっぽいものが返ってきたので(多分)問題ないはず。

力の分布からバランスボードの踏み方を推定

上で書いた方法で4つの領域にどれくらいの力が加わったかが取得できるので、この値から実際に「どんな格好でバランスボードを踏んでいたか?」を推定します。

この推定方法が実際はかなり苦労しました。。多分一番時間がかかったところ。何せなかなか思い通りの推定ができないし、ノイズがものすごい量で入ってくるし、そのノイズを無視する閾値をどれくらいにするかを測定するために毎晩バランスボードを踏んだり降りたり、、な毎日でした。で、いろんなパターンを考えて試行錯誤した結果、辿り着いた推定方法はこんなアルゴリズム。実はかなり単純。

  1. 4方向の力(f1, f2, f3, f4)の割合rnを求める(rn=fn/Σfi)
  2. rnの最小値mn=min(rn)を求める
  3. mn以外のriが、全て閾値thを超えていた場合、rnの方向の真逆を踏んでいたこととする(mnが左の場合、上、右、下方向にかかる力が閾値を超えていた場合は右側を踏んでいたことと見なす)
  4. 閾値を超えていなかった場合は、真ん中を踏んでいたことと見なす

これだけ。超シンプル。でもこれくらいでちょうどいい感じの結果でした。あと結果の履歴をとって、そこから判断とかも行っていたのですが、そこまでやらなくても方向検知のレベルであれば、実際は問題のない結果なのでこれで良しとしました。

Webサーバ

で、方向検知までできるとWeb屋としてはこの結果をなんとかHTML+Javascriptにフィードバックしたい、となります。そこでローカルでWebサーバ(Jetty6.1)を立てて、リモートのWebサイトからJSONPでアクセスできるようなAPIを作ることにしました。

「リモートのサイトからlocalhostにアクセスできるの?」という話が出るかもしれませんが、ファイルシステムにアクセスするのではなく、あくまで外部サイトのホストがたまたまlocalhostだった、ということなので問題なくアクセス可能です。このあたり、P2Pストリーミングソフトなんかだと、クライアントソフトがデータをかき集めてきて、Webサーバ上のプレイヤがlocalhostから再生する、なんて流れになるのと同じように考えてみればいいと思います。

Javascript(JSONP)

今回WebサーバでJettyを選んだのはCometが簡単に実装できたことが理由に上げられます。JSONP+Cometな組み合わせはLingrが既に実績ありましたし、Cometじゃないとポーリングを繰り返ししすぎるのもなんだか気持ち悪いです。実際はバランスWiiボードからの情報は全部JSONPで拾うには速度が足りないのですが、よほど細かなゲームを実装とかしない限り、特に今回のストリートビューのデモのようなものであれば、気にならない取りこぼしでした。

Webブラウザ(出力)

出力についてはJSONPで取得した情報をもとに、ステップなのか、どちらかの方向に傾いているのか?をJavaScriptで判断し、Google Map APIの関数をコールしています。この判断も単純に、3回連続した値がきたかどうかで判断しています。たとえばCENTERに重心がかかっていたものが3回連続できたら、真ん中を踏み続けていたということからステップしていたんだな、のような感じです。

ハードウェアとJavaScript連携はおもしろい!

今回のデモはとりあえず動くものを作りたかったので、凝った作りに全くなっていないし、超汚いコードになっていますが、localhostのJavaScriptで特別なオブジェクトを生成することで、このモジュールをインストールしているかどうか、の判定もリモートのサイトで行うことも可能です。

また、Wiiリモコンやヌンチャクの情報もWiiRemoteJは扱うことができるので、JSONP APIを同じように用意してあげて、ExternalInterfaceを使えばFlashとも連携できて、あれ、これ普通にWii本体なしでゲーム作れるんじゃね??と、夢も膨らみまくりです。うおー。

ソースコード一式

超汚いまま整形なしですが、勢いで置いておきます。でもあとで改訂するかも。(これcodereposとか利用させてもらったほうがいいのかな)

MapWalker.zip

Javaのソースのtv.katsuma.walker.MapWalkerをビルド&実行して、バランスWIiボードのSYNCボタンを押して認識させた後にここをアクセス。うまく起動できれば実際の渋谷を「本当に歩く」ことができます!(起動できていない場合、アクセスしてもJavaScriptエラーでとまっちゃいます。ここエラー処理いまサボってます。あとSYNCボタンおして例外で落ちちゃうことが割とあるのですが何回か繰り返すと繫がると思います><)

Flash Player 9,0,115,0 だとRed5 0.6.3 でSharedObjectが扱えない

  • 2008年2月 5日 02:34
  • Java

すごく限定的で細かい問題だけど丸一日を潰されたので、その備忘録に。内容は件名の通りです。

2008/02/05 時点での最新のFlash Player(9,0,115,0)だと、Red5(0.6.3)で、SharedObjectがうまく扱えません。具体的に言うとActionScript3で

this.so = SharedObject.getRemote(this.session, this.nc.uri, true);
this.so.addEventListener(SyncEvent.SYNC, this.syncListener);
this.so.connect(this.nc);

こんな感じのコードを書いたときに、

this.so.setProperty('message', msg);

と、setPropertyでso.dataを変更したときにSyncイベントが上がってこない、という不具合。イベントが上がらないから、イベントリスナでキャッチできずに処理が進まない、という悪循環。もう少し細かく言うと、so.setPropertyする瞬間にorg.red5.server.net.protocol.ProtocolExceptionが発生して、ここで処理が止まっちゃってるのが問題です。

これ、1つ前のバージョンのFlash Player(9,0,47,0)だと発生しないから厄介です。9,0,115,0は9系の中でかなり大きな機能追加+修正がかかっているので要注意。9,0,47,0からの差分はリリースノートを参照。

さて、この問題ですがどうやらRed5開発陣には既に通っている問題のようです。

Javaで手軽にJSON - org.json.simple

Javaでサーバプログラムなんかを書いてクライアントにJSONでレスポンスを返す、なんてケースも最近は増えてきつつあります。小さなJSONなら自前で書いても問題はないのですが、やはりライブラリに頼ったほうがバグも少なくて開発も効率的です。 そんなときorg.json.simpleは、その名の通りシンプルながらもなかなか使えるいい感じです。 使い方は上のリンクにもあるテキストファイルの通りなのですが、簡単にメモっておきます。

Index of all entries

Home > develop > Java Archive

Search
Feeds

Return to page top