次の方法で共有


位置認識プログラミング

Windows Phone 7 で Bing Maps API を使用してルートを表示する

Sandrino Di Di

コード サンプルのダウンロード

Microsoft Windows Phone 7 には、ユーザーの現在位置と移動が緯度と経度 (ときには高度) で表現され、ユーザーの場所を判断できる使いやすい地理位置情報 API があります。このデータにアクセスできれば、Windows Phone 7 アプリケーションに位置認識機能を組み込むことができます。

ハンバーガー ショップを検索するアプリケーションを作成しているのであれば、ユーザーの現在位置を基に一番近いショップを見つけ、メニューと宣伝を一緒に表示できれば最高です。また、近くにいる人を見つける機能も便利です。この機能は、打ち合わせの合間に、顧客を訪問できるかどうか確認したいと考えているセールスマンに最適です。

この記事では、このデータを Windows Phone 7 アプリケーションで使って、さまざまなやり方でルートや場所を表示する方法に重点を置いて説明します。実際のデータは、Bing Maps API から入手します。そこで、高度な概念に触れる前に、Bing Maps API の基礎について見ておくことが重要です。

Bing Maps API の概要

まず、作業する Bing のアカウントを作成します。Bing Maps Account Center (bingmapsportal.com、英語) で、Windows Live ID を使用して Bing Maps アカウントにサインアップします。アカウントを作成したら、アカウントの詳細のページからキーを作成できます。Windows Phone 7 用のアプリケーションを作成しているため、[Application URL] (アプリケーションの URL) には「http://localhost」などを記述できます。

このページでは、アカウントを作成するだけでなく、Bing Maps API の使用量を監視することもできます。運用アプリケーションで Bing Maps を使用する場合は、このページに戻って、ライセンスについての問い合わせが必要です。

実際には Bing Maps API はいくつかのサービスを提供します。今回の Windows Phone 7 アプリケーションでは SOAP サービスを利用します。ここでは、次のサービスについて簡単に概要を説明します。

  • Geocode: dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc (英語)
  • Imagery: dev.virtualearth.net/webservices/v1/imageryservice/imageryservice.svc (英語)
  • Route: dev.virtualearth.net/webservices/v1/routeservice/routeservice.svc (英語)
  • Search: dev.virtualearth.net/webservices/v1/searchservice/searchservice.svc (英語)

Geocode サービスは、座標や住所を操作できるようにします。Imagery サービスは、実際の画像 (航空写真、俯瞰図、道路地図) を操作できるようにします。Route サービスは、2 つ以上の地点間のルートを計算できるようにします。Search サービスは、ユーザー入力 (「ブリュッセルのハンバーガー ショップ」など) に基づいて場所を探すことができるようにします。

これらのサービスを利用するには、先ほどの URL の 1 つに "サービス参照" を追加するだけです。この操作によって、ServiceReferences.ClientConfig ファイルが作成または更新されます。図 1 に、Geocode サービスの Geocode メソッドを呼び出す方法の例を示します。

図 1 Geocode サービスの Geocode メソッドの呼び出し

// Create the request. 
var geoRequest = new GeocodeRequest(); 
geoRequest.Credentials = new Credentials(); 
geoRequest.Credentials.ApplicationId = "<my API key>"; 
geoRequest.Address = new Address(); 
geoRequest.Address.CountryRegion = "Belgium"; 
geoRequest.Address.PostalTown = "Brussels"; 
             
// Execute the request and display the results. 
var geoClient = new GeocodeServiceClient("BasicHttpBinding_IGeocodeService"); 
geoClient.GeocodeAsync(geoRequest); 
geoClient.GeocodeCompleted += (s, e) => 
{
  if (e.Result != null && e.Result.Results.Any(o => 
    o.Locations != null && o.Locations.Any()))
    Location = e.Result.Results.FirstOrDefault().Locations.FirstOrDefault();
  else if (e.Error != null)
    Error = e.Error.Message;
  else
    Error = "No results or locations found."; 
};

