使ってみよう! Bing API/SDK

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

はじめに

前回に引き続いて今回もBing Map App SDKの紹介です。前回はごく簡単なアプリケーションを作り、実行方法を紹介しました。今回は新たにBing Map Appの次の機能を紹介します。

  • パネルの追加
  • レイヤーのシリアライズ

今回作成するMap Appは、図1のように県庁所在地の場所にプッシュピンを表示する単純なものですが、左側のパネルを活用しています。パネル内の地名をクリックするとその場所へ移動します。

図1 県庁所在地 Bing Map App
図1 県庁所在地 Bing Map App

プラグイン クラスの作成

最初にMap Appのプロジェクトおよびプラグイン クラスを作りましょう。前回と同様ですので、前回の内容を参照しながら作成してください。

プロジェクトはC#のSilverlightクラス ライブラリ プロジェクトです。プロジェクト作成後、Bing Map App SDKのライブラリーの参照の追加をしてください。今回はプロジェクト名をCapitalCitiesBingMapAppとしました。

続いて、Pluginクラスを継承したクラスを作ります。またContractのImportも行います。クラス名はCapitalCitiesPlugin、ファイル名はCapitalCitiesPlugin.csとしました。コードは次のようになります。Initializeメソッドの内容は後から編集します。

namespace CapitalCitiesBingMapApp
{
    using System;
    using Microsoft.Maps.Core;
    using Microsoft.Maps.Plugins;
 
    public class CapitalCitiesPlugin : Plugin
    {
        private Layer mainLayer;
 
        [ImportSingle("Microsoft/MapContract", ImportLoadPolicy.Synchronous)]
        public MapContract DefaultMap { get; set; }
 
        [ImportSingle("Microsoft/LayerManagerContract", ImportLoadPolicy.Synchronous)]
        public LayerManagerContract LayerManagerContract { get; set; }
 
        [ImportSingle("Microsoft/PushpinFactoryContract", ImportLoadPolicy.Synchronous)]
        public PushpinFactoryContract PushpinFactoryContract { get; set; }

        public override void Initialize()
        {
            // (後で編集)
        }
 
        public override void Activate(System.Collections.Generic.IDictionary<string, string> activationParameters)
        {
            if (LayerManagerContract.ContainsLayer(this.mainLayer))
            {
                LayerManagerContract.BringToFront(this.mainLayer);
            }
            else
            {
                LayerManagerContract.AddLayer(this.mainLayer);
            }
        }
 
    }
}

エンティティの作成

エンティティとはプッシュピンなどの地図上に表示するアイテムでした。前回のコードでは、Initializeメソッド内で直接、Entityクラスそのものを生成して使用していましたが、今回はEntityクラスを継承して、県庁所在地を表すエンティティ クラスを作成して使用します。といっても県庁所在地の名前を設定するプロパティを追加しただけの簡単なものです。

プロジェクトに新しいクラスを追加して、次のようにコードを記述します。ファイル名はCityEntity.cs、クラス名はCityEntityとします。

namespace CapitalCitiesBingMapApp
{
    using Microsoft.Maps.Core;
 
    public class CityEntity : Entity
    {
        public string Name { get; set; }
    }
}

レイヤーの作成

続いてレイヤーを作成しましょう。レイヤーには県庁所在地を示すエンティティを追加します。また、レイヤーにはパネルも追加します。まずは、エンティティを追加する処理を書きましょう。

前回のコードではエンティティの追加を、プラグイン クラスのInitializeメソッドで行っていましたが、今回はレイヤーのコンストラクタ内で行います。

XMLファイルの追加

県庁所在地の名前と経緯度情報は、情報を記載したXMLファイルを用意して、ここから情報を取得するようにしましょう。プロジェクトにリソースとして追加して、レイヤーのコンストラクタ処理のときに読み込みエンティティを作成することにします。

