Colin Eberhardt's Adventures in .NET

Windows Phone 7 – Browsing your Photos via Bing Maps

January 16th, 2012

The Windows Phone 7 camera gives you the option to record the location where a picture was taken (under Settings => applications => pictures+camera). With this feature turned on, each application has their latitude, longitude and altitude stored as part of the standard EXIF data. I thought it would be fun to combine the previous blog post I wrote on pushpin clustering with the photos on my camera, to allow me to explore them via a Bing Maps control. With not much more than 100 lines of code I came up with an application which I think is a lot of fun to use.

Here are all the photos on my phone, note the way the pushpins are clustered.

Here are a few pictures I took in New York, of the One World Trade Centre and the Stock Exchange.

Here are some pictures around Europe, including one of Gergely Orosz waiting for his turn in the Edinburgh Marathon Relay.

And finally, some pictures I took whilst running around Kielder Water during Kielder marathon.

Accessing the EXIF data

You can access the photos on a WP7 device via the XNA MediaLibrary class. The interface that this class provides gives you access to Picture instances which have properties that allow you to access the width / height and a few other basic attributes. They also have methods that return streams which can be used to read the thumbnail and image data, however, they do not expose the picture location. This is ‘hidden’ within the EXIF data.

Fortunately there is a C# implementation of an EXIF decoder available on codeproject, which, with a few tweaks by Tim Heuer works just fine within Silverlight for Windows Phone 7.

With this library, accessing the EXIF data is a one-liner:

JpegInfo info = ExifReader.ReadJpeg(picture.GetImage(), picture.Name);

The JpegInfo class exposes the raw EXIF geolocation data, which is detailed in the EXIF specification as being expressed as separate components of degrees, minutes and seconds together with a reference direction (North / South, East / West). We can convert from the sexagesimal numeric system used in EXIF, to the decimal system as follows:

private static double DecodeLatitude(JpegInfo info)
{
  double degrees = ToDegrees(info.GpsLatitude);
  return info.GpsLatitudeRef == ExifGpsLatitudeRef.North ? degrees : -degrees;
}
 
private static double DecodeLongitude(JpegInfo info)
{
  double degrees = ToDegrees(info.GpsLongitude);
  return info.GpsLongitudeRef == ExifGpsLongitudeRef.East ? degrees : -degrees;
}
 
public static double ToDegrees(double[] data)
{
  return data[0] + data[1] / 60.0 + data[2] / (60.0 * 60.0);
}

Analysing the images

When the application starts a BackgroundWorker is used to read the EXIF data for all of the pictures in the phone’s media library, with those that have geolocation data available being stored in a separate list:

BackgroundWorker bw = new BackgroundWorker();
bw.WorkerReportsProgress = true;
 
// analyse the pictures that reside in the Media Library in a background thread
bw.DoWork += (s, e) =>
{
  var ml = new MediaLibrary();
 
  using (var pics = ml.Pictures)
  {
    int total = pics.Count;
    int index = 0;
    foreach (Picture picture in pics)
    {
      // read the EXIF data for this image
      JpegInfo info = ExifReader.ReadJpeg(picture.GetImage(), picture.Name);
 
      // check if we have co-ordinates
      if (info.GpsLatitude.First() != 0.0)
      {
        _images.Add(new LocatedImage()
        {
          Picture = picture,
          Lat = DecodeLatitude(info),
          Long = DecodeLongitude(info)
        });
      }
 
      // report progress back to the UI thread
      string progress = string.Format("{0} / {1}", index, total);
      bw.ReportProgress((index * 100 / total), progress);
 
      index++;
    }
  }
};
 
// update progress on the UI thread
bw.ProgressChanged += (s, e) =>
  {
    string title = (string)e.UserState;
    ApplicationTitle.Text = title;
  };
 
bw.RunWorkerAsync();
 
// when analysis is complete, add the pushpins
bw.RunWorkerCompleted += (s, e) =>
  {
    ApplicationTitle.Text = "";
    AddPushpins();
  };

When the pictures have all been analysed, a pushpin is created for each image which is then added to the clusterer described in my previous blog post.

private void AddPushpins()
{
  List<Pushpin> pushPins = new List<Pushpin>();
 
  // create a pushpin for each picture
  foreach (var image in _images)
  {
    Location location = new Location()
    {
      Latitude = image.Lat,
      Longitude = image.Long
    };
 
    Pushpin myPushpin = new Pushpin()
    {
      Location = location,
      DataContext = image,
      Content = image,
      ContentTemplate = this.Resources["MarkerTemplate"] as DataTemplate
    };
 
    pushPins.Add(myPushpin);
  }
 
  // add them to the map via a clusterer
  var clusterer = new PushpinClusterer(map, pushPins, this.Resources["ClusterTemplate"] as DataTemplate);
}

