グラフ操作フレームワーク:Grafeo公開

Smalltalk

データを元にグラフを生成して、ぐりぐりとグラフを扱うにはRoassalという素晴らしいツールが既にある。一方で、小規模なグラフを作ったり操作したりできるツールの必要性もあって、ここ数年そういったソフトウェアの開発を行っていた。もとはFluoのグラフ操作部分だったのが、汎用的になるようにリファクタリングを重ねていったら、それなりに使えそうなものができたので公開することにした。

Grafeo

方向性としては、状態遷移図を作ったりするようなツールのフレームワークを目指しつつ、ちょっとしたグラフを描きたいといった場合に便利なツールとしても使えるようにしたい。
まだ公開していないが、このGrafeoをベースに既存のFluoを構築したり、umejavaさんのSState向けのUIを作ったりしているので、ぼちぼちと公開して行こうと思っている。

Liva PCにubuntu 14.04.2をインストールする

以前にアマゾンで買ったECSのLiva PCにubuntu 14.04.2のデスクトップ版をインストールした。
要するに、

  1. Ubuntuのサイトから14.04.2 LTSの64bit版をダウンロードする。
  2. Rufusを起動して「MBR UEFIコンピュータのためにパーティション構成」を選んで適当なUSBメモリに書き込む。
  3. Liva PCのBIOSを立ち上げて(電源オン時にDELキーを押す)、BOOTでUSBデバイスを優先にする。
  4. あとは普通にインストール。

だけでよかった。lubuntuも同様。

Raspberry pi model B(Raspbian wheezy版)にROS indigoをインストールする

この記事はRaspbian wheezy用のROS indigoビルドについて書いている。jessie用については別記事を参照のこと。

Raspberry piにROS/indigoをインストールする。現時点でRaspberry pi用のROSのバイナリパッケージは提供されていないようなので、直接ビルドしていくことになる。手順の概要は以下の通り。

  1. Raspbianをインストールする。
  2. ROSビルドの設定を行う。
  3. ROS本体をビルドする。

Raspbianのインストール

まず、Raspberry piのダウンロードページからNOOBSのZIPファイルをダウンロードする。

ダウンロードするしたZIPファイルを適当なフォルダに展開し、FATフォーマットされたSDカードにコピーする。

Raspberry piを適切にセットアップし、インターネット接続できるようにネットワークケーブルを接続したあと、NOOBSをコピーしたSDカードを挿入して起動する。

起動後の画面で日本語を選び、Raspbianのインストールを選んでインストールを始める。

このあたりの手順は、Raspberry piのセットアップページを参照する。

一般的なサイトで紹介している設定を済ませておくと良いだろう。

ROSビルドの設定

この手順はROSのページを参考にしている。要は忠実に一つずつこなしていくことだ。

2016/06/13追記
この記事はRaspberry piのOSであるRaspbianのバージョンがwheezyの頃の内容である。現時点でのRaspbianはjessieになるので、インストールするパッケージは異なる。この場合は別記事を参照してほしい。

なお、自分のRasperry piがどのバージョンのRaspbianで動作しているかどうかは、以下のコマンドで確認できる。

$ lsb_release -a
No LSB modules are available.
Distributor ID:	Raspbian
Description:	Raspbian GNU/Linux 8.0 (jessie)
Release:	8.0
Codename:	jessie
$ 

Codenameの行に動作中のバージョンが表示される。
2016/06/13追記終了

リポジトリの設定と更新

ROSのリポジトリを設定し、認証キーを追加する。

sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu wheezy main" > /etc/apt/sources.list.d/ros-latest.list'
wget https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -O - | sudo apt-key add -

aptのパッケージインデックスを更新する。

sudo apt-get update
sudo apt-get upgrade

ビルド用パッケージのインストール

ビルドに必要なパッケージをインストールする。

sudo apt-get install python-setuptools python-pip python-yaml python-argparse python-distribute python-docutils python-dateutil python-setuptools python-six
sudo pip install rosdep rosinstall_generator wstool rosinstall

rosdepを初期化する。

sudo rosdep init
rosdep update

catkinのワークスペースを作成する。

mkdir ~/ros_catkin_ws
cd ~/ros_catkin_ws

Variantの選択

ここで、ROSのどんなパッケージをビルドするかを選ぶことになる。推奨されているvariantのはros_commだが、他にdesktopやrobotなどがある。目的に応じて好きなものを選べばよい。variantはREP 131のページで紹介されている。

適当なvariantを選んだら、以下のようなコマンドでインストールの設定を行う。なお、下はros_comm用のコマンドなので、選んだvariantに応じてros_commの部分(3カ所ある)をvariantの名前で置き換えて実行する。

rosinstall_generator ros_comm --rosdistro indigo --deps --wet-only --exclude roslisp --tar > indigo-ros_comm-wet.rosinstall
wstool init src indigo-ros_comm-wet.rosinstall

なお、Collada DOMが不要ならば以下のように –excludeの後ろに collada_parser と collada_urdf を追加する。

rosinstall_generator ros_comm --rosdistro indigo --deps --wet-only --exclude roslisp collada_parser collada_urdf --tar > indigo-ros_comm-wet.rosinstall
wstool init src indigo-ros_comm-wet.rosinstall

