Programação com reconhecimento de local

Visualizando rotas do Bing no Windows Phone 7

Sandrino Di Di

Baixar o código de exemplo

O Microsoft Windows Phone 7 é fornecido com uma API de localização geográfica fácil de usar que permite determinar a posição atual e a movimentação de um usuário, que são expressas em latitude e longitude (às vezes, também em altitude). Após ter acesso a esses dados, você estará pronto para criar recursos com reconhecimento de local no aplicativo do Windows Phone 7.

Se estiver criando um aplicativo para uma lanchonete, seria ótimo se — além de apresentar o menu e as promoções — você também pudesse localizar o restaurante mais próximo com base na localização atual de um usuário. Outro recurso excelente seria a possibilidade de encontrar pessoas próximas a você. Imagine cenários para vendedores que querem confirmar se é possível visitar seus clientes entre as reuniões.

Este artigo concentra-se em como trazer esses dados para o aplicativo do Windows Phone 7 e em como visualizar rotas e localizações de maneiras diferentes. Os dados reais vêm da API do Bing Maps. Então, antes que eu possa mostrar alguns conceitos avançados, é importante conhecer os fundamentos básicos sobre a API do Bing Maps.

Introdução à API do Bing Maps

A primeira coisa que você precisa é de uma conta ativa no Bing. No Bing Maps Account Center (bingmapsportal.com), você deve usar um Windows Live ID para se inscrever e obter uma conta no Bing Maps. Depois de criar a sua conta, você terá acesso aos detalhes da conta, onde será possível criar uma chave. Como você criará um aplicativo para o Windows Phone 7, é permitido escrever algo como http://localhost como a URL do aplicativo.

Além de criar uma conta, esta página também permite monitorar o uso da API do Bing Maps. Caso decida usar o Bing Maps em um aplicativo de produção, você também precisará voltar para esta página para entrar em contato com alguém e conseguir informações sobre o licenciamento.

A API do Bing Maps fornece alguns serviços, e o aplicativo do Windows Phone 7 consumirá os serviços SOAP. Apresentarei breves visões gerais dos seguintes serviços:

  • 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  

O serviço Geocode permite trabalhar com coordenadas e endereços; o serviço Imagery permitirá trabalhar com imagens reais (aéreas, panorâmicas e de estrada); o serviço Route ajudará a calcular a rota entre dois ou mais pontos; e o serviço Search permite buscar localizações com base em informações humanas (como “restaurante em Bruxelas”).

Para usar esses serviços, você só precisa adicionar uma “Referência de Serviço” a uma das URLs anteriores. Observe que esta ação irá criar ou atualizar o arquivo ServiceReferences.ClientConfig. A Figura 1 mostra um exemplo de como invocar o método Geocode do serviço Geocode.

Figura 1 Invocando o método Geocode do serviço 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."; 
};

Cada método de serviço é baseado em solicitação/resposta. Você cria um objeto de solicitação, no qual prepara a pergunta para o servidor e configura a chave da API. Nesse caso, criei uma GeocodeRequest e solicitarei que o servidor me forneça a GeocodeLocation de “Bruxelas, Bélgica”. Depois de criar a solicitação, invoco o cliente de uma forma assíncrona. E, por fim, você sempre receberá uma resposta que lhe fornecerá informações — e, em caso de problemas, também encontrará o erro. Execute o aplicativo de exemplo no download que acompanha este artigo para exibir o GeocodeServiceClient em ação e ver como a localização (ou o erro) é exibida na tela usando a ligação de dados.

O aplicativo usará os serviços Geocode e Route para calcular a rota entre os dois endereços e exibi-la ao usuário.

Cálculo da rota

Usando o serviço Bing Route, você pode calcular a rota do ponto A até o ponto B. Assim como no exemplo anterior, ele trabalha com uma solicitação/resposta. Você precisará encontrar a localização geográfica real de cada endereço primeiro (usando uma GeocodeRequest), e usando essas localizações, poderá criar uma RouteRequest. O aplicativo de exemplo no download que acompanha este artigo contém todo o código, mas a Figura 2 mostra um breve exemplo de como isso é feito.

