Navigation

Friday 12 April 2013

Automatically extracting GPS data from photos and populating Geolocation fields in SharePoint 2013

This has been a bit of a pet project of mine, ever since my wife spent 6 months working in the highlands of Scotland. She was taking a lot of photos and wanted to track the location easily (for a project). Now most modern mobile phones (and most high end DSLR cameras) can do this quite easily, but the problem has always been getting that location information out again .. so I thought:
"Hey, now SharePoint has built-in Geo-Location support with mapping! Wouldn't it be great if we could extract the GPS data from photos automatically and map it from an asset library?"And this is how I did it ..
SharePoint Geolocation and Bing Maps
Well .. first off I wonder how many of you out there knew that SharePoint could do this out of the box:
Pop-Up maps when you click on a Geolocation field in a SharePoint List View
Adding a Geolocation field enables a new "Map View" which shows you all the "pins" and associated information
That is completely "out of the box" mapping capabilities (not a single line of code). All you need to do is:
  • Create a Geolocation field (you won't be able to do this through the UI. You will need to create a field of type "Geolocation" either programmatically or through PowerShell)
  • Specify a Bing Maps API Key for your farm or site through PowerShell
  • Install the SQL CLR Types package on your Web Front End servers (as this is needed to use the new SQL Server "geo-spacial" data types)
I'm not going to repeat all of the information in this blog post as this is already fairly well documented. You can find full instructions on how to configure this on TechNet here:
http://msdn.microsoft.com/en-us/library/jj163135.aspx 
There is also some pretty good blog content by Rich Ross (@rich_ross) and Tobias Zimmergren (@zimmergren):
http://richross.me/2012/12/22/sharepoint-2013-geolocation/
http://zimmergren.net/technical/sp-2013-getting-started-with-the-new-geolocation-field-in-sharepoint-2013 

Photos, GPS and EXIF Information
Now, if you want to start playing about with GPS and Photos then you need to at least know about EXIF. This is basically the widely adopted standard for storing additional information in photos (such as the date and location it was taken, as well as more photography specific info like the lense aperture, shutter speed and camera make / model which was used). All of this can be found in quite a lot of detail in the EXIF specification (if you are having trouble sleeping).
You can actually see this in action if you look at the properties of an image file which you know has GPS information in it (say, one from your mobile phone).
EXIF and Lat / Long information as seen from a file properties dialog
Now two things should jump out at your here (especially as I've put a big red box around them both!). First is the EXIF version which as used (which shouldn't be of a massive interest to anyone, but at least shows you it has EXIF information).

Secondly is the latitute and longitude values which we need for our GPS coordinates. What might strike you as slightly odd is the format, as they are actually made up of three numbers in the format:
Degrees; Minutes; Seconds
GPS coordinates used by Bing / Google maps (and also in SharePoint Geolocation fields) need to use "decimal-degrees" which you can get quite easily from the formula:
Degrees + (Minutes / 60) + (Seconds / 3600)
In order to actually get these values out and leverage this in code then you will need a library which can extract and parse this information. Serious credit needs to go to the codeplex project: ExifLib library by Scott McKenzie. I used this extensively in my code and it worked a charm!

The usage of this library is pretty straightforward. First you load the image file as a .NET Stream object into the constructor.

ExifLib.ExifReader reader = new ExifLib.ExifReader(stream);

Once you've done that you use a generic method to spit out the EXIF tags using an enum and an out variable:

double[] latitude = new double[3];
double[] longitude = new double[3];
reader.GetTagValue<double[]>(ExifLib.ExifTags.GPSLatitude, out latitude);
reader.GetTagValue<double[]>(ExifLib.ExifTags.GPSLongitude, out longitude);

Once Now the sharp among you may have already spotted that the format is a little odd, that we are pulling out a 3-element array of double values.

This is because the EXIF information stores it in this format (the Degrees, Minutes, Seconds values) so we then need a simple method to convert them to "Decimal Degrees".

static Double ConvertDegreesToDecimalDegrees(Double[] degrees)
{
   if (degrees.Length == 3)  
   {                  
      Double returnValue = degrees[0] + 
         (degrees[1] / 60) + 
         (degrees[2] / 3600);    
      return returnValue;   
   }              
   else  
   {
      throw new ArgumentException("The degrees array must contain 3 different values for Degrees, Minutes and Seconds"); 
   }
} 
Once we have this method, it is pretty easy to convert the values over to a single double.
 
double decimalLatitude = ConvertDegreesToDecimalDegrees(latitude);
double decimalLongitude = ConvertDegreesToDecimalDegrees(longitude);

However, this is not all, because imperial (degrees, minutes, seconds) values also include another nuance:
  • Longitude measures the distance from the GMT vertical line
  • Latitude measures the distance from the Equator
For "normal" metric GPS coordinates to work we need to do the following:
  • Any longitude value which is WEST of the GMT line should be negative
  • Any latitude value which is SOUTH of the equator should be negative
And we can work that out by using two other EXIF values known as "Longitude Ref" (which is either "W" or "E") and "Latitude Ref" (which is either "N" or "S"). This basically gives us our compass points. So we need a final check to make sure our values are correctly either positive or negative.

string latitudeRef = String.Empty;
string longitudeRef = String.Empty;

reader.GetTagValue<string>(ExifLib.ExifTags.GPSLongitudeRef, out longitudeRef);
reader.GetTagValue<string>(ExifLib.ExifTags.GPSLatitudeRef, out latitudeRef);

if (latitudeRef == "S")
{  
  // it is "south" therefore needs to be a negative number
  decimalLatitude = 0 - decimalLatitude;
}

if (longitudeRef == "W")
{
  // it is "West" therefore needs to be a negative number
  decimalLongitude = 0 - decimalLongitude;
} 

Finally, once we have our values we can use the new SPFieldGeolocationValue class to assign it to a list item in SharePoint (in this case assigning it to a Geolocation field I created with an internal name of "Location")

// update the location field
 properties.ListItem["Location"] = 
   new SPFieldGeolocationValue(decimalLatitude, decimalLongitude);

// update the list item without affecting the modified date / editor
 properties.ListItem.SystemUpdate(false);

Bringing this all together in SharePoint
To be honest, for most SharePoint developers out there the rest should be pretty smooth sailing, but I'll show you the event receiver code below.

public class ItemAddedReceiver : SPItemEventReceiver
    {
        public override void ItemUpdated(SPItemEventProperties properties)
        {
            if (properties.ListItem.File.Exists &&
                properties.ListItem.Fields.ContainsField("Location"))
            {
                byte[] fileBytes = properties.ListItem.File.OpenBinary();

                MemoryStream stream = new MemoryStream(fileBytes);

                try
                {
                    ExifLib.ExifReader reader = new ExifLib.ExifReader(stream);

                    // we need 3-value double arrays
                    // the values are Degrees | Minutes | Seconds
                    double[] latitude = new double[3];
                    double[] longitude = new double[3];

                    // The Longitude and Latitude References tell us
                    // whether it is North or South of the Equator (Latitude) or
                    // whether it is West or East of Greenwich (Longitude)
                    // this will be a single character string representing
                    // the compass points ("N", "E", "S", "W")
                    string latitudeRef = String.Empty;
                    string longitudeRef = String.Empty;
                    
                    // try to retrieve values using the ExifLib library
                    // if the image doesn't find one of the values then this boolean 
                    // variable will be false
                    bool gotValue = reader.GetTagValue<double[]>(ExifLib.ExifTags.GPSLatitude, out latitude)
                        && reader.GetTagValue<double[]>(ExifLib.ExifTags.GPSLongitude, out longitude)
                        && reader.GetTagValue<string>(ExifLib.ExifTags.GPSLongitudeRef, out longitudeRef)
                        && reader.GetTagValue<string>(ExifLib.ExifTags.GPSLatitudeRef, out latitudeRef);
                                        

                    if (gotValue)
                    {
                        // convert degrees / minutes / seconds to decimal degrees
                        double decimalLatitude = ConvertDegreesToDecimalDegrees(latitude);
                        if (latitudeRef == "S")
                        {
                            // it is "south" therefore needs to be a negative number
                            decimalLatitude = 0 - decimalLatitude;
                        }
                        
                        double decimalLongitude = ConvertDegreesToDecimalDegrees(longitude);
                        if (longitudeRef == "W")
                        {
                            // it is "West" therefore needs to be a negative number
                            decimalLongitude = 0 - decimalLongitude;
                        }

                        // update the location field
                        properties.ListItem["Location"] = new SPFieldGeolocationValue(decimalLatitude, decimalLongitude);
                        properties.ListItem.SystemUpdate(false);
                    }
                }
                catch { }
            }
        }

        /// <summary>
        /// Converts degrees to a decimal-degree value
        /// Expects a Double array with 3 values (one for each of degrees / minutes / seconds)
        /// </summary>
        /// <param name="degrees">The Double[3] containing the original values</param>
        /// <returns>The new decimal degree value</returns>
        static Double ConvertDegreesToDecimalDegrees(Double[] degrees)
        {
            if (degrees.Length == 3)
            {
                Double returnValue = degrees[0] + (degrees[1] / 60) + (degrees[2] / 3600);
                return returnValue;
            }
            else
            {
                throw new ArgumentException("The degrees array must contain 3 different values for Degrees, Minutes and Seconds");
            }
        }
    }

I have also uploaded a full code sample (including a working version of the ExifLib.dll) which does the following:
  • Creates a Location site column and an Asset Library list instance
  • Event Receiver which fires on ItemAdded to extract the GPS EXIF info and place it into the Geolocation field.
  • Feature Receiver to add the Geolocation field and attach the event receiver to the asset library.


You can download the package here: http://sdrv.ms/122g8Cs

Hope you've enjoyed this post, and equally hope you can make use of the code samples! Enjoy.