各サービス メソッドは、要求と応答のメカニズムに基づいています。図 1 では、サーバーへの質問を準備し、API のキーを構成する、要求オブジェクトを作成しています。今回の場合は、GeocodeRequest を作成して、サーバーに「ベルギー (Belgium)」の「ブリュッセル (Brussels)」の GeocodeLocation を提供するよう要求しています。要求を作成したら、非同期方式でクライアントを呼び出します。最終的には、情報を提供する応答を受け取ります。問題があれば、エラーが発生します。付属のダウンロードのサンプル アプリケーションを実行し、GeocodeServiceClient が実行されているところを見て、場所 (またはエラー) がデータ バインドを使用してどのように画面に表示されるかを確認してください。

今回のアプリケーションは、2 つの住所間のルートを計算してユーザーに表示するために、Geocode および Route サービスを使用します。

ルートを計算する

Bing Route サービスを使用すると、A 地点から B 地点までのルートを計算できます。前の例と同じく、このサービスは要求と応答を使って操作します。まず、(GeocodeRequest を使用して) 各住所の実際の地理位置情報を見つけます。これらの地理位置情報を使用して、RouteRequest を作成します。付属のダウンロードのサンプル アプリケーションにはすべてのコードが含まれていますが、図 2 にコードの例を簡単に示します。

図 2 RouteRequest の作成

// Create the request. 
var routeRequest = new RouteRequest(); 
routeRequest.Credentials = new Credentials(); 
routeRequest.Credentials.ApplicationId = "<my API key>"; 
routeRequest.Waypoints = new ObservableCollection<Waypoint>(); 
routeRequest.Waypoints.Add(fromWaypoint); 
routeRequest.Waypoints.Add(toWaypoint); 
routeRequest.Options = new RouteOptions(); 
routeRequest.Options.RoutePathType = RoutePathType.Points; 
routeRequest.UserProfile = new UserProfile(); 
routeRequest.UserProfile.DistanceUnit = DistanceUnit.Kilometer; 
                 
// Execute the request. 
var routeClient = new RouteServiceClient("BasicHttpBinding_IRouteService"); 
routeClient.CalculateRouteCompleted += 
  new EventHandler<CalculateRouteCompletedEventArgs>(OnRouteComplete); 
routeClient.CalculateRouteAsync(routeRequest);

要求の Waypoints プロパティは、複数の中間地点を追加できるようにするコレクションです。これは、単に A 地点から B 地点ではなく、完全な道順を知るときに便利です。

CalculateRouteAsync メソッドを実行すると、サービスは、ルートの計算、すべての道順項目 (方向を変えたり、道順から外れたりするなどの動作) の列挙、時間と距離の計算、全地点 (地理位置情報) の列挙といった、多くの作業を開始します。図 3 に、RouteResponse に含まれる重要なデータの概要をいくつか示します。

RouteResponse Content

図 3 RouteResponse のコンテンツ

地図上にルートを表示する

1 つ目の例では、地図上にルートを表示するために RoutePath.Points を使用しています。Windows Phone 7 Toolkit には Bing Maps コントロールが既に含まれているため、Microsoft.Phone.Controls.Maps アセンブリへの参照を追加するだけでかまいません。追加すると、Windows Phone に地図を簡単に表示できます。次に、ブリュッセルの地図を表示する方法の一例を示します (API のキーを設定するために CredentialsProvider が必要です)。

<maps:Map Center="50.851041,4.361572" ZoomLevel="10" 
  CredentialsProvider="{StaticResource MapCredentials}" />

Maps アセンブリのコントロールを使う場合は、Bing サービスへのサービス参照を追加する前に、Maps アセンブリへの参照を追加することをお勧めします。このようにすると、サービス参照は新しい型を作成するのではなく、Microsoft.Phone.Controls.Maps.Platform.Location などの型を再利用するため、サービスから返されるデータを使用するために変換メソッドや値コンバーターを作成する必要がありません。

これで、2 つの地点間のルートを計算する方法と、Windows Phone に地図を表示する方法がわかりました。この 2 つの手法を組み合わせて、ルートを地図上に表示しましょう。RoutePath における Points は、地図上に描画を行うために使用します。Maps コントロールによって、(たとえば、ルートの開始と終了を表すために) Pushpin や、(GeoCoordinates に基づいてルートを描画するために) MapPolyline などの図形を追加することができます。

