使ってみよう! Bing API/SDK

第8回Hello, Bing Map App!─⁠─Silverlightで作るBing Mapsアプリケーション(8)

Map App SDKのアップデート

今回は、前回の続きでMy Photo Map Appの作成です。各機能の処理を記述していきますが、その前にMap App SDKのアップデートがありましたので、その紹介です。

これまで使用してきたMap App SDKおよび開発用のMap AppのMap app test toolが、9月16日にアップデートされています。

今回の更新でVisual StudioのMap Appプロジェクトテンプレートを使用した場合、Visual Studioから実行可能になっており、段違いに開発しやすくなっています。テンプレートで生成されるファイルやコードも拡充しています。

この更新に合わせて、Map app test toolも更新され、Silverlightアプリケーションで生成されるXAPファイルの指定が可能なっています。これまでと同様にDLLファイルを指定してMap Appの登録も可能です。DLLファイルを指定する際のインターフェースが使いやすく改良もされています図1⁠。

図1 Map app test tool
図1 Map app test tool

さらに、プラグインの実行時に例外が発生した場合、エラーダイアログを表示する改良や、Map App審査登録時にXAPファイルを指定可能などの変更もあります。

以前のSDKをアンインストール後、新しいバージョンをインストールしておきましょう。前回・今回の内容では新しいSDKのテンプレートを使用した内容ではありませんが、テンプレートへコード内容を移すことは難しくないと思いますのでトライしてみてください。

パネルのイベント

前回何も処理を記述していなかったパネルの各コントロールのイベントを順に実装していきましょう。イベントは次の3か所ありました。

  • リンクのクリック(HyperlinkButton_Click)
  • リストボックスのドロップ(PhotoListBox_Drop)
  • ボタンのクリック(Button_Click)

前回に作成済みのMyPhotoPanelクラス、MyPhotoPanel.xaml.csファイル内のコードを編集します。

ボタンのクリック

まず、⁠Add photo」ボタンクリックしたときの処理内容は次のように行います。

  1. ファイルを開くダイアログを表示
  2. 選択された写真(JPEG)ファイルを読み込み、PhotoEntityオブジェクトを生成
  3. PhotoEntity オブジェクトをPhotoItemsコレクションに追加
  4. PhotoEntityオブジェクトをレイヤーに追加
  5. 追加した地点へ地図を移動

PhotoEntityは前回作成したエンティティ クラス、PhotoItemsはPhotoItemのコレクションです。PhotoEntityの生成には、写真ファイルから経緯度情報とサムネイルの取得処理がありますが、これは別にメソッドを作って行います。

上記内容のコードを記述すると次のようになります。

private void Button_Click(object sender, RoutedEventArgs e)
{
    // ファイルを開くダイアログを表示
    var dialog = new OpenFileDialog();
    dialog.Filter = "写真ファイル(*.jpg, *.jpeg)|*.jpg;*.jpeg";
    if (dialog.ShowDialog() != true)
    {
        return;
    }
    
    try
    {
        // PhotoEntity 作成
        var entity = CreateEntity(dialog.File); // (CreateEntity は後で作成)
        if (entity != null)
        {
            // コレクションに追加
            PhotoItems.Add(entity);
            // レイヤーに追加
            this.plugin.MainLayer.Entities.Add(entity);
            // 追加した地点へ地図を移動
            this.plugin.DefaultMap.SetView(entity, 15);
        }
    }
    catch (Exception)
    {
        // Do nothing
    }
}

CreateEntityメソッドは後で作成します。

リストボックスへファイルのドロップ

続いてListBoxのDropイベント処理です。Silverlight 4からWebブラウザーにドロップされたファイルを開くことができるようになっています。ボタンクリック処理と同様に、リストボックスにファイルをドロップしても写真ファイルを開けるようにしましょう。

