2009年1月31日土曜日

天気予報アプリが作りたい、その名はjWeeklyForecast(仮)

  • マップをタップしたら、タップしたあたりの天気予報がぱぱっと出る。ただそれだけのアプリ
  • 出来れば一週間先まで見たい。
  • 天気情報はYahoo.co.jpからスクレイピングする(Yahooさんごめんなさい)
  • そのためにバックグラウンドサーバーが欲しいのでGoogle App Engineで作っちゃう

最近、fladdictさんが日本のiPhone開発コミュニティ全体に「情報をもっとオープンにしていこう」と働きかけています。
その結果が少しずつ実をつけてきたのを肌で感じています。
たった一人の働きかけでここまでコミュニティ全体の流れを変えることができるものなのか、と感動することしきりです。

ということで私もこの流れに乗っかり、現在作成中のアプリを洗いざらい公開することにしました。

現在、jWeeklyForecastという天気予報アプリを作成しています。
  • 地図から予報を見る場所を選択したい
  • 一週間先の予報が見たい
  • 日本の天気だけ見られればよい
というニーズを満たすために開発を始めたのですが、私がトロトロしているうちにウェザーニュースさんがもうこれ以上完璧な天気予報アプリは無いだろうというぐらい完璧なアプリをリリースされてしまいましたので、すでに私のニーズは満たされてしまいました。
ここから先は自分の勉強と日本iPhone開発コミュニティへのネタ還元のためにアプリの作成を続けていこうと思います。

「日本iPhone開発コミュニティに対するネタ還元」という観点から見たjWeeklyForecastの価値ですが、
  • 地図(route-me)を使っている
  • バックエンドサーバーを自作している(Google App Engine)
この2点が大きくアピールできるのではないかと思います。
特に最近fladdictさんをはじめとして台頭しているFlasherさんたちは、バックグラウンドサーバーを自分で作った経験がほとんど無いはずです。ですので、無料で簡単に、自分の(ひいては他の開発者の皆さんのための)iPhoneアプリ用APIサーバーなどを作る方法のノウハウなどが提供できれば少しでも価値になるのではないかなーなどと考えてます。

問題は、私が大変飽きっぽい上にすぐ挫折するので、最後までやり遂げられるかとっても不安ですが・・・
まぁ若輩者の戯れ言と思っておつきあいいただければ幸いです。

ドメインakisute.comを取得いたしました

本日akisute.comドメインを取得いたしましたので、
http://akisute.com/
こちらのアドレスでも本Blogにアクセスできるようになりました!
どうぞよろしくお願いします。


余談ですが、
http://www.akisute.net/
こちらの方と私は別人ですので、あしからずご了承ください。

さらに余談ですが、
http://aniki.kululu.net/sc/talk/t_index.htm
こちらの方とも私は別人ですので、あしからずご了承ください。

2009年1月26日月曜日

ThinkingRockを使ってGTDを初めてみました


  • 最近はタスクが多すぎる!!
  • Remember The Milkだけではもう限界!サブプロジェクトすら作れないし・・・リストにはいつもタスクがあふれている!
  • そこでGTDを本格的にやってみる
  • でもOmniFocusは高くて躊躇
  • と思ってたら救世主登場、その名はThinkingRock
  • Javaなのでマルチプラットフォームなのもいい感じ
ThinkingRockというJava製のGTDアプリケーションを最近使い始めました。
同時並行で5つぐらいのプロジェクトを抱えており、さすがにRemember The Milkだけでは仕事を背負いきれなくなってきたのが始めたきっかけです。

このアプリの特徴を一言で表現するなら、

超本格的、でも簡単

これです。

実は私、GTDについてはウェブで聞きかじった程度の知識しかありません。
しかしこのThinkingRock、
「思考を集めるための画面」
「集めた思考を仕分けするための画面」
「仕分けした思考をプロジェクトやアクションとして管理するための画面」
が明示的に分離されており、
何を入力すればよいかもはっきりと決まっているので、後はヘルプを見ながらそれに従って項目を入力したりタスクを仕分けしていけば厳格にGTDに乗っ取ったタスク管理が自動的にできてしまうといった作りになっています。


このように、GTD全体の構成を表したOverviewが用意されております。

ヘルプにもGTD関連の情報が満載で、
わからないことがあれば頼りになります。
(たいていこういうツールのヘルプって頼りにならないので、驚きでした)

ヘルプ画面はこんな感じです。英語ばっかりで難しいですが・・・読めないことはないです!

サブプロジェクトとかも作成でき、コンテキストも作れて高性能でいい感じです。
気に入ったので職場のWindowsマシンにも導入しました。

2009年1月24日土曜日

route-meでタッチイベントを扱いたい

  • タッチイベントを扱うときはRMMapViewDelegateプロトコルを採用する
  • シングルタッチ、ダブルタッチを感知したり、マーカー上のタッチやドラッグを感知したり、地図の移動およびズームを感知したりできる
  • 現状、マップのドラッグやズームを使用不可能にするための手段は用意されていない。Delegateの返り値による操作もできない。
  • UIMapViewにenableDraggingおよびenableZoomというインスタンス変数が用意されているが、mapView->enableDraggingのようにしてアクセスしようとするとコンパイルエラーになってしまう。

route-me上でタッチイベントを取得してみました。

こんな感じで、画面上のタップした点を取得することができます。

タッチイベントの取得方法は、
まずRMMapViewDelegateプロトコルを任意のクラスに適合させて、
@interface MapViewController : UIViewController  {
/* 中略 */
- (void) singleTapOnMap: (RMMapView*) map At: (CGPoint) point
{
NSString *message = [NSString stringWithFormat:@"(%f, %f)", point.x, point.y];
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"singleTapOnMap"
message:message
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alertView show];
}
/* 後略 */