サービスから返される地点は、Maps コントロールが使用される地点と同じ型ではないため、適切な型に地点を変換する小さな拡張メソッドを 2 つ作成しました (図 4 参照)。

図 4 地点を適切な型に変換する拡張メソッド

public static GeoCoordinate ToCoordinate(this Location routeLocation) 
{ 
  return new GeoCoordinate(routeLocation.Latitude, routeLocation.Longitude); 
} 
  
public static LocationCollection ToCoordinates(this IEnumerable<Location> points)
{ 
  var locations = new LocationCollection(); 
  
  if (points != null) 
  { 
    foreach (var point in points) 
    { 
      locations.Add(point.ToCoordinate()); 
    } 
  } 
  
  return locations; 
}

CalculateRoute メソッドが完成したら、RouteResponse でこれらの拡張メソッドを使用することができます。変換が終わると、これらの拡張メソッドは、Bing Maps コントロールにバインドするために使用できる型など、複数の型を返します。これは Silverlight アプリケーションなので、実際の変換を行うには IValueConverter を使用します。図 5 に、Locations を GeoCoordinates に変換する値コンバーターの例を示します。

図 5 IValueConverter の使用

public class LocationConverter : IValueConverter 
{ 
  public object Convert(object value, Type targetType,  
    object parameter, CultureInfo culture) 
  { 
    if (value is Location) 
    { 
      return (value as Location).ToCoordinate(); 
    } 
    else if (value is IEnumerable<Location>) 
    { 
      return (value as IEnumerable<Location>).ToCoordinates(); 
    } 
    else 
    { 
      return null; 
    } 
  }
}

では、データ バインドを構成してみましょう。データ バインドはコンバーターを使用するので、最初にコンバーターを宣言しておくことが重要です。コンバーターは、次のように、ページのリソースか、アプリケーションのリソース (コンバーターを再利用する場合) で宣言できます。

<phone:PhoneApplicationPage.Resources>
  <converters:LocationConverter x:Key="locationConverter" />
  <converters:ItineraryItemDisplayConverter x:Key="itineraryConverter" />
</phone:PhoneApplicationPage.Resources>

コンバーターを宣言したら、Maps コントロールや (MapPolyline や Pushpin などの) 他のオーバーレイ コントロールを追加して、それらのコントロールを必要なプロパティにバインドすることができます。

<maps:Map Center="50.851041,4.361572" ZoomLevel="10" 
  CredentialsProvider="{StaticResource MapCredentials}">
  <maps:MapPolyline Locations="{Binding RoutePoints, 
    Converter={StaticResource locationConverter}}" 
    Stroke="#FF0000FF" StrokeThickness="5" />
  <maps:Pushpin Location="{Binding StartPoint, 
    Converter={StaticResource locationConverter}}" Content="Start" />
  <maps:Pushpin Location="{Binding EndPoint, 
    Converter={StaticResource locationConverter}}" Content="End" />
</maps:Map>

ご覧のとおり、これらのバインドは、Maps コントロールが理解する形式にデータを変換するため、前に宣言したコンバーターを使用しています。最後に、CalculateMethod が完成したら、これらのプロパティを設定する必要があります。

private void OnRouteComplete(object sender, CalculateRouteCompletedEventArgs e)
{
  if (e.Result != null && e.Result.Result != null 
    && e.Result.Result.Legs != null & e.Result.Result.Legs.Any())
  {
    var result = e.Result.Result;
    var legs = result.Legs.FirstOrDefault();
 
    StartPoint = legs.ActualStart;
    EndPoint = legs.ActualEnd;
    RoutePoints = result.RoutePath.Points;
    Itinerary = legs.Itinerary;
  }
}

図 6 は、アプリケーションを起動してルートを計算した後の画面です。

Visual Representation of the Route

図 6 ルート表示

道順を表示する

おわかりのように、地図上にルートを表示するのはごく基本的な操作です。次の例では、各 ItineraryItem のテキストと概要を使用して、最初から最後までの道順を表示するカスタム コントロールを作成する方法について説明します。図 7 は最終結果を示しています。

Displaying Start-to-End Directions

図 7 最初から最後までの道順の表示

