Compartir a través de


Programación con reconocimiento de ubicación

Visualización de rutas de Bing en Windows Phone 7

Sandrino Di Di

Descargar el ejemplo de código

Microsoft Windows Phone 7 incluye una API de ubicación geográfica fácil de usar que le permite determinar la posición actual y el movimiento de un usuario, expresados en latitud y longitud (y a veces también en altitud). Una vez que obtiene acceso a estos datos, está preparado para crear características con reconocimiento de ubicación en la aplicación de Windows Phone 7.

Si está creando una aplicación para un restaurante de hamburguesas, sería excelente si (además de presentar el menú y las promociones) también pudiera ubicar el restaurante más cercano basándose en la ubicación actual de un usuario. Otra muy buena característica podría ser la capacidad de encontrar personas ubicadas cerca suyo. Piense en escenarios relacionados con vendedores que deseen ver si puede visitar a sus clientes entre reuniones.

Este artículo se centrará en cómo llevar estos datos a una aplicación de Windows Phone 7 y cómo visualizar rutas y ubicaciones de maneras distintas. Los datos reales provienen de la API de Bing Maps. Por lo tanto, antes de poder mostrarle algunos conceptos avanzados, es importante echar un vistazo a los aspectos básicos de la API de Bing Maps.

Introducción a la API de Bing Maps

Lo primero que necesita es una cuenta Bing que funcione. En el Centro de cuentas de Bing Maps (bingmapsportal.com), deberá usar un Windows Live ID para suscribirse y obtener una cuenta de Bing Maps. Después de crear la cuenta, obtendrá acceso a los detalles de esta, donde será posible crear una clave. Debido a que creará una aplicación para Windows Phone 7, podrá escribir algo similar a http://localhost para la dirección URL de la aplicación.

Además de crear una cuenta, en esta página también podrá supervisar el uso de la API de Bing Maps. Si decide usar Bing Maps en una aplicación de producción, también deberá volver a esta página para ponerse en contacto con alguien respecto de las licencias.

En realidad, la API de Bing Maps presta unos pocos servicios, y la aplicación de Windows Phone 7 consumirá los servicios SOAP. En este artículo hablaré brevemente sobre los siguientes servicios:

  • Código geográfico dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc
  • Imágenes dev.virtualearth.net/webservices/v1/imageryservice/imageryservice.svc
  • Ruta dev.virtualearth.net/webservices/v1/routeservice/routeservice.svc
  • Búsqueda dev.virtualearth.net/webservices/v1/searchservice/searchservice.svc  

El servicio Código geográfico le permite trabajar con coordenadas y direcciones; el servicio Imágenes le permitirá trabajar con imágenes reales (aéreas, a vista de pájaro y vista de mapa de carreteras); el servicio Ruta le ayudará a calcular la ruta entre dos o más puntos y el servicio Búsqueda le permitirá buscar ubicaciones basándose en entrada humana (como “restaurante en Bruselas”).

Para usar estos servicios, solo necesita agregar una “referencia de servicio” a una de las direcciones URL anteriores. Observe que esta acción creará o actualizará el archivo ServiceReferences.ClientConfig. La figura 1 muestra un ejemplo de cómo puede invocar el método Código geográfico del servicio Código geográfico.

Figura 1 Invocación del método Código geográfico del servicio Código geográfico

// 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 servicio se basa en solicitud/respuesta. Se crea un objeto de solicitud donde se prepara la pregunta para el servidor y se configura la clave API. En este caso, creé una GeocodeRequest y solicitaré que el servidor me brinde la GeocodeLocation de “Bruselas, Bélgica”. Después de crear la solicitud, simplemente invoco al cliente de manera asincrónica. Y, finalmente, siempre recibirá una respuesta que le brindará información y, en caso de existir problemas, también encontrará el error. Ejecute la aplicación de muestra en la descarga que acompaña este artículo para ver en acción al GeocodeServiceClient y vea cómo la ubicación (o el error) aparece en la pantalla mediante el uso de enlace de datos.

