アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

WPF で Google Map & GeoTag その 3

WPF で Google Maps API & GeoTag を扱うシリーズその 3。

前回は GeoTag 編集した内容を EXIF として画像へ保存するところまで実装した。今回は GeoTag から住所を取得し、その内容を IPTC の撮影場所情報として保存する...予定だったのだが、IPTC 書き込みが上手くいかなかったので住所取得だけ実装してみる。

サンプル プログラム

前回のプログラムからの主な変更点。

  • 住所取得をサポート

サンプル プログラムのプロジェクト一式は以下。ビルドには Visual Studio 2008 SP1、プログラムの実行には NET Framework 3.5 SP1 とネットワーク接続された環境が必要となる。

※緯度・経度の方角のパス指定が間違っていたので、2009/11/08 に zip を修正版に差し替えました。

このプログラムの GeoTag 書き換えは画像を書き換えるため、バグにより破損する可能性もあります。試される場合はテスト用の画像を別途用意する事を推奨します。[/warning]

住所の取得

住所の取得には、Google Map API の google.maps.Geocoder オブジェクトを使用する。これまでは google.maps.Geocoder.geocode メソッドに住所を指定する事で、緯度・経度の情報を取得してきたが、住所を取得する場合は、このメソッドに緯度・経度を指定する。

今回のプログラムの JavaScript 部分では、以下がその処理となる。

/**
 * 指定されたマーカーのコレクション中の住所を取得します。
 *
 * @param   id  住所を取得するマーカーの識別子。
 */
function getAddress( id )
{
    var index = searchMarker( id );
    if( index == -1 ) { return; }

    var latlng = new google.maps.LatLng(  markers[ index ].position.lat(), markers[ index ].position.lng() );

    if( geo )
    {
        geo.geocode( { 'latLng': latlng }, function( results, status )
        {
            if( results && results[ 0 ] )
            {
                // 住所は種別と内容に分かれているが、それらを「種別:内容」という書式でまとめ、
                // 種別ごとのデータ区切りは ; とする事で、マネージコード側の加工を容易にする。
                //
                var info;
                var address = results[ 0 ].address_components;
                for( i = 0; i < address.length; ++i )
                {
                    var types = address[ i ].types;
                    for( j = 0; j < types.length; ++j )
                    {
                        info += "[" + types[ j ] + "]" + address[ i ].long_name + ";";
                    }
                }

                // 最後に整形済みの住所を加えておく
                info += "[formatted_address]" + results[ 0 ].formatted_address + ";";
                window.external.OnMarkerAddress( id, info );
            }
        } );
    }
}

var info; からの処理は住所データの詳細と Geocoder が整形してくれた住所文字列を種別毎にパッケージして [種別 1]内容 1;[種別 2]内容 2; ...etc のように文字列化している。

Geocoder から取得できる住所情報は formatted_addressaddress_components に大別される。前者は「日本東京都新宿区坂町~」のように整形済みの文字列で、後者は国や県などの配列となっている。詳細については、以下のリファレンスを参照の事 。

最後の window.external.OnMarkerAddress で C# 側のメソッドを呼び出している。この対象となる MapHost.OnMarkerAddress メソッドにブレーク ポイントを貼ってからマーカー移動すると実際に取得されたデータを確認できるはず。

住所の解析

Goecoder から取得した address_components の配列は

  1. 詳細
  2. 広域

という順番で並んでいる。例えば「日本東京都新宿区坂町 1-5」のような住所の場合、配列の並びは { "5", "1", "坂町", "新宿区", "東京都", "日本" } のようになる。

要素の種別は types から判定可能。locality より小さい部分はすべて sublocality になるため県や州より詳細な情報を参照するなら順番を意識しなければならない。なおこの部分は administrative_area_level_2、3 となることもあるようだ。テストを行っていて、テキサス州の住所取得に失敗した事で気づいた。

今回のプログラムでは住所を IPTC の撮影場所として保存したいので、少なくとも都市までは取得しなければならない。国を coutry、都市は administrative_area_level_1 で取得しつつ残りを処理する。これを Address クラスへ以下のように実装。文字列の定数定義などは長くなるので割愛。