The template used for the pushpins simply renders the image thumbnail:

<DataTemplate x:Key="MarkerTemplate">
  <Border BorderBrush="White" BorderThickness="1">
    <Image Source="{Binding Picture, Converter={StaticResource PictureThumbnailConverter}}"
            Width="80" Height="80"/>
  </Border>
</DataTemplate>

This makes use of a simple value converter which takes a Picture instance and converts it into a BitmapImage which is used as the Source for the image:

public class PictureThumbnailConverter : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  {
    Picture picture = value as Picture;
    BitmapImage src = new BitmapImage();
    src.SetSource(picture.GetThumbnail());
    return src;
  }
 
  public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  {
    return null;
  }
}

The puhspin clusterer allows you to specify a separate template for clustered pushpins. The DataContext for this template is a list of the DataContexts of the clustered pins that it represents. For this application I created a template which renders what looks like a ‘stack’ of images. The number of pictures in the cluster is rendered as a TextBlock and the last image in the cluster rendered.

<DataTemplate x:Key="ClusterTemplate">
  <Grid Width="75" Height="75">
    <Canvas>
      <Border Style="{StaticResource FakePhoto}"
              Canvas.Left="0" Canvas.Top="0"/>
      <Border Style="{StaticResource FakePhoto}"
              Canvas.Left="5" Canvas.Top="5"/>
      <Border BorderBrush="White" BorderThickness="1"
              Canvas.Left="10" Canvas.Top="10"
              DataContext="{Binding Path=., Converter={StaticResource LastConverter}}">
        <Image Source="{Binding Picture, Converter={StaticResource PictureThumbnailConverter}}"
                Width="60" Height="60"/>
      </Border>
      <TextBlock Text="{Binding Count}"
                  Opacity="0.5"
                  Canvas.Left="25" Canvas.Top="15"
                  FontSize="35"/>
    </Canvas>
  </Grid>      
</DataTemplate>
 
<Style TargetType="Border" x:Key="FakePhoto">
  <Setter Property="Width" Value="60"/>
  <Setter Property="Height" Value="60"/>
  <Setter Property="BorderBrush" Value="White"/>
  <Setter Property="Background" Value="Black"/>
  <Setter Property="BorderThickness" Value="1"/>
</Style>

The code that renders the last image is a bit cunning, it uses a value converter that performs a Linq style ‘last’ operations, extracting the last items from a collection of objects:

public class LastConverter : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  {
    IList enumerable = value as IList;
    return enumerable.Cast<object>().Last();
  }
 
  public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  {
    return null;
  }
}

This feels quite neat to me :-)

The clustered pins look like the following, which is a cluster of 5 images around Paris, with the stunning La Grande Arche de la Défense as the image at the top of the cluster:

Despite its simplicity, I have had a lot of fun playing with this application. It has certainly encouraged me to take as many photos as possible whenever I go travelling.

You can download the full sourcecode here: PhotoBrowser.zip

Regards, Colin E.

Pushpin Clustering with the Windows Phone 7 Bing Map control

November 21st, 2011

This blog post provides a simple utility class that will cluster pushpins on a Bing Map control. This utility provides a way to achieve great performance with 1000s of pushpins.

The Bing Map control for Windows Phone 7 is a versatile control, allowing you to provide your users with an interactive map. The control has some very useful features such as Pushpins, which you can anchor to a map coordinate so that they move automatically as the user pans / zooms the map. However, I have found that in practice, if you have more than ~30 pushpins visible on a map, the pan / zoom performance starts to degrade (on a real device). Therefore, in order to provide the best user-experience, it is advisable to only render a handful of pushpins on the map at any one time.

This blog post describes a simple approach to clustering pushpins, as shown in the video below, which renders the location of ~500 juggling clubs worldwide:

The clustering code is very easy to use; just create a PushpinClusterer instance, pass your Pushpins, the map and the template to use for the clusters marker:

var clusterer = new PushpinClusterer(map, pins,
        this.Resources["ClusterTemplate"] as DataTemplate);

And that’s it!

The Implementation

Clustering of map markers is a common problem. Searching the internet I found a number of blog posts describing techniques for clustering Google Maps markers, one such blog post described a simple algorithm which clusters points that are within a fixed pixel distance from each other. I decided to take this algorithm and apply it to the Silverlight Bing Maps control.

The clusterer is a pretty simple class, whenever the map view changes (which occurs after pan or zoom), the clusterer iterates over all the pins, any that are closer than 50 pixels to each other, are merged together.