適合させたクラスのオブジェクトをRMMapViewのdelegateプロパティにセットします。
RMMapView *mapView = [[[RMMapView alloc]
initWithFrame:[UIScreen mainScreen].applicationFrame WithLocation:initialLocation.coordinate]
autorelease];
mapView.delegate = self;

普通のUIViewとほとんど同じですね。

しかしこの方法では画面上の座標がとれるだけで、緯度経度を取ることができませんので、
RMMapViewのメソッドを用いて、緯度経度に変換します。
@interface MapViewController : UIViewController  {
/* 中略 */
- (void) singleTapOnMap: (RMMapView*) map At: (CGPoint) point
{
//ポイントをLatLongに変換して表示する
CLLocationCoordinate2D coordinate = [map pixelToLatLong:point];
NSString *message = [NSString stringWithFormat:@"(%f, %f)", coordinate.latitude, coordinate.longitude];
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"singleTapOnMap"
message:message
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alertView show];
}
/* 後略 */

これで緯度経度になりました。

ところで、地図のスクロールやズームを制限して利用したいというときがあると思います。
たとえば日本向けのアプリなのにアルゼンチンの地図をスクロールして表示されたら困るという場合です。

早速地図のスクロールを制限する方法を調べてみたところ、
RMMapViewにいかにもそれらしいメンバー変数を発見。
@interface RMMapView : UIView
{
RMMapContents *contents;
id delegate;
BOOL enableDragging;
BOOL enableZoom;
RMGestureDetails lastGesture;
float decelerationFactor;
}

しかしながらこのメンバー変数、プロパティも操作するためのメソッドもなく、外部から操作することができません。
ためしに無理矢理以下のようなコードを書いてアクセスしてみたところ、コンパイルではねられました。
RMMapView *mapView = [[[RMMapView alloc]
initWithFrame:[UIScreen mainScreen].applicationFrame WithLocation:initialLocation.coordinate]
autorelease];
//以下の行でエラー。アクセスできません
mapView->enableDragging = NO;

残念ながらまだこの機能は現在のバージョンのroute-meでは利用できないみたいです。

その他、delegateメソッドの返り値をNOにしたらスクロールしなくなるとかそういう機能がないか調べてみましたがやはりなさそうで、今のところ地図のスクロールおよびズームを制限するのは難しそうです。今後のバージョンアップに期待ですね。

2009年1月19日月曜日

英語の発音を練習するアプリ「iはつおん」をAppBank様から頂戴いたしました

  • こちらのキャンペーンに応募→http://www.appbank.net/2009/01/13/iphone-application/6376.php
  • 見事当選!(AppBank様ありがとうございます)
  • 数ある英語の発音23種類を、お手本を聞きながら実際に自分で発音して練習できるアプリ
  • 「録音」機能がついていて、自分の発音を録音し、お手本と聞き比べられるのがすごくよい
  • 一音単位だけではなくて、単語単位での発音練習も可能
  • バージョン1.1現在、'U'の発音方法の説明がバグっていて、'Z'のものになっている
  • 単語単位だけではなくて、実際の文章単位での発音練習もできるようにして欲しい
毎日欠かさずチェックしている、iPhoneアプリレビューサイトのAppBank様。そのAppBank様の「英語発音練習iPhoneアプリ「iはつおん」無料プレゼントキャンペーンのお知らせ。」を見て、まさにこれぞチャンス!と思い速攻で申し込んだところ、なんと当選してしまいました。

当選してアプリとiTunes Store (US)のアカウントという飛び切りのお年玉をいただくことになりました。本当にありがとうございます。
受かったからには気合いを入れてレビューを書かねば!

まず最初に1点だけバグが見つかったので、その報告だけ。

なるほど、Aの発音はお腹から「ズッ」っと発声すればいいのかー・・・って違うわ!


【2009/01/22】
iはつおんの機能のレビューにつきましてはこちらを参考にしていただくとして、
ここでは、私の英語学習体験とiはつおんによる英語学習を照らし合わせたレビューをしてみます。

まずは私自身の英語学習歴から簡単にご説明。
  • 12歳で塾(公文式だったかな)に通い始めて、そこで英語を覚える。たいしたことはしていないけれども、「英語は日本語じゃないから日本語とは違うようにしゃべろう」と決心したことだけは覚えている。おかげで変な日本人発音だけはしないようになりました。
  • 13歳で中学に入る。中1の英語のセンセイがとてもよい女性の先生だったためめきめき覚える。ちなみに2年3年のころのセンセイは大変生徒から不評であった。
  • 14歳でウルティマオンラインを始める。当時のUOはAsukaとかYamatoとか日本人向けサーバーができはじめてはいたが、まだNPCは平気で英語をしゃべったりしていた。そのため必然的に英語を取得。"vender buy bank guards i ban thee"などといった日常ではあまり使わないであろう単語を中心に読み書きできるようになりました。
  • 15歳ぐらいでInfantryという神ゲーに出会う。こちら英語版しかなく、音声も画面メッセージもすべて英語。味方も敵も外人。必然的に英語で"n00b lamers you can't even stop my single assault =P"などとののしりあうことができるようになりました。
  • 16歳ぐらいでDiablo2という宇宙最強のRPGに出会う。こちら英語版しか(ry)・・・なのですが、Diablo2はInfantryと違ってRPGであったため、キャラが英語でストーリーをしゃべってくれます。おかげである程度英語が聴けるようになりました。
  • 17歳ぐらいで世界遺産級のRTSゲームことWarcraft3に出会う。こちら今でもプレイしていますが、これまたキャラが英語でしゃべるので(ry)。またこのゲーム、キャラの台詞がいちいちカッコイイ(例:これとか)ので、ついつい口ずさんでしまう。端から見るとただのキモいオタクでしたが、この実際に声に出したところが後々の英語力として残ったのではないかと思ってます。
  • その他World of Warcraftなどプレイして今に至る。