上記の例で、Legs が RouteResult プロパティの 1 つであることがわかります。Legs プロパティには 1 つ以上の "Leg" オブジェクトが含まれ、各オブジェクトに ItineraryItem のコレクションが含まれています。ItineraryItem を使用すると、図 7 のようにコントロールを設定できます。図 7 の各行は、各道順の合計秒数と合計距離を含む ItineraryItem と、現在の道順のインデックスを表示します。ItineraryItem は現在の道順の数を数えないため、ItineraryItemDisplay という小さなクラスを作成しました。

public class ItineraryItemDisplay  
{
  public int Index { get; set; }
  public long TotalSeconds { get; set; }
  public string Text { get; set; }
  public double Distance { get; set; }
}

付属のダウンロードのサンプル コードには、次のシグネチャを持つ拡張メソッドも含めています。

public static ObservableCollection
  <ItineraryItemDisplay> ToDisplay(this 
  ObservableCollection<ItineraryItem> items)

このメソッドのコードは、すべての項目をループ処理し、新しい ItineraryItemDisplay オブジェクトに重要な値を書き込み、そして、Index プロパティ内の現在までの道順の数を数えます。最後に、ItineraryItemDisplayConverter は、データ バインド中に変換を処理します。図 7 でお気付きになったかもしれませんが、各道順は、ItineraryItemBlock というカスタム コントロールによって、わかりやすく書式設定しています (都市名と通りを太字にしています)。このコントロールの目的は、ItineraryItem のテキストをわかりやすく書式設定することだけです。図 7 には、追加情報を含む青いブロックもありますが、これは通常のデータ バインドです。

[TemplatePart(Name = "ItemTextBlock", Type = 
  typeof(TextBlock))] public class
  ItineraryItemBlock : Control

TemplatePart 属性は、ControlTemplate になくてはならない要素と、その要素をどの型にすべきかを定義します。今回の場合は、ItemTextBlock という TextBlock にします。

<Style TargetType="controls:ItineraryItemBlock" x:Key="ItineraryItemBlock">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType=
        "controls:ItineraryItemBlock">
        <TextBlock x:Name="ItemTextBlock" 
          TextWrapping="Wrap" 
          LineStackingStrategy=
          "BlockLineHeight" LineHeight="43" />
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

TextBlock にする理由は明白です。TextBlock.Inlines プロパティを使用すると、コードの TextBlock にコンテンツを追加できます。カスタム コントロールの OnApplyMethod はオーバーライドできます。これは、ItemTextBlock を取得するときに実行します (図 8 参照)。

図 8 TextBlock の検索

/// <summary> 
/// When the template is applied, find the textblock. 
/// </summary> 
public override void OnApplyTemplate() 
{ 
  base.OnApplyTemplate(); 
  
  // Get textblock. 
  textBlock = GetTemplateChild("ItemTextBlock") as TextBlock; 
  if (textBlock == null) 
    throw new InvalidOperationException
      ("Unable to find 'ItemTextBlock' TextBlock in the template."); 
  
  // Set the text if it was assigned before loading the template. 
  if (!String.IsNullOrEmpty(Text)) 
    SetItinerary(Text); 
}

ItineraryItem.Text プロパティは細かく調べ、別のなんらかの書式設定でこの TextBlock に情報を設定するのに使用します。実際、ItineraryItem.Text プロパティには、通常のテキストにすぎないものしか含まれないため、これは簡単です。いくつかの部分は XML タグで囲みます。

<VirtualEarth:Action>Turn</VirtualEarth:Action> 
  <VirtualEarth:TurnDir>left</VirtualEarth:TurnDir>onto  
  <VirtualEarth:RoadName>Guido Gezellestraat
  </VirtualEarth:RoadName>

都市と通りの名前のみを強調したいので、VirtualEarth:Action や VirtualEarth:TurnDir などのタグを削除する小さなメソッドを作成しました。TextBlock を取得したら、SetItinerary メソッドを呼び出します。ここで、Inlines を TextBlock に追加します (図 9 参照)。

図 9 SetItinerary メソッドでの TextBlock への Inlines の追加