Once the pins have been clustered, only those which would currently be visible (based on the map viewport) are added to the map:

/// <summary>
/// Clusters the given pins on the supplied map
/// </summary>
public PushpinClusterer(Map map, List<Pushpin> pins, DataTemplate clusterTemplate)
{
  _map = map;
  _pins = pins;
  ClusterTemplate = clusterTemplate;
 
  _map.ViewChangeEnd += (s, e) => RenderPins();
}
 
 
/// <summary>
/// Re-render the pushpins based on the current zoom level
/// </summary>
private void RenderPins()
{
  List<PushpinContainer> pinsToAdd = new List<PushpinContainer>();
 
  // consider each pin in turn
  foreach (var pin in _pins)
  {
    var newPinContainer = new PushpinContainer(pin,
      _map.LocationToViewportPoint(pin.Location));
 
    bool addNewPin = true;
 
    // determine how close they are to existing pins
    foreach(var pinContainer in pinsToAdd)
    {
      double distance = ComputeDistance(pinContainer.ScreenLocation, newPinContainer.ScreenLocation);
 
      // if the distance threshold is exceeded, do not add this pin, instead
      // add it to a cluster
      if (distance < DistanceThreshold)
      {
        pinContainer.Merge(newPinContainer);            
        addNewPin = false;
        break;
      }
    }
 
    if (addNewPin)
    {
      pinsToAdd.Add(newPinContainer);
    }
  }
 
  // asynchronously update the map
  _map.Dispatcher.BeginInvoke(() =>
    {
      _map.Children.Clear();
      foreach (var projectedPin in pinsToAdd.Where(pin => PointIsVisibleInMap(pin.ScreenLocation, _map)))
      {
        _map.Children.Add(projectedPin.GetElement(ClusterTemplate));
      }
    });
 
}
 
/// <summary>
/// Gets whether the given point is within the map bounds
/// </summary>
private static bool PointIsVisibleInMap(Point point, Map map)
{
  return point.X > 0 && point.X < map.ActualWidth &&
          point.Y > 0 && point.Y < map.ActualHeight;
}
 
/// <summary>
/// Computes the cartesian distance between points
/// </summary>
private double ComputeDistance(Point p1, Point p2)
{
  return Math.Sqrt((p1.X - p2.X) * (p1.X - p2.X) + (p1.Y - p2.Y) * (p1.Y - p2.Y));
}

The PushpinContainer is a class that holds a Pushpin, however, if one or more additional Pushpins are added to it, it will become a cluster. The class is given in full below:

/// <summary>
/// A container for one or more pushpins at a given screen coordinate.
/// </summary>
public class PushpinContainer
{
  private List<Pushpin> _pushpins = new List<Pushpin>();
 
  /// <summary>
  /// Creates a container for the given pushpin
  /// </summary>
  public PushpinContainer(Pushpin pushpin, Point location)
  {
    _pushpins.Add(pushpin);
    ScreenLocation = location;
  }
 
  /// <summary>
  /// Adds the pins from the given container
  /// </summary>
  public void Merge(PushpinContainer pinContainer)
  {
    foreach (var pin in pinContainer._pushpins)
    {
      _pushpins.Add(pin);
    }
  }
 
  /// <summary>
  /// Gets or sets the current screen location of this container
  /// </summary>
  public Point ScreenLocation { get; private set; }
 
  /// <summary>
  /// Gets the visual representation of the contents of this container. If it is 
  /// a single pushpin, the pushpin itself is returned. If multiple pushpins are present
  /// a pushpin with the given clusterTemplate is returned.
  /// </summary>
  public FrameworkElement GetElement(DataTemplate clusterTemplate)
  {
    if (_pushpins.Count == 1)
    {
      return _pushpins[0];
    }
    else
    {
      return new Pushpin()
      {
        Location = _pushpins.First().Location,
        Content = _pushpins.Select(pin => pin.DataContext).ToList(),
        ContentTemplate = clusterTemplate,
        Background = new SolidColorBrush(Colors.Red)
      };
    }
  }    
}

The GetElement method will either return the Pushpin, if it has not been clustered, or a new Pushpin with the ClusterTemplate applied. Note, the Content property of the clustered pin is a list of the DataContext properties of all the pins it ‘contains’.

The example application, which renders the location of ~500 juggling clubs, uses a very simple cluster template which simply indicates the number of points that have been clustered:

<DataTemplate x:Key="ClusterTemplate">
  <TextBlock Text="{Binding Count}"/>
</DataTemplate>

However, I am sure that with a bit of creativity, a more interesting template could be created!