この時点で選んだvariantを間違えてしまった場合は、srcフォルダを削除して上のコマンドをやりなおせば良い。

必要なパッケージのインストール

ROSのビルドに必要だが、Raspbianではstableになっていないパッケージをそれぞれビルドしておく。まずは、その準備を行う。

sudo apt-get install checkinstall cmake
sudo sh -c 'echo "deb-src http://mirrordirector.raspbian.org/raspbian/ testing main contrib non-free rpi" >> /etc/apt/sources.list'
sudo apt-get update

libconsole-bridge-dev,liblz4-dev,liburdfdom-headers-devをインストールする。

mkdir ~/ros_catkin_ws/external_src
cd ~/ros_catkin_ws/external_src
sudo apt-get build-dep console-bridge
apt-get source -b console-bridge
sudo dpkg -i libconsole-bridge0.2_*.deb libconsole-bridge-dev_*.deb
apt-get source -b lz4
sudo dpkg -i liblz4-*.deb
git clone https://github.com/ros/urdfdom_headers.git
cd urdfdom_headers
cmake .
sudo checkinstall make install

最後のcheckinstallを行うと、以下のような設定が現れる。

0 -  Maintainer: [ root@raspberrypi ]
1 -  Summary: [ Package created with checkinstall 1.6.2 ]
2 -  Name:    [ urdfdom-headers ]
3 -  Version: [ 20150804 ]
4 -  Release: [ 1 ]
5 -  License: [ GPL ]
6 -  Group:   [ checkinstall ]
7 -  Architecture: [ armhf ]
8 -  Source location: [ urdfdom_headers ]
9 -  Alternate source location: [  ]
10 - Requires: [  ]
11 - Provides: [ urdfdom-headers ]
12 - Conflicts: [  ]
13 - Replaces: [  ]
Enter a number to change any of them or press ENTER to continue:

2番のNameをurdfdom_headersからliburdfdom-headers-devに変更する必要があるので、2 を入力してEnterを押し、次に liburdfdom-headers-dev と入力する。再度確認の設定が表示されたらそのまま Enter を押して続ける。

cd ..
sudo apt-get install libboost-test-dev libtinyxml-dev
git clone https://github.com/ros/urdfdom.git
cd urdfdom
cmake .
sudo checkinstall make install

最後のcheckinstallを行うと、以下のような設定が現れる。

0 -  Maintainer: [ root@raspberrypi ]
1 -  Summary: [ Package created with checkinstall 1.6.2 ]
2 -  Name:    [ urdfdom ]
3 -  Version: [ 20150804 ]
4 -  Release: [ 1 ]
5 -  License: [ GPL ]
6 -  Group:   [ checkinstall ]
7 -  Architecture: [ armhf ]
8 -  Source location: [ urdfdom ]
9 -  Alternate source location: [  ]
10 - Requires: [  ]
11 - Provides: [ urdfdom ]
12 - Conflicts: [  ]
13 - Replaces: [  ]
Enter a number to change any of them or press ENTER to continue:

2番のNameをurdfdomからliburdfdom-devに変更する必要があるので、2 を入力してEnterを押し、次に liburdfdom-dev と入力する。再度確認の設定が表示されたらそのまま Enter を押して続ける。

Collada DOMのインストール

variantsの選択時に Collada DOM を除外していなければ、以下を実行する。

cd ~/ros_catkin_ws/external_src
sudo apt-get install libboost-filesystem-dev libxml2-dev
wget http://downloads.sourceforge.net/project/collada-dom/Collada%20DOM/Collada%20DOM%202.4/collada-dom-2.4.0.tgz
tar -xzf collada-dom-2.4.0.tgz
cd collada-dom-2.4.0
cmake .
sudo checkinstall make install

checkinstallを行うと、以下のような設定が現れる。

0 -  Maintainer: [ root@raspberrypi ]
1 -  Summary: [ Package created with checkinstall 1.6.2 ]
2 -  Name:    [ collada-dom ]
3 -  Version: [ 2.4.0 ]
4 -  Release: [ 1 ]
5 -  License: [ GPL ]
6 -  Group:   [ checkinstall ]
7 -  Architecture: [ armhf ]
8 -  Source location: [ collada-dom-2.4.0 ]
9 -  Alternate source location: [  ]
10 - Requires: [  ]
11 - Provides: [ collada-dom ]
12 - Conflicts: [  ]
13 - Replaces: [  ]
Enter a number to change any of them or press ENTER to continue:

2番のNameをcollada-domからcollada-dom-devに変更する必要があるので、2 を入力してEnterを押し、次に collada-dom-dev と入力する。再度確認の設定が表示されたらそのまま Enter を押して続ける。

collada_urdfへのパッチ

Collada DOMをインストールした場合、以下のようにcollada_urdfへパッチを当てる必要がある。

Google Groupのエントリに添付されているパッチファイル(0001-fixed-arm-build.patch)をダウンロードし、ホームディレクトリにコピーしておく。

以下を入力してパッチを当てる。