// Read the input 
string dummyXml = String.Format(
  "<Itinerary xmlns:VirtualEarth=\"http://dummy\">{0}</Itinerary>", 
  itinerary); 
using (var stringReader = new StringReader(dummyXml)) 
{ 
  // Trace the previous element. 
  string previousElement = ""; 

  // Parse the dummy xml. 
  using (var xmlReader = XmlReader.Create(stringReader)) 
  { 
    // Read each element. 
    while (xmlReader.Read()) 
    { 
      // Add to textblock. 
      if (!String.IsNullOrEmpty(xmlReader.Value)) 
      { 
        if (previousElement.StartsWith("VirtualEarth:")) 
        { 
          textBlock.Inlines.Add(new Run() 
            { Text = xmlReader.Value, FontWeight = FontWeights.Bold }); 
        } 
        else 
        { 
          textBlock.Inlines.Add(new Run() { Text = xmlReader.Value }); 
        } 
      } 

      // Store the previous element. 
      if (xmlReader.NodeType == XmlNodeType.Element) 
        previousElement = xmlReader.Name; 
      else 
      previousElement = ""; 
    } 
  } 
}

前の XML テキストの例を見るとわかるように、テキストのすべての部分を XML 要素に含めているわけではありません。このテキストを XmlReader で使用できるようにするには、まず、ダミーの XML 要素でラップします。これにより、このテキストを含む新しい XmlReader を作成することができます。XmlReader.Read メソッドを使用すると、XML 文字列の各部分をループ処理できます。

NodeType から、XML 文字列における現在位置を把握します。たとえば、<VirtualEarth:RoadName>Guido Gezellestraat</VirtualEarth:RoadName> という要素について考えてみましょう。XmlReader.Read メソッドを使用すると、3 つの反復処理が行われます。1 つ目は <VirtualEarth:RoadName> で、これは XmlNodeType.Element です。目的は、通りと都市を太字で書式設定することなので、ここでは、FontWeight を bold に設定した TextBox.Inlines に Run オブジェクトを追加します。Inlines に Run オブジェクトを追加すると、単に TextBlock にいくつかのテキストが追加されます。

書式設定を必要としないケースでは、書式設定のプロパティを設定していない、テキストのみを含む通常の Run オブジェクトを追加します。

カスタム コントロールについては以上です。ItineraryItemDisplay のレコード全体は、ListBox 用のカスタム DataTemplate を使用して表示します。この DataTemplate には、カスタム コントロールへの参照も含めています (図 10 参照)。

図 10 Listbox 用のカスタム DataTemplate における ItineraryItemDisplay レコード全体

<!-- Template for a full item (includes duration and time) --> 
<DataTemplate x:Key="ItineraryItemComplete"> 
  <Grid Height="173" Margin="12,0,12,12"> 
    <!-- Left part: Index, Distance, Duration. --> 
    <Grid HorizontalAlignment="Left" Width="75"> 
      <Grid.ColumnDefinitions> 
        <ColumnDefinition Width="25*" /> 
        <ColumnDefinition Width="25*" /> 
        <ColumnDefinition Width="25*" /> 
        <ColumnDefinition Width="25*" /> 
      </Grid.ColumnDefinitions> 
      <Grid.RowDefinitions> 
        <RowDefinition Height="50*"></RowDefinition> 
        <RowDefinition Height="20*"></RowDefinition> 
        <RowDefinition Height="20*"></RowDefinition> 
      </Grid.RowDefinitions> 

      <!-- Gray rectangle. --> 
      <Rectangle Grid.ColumnSpan="4" Grid.RowSpan="3" Fill="#FF0189B4" /> 

      <!-- Metadata fields. --> 
      <TextBlock Text="{Binding Index}" 
        Style="{StaticResource ItineraryItemMetadata}"    
        Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" /> 
      <TextBlock Text="{Binding Distance, 
        Converter={StaticResource kilometers}}" 
        Style="{StaticResource ItineraryItemMetadata}" 
        FontSize="{StaticResource PhoneFontSizeSmall}"                 
        Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="4" /> 
      <TextBlock Text="{Binding TotalSeconds, 
        Converter={StaticResource seconds}}" 
        Style="{StaticResource ItineraryItemMetadata}" 
        FontSize="{StaticResource PhoneFontSizeSmall}" 
        Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="4" /> 
    </Grid> 

    <!-- Right part to show directions. --> 
    <StackPanel Margin="84,-4,0,0" VerticalAlignment="Top" > 
      <controls:ItineraryItemBlock Text="{Binding Text}" 
        Style="{StaticResource ItineraryItemBlock}" 
        FontSize="{StaticResource PhoneFontSizeLarge}" 
        Foreground="{StaticResource PhoneForegroundBrush}"   
        Padding="0,3,0,0" Margin="0,0,0,5" /> 
    </StackPanel> 
  </Grid> 
