WP7 Code: Reverse GeoCoding with the Bing Maps Service

 

My previous blog posts showed how to obtain location as an event stream from the Windows Phone 7 GeoLocation API, and how to translate lat/long position readings into distances. Here I show how to build an application that combines data from the phone’s geolocation subsystem with data from the cloud. In effect it is a data mashup running in your pocket, and powered by a battery :)

I will be (re)using some of the code from the previous applications. As this is their 3rd use it makes sense to pull the common functionality into a class library. Create a new Windows Phone Application project, and then add a Windows Phone Class Library to it. Add the following classes to this library (they require a few additional references that are not included by default):

using System;
using System.Device.Location;
using Microsoft.Phone.Reactive;

namespace GeoLocation
{
    public static class LocationHelpers
    {
        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; };
            }
            );
        }
    }
}

and

using System;
using System.Device.Location;

namespace GeoLocation
{
    public enum DistanceIn { Miles, Kilometers };

    public static class Haversine
    {

        public static double Between(this DistanceIn @in, GeoPosition<GeoCoordinate> here, GeoPosition<GeoCoordinate> there)
        {
            var r = (@in == DistanceIn.Miles) ? 3960 : 6371;
            var dLat = (there.Location.Latitude - here.Location.Latitude).ToRadian();
            var dLon = (there.Location.Longitude - here.Location.Longitude).ToRadian();
            var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
                    Math.Cos(here.Location.Latitude.ToRadian()) * Math.Cos(there.Location.Latitude.ToRadian()) *
                    Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
            var c = 2 * Math.Asin(Math.Min(1, Math.Sqrt(a)));
            var d = r * c;
            return d;
        }

        private static double ToRadian(this double val)
        {
            return (Math.PI / 180) * val;
        }
    }
}

Now to the main application code. Just like the previous applications the UI is minimalistic :) Add the following XAML (in green) within the ContentPanel Grid control:

<!--ContentPanel - place additional content here-->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <Button Content="GeoCode" Height="72" HorizontalAlignment="Left" Margin="148,519,0,0" Name="geoCodeButton" VerticalAlignment="Top" Width="160" />
<TextBlock Height="70" HorizontalAlignment="Left" Margin="46,35,0,0" Name="textBlock1" Text="GeoLocation" VerticalAlignment="Top" Width="319" Style="{StaticResource PhoneTextLargeStyle}" />
<TextBlock Height="40" HorizontalAlignment="Left" Margin="46,87,0,0" Name="statusTextBlock" Text="No data" VerticalAlignment="Top" Width="319" />
<TextBlock Height="70" HorizontalAlignment="Left" Margin="46,363,0,0" Name="textBlock3" Style="{StaticResource PhoneTextLargeStyle}" Text="Reverse GeoCoding" VerticalAlignment="Top" Width="319" />
<TextBlock Height="40" HorizontalAlignment="Left" Margin="46,439,0,0" Name="addressTextBlock" Text="(none)" VerticalAlignment="Top" Width="319" />
<TextBlock Height="40" HorizontalAlignment="Left" Margin="46,133,0,0" Name="locationTextBlock" Text="(none)" VerticalAlignment="Top" Width="319" />
</Grid>

The application holds a GeoCoordinateWatcher in an instance variable, and stops it when the user navigates away from the page:

namespace ReverseGeoCode
{
    public partial class MainPage : PhoneApplicationPage
    {
        private GeoCoordinateWatcher gcw;

        public MainPage()
        {
            InitializeComponent();
        }

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

            gcw.Stop();
        }

The OnNavigatedTo override starts the geolocation subsystem. It also prepares the UI (disabling the button), and then updates it in response to the events from the GeoLocationWatcher instance. Similar to the previous examples the implementation leverages RxLINQ. Here’s the code:

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

    if (gcw == null)
        gcw = new GeoCoordinateWatcher { MovementThreshold = 0.0 };

    gcw.Start();

    geoCodeButton.IsEnabled = false;

    var statusChanges = gcw.GetStatusChangedEventStream().Select(sc =>
        {
            var message=string.Empty;
            var isReady=false;
            switch (sc.Status)
            {
                case GeoPositionStatus.Disabled:
                    message = "Disabled";
                    break;
                case GeoPositionStatus.Initializing:
                    message = "Initializing";
                    break;
                case GeoPositionStatus.NoData:
                    message = "No data";
                    break;
                case GeoPositionStatus.Ready:
                    message = "Ready, waiting for update...";
                    isReady = true;
                    break;
                default:
                    break;
            }
            return new { isReady, message };
        });

    statusChanges.Subscribe(sc =>
        {
            statusTextBlock.Text = sc.message;
        });