ごらんの通り、英語は完全にPCゲームから覚えています。
まともに発音記号の読み方がわかるようになったのは大学に入ってからだったり、英単語帳とか大学受験の年になって慌てて買ったというような具合ですので、
いわゆる「英語学習」というような型にはめる勉強法をほとんどしていないのです。

そんな私がiはつおん、早速試してみました。



私が見つけたiはつおんの最大のメリットは、「正しい発声方法」の「イラスト」と「解説」と「お手本発声」と「自分へのフィードバック(録音)」の4点セットが1画面で提供されていることでした。
普通の英語教材って、
  • CDだったら音はありますが発声方法がない
  • 本だと発声方法はあるけど音がない
  • そもそも自分の発音が正しいのか聴くことができない、テープレコーダーとか大げさで面倒すぎる
などなど、この4点を同時にまとめてカバーしてくれる教材なんて皆無だったのです。
ところがiはつおんなら、発声方法を見ながら、正しい音を同時に聞きつつ、さらに自分で同時に発声して練習もでき、再生ボタンを押せばすぐにフィードバックが返ってきます。
この4つの機能が1画面にまとまっている!というのは他にないアドバンテージですね。



逆に、問題点として以下の2点が浮かびました。
  • 文章単位での練習ができない。せいぜい単語単位までの音の流れしかつかめず、単語同士の接続の流れ(極端に言えばan appleがあんあっぷるじゃなくてあなぽーになる)が身につかないのではないか
  • やればやるほど目に見えて増える項目がないので達成感がない、楽しくない、したがって続かないのではないか
私の経験と照らし合わせると、
  • 聴く内容は文章、またはストーリー
  • 楽しいので毎日毎日朝から晩までやっていた
という要素があり、これが学習効果としてすごくよかったのではないのかと今になって思うからです。

また、例となる単語の数がそれほど多くない(1発音記号につき12個しかない)ので、やはり継続性に欠けると思います。
現在の私は、もっぱらiはつおんを発音方法のリファレンスにしながら、YouTubeの英語の動画の台詞をまねして発音してみたりという勉強方法にシフトしており、iはつおん単品での学習は行っていません。

現在のままでも発音方法のリファレンスとして使う分には100点満点クラスのアプリだと思いますが、せっかくならやはりよい学習コンテンツと組み合わせていただきたい!と感じてます。

そこでもしiはつおん2を出す予定がおありなら、是非ストーリー形式で文章を読み上げていくような形にして欲しいです。
英単語帳にDUOというシリーズがありますが、まさにこのシリーズをiはつおんのシステムでやれば、これまでの英語学習教材にはない学習体験が得られるかも!と考えているのですが、いかがでしょう?

RakuRaku Technologies さん、どうですか!?次はDUOでやっていただけませんか!?

2009年1月18日日曜日

iMac上でWindows XP Professional アップグレード版をBoot Campする

  • AppleはVistaまたはXPの通常版のみをサポートしている
  • が、実際にはアップグレード版もインストールできる
  • そのままではインストールできないので、外付けDVDドライブを買ってくる必要がある
  • ディスプレイドライバを更新する際に、ツールを利用してちょっとしたが必要になる
    http://www28.atwiki.jp/2chmac/pages/59.html を参照のこと
iMac (Early 2008モデル)にWindows XP Professional アップグレード版をBoot Campを用いてインストールしたときの手順メモを公開いたします。メモなのであんまり期待しないでください。



○Boot Campする前にまず覚えておきたい基本動作
・iMacのディスクは再起動時にマウスボタン(右でも左でも)を押しっぱなしにしていれば取り出せる
・再起動時にOptionキーを押しっぱなしにしていると、MacでブートするかWindowsでブートするか選択できる


○アップグレード版Windowsをインストールする方法
・外付けのDVDドライブを用意する。接続はFireWire推奨、でも最近のiMac(Leopard以降)ならばUSBでも起動ディスクとして認識してくれると思う。
・アップグレード対象の古いWindowsを用意する。自分の場合はWindows 2000のSP4適用済みディスクを利用した。
・本体にWindows XPを入れて、外付けにWindows 2000を入れた状態でBoot Campアシスタントからインストールを開始。
 途中で例によって昔のディスクを入れてくれと言われて一度止まるが、そのまま続行すると昔のWindowsを認識してくれてインストールできた
・インストール後のユーザー登録はネットで一発だった。
・パーティションサイズは32GB以下であればFAT32になってMacから読み書きできる。
 それ以上になるとNTFSになるので単純にMacからは見えない。
・インストールするパーティションを聞かれたら、Cドライブ(BOOTCAMP)を選択すること。また、再度FAT32でフォーマットをかけること。


○ディスプレイドライバ
・Leopard付属のドライバでほとんどは事足りるが、そのままではATI Catalyst Control Centerがインストールされないため、目つぶし液晶を暗くすることができない。
(もちろんBoot Campアシスタントを利用すればある程度は暗くできるが、それでもまだ明るすぎるので)
http://www28.atwiki.jp/2chmac/pages/59.html こちらのページに記載されている、「Q ドライバをインストールしてもグラフィックの細かな設定ができません」の項目を参考にして、最新のディスプレイドライバ with Catalyst Control CenterをATIからダウンロードしインストールすると、Control Centerから明度の調整ができるようになって一件落着