ドロップを許可するには、ListBoxのAllowDropプロパティをTrueに設定します(前回のXAMLの記述で行っています⁠⁠。ドロップ時の処理は次のようになります。

private void PhotoListBox_Drop(object sender, DragEventArgs e)
{
    try
    {
        var files = (FileInfo[])e.Data.GetData(DataFormats.FileDrop);
        PhotoEntity lastEntity = null;
        foreach (var f in files)
        {
            var entity = CreateEntity(f);
            if (entity != null)
            {
                lastEntity = entity;
                PhotoItems.Add(entity);
                this.plugin.MainLayer.Entities.Add(entity);
            }
        }

        // 最後に追加した写真の場所へ移動
        if (lastEntity != null)
        {
            this.plugin.DefaultMap.SetView(lastEntity, 15);
        }
    }
    catch (Exception)
    {
        // Do nothing
    }
}

複数のファイルがドロップされた場合も対応しています。そのためボタンクリック時とは少し記述が異なりますが、FileInfoオブジェクトを、OpenFileDialog.Fileプロパティから得るか、DragEventArgs.Data.GetDataプロパティから得るかの違いだけです。

Silverlightアプリケーションでは、ユーザーファイルのアクセスは通常できませんが、このようにユーザー自身がダイアログを通してファイルの選択や、ブラウザーへファイルをドロップした場合は、アプリケーションからファイルアクセスが可能です。

リンクのクリック

最後にHyperlinkButtonのClickイベントの処理です。ListBox内の各アイテムはひとつのHyperlinkButtonがあり、これをクリックしたときの処理です。処理内容は、クリックしたアイテムの写真の場所への移動です。

private void HyperlinkButton_Click(object sender, RoutedEventArgs e)
{
    var entity = (PhotoEntity)((HyperlinkButton)sender).DataContext;
    this.plugin.DefaultMap.SetView(entity, 15);
}

少しわかりにくいかもしれませんが、アプリケーションをXAMLのデータバインディング機能を使用して作成し、HyperlinkButton.DataContextプロパティからPhotoEntityオブジェクトを参照できる構成となるため、上記コードのように記述すると写真の場所へ移動できます。

以上でイベント処理の記述は完了です。

エンティティの作成

次にユーザーが選択した写真ファイルから、PhotoEntityオブジェクトを作成します。既に記述したコードのCreateEntityメソッド部分です。このメソッドは、FileInfoオブジェクトからPhotoEnityを作成して返します。

private PhotoEntity CreateEntity(FileInfo file)
{
    var entity = new PhotoEntity();
    entity.Name = file.Name; // 名前はファイル名を使用
    entity.Id = Guid.NewGuid().ToString(); // ID は GUID を使用

    // (ここに処理を追記)

    return entity;
}

サムネイル・経緯度情報の取得

PhotoEntityオブジェクトの作成には次の処理が必要です。

  • ファイルに含まれるサムネイルの取得
  • ファイルに含まれる経緯度情報の取得

最近のデジタルカメラで撮影した写真には、カメラの情報や、写真のサムネイル、GPS機能がある場合は撮影場所の経緯度情報などがExifという形式でファイルに含まれています。上記の各情報の取得には、このExifのデータを利用します。

また、今回作成のアプリケーションでは処理を簡単にするため、ExifデータにGPSの経緯度およびサムネイルが含まれていない場合は、リストに追加しません。

Exifデータから情報の取得は、.NET Frameworkでは可能ですが、Silverlightからはライブラリーが用意されていません。そのため、今回はCodeProjectで公開されているExifLibというライブラリーを利用します。ライブラリーを使用しない場合も、Exifデータから必要な情報の読み取りだけであれば、比較的容易に実装できるのではないかと思います。仕様も公開されていますので興味のある方は調べてみるとよいでしょう。

それでは、ExifLibを使用した場合のコードを示します。あらかじめソースコードをダウンロードおよびビルドを行い、ExifLib.dllをプロジェクトの参照に追加しておいてください。用意されているExifReaderクラスのReadJpegメソッドを使用するとExifデータの参照が可能です。メソッドの引数にはFileInfoオブジェクトを渡します。

var info = ExifLib.ExifReader.ReadJpeg(file);
if (info.GpsLatitudeRef == ExifLib.ExifGpsLatitudeRef.Unknown ||
    info.ThumbnailData == null)
{
    // 経緯度情報・サムネイルが含まれていない場合は null を返す
    return null;
}

最初にGPS経緯度情報の取得です。Exifでは経緯度は、東経139度44分28秒のように度・分・秒で分かれて記録されていますので、Bing Maps用に度単位に変換して、Locationオブジェクトを生成します。西経・南緯で記録されている場合は東経・北緯基準へ変換します。

// 経緯度を Bing Maps で使用する形式に変換
var latitude = info.GpsLatitude[0] +
    info.GpsLatitude[1] / 60.0 +
    info.GpsLatitude[2] / 3600.0;
if (info.GpsLatitudeRef == ExifLib.ExifGpsLatitudeRef.South)
{
    latitude = -latitude;
}

var longitude = info.GpsLongitude[0] +
    info.GpsLongitude[1] / 60.0 +
    info.GpsLongitude[2] / 3600.0;
if (info.GpsLongitudeRef == ExifLib.ExifGpsLongitudeRef.West)
{
    longitude = -longitude;
}

var location = new Location(latitude, longitude);

サムネイルの取得は次のようになります。

// 画像の読み込み
var img = new BitmapImage();
using (var ms = new MemoryStream(info.ThumbnailData))
{
    img.SetSource(ms);
}

取得した情報を、PhotoEntityのプロパティに設定して完了です。

entity.BitmapImage = img;

// プッシュピンの作成
var pin = plugin.PushpinFactoryContract.CreateStandardPushpin(location);
entity.Primitive = pin;

// MediaPushpin にする場合
// var image = new Image() { Source = entity.BitmapImage };
// var pin = plugin.PushpinFactoryContract.CreateMediaPushpin(location, (Entity ey) =>
// {
//     return image;
// });

実行と確認

以上でエンティティ作成部分ができ、アプリケーションとして動作するようになったはずです。ここまでの内容を実行してみましょう図2⁠。Map app test toolでDLLを指定する際に、MyPhotoMapApp.dllとExifLib.dllの指定が必要になります。また、実行の確認には、サムネイル・経緯度情報を含んだJPEGファイルが必要です。

図2 実行結果
図2 実行結果

写真を追加すると、パネルのリストボックスおよびレイヤーへ反映されたでしょうか。リストボックスの各アイテムをクリックすると、写真の場所へ移動します。

ポップアップ

次は、プッシュピンをクリックした場合、写真のサムネイルをポップアップ表示できるようにしてみましょう。ポップアップは、第1回でも紹介しています。そのときとは異なり、今回はポップアップ用のコントロールを用意します。

ポップアップの見た目は、ユーザーコントロールを使用して自由にデザインできます。プロジェクトにSilverlightユーザーコントロールを追加して次のようにXAMLを編集します。ここではクラス名をPopupControlとしました。

PopupControl.xaml
<UserControl x:Class="MyPhotoMapApp.PopupControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid x:Name="LayoutRoot" Background="White">
        <Image Source="{Binding BitmapImage}" Margin="10">
            <Image.Effect>
                <DropShadowEffect BlurRadius="5" ShadowDepth="1" Direction="0" Color="Gray" />
            </Image.Effect>
        </Image>
    </Grid>
</UserControl>

ポップアップを登録するメソッドは、MyPhotoPanelクラスの方に次のように追加します。

MyPhotoPanel.xaml.cs
private void RegisterPopup(PhotoEntity entity)
{
    this.plugin.PopupContract.Register(entity, (PopupStateChangeContext context) =>
    {
        context.Title = entity.Name;
        if (context.State == PopupState.Normal)
        {
            context.Content = new PopupControl() { DataContext = entity };
        }
    });
}

このメソッドをCreateEntityメソッド内で呼び出すように編集すればポップアップがアプリケーションで有効になります。

private PhotoEntity CreateEntity(FileInfo file)
{
    // (省略)

    // (追加)
    // ポップアップの登録
    RegisterPopup(entity);

    return entity;
}

設定の保存

最後に新しいContractを紹介します。Map Appの状態や設定を保存できるようにしてみましょう。

分離ストレージ

.NET FrameworkやSilverlightでは、設定などを保存するために分離ストレージという仕組みが用意されています。分離ストレージを使用すると、ユーザーおよびアプリケーション別にデータが分離され、アプリケーションは自アプリケーションのデータにのみアクセスできます。また、Silverlightのようにユーザーのファイルにアクセス権限がない低い権限の場合も分離ストレージを利用できます。

Map Appでもこの分離ストレージが利用できます。ただし、通常のSilverlightアプリケーションとは異なり、Contractによって提供されたものを利用します。プラグインのクラス、MyPhotoPluginに、これまでの連載と同様に次のようにプロパティを宣言して使います。

MyPhotoPlugin.cs
[ImportSingle("Microsoft/IsolatedStorageContract", ImportLoadPolicy.Synchronous)]
public IsolatedStorageContract IsolatedStorageContract { get; set; }

分離ストレージを使用したデータは、クライアント領域に通常は保存されます。そのためユーザーが異なるPCからアプリケーションを実行した場合、当然ながら別PCで保存した内容は参照できません。

My Photo Map Appに追加された写真の保存

今回のアプリケーションでは分離ストレージに、追加した写真の情報を保存して、次回実行時も以前追加した写真が参照できるようにしてみましょう。ただし、削除処理や利用可能なストレージ容量のチェックなどは実装していませんので、Map Appでの分離ストレージの利用サンプルとして使用してください。

保存する内容は、写真ファイルをそのまま保存するのではなくPhotoEntityオブジェクトを後から生成できる次の内容を保存します。

  • ID
  • 名前
  • 経緯度
  • サムネイル画像

サムネイル画像は写真ごとに別のファイルとして、それ以外はひとつのXMLファイルとして保存するようにします。

分離ストレージにファイルを作成するには、IsolatedStorageContract.GetPluginSpecificIsolatedStorageFileメソッドを使用します。このメソッドは何度も使うため、MyPhotoPanelクラス内にプロパティとして次のように参照できるようにしておきましょう。

private PluginIsolatedStorageFile IsolatedStorageFile
{
    get
    {
        return this.plugin.IsolatedStorageContract.GetPluginSpecificIsolatedStorageFile(this.plugin.Token);
    }
}

ちなみにファイルを作成して設定を保存する方法以外にもPluginIsolatedStorageSettingsクラスを使用した方法もあります。次のように、設定をキーで参照できます。

var settings = this.plugin.IsolatedStorageContract.GetPluginSpecificIsolatedStorageSettings(this.plugin.Token);

// 設定の保存
settings["key"] = "value";

// 設定の参照
string value;
if (settings.TryGetValue<string>("key", out value))
{
    // Do something
}

保存処理

それでは、保存部分をMyPhotoPanelクラス内に記述します。作成するXMLファイルは次のような構成にします。写真の数だけ、<photos>要素の中に<photo>要素があります。各<photo>要素に対応するサムネイルの画像は、画像ファイル名にIDの値を使用し、参照するものとします。

<?xml version="1.0" encoding="utf-8"?>
<photos>
        <photo id="1a1618d9-a138-4f61-9161-278dd6d5c65c"
               name="DSC00001.JPG"
               latitude="36.087925"
               longitude="139.7249733" />
        <photo id="a03D363a-7a2B-42fc-8d53-f3e78a59d2b6"
               name="DSC00002.JPG"
               latitude="36.077934"
               longitude="139.719575" />
</photos>

XMLファイルを作成する部分のメソッドは次のように記述します。

private void Save()
{
    var stream = IsolatedStorageFile.CreateFile("photos.xml");
    var writer = System.Xml.XmlWriter.Create(stream);
    writer.WriteStartDocument();
    writer.WriteStartElement("photos");

    foreach (var item in PhotoItems)
    {
        writer.WriteStartElement("photo");

        writer.WriteStartAttribute("id");
        writer.WriteString(item.Id);
        writer.WriteEndAttribute();

        writer.WriteStartAttribute("name");
        writer.WriteString(item.Name);
        writer.WriteEndAttribute();

        // 経緯度の書き込み
        var pin = (PointPrimitive)item.Primitive;
        writer.WriteStartAttribute("latitude");
        writer.WriteString(pin.Location.Latitude.ToString());
        writer.WriteEndAttribute();

        writer.WriteStartAttribute("longitude");
        writer.WriteString(pin.Location.Longitude.ToString());
        writer.WriteEndAttribute();

        writer.WriteEndElement(); // </photo>
    }

    writer.WriteEndElement(); // </photos>
    writer.Close();
    stream.Close();
}

このSaveメソッドを呼び出す場所は、ボタンクリック時(Button_Click)とリストボックスのドロップ処理時(PhotoListBox_Drop)の2か所です。各メソッドの最後に呼出しの記述を追加してください。

後は、サムネイル画像の保存処理部分です。これはCreateEntityメソッド内で行うことにしましょう。サムネイルを取得した時点で次のように保存します。

// 画像の読み込み
var img = new BitmapImage();
using (var ms = new MemoryStream(info.ThumbnailData))
{
    img.SetSource(ms);

    // (以下を追記)
    // ID の名前で画像を保存
    using (var f = IsolatedStorageFile.CreateFile(entity.Id))
    {
        ms.WriteTo(f);
    }
}

保存部分は以上です。分離ストレージに保存したファイルは、ローカルのユーザーデータ保存領域にあります。既定であれば、C:\ユーザー名\Owner\AppData\LocalLow\Microsoft\Silverlight フォルダーのどこかに保存されています。デバッグ時にファイルを参照したい場合は、検索でphotos.xmlを探してみるとよいでしょう。

読み取り処理

実行時に、保存したphotos.xml からPhotoEntiyオブジェクトを生成して、アプリケーションに反映させます。読み取り処理部分を行うメソッドを次のようにMyPhotoPanel内に記述します。アクセス修飾子がpublicであることに注意してください。

public void Load()
{
    if (!this.IsolatedStorageFile.FileExists("photos.xml"))
    {
        return;
    }

    try
    {
        var stream = this.IsolatedStorageFile.OpenFile("photos.xml", FileMode.Open);
        var reader = System.Xml.XmlReader.Create(stream, new System.Xml.XmlReaderSettings());
        
        while (reader.Read())
        {
            if (reader.NodeType == System.Xml.XmlNodeType.Element &&
                reader.LocalName == "photo")
            {
                var entity = new PhotoEntity();

                reader.MoveToAttribute("id");
                entity.Id = reader.Value;

                reader.MoveToAttribute("name");
                entity.Name = reader.Value;

                var location = new Location();
                reader.MoveToAttribute("latitude");
                location.Latitude = Convert.ToDouble(reader.Value);
                reader.MoveToAttribute("longitude");
                location.Longitude = Convert.ToDouble(reader.Value);

                // プッシュピン作成
                entity.Primitive = this.plugin.PushpinFactoryContract.CreateStandardPushpin(location);

                // 画像読み込み
                entity.BitmapImage = new BitmapImage();
                entity.BitmapImage.SetSource(IsolatedStorageFile.OpenFile(entity.Id, FileMode.Open, FileAccess.Read));

                this.plugin.MainLayer.Entities.Add(entity); // レイヤーに追加
                PhotoItems.Add(entity); // コレクションに追加
                RegisterPopup(entity); // ポップアップ登録
            }
        }
    }
    catch (Exception)
    {
        // Do nothing
    }
}

このLoadメソッドを呼び出すタイミングですが、レイヤーに追加処理などを行っているため注意が必要です。今回のクラス構造では、レイヤー生成時にパネルの生成をしています。パネル生成時にレイヤーにエンティティを追加しようとすると例外が発生します。そこで今回は、プラグインのInitializeメソッド内で次のようにLoadメソッドを呼びます。

MyPhotoPlugin.cs
public override void Initialize()
{            
    base.Initialize();
    this.MainLayer = new MyPhotoLayer(this.Token, this);
    ((MyPhotoPanel)this.MainLayer.Panel).Load(); // 追加
}

以上でMy Photo Map Appの完成です。実行して動作を確認してみてください。うまく動いたでしょうか?

おわりに

My Photo Map Appの作成は、いかがでしたでしょうか。ちょうどMap App SDKの更新もあり、開発が用意になっていますのでぜひ作成してMap Appを登録してみてください。今回でMap App開発の紹介は一段落して、次回からは別のAPI/SDKを紹介していく予定です。

おすすめ記事

記事・ニュース一覧