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 ofGergely Orosz waiting for his turn in the Edinburgh Marathon Relay. | And finally, some pictures I took whilst running aroundKielder 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:
Collapse |
Copy Code
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:
Collapse |
Copy Code
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:
Collapse |
Copy Code
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.
Collapse |
Copy Code
private void AddPushpins()
{
List<Pushpin> pushPins = new List<Pushpin>();
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);
}
var clusterer = new PushpinClusterer(map, pushPins, this.Resources["ClusterTemplate"] as DataTemplate);
}
The template used for the pushpins simply renders the image thumbnail:
Collapse |
Copy Code
<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:
Collapse |
Copy Code
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 aTextBlock
and the last image in the cluster rendered.
Collapse |
Copy Code
<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:
Collapse |
Copy Code
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.
License