読者です 読者をやめる 読者になる 読者になる

プログラミングノート

一からものを作ることが好きなエンジニアの開発ブログです。

Google Maps JavaScript APIを利用したジオコーディングの実装について

iOS

地名, 住所から緯度経度を検索したいと思い、まずはCoreLocation frameworkのCLGeocoderを利用してみました。

CLGeocoder *geocoder = [[[CLGeocoder alloc] init] autorelease];
  [geocoder geocodeAddressString:@"東京タワー" completionHandler:^(NSArray* placemarks, NSError* error){
    if(error){
        NSLog(@"%@", [error description]);
        return;
    }
    NSLog(@"%@", [placemarks description]);
}];


検索箇所だけを抜き出すとこれだけなので、かなりお手軽でいい感じだったのですが、残念ながら検索DBがまだあまり充実していないらしく、プロダクトで利用するにはちょっと微妙な検索結果でした。


良い方法ないかなーと思ってさまよっていたのですが、Google Maps JavaScript API V3で提供されているジオコーディングAPIが理想に近い感じだったので、これをアプリ内から利用できるようにしてみました。

JavaScript

まずは与えられた文字から複数の該当地点を返す処理をJavaScriptで作ります。{"name":"名称", "address":"住所", "lat":緯度, "lon":経度}を配列にしてJSONで返します。UIWebViewでWebとネイティブを相互連携させる方法についてに記載している方法でネイティブ側に渡すため "geocoder://JSONデータ" として返しています。(エラーの場合は "error://ステータス")

<!DOCTYPE html>
<html>
  <head>
  <meta charset="utf-8" />
  <script src="https://maps.googleapis.com/maps/api/js?sensor=false"></script>
  <script>
    var geocoder = new google.maps.Geocoder();
    
    function search(query){
      var func = function(results, status) {
        if (status == google.maps.GeocoderStatus.OK) {
          var locations = [];
          for(var i=0, len=results.length; i<len; i++){        
            var ll = results[i].geometry.location;
            var poi = {
              "name": extractPoi(results[i]),
              "address": extractAddress(results[i]),
              "lat": ll.lat(),
              "lon" : ll.lng()
            };
            locations.push(poi);
          }      
          document.location = "geocoder://" + JSON.stringify(locations);      
        }else{
          document.location = "error://" + status;
        }
      }
      geocoder.geocode({'address': query}, func);
    }

    // 住所リストの中から名称を取得    
    function extractPoi(geocoder_results){
      var address_components = geocoder_results.address_components;    
      for(var i=0, len = address_components.length; i<len; i++){
        var comp = address_components[i];      
        for(var j=0, len2 = comp.types.length; j<len2; j++){
          if(comp.types[j] == "point_of_interest"){
            return comp.long_name;
          }
        }
      }
      return extractAddress(geocoder_results);
    }

    // 住所に含まれる「日本, 」はいらないのでトル
    function extractAddress(geocoder_results){
      return  geocoder_results.formatted_address.replace(/^日本, /, "");
    }    
  </script>
  </head>
  <body />
</html>

ネイティブ側

次にこれをネイティブ側から実行するためのクラスを作ります。UIWebViewを生成し、予め上記のHTMLをロードさせておきます。ロード完了までに少し時間がかかるので、生成直後には検索を行えません。このWebViewはAPIアクセスのためだけに利用するので、画面自体には表示しません。SBJsonを利用して、"geocoder://JSONデータ" をNSDictionaryに変換しています。

Geocoder.h
typedef void (^GeocoderHandler)(NSDictionary *placeInfo, NSError* error);
@interface Geocoder : NSObject
- (void)locationByWord:(NSString *)word completion:(GeocoderHandler)handler;
@end
Geocoder.m
#import "Geocoder.h"
#import "SBJson.h"

@interface Geocoder() <UIWebViewDelegate>
@property (nonatomic, retain) UIWebView *webView;
@property (nonatomic, copy) GeocoderHandler hander;
@end

@implementation Geocoder

- (void)dealloc{
  [_hander release];
  [_webView release];
  [super dealloc];
}

- (id)init{
  if(self = [super init]){
    UIWebView *_web = [[UIWebView alloc] initWithFrame:CGRectZero];
    _web.delegate = self;
    self.webView = _web;
    [_web release];
        
    // 先ほど作成したHTMLをロードする
    NSString *path = [[NSBundle mainBundle] pathForResource:@"geocoder" ofType:@"html"];
    [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]]];
  }
  return self;
}

- (void)locationByWord:(NSString *)word completion:(GeocoderHandler)handler{
  [_hander release], _hander = nil;
  self.hander = handler;

  NSString *w = word;
  w = [w stringByReplacingOccurrencesOfString:@"\"" withString:@""];
  w = [w stringByReplacingOccurrencesOfString:@"'" withString:@""];
    
  // JSの関数を叩く
  NSString *func = [NSString stringWithFormat:@"search('%@')", w];
  [self.webView stringByEvaluatingJavaScriptFromString:func];
}

// UIWebViewDelegate -> document.locationが変更されると呼ばれる
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
  NSString *requestString = [[request URL] absoluteString];

  if ([requestString rangeOfString:@"error://"].location != NSNotFound){
    NSString *message = [self decodeString:[requestString stringByReplacingOccurrencesOfString:@"error://" withString:@""]];
    NSDictionary *info = @{
      NSLocalizedDescriptionKey: message
    };
    NSError *error = [[[NSError alloc] initWithDomain:@"GeocoderError" code:0 userInfo:info] autorelease];
    self.hander(nil, error);
    return NO;
        
  }else if ([requestString rangeOfString:@"geocoder://"].location != NSNotFound){
    NSString *json = [self decodeString:[requestString stringByReplacingOccurrencesOfString:@"geocoder://" withString:@""]];
    SBJsonParser *parser = [[SBJsonParser alloc] init];
    NSDictionary *object = [parser objectWithString:json error:nil];
    [parser release];
    self.hander(object, nil);
    return NO;
  }
  return YES;
}

// Util
- (NSString *)decodeString:(NSString *)encodedString{
  NSString *s = (NSString *)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL,
                                                                                      (CFStringRef)encodedString,
                                                                                      CFSTR(""),
                                                                                      kCFStringEncodingUTF8);
  return [s autorelease];
}
@end

ネイティブ側2 検索実行

最後に、上記クラスを通してジオコーディングAPIにアクセスします。CoreLocationのAPIと同じような感じで利用できるようにしてみました。これでGoogleのジオコーディングAPIにアクセスし、複数地点の検索結果を取得することができます。

// 予めどこかで生成しておく
Geocoder *geo = [[Geocoder alloc] init];
self.geocoder = geo;
[geo release];

// 検索実行
- (void)searchBarSearchButtonClicked:(UISearchBar *)uiSearchBar {
  [uiSearchBar resignFirstResponder];
    
  [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
  [self.geocoder locationByWord:uiSearchBar.text completion:^(NSDictionary *placeInfo, NSError *error){
    if(error){
      NSLog(@"%@", [error localizedDescription]);
    }else{
      NSLog(@"%@", placeInfo); 
    }
    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
  }];    
}

検索結果画面

こんな感じで利用できます!



経路検索やスクレイピングなどの他のテーマで、同じような処理を見かけたことがありましたが、色々と応用範囲が広そうな感じです。(ただこれって審査通るのだろうか..?)