iMac買っちゃいました



もうすぐiLife'09がでたり、3月ぐらいには新モデルが出たりするのはわかりきっているのですが、
それでも我慢できなかったので・・・買っちゃいましたよ。

Windowsからの移行だったので過去の資産が使えなくなって困ったりしないかと不安で仕方がなかったのですが、想像より遙かに簡単に移行ができました。
躓いたのは動画(WMVとかDivXは専用のプラグインを入れないと見られない)と、
Leopardのマウスドライバは最悪最低なので、USB Overdriveというサードパーティ製のマウスドライバを使用する必要があるところぐらいでした。

Boot Campも実行してWindows XPと2本立てです。
処理速度も現行最速とはほど遠いですが、それでも自分の用途にとっては十分すぎるぐらいです。

今回iMacにインストールしたソフトウェア一覧はこちらです。
http://osx.iusethis.com/user/akisute

2009年1月11日日曜日

iPhoneやiPhoneシミュレータ上でNSURLCacheクラスを使う

  • NSURLConnectionやNSURLDownloadを利用すると自動的にNSURLCacheにキャッシュデータを蓄える
  • iPhoneシミュレータは/private/var/folders/XX/XXXXXXXXXXXXXXXXXXXXXXX/-Caches-/iPhoneのアプリ名/Cache.dbの中にキャッシュデータを蓄えている
  • iPhone実機では、メモリ上へのキャッシュは働くがファイル上へのキャッシュは行われない。したがってアプリを終了するとキャッシュはすべて消える。
  • NSURLCacheクラスについて参考 http://episteme.arstechnica.com/eve/forums/a/tpc/f/8300945231/m/863005881931/p/5
  • [NSURLRequest setCachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData];を使って、意図的にNSURLCacheクラスへのキャッシュを止めることもできる
  • キャッシュを停止する方法の例 http://github.com/takuma104/ntlniph/tree/master/Classes/models/NTLNHttpClient.m 76行目

HTTP通信を行う際にキャッシュを使いたい場合があると思います。
特に通信状況の良くないiPhoneプログラムでは、キャッシュを利用したいと思う機会が多いはずです。
Cocoaフレームワーク上でHTTP通信を行う場合には、NSURLConnectionクラスやNSURLDownloadクラスを利用するのが一般的だと思いますが、
これらの通信クラスを利用すると、自動的にNSURLCacheクラスのShared Instanceに通信結果がキャッシュされていくようなしくみになっています。

キャッシュされた結果は以下のようなコードで取り出せます。
  NSURL *url = [NSURL URLWithString: urlStr];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//キャッシュされたURLレスポンスを、NSURLCacheのshared instanceから取得します
NSCachedURLResponse *cachedData = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];

こうして取得されたNSCachedURLResponseは通常のNSURLResponseと同じように扱うことができます。

で、問題になってくるのはここからです。
このNSURLCacheクラスは2種類のキャッシュを内部的に持っています。メモリキャッシュとファイルキャッシュです。
メモリキャッシュの方はもはや説明不要だと思うのですが、やっかいなのはファイルキャッシュのほうです。再起動しても結果が消えないため、以前の結果が表示されると言うことが起こりえます。
iPhoneシミュレータ上で実行された場合、どこにこのURLレスポンスのキャッシュが保存されているかを調べてみたところ、
/private/var/folders/XX/XXXXXXXXXXXXXXXXXXXXXXX/-Caches-/iPhoneのアプリ名/Cache.db

の中にキャッシュが生成されていることがわかりました。(XXの部分は実行するマシンによって異なります。)
従って、キャッシュが不要になった場合はこのファイルを消してください。

2009年1月5日月曜日