cd ~/ros_catkin_ws/src/robot_model/collada_urdf/
patch -p1 < ~/0001-fixed-arm-build.patch

依存関係の解決

rosdepを実行して依存関係を解決する。

cd ~/ros_catkin_ws
rosdep install --from-paths src --ignore-src --rosdistro indigo -y -r --os=debian:wheezy

上記を実行すると、python-rosdep, python-catkin-pkg, python-rospkg, python-rosdistro, sbclがない旨のエラーメッセージが表示されるが、これらはpip経由でインストールしているので無視してよい。

ROS本体のビルド

後は以下を入力して本体をビルドする。

cd ~/ros_catkin_ws
sudo ./src/catkin/bin/catkin_make_isolated --install -DCMAKE_BUILD_TYPE=Release --install-space /opt/ros/indigo

終わるまでひたすら待つしかない。SSH経由だとタイムアウトする可能性があるのでタイムアウトしないように設定するかコンソール経由でビルドする。

動作確認

.bashrcで初期化スクリプトを実行するようにする。

echo "source /opt/ros/indigo/setup.bash" >> ~/.bashrc
source ~/.bashrc

roscoreを起動する。

roscore
... logging to /home/pi/.ros/log/9781754e-3a9d-11e5-8b98-b827ebe98f3c/roslaunch-raspberrypi-11718.log
Checking log directory for disk usage. This may take awhile.
Press Ctrl-C to interrupt
Done checking log file disk usage. Usage is <1GB.

started roslaunch server http://raspberrypi:42226/
ros_comm version 1.11.13


SUMMARY
========

PARAMETERS
 * /rosdistro: indigo
 * /rosversion: 1.11.13

NODES

auto-starting new master
process[master]: started with pid [11735]
ROS_MASTER_URI=http://raspberrypi:11311/

setting /run_id to 9781754e-3a9d-11e5-8b98-b827ebe98f3c
process[rosout-1]: started with pid [11748]
started core service [/rosout]

Ctrl-Zを押してプロセスを止め、プロンプトでbgを入力してバックグラウンド動作させる。

bg

トピックの一覧を表示させる。

rostopic list
/rosout
/rosout_agg

おそらく正常動作しているだろう。fgを入力した後、Ctrl-Cでroscoreの動作を止める。

fg
roscore
^C[rosout-1] killing on exit
[master] killing on exit
shutting down processing monitor...
... shutting down processing monitor complete
done

ここまでくるのにかなりの日数を要してしまった…

ROS indigoでiRobot Createを動かすまで

以前にもTurtleBotの関係でベースとなるiRobot Createを動かす手順をまとめたが、今回は別の方向に進む準備段階としてできるだけ少ない手間でCreateを動かす手順をまとめたい。

OSのインストール

OSとしてubuntuの軽量ディストリビューションであるlinuxBeanを用いた。linuxBeanの公式サイトから、linuxBean 14.04 (Trifolium) 14.04.2をダウンロードする。ROS/indigoを用いるために14.04を選んでインストールした。
必要な設定などは過去の記事を参考にしながら進めた。

ROS/indigoのインストール

ROSのインストール手順に従って進めて行く。

まず、パッケージのリポジトリ設定を行う。

sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list'

次にキーの設定を行う。

sudo apt-key adv --keyserver hkp://pool.sks-keyservers.net --recv-key 0xB01FA116

パッケージインデックスの更新を行う。

sudo apt-get update

GUIツールなしでindigoをインストールする。

sudo apt-get install ros-indigo-ros-base

rosdepの初期化を行う。

sudo rosdep init
rosdep update

環境変数などの設定を行う。

echo "source /opt/ros/indigo/setup.bash" >> ~/.bashrc
source ~/.bashrc

rosinstallをインストールする。

sudo apt-get install python-rosinstall

catkin用のワークスペースを作成する

mkdir -p ~/catkin_ws/src
cd ~/catkin_ws/src
catkin_init_workspace
cd ..
catkin_make

Createノードの準備

Create用のパッケージを導入する

sudo apt-get install ros-indigo-create-node

CreateとUSBシリアルアダプタを用いて接続する。

上記のノードで使う /dev/ttyUSB0 を見てみると以下のようになっている。

ls -l /dev/ttyUSB0
crw-rw---- 1 root dialout 188,0 7月 23 14:58 /dev/ttyUSB0

/dev/ttyUSB0にアクセスできるように、自分のユーザー名(hoge)をdialoutグループに追加する。

sudo vigr

/etc/groupファイルのうち、dialoutで始まる行の末尾に自分のユーザー名(hoge)を追加する。

dialout:x:20:hoge

Createへの接続テスト

roscoreを起動する。

roscore

(別のターミナルを開いて)create_nodeを起動する。

rosrun create_node turtlebot_node.py

赤字でエラーメッセージが表示されなければ(おそらく)問題ない。

Createを前進させる。

rostopic pub -1 /cmd_vel geometry_msgs/Twist -- '[10,0,0]' '[0,0,0]'

Createが数センチ動けばOK。

はじめてのMorphicチュートリアル(第14回)「文字の表示」

モーフに文字を表示する方法について学ぶ。