/// <summary>
/// 住所を表します。
/// </summary>
class Address
{
    /// <summary>
    /// JavaScript によって、Goole Map の Geocoder から取得した住所情報から Address インスタンスを生成します。
    /// Address クラスのインスタンスは、このメソッドだけが生成可能です。
    /// </summary>
    /// <param name="address">Geocoder から取得した住所文字列。</param>
    /// <returns></returns>
    public static Address FromGeocoderAddress( string address )
    {
        if( String.IsNullOrEmpty( address ) ) { return null; }

        // 整形済みの住所は常に存在している必要がある
        var formatedAddress = Address.GetAdressValue( address, Address.TagFormatedAddress );
        if( String.IsNullOrEmpty( formatedAddress ) ) { return null; }

        return new Address()
        {
            FormatedAddress = formatedAddress,
            Country         = Address.GetAdressValue( address, Address.TagCountry ),
            ProvinceState   = Address.GetAdressValue( address, Address.TagProvinceState ),
            City            = Address.GetAddressValueCity( address )
        };
    }

    /// <summary>
    /// JavaScript によって、Goole Map の Geocoder から取得した住所情報から、一つの住所区分を取得します。
    /// 住所情報となる文字列は、"country:日本;administrative_area_level_1:東京都; ...etc" という形式となります。
    /// </summary>
    /// <param name="address">Geocoder から取得した住所情報の文字列。</param>
    /// <param name="tag">取得する値の種別を示すタグ名。</param>
    /// <returns>取得した住所区分。</returns>
    private static string GetAdressValue( string address, string tag )
    {
        return Address.GetAdressValue( address, tag, true );
    }

    /// <summary>
    /// JavaScript によって、Goole Map の Geocoder から取得した住所情報から、一つの住所区分を取得します。
    /// 住所情報となる文字列は、"country:日本;administrative_area_level_1:東京都; ...etc" という形式となります。
    /// </summary>
    /// <param name="address">Geocoder から取得した住所情報の文字列。</param>
    /// <param name="tag">取得する値の種別を示すタグ名。</param>
    /// <param name="isLastIndexOf">tag を先頭から検索する場合は true、末尾からの場合は false。</param>
    /// <returns>取得した住所区分。</returns>
    private static string GetAdressValue( string address, string tag, bool isIndexOf )
    {
        int index = ( isIndexOf ? address.IndexOf( tag ) : address.LastIndexOf( tag ) );
        if( index == -1 ) { return null; }

        int begin = index + tag.Length;
        index = address.IndexOf( ";", begin );
        if( index == -1 ) { return null; }

        int length = index - begin;
        return address.Substring( begin, length );
    }

    /// <summary>
    /// 都市情報を取得します。
    /// </summary>
    /// <param name="address">Geocoder から取得した住所情報の文字列。</param>
    /// <returns></returns>
    private static string GetAddressValueCity( string address )
    {
        // 例えば住所の都市部分が A 市 B 町 6-2 の場合、{ "2", "6", "B 町", "A 市" } のように格納されていて、
        // "A 市" が TagCity、それ以降が全て TagSubCity になるので、TagCity と最後に登場する TagSubCity で取得する。
        //
        var city = String.Format( "{0}{1}", Address.GetAdressValue( address, Address.TagCity ), Address.GetAdressValue( address, Address.TagSubCity, false ) );
        if( String.IsNullOrEmpty( city ) )
        {
            city = String.Format( "{0}{1}", Address.GetAdressValue( address, Address.TagProvinceState2 ), Address.GetAdressValue( address, Address.TagProvinceState3 ) );
        }

        return city;
    }
}

GetAddressValueCity メソッドの処理が配列の並びを意識した対策となる。

ちなみに文字列の解析で String.Split の代りに String.IndexOfLastIndexOf を使用している理由は、これらの処理がそれなりに多く実行される事を想定している為となる。以下のリファレンスにも解説されているが Split メソッドの生成する分割結果は都度メモリの確保を行う。

住所データをデバッガなどで見ると分かるがデータは country などの固有の種別と political という汎用な種別の 2 種類を持つ。そのため単純に Split すると必要なデータ量は 2 倍になる。この程度のメモリなら現実的に問題にはならないだろう。しかし不要なメモリが確保されるのは個人的に嫌なので対策することにした。

プログラムの実行

プログラムを実行すると以下のようになる。右下へ取得した住所を表示するようにした。

スクリーン ショット

より詳細な住所も持っているけど表示には Geocoder が整形したものを指定。ただし海上や山間部、住所が公開されていない土地などはそもそも住所を取得できない。そのため何も表示されないだろう。

一部の住所、例えば新宿都庁近辺や皇居などは住所の代りに郵便番号が表示される。この動作が Google Maps API の仕様なのかは謎である。だいたいは住所の取得が可能で国道上などではそれと分かるような表記になってくれるから、意外な用途が見つかるかもしれない。

このシリーズに次回があるなら、取得した住所の IPTC 保存に再チャレンジしてみたい。