[Swift] MapKitを使って”ジオコーディング・逆ジオコーディング”をやってみる

2018.12.11

はじめに

モバイルアプリサービス部の中安です。

今回は、先日「MapKitを使って”場所のサジェスト”を簡単に実装してみる」のお題で書いたブログに続いて、簡単にジオコーディングをやってみようというテーマでやってみます。

地図を使ったTipsについては実は入社してすぐにもやっていたりするので、興味のある方はこちらのブログもどうぞ

ジオコーディング・逆ジオコーディング について

最近のブログは言葉の定義から始めています。今回もまずは基本的なおさらいから

「ジオコーディング」

住所や地名、駅名などの地理的情報を、緯度・経度の座標値に変換する技術のことである。日常生活で使用している、場所を示すキーワードを用いて地図上の場所を特定することができるため、地図データの利用効率を向上させることができる。(Weblio 辞書)

「逆ジオコーディング」

ジオコーディングの後に、座標に応じた地点の地図を表示するソフトウェアを連動させることにより、地名を入力して、該当地点の地図を表示することができる。地図検索サービスがこの応用例である。座標を入力して、該当地点の地名を得ることもこの技術範囲であり、厳密には逆ジオコーディングと称する。(Wikipedia)

おさらいすると

  • ジオコーディング ・・・ 住所から緯度経度に変換すること
  • 逆ジオコーディング ・・・ 緯度経度から住所に変換すること

GoogleMapに住所を入れて検索すると、その場所の地図を表示してくれて緯度経度まで表示してくれます。まさにこれがジオコーディングの技術であるといえます。

iOSにおけるジオコーディング

ジオコーディングも逆ジオコーディングも CoreLocation フレームワークに搭載された CLGeocoder クラスを使用することで簡単に実現できます。 CoreLocationMapKit フレームワークに含まれるので、地図を使う場合は MapKitをインポートするだけで使用できると思います。

CLGeocoderクラスでジオコーディングを実行すると、CLPlacemarkというオブジェクトが結果として返ります。あとはそれを使いたいように使うだけですね

ジオコーディング

実装はすごく簡単です。

import MapKit // import CoreLocation でも可

let address = "どこかの住所"
CLGeocoder().geocodeAddressString(address) { placemarks, error in
    if let lat = placemarks?.first?.location?.coordinate.latitude {
        print("緯度 : \(lat)")
    }
    if let lng = placemarks?.first?.location?.coordinate.longitude {
        print("経度 : \(lng)")
    }
}

placemark.location には、CLLocation オブジェクトが代入されていて緯度経度が簡単に取れます。

空文字を与えたり、存在しない住所を引数にしたりして取得できなかった場合は error が nil ではなくなるので、 本来であればハンドリングしてあげる必要もあると思います。

ちなみに「こりん星」をここに与えてみると、アメリカがヒットしました。こりん星はアメリカにあるらしいです。

逆ジオコーディング

import MapKit // import CoreLocation でも可

let location = CLLocation(latitude: {緯度}, longitude: {経度})
CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
    guard let placemark = placemarks?.first, error == nil else { return }
    // あとは煮るなり焼くなり
}

CLPlacemarkオブジェクトには実に様々な住所に関わる情報が入っています。 これらを組み合わせて住所を取得することができます。

プロパティ名 リファレンスの説明
location CLLocation? The location object containing latitude and longitude information. This object is used to initialize the placemark object.
name String? The name of the placemark.
isoCountryCode String? The abbreviated country name. This string is the standard abbreviation used to refer to the country. For example, if the placemark location is Apple’s headquarters, the value for this property would be the string “US”.
country String? The name of the country associated with the placemark. If the placemark location is Apple’s headquarters, for example, the value for this property would be the string “United States”.
postalCode String? The postal code associated with the placemark. If the placemark location is Apple’s headquarters, for example, the value for this property would be the string “95014”.
administrativeArea String? The state or province associated with the placemark. The string in this property can be either the spelled out name of the administrative area or its designated abbreviation, if one exists. If the placemark location is Apple’s headquarters, for example, the value for this property would be the string “CA” or “California”.
subAdministrativeArea String? Additional administrative area information for the placemark. Subadministrative areas typically correspond to counties or other regions that are then organized into a larger administrative area or state. For example, if the placemark location is Apple’s headquarters, the value for this property would be the string “Santa Clara”, which is the county in California that contains the city of Cupertino.
locality String? The city associated with the placemark. If the placemark location is Apple’s headquarters, for example, the value for this property would be the string “Cupertino”.
subLocality String? Additional city-level information for the placemark. This property contains additional information, such as the name of the neighborhood or landmark associated with the placemark. It might also refer to a common name that is associated with the location.
thoroughfare String? The street address associated with the placemark. The street address contains the street name. For example, if the placemark location is Apple’s headquarters, the value for this property would be the string “Infinite Loop”.
subThoroughfare String? Additional street-level information for the placemark. Subthroughfares provide information such as the street number for the location. For example, if the placemark location is Apple’s headquarters (1 Infinite Loop), the value for this property would be the string “1”.
region CLRegion? The geographic region associated with the placemark.
timeZone TimeZone? The time zone associated with the placemark.
inlandWater String? The name of the inland water body associated with the placemark. For coordinates that lie over an inland body of water, this property contains the name of that water body—the name of a lake, stream, river, or other waterway.
ocean String? The name of the ocean associated with the placemark. For coordinates that lie over an ocean, this property contains the name of the ocean.
areasOfInterest [String]? The relevant areas of interest associated with the placemark. Examples of an area of interest are the name of a military base, large national park, or an attraction such as the Eiffel Tower, Disneyland, or Golden Gate Park.

