13 March 2013

Simple reverse geocoding with Windows Phone 8 and MVVMLight

screenshotHaving worked in Geographical Information Systems over 20 years, I can tell you rightfully the new Windows Phone 8 mapping and location abilities are more than enough to make a map maniac like me getting twinkly eyes. It has capabilities that are unheard of even just a couple of years ago – and I don’t need a big workstation, I don’t even need a PC - it’s running on my phone. The world in my pocket – in the most literal sense possible.

Two popular applications of GIS are geocoding and reverse geocoding. Geocoding enables you to find the position of earth for a descriptive text – say an address, city, building name, or any other phrase indicating a place on Earth. This is usually rather straightforward. Reverse geocoding is exactly the opposite – it’s the “what’s here?” question – given a location, what do I find here? Incidentally, answering questions like this is how I make a living at Vicrea.

Windows Phone 8 makes reverse geocoding almost embarrassingly easy. Even when using MVVMLight. So I made a simple app that show the address(es) found at the location where you tap on the map.

We start off with a simple model with two properties:

using System.Collections.ObjectModel;
using System.Device.Location;
using System.Linq;
using GalaSoft.MvvmLight;
using Microsoft.Phone.Maps.Services;

namespace TapReverseGeocode.Logic.ViewModels
{
  public class MapViewModel : ViewModelBase
  {
    public MapViewModel()
    {
      Addresses = new ObservableCollection<string>();
    }

    private GeoCoordinate tapCoordinate;
    public GeoCoordinate TapCoordinate
    {
      get { return tapCoordinate; }
      set
      {
        tapCoordinate = value;
        RaisePropertyChanged(() => TapCoordinate);
        StartReverseGeoCoding();
      }
    }

    public ObservableCollection<string> Addresses { get; set; }
  }
}

The ObservableCollection “Addresses” will hold the results, and as usual when binding to ObservableCollection you must make sure it is initialized before anything else – the constructor is a good place for that. The designer can bind this to some kind of GUI element that displays the result.

The TapCoordinate property is a GeoCoordinate and that fires off the actual reverse geocoding – and I have omitted the usual “if (viewModelPropertyName != value)” check on purpose. Even when the user taps the same location twice, I want to have the reverse geocoding code to fire every time.

The code that starts the reverse geocoding itself ain’t quite rocket science:

private void StartReverseGeoCoding()
{
  var reverseGeocode = new ReverseGeocodeQuery();
  reverseGeocode.GeoCoordinate = 
    new GeoCoordinate(TapCoordinate.Latitude, TapCoordinate.Longitude);
  reverseGeocode.QueryCompleted += ReverseGeocodeQueryCompleted;
  reverseGeocode.QueryAsync();
}

To prevent race conditions I make a new GeoCoordinate from the one provided by the user, set up a call back, and fire off the async query.

The final piece is this simple callback that processes the result of the reverse geocoding.

private void ReverseGeocodeQueryCompleted(object sender, 
  QueryCompletedEventArgs<System.Collections.Generic.IList<MapLocation>> e)
{
  var reverseGeocode = sender as ReverseGeocodeQuery;
  if (reverseGeocode != null)
  {
    reverseGeocode.QueryCompleted -= ReverseGeocodeQueryCompleted;
  }
  Addresses.Clear();
  if (!e.Cancelled)
  {
    foreach (var adress in e.Result.Select(adrInfo => adrInfo.Information.Address))
    {
      Addresses.Add(string.Format("{0} {1} {2} {3} {4}", 
        adress.Street, adress.HouseNumber, adress.PostalCode,
        adress.City,adress.Country).Trim());
    }
  }
}

It clears up the callback, clears the Addresses list, and then processes the parts of the result into a single string per address. Like any good reverse geocoding service Microsoft have implemented this to return a set of results – there may be more addresses on one location, for instance in a large apartment building – although I never got more than one result back per location when I tested this in the Netherlands.

This a complete reverse geocoding viewmodel that basically does not care where the GeoCoordinate comes from, or the result goes to. So this is very versatile. There isn’t any GUI, and yet we already have a working app