モーフに文字を表示するには、おおまかに2通りの方法がある。1つはStringMorphを使う方法、もう1つはdrawOn:で直接描画する方法である。

StringMorphを使う

StringMorphは文字を表示するためのモーフである。

(StringMorph contents: 'aiueo') openInWorld.

上のコードをPlaygroundに入力してDo itすれば、画面の左上に小さく aiueo と表示されるのがわかる。
StringMorph
これは文字を表示するモーフであり、マウスでドラッグして好きな場所に移動することができる。

表示するフォント名やポイント数を指定したい場合には、contents:の代わりにcontents:fonts:メッセージを送る。

(StringMorph contents: 'あいうえお'
                  font: (LogicalFont familyName: 'Osaka' pointSize: 30)) openInWorld.

StringMorphをサブモーフにすることもできる。MyMorph2でメッセージを表示するようにするなら、initializeメソッドを以下のように変更する。

initialize
    super initialize.
    ActiveHand keyboardFocus: self.
    keys := Set new.
    (StringMorph contents: 'Hello!'
                 font: (LogicalFont familyName: 'Apple Chancery' pointSize: 30))
        in: [ :morph | 
            morph topLeft: self topRight.
            morph color: Color red.
            self addMorphBack: morph ]

後半の4行がStringMorphを追加している部分である。簡単に説明しておくと、まず指定した文字列とフォントでStringMorphを生成しておき、得られたモーフに対して位置を設定(StringMorphの左上がMyMorph2の右上の座標になるように)して赤色にし、サブモーフとして追加するという流れになっている。
Screen Shot 2015-06-18 at 7.26.01
残念ながらStringMorphでは背景色を設定できないので、背景色が必要な場合にはStringMorphを、色を付けた素のモーフのサブモーフにして同じ位置に設定する。

drawOn:で表示する

drawOn:はモーフを描画する際に送られるメッセージで、第12回ではそのことを利用して画像を表示した。これと同様にモーフ描画の一部として文字を書く方法がある。

モーフを描画する必要が生じると、各モーフに対してdrawOn:というメッセージが送られる。メッセージには描画先としてのCanvasオブジェクトが引数として添えられているので、このCanvasオブジェクトに描画メッセージを送ってモーフを描く。

文字を描くためにCanvasオブジェクトに送ることのできるメッセージはいくつかあり、Canvasクラスのdrawing-textプロトコルを見ればわかるが、実際にはdrawString:at:メッセージか、drawString:at:font:color:メッセージだけで事足りるだろう。前者は指定された文字列を指定された位置にデフォルトフォントを用いて黒色で描画するというものであり、後者は更にフォントと色を指定できるというものである。

応用例として、押下中の文字を表示する機能をMyMorph2に付けてみよう。

drawOn: aCanvas
    super drawOn: aCanvas.
    aCanvas drawString: keys asString at: self topLeft

2行目は本来のモーフの描画を、親クラスであるMyMorphに行わせるためのものである。3行目では、押下中のキーを保持しているインスタンス変数keysの内容を文字列に変換し、モーフの左上に描画している。

drawOn:を用いる際の注意点として、特定のタイミングで明示的に表示内容を更新したい場合には、自分自身に対してchangedメッセージを送る必要があることである。MyMorph/MyMorph2では、画像を表示させる際に大きさや位置を変更しているため暗黙的な描画が行われるのでchangedメッセージを送る必要はないが、文字列内容だけを変更した場合などは、変更後に描画を行わせるためにself changed.というメッセージ式を実行する必要がある。

(第14回おわり)

はじめてのMorphicチュートリアル(第13回)「サブクラス化とキーイベントの処理」

クラスの機能を拡張するにはメソッドを追加していく必要があることを第5回の記事以降で学んできた。

既存のクラスの機能を維持しつつ、似たような新たな機能を持った異なるクラスを必要とする場合、第4回で学んだクラス定義によって新たなクラスを作成する。

例えば、今まで作成したMyMorphクラスと同様にScratch Catが移動していくのだが、マウスではなくキーボード操作によって移動するようなクラスが欲しい場合、MyMorphを親(基底)クラスとする新たなクラスを作成する。そうすればMyMorphの機能を引き継いだ、新たなクラスを作ることができる。

ここでは、そのような新しいクラスをMyMorph2として定義してみよう。

MyMorph subclass: #MyMorph2
    instanceVariableNames: ''
    classVariableNames: ''
    category: 'Hajimeteno Morphic Tutorial'

この新しいMyMorph2では、モーフをキーボードで操作してみよう。以前の記事ではキーボードイベントの取り扱いが中途半端になっていたので、この回でより具体的にキーボードイベントを扱う。

MyMorph2の基本的な仕様としては、上下左右の矢印キーが押されている間だけ矢印の向きにモーフが動くようにする。

キーボード入力に反応するために定義しなければならないメソッドは以下の通りである。

  • handlesKeyDown:
  • handlesKeyUp:
  • keyDown:
  • keyUp:

前2者はモーフがキーボードイベントを受け取るためにtrueを返す必要がある。後2者で受け取ったキーボードイベントを実際に処理する。まず、前2者のメソッドは以下のようになる。

