WP7 Code: Using the GeoLocation API

 

I’m kicking off a series of blog posts focused on writing Windows Phone 7 code with one of the APIs that will probably attract many developers interested in getting their feet wet: GeoLocation. Building this code requires:

Before I begin, a few things to be aware of. First, the samples are not intended to be production code. In other words, don’t use this code in your avionics system. Second, I emphasize Windows Phone code at the expense of other aspects. For example, while data binding may provide an elegant solution to updating GUI elements, I’m leaving its implementation as an exercise to the reader :) Caveat emptor.

Enough prose, let’s write some code. Start by opening a new Windows Phone Application in Visual Studio. This will create the necessary directories and unfold the appropriate templates. Add a TextBlock to the Grid called ContentPanel; the XAML will look similar to the following (new content in green):

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <TextBlock Height="30" HorizontalAlignment="Left" Margin="0,49,0,0" Name="textBlock1" Text="TextBlock" VerticalAlignment="Top" Width="450" />
</Grid>

Next add a reference to the assembly holding the GeoLocation API (System.Device), and the appropriate using directive:

using System.Device.Location;

As Windows Phone 7 includes the .NET Reactive Framework (Rx) I will be using it for event-based code. To that end add references to System.Observable and Microsoft.Phone.Reactive, and the appropriate using directive:

using Microsoft.Phone.Reactive;

An instance of GeoLocationWatcher held in an instance variable of the MainPage class provides access to the location information. The OnNavigatedTo and OnNavigatedFrom methods ensure that the location watcher is started when the page opens, and stopped when it is abandoned.

public partial class MainPage : PhoneApplicationPage
{
    GeoCoordinateWatcher gcw;

    // Constructor
    public MainPage()
    {
        InitializeComponent();
    }

    protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
    {
        base.OnNavigatedTo(e);

        if (gcw == null)
            gcw = new GeoCoordinateWatcher();

        gcw.Start();

        ShowGeoLocation();
    }

    protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
    {
        base.OnNavigatedFrom(e);

        if (gcw != null)
            gcw.Stop();
    }

    // snip

The ShowGeoLocation method contains code that updates the UI (i.e., text box) with the location information. Before covering that code let’s go over a couple of helper methods that convert the .NET events raised by the GeoLocationWatcher class into Rx event streams. The first event of interest is StatusChanged. It signals when the location subsystem is ready and thus able to provide location information. The second event is PositionChanged, which signals whenever the position of the device, as determined by the location subsystem, changes. The following extension methods provide the corresponding event streams:

public static class Helpers
{
    public static IObservable<GeoPositionStatusChangedEventArgs> GetStatusChangedEventStream(this GeoCoordinateWatcher watcher)
    {
        return Observable.Create<GeoPositionStatusChangedEventArgs>(observer =>
            {
                EventHandler<GeoPositionStatusChangedEventArgs> handler = (s, e) =>
                    {
                        observer.OnNext(e);
                    };
                watcher.StatusChanged += handler;
                return () => { watcher.StatusChanged -= handler; };
            }
        );
    }

    public static IObservable<GeoPositionChangedEventArgs<GeoCoordinate>> GetPositionChangedEventStream(this GeoCoordinateWatcher watcher)
    {
        return Observable.Create<GeoPositionChangedEventArgs<GeoCoordinate>>(observable =>
            {
                EventHandler<GeoPositionChangedEventArgs<GeoCoordinate>> handler = (s, e) =>
                    {
                        observable.OnNext(e);
                    };
                watcher.PositionChanged += handler;
                return () => { watcher.PositionChanged -= handler; };
            }
        );
    }
}

If you’re building an application that needs to determine the position just once (e.g., what’s around me based on where I’m at) then the following sequence of queries does just that, updating the contents of the text box with the location information:

public void ShowGeoLocation()
{
    var statusChanges = from statusChanged in gcw.GetStatusChangedEventStream()
                        where statusChanged.Status == GeoPositionStatus.Ready
                        select statusChanged;

    var positionChanges = from s in statusChanges
                          from position in gcw.GetPositionChangedEventStream()
                          select position.Position;

    var positions = from position in positionChanges
                         where position.Location.HorizontalAccuracy <= 100
                         select position;

    positions.Take(1).Subscribe(firstLocationFix =>
        {
            textBlock1.Text = string.Format("{0},{1}"
                                   , firstLocationFix.Location.Latitude
                                   , firstLocationFix.Location.Longitude
                                   );
        }
    );

}

The first query (statusChanges) represents an event stream comprised of status changed events signaling that the location subsystem is ready. The second query (positionChanges) is a join between statusChanges and position changed events; this ensures that the latter is gated by the former. Finally, the third query (positions) filters the position changes based on the horizontal accuracy of each reading. The accuracy depends on how the location subsystem determines the location, and the code above discard readings below 100m accuracy. The last statement updates the UI with the first reading satisfying the (composed by now) queries:

  • Filtering of StatusChanged events by Status property
  • Joining between filtered StatusChanged events and LocationChanged events
  • Filtering of resulting LocationChanged events by HorizontalAccuracy property

What if you’re building an application that needs the series of location events (e.g., turn-by-turn navigation) rather than just one location? The Rx queries shown above remain unchanged. The only change involves how the event stream is consumed. The Take method is gone, and Action argument updates the text box contents. Here’s the ShowGeoLocation code, with the updated statement in green:

public void ShowGeoLocation()
{
    var statusChanges = from statusChanged in gcw.GetStatusChangedEventStream()
                        where statusChanged.Status == GeoPositionStatus.Ready
                        select statusChanged;

    var positionChanges = from s in statusChanges
                          from position in gcw.GetPositionChangedEventStream()
                          select position.Position;

    var positions = from position in positionChanges
                         where position.Location.HorizontalAccuracy <= 100
                         select position;

    positions.Subscribe(position =>
{
textBlock1.Text = string.Format( "{0},{1}"
, position.Location.Latitude
, position.Location.Longitude
);
}
);

}

Even though the displayed latitude and longitude values may not change, the read values might change. To illustrate that the final update to this code adds a sequence number to the display. It also displays the horizontal accuracy. The code implements the sequence with the Scan operator (which applies an accumulator over the events) and an anonymous class encapsulating the sequence number. Here’s the ShowGeoLocation code, with the updated statement in green:

public void ShowGeoLocation()
{
    var statusChanges = from statusChanged in gcw.GetStatusChangedEventStream()
                        where statusChanged.Status == GeoPositionStatus.Ready
                        select statusChanged;

    var positionChanges = from s in statusChanges
                          from position in gcw.GetPositionChangedEventStream()
                          select position.Position;

    var positions = from position in positionChanges
                         where position.Location.HorizontalAccuracy <= 1000
                         select position;

   positions.Scan(new { i = 0
, p = default(GeoPosition<GeoCoordinate>)
},
(a,e) => new { i = a.i + 1
, p = e }
).Subscribe(e =>
{
textBlock1.Text = string.Format( "{0}:{1},{2}@{3}"
, e.i
, e.p.Location.Latitude
, e.p.Location.Longitude
, e.p.Location.HorizontalAccuracy
);
}
);

}

In summary, this post has shown:

  • How to get started with the Windows Phone GeoLocation API,
  • How to convert the StatusChanged and PositionChanged events into Rx event streams, and
  • How to write LINQ queries against them.