Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Visualizzazione degli itinerari di Bing in Windows Phone 7
Sandrino Di Di
Microsoft Windows Phone 7 include un'API di georilevamento facile da utilizzare che consente di stabilire la posizione corrente e gli spostamenti di un utente, espressi in latitudine e longitudine (e talvolta anche la latitudine). Una volta ottenuto l'accesso ai dati, saremo pronti a creare le funzionalità di riconoscimento della posizione nella nostra applicazione Windows Phone 7.
Se si crea un'applicazione per un fastfood, sarebbe fantastico se, oltre a presentare il menu e le promozioni, fosse possibile anche individuare il ristorante più vicino in base alla posizione corrente dell'utente. Un'altra eccezionale funzionalità sarebbe rappresentata dalla capacità di trovare altre persone nelle vicinanze. Si immagini situazioni in cui il personale di vendita desideri verificare se è possibile fare visita ai propri clienti negli intervalli tra una riunione e l'altra.
Questo articolo è incentrato sul modo in cui trasferire tali dati in un'applicazione Windows Phone 7 e sul modo in cui visualizzare itinerari e posizioni in modi diversi. I dati veri e propri provengono dall'API Bing Maps. Quindi, prima di potervi illustrare alcuni concetti avanzati, è importante dare uno sguardo alle nozioni fondamentali relative all'API Bing Maps.
Introduzione all'API Bing Maps
Innanzitutto, è necessario un account Bing attivo. Sul sito Web Bing Maps Account Center (bingmapsportal.com), è necessario utilizzare un Windows Live ID per registrarsi e ottenere un account Bing Maps. Dopo aver creato l'account, sarà possibile accedere ai dettagli dell'account, in cui sarà possibile creare una chiave. Poiché creeremo un'applicazione Windows Phone 7, è consentito scrivere qualcosa del tipo http://localhost come URL dell'applicazione.
Oltre alla creazione di un account, in questa pagina è possibile inoltre monitorare l'utilizzo dell'API Bing Maps. Qualora decidessimo di utilizzare Bing Maps in un'applicazione di produzione, sarà inoltre necessario a questa pagina per ottenere informazioni sulla licenza.
L'API Bing Maps API fornisce in realtà alcuni servizi e l'applicazione Windows Phone 7 utilizzerà i servizi SOAP. Di seguito riporto una rapida introduzione ai seguenti servizi:
- 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
Il servizio Geocode consente di utilizzare le coordinate e gli indirizzi; il servizio Imagery consente di utilizzare le immagini vere e proprie (aeree, ravvicinate e stradali); il servizio Route consente di calcolare l'itinerario tra due o più punti; per finire, il servizio Search consente di cercare posizioni in base all'input dell'utente (ad esempio, "ristorante a Roma").
Per utilizzare tali servizi, è necessario aggiungere solo un "riferimento al servizio" a uno degli URL precedenti. Tenere presente che questa azione comporterà la creazione o l'aggiornamento del file ServiceReferences.ClientConfig. Nella Figura 1 viene mostrato un esempio di come è possibile richiamare il metodo Geocode del servizio Geocode.
Figura 1 Come richiamare il metodo Geocode del servizio 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.";
};
Ciascun metodo del servizio è di tipo richiesta/risposta. Si crea un oggetto richiesta in cui preparare la domanda per il server e configurare la chiave dell'API. In questo caso, ho creato l'elemento GeocodeRequest con cui chiedere al server di fornire l'elemento GeocodeLocation di "Brussels, Belgium" (Bruxelles, Belgio). Dopo aver creato la richiesta, viene richiamato il client in modo asincrono. Infine, si riceverà una risposta che fornirà le informazioni e, in caso di problemi, consentirà di trovare l'errore. Eseguire l'applicazione di esempio nel download associato a questo articolo per visualizzare l'elemento GeocodeServiceClient in azione e verificare come la posizione (o l'errore) viene visualizzato mediante un'associazione dati.
L'applicazione utilizzerà i servizi Geocode e Route per calcolare l'itinerario tra due indirizzi e mostrarli all'utente.
Calcolo dell'itinerario
Mediante il servizio Route di Bing, è possibile calcolare l'itinerario da un punto A a un punto B. Esattamente come nell'esempio precedente, l'operazione è di tipo richiesta/risposta. Sarà necessario innanzitutto trovare la posizione geografica effettiva di ciascun indirizzo (mediante un elemento GeocodeRequest) e mediante tali posizioni sarà possibile creare un elemento RouteRequest. L'applicazione di esempio nel download associato a questo articolo contiene tutto il relativo codice, ma nella Figura 2 è illustrato un breve esempio di come avviene questa operazione.
Figura 2 Creazione di un elemento 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);
Tenere presente che la proprietà Waypoints della richiesta è una raccolta che consente di aggiungere più punti percorso. Ciò può rivelarsi interessante quando è necessario conoscere l'itinerario completo invece di un semplice tratto compreso tra un punto A e un punto B.
Con l'esecuzione del metodo CalculateRouteAsync, verranno eseguite le attività più impegnative: il calcolo dell'itinerario, creazione di un elenco con tutti gli elementi dell'itinerario (azioni quali svolte, uscite e così via), il calcolo della durata e della distanza, creazione di un elenco con tutti i punti (posizioni geografiche) e altro. Nella Figura 3 è illustrata una panoramica di alcuni dati importanti presenti nell'elemento RouteResponse.
Figura 3 Contenuto dell'elemento RouteResponse
Visualizzazione dell'itinerario su una mappa
Nel mio primo esempio, utilizzerò la proprietà RoutePath.Points per visualizzare l'itinerario su una mappa. Poiché il Windows Phone 7 Toolkit già comprendere il controllo Bing Maps, sarà sufficiente aggiungere un riferimento all'assembly Microsoft.Phone.Controls.Maps. Dopo aver completato questa operazione, è semplice visualizzare la mappa nel telefono. Di seguito è riportato un esempio di come visualizzare la mappa di Bruxelles (è richiesto l'elemento CredentialsProvider per impostare la chiave dell'API):
<maps:Map Center="50.851041,4.361572" ZoomLevel="10"
CredentialsProvider="{StaticResource MapCredentials}" />
Se si intendono utilizzare uno dei controlli dell'assembly Maps, consiglio di aggiungere un riferimento a tale assembly prima di aggiungere un riferimento al servizio ai servizi Bing. Il riferimento al servizio riutilizzerà quindi tipi, quali Microsoft.Phone.Controls.Maps.Platform.Location invece di crearne di nuovi e non sarà necessario scrivere metodi di conversione o convertitori di valori per utilizzare alcuni dei dati restituiti dal servizio.
Quindi, ora sappiamo come calcolare l'itinerario tra due punti e come visualizzare una mappa nel telefono. Associamo queste due tecniche per visualizzare l'itinerario sulla mappa. I punti nell'elemento RoutePath verranno utilizzati per il disegno sulla mappa. Il controllo Map consente di aggiungere forme, quali un elemento Pushpin (per mostrare l'inizio e la fine dell'itinerario, ad esempio) e un elemento MapPolyline (per disegnare l'itinerario basato su coordinate geografiche).
Poiché i punti restituiti dal servizio non sono dello stesso tipo dei punti utilizzati dal controllo Maps, ho creato due piccoli metodi di estensione per convertire i punti nel tipo corretto, come mostrato nella Figura 4.
Figura 4 Metodi di estensione per convertire i punti nel tipo corretto
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;
}
È possibile utilizzare tali metodi di estensione nell'elemento RouteResponse quando viene completato il metodo CalculateRoute. Dopo la conversione, tali metodi di estensione restituiranno tipi che possono, ad esempio, essere utilizzati per l'associazione al controllo Bing Maps. Poiché si tratta di un'applicazione Silverlight, dobbiamo utilizzare un elemento IValueConverter per eseguire l'effettiva conversione. Nella Figura 5 viene illustrato un esempio del convertitore di valori che convertirà le posizioni in coordinate geografiche.
Figura 5 Utilizzo di un elemento 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;
}
}
}
Passiamo ora alla configurazione dell'associazione dati. L'associazione dati utilizzerà i convertitori, pertanto è importante dichiararli ed è possibile farlo nelle risorse della pagina o dell'applicazione (se si prevede di riutilizzare i convertitori), come mostrato di seguito:
<phone:PhoneApplicationPage.Resources>
<converters:LocationConverter x:Key="locationConverter" />
<converters:ItineraryItemDisplayConverter x:Key="itineraryConverter" />
</phone:PhoneApplicationPage.Resources>
Dopo aver dichiarato i convertitori, è possibile aggiungere il controllo Maps e altri controlli di sovrapposizione (come MapPolyline e Pushpin) e associarli alle proprietà richieste, come mostrato di seguito:
<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>
Come noterete, tali associazioni utilizzano i convertitori dichiarati in precedenza per convertire i dati in un formato compreso dal controllo Maps. Infine, è necessario impostare le seguenti proprietà dopo il completamento del metodo CalculateMethod, come mostrato di seguito:
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;
}
}
Nella Figura 6 viene illustrata la schermata visualizzata dopo l'avvio dell'applicazione e il calcolo dell'itinerario.
Figura 6 Rappresentazione visiva dell'itinerario
Visualizzazione delle indicazioni
Come potete notare, la visualizzazione dell'itinerario su una mappa è un'operazione molto semplice. Nel prossimo esempio, vi mostrerò come creare un controllo personalizzato in cui visualizzare le indicazioni complete mediante il testo e il riepilogo di ciascun elemento ItineraryItem. Nella Figura 7 è riportato il risultato finale.
Figura 7 Visualizzazioni delle indicazioni complete
Nella Figura 1, Legs rappresenta una delle proprietà della classe RouteResult. La proprietà Legs contiene uno o più oggetti "Leg", ciascuno dei quali comprende una raccolta di elementi ItineraryItem. Mediante gli elementi dell'itinerario, sarà possibile riempire il controllo visualizzato nella Figura 7. Su ciascuna riga riportata nella Figura 7 viene visualizzato un elemento ItineraryItem con i secondi totali e una distanza totale relativa al passaggio specifico e l'indice del passaggio corrente. Nell'elemento ItineraryItem non viene monitorato il conteggio corrente dei passaggi, pertanto ho creato una piccola classe denominata ItineraryItemDisplay:
public class ItineraryItemDisplay
{
public int Index { get; set; }
public long TotalSeconds { get; set; }
public string Text { get; set; }
public double Distance { get; set; }
}
Il codice di esempio nel download associato a questo articolo contiene anche un metodo di estensione con la seguente firma:
public static ObservableCollection
<ItineraryItemDisplay> ToDisplay(this
ObservableCollection<ItineraryItem> items)
Il codice di questo metodo scorre ciclicamente tutti gli elementi, scrive i valori importanti in un nuovo oggetto ItineraryItemDisplay e monitora inoltre il conteggio corrente dei passaggi nella proprietà Index. Infine, l'elemento ItineraryItemDisplayConverter si prende cura della conversione durante l'associazione dati. Come avrete notato nella Figura 7, ciascun passaggio dell'itinerario viene formattato in modo corretto (le città e le strade sono in grassetto) mediante un controllo personalizzato denominato ItineraryItemBlock, il cui unico scopo è formattare il testo dell'elemento ItineraryItem in modo corretto. Nella Figura 7, è inoltre visibile un blocco di colore blu con alcune informazioni aggiuntive, ma si tratta di semplice associazione dati:
[TemplatePart(Name = "ItemTextBlock", Type =
typeof(TextBlock))] public class
ItineraryItemBlock : Control
L'attributo TemplatePart definisce un elemento che deve essere presente nell'elemento ControlTemplate e anche il tipo di elemento da utilizzare. In tal caso, deve essere un TextBlock denominato 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>
Il motivo della scelta di un elemento TextBlock è ovvio. Mediante la proprietà Inlines di TextBlock, è possibile aggiungere del contenuto all'elemento TextBlock nel codice. Il metodo OnApplyMethod può essere ignorato in un controllo personalizzato e ciò si verifica quando è necessario utilizzare l'elemento ItemTextBlock (vedere la Figura 8).
Figura 8 Ricerca dell'elemento 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 proprietà Text dell'elemento ItineraryItem Text verrà analizzata e utilizzata per riempire il TextBlock con altra formattazione aggiuntiva. In realtà, si tratta di un'operazione semplice poiché la proprietà Text contiene ben altro che semplice testo. Alcune parti sono circondate da tag XML:
<VirtualEarth:Action>Turn</VirtualEarth:Action>
<VirtualEarth:TurnDir>left</VirtualEarth:TurnDir>onto
<VirtualEarth:RoadName>Guido Gezellestraat
</VirtualEarth:RoadName>
Poiché voglio solo evidenziare città e nomi di strade, ho scritto un piccolo metodo per eliminare tag quali VirtualEarth:Action o VirtualEarth:TurnDir. Dopo aver recuperato il TextBlock, viene chiamato il metodo SetItinerary e questa è la posizione in cui le proprietà Inlines vengono aggiunte al TextBlock (vedere la Figura 9).
Figura 9 Aggiunta della proprietà Inlines a TextBlock con il metodo 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 = "";
}
}
}
Come avrete notato nell'esempio di testo XML precedente, non tutte le parti di testo sono contenute in un elemento XML. Per poter utilizzare il testo in un elemento XmlReader, la prima operazione da fare è inserirlo in un elemento XML fittizio. Ciò consente di creare un nuovo elemento XmlReader con il testo. Mediante il metodo Read di XmlReader, è possibile scorrere ciclicamente ciascuna parte della stringa XML.
In base al NodeType, è possibile stabilire la posizione corrente nella stringa XML. Ad esempio, consideriamo il seguente elemento: <VirtualEarth:RoadName>Guido Gezellestraat</VirtualEarth:RoadName>. Mediante il metodo Read di XmlReader, saranno disponibili tre iterazioni. La prima è <VirtualEarth:RoadName> che rappresenta un elemento XmlNodeType.Element. Poiché il nostro obiettivo consiste nel formattare le strade e le città in grassetto, aggiungiamo un oggetto Run alla proprietà Inlines di TextBlock con la proprietà FontWeight impostata su Bold. L'aggiunta di un oggetto Run alla proprietà Inlines consente di associare del testo all'elemento TextBlock.
In qualsiasi altro caso, non è necessaria la formattazione e sarà necessario aggiungere un normale oggetto Run contenente solo testo senza proprietà di formattazione impostate.
Questa è la procedura per la creazione del controllo personalizzato. L'intero record della classe ItineraryItemDisplay viene visualizzato mediante un DataTemplate personalizzato per l'elemento ListBox. Tale DataTemplate contiene inoltre un riferimento al controllo personalizzato (vedere la Figura 10).
Figure 10 L'intero record della classe ItineraryItemDisplay viene visualizzato mediante un DataTemplate personalizzato per l'elemento 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>
Ora che il controllo personalizzato e le impostazioni dello stile sono pronte, non ci resta che implementarle nel controllo Pivot e nel codice. Come ho menzionato in precedenza, il controllo personalizzato e il DataTemplate verrà utilizzato in un elemento ListBox:
<controls:PivotItem Header="Directions">
<ListBox ItemsSource=
"{Binding Itinerary, Converter={StaticResource itineraryConverter}}"
Grid.RowSpan="2" ItemTemplate="{StaticResource ItineraryItemComplete}" />
</controls:PivotItem>
Questo elemento ItemSource di ListBox è associato alla proprietà Itinerary e rappresenta la modalità con cui viene popolata la proprietà; successivamente, l'elemento ItineraryItemDisplayConverter si occupa del resto. Come potete notare, mediante un controllo personalizzato e alcune impostazioni di stile, è possibile utilizzare i dati sull'itinerario ricavati dal servizio Route e renderli accattivanti per l'utente:
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;
}
}
Ricerca della posizione corrente
Negli esempi precedenti, abbiamo imparato a utilizzare i servizi Geocode e Route per ottenere le indicazioni per percorrere un itinerario compreso tra un punto A e un punto B e come visualizzare tali indicazioni. A questo punto, dobbiamo esaminare l'API di georilevamento.
GeoCoordinateWatcher è la classe da utilizzare per individuare le coordinate GPS:
coordinateWatcher = new GeoCoordinateWatcher
(GeoPositionAccuracy.High); coordinateWatcher.StatusChanged +=
new EventHandler<GeoPositionStatusChangedEventArgs>(OnCoordinateUpdate);
coordinateWatcher.Start();
La classe GeoCoordinateWatcher passerà attraverso diverse fasi dopo l'esecuzione del metodo Start, ma quando lo stato è impostato su Ready, sarà possibile accedere alla posizione corrente. È buona norma chiamare il metodo Stop dopo aver utilizzato la classe GeoCoordinateWatcher:
private void OnCoordinateStatusChanged(object sender,
GeoPositionStatusChangedEventArgs e)
{
if (e.Status == GeoPositionStatus.Ready)
{
coordinateWatcher.Stop();
// Get position.
fromLocation = coordinateWatcher.Position.Location;
LocationLoaded();
}
}
Ora, l'applicazione di esempio fornisce anche funzionalità di rilevamento della posizione. La classe GeoCoordinateWatcher espone inoltre un evento PositionChanged che consente di monitorare quando cambia la posizione. Se si sta creando un'applicazione per la visualizzazioni di indicazioni, è possibile utilizzare le modifiche della posizione per scorrere automaticamente ciascuna fase e riprodurre anche un suono indicato in VirtualEarth:Action nel testo dell'elemento ItineraryItem. Alla fine si otterrà un'applicazione di navigazione GPS vera e propria.
Il debug dell'applicazione viene effettuato mediante l'emulatore di Windows Phone 7? Se si stanno testando le funzionalità di geolocalizzazione dell'applicazione, è possibile che si incorra in un piccolo problema con lo stato della classe GeoCoordinateWatcher: rimarrà sempre sullo stato NoData e non passerà mai allo stato Ready. Questo è il motivo per cui è importante scrivere il codice in base all'interfaccia (IGeoPositionWatcher<GeoCoordinate>) e non all'implementazione (GeoCoordinateWatcher). Nel post del blog di Tim Heuer (bit.ly/cW4fM1), è possibile scaricare la classe EventListGeoLocationMock che simula un dispositivo GPS reale.
La classe EventListGeoLocationMock accetta una raccolta di elementi GeoCoordinateEventMocks che dovrebbero simulare le coordinate dell'utente nel tempo. Ciò consentirà di verificare la posizione e gli spostamenti dell'utente:
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();
In base al nome del dispositivo, è possibile determinare se l'applicazione è in esecuzione in un dispositivo reale o nell'emulatore per decidere quale interfaccia IGeoPositionWatcher utilizzare. Consiglio di cercare la proprietà estesa "DeviceName", che è sempre impostata su XDeviceEmulator durante l'esecuzione dell'applicazione nell'emulatore:
private static bool IsEmulator()
{
return (Microsoft.Phone.Info.DeviceExtendedProperties.GetValue("DeviceName")
as string) == "XDeviceEmulator";
}
In alternativa, nel blog di Dragos Manolescu (bit.ly/h72vXj), è disponibile un altro modo per simulare i flussi di eventi di Windows Phone 7 mediante Reactive Extensions o Rx.
Applicazioni reali e prestazioni
Quando si crea un'applicazione destinata alla vendita, ovviamente è necessario renderla accattivante per l'utente, il quale ricercherà un'applicazione rapida con funzionalità eccezionali. Negli esempi precedenti è stato dimostrato che è necessario chiamare alcuni metodi di servizi Web e gestire alcuni eventi asincroni prima di poter mostrare dei risultati all'utente. Non dimentichiamo che l'applicazione è in esecuzione in un dispositivo mobile e una normale connessione Wi-Fi non è sempre disponibile.
La riduzione delle chiamate ai servizi Web e dei dati trasferiti può velocizzare l'applicazione. Nell'introduzione, ho menzionato un'applicazione destinata a un fastfood che fornisce il menu e funzionalità di rilevamento della posizione. Se si sta creando una tale applicazione, è possibile che esista un servizio in esecuzione in un cloud che fornisce i menu e le promozioni al telefono. Perché non utilizzare questo servizio per eseguire calcoli complessi invece di eseguirli nel telefono? Ecco un esempio:
[ServiceContract]
public interface IRestaurantLocator
{
[OperationContract]
NearResult GetNear(Location location);
}
È possibile creare un servizio che utilizzi la posizione corrente dell'utente che avvierà alcuni thread (nell'esempio viene utilizzato Parallel.ForEach) e calcolerò la distanza tra la posizione corrente e altri ristoranti simultaneamente (vedere la Figura 11).
Figura 11 Calcolo della distanza tra la posizione di un utente e tre ristoranti nelle vicinanze
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;
}
Eseguendo un loop attraverso ciascun ristorante nell'elenco dei ristoranti in parallelo, la posizione di ciascun fastfood verrà convertita in una posizione geografica mediante l'elemento GeocodeServiceClient. Tramite questa posizione e la posizione dell'utente, l'itinerario viene calcolato tra questi punti mediante l'elemento RouteServiceClient. Infine, la proprietà TotalSeconds del riepilogo dell'itinerario viene utilizzata per trovare i ristoranti più vicini che vengono inviati al dispositivo.
Il vantaggio in questo caso risiede nel fatto che i calcoli vengono eseguiti contemporaneamente (mediante Parallel.ForEach e a seconda delle risorse della macchina) e, una volta completati, solo i dati importanti vengono inviati al dispositivo Windows Phone. Dal punto di vista delle prestazioni, si percepirà una differenza: l'applicazione mobile chiamerà solo un unico metodo di servizio Web e solo una ridotta quantità di dati verrà trasferita.
Inoltre, il codice e le chiamate asincrone nel dispositivo Windows Phone 7 si riducono considerevolmente, come mostrato di seguito:
var client = new RestaurantLocatorClient();
client.GetNearCompleted += new EventHandler<
GetNearCompletedEventArgs>(OnGetNearComplete);
client.GetNearAsync(location);
Nella Figura 12 viene illustrato come vengono visualizzati i ristoranti nelle vicinanze nel telefono.
Figura 12 Visualizzazione nel telefono di tre ristoranti nelle vicinanze
Invio al Marketplace
L'ultimo aspetto che vorrei menzionare è il processo di invio al Windows Phone 7 Marketplace. L'applicazione deve soddisfare un insieme di requisiti per poter essere pubblicata nel Marketplace. Uno di questi requisiti consiste nella definizione delle funzionalità dell'applicazione nel relativo file manifesto. Se si decide di utilizzare la classe GeoCoordinateWatcher, sarà inoltre necessario definire la funzionalità ID_CAP_LOCATION nel file manifesto dell'applicazione.
Nella pagina della Libreria MSDN intitolata "How to: Use the Capability Detection Tool for Windows Phone" (in lingua inglese) (bit.ly/hp7fjG), viene spiegato come utilizzare lo strumento Capability Detection Tool per individuare tutte le funzionalità utilizzate dall'applicazione. Consiglio di leggere l'intero articolo prima di inviare la propria applicazione.
Applicazione di esempio
Il download di codice associato a questo l'articolo contiene una soluzione con due progetti. Uno è una libreria di classi che contiene il controllo, i metodi di estensione e gli stili che è possibile integrare facilmente nei propri progetti. Il secondo è un'applicazione pivot per Windows Phone 7 di esempio che integra tutti gli esempi in una piccola applicazione.
Sandrino Di Mattia è un appassionato di prodotti Microsoft. Nella sua professione di consulente tecnico presso RealDolmen, integra le tecnologie e i prodotti Microsoft per creare soluzioni che funzionino per i clienti e per le relative aziende. Nel tempo libero, partecipa anche a gruppi di utenti belgi e scrive articoli nel suo blog all'indirizzo blog.sandrinodimattia.net.
Un ringraziamento al seguente esperto tecnico per la revisione dell'articolo: Dragos Manolescu