handlesKeyDown: evt
    ^ true
handlesKeyUp: evt
    ^ true

これらのメソッドにより、キーボードが押された瞬間にkeyDown:メッセージがモーフに送られ、離された瞬間にkeyUp:メッセージが送られる。それぞれキーボードイベントが引数となっている。

キーボードイベントの実体はKeyboardEventオブジェクトであり、keyメッセージを送るとKeyオブジェクトが得られる。Keyオブジェクトにnameメッセージを送ると押されたキーの情報を得ることができる。少し寄り道してkeyDown:メッセージの内容を表示して、どんな内容となっているのか確認してみよう。

keyDown: evt
    self inform: evt key name

今までの3つのメソッドを実装したら以下をDo itする。

ActiveHand keyboardFocus: MyMorph2 new openInWorld.

キーボードイベントの説明のところで述べたように、キーボードフォーカスをモーフに移動しないとキーボードイベントを受け取れない。そこで、生成したモーフに強制的にキーボードフォーカスを設定している。MyMorph2が現れたら適当なキーを押してみると、押した瞬間と押し続けている間、画面の左下にメッセージが現れ、Keyオブジェクトのnameが表示される。

矢印キーのnameは以下のような文字列である。

  • KP_UP — 上矢印
  • KP_DOWN — 下矢印
  • KP_LEFT — 左矢印
  • KP_RIGHT — 右矢印

これらの値を使えば押されたキーの判定を行うことができる。

矢印キーが押されている間だけ矢印の向きにモーフを動かすとすると、押されているキーを覚えておく必要がある。そこで、MyMorph2のクラス定義を修正して、keysという名前のインスタンス変数を追加する。

MyMorph subclass: #MyMorph2
    instanceVariableNames: 'keys'
    classVariableNames: ''
    category: 'Hajimeteno Morphic Tutorial'

押されたキーを全てkeysに覚えておくため、keyDown:メッセージを受け取ったタイミングでKeyオブジェクトの名前をkeysに追加し、keyUp:メッセージで除外する。キーが押し続けられるとkeyDown:メッセージを複数受け取ることになるが、そのあたりをうまく処理するためにSetオブジェクトを用いることにする。Setは集合のようにデータを扱うためのクラスで、同じデータを複数追加しても個数はカウントされない。MyMorph2のinitializeメソッドを用いて初期化しておく。ついでにキーボードフォーカスを自分自身にセットするようにしておこう。

initialize
    super initialize.
    ActiveHand keyboardFocus: self.
    keys := Set new

モーフの移動は今までと同じくstepメッセージを受け取ったタイミングで行うが、移動量であるvecはkeyDown:とkeyUp:のタイミングで更新することにする。keysの内容に基づいてvecの値を計算するメソッドcalcVecを新たに作成する。

calcVec
    vec := 0 @ 0.
    (keys includes: 'KP_UP')
        ifTrue: [ vec := vec + (0 @ -10) ].
    (keys includes: 'KP_DOWN')
        ifTrue: [ vec := vec + (0 @ 10) ].
    (keys includes: 'KP_LEFT')
        ifTrue: [ vec := vec + (-10 @ 0) ].
    (keys includes: 'KP_RIGHT')
        ifTrue: [ vec := vec + (10 @ 0) ]

Setオブジェクトであるkeysに送られているincludes:というメッセージは、引数のデータがSetオブジェクトにあるかどうかを調べるためのものである。calcVecメソッドでは押されているキーに応じて上下左右の移動量をセットしている。

keyDown:メソッドでは、単に受け取ったキーボードイベントのKeyオブジェクトの名前をkeysに追加し、自分自身にcalcVecメッセージを送るだけである。

keyDown: evt
    keys add: evt key name.
    self calcVec

Setなど複数のデータを扱うことのできるものをコレクションと呼ぶ。コレクションにデータを追加する場合、たいていadd:メッセージを用いる。

keyUp:メソッドでは、単に受け取ったキーボードイベントのKeyオブジェクトの名前をkeysから取り除き、自分自身にcalcVecメッセージを送る。

keyUp: evt
    keys remove: evt key name ifAbsent: [  ].
    self calcVec

Setオブジェクトからデータを削除するにはremove:メッセージを使うが、削除したいデータがないとエラーになるので、ここではremove:ifAbsent:というメッセージを用いている。2番目の引数は、データがなかった場合の処理を書くが、ここでは何もしないようにしている。

以上で矢印キーにより上下左右に移動するMyMorph2ができた。

(第13回おわり)

はじめてのMorphicチュートリアル(番外編)「メソッドを整理する」

クラスを拡張してメソッドを増やして行くと、System Browserでのメソッドの一覧性が悪くなってくる。たとえば、今までにMyMorphのクラスで定義したメソッドは14個程度だが、ペインに入りきらない状況になっているだろう。
メソッド一覧

Smalltalkでは、メソッドの分類を行うためにプロトコルと呼ばれるものを用意している。プロトコル自体はプログラムの動作に何の影響もなく、単にプログラマにメソッドの分類方法を提供しているだけのものだ。とはいえ、プロトコルがなければ数十ときに数百に上るメソッドを持ったクラスのメンテナンスがとても面倒なものになってしまう。今回のチュートリアルでは、プロトコルによる分類について学ぶ。