Figura 2 Criação de uma 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);

Observe que a propriedade Waypoints da solicitação é uma coleção que permite adicionar vários marcos. Isso pode ser interessante quando você precisar saber a rota para um itinerário completo, em vez de somente do ponto A ao ponto B.

Quando você executar o método CalculateRouteAsync, o serviço começará a fazer o trabalho pesado: calcular a rota; listar todos os itens do itinerário (ações como virar, pegar uma saída e assim por diante); calcular a duração e a distância; listar todos os pontos (localizações geográficas) e muito mais. A Figura 3 mostra uma visão geral de alguns dados importantes presentes na RouteResponse.

RouteResponse Content

Figura 3 Conteúdo da RouteResponse

Exibindo a rota em um mapa

Em meu primeiro exemplo, usarei os RoutePath Points para exibir a rota em um mapa. Como o Windows Phone 7 Toolkit já inclui o controle do Bing Maps, você só precisará adicionar uma referência ao assembly do Microsoft.Phone.Controls.Maps. Depois disso, é fácil exibir o mapa no telefone. Este é um exemplo de como exibir o mapa mostrando Bruxelas (o CredentialsProvider é necessário para definir a chave da API):

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

Se você pretende usar algum dos controles no assembly do Maps, aconselho-o a adicionar uma referência a esse assembly antes de adicionar uma referência de serviço aos serviços do Bing. Em seguida, a referência de serviço reutilizará tipos como Microsoft.Phone.Controls.Maps.Platform.Location em vez de criar novos tipos, e você não precisará criar métodos de conversão ou conversores de valor para usar alguns dos dados retornados pelo serviço.

Então, agora você sabe como calcular a rota entre dois pontos e exibir um mapa no telefone. Vamos reunir essas duas técnicas para visualizar a rota no mapa. Os Points no RoutePath serão usados para desenhar o mapa. O controle Map permite adicionar formas, como uma Pushpin (para mostrar o início e o fim da rota, por exemplo) e uma MapPolyline (para desenhar a rota com base nas GeoCoordinates).

Como os pontos retornados pelo serviço não são do mesmo tipo dos pontos usados no controle Maps, criei dois métodos de curta extensão para converter os pontos para o tipo correto, mostrados na Figura 4.

Figura 4 Métodos de extensão para converter pontos para o tipo correto

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; 
}

Você pode usar esses métodos de extensão na RouteResponse quando o método CalculateRoute estiver concluído. Depois da conversão, esses métodos de extensão retornarão tipos que podem, por exemplo, ser usados para a ligação ao controle do Bing Maps. Como esse é um aplicativo do Silverlight, devemos usar um IValueConverter para que seja feita a conversão propriamente dita. A Figura 5 mostra um exemplo do conversor de valor que converterá Locations para GeoCoordinates.

Figura 5 Uso de um 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; 
    } 
  }
}

Agora, é hora de configurar a ligação de dados. A ligação de dados usará os conversores, portanto, é importante declará-los primeiro. Eles podem ser declarados nos recursos da página ou nos recursos do aplicativo (se você planejar reutilizar os conversores), como mostrado aqui:

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

Depois de declarar os conversores, você pode adicionar o controle dos mapas e outros controles de sobreposição (como MapPolyline e Pushpin) e ligá-los às propriedades necessárias, como mostrado aqui:

<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>

Como você pode ver, essas ligações usam os conversores que foram declarados anteriormente para converter os dados para o formato compreendido pelo controle de mapas. Por fim, você precisa definir essas propriedades depois que o CalculateMethod estiver concluído, como mostrado aqui:

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;
  }
}

A Figura 6 mostra a tela depois de inicializar o aplicativo e calcular a rota.