The initial XAML for binding this stuff – after setting the datacontext to this viewmodel – looks pretty simple:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
  <maps:Map/>
  <Grid Height="58" VerticalAlignment="Top" Background="#7F000000">
    <phone:LongListSelector ItemsSource="{Binding Addresses}" 
      HorizontalContentAlignment="Left" Margin="12,0"/>
  </Grid>
</Grid>

… and then we run into a challenge. Two actually. The last tapped location is not a property we can bind to, and that the location is a Point – a screen location, not a GeoCoordinate in real world coordinates.

This can be solved by using an Attached Dependency Property (I think), by some Code Behind or my trademark way - by creating a simple behavior. After all, I don’t want to bother designers with code and I like the easy reusability of a behavior:

using System.Device.Location;
using System.Windows;
using Microsoft.Phone.Maps.Controls;
using Wp7nl.Behaviors;

namespace Wp8nl.Behaviors
{
  public class TapToCoordinateBehavior : SafeBehavior<Map>
  {
    protected override void OnSetup()
    {
      AssociatedObject.Tap += AssociatedObjectTap;
    }

    void AssociatedObjectTap(object sender, 
      System.Windows.Input.GestureEventArgs e)
    {
      var tapPosition = e.GetPosition((UIElement)sender);
      TappedCoordinate = 
        AssociatedObject.ConvertViewportPointToGeoCoordinate(tapPosition);
    }

    protected override void OnCleanup()
    {
      AssociatedObject.Tap -= AssociatedObjectTap;
    }

 // GeoCoordinate TappedCoordinate dependency property omitted

   }
}

This behavior is implemented as a SafeBehavior child class, to prevent memory leaks. It’s actually pretty simple – it traps the ‘Tap’ event, determines the location, converts it to a GeoCoordinate and puts it into the TappedCoordinate Dependency Property. Which, in turn, can be bound to the view model. The designer can simply drag this behavior on top of the map and set up the data binding. Don’t you love Blend? XAML take 2:

<Grid x:Name="ContentPanel" Grid.Row="1" 
   Margin="12,0,12,0">
  <maps:Map>
    <i:Interaction.Behaviors>
      <Behaviors:TapToCoordinateBehavior 
          TappedCoordinate="{Binding TapCoordinate, Mode=TwoWay}"/>
    </i:Interaction.Behaviors>
  </maps:Map>
  <Grid Height="58" VerticalAlignment="Top" Background="#7F000000">
    <phone:LongListSelector ItemsSource="{Binding Addresses}" 
           HorizontalContentAlignment="Left" Margin="12,0"/>
  </Grid>
</Grid>

And that’s all there is to it. Reverse geocoding is Windows Phone 8 is insanely easy.

Full source code, as usual, can be downloaded here.

4 comments:

Enzo said...

Thank you for your example.
How can be modified to make the reverseGeoCoding only when a touch&hold gesture is made on the map? Is it possible to filter in some way the gesture in the TapToCoordinateBehaviaur?

Kind regards
Enzo Contini

Joost van Schaik said...

@Enzo,

I suppose it can be adapted for long press but I have not looked in detail. I believe the phone toolkit implements a long press

Unknown said...

Hi,

nice article :-)

btw: this is my alternative (maybe easier)


public class MapGestureToCommand : EventToCommand
{

protected override void Invoke(object parameter)
{
object param = null;

Map map = this.AssociatedObject as Map;

if (map != null)
{
// params
GestureEventArgs e = parameter as GestureEventArgs;

// convert to GeoCoordinate
if (e != null)
{
param = map.ConvertViewportPointToGeoCoordinate(e.GetPosition(map));
}
}

// null
if (param == null) { param = parameter; }

base.Invoke(param);
}


}

Joost van Schaik said...

@Jakub possibly, but what I don't like about these kind of code snippets is that they don't demonstrate how they are used. You implicitly assume your readers will know that. How about blogging your code with some explanation and a sample solution? I will make a link to your blog if you will ;-)