まずプロトコルについて。以前、クラスの分類にクラスカテゴリを用いることを述べたが、プロトコルも同様のものである。座標をあらわすPointクラスを例にあげて説明しよう。
座標をあらわすPointクラスには様々なメソッドが定義されており、座標自身の情報の設定や取得に加えて、座標同士の演算や比較、変換や便利な機能など、およそ100のメソッドがある。
Pointクラス

Point methods size. "99"

プロトコルによって多数のメソッドを分類する。Pointクラスには以下のようなプロトコルがある。

Point protocols joinUsing: String cr.
 "'-- all --
copying
*Fuel
accessing
private
point functions
printing
self evaluating
transforming
interpolating
testing
geometry
truncation and roundoff
polar coordinates
extent functions
converting
truncation and round off
comparing
arithmetic'"

先頭の’– all –‘は、全てのメソッドの一覧を得る際の擬似的なものであり、アスタリスクで始まるプロトコルは他のパッケージに属しているものを表す。その他は、Pointクラスにおけるさまざまな役割や機能を表すプロトコルとなっている。

どのメソッドをどのプロトコルに配置すべきかはクラスの設計者が決定しなければならない。このあたりの判断はなかなか難しい。既存のクラスを参考にしながらどのプロトコルに配置するかを決めていく。例えば、printOn:というメッセージが他のクラスではどのようなプロトコルで定義されているか調べるには以下のようにする。

SystemNavigation default browseAllImplementorsOf: #printOn:

printOn:を定義しているクラスの一覧が表示され、クラス名の右側の丸括弧の内側に定義されているプロトコル名が表示される。
Implementers
これを見ればprintingプロトコルで定義されているのがわかる。

最近のブラウザでは親クラスの配置を参考に、自動的にプロトコルへ振り分けてくれる機能を持っている。SystemBrowserのプロトコルペインで右クリックすると、categorize all uncategorizedというメニュー項目が現れるのでクリックすると、プロトコルに分類していないメソッドを自動的に分類してくれる。
categorize methods
メニューを選んで分類が終わると以下のようになる。
categorized list
おおむねそれらしく配置されているが、handlesMouseOver:がevent handlingではなく、recategorizedに分類されているのは違和感がある。このような場合には手動で分類する。handlesMouseOver:をドラッグしながらevent handlingでドロップすれば良い。

なお、どこにも定義されていないような自分メソッドを作った場合、既存のプロトコルで適切なものがなければ自分で作成することになる。新たにプロトコルを作成するには、プロトコルペインでAdd protocol…を選べば良い。

メソッドはどのプロトコルに分類したからといって実行性能に影響するわけではない。プロトコルはクラスの開発者や利用者がメソッドを探しやすくするための工夫である。Smalltalkerは「こんな機能(メソッド)ないかな?」と思ったときにプロトコルを手がかりにメソッドを探すことが多い。うまくメソッドを分類できれば時間短縮につながるので、ひと手間かけてプロトコルに分類することには価値がある。

(番外編おわり)

はじめてのMorphicチュートリアル(第12回)「画像の表示」

今回は画像ファイルをモーフに表示する方法について学ぶ。

はじめてのMorphicチュートリアル(番外編)「ScratchCatをダウンロードする」でダウンロードしたScratch Catをモーフの表面に表示させる。Scratchのメディアサイトから画像ファイルをダウンロードしておいてもよい。いずれにせよ、ダウンロードしたファイルが展開され、画像ファイルが適当なフォルダに格納されている前提で話を進める。

ファイルに保存された画像を表示するには、画像ファイルを読み込んでFormオブジェクトを作り、それを表示するという流れで処理を行う。

画像ファイルを読み込むには以下のようにする。(ファイル名はフルパスで指定してもよい)

f1 := PNGReadWriter formFromFileNamed: 'Cat_1_Bitmap.png'.

PNGReadWriterはPNG形式のファイルの読み書きを行うためのクラスである。他にもBMPReadWriterやGIFReadWriter、AnimatedGIFReadWriter、JPEGReadWriterがある。各クラスに対し、ファイル名の引数を付けてformFromFileNamed:というキーワードメッセージを送れば、指定した画像ファイルを読み込んでFormオブジェクトを生成する。

生成して得られたFormオブジェクトから大きさやビット深度などの情報を得ることができる。

f1 extent. "(184@200)"
f1 depth. "32"

MyMorphで色のついた四角形の変わりにScratch Catを表示するなら、事前に画像を読み込んでおいてモーフ表示の際に描画する必要がある。そのため、画像ファイルを読み込んで得たFormオブジェクトは、formという名前のインスタンス変数で覚えておくことにする。

新たなインスタンス変数を追加するには、MyMorphのクラス定義を以下のように変える必要がある。

Morph subclass: #MyMorph
    instanceVariableNames: 'vec form'
    classVariableNames: ''
    category: 'Hajimeteno Morphic Tutorial'