Visual Representation of the Route

Figura 6 Representação visual da rota

Exibição de direções

Como você pode ver, a exibição da rota em um mapa é algo bastante padronizado. No próximo exemplo, mostrarei como você pode criar um controle personalizado que exibe as direções do início ao fim usando o texto e o resumo de cada ItineraryItem. A Figura 7 mostra o resultado final.

Displaying Start-to-End Directions

Figura 7 Exibição das direções do início ao fim

Na Figura 1, você pode ver que Legs é uma das propriedades do RouteResult. A propriedade Legs contém um ou mais objetos “Led”, sendo que cada um deles inclui uma coleção de ItineraryItems. Usando o ItineraryItems, será possível preencher o controle que você pode ver na Figura 7. Cada linha na Figura 7 mostra um ItineraryItem com o número total de segundos e a distância total de cada etapa, bem como o índice da etapa atual. O ItineraryItem não mantém o controle da contagem da etapa atual, por isso, criei uma pequena classe chamada ItineraryItemDisplay:

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

O código de exemplo no download que acompanha este artigo também contém um método de extensão com a assinatura a seguir:

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

O código nesse método executa um loop em todos os itens, grava os valores importantes em um novo objeto ItineraryItemDisplay e também mantém o controle da contagem da etapa atual na propriedade Index. Por fim, o ItineraryItemDisplayConverter se encarrega da conversão durante a ligação de dados. Como você deve ter notado na Figura 7, cada etapa do itinerário é bem formatada (cidades e ruas são marcadas em negrito) usando um controle personalizado chamado ItineraryItemBlock. Seu único objetivo é formatar o texto do ItineraryItem de maneira simples. Na Figura 7, você pode ver também um bloco azul com algumas informações extras, mas a ligação de dados padrão é:

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

O atributo TemplatePart define um elemento que deve estar presente no modelo de controle, e também qual deve ser o tipo desse elemento. Nesse caso, deve ser um TextBlock chamado ItemTextBlock:

<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>

O motivo para escolher um TextBlock é óbvio. Usando a propriedade TextBlock Inlines, você pode adicionar conteúdo ao TextBlock em código. O OnApplyMethod pode ser substituído em um controle personalizado, e é nesse momento que você deve encontrar o ItemTextBlock (consulte a Figura 8).

Figura 8 Como encontrar o 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); 
}

A propriedade ItineraryItem Text será examinada e usada para preencher esse TextBlock com algumas formatações extras. Na verdade, é fácil fazer isso porque a propriedade Text contém um pouco mais do que textos comuns. Algumas partes são cercadas por marcas XML:

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

Como quero destacar somente nomes de cidades e estradas, escrevi um método pequeno que retira marcas como VirtualEarth:Action ou VirtualEarth:TurnDir. Depois de recuperar o TextBlock, o método SetItinerary é chamado, e é aqui que os Inlines são adicionados ao TextBlock (consulte a Figura 9).

Figura 9 Adição dos Inlines ao TextBlock com o método SetItinerary

// 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 = ""; 
    } 
  } 
}

Como você pode ver no exemplo anterior de texto XML, nem todas as partes do texto estão contidas em um elemento XML. Para conseguir usar esse texto em um XmlReader, a primeira coisa que deve ser feita é encapsulá-lo em um elemento XML fictício. Isso nos permite criar um novo XmlReader com esse texto. Usando o método XmlReader Read, você pode executar um loop em cada parte da cadeia de caracteres XML.

Com base no NodeType, você pode calcular a posição atual na cadeia de caracteres XML. Por exemplo, considere o seguinte elemento: <VirtualEarth:RoadName>Guido Gezellestraat</VirtualEarth:RoadName>. Usando o método XmlReader Read, você terá três iterações. A primeira é <VirtualEarth:RoadName> e é um XmlNodeType.Element. Como o objetivo é formatar as estradas e as cidades em negrito, é aqui que um objeto Run é adicionado ao TextBlock Inlines com a FontWeight definida como negrito. Adicionar um objeto Run ao Inlines acrescenta somente alguns textos ao TextBlock.

