アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

WPF で Google Map & GeoTag

WPF で Google Maps API を利用するシリーズでマップ操作、GeoTag 読み込みシリーズで JPEG の EXIF から GeoTag 読み込みが行えるようになった。

これらの知見を活かし、この記事では GeoTag 編集に挑戦。今回は画像から GeoTag を取得し、Google Maps 上へマーカーとして表示するところまで実装する。

サンプル プログラム

今回のサンプル仕様をまとめる。

  • 取り込んだ画像の GPS 情報から GeoTag を取得する
  • GeoTag をマーカーとしてマップ上に表示する
  • 画像がリスト ボックス上で選択されたらマーカー選択も移動
  • マップ上でマーカーが選択されると、リスト ボックス上のマーカー選択も移動
  • マーカーを動かすと画像の GeoTag を即時更新
  • ただし GeoTag は読み取りのみで画像への書き込みはしない

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

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

マネージ コードと JavaScript の連動

これまでのサンプルでは MapHost というクラスを JavaScript 側へ公開し、マネージ コードと連携させていた。しかしこのクラス自体にデータを持たせていたため、その更新と View 通知の流れが分かりにくくなっていた。

そこで今回からデータ部分を取り除いてイベント ハンドラに徹するよう、再設計を行った。

/// <summary>
/// Google Map の操作を行う JavaScript に関連付けられるクラスです。
/// </summary>
[ComVisible( true )]
public class MapHost
{
    /// <summary>
    /// マップの移動が行われた時、スクリプト側から呼び出されます。
    /// </summary>
    /// <param name="latitude">緯度。</param>
    /// <param name="longitude">経度。</param>
    public void OnMapMoved( double latitude, double longitude )
    {
        this.MapChanged( this, new MapChangedEventArgs( MapAction.Moved, latitude, longitude ) );
    }

    /// <summary>
    /// マーカーが追加された時、スクリプト側から呼び出されます。
    /// </summary>
    /// <param name="id">マーカーの識別子。</param>
    /// <param name="latitude">緯度。</param>
    /// <param name="longitude">経度。</param>
    public void OnMarkerAdded( int id, double latitude, double longitude )
    {
        this.MarkerChanged( this, new MarkerChangedEventArgs( MarkerAction.Added, id, latitude, longitude ) );
    }

    // ...中略

    /// <summary>
    /// マップが変更された時に発生します。
    /// </summary>
    public event EventHandler< MapChangedEventArgs > MapChanged;

    /// <summary>
    /// マーカーが変更された時に発生します。
    /// </summary>
    public event EventHandler< MarkerChangedEventArgs > MarkerChanged;
}

On~ メソッドが JavaScript から呼び出されると、その結果を MapChanged または MarkerChanged としてマネージ コードへ通知している。操作内容を enum、操作結果 ( 対象マーカーや緯度・経度 ) をイベント データ クラスとして定義しており、ハンドラにはこれらが渡される。

イベント ハンドラにする事により、このクラスはデータ更新を仲介するだけで、具体的なデータを知る必要はなくなる。また、イベント ハンドラは += 演算子によって複数設定できるので、更新を受け取る箇所が増えたときも対応しやすくなる。

ListBox とマーカーの選択連動

ListBox の選択変更は SelectionChanged イベントによって通知される。しかしこの方法だと MVVM 的に View へ実装する必要があるので ViewModel への通知が面倒になる。

添付プロパティを利用して SelectionChanged に Command をバインドする方法もあるが、今回は ListBox.SelectedItem と ViewModel をバインドする方法を採用した。

まず ObservableCollection< ImageViewModel > というコレクションが ListBox にバインドされていて、ListBox の SelectionMode は Single ( 単一選択 ) とする。

この時、ListBox.SelectedItem は ImageViewModel となるので、以下のようなプロパティをバインドできる。

private ImageViewModel _selectedImage;

public ImageViewModel SelectedImage
{
    get
    {
        return this._selectedImage;
    }
    set
    {
        this._selectedImage = value;
    }
}

ListBox 上で選択する度にプロパティの set が呼び出されるので、この中でマーカーの選択変更を行える。またマップ上でマーカーに対してクリックやドラッグなどの操作が行われた場合は JavaScript からの通知を受け取った後、マーカーに対応する ImageViewModel インスタンスをこのプロパティに代入する事で ListBox の選択が変更される。

ListBox.SelectedItem を変更した場合、ListBox は自身にバインドされているアイテムから同じインスタンスを検索し、自動的にそれを選択するようになっているので、今回の実装ではこの動作を利用している。

マーカーと画像の関連付け

以前の記事では、マーカーに固有の ID を割り当てていたが、今回のプログラムでは画像に ID を割り当て、それをマーカーと共有するようにしている。

画像は増減する可能性がないが、GeoTag は追加・削除を行うつもりなので、より不変なデータに対して ID を割り当てる方が好ましいと思ったのでそうしている。

その為、JavaScript 側のマーカー追加関数の定義も以下のようになっている。

function addMarker( id, latitude, longitude )
{
    // ...処理
}

id には画像の ID を指定する。画像には必ず ID が割り当てられているので、マーカーは画像と一対一対応するようになる。

その為、マーカー選択を JavaScript 側から通知する場合、id を通知すれば、マネージ コードはその値から選択すべき画像を検索できる。

プログラムの実行

プログラムを実行すると、以下のようになる。

スクリーンショット

「画像を追加」ボタンを押す事で画像を取り込める。GPS 情報が設定されていれば読み込み後に GeoTag を生成、Google Maps 上にマーカーとして配置する。

GeoTag のない画像の場合、「マーカーを追加」ボタンを押す事で GeoTag を設定できる。GeoTag ありならば「選択しているマーカーの位置へ移動」ボタンでその位置へ移動して「選択しているマーカーを削除」ボタンでマーカーを削除出来る。マーカーを削除した場合、「マーカーを追加」ボタンでマーカーの再設定が行える。

GPS 情報をもつ JPEG 画像を用意する場合、GPS 埋め込みに対応したデジカメやスマートフォンで写真を撮影するか、以下のアプリで設定できる。