System BrowserでMyMorphクラスを変更してAcceptするか、適当な場所で上記をDo itする。

画像ファイルはinitializeメソッドの中で(つまりモーフ生成時に)読み込むことにする。モーフ生成のたびに同じ画像ファイルを読み込むのは効率が悪いが、ここでは単純化のためにそうする。また、元のMyMorphは16×16ピクセルの大きさなので、Scratch Catを表示できない。そこで、モーフの大きさを画像の大きさに設定している。また、少し早めに動作するよう10ピクセルずつの移動とした。

initialize
    super initialize.
    vec := 10 @ 0.
    form := PNGReadWriter formFromFileNamed: 'Cat_1_Bitmap.png'.
    self extent: form extent

モーフはdrawOn:メッセージが送られると引数に与えられたキャンバスへ自分自身を描画するようになっており、現状ではモーフの大きさの色の付いた四角形を表示している。そこで、読み込んだFormオブジェクトを表示するために以下のようなdrawOn:メソッドを実装する。

drawOn: aCanvas
    aCanvas drawImage: form at: self topLeft

drawImage:at:というキーワードメッセージは2つの引数を持ち、最初に表示させたいFormオブジェクト、次に画面上の位置を指定する。

以上のような変更を加えた上で、MyMorph new openInWorldをDo itしてみると、以下のように表示される。
Scratch Cat bug
ご覧の通り、Scratch Catが表示されるものの、モーフが動くたびに分身が残って表示されてしまう。このような現象は、今回のように透明部分を含む画像を描画したり、全ての表示範囲を使わずに描画したモーフを動かす際に発生してしまう。

これを回避するには数カ所を変更する必要がある。まず必要なことは、先ほど実装したdrawOn:で、drawImage:at:の代わりにtranslucentImage:at:を用いて、透明部分の描画に対応することである。

drawOn: aCanvas
    aCanvas translucentImage: form at: self topLeft

更にモーフ自身の色が透明であるように設定する。

initialize
    super initialize.
    vec := 10 @ 0.
    self color: Color transparent.
    form := PNGReadWriter formFromFileNamed: 'Cat_1_Bitmap.png'.
    self extent: form extent

中央の行で透明色に設定している。

通常はこれだけで良いのだが、MyMorphではstepのたびに色を変えるようにしていたので、色を変えないようにstepメソッドを修正する。

step
    self topLeft: (self topLeft + vec)

以上で画像ファイルを表示させることができた。

2枚の画像を交互に表示させるのは今までの延長線上でできるので演習にしておこう。

(第12回おわり)

はじめてのMorphicチュートリアル(番外編)「ScratchCatをダウンロードする」

Cat_1_Bitmap

Scratchのキャラクターの猫をサイトからダウンロードする方法について。

Scratch Mediaのウェブページを開いて、そこから「SVG+PNG (2 Costumes)」という圧縮ファイルをダウンロードして展開するということなので、ブラウザとOS上の操作があれば解決する簡単な仕事なのだが、これをPharoでやるとどうなるかということである。

まず、http://wiki.scratch.mit.edu/w/images/ScratchCat.zip のファイルをダウンロードするには、以下のようにZnEasyクラスを使うのが簡単である。

res := ZnEasy get: 'http://wiki.scratch.mit.edu/w/images/ScratchCat.zip'.

こうすると、resにはZnResponseオブジェクトが返ってくる。問題なければ以下のような内容となる。

"a ZnResponse(200 OK application/zip 12017B)"

12017バイトのZIPファイルが得られることがわかる。

ZIPファイルの内容を得るには、contentsというメッセージを送れば良い。

res contents.

ZIPファイルをアーカイブとして読み込むには、ZipArchiveのインスタンスに対してreadFrom:でストリームを与える。

arc := ZipArchive new readFrom: res contents readStream.

アーカイブされたファイル群(メンバー)は以下のように知ることができる。

arc members.
"an OrderedCollection(a ZipFileMember(Cat_1.svg) a ZipFileMember(Cat_2.svg)
 a ZipFileMember(Cat_1_Bitmap.png) a ZipFileMember(Cat_2_Bitmap.png))"

4つの画像ファイルが入っていることがわかる。

アーカイブから特定の画像ファイルを保存するには、以下のようにすればよい。

arc extractMember: 'Cat_1_Bitmap.png'.

簡単!と思ったのだが落とし穴があった。上をDo itするとエラーが発生する。
error

どうやらZipFileMemberクラスの extractToFileNamed:inDirectory: メソッドに問題があるらしい。
エラー内容

エラーを起こしている箇所はここである。

    fullDir 
        forceNewFileNamed: file basename 
        do: [:stream |  self extractTo: stream]]

よく見るとメソッド内でfullDirの値を設定している箇所がない。実際、fullDirの内容はnilだし、エラーはUndefinedObject(つまりnil)に対するメッセージ送信となっている。

これは想像だが、以前にFileDirectoryをFileSystemに置き換えた際、この部分の修正が取り残されたのではないだろうか。コメントぐらい残しとけよって感じである。Pharo1.4までさかのぼってみると、想像どおりFileDirectoryを用いてfullDirが作られていた。