</DataTemplate>

カスタム コントロールとスタイル設定の準備が整ったので、後は、これをピボット コントロールとコード内に実装するだけです。先ほど説明したように、カスタム コントロールと DataTemplate は ListBox で使用します。

<controls:PivotItem Header="Directions">
  <ListBox ItemsSource=
    "{Binding Itinerary, Converter={StaticResource itineraryConverter}}" 
    Grid.RowSpan="2" ItemTemplate="{StaticResource ItineraryItemComplete}" />
</controls:PivotItem>

この ListBox ItemSource は、Itinerary プロパティにバインドします。これが、プロパティの設定方法です。その後、ItineraryItemDisplayConverter が残りの作業を行います。ご覧のとおり、カスタム コントロールと少しのスタイル設定を使用すると、Route サービスから道順データを取得して、そのデータをユーザーにとって魅力的に表示することができます。

private void OnRouteComplete(object sender, CalculateRouteCompletedEventArgs e)
{
  if (e.Result != null && e.Result.Result != null && 
    e.Result.Result.Legs != null & e.Result.Result.Legs.Any())
  {
    ...
    Itinerary = e.Result.Result.Legs.FirstOrDefault().Itinerary;
  }
}

現在位置を検索する

前の例では、A 地点から B 地点への道順を取得するために Geocode と Route サービスを使用する方法と、それらの道順を表示する方法について学びました。ここからは、地理位置情報 API について見ていきましょう。

GeoCoordinateWatcher は、現在の GPS 座標を見つけるために使用するクラスです。

coordinateWatcher = new GeoCoordinateWatcher
  (GeoPositionAccuracy.High); coordinateWatcher.StatusChanged += 
  new EventHandler<GeoPositionStatusChangedEventArgs>(OnCoordinateUpdate); 
coordinateWatcher.Start();

GeoCoordinateWatcher は、Start メソッドを実行した後、さまざまな段階を経ますが、状態が Ready に設定されると、現在位置にアクセスできるようになります。ベスト プラクティスは、GeoCoordinateWatcher を操作し終わった後、Stop メソッドを呼び出すことです。

private void OnCoordinateStatusChanged(object sender,  
  GeoPositionStatusChangedEventArgs e) 
{ 
  if (e.Status == GeoPositionStatus.Ready) 
  {
    coordinateWatcher.Stop();
 
    // Get position.
    fromLocation = coordinateWatcher.Position.Location;
    LocationLoaded(); 
  } 
}

これで、サンプル アプリケーションも位置認識機能を提供するようになります。GeoCoordinateWatcher は、位置が変わったときを監視することができる PositionChanged イベントも公開します。道順を表示するアプリケーションを作成している場合に位置の変化を使用すると、各道順を自動的にスクロールしたり、ItineraryItem.Text の VirtualEarth:Action に基づいて音を鳴らしたりすることができます。最終的には、実際の GPS ナビゲーションのようなアプリケーションを作成することが可能になります。

Windows Phone 7 エミュレーターを使用してアプリケーションのデバッグを実行する場合は考慮することがあります。アプリケーションの地理位置情報機能をテストする場合、GeoCoordinateWatcher.Status が、常に NoData のままで、Ready には決して変わらないという小さな問題に遭遇します。コードを実装 (GeoCoordinateWatcher) ではなくインターフェイス (IGeoPositionWatcher<GeoCoordinate>) に対して記述することが重要なのは、これが理由です。Tim Heuer のブログ (bit.ly/cW4fM1、英語) から、実際の GPS デバイスのシミュレーションを行う EventListGeoLocationMock クラスをダウンロードできます。