    var positionUpdates = from sc in statusChanges
                          where sc.isReady
                          from pos in gcw.GetPositionChangedEventStream()
                          let location = pos.Position.Location
                          where (!Double.IsNaN(location.Latitude) && !Double.IsNaN(location.Longitude) && location.HorizontalAccuracy <= 100.0)
                          select location;

    positionUpdates.Take(1).Subscribe(_ =>
        {
            geoCodeButton.IsEnabled = true;
        });

    positionUpdates.Subscribe(l =>
            {
                statusTextBlock.Text = string.Format("Location acquired (+/-{0}m)", l.HorizontalAccuracy);
                locationTextBlock.Text = string.Format( "{0:###.0000N;###.0000S;0}, {1:###.0000E;###.0000W;0}"
                                                      , l.Latitude
                                                      , l.Longitude
                                                      );
            });
}

The statusChanges query projects the GeoPositionStatusChangedEventArgs event stream into a stream of (bool,string) pairs, with the bool indicating whether the geolocation system is ready (i.e., does it have a locked location), and the string its status in a user-friendly manner.The statusChanges subscriber updates the UI with the appropriate message. (Other resources/blogs show how to update the UI with data binding so I’m not doing it here.)

The positionUpdates query joins the status changes event stream with the location changes event stream, extracts the location from the position (via let), and projects the result into a location event stream. The two where clauses filter out unwanted status changes (e.g., GeoPositionStatus.Initializing) and location readings (e.g., HorizontalAccuracy over 100 meters).

The first event from this event stream enables the geoCodeButton. The Take combinator pulls a specified number of events—in this case just 1.

Finally, the last subscriber to the positionUpdates query updates the UI with the accuracy and lat/long from the location pushed by the phone’s geolocation subsystem.

At this point the application, though not a mashup, works with data provided solely by the device. See the following screenshot for a sample.

image

The last bit to add is a component that pulls data from the cloud. With the location information readily available the Bing Maps GeoCode Service provides an easy way to do just that. So let’s extend the code to issue ReverseGeocodeRequests and translate the lat/long into street addresses. Note that using Bing Services requires a Bing Maps Key (if you don’t already have one); authoritative information here.

To add a GeoCode service client to the project go to the project’s references, right-click, and select Add Service Reference:

image

Enter dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc in the Address box (more info here), and GeoCodeService as the Namespace:

image

This will query the service/wsdl and add the corresponding files to the solution (under the hood VisualStudio does all the code generation):

image

Add using GeoCode.GeoCodeService; to the MainPage.xaml.cs file and the following instance variable and property to the MainPage class:

private GeocodeServiceClient geoCodeClient;
public GeocodeServiceClient GeoCodeClient
{
    get
    {
        if (null == geoCodeClient)
        {
            var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
            binding.MaxReceivedMessageSize = int.MaxValue;
            binding.MaxBufferSize = int.MaxValue;
            var serviceUri = new UriBuilder(@"dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc");
            serviceUri.Scheme = Uri.UriSchemeHttps;
            serviceUri.Port = -1;
            geoCodeClient = new GeocodeServiceClient(binding, new EndpointAddress(serviceUri.Uri));
        }
        return geoCodeClient;
    }
}

As this is a cloud service the response arrives asynchronously. Rx allows one to regard this as an event stream, and query it via LINQ. First add the following extension method to the Helpers class. It is the same pattern as with location changed events.

public static class Helpers
{
    public static IObservable<ReverseGeocodeCompletedEventArgs> GetGeocodeCompletedEventStream(this GeocodeServiceClient gcss)
    {
        return Observable.Create<ReverseGeocodeCompletedEventArgs>(observable =>
            {
                EventHandler<ReverseGeocodeCompletedEventArgs> handler = (s, e) =>
                    {
                        observable.OnNext(e);
                    };
                gcss.ReverseGeocodeCompleted += handler;
                return () => { gcss.ReverseGeocodeCompleted -= handler; };
            });
    }
}

Next add the following method to MainPage.xaml.cs—don’t forget to replace the uppercase text with your own Bing Maps App Id:

    private void ResolveAddressFor(GeoCoordinate p)
    {
        geoCodeButton.IsEnabled = false;
        var request = new ReverseGeocodeRequest();
        request.Credentials = new Credentials { ApplicationId = @"REPLACE WITH YOUR OWN APP ID" };
        request.Location = new Location { Latitude = p.Latitude, Longitude = p.Longitude };
        request.ExecutionOptions = new ExecutionOptions { SuppressFaults = true };

        var results = from geoCodeResponse in GeoCodeClient.GetGeocodeCompletedEventStream()
                      where geoCodeResponse.Result.ResponseSummary.StatusCode == ResponseStatusCode.Success
                      select geoCodeResponse.Result;

        results.Subscribe(result =>
            {
                var msg = string.Empty;
                switch (result.Results.Count)
                {
                    case 0:
                        msg = @"No results";
                        break;
                    case 1:
                        msg = string.Format("{0}", result.Results.First().DisplayName);
                        break;
                    default:
                        msg = string.Format("(1/{0}) {1}", result.Results.Count, result.Results.First().DisplayName);
                        break;
                }
                addressTextBlock.Text = msg;
            });

        var errors = from geoCodeResponse in GeoCodeClient.GetGeocodeCompletedEventStream()
                     where geoCodeResponse.Result.ResponseSummary.StatusCode != ResponseStatusCode.Success
                     select geoCodeResponse.Result;
        errors.Subscribe(result =>
            {
                addressTextBlock.Text = string.Format("Error: {0}", result.ResponseSummary.FaultReason);
                this.GeoCodeClient.CloseAsync();
            });

        results.Merge(errors).Subscribe(_ => geoCodeButton.IsEnabled = true);

        this.GeoCodeClient.ReverseGeocodeAsync(request);
    }
}