ざざざっとリファレンス通り書いてみましたが、日本向けのiOSアプリでは日本の住所を使用することが多いと思います。日本国内での地図はどのようにとれるかをまとめてみます。

プロパティ名 概略 取得結果
location CLLocationオブジェクト (国会議事堂の場合)
35.67626984,139.74646739
name 名前 (国会議事堂の場合)
永田町1丁目7
(大阪駅の場合)
大阪駅
isoCountryCode ISO国コード JP
country 国名 日本
postalCode 郵便番号 (国会議事堂の場合)
100-0014
administrativeArea 都道府県 (国会議事堂の場合)
東京都
subAdministrativeArea (埼玉県秩父郡東秩父村の場合)
秩父郡
locality 市区町村 (国会議事堂の場合)
千代田区
(大阪駅の場合)
大阪市北区
(埼玉県秩父郡東秩父村の場合)
秩父郡東秩父村
subLocality 丁番なしの地名 (国会議事堂の場合)
永田町
(大阪駅の場合)
梅田
thoroughfare 丁目がある場合、それを含む地名 (国会議事堂の場合)
永田町1丁目
(大阪駅の場合)
梅田3丁目
subThoroughfare 番地 (国会議事堂の場合)
7
(大阪駅の場合)
1番1号
region CLRegionオブジェクト CLCircularRegion
timeZone TimeZoneオブジェクト Asia/Tokyo
inlandWater 内陸水 (琵琶湖の場合)
Lake Biwa
(霞ヶ浦の場合)
nil
ocean 海名 日本近郊はフィリピン海と北太平洋が返却されるがあまり正確ではない
areasOfInterest 関心のあるエリア 大阪駅や新宿駅は返却されるが、東京駅は返されなかった。また、国内の観光地なども返る返らないが存在する

※海外の場合は若干違いがあることに注意が必要

このようにしてみると、例えば国会議事堂の住所を取得しようとする場合に、 administrativeArea + locality + name で「東京都千代田区永田町1丁目7」と取得できるように見えます。

しかし、name は罠が潜んでいて、例えば大阪駅の場合に上記の方法で取得すると、本来取りたい住所は「大阪府大阪市北区梅田3丁目1番1号」であるにもかかわらず「大阪府大阪市北区大阪駅」という住所になってしまいます。 基本的には「地名+丁番」という形式なのですが「施設名」になったり「緯度経度」になったりと不安定な要素なので、name は住所取得という目的のためには使わないほうがよさそうです。

ここは、administrativeArea + locality + thoroughfare + subThoroughfare であるほうが自然な住所が取れます。

また、iOS11からは postalAddress プロパティが addressDictionary に代わって登場しています。 同じような情報が取得できるのですが、こちらは"連絡先"用の Contacts フレームワークと融和するためのものとあるので割愛します。

おためしアプリ

最後に、逆ジオコーディングが簡単に試せるような"おためしアプリ"を作ってみました。

ストーリーボード

ストーリーボードには、マップビュー(MKMapView)と結果を表示するためのラベル(UILabel)を配置しており、IBOutletは赤字で示したとおりです。

ビューコントローラ

マップビューにタップジェスチャレコグナイザを与えることにより、地図のタップイベントを拾います。イベントがハンドルされると convert(_: toCoordinateFrom:) メソッドにより、地図のどこがタップされたかを緯度経度で取得しています。 あとはそれを逆ジオコーディングするだけです。

import MapKit

class ViewController: UIViewController {
    
    @IBOutlet private weak var map: MKMapView!
    @IBOutlet private weak var geocodeLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let gesture = UITapGestureRecognizer(target: self, action: #selector(mapDidTap(_:)))
        map.addGestureRecognizer(gesture)
        map.setRegion(defaultRegion, animated: false)
    }
    
    @objc private func mapDidTap(_ gesture: UITapGestureRecognizer) {
        let coordinate = map.convert(gesture.location(in: map), toCoordinateFrom: map)
        let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
        CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
            guard
                let placemark = placemarks?.first, error == nil,
                let administrativeArea = placemark.administrativeArea, // 都道府県
                let locality = placemark.locality, // 市区町村
                let thoroughfare = placemark.thoroughfare, // 地名(丁目)
                let subThoroughfare = placemark.subThoroughfare, // 番地
                let postalCode = placemark.postalCode, // 郵便番号
                let location = placemark.location // 緯度経度情報
                else {
                    self.geocodeLabel.text = ""
                    return
            }
            
            self.geocodeLabel.text = """
                〒\(postalCode)
                \(administrativeArea)\(locality)\(thoroughfare)\(subThoroughfare)
                \(location.coordinate.latitude), \(location.coordinate.longitude)
            """
        }
    }
    
    private var defaultRegion: MKCoordinateRegion {
        let coordinate = CLLocationCoordinate2D( // 大阪駅
            latitude: 34.7024854,
            longitude: 135.4937619
        )
        let span = MKCoordinateSpan (
            latitudeDelta: 0.01,
            longitudeDelta: 0.01
        )
        return MKCoordinateRegion(center: coordinate, span: span)
    }
}

こんな感じに動きます

地図をタップすると、下のラベルにタップした箇所の郵便番号と住所と緯度経度が表示されます。(日本でしか試してないです)

最後に

前回「場所のサジェスト」について書きましたが、 これと逆ジオコーディングとを組み合わせると更に立地な地図アプリに近づけるような気がします。

何かの参考になれば幸いです。