route-meの、地図画像 (Tile) のキャッシュを操作したい

  • 公式のドキュメントはこちら http://code.google.com/p/route-me/wiki/CacheConfiguration
  • キャッシュにはメモリに対するキャッシュ、ファイルを利用したキャッシュ、sqliteを利用したキャッシュの3通りがある。デフォルトではメモリに32枚、sqliteに無制限に地図画像をキャッシュするようになっている。
  • キャッシュの設定を変更するにはrouteme.plistファイルを用いる
  • route-meは内部的にsqliteを処理するためにFMDBライブラリ (http://gusmueller.com/blog/archives/2008/03/fmdb_for_iphone.html) を利用しているため、route-meを利用したプロジェクトでは何もしなくてもFMDBが使える
  • たとえば、一度OpenStreetMapで開いた地図画像のキャッシュは、地図提供元をVirtualEarthにしてもOpenStreetMapにしても残ってしまうので、一度キャッシュクリアする必要がある
  • キャッシュをまとめて削除したいときはrouteme.plist中でdb-cacheのcapacityとminimalPurgeを1にする
  • またはSQLを直接発行してZCACHEテーブルをDELETEすればよい
  • またはiPhone Simulatorのdbファイルを直接Finderから削除する (~/Library/Application Support/iPhone Simulator/User/Applications/英字の長ったらしい識別子/Documents/*.sqlite) 参考はhttp://ameblo.jp/xcc/entry-10171488747.html

route-meを使っているうちに、困った事態に遭遇しました。
最初の頃に使っていたOpenStreetMapの地図画像が、いつまでたってもVirtualEarthの地図画像に入れ替わらないんです。

ごらんの通りOpenStreetMapの地図が出ます。tileSourceはVirtualEarthになっているのに!

iPhoneシミュレータをリセットしたりアプリケーションを削除したりしましたが効果なしです。
そこで原因を調査したところ、route-meは内部的にsqliteを用いた画像データのキャッシュを持っており、このキャッシュが自動的に最新のデータと入れ替わらないのが原因であることがわかりました。
(通常キャッシュはある程度保持期間が決まっていて、保持期間を過ぎたデータは破棄するようになっていると思うのですが、route-meについては少なくとも1日2日程度の期間ではキャッシュを破棄してくれないようです)
(というかsqliteのデータベースってアプリケーションごとに/var/root/Library/アプリケーション名/以下に配置されているからアプリケーションを削除したら消えるんじゃないんですか・・・?それとも全部ゴミになって残るの?単にiPhoneシミュレータが悪い?)

そこでキャッシュを破棄する方法を調べるがてら、route-meの地図画像キャッシュを設定する方法について調べてみました。
http://code.google.com/p/route-me/wiki/CacheConfiguration
公式のドキュメントをみると、"routeme.plist"というplistファイルを作成してビルドターゲットに加えることで、地図画像キャッシュの動作を変更することができるらしいです。

こんな感じで。


こうすることで、sqliteを用いたキャッシュの中に保存されるデータ量を制限することができます。
ただし、データベースの構造をみても、キャッシュ古くなったデータを自動的に削除するような機能はないようです。そのため、毎日決まった箇所の地図データを見るようなアプリの場合は、何年たっても地図の画像データが更新されません。
このように地図の画像データが更新されない恐れがある場合には、sqliteを用いたキャッシュを利用しないように設定するのがよいと思います。


さて、これで今後のキャッシュの利用方法は設定することができましたので、いますでにキャッシュされてしまっているデータを削除しましょう。
route-meではZCACHEというテーブルの中に地図画像キャッシュデータを保存していますので、このテーブルのデータを削除すればよいです。
sqliteにはTRUNCATEが無いので、DELETE文を使ってすべてのデータを削除します。
DELETE FROM ZCACHE

参考:http://www.mail-archive.com/sqlite-users@sqlite.org/msg09144.html

また、iPhoneシミュレータ上でキャッシュを消したい場合には、以下のパスにsqliteのデータベースファイルが存在しますので、そちらを丸ごと削除すればより簡単です。
(~/Library/Application Support/iPhone Simulator/User/Applications/英字の長ったらしい識別子/Documents/*.sqlite)

さぁ削除していざチャレンジ!


・・・あの、変わってないんですけど。何で?どうして?
ログをコンソールから調べてみましたが、routeme.plistが正しく読み込まれていて、間違いなくDBへのアクセスは発生していません。
となると怪しいのは地図提供元URLへのアクセスです。ここで何らかの仕組みが働いてキャッシュされているのではないかと思って調査してみました。
すると、RMWebTileImageクラスの中にこんなソースを発見。
 NSURL *url = [NSURL URLWithString: urlStr];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSCachedURLResponse *cachedData = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];

怪しいです。怪しさ大爆発です。次回はちょっと脇道にそれて、このNSURLCacheについて調査してみようと思います。

2009年1月3日土曜日

iPhoneでMapView route-meを使ってみよう! RMMarkerManager編

  • マーカーを配置するには、まずRMMarkerクラスのインスタンスを適当に生成する
  • 生成には[RMMarker markerWithNamedStyle:]を使うと便利。styleNameはRMMarker.hで2種類ほど定数として定義されている
  • マーカーを生成したら、[RMMarkerManager addMarker: AtLatLong:]で地図上に配置する
  • RMMarkerのlocation初期値はおそらくnilなので、生成したマーカーを何も考えずに[RMMarkerManager addMarker]とするとエラーになるはず(未確認)
  • マーカーにラベルをつけるときには[RMMarker setLabel:]または[RMMarker setTextLabel:]でよい。setTextLabelは正直いまいち

地図のズーム設定に初期位置設定もできるようになり、さらには地図の提供元の設定もできるようになりました。
しかしこれだけではただの地図が存在するだけです。実用的なアプリケーションにするためには、地図上に情報を配置して、アプリケーションからユーザーに何かを提示してあげなければなりません。
ということでお次は地図上にマーカーを配置してみようと思います。

route-meには標準でマーカーを管理するための機能があります。
マーカーそのものを表すRMMarkerクラスと、マーカーを管理するRMMarkerManagerクラスです。
まずはRMMarkerManagerクラスからみていきましょう。RMMarkerManager.hから、重要そうなプロパティおよびメソッドだけを切り出してみました。
//マーカーの追加・削除・表示非表示
- (void) addMarker: (RMMarker*)marker;
- (void) addMarker: (RMMarker*)marker AtLatLong:(CLLocationCoordinate2D)point;
- (void) addDefaultMarkerAt: (CLLocationCoordinate2D)point;
- (void) removeMarkers;
- (void) hideAllMarkers;
- (void) unhideAllMarkers;

//マーカーの取得
- (NSArray *)getMarkers;
- (void) removeMarker:(RMMarker *)marker;
- (CGPoint) getMarkerScreenCoordinate: (RMMarker *)marker;
- (CLLocationCoordinate2D) getMarkerCoordinate2D: (RMMarker *) marker;
- (NSArray *) getMarkersForScreenBounds;

//状態を取得
- (BOOL) isMarkerWithinScreenBounds:(RMMarker*)marker;
- (BOOL) isMarker:(RMMarker*)marker withinBounds:(CGRect)rect;
- (BOOL) managingMarker:(RMMarker*)marker;

//マーカーの移動
- (void) moveMarker:(RMMarker *)marker AtLatLon:(RMLatLong)point;
- (void) moveMarker:(RMMarker *)marker AtXY:(CGPoint)point;

一通り必要そうなメソッドはそろっており、「使い方等はもう宣言をみればわかるだろ!」といわんばかりの丁寧な作りになっています。
マーカーを配置したければ、- (void) addMarker: (RMMarker*)markerあたりを呼び出せばよさそうですね。

次にRMMarker.hをみてみます。重要な物だけ抜粋してみました。
//マーカーインスタンスの生成
+ (RMMarker*) markerWithNamedStyle: (NSString*) styleName;
- (id) initWithCGImage: (CGImageRef) image anchorPoint: (CGPoint) anchorPoint;
- (id) initWithCGImage: (CGImageRef) image;
- (id) initWithKey: (NSString*) key;
- (id) initWithUIImage: (UIImage*) image;
- (id) initWithStyle: (RMMarkerStyle*) style;
- (id) initWithNamedStyle: (NSString*) styleName;

//ラベルの設定および表示&非表示
- (void) setLabel: (UIView*)aView;
- (void) setTextLabel: (NSString*)text;
- (void) setTextLabel: (NSString*)text toPosition:(CGPoint)position;
- (void) toggleLabel;
- (void) showLabel;
- (void) hideLabel;
- (void) removeLabel;

//マーカーの画像変更および表示&非表示
- (void) replaceImage:(CGImageRef)image anchorPoint:(CGPoint)_anchorPoint;
- (void) hide;
- (void) unhide;

//各種プロパティ(おそらくここから直接操作することはない)
@property (assign, nonatomic) RMXYPoint location;
@property (retain) NSObject* data;
@property (nonatomic, retain) UIView* labelView;

デフォルトで用意されているマーカーを利用する場合は、+ (RMMarker*) markerWithNamedStyle: (NSString*) styleNameを使えばよさそうです。
自分でマーカーの画像を指定する場合は- (id) initWithUIImage: (UIImage*) image;などを用いればうまくいきそうです。

では実際に地図上にマーカーを配置してみます。
例によってRMMapViewの初期表示時に、以下のようなソースを追加します。
    CLLocation *markerLocation = [[[CLLocation alloc]
initWithLatitude:35.689613731585375 longitude:139.7616720199585]
autorelease];
RMMarker *marker = [RMMarker markerWithNamedStyle:RMMarkerRedKey];
[mapView.markerManager addMarker:marker AtLatLong:markerLocation.coordinate];

ソースの先頭で"RMMarker.h"と"RMMarkerManager.h"を#importするのをお忘れなく!
では実行してみましょう。


ばっちりです!マーカーが表示されました。

今度はマーカーにラベルをつけてみます。
ラベルとはマーカーの上に表示される説明書き領域です。RMMarkerには標準でラベルを扱う機能があります。
ラベルとしてシンプルに文字列を指定する方法と、凝ったUIViewを指定する方法が用意されていますが、まずはシンプルに文字列を出してみようと思います。
    [marker setTextLabel:@"Japan Meteorological Agency"];

これを実行すると・・・


出ましたね。
しかし正直言ってこれではわかりづらすぎです。とても実用に耐えません。
ラベルを使いたい場合には、setTextLabel:を用いず、きちんとしたUIViewを設計してから、setLabel:を利用して指定する方がよいと思います。

2009年1月1日木曜日

iPhoneでMapView route-meを使ってみよう! RMMapContents詳細編

  • 利用する地図の提供元を変更したい場合には、tileSourceプロパティを変更する。利用できるtileSourceはMapViewプロジェクト内の/Map/Tile Sourceグループの中にある
  • 地図画像(Tile)が更新されたときに呼び出すためのdelegate(RMTilesUpdateDelegate)を持っている
  • 投影方法(Projection)をメルカトル図法以外に変更できるようなプロパティがあるが、実際に変更できるかどうかは未調査

前回の記事ではroute-meライブラリのRMMapViewクラスを調査して、地図の初期表示座標やズーム倍率を設定する方法を調べてみました。
今回はroute-meライブラリのうち、地図の実体そのものを表現するRMMapContentsクラスを調査してみようと思います。

さっそくRMMapContents.hを読んでみます。利用できるプロパティとメソッドの一覧はこんな感じです。
@property (readwrite) CLLocationCoordinate2D mapCenter;
@property (readwrite) RMXYRect XYBounds;
@property (readonly) RMTileRect tileBounds;
@property (readonly) CGRect screenBounds;
@property (readwrite) float scale;
@property (readwrite) float zoom;

@property (readwrite) float minZoom, maxZoom;

@property (readonly) RMTileImageSet *imagesOnScreen;

@property (readonly) RMProjection *projection;
@property (readonly) id mercatorToTileProjection;
@property (readonly) RMMercatorToScreenProjection *mercatorToScreenProjection;

@property (retain, readwrite) id tileSource;
@property (retain, readwrite) RMMapRenderer *renderer;

@property (readonly) CALayer *layer;

@property (retain, readwrite) RMMapLayer *background;
@property (retain, readwrite) RMLayerSet *overlay;
@property (retain, readonly) RMMarkerManager *markerManager;
@property (nonatomic, retain) id tilesUpdateDelegate;
@property (readwrite) NSUInteger boundingMask;

- (id) initForView: (UIView*) view;
- (id) initForView: (UIView*) view WithLocation:(CLLocationCoordinate2D)latlong;

// Designated initialiser
- (id)initForView:(UIView*)view WithTileSource:(id)tileSource WithRenderer:(RMMapRenderer*)renderer LookingAt:(CLLocationCoordinate2D)latlong;

- (void) didReceiveMemoryWarning;

- (void)moveToLatLong: (CLLocationCoordinate2D)latlong;
- (void)moveToXYPoint: (RMXYPoint)aPoint;

- (void)moveBy: (CGSize) delta;
- (void)zoomByFactor: (float) zoomFactor near:(CGPoint) center;
- (void)zoomInToNextNativeZoomAt:(CGPoint) pivot animated:(BOOL) animated;
- (void)zoomByFactor: (float) zoomFactor near:(CGPoint) center animated:(BOOL) animated;

- (void)zoomInToNextNativeZoomAt:(CGPoint) pivot;
- (float)adjustZoomForBoundingMask:(float)zoomFactor;
- (void)adjustMapPlacementWithScale:(float)aScale;
- (void)setZoomBounds:(float)aMinZoom maxZoom:(float)aMaxZoom;

- (void) drawRect: (CGRect) rect;

// During touch and move operations on the iphone its good practice to
// hold off on any particularly expensive operations so the user's
+ (BOOL) performExpensiveOperations;
+ (void) setPerformExpensiveOperations: (BOOL)p;

- (CGPoint)latLongToPixel:(CLLocationCoordinate2D)latlong;
- (CGPoint)latLongToPixel:(CLLocationCoordinate2D)latlong withScale:(float)aScale;
- (CLLocationCoordinate2D)pixelToLatLong:(CGPoint)aPixel;
- (CLLocationCoordinate2D)pixelToLatLong:(CGPoint)aPixel withScale:(float)aScale;

- (void)zoomWithLatLngBoundsNorthEast:(CLLocationCoordinate2D)ne SouthWest:(CLLocationCoordinate2D)se;
- (void)zoomWithRMMercatorRectBounds:(RMXYRect)bounds;

- (RMLatLongBounds) getScreenCoordinateBounds;
- (RMLatLongBounds) getCoordinateBounds:(CGRect) rect;

- (void) tilesUpdatedRegion:(CGRect)region;

長い!長いです!
RMMapContentsクラス独自のメソッドだけではなくて、RMMapViewクラスが持っている機能と同じ、地図の表示位置を動かしたり、拡大倍率を操作するメソッドも持っています。
どういうことかと調べてみると・・・
-(void) moveToXYPoint: (RMXYPoint) aPoint
{
if (delegateHasBeforeMapMove) [delegate beforeMapMove: self];
[contents moveToXYPoint:aPoint];
if (delegateHasAfterMapMove) [delegate afterMapMove: self];
}
-(void) moveToLatLong: (CLLocationCoordinate2D) point
{
if (delegateHasBeforeMapMove) [delegate beforeMapMove: self];
[contents moveToLatLong:point];
if (delegateHasAfterMapMove) [delegate afterMapMove: self];
}

どうやらRMMapViewクラスのメソッドの実装は、デリゲートのメソッドの呼び出しに加えて、RMMapContentsクラスのメソッドを呼び出しているだけみたいですね。

閑話休題。
たくさん利用できるプロパティとメソッドがありますが、今回はその中でも「利用する地図の提供元を制御する」方法を調べてみようと思います。

RMMapViewの標準の地図提供元はOpenStreetViewです。
http://www.openstreetmap.org/
OpenStreetViewとは一般のユーザーがGPSを利用して測定した道などの地理情報を自由にアップロードして作るオープンソースな地図プロジェクトです。
このOpenStreetView、ヨーロッパなどのプロジェクト参加者の多い地域では非常に精度がよいのですが、
日本ではまだそれほど定着していないようで、ちょっと田舎に入ると地図の精度が絶望的なことになってきます。

ということで、地図の提供元をOpenStreetViewから別の提供元に変更する必要があります。
route-meでは、OpenStreetView以外にも標準で、
Microsoft VirtualEarth(http://www.microsoft.com/japan/virtualearth/ http://maps.live.com/
またはCloudMade(http://www.cloudmade.com/ http://maps.cloudmade.com/
の地図を利用することができます。

やり方は簡単で、RMMapViewのcontentsプロパティのtileSourceプロパティに、
MapViewプロジェクト内の/Map/Tile Sourceグループ内のクラスを指定してやるだけです。
たとえばMicrosoft VirtualEarthを利用するときは、以下のようにします。
    RMMapView *mapView = [[[RMMapView alloc]
initWithFrame:[UIScreen mainScreen].applicationFrame WithLocation:initialLocation.coordinate]
autorelease];
mapView.contents.tileSource = [[[RMVirtualEarthSource alloc] init] autorelease];

これを実行すると以下のようになります。


見事VirtualEarthになりました。これで日本地図を拡大しても安心です。

※2009/01/11追記
本記事で紹介している、RMMapViewクラスのcontentsプロパティのtileSourceプロパティを書き換える方法を用いると、
新しくサーバーからロードされてくるタイルに関しては変更後のtileSourceからロードされてくるのですが、
既存のロード済みのタイルに関しては変更前のtileSourceからロードされてキャッシュされている画像がそのまま表示されてしまいます。

従って、以下のような実装ができません。
  • 最初からOpenStreetMap以外の地図提供元を利用した実装ができない。RMMapViewのinitと同時に最初の地図情報がOpenStreetMapからロードされてしまうため。
  • あとからユーザーの操作に応じて地図の提供元を変更するような実装ができない。
この問題はroute-meプロジェクトのバグトラッカーにも掲載されています。
http://code.google.com/p/route-me/issues/detail?id=12#c5
現在のところは残念ながら改善されていないようです。とりあえず今のところは、以下のような対応を心がけましょう。
  • 途中で地図の提供元を変更する場合には、動的に画像キャッシュをクリアする。ただし、簡単にキャッシュをクリアできるような構造になってはいない。
  • 最初からOpenStreetMap以外の地図提供元を利用するときには、ダウンロードしてきたMapViewプロジェクトのソース自体を直接修正する。
具体的にはRMMapContents.mの56行目の、以下の箇所を書き換えることで実現できます。
- (id) initForView: (UIView*) view WithLocation:(CLLocationCoordinate2D)latlong
{
//この行のRMOpenStreetMapsSourceを変更する
id _tileSource = [[RMOpenStreetMapsSource alloc] init];
RMMapRenderer *_renderer = [[RMCoreAnimationRenderer alloc] initWithContent:self];

id mapContents = [self initForView:view WithTileSource:_tileSource WithRenderer:_renderer LookingAt:latlong];
[_tileSource release];
[_renderer release];

return mapContents;
}

バグトラッカーにも高い優先順位で掲載されているので、近い将来に対応されることが期待できますので、それまではちょっと様子見という感じでしょうか?

iPhoneでMapView route-meを使ってみよう! RMMapView詳細編

  • RMMapViewには主に「現在の表示位置」「現在のズーム」を操作するためのメソッドが用意されている
  • RMMapViewのプロパティとして、マップ自身を表すRMMapContents、マップ上のマーカーを操作するRMMarkerManager、そしてマップからの操作を受け取るRMMapViewDelegateが用意されている

前回の記事では実際にroute-meの地図を自分のアプリに組み込んでiPhoneシミュレーター上で動作させるところまでをやってみました。
今回はさらに一歩進んで、route-meの実体であるRMMapViewの使い方について調べてみようと思います。

○ソースを読んでみる
公式ページを見ても一切ドキュメントが用意されていない・・・ので、ココはソースを読んで解析するしかありません。
まずは前回、実際にViewとして自分のアプリに読み込ませた、RMMapView.hを調査してみます。
// Any other functions you need to manipulate the mapyou can access through this
// property. The contents structure holds the actual map bits.
@property (readonly) RMMapContents *contents;

@property (retain, readonly) RMMarkerManager *markerManager;

// do not retain the delegate so you can let the corresponding controller implement the
// delegate without circular references
@property (assign) id delegate;
@property (readwrite) float decelerationFactor;

- (id)initWithFrame:(CGRect)frame WithLocation:(CLLocationCoordinate2D)latlong;

- (void)moveToLatLong: (CLLocationCoordinate2D)latlong;
- (void)moveToXYPoint: (RMXYPoint)aPoint;

- (void)moveBy: (CGSize) delta;
- (void)zoomByFactor: (float) zoomFactor near:(CGPoint) aPoint;
- (CGPoint)latLongToPixel:(CLLocationCoordinate2D)latlong;
- (CLLocationCoordinate2D)pixelToLatLong:(CGPoint)aPixel;
- (void)zoomInToNextNativeZoomAt:(CGPoint) pivot;
- (void)setZoom:(int)zoomInt;
- (void)zoomWithLatLngBoundsNorthEast:(CLLocationCoordinate2D)ne SouthWest:(CLLocationCoordinate2D)se;
- (void)setZoomBounds:(float)aMinZoom maxZoom:(float)aMaxZoom;

- (RMLatLongBounds) getScreenCoordinateBounds;

うーん読みやすい。すばらしい。
ヘッダのソースコードを読めば、リファレンスがなくても普通に使えそうな感じですね。

では実際に使ってみます。
手始めに、前回までは初期表示としてオーストラリアの片田舎が表示されるようになっていたのですが、
これを日本全体が見えるような初期位置とズーム倍率に設定してみようと思います。

MapViewを初期生成するところで、以下のように初期位置を設定します。
- (void)loadView
{
CLLocation *initialLocation = [[[CLLocation alloc]
initWithLatitude:35.0 longitude:135.0]
autorelease];
self.view = [[[RMMapView alloc]
initWithFrame:[UIScreen mainScreen].applicationFrame WithLocation:initialLocation.coordinate]
autorelease]
}

initWithFrame:(CGRect)frame WithLocation:(CLLocationCoordinate2D)latlongの第2引数の値を生成するために、CLLocationを利用します。
CLLocationはCocoa Touchに標準で付属されている、Core Locationライブラリに含まれるクラスです。
GPSを使ったアプリを作られたことのある方でしたら馴染み深いクラスだと思います。GPSを使う場合には、上記の例のように直接initすることはなく、CLLocationManagerからCLLocationの値を生成します。
CoreLocationを利用するためだと思いますが、ビルド時にCLLocationの実体がないのでリンクができないと怒られてしまいます。以下の図のようにCoreLocation.Frameworkをプロジェクトに追加してください。もちろん追加したらチェックボックスを入れてターゲットに追加するのも忘れずに。


これでビルドが通るはずです。やってみましょう。


初期位置が日本になりました!でもズームがちょっと近すぎる感じがします。もう少し日本全体が入るようにしてみましょう。
[self.view setZoom:5];


これでも何となくうまくいきましたが、ズーム率をint型で指定するのはいまいちわかりづらいです。
表示したい地理的位置がわかっている場合には、緯度経度の値を用いてズームを設定するための便利なメソッドが用意されています。
CLLocation *northEast = [[[CLLocation alloc]
initWithLatitude:40.0 longitude:145.0]
autorelease];
CLLocation *southWest = [[[CLLocation alloc]
initWithLatitude:30.0 longitude:125.0]
autorelease];
[self.view zoomWithLatLngBoundsNorthEast:northEast.coordinate SouthWest:southWest.coordinate];


いい感じですね。日本全体が見えるようになりました。

その他、地図の中心位置を移動するためのメソッド(画面上のXY座標指定、緯度経度指定、偏差指定の3タイプあります)や、
Mapアプリ上で特定地点をダブルタップしたときのように、地図上の特定の位置に向かってズームするためのメソッドが用意されています。