プロジェクトに新しい項目としてXMLファイルを追加します図2⁠。ファイル名はCities.xmlとします。追加後、ソリューションエクスプローラからファイルを選択して、ビルドアクション プロパティを「埋め込まれたリソース」に設定します図3⁠。

図2 XMLファイルの追加
図2 XMLファイルの追加
図3 埋め込まれたリソース
図3 埋め込まれたリソース

XMLファイルの内容は次のように、47都道府県分の<Pushpin>要素を記述して、属性として地名と経緯度の値を設定します。

<?xml version="1.0" encoding="utf-8" ?>
<Pushpins>
  <Pushpin Name="札幌市" Latitude="43.0646147" Longitude="141.3468074" />
  <Pushpin Name="青森市" Latitude="40.8243077" Longitude="140.7399984" />
  <Pushpin Name="盛岡市" Latitude="39.7036194" Longitude="141.1526839" />
(省略)
  <Pushpin Name="鹿児島市" Latitude="31.5610825" Longitude="130.5577279" />
  <Pushpin Name="那覇市" Latitude="26.2124013" Longitude="127.6809317" />
</Pushpins>

以上でXMLファイルの準備は完了です。内容からわかるように別に県庁所在地である必要はなく、好きな地名と経緯度を記述して構いません。

レイヤークラスの作成

次にLayerクラスを継承したレイヤー クラスを作成します。プロジェクトに新しいクラスを追加します。クラス名はCityLayer、ファイル名はCityLayer.csにします。

コンストラクタ内でXMLファイルを読み込み、その値からCityEntityオブジェクトを生成し、レイヤーに追加していく処理は次のようになります。プッシュピンの生成もここで行っています。

namespace CapitalCitiesBingMapApp
{
    using Microsoft.Maps.Core;
    using Microsoft.Maps.Plugins;
    using Microsoft.Maps.MapControl;
    using System.Collections.Generic;
    using System;
    using System.Windows;
    using System.IO;
    using System.Xml;
 
    public class CityLayer : Layer
    {
        private CapitalCitiesPlugin plugin;
 
        public CityLayer(PluginToken pluginToken, CapitalCitiesPlugin plugin) : base(pluginToken)
        {
            this.plugin = plugin;
            
            // リソースから XML ファイル読み込み
            var stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("CapitalCitiesBingMapApp.Cities.xml");
            using (var reader = XmlReader.Create(stream, new XmlReaderSettings()))
            {
                while (reader.Read())
                {
                    if (reader.NodeType == XmlNodeType.Element &&
                        reader.LocalName == "Pushpin")
                    {
                        // Entity の作成
                        var entity = new CityEntity();
 
                        // 県庁所在地名
                        reader.MoveToAttribute("Name");
                        entity.Name = reader.Value;
 
                        // 経緯度
                        var location = new Location();
                        reader.MoveToAttribute("Latitude");
                        location.Latitude = double.Parse(reader.Value);
                        reader.MoveToAttribute("Longitude");
                        location.Longitude = double.Parse(reader.Value);
 
                        // プッシュピンの追加
                        entity.Primitive = plugin.PushpinFactoryContract.CreateStandardPushpin(location, entity.Name[0].ToString());
 
                        // レイヤーに追加
                        this.Entities.Add(entity);
                    }
                }
            }
        }
    }
}

このレイヤーはプラグイン クラスから生成されることを想定しています。また、内部でプッシュピンの作成などを行うため、プラグインの参照をコンストラクタ引数で受け取り、プラグインの持つContractを使用できるようにしています。

このレイヤーを生成するコードをプラグインのInitializeメソッドに記述しておきましょう。

public override void Initialize()
{
    this.mainLayer = new CityLayer(this.Token, this);
}

パネルの作成

それでは、パネルを作成してみましょう。パネルは、ユーザー コントロールとして作成します。プロジェクトにSilverlightユーザー コントロールを追加します図4⁠。ファイル名は、CityPanel.xamlとしました。