La aplicación usará los servicios Código geográfico y Ruta para calcular la ruta entre dos direcciones y mostrárselas al usuario.

Cálculo de la ruta

Mediante el servicio Ruta de Bing, puede calcular la ruta desde el punto A al punto B. Tal como en el ejemplo anterior, esto funciona con una solicitud/respuesta. Primero deberá encontrar la ubicación geográfica real de cada dirección (a través de una GeocodeRequest) y, con estas ubicaciones, podrá crear una RouteRequest. La aplicación de muestra en la descarga que acompaña a este artículo contiene todo el código, pero la figura 2 muestra un breve ejemplo de cómo se hace esto.

Figura 2 Creación de una 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 la propiedad Puntos de referencia de la solicitud es una recopilación que le permite agregar varios puntos de referencia. Esto puede resultar interesante cuando necesite conocer la ruta de un itinerario completo en lugar de una ruta solo desde el punto A al punto B.

Cuando ejecute el método CalculateRouteAsync, el servicio comenzará a realizar el trabajo duro: calcular la ruta; enumerar todos los elementos del itinerario (acciones como girar, tomar una salida, etc.); calcular la duración y la distancia; enumerar todos los puntos (ubicaciones geográficas) y más. La figura 3 muestra una introducción a algunos de los datos importantes presentes en la RouteResponse.

RouteResponse Content

Figura 3 Contenido de RouteResponse

Visualización de la ruta en un mapa

En mi primer ejemplo, usaré los puntos de RoutePath para mostrar la ruta en un mapa. Como el kit de herramientas de Windows Phone 7 ya incluye el control de Bing Maps, solo deberá agregar una referencia al ensamblado Microsoft.Phone.Controls.Maps. Después de eso, es fácil mostrar el mapa en el teléfono. A continuación, presentamos un ejemplo de cómo visualizar el mapa que muestra Bruselas (se necesita el CredentialsProvider para definir la clave API):

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

Si su intención es usar cualquiera de los controles en el ensamblado Mapas, le aconsejo agregar una referencia a este ensamblado antes de agregar una referencia de servicio a los servicios de Bing. Así, la referencia de servicio volverá a usar tipos como Microsoft.Phone.Controls.Maps.Platform.Location en lugar de crear tipos nuevos, y no será necesario que escriba métodos de conversión o convertidores de valores para usar algunos de los datos devueltos por el servicio.

Ahora sabe cómo calcular la ruta entre dos puntos y cómo visualizar un mapa en el teléfono. Unamos ahora estas dos técnicas para visualizar la ruta en el mapa. Los puntos en la RoutePath se usarán para dibujar en el mapa. El control de mapa le permite agregar formas como un marcador de posición (por ejemplo, para mostrar el comienzo y el final de la ruta) y una MapPolyline (para dibujar la ruta según las coordenadas geográficas).

Como los puntos que devuelve el servicio no son del mismo tipo que los puntos que usa el control de mapas, he creado dos pequeños métodos de extensión para convertir los puntos al tipo correcto, los que aparecen en la figura 4.

Figura 4 Métodos de extensión para convertir puntos al tipo correcto

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

Puede usar estos métodos de extensión en la RouteResponse cuando se completa el método CalculateRoute. Después de la conversión, estos métodos de extensión devolverán tipos que, por ejemplo, se pueden usar para enlazar al control de Bing Maps. Como esta es una aplicación de Silverlight, debemos usar un IValueConverter para realizar la conversión real. La figura 5 muestra un ejemplo del conversor de valores que convertirá las ubicaciones en coordenadas geográficas.

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

Ahora es momento de configurar el enlace de datos. El enlace de datos usará los conversores, por lo que es importante declararlos primero. Estos se pueden declarar en los recursos de página o en los recursos de aplicación (si planea volver a usar los conversores), tal como aparece a continuación:

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

Después de declarar los conversores, puede agregar el control de mapas y luego superponer los controles (como MapPolyline y el marcador de posición) y enlazarlos a las propiedades requeridas, tal como aparece aquí:

<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 puede ver, estos enlaces usan los conversores que se declararon anteriormente para convertir los datos en un formato reconocible para el control de mapas. Finalmente, debe definir estas propiedades una vez que se complete el CalculateMethod, tal como se muestra a continuación:

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

La figura 6 muestra la pantalla después de iniciar la aplicación y de calcular la ruta.

Visual Representation of the Route

Figura 6 Representación visual de la ruta

Visualización de direcciones

Como puede ver, la visualización de una ruta en un mapa es un procedimiento muy estándar. En el próximo ejemplo, mostraré cómo se puede construir un control personalizado que muestre las direcciones de principio a fin usando el texto y el resumen de cada ItineraryItem. La figura 7 muestra el resultado final.

Displaying Start-to-End Directions

Figura 7 Visualización de direcciones de principio a fin

En la figura 1, puede ver que una de las propiedades de RouteResult es Tramos. La propiedad Tramos contiene uno o más objetos “tramos”, donde cada uno de los cuales incluye una recopilación de ItineraryItems. Mediante el uso de los ItineraryItems, será posible llenar el control que puede ver en la figura 7. Cada una de las líneas que aparecen en la figura 7 muestra un ItineraryItem con el total de segundos y la distancia total para ese paso y el índice del paso actual. El ItineraryItem no lleva un seguimiento del número de pasos actual, por lo tanto, creé una clase pequeña llamada ItineraryItemDisplay:

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

El código de muestra en la descarga que acompaña este artículo también contiene un método de extensión con la siguiente firma:

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

El código en este método pasa por todos los elementos, escribe los valores importantes en un objeto ItineraryItemDisplay nuevo y, además, lleva un seguimiento del número actual de pasos en la propiedad Índice. Finalmente, el ItineraryItemDisplayConverter se encarga de la conversión durante el enlace de datos. Tal como puede haber observado en la figura 7, cada paso del itinerario está con un formato muy cuidado (las ciudades y las calles aparecen en negrita), mediante un control personalizado llamado ItineraryItemBlock. Su único objetivo es dar un formato claro al texto de ItineraryItem. En lafigura 7, también puede ver un bloque de color azul con alguna información adicional, pero es un enlace de datos normal:

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

El atributo TemplatePart define un elemento que debiera estar presente en la plantilla de control, y también qué tipo de elemento debiera ser. En este caso, debiera ser un TextBlock llamado 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>

El motivo para elegir un TextBlock resulta obvio. Usando la propiedad Elementos incorporados de TextBlock, puede agregar contenido al TextBlock en el código. El método OnApplyMethod se puede sobrescribir en un control personalizado, y será así cuando desee echar mano al ItemTextBlock (consulte la figura 8).

Figura 8 Detección del 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); 
}

La propiedad Texto de ItineraryItem se analizará y usará para completar este TextBlock con un formato adicional. En realidad, estos resulta fácil de hacer, porque la propiedad Texto contiene poco más que texto normal. Algunas partes están rodeadas de etiquetas XML:

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

Como solo deseo resaltar los nombres de ciudades y caminos, escribí un método pequeño que quita etiquetas como VirtualEarth:Action o VirtualEarth:TurnDir. Después de recuperar el TextBlock, se llama al método SetItinerary y es ahí donde se agregan los elementos incorporados al TextBlock (consulte la figura 9).

Figura 9 Adición de elementos incorporados a TextBlock con el 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 = ""; 
    } 
  } 
}

Tal como puede ver en el ejemplo de texto XML anterior, no todas las partes del texto están contenidas en un elemento XML. A fin de poder usar este texto en un XmlReader, lo primero que se debe hacer es incluirlo en un elemento XML ficticio. Esto nos permite crear un nuevo XmlReader con este texto. Con el método de lectura XmlReader, puede iniciar un bucle por cada parte de la cadena XML.