EventListGeoLocationMock クラスは、ユーザーの座標を適切なタイミングでシミュレーションする GeoCoordinateEventMocks のコレクションを受け取ります。これにより、ユーザーの位置と動きをテストできます。使用法を次に示します。

GeoCoordinateEventMock[] events = new GeoCoordinateEventMock[]  
{  
  new  GeoCoordinateEventMock { Latitude = 50, Longitude = 6, 
    Time = new TimeSpan(0,0,5) }, 
  new  GeoCoordinateEventMock { Latitude = 50, Longitude = 7, 
    Time = new TimeSpan(0,15,0) } 
};
  
IGeoPositionWatcher<GeoCoordinate>  coordinateWatcher = 
  new EventListGeoLocationMock(events); coordinateWatcher.StatusChanged += 
  new EventHandler<GeoPositionStatusChangedEventArgs>(...); 
coordinateWatcher.Start();

デバイス名から、アプリケーションが実際のデバイスとエミュレーターのどちらで実行されているかがわかるため、どちらの IGeoPositionWatcher を使用すべきか判断できます。このためには、DeviceName 拡張プロパティを探します。このプロパティは、エミュレーターでアプリケーションを実行するときは常に XDeviceEmulator に設定されます。

private static bool IsEmulator() 
{ 
  return (Microsoft.Phone.Info.DeviceExtendedProperties.GetValue("DeviceName") 
    as string) == "XDeviceEmulator"; 
}

また、Dragos Manolescu のブログ (bit.ly/h72vXj、英語) では、Reactive Extensions (Rx) を使用して Windows Phone 7 イベント ストリームのモックを作成する別の方法も紹介されています。

実際のアプリケーションとパフォーマンス

作成したアプリケーションを販売するときは、そのアプリケーションがユーザーにとって魅力的なものでなくてはならないことは明白です。ユーザーは、優れた機能を持つ、動作の速いアプリケーションを希望します。前の例では、ユーザーに結果を表示するには、Web サービス メソッドを呼び出して、非同期イベントを処理する必要があることを示しました。しかし、アプリケーションがモバイル デバイスで実行されていて、通常の Wi-Fi 接続は常に使用できるとは限らないことを忘れないでください。

ネットワーク経由の Web サービスの呼び出しとデータの転送を減らすことで、アプリケーションを高速にすることができます。冒頭で、メニューと位置認識機能を提供するハンバーガー ショップのアプリケーションについて触れました。そのようなアプリケーションを作成する場合は、メニューと宣伝を Windows Phone に提供する、クラウドで実行されるサービスがあるはずです。Windows Phone で複雑な計算を実行するのではなく、このサービスを使用して複雑な計算を実行しましょう。以下に例を示します。

[ServiceContract] 
public interface IRestaurantLocator 
{ 
  [OperationContract] 
  NearResult GetNear(Location location); 
}

ユーザーの現在位置を使用するサービスを作成することができます。サービスはいくつかのスレッド (例では Parallel.ForEach を使用しています) から開始して、この位置と別のショップ間の距離を同時に計算します (図 11 参照)。

図 11 ユーザーの場所と近くの 3 つのショップの間の距離の計算