図4 ユーザー コントロールの追加
図4 ユーザー コントロールの追加

追加したUserControlのXAMLを編集して、<Grid>要素内に次のコードを追加します。

<ListBox Margin="5" ItemsSource="{Binding}" SelectionChanged="ListBox_SelectionChanged">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock 
                Text="{Binding Name}" 
                HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                FontWeight="Bold" FontSize="14" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

XAML内の「SelectionChanged="ListBox_SelectionChanged"」部分は、リストボックスの選択が変更されたイベントの時に呼ぶメソッド名を指定しています。すべて自分で記述せず、Visual Studioの機能を利用してイベント処理を追加します。 プロパティウィンドウからイベントタブを選択して「SelectionChanged」の項目に「ListBox_SelectionChanged」を設定する図5と、CityPanel.xaml.csにイベント処理のコードが自動で生成されます。

図5 イベントの登録
図5 イベントの登録

CityPanel.xaml.csのコードは次のように編集します。ItemMouseClickedというイベントを定義して、パネルで地名を選択したときにイベントが発生するようにしています。

namespace CapitalCitiesBingMapApp
{
    using System;
    using System.Windows;
    using System.Windows.Controls;
 
    public partial class CityPanel : UserControl
    {
        public CityPanel()
        {
            InitializeComponent();
        }
 
        public event EventHandler<RoutedEventArgs> ItemMouseClicked;
 
        private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            CityEntity city = (CityEntity)((ListBox)sender).SelectedItem;
            if (this.ItemMouseClicked != null)
            {
                this.ItemMouseClicked(city, e);
            }
        }
    }
}

以上で、パネル自体の記述は完成です。

レイヤーはひとつのパネルを持つことが可能です。ここで作成したパネルは、作成済みのCityLayerクラスで生成し、レイヤーのパネルとして設定します。CityLayerのコンストラクタの処理の最後に次のコードを追記します。

var panel = new CityPanel() { DataContext = this.Entities };
panel.ItemMouseClicked += new EventHandler<RoutedEventArgs>(this.Panel_ItemMouseClicked);
this.Panel = panel;
this.Title = "県庁所在地";

CityPanelのDataContextプロパティにレイヤーのエンティティのコレクションを指定し、パネル上に県庁所在地の情報を表示できるようにしています。また、ここではレイヤーのTitleプロパティも合わせて設定しています。Titleプロパティに設定した値は図1のようにパネル上部に表示されます。

上記コードのパネルの項目をクリックしたときの処理も記述しましょう。クラス内に次のメソッドを追記します。

private void Panel_ItemMouseClicked(object sender, RoutedEventArgs e)
{
    var entity = sender as CityEntity;
    if (entity != null)
    {
        this.plugin.DefaultMap.SetView(entity, this.plugin.DefaultMap.ZoomLevel);
    }
}

項目をクリックすると、その県庁所在地の場所まで地図を移動させます。プラグインのMapContract型のDefaultMapプロパティを使ってSetViewメソッドにより指定した場所へ移動させています。

以上で、県庁所在地にプッシュピンを表示し、パネルから移動が可能なMap Appができました。ビルドして実行してみましょう。実行方法は前回の内容を参照してください。うまく動いたでしょうか?

レイヤーのシリアライズ

ここでレイヤーのシリアライズについて紹介しておきます。今回作成したMap Appを実行して、ある県庁所在地に移動していたとします。その状態から、検索ボックスにどこか別の場所を入力して移動します。このときWebブラウザーの戻るボタンをクリックしてみると、直前に表示していた地図の状態には戻りません。ユーザーが期待した動作(直前に表示していた状態に戻る)とは異なってしまいます。期待した動作にするために、レイヤーのシリアライズを行います。

ここでのシリアライズとは、ある状態を保持しているオブジェクトを、文字列などの情報に変換することです。変換するとファイルなどに保存が可能になります。そして必要なときに、変換・保存した情報からある状態のオブジェクトを再度生成します。この元に戻すことをデシリアライズと呼びます。

