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アプリ上で特定地点をダブルタップしたときのように、地図上の特定の位置に向かってズームするためのメソッドが用意されています。