Según el NodeType,puede descubrir la ubicación actual en la cadena XML. Por ejemplo, considere el siguiente elemento: <VirtualEarth:RoadName>Guido Gezellestraat</VirtualEarth:RoadName>. Con el método de lectura XmlReader, tendrá tres iteraciones. La primera es <VirtualEarth:RoadName> y es un XmlNodeType.Element. Como el objetivo es dar formato en negrita a los caminos y a las ciudades, aquí es cuando se agrega un objeto Ejecutar a los elementos incorporados de TextBlock con el FontWeight definido en negrita. Agregar un objeto Ejecutar a los elementos incorporados solo anexa cierto texto al TextBlock.

En cualquier otro caso, no es necesario dar formato y es aquí donde querrá agregar un objeto Ejecutar normal que contenga solo texto sin ninguna propiedad de formato establecida.

Eso es todo en lo que respecta al control personalizado. El registro ItineraryItemDisplay completo aparece al usar una DataTemplate personalizada para el ListBox. Esta DataTemplate también contiene una referencia al control personalizado (consulte la figura 10).

Figura 10 El registro ItineraryItemDisplay completo en una DataTemplate personalizada para el 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>

Ahora que el control personalizado y el estilo están listos, la única tarea que queda por hacer es implementar esto en el control dinámico y en el código. Tal como lo mencioné anteriormente, el control personalizado y la DataTemplate se usarán en un ListBox:

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

El ItemSource de este ListBox está enlazado a la propiedad Itinerario y la propiedad se rellena de esta manera; después, el ItineraryItemDisplayConverter hace el resto. Como puede ver, con el uso de un control personalizado y un poco de estilo, puede tomar los datos del itinerario desde el servicio de ruta y hacerlos atractivos para el usuario:

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

Detección de su ubicación actual

En los ejemplos anteriores, aprendió a usar los servicios de Ruta y Código geográfico para obtener direcciones para ir desde el punto A al punto B y a visualizar estas direcciones. Ahora es momento de analizar la API de ubicación geográfica.

GeoCoordinateWatcher es la clase que usará para averiguar las coordenadas actuales de GPS:

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

El GeoCoordinateWatcher pasará por distintas etapas después de ejecutar el método Inicio, pero cuando el estado se defina en Listo, tendrá acceso a la ubicación actual. Un procedimiento recomendado es llamar al método Detener una vez que termine de trabajar con el GeoCoordinateWatcher:

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

La aplicación de muestra también proporciona características con reconocimiento de ubicación. GeoCoordinateWatcher también expone un evento PositionChanged que le permite supervisar cuándo cambia la ubicación. Si está creando una aplicación que muestra direcciones, podría usar los cambios de ubicación para desplazarse a través de cada paso de manera automática e incluso reproducir un sonido basándose en VirtualEarth:Action en el texto ItineraryItem. Terminará teniendo una aplicación de navegación GPS real.

¿Está depurando la aplicación con el emulador de Windows Phone 7? Si está probando las funcionalidades de localización geográfica de la aplicación, es posible que se tope con un pequeño problema con el estado del GeoCoordinateWatcher: siempre estará en NoData y nunca cambiará a Listo. Es por esta razón que resulta importante escribir el código respecto a la interfaz (IGeoPositionWatcher<GeoCoordinate>) y no a la implementación (GeoCoordinateWatcher). En la publicación del blog de Tim Heuer (bit.ly/cW4fM1), puede descargar la EventListGeoLocationMock que simula un dispositivo GPS real.

La clase EventListGeoLocationMock acepta una recopilación de GeoCoordinateEventMocks que debieran simular las coordenadas del usuario a tiempo. Esto permitirá probar la ubicación y el movimiento del usuario:

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

Según el nombre del dispositivo, se podría determinar si la aplicación está en ejecución en un dispositivo real o en el emulador a fin de decidir IGeoPositionWatcher usar. Busque la propiedad extendida “DeviceName”, que siempre está definida en XDeviceEmulator cuando se ejecuta la aplicación en el emulador:

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

De manera alternativa, en el blog (bit.ly/h72vXj) de Dragos Manolescu, puede encontrar otro modo de simular las secuencias de eventos de Windows Phone 7 mediante el uso de extensiones reactivas (Rx). 

Aplicaciones reales y rendimiento