Em qualquer outro caso, você não precisa formatar, e é aqui que você deve adicionar um objeto Run normal contendo somente texto, sem nenhuma propriedade de formatação definida.

Com isso, concluímos o controle personalizado. O registro completo do ItineraryItemDisplay é exibido usando um DataTemplate personalizado para a ListBox. Esse DataTemplate também contém uma referência ao controle personalizado (consulte a Figura 10).

Figura 10 O registro completo do ItineraryItemDisplay em um DataTemplate personalizado para a ListBox

<!-- 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>

Agora que o controle personalizado e o estilo estão prontos, a única tarefa que precisa ser feita é implementá-los no controle Pivot e no código. Como mencionei anteriormente, o controle personalizado e o DataTemplate serão usados em uma ListBox:

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

Esse ListBox ItemSource está ligado à propriedade Itinerary, e é desse modo que a propriedade é preenchida; depois, o ItineraryItemDisplayConverter faz o resto. Como você pode ver, usando um controle personalizado e um pouco de estilo, você pode pegar os dados de itinerário do serviço Route e torná-los atraentes para o usuário:

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;
  }
}

Localização da sua posição atual

No exemplos anteriores, você aprendeu a usar os serviços Geocode e Route para obter direções do ponto A ao ponto B e a visualizar essas direções. Agora, é hora de observar a API de localização geográfica.

GeoCoordinateWatcher é a classe que você usará para descobrir as coordenadas atuais do GPS:

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

O GeoCoordinateWatcher passará por estágios diferentes depois de executar o método Start, mas quando o status estiver definido como Ready, você terá acesso à posição atual. A prática recomendada é chamar o método Stop depois que tiver concluído o trabalho com o GeoCoordinateWatcher:

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

Agora, o aplicativo de exemplo também fornece recursos de reconhecimento de local. O GeoCoordinateWatcher também expõe um evento PositionChanged que permite monitorar quando a posição for alterada. Se estiver criando um aplicativo que exibe direções, você pode usar as alterações na posição para percorrer cada uma das etapas automaticamente e até mesmo reproduzir um som baseado no VirtualEarth:Action no ItineraryItem Text. Você acabará criando um verdadeiro aplicativo de navegação GPS.

Você está depurando o aplicativo usando o emulador do Windows Phone 7? Se estiver testando as funcionalidades de localização geográfica do seu aplicativo, você pode se deparar com um pequeno problema com o GeoCoordinateWatcher Status: ele sempre ficará em NoData e nunca mudará para Ready. É por esse motivo que é importante escrever seu código com base na interface (IGeoPositionWatcher<GeoCoordinate>) e não na implementação (GeoCoordinateWatcher). Na postagem no blog de Tim Heuer (bit.ly/cW4fM1), você pode baixar a classe EventListGeoLocationMock que simula um dispositivo GPS de verdade.

A classe EventListGeoLocationMock aceita uma coleção de GeoCoordinateEventMocks que deve simular as coordenadas do usuário no momento. Isso permitirá testar a localização e a movimentação do usuário:

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();

Com base no nome do dispositivo, você pode determinar se o aplicativo está sendo executado em um dispositivo real ou no emulador para decidir qual IGeoPositionWatcher usar. Procure a propriedade estendida “DeviceName”, que sempre está definida como XDeviceEmulator, quando executar o aplicativo no emulador:

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

Como alternativa, no blog de Dragos Manolescu (bit.ly/h72vXj), você pode encontrar outra maneira de simular os fluxos de eventos do Windows Phone 7 usando Reactive Extensions, ou Rx. 

Aplicativos reais e desempenho