public NearResult GetNear(BingRoute.Location location) 
{ 
  var near = new NearResult(); 
  near.Restaurants = new List<RestaurantResult>(); 
  
  ... 
  
  Parallel.ForEach(restaurants, (resto) => 
  { 
    try 
    { 
      // Build geo request. 
      var geoRequest = new BingGeo.GeocodeRequest(); 
      ... 
  
      // Get the restaurant's location. 
      var geoResponse = geoClient.Geocode(geoRequest); 
  
      // Restaurant position. 
      if (geoResponse.Results.Any()) 
      { 
        var restoLocation = 
          geoResponse.Results.FirstOrDefault().Locations.FirstOrDefault(); 
        if (restoLocation != null) 
        { 
          // Build route request. 
          var fromWaypoint = new Waypoint(); 
          fromWaypoint.Description = "Current Position"; 
          ...; 
  
          var toWaypoint = new Waypoint(); 
          ... 
  
          // Create the request. 
          var routeRequest = new RouteRequest(); 
          routeRequest.Waypoints = new Waypoint[2]; 
          routeRequest.Waypoints[0] = fromWaypoint; 
          routeRequest.Waypoints[1] = toWaypoint; 
          ... 
  
          // Execute the request. 
          var routeClient = new RouteServiceClient(); 
          var routeResponse = routeClient.CalculateRoute(routeRequest); 
  
          // Add the result to the result list. 
          if (routeResponse.Result != null) 
          { 
            var result = new RestaurantResult(); 
            result.Name = resto.Name; 
            result.Distance = routeResponse.Result.Summary.Distance; 
            result.TotalSeconds = routeResponse.Result.Summary.TimeInSeconds;
            results.Add(result); 
          } 
        } 
      } 
    } 
    catch (Exception ex) 
    { 
      // Take appropriate measures to log the error and/or show it to the end user. 
    } 
  }); 
  

  // Get the top 3 restaurants.
  int i = 1;
  var topRestaurants = results.OrderBy(o => o.TotalSeconds)
                       .Take(3)
                       .Select(o => { o.Index = i++; return o; });
 
  // Done.
  near.Restaurants.AddRange(topRestaurants);
  return near;
 
}

ショップの一覧で、各ショップを並列にループ処理すると、各ショップの場所が、GeocodeServiceClient を使用して地理位置情報に変換されます。この場所とユーザーの場所を使用すると、RouteServiceClient によってこれらの地点間でルートが計算されます。最後に、一番近くにあるショップを 3 つ見つけるために、ルートの概要の TotalSeconds プロパティを使用し、それらのショップをデバイスに送信します。

この利点は、計算を (Parallel.ForEach を使用して、コンピューターのリソースに応じて) 同時実行し、計算が完了したら、関連するデータのみを Windows Phone に送信する点です。パフォーマンスの点で違いがおわかりになると思います。このモバイル アプリケーションは、単一の Web サービス メソッドのみを呼び出し、ネットワーク経由でのデータの送信は少ししかありません。

それに加え、Windows Phone 7 のコードと非同期呼び出しが劇的に減ります。

var client = new RestaurantLocatorClient(); 
client.GetNearCompleted += new EventHandler<
  GetNearCompletedEventArgs>(OnGetNearComplete); 
client.GetNearAsync(location);

図 12 は、近くのショップを表示している Windows Phone の画面です。

The Phone Display of Three Nearby Restaurants

図 12 Windows Phone 7 に表示されている近くのショップ 3 つ

Marketplace に提出する

最後に、Windows Phone 7 の Marketplace へのアプリケーションの提出プロセスについてお話します。アプリケーションを Marketplace で公開するには、アプリケーションがいくつかの要件を満たす必要があります。要件の 1 つは、アプリケーションのマニフェスト ファイルで、アプリケーションの機能を定義していることです。GeoCoordinateWatcher を使用する場合、アプリケーションのマニフェスト ファイルで ID_CAP_LOCATION 機能を定義する必要もあります。

MSDN ライブラリの「方法: Capability Detection Tool for Windows Phone を使用する」(bit.ly/hp7fjG、英語) では、アプリケーションが使用するすべての機能を検出する Capability Detection Tool の使い方が説明されています。アプリケーションを提出する前に、この記事をお読みください。

サンプル アプリケーション

ダウンロードできるこのアプリケーションのコードには、2 つのプロジェクトを持つソリューションが含まれています。1 つ目は、自身のプロジェクトに簡単に統合できる、コントロール、拡張メソッド、およびスタイルを含むクラス ライブラリです。2 つ目は、すべての例を小さなアプリケーションに結合する、Windows Phone 7 ピボット アプリケーションのサンプルです。

Sandrino Di Mattia は、マイクロソフトのファンです。RealDolmen でテクニカル コンサルタントとして働いており、マイクロソフトのテクノロジと製品を統合し、顧客とそのビジネスに役立つソリューションを作成しています。余暇には、ベルギーのユーザー グループに参加したり、ブログ (blog.sandrinodimattia.net、英語) を執筆したりしています。

この記事のレビューに協力してくれた技術スタッフの Dragos Manolescu に心より感謝いたします。