This method prepares the web service request. The key pieces include the location information provided by the phone’s geolocation subsystem, and the application ID that identifies you to the Bing service. The results  RxLINQ query leverages the extension method that bridges between .NET events and Rx event streams, selecting only successful responses. The results subscriber updates the UI with the first resolved address. Likewise, the errors RxLINQ query selects unsuccessful events, and its subscriber updates the UI with the fault reason. Finally, upon a successful or an error response (via the Rx Merge combinator) the code updates the UI by (re)enabling the  button (which is disabled at the beginning of the method). In effect, the UI signals that Bing maps is servicing the request and prevents the user from triggering a new request during this time. Finally, the method invokes the ReverseGeocodeAsync method on the service client. This is where the code leverages the cloud.

To complete the puzzle, upon each tap of the geoCodeButton the code must take the last location reading, invoke the Bing Map Geocode service passing the location information, and update the UI accordingly. The following extension method (add it to the helpers class) brings the button’s click events into the Rx realm:

public static IObservable<RoutedEventArgs> GetClickEventStream(this Button button)
{
    return Observable.Create<RoutedEventArgs>(observable =>
    {
        RoutedEventHandler handler = (s, e) =>
        {
            observable.OnNext(e);
        };
        button.Click += handler;
        return () => { button.Click -= handler; };
    });
}

The  Rx piece de resistance is the query that updates the UI. The tricky bit stems from using the latest location information, which updates as often as the clauses in the positionUpdates query evaluate to true. Initially I stored the latest location into an instance variable, and had button click events update the UI with that information. However my friend, colleague, and Rx founding father Erik Meijer suggested a more elegant approach that accomplishes the same without side-effects. Here are the queries that tie button clicks and location updates—add this snippet at the end of the OnNavigatedTo method:

var buttonClicks = geoCodeButton.GetClickEventStream();

var latestLocations = positionUpdates.Publish(updates => from position in updates
                                            from click in buttonClicks.Take(1).TakeUntil(updates)
                                            select position);

latestLocations.Subscribe(l =>
    ResolveAddressFor(l)
    );

The TakeUntil combinator synchronizes the button click with the location changed event stream; the Publish operator allows the updates stream to appear in two different places without duplicating the effects of any potential side-effects. In case it’s too hard to put together all the above pieces the complete code for MainPage.xaml.cs follows. To run it you need the XAML as well as the Bing Maps GeoCode service reference (with the appropriate application ID):

using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using Microsoft.Phone.Controls;
using System.Device.Location;
using Microsoft.Phone.Reactive;
using GeoLocation;
using GeoCode.GeoCodeService;
using System.ServiceModel;

namespace ReverseGeoCode
{
    public partial class MainPage : PhoneApplicationPage
    {
        private GeoCoordinateWatcher gcw;

        private GeocodeServiceClient geoCodeClient;
        public GeocodeServiceClient GeoCodeClient
        {
            get
            {
                if (null == geoCodeClient)
                {
                    var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
                    binding.MaxReceivedMessageSize = int.MaxValue;
                    binding.MaxBufferSize = int.MaxValue;
                    var serviceUri = new UriBuilder(@"dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc");
                    serviceUri.Scheme = Uri.UriSchemeHttps;
                    serviceUri.Port = -1;
                    geoCodeClient = new GeocodeServiceClient(binding, new EndpointAddress(serviceUri.Uri));
                }
                return geoCodeClient;
            }
        }
        public MainPage()
        {
            InitializeComponent();
        }

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

            gcw.Stop();
        }

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

            if (gcw == null)
                gcw = new GeoCoordinateWatcher { MovementThreshold = 0.0 };

            gcw.Start();

            geoCodeButton.IsEnabled = false;