extractToFileNamed: aLocalFileName inDirectory: dir
    | fullName fullDir |
    self isEncrypted ifTrue: [ ^self error: 'encryption unsupported' ].
    fullName := dir fullNameFor: aLocalFileName.
    fullDir := FileDirectory forFileName: fullName.
    fullDir assureExistence.
    self isDirectory 
        ifFalse: [ fullDir 
            forceNewFileNamed: (FileDirectory localNameFor: fullName)
            do: [:stream |  self extractTo: stream]]
    ifTrue: [ fullDir assureExistence ]

こういうことでメゲていてはいけない。せっかく全てのソースがアクセス可能なのだから自分で何とかすることを考えるべきである。

fullDirにはforceNewFileNamed:do:というメッセージとassureExistenceというメッセージが送られている。前者は新しいファイルを作ってファイルの中身を流し込んでおり、後者はディレクトリを作っているにすぎない。これを現在のFileSystemで実現することを考える。

    self isDirectory
        ifFalse: [ 
            file isFile
                ifTrue: [ file delete ].
            self extractTo: file writeStream ]
        ifTrue: [ file ensureCreateDirectory ]

上記が変更部分である。self isDirectory以降を書き換える。

これで晴れてZIPアーカイブからファイルを展開することができる。

arc extractMember: 'Cat_1_Bitmap.png'.
arc extractMember: 'Cat_2_Bitmap.png'.

以下をDo itすれば、展開したファイルをモーフとして取り込むことができる。

(PNGReadWriter formFromFileNamed: 'Cat_1_Bitmap.png') asMorph openInWorld.
(PNGReadWriter formFromFileNamed: 'Cat_2_Bitmap.png') asMorph openInWorld.

(番外編おわり)

はじめてのMorphicチュートリアル(第11回)「状態」

今回はモーフに状態を持たせることについて学ぶ。

もちろん今までもモーフは大きさ、位置、色などさまざまな状態を持っていたが、それとは異なる内部的な状態を持たせたい。

例として今まで右に移動するだけだったMyMorphを、クリックするたびに90度ずつ向きを変えて移動するようにしよう。

さて、MyMorphで「移動」を実現しているのはstepメソッドである。

step
    self color: Color random.
    self topLeft: (self topLeft + (2@0))

MyMorphの左上のX座標を2増やすことで右に2ピクセル移動する。

例えばモーフが右に90度向きを変える(つまり下向きに移動する)とすれば、3行目は以下のようになるだろう。

self topLeft: (self topLeft + (0@2))

さらに90度向きを変えると左向きに進み、コードは以下のようになる。

self topLeft: (self topLeft - (2@0))

モーフの左上座標に対して、一定数の座標値を加減することで向きが変わると考えられるので、以下のようにコードを変更できる。

self topLeft: (self topLeft + vec)

つまり、座標値に加えるvecが2@0であれば右に進み、0@2であれば下に進むので、(vecは)モーフが進む方向を表す状態とみなせる。

このvecは動作しているモーフ固有の情報である。他にMyMorphモーフがあったとしても、それぞれの値は独立している。

このような値を表すのにSmalltalkではインスタンス変数を用いる。インスタンス変数はクラスを定義する際に宣言し、そのクラスに属する全てのモーフが独立した値を持つようになる。

MyMorphにインスタンス変数vecを加えるには、以下のようにクラス定義を変更する。

Morph subclass: #MyMorph
	instanceVariableNames: 'vec'
	classVariableNames: ''
	category: 'Hajimeteno Morphic Tutorial'

文字通りinstanceVariableNames:の後の文字列が、インスタンス変数の並びを表す。複数のインスタンス変数が必要ならば、スペースで区切り両端をシングルクオートで囲んで(つまり文字列として)指定する。

第4回のクラス定義と同じ方法で、クラス定義のテンプレートを上のように修正してAcceptすれば、既存のMyMorphの定義は上書きされる。上書きをしてもメソッド定義は消えないので心配しなくて良いが、動作中のモーフがあるとクラス定義の変更の影響を受ける場合があるので、あらかじめ削除しておいた方が無難である。

インスタンス変数vecを加えたMyMorphをAcceptしたら、stepの内容を以下のように変更する。

step
    self color: Color random.
    self topLeft: (self topLeft + vec)

この状態でMyMorphを動かすと、vecが未定義なためにエラーが発生する。モーフの生成と同時にvecの初期値を設定する必要がある。

そのため第6回で説明したようにinitializeメソッドを以下のように変更し、vecの初期値を2@0とする。(モーフは右に進む)

initialize
    super initialize.
    vec := 2@0.
    self extent: 16@16

クリックするたびにモーフが90度向きを変えるようにするため、mouseDown:メソッドを修正する。

mouseDown: anEvent
    self extent: self extent + (10@10).
    vec := vec y negated @ vec x

インスタンス変数vecの値を変更する部分で説明が必要なのはnegatedメッセージの意味である。negatedメッセージはメッセージを送った数値の正負を反転した値を返すメッセージである。

上記までの変更を行えば、MyMorphはクリックするたびに右へ90度向きを変えながら移動するようになる。

(第11回おわり)