Finally, the example application handles left mouse-clicks on the map, inspecting the DataContext of the clicked element in order to render the juggling club or cluster of juggling clubs which were clicked upon:

private void Map_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
  var fe = e.OriginalSource as FrameworkElement;
  if (fe.DataContext is string)
  {
    itemList.ItemsSource = new List<string>() { (string)fe.DataContext };
  }
 
  if (fe.DataContext is IEnumerable<object>)
  {
    itemList.ItemsSource = (fe.DataContext as IEnumerable<object>).Cast<string>();
  }
}

Let me know if you find this code useful, or you apply it in your application.

You can download the full sourcecode here: MarkerClustering.zip

Regards, Colin E.

Google Sky on Windows Phone 7

February 21st, 2011

This blog post shows just how easy it is to use Google Sky as a tile source for Bing Maps, bringing the universe to Windows Phone 7!

Personally I think mapping is one of the most exciting forms of application for mobile devices – the fantastic imagery available from Bing and Google maps, coupled with GPS technology, results in some pretty amazing tools. As a Windows Phone 7 developer, I have spent a fair bit of time poking round the Silverlight Bing Maps APIs. A really cool feature of the Bing Maps control is that it accepts custom tile-sources. You can find code elsewhere that explains how to use this to render Google Maps data via a Bing maps chart control. For a bit of fun I decided to use this approach to render Google Sky on WP7 …

Finding the correct URL format is as easy as opening up firebug and looking at the HTTP traffic when using Google Sky:

With this knowledge, creating a custom tile source for Bing maps is pretty trivial

public class GoogleTile : Microsoft.Phone.Controls.Maps.TileSource
{
    public GoogleTile()
    {
        UriFormat = @"http://mw1.google.com/mw-planetary/sky/skytiles_v1/{0}_{1}_{2}.jpg";
    }
 
    public override Uri GetUri(int x, int y, int zoomLevel)
    {
        if (zoomLevel > 0)
        {
            var Url = string.Format(UriFormat, x, y, zoomLevel);
            return new Uri(Url);
        }
        return null;
    }
}

And associating your tile source with a map control is as simple as this …

<map:Map Name="map"
    CopyrightVisibility="Collapsed" LogoVisibility="Collapsed" ScaleVisibility="Collapsed"
    ZoomLevel="2"
    CredentialsProvider="-- YOUR API KEY GOES HERE!!! ---">
    <map:Map.Mode>
        <mapCore:MercatorMode/>
    </map:Map.Mode>
    <map:MapTileLayer>
        <map:MapTileLayer.TileSources>
            <local:GoogleTile/>
        </map:MapTileLayer.TileSources>
    </map:MapTileLayer>
</map:Map>

When exploring the sky, funnily enough, it becomes pretty obvious that most of it is black! In order to make this a more interesting application I added a little ‘menu’ across the bottom that allows you to select from the list of 110 Messier Objects, a catalogue of interesting astronomical objects that are not comets. The thumbnail images for each object were scraped from Wikipedia, and the coordinates retrieved from the SIMBAD astronomical database via a little C# command line app.

They are rendered in the UI via an ItemsControl:

<ItemsControl x:Name="MessierObjects"
                Grid.Row="2">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Grid Width="100" Height="100"
                    MouseLeftButtonUp="Grid_MouseLeftButtonUp">
                <Image Source="{Binding Path=ThumbSource}"
                        Stretch="Fill" Margin="5"/>
                <TextBlock Text="{Binding Path=Name}"
                            Margin="5"
                            FontSize="25" FontWeight="Bold"
                            Foreground="LightGray"
                            Opacity="0.5"/>
            </Grid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.Template>
        <ControlTemplate>
            <ScrollViewer HorizontalScrollBarVisibility="Visible">
                <ItemsPresenter/>
            </ScrollViewer>
        </ControlTemplate>
    </ItemsControl.Template>
</ItemsControl>

With a click handler that navigates the map control to the correct location:

private void Grid_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    var messier = ((FrameworkElement)sender).DataContext as MessierObject;
 
    map.SetView(messier.Coords, 11);
}

I would love to build this application further, however, usage of Google Maps tiles outside of their service is a violation of their terms and conditions. For that reason, this code is shown just for a bit of fun, it shows easy it is to bring together two different technologies, Bing Maps and the Google Sky tile imagery, to create something cool with very few lines of code.

As an aside, I would have liked to have used the data from the less well know Microsoft WorldWide Telescope, which is accessible through Bing Maps, but just couldn’t work out the tile URL format. This is a shame, I would be happier building this application further based on a Microsoft datasource.

You can download the sourcecode here: GoogleSkyWP7.zip

Regards,
Colin E.