Cuando crea una aplicación y desea venderla, obviamente debe resultar atractiva para el usuario. El usuario deseará una aplicación rápida con excelentes características. Los ejemplos anteriores mostraron que será necesario llamar a algunos métodos de servicio web y controlar ciertos eventos asincrónicos antes de que se puedan mostrar algunos resultados al usuario. No olvide que la aplicación se ejecuta en un dispositivo móvil y que no siempre hay disponible una conexión Wi-Fi normal.

Disminuir las llamadas de servicio web y los datos a través de la red podría acelerar la aplicación. En la introducción, hablé sobre una aplicación para restaurantes que brinda el menú y ofrece características con reconocimiento de ubicación. Si crea una aplicación de ese tipo, podría haber un servicio en ejecución en una nube que proporcione menús y promociones por teléfono. ¿Por qué no usar este servicio para realizar los cálculos complejos en lugar de ejecutarlos en el teléfono? Aquí se muestra un ejemplo:

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

Podría crear un servicio que use la ubicación actual del usuario. Este servicio iniciará algunos subprocesos (el ejemplo usa Parallel.ForEach) y calculará la distancia entre esta ubicación y otros restaurantes de manera simultánea (consulte la figura 11).

Figura 11 Cálculo de la distancia entre la ubicación de un usuario y tres restaurantes cercanos

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

Al iniciar un bucle por cada restaurante de los que aparecen en la lista de restaurantes en paralelo, la ubicación de cada restaurante se convertirá en una ubicación geográfica mediante el uso del GeocodeServiceClient. Mediante el uso de esta ubicación y de la ubicación del usuario, la ruta se calcula entre estos puntos a través del RouteServiceClient. Finalmente, la propiedad TotalSeconds del resumen de la ruta se usa para encontrar los tres restaurantes más cercanos y son estos los que se envían al dispositivo.

Aquí la ventaja es que los cálculos se ejecutan al mismo tiempo (con Parallel.ForEach y dependiendo de los recursos de la máquina) y, una vez que se completan, solo los datos importantes van al Windows Phone. En lo que se refiere al rendimiento, se sentirá la diferencia; la aplicación móvil solo llamará a un método de servicio web y solo unos pocos datos irán a través de la red.

Además de eso, el código y las llamadas asincrónicas en el Windows Phone 7 disminuyen considerablemente, tal como se demuestra a continuación:

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

La figura 12 muestra en el teléfono la pantalla de los restaurantes cercanos.

The Phone Display of Three Nearby Restaurants

Figura 12 La pantalla del teléfono que muestra los tres restaurantes cercanos

Envío al Catálogo de soluciones

Lo último que deseo mencionar es el proceso de envío al Catálogo de soluciones de Windows Phone 7. La aplicación debe cumplir con un conjunto de requisitos para poder formar parte del Catálogo de soluciones. Uno de estos requisitos es definir las capacidades de la aplicación en el archivo de manifiesto de la aplicación. Si decide usar GeoCoordinateWatcher, también deberá definir la capacidad ID_CAP_LOCATION en el archivo de manifiesto de la aplicación.

La página de MSDN Library, “Procedimiento: uso de la herramienta de detección de capacidad para Windows Phone” (bit.ly/hp7fjG), explica cómo usar la herramienta de detección de capacidad para detectar todas las capacidades que usa la aplicación. Asegúrese de leer el artículo completo antes de enviar su aplicación.

Aplicación de muestra

La descarga del código para esta aplicación contiene una solución con dos proyectos. Uno es una biblioteca de clases que contiene los métodos de control y extensión y estilos que puede integrar fácilmente en sus propios proyectos. El segundo es una aplicación dinámica de Windows Phone 7 de muestra que combina todos los ejemplos en una aplicación pequeña.

Sandrino Di Mattia es un entusiasta de Microsoft. En su trabajo como consultor técnico de RealDolmen, integra las tecnologías y productos de Microsoft para crear soluciones que trabajen para clientes y sus negocios. En su tiempo libre, participa en grupos de usuarios de Bélgica y escribe artículos en su blog, blog.sandrinodimattia.net.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Dragos Manolescu