Se estiver criando um aplicativo e quiser vendê-lo, ele deve ser atraente para o usuário. O usuário desejará um aplicativo rápido e que tenha excelentes recursos. Os exemplos anteriores mostraram que você precisará chamar alguns métodos de serviço Web e lidar com alguns eventos assíncronos antes de poder mostrar alguns resultados ao usuário. Não se esqueça de que seu aplicativo está sendo executado em um dispositivo móvel e que uma conexão Wi-Fi regular nem sempre está disponível.

A redução de chamadas de serviço Web e da transmissão de dados pode acelerar seu aplicativo. Na introdução, falei sobre o aplicativo para restaurante que fornece o menu e recursos de reconhecimento de local. Se estiver criando um aplicativo como esse, pode haver um serviço em execução na nuvem que fornece menus e promoções para o telefone. Por que não usar esse serviço para realizar os cálculos complexos em vez de executá-los no telefone? Eis um exemplo:

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

Você pode criar um serviço que usa a localização atual do usuário. O serviço irá iniciar alguns threads (o exemplo usa Parallel.ForEach) e calcular a distância entre essa localização e outros restaurantes simultaneamente (consulte a Figura 11).

Figura 11 Cálculo da distância entre a localização de um usuário e três restaurantes próximos

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;
 
}

Executando um loop em cada um dos restaurantes na lista de restaurantes em paralelo, a localização de cada restaurante será convertida em uma localização geográfica usando o GeocodeServiceClient. Usando essa localização e a localização do usuário, a rota é calculada entre esses pontos usando o RouteServiceClient. Por fim, a propriedade TotalSeconds do resumo da rota é usada para encontrar os três restaurantes mais próximos, e eles são enviados ao dispositivo.

A vantagem aqui é que os cálculos são executados ao mesmo tempo (usando Parallel.ForEach e dependendo dos recursos da máquina), e assim que estiverem concluídos, somente os dados relevantes são enviados ao Windows Phone. Em termos de desempenho, você perceberá a diferença; o aplicativo móvel chamará somente um método de serviço Web e apenas alguns dados são transmitidos.

Além disso, o código e as chamadas assíncronas no Windows Phone 7 são reduzidos drasticamente, conforme demonstrado aqui:

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

A Figura 12 mostra a exibição dos restaurantes próximos no telefone.

The Phone Display of Three Nearby Restaurants

Figura 12 Exibição dos três restaurantes mais próximos no telefone

Envio ao Marketplace

A última coisa que quero mencionar é o processo de envio ao Windows Phone 7 Marketplace. Seu aplicativo deve estar de acordo com um conjunto de requisitos para ser aceito no Marketplace. Um desses requisitos é definir os recursos do seu aplicativo no arquivo de manifesto do aplicativo. Se você decidir usar o GeoCoordinateWatcher, também deverá definir o recurso ID_CAP_LOCATION no arquivo de manifesto do aplicativo.

A página da Biblioteca MSDN, “Como: usar a Ferramenta de Detecção de Recursos do Windows Phone” (bit.ly/hp7fjG), explica como usar a Ferramenta de Detecção de Recursos para detectar todos os recursos usados pelo seu aplicativo. Leia todo o artigo antes de enviar o seu aplicativo!

Aplicativo de exemplo

O download do código deste aplicativo contém uma solução com dois projetos. Um deles é uma biblioteca de classes que contém o controle, métodos de extensão e estilos que podem ser facilmente integrados aos seus próprios projetos. O segundo é um aplicativo de exemplo Windows Phone 7 Pivot que reúne todos os exemplos em um aplicativo pequeno.

Sandrino Di Mattia é um entusiasta da Microsoft. Em seu trabalho como consultor técnico na RealDolmen, integra tecnologias e produtos da Microsoft para criar soluções que funcionem para os clientes e seus negócios. Em seu tempo livre, participa de grupos de usuários belgas e escreve artigos em seu blog, blog.sandrinodimattia.net.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Dragos Manolescu