つまり、ここではレイヤーの持っている情報をシリアライズし、またWebブラウザーの戻るボタンなどにより再度表示を行いたいタイミングでデシリアライズを行い、レイヤーを復元します。

レイヤーのシリアライズは、SerializationContractというContractを使用します。これまで紹介したContractはImportして提供される機能をプラグインから利用していましたが、それらとは異なり、プラグインの提供する機能としてExportします。CapitalCitiesPluginクラスに次のプロパティを追加します。

[ExportSingle("Microsoft/LayerSerializationContract")]
public SerializationContract<Layer> LayerSerializationContract { get; private set; }

プラグインは機能を提供側ですので、その機能を実装します。SerializationContract<Layer>クラスを継承したクラスを作成し、シリアライズとデシリアライズ機能を記述します。プロジェクトに、LayerSerializationContractという名前のクラスを追加して次のように記述します。

namespace CapitalCitiesBingMapApp
{
    using System;
    using System.Collections.Generic;
    using System.Windows;
    using Microsoft.Maps.Core;
    using Microsoft.Maps.MapControl;
    using Microsoft.Maps.Plugins;

    public class LayerSerializationContract : SerializationContract<Layer>
    {
        private Layer layer;

        public LayerSerializationContract(Layer layer)
        {
            this.layer = layer;
        }

        public override Dictionary<string, string> Serialize(Layer layer)
        {
            // レイヤー情報を string で表し キーと値のコレクションを返す
            var dictionary = new Dictionary<string, string>();
            return dictionary;
        }

        public override void Deserialize(Dictionary<string, string> serializedResult, Action<Layer> callbackAction)
        {
            // キーと値のコレクションからレイヤー情報を取得し
            // this.layer に対して適切な値などを設定した後
            // コールバックメソッドに渡す
            if (callbackAction != null)
            {
                callbackAction(this.layer);
            }
        }
    }
}

コードの内容はいたって簡単です。コンストラクタでは、シリアライズ・デシリアライズを行うレイヤーオブジェクトを引数として受け取ります。Serializeメソッド、Deserializeメソッドではそれぞれ、受け取ったレイヤーに対してシリアライズ・デシリアライズを行います。戻り値や引数からもわかるように、レイヤー情報は文字列のキーと値の組み合わせのコレクションに変換します。

上記コードでは、シリアライズ・デシリアライズ処理を特に何もしていません。実は、今回のアプリケーション程度の、プッシュピンの位置や地図の表示位置はライブラリーで対処してくれるようです。独自にレイヤーに対して情報を設定している場合は、シリアライズ・デシリアライズ処理が必要になってきます。

最後に、プラグインのLayerSerializationContractプロパティにLayerSerializationContractクラスのオブジェクトを設定するコードを書きましょう。Initializeメソッドの内容を次のように編集します。

public override void Initialize()
{
    this.mainLayer = new CityLayer(this.Token, this);
    this.LayerSerializationContract = new LayerSerializationContract(this.mainLayer);
}

以上でBing MapがこのMap Appのシリアライズ・デシリアライズ機能が使用できるようになりました。次の手順で動作を確認してみましょう。

  1. Map Appを実行する
  2. 検索ボックスに地名を入力し、その場所へ移動する
  3. Webブラウザーの戻るボタンをクリックする

シリアライズ・デシリアライズ処理した場合と、していない場合と比較してみてください。きちんとシリアライズ・デシリアライズ処理は動作したでしょうか?

ちなみに、Map App実行時に最初はInitializeメソッド、Activateメソッドの順で呼ばれますが、デシリアライズ処理がされる場合、Activateメソッドは呼ばれません。


いかがでしたでしょうか。今回も簡単なアプリケーションを作成してみました。まだまだMap Appとして作りこむ要素が残っています。次回もMap Appの作成について紹介します。

おすすめ記事

記事・ニュース一覧