            var statusChanges = gcw.GetStatusChangedEventStream().Select(sc =>
                {
                    var message=string.Empty;
                    var isReady=false;
                    switch (sc.Status)
                    {
                        case GeoPositionStatus.Disabled:
                            message = "Disabled";
                            break;
                        case GeoPositionStatus.Initializing:
                            message = "Initializing";
                            break;
                        case GeoPositionStatus.NoData:
                            message = "No data";
                            break;
                        case GeoPositionStatus.Ready:
                            message = "Ready, waiting for update...";
                            isReady = true;
                            break;
                        default:
                            break;
                    }
                    return new { isReady, message };
                });

            statusChanges.Subscribe(sc =>
                {
                    statusTextBlock.Text = sc.message;
                });

            var positionUpdates = from sc in statusChanges
                                  where sc.isReady
                                  from pos in gcw.GetPositionChangedEventStream()
                                  let location = pos.Position.Location
                                  where (!Double.IsNaN(location.Latitude) && !Double.IsNaN(location.Longitude) && location.HorizontalAccuracy <= 100.0)
                                  select location;

            positionUpdates.Take(1).Subscribe(_ =>
                {
                    geoCodeButton.IsEnabled = true;
                });

            positionUpdates.Subscribe(l =>
                    {
                        statusTextBlock.Text = string.Format("Location acquired (+/-{0}m)", l.HorizontalAccuracy);
                        locationTextBlock.Text = string.Format( "{0:###.0000N;###.0000S;0}, {1:###.0000E;###.0000W;0}"
                                                              , l.Latitude
                                                              , l.Longitude
                                                              );
                    });

            var buttonClicks = geoCodeButton.GetClickEventStream();

            var latestLocations = positionUpdates.Publish(updates => from position in updates
                                                        from click in buttonClicks.Take(1).TakeUntil(updates)
                                                        select position);

            latestLocations.Subscribe(l =>
                ResolveAddressFor(l)
                );
        }

        private void ResolveAddressFor(GeoCoordinate p)
        {
            geoCodeButton.IsEnabled = false;
            var request = new ReverseGeocodeRequest();
            request.Credentials = new Credentials { ApplicationId = @"REPLACE WITH YOUR OWN APP ID" };
            request.Location = new Location { Latitude = p.Latitude, Longitude = p.Longitude };
            request.ExecutionOptions = new ExecutionOptions { SuppressFaults = true };

            var results = from geoCodeResponse in GeoCodeClient.GetGeocodeCompletedEventStream()
                          where geoCodeResponse.Result.ResponseSummary.StatusCode == ResponseStatusCode.Success
                          select geoCodeResponse.Result;

            results.Subscribe(result =>
                {
                    var msg = string.Empty;
                    switch (result.Results.Count)
                    {
                        case 0:
                            msg = @"No results";
                            break;
                        case 1:
                            msg = string.Format("{0}", result.Results.First().DisplayName);
                            break;
                        default:
                            msg = string.Format("(1/{0}) {1}", result.Results.Count, result.Results.First().DisplayName);
                            break;
                    }
                    addressTextBlock.Text = msg;
                });

            var errors = from geoCodeResponse in GeoCodeClient.GetGeocodeCompletedEventStream()
                         where geoCodeResponse.Result.ResponseSummary.StatusCode != ResponseStatusCode.Success
                         select geoCodeResponse.Result;
            errors.Subscribe(result =>
                {
                    addressTextBlock.Text = string.Format("Error: {0}", result.ResponseSummary.FaultReason);
                    this.GeoCodeClient.CloseAsync();
                });

            results.Merge(errors).Subscribe(_ => geoCodeButton.IsEnabled = true);

            this.GeoCodeClient.ReverseGeocodeAsync(request);
        }
    }

    public static class Helpers
    {
        public static IObservable<RoutedEventArgs> GetClickEventStream(this Button button)
        {
            return Observable.Create<RoutedEventArgs>(observable =>
            {
                RoutedEventHandler handler = (s, e) =>
                {
                    observable.OnNext(e);
                };
                button.Click += handler;
                return () => { button.Click -= handler; };
            });
        }

        public static IObservable<ReverseGeocodeCompletedEventArgs> GetGeocodeCompletedEventStream(this GeocodeServiceClient gcss)
        {
            return Observable.Create<ReverseGeocodeCompletedEventArgs>(observable =>
                {
                    EventHandler<ReverseGeocodeCompletedEventArgs> handler = (s, e) =>
                        {
                            observable.OnNext(e);
                        };
                    gcss.ReverseGeocodeCompleted += handler;
                    return () => { gcss.ReverseGeocodeCompleted -= handler; };
                });
        }
    }
}

Here’s the screenshot with the address information for the coordinates picked up in my office (it looks like not all the address information is available):

image

In summary, this post has shown:

  • How to use RxLINQ queries for UI updates,
  • How to use RxLINQ queries for cloud services’ results, and
  • How to mash up Windows Phone 7 GeoLocation data with data provided by Bing Map Geocode service.