November 25th, 2013

Extracting GPS coordinates from a photo and plotting it on a map

Today’s Little Program extracts GPS coordinates from a photo and plots it on a map. Remember, Little Programs do little to no error checking, because that’s how they roll.

#define STRICT
#define UNICODE
#define _UNICODE
#include <windows.h>
#include <shlobj.h>
#include <shellapi.h>
#include <propidl.h>
#include <propkey.h>
#include <propvarutil.h>
#include <atlbase.h>
#include <atlalloc.h>
#include <strsafe.h>
void OpenMap(double dblLatitude, double dblLongitude)
{
 wchar_t szUrl[1024];
 StringCchPrintf(szUrl, ARRAYSIZE(szUrl),
  L"http://www.bing.com/maps/default.aspx?v=2&q=%f,%f",
  dblLatitude, dblLongitude);
 ShellExecute(nullptr, nullptr, szUrl, nullptr, nullptr, SW_NORMAL);
}

We start with a simple function that takes a latitude and longitude and opens a Web page that highlights that coordinate. In a real program, you probably would do something more interesting with the coordinates, but I’m opening a Web page just to do something.

class CPropVariant : public PROPVARIANT {
public:
 CPropVariant() { PropVariantInit(this); }
 ~CPropVariant() { PropVariantClear(this); }
};

The CProp­Variant class is an incredibly lame wrapper around PROP­VARIANT for RAII purposes.

HRESULT GetGPSCoordinateAsDecimal(
    IShellItem2 *psi2,
    REFPROPERTYKEY pkey,
    REFPROPERTYKEY pkeyRef,
    double *pdbl)
{
 CPropVariant spvar;
 HRESULT hr = psi2->GetProperty(pkey, &spvar);
 if (FAILED(hr)) return hr;
 double rgdbl[3];
 ULONG cElt;
 hr = PropVariantToDoubleVector(spvar, rgdbl, 3, &cElt);
 if (FAILED(hr)) return hr;
 if (cElt != 3) return E_INVALIDARG;
 double coord = rgdbl[0] + rgdbl[1] / 60.0 + rgdbl[2] / 60.0 / 60.0;
 CComHeapPtr<wchar_t> spszDir;
 hr = psi2->GetString(pkeyRef, &spszDir);
 if (FAILED(hr)) return hr;
 if (spszDir[0] == L'W' || spszDir[0] == L'S') coord = -coord;
 *pdbl = coord;
 return S_OK;
}

The Get­GPS­Coordinate­As­Decimal function is where the real work happens. GPS latitude and longitude are encoded in the shell property system as a bunch of related properties.

Property Type Meaning
System.GPS.DestLatitudeNumerator UINT[3] numerators for degrees, minutes, and seconds of latitude
System.GPS.DestLatitudeDenominator UINT[3] denominators for degrees, minutes, and seconds of latitude
System.GPS.DestLatitude double[3] degrees, minutes, and seconds of latitude (numerator ÷ denominator)
System.GPS.DestLatitudeRef string “N” or “S”
System.GPS.DestLongitudeNumerator UINT[3] numerators for degrees, minutes, and seconds of longitude
System.GPS.DestLongitudeDenominator UINT[3] denominators for degrees, minutes, and seconds of longitude
System.GPS.DestLongitude double[3] degrees, minutes, and seconds of Longitude (numerator ÷ denominator)
System.GPS.DestLongitudeRef string “E” or “W”

Each of the coordinates is recorded in DMS form as pairs of unsigned integers (numerator and denominator). The direction is recorded as a string as a separate property. Why this wacky format? Probably because that’s the way EXIF records it.

For convenience, there is a combo property which does the division for you (but frustratingly, does not flip the sign for direction). And if you want the coordinates in decimal form, then you’ll have to do the DMS-to-decimal conversion yourself.

We start by getting the DMS value as a PROP­VARIANT then converting it to an array of doubles. (There had better be three of them.) We then use the power of mathematics to convert from DMS to decimal degrees.

Finally, we flip the sign if the direction from center is West or South.

Now it’s time to put these functions together.

int __cdecl wmain(int argc, wchar_t **argv)
{
 if (argc < 2) return 0;
 CCoInitialize init;
 CComPtr<IShellItem2> spsi2;
 if (FAILED(SHCreateItemFromParsingName(argv[1],
              nullptr, IID_PPV_ARGS(&spsi2)))) return 0;
 double dblLong, dblLat;
 if (FAILED(GetGPSCoordinateAsDecimal(spsi2, PKEY_GPS_Longitude,
                    PKEY_GPS_LongitudeRef, &dblLong))) return 0;
 if (FAILED(GetGPSCoordinateAsDecimal(spsi2, PKEY_GPS_Latitude,
                    PKEY_GPS_LatitudeRef, &dblLat))) return 0;
 OpenMap(dblLong, dblLat);
 return 0;
}

Find a photo with GPS information encoded inside it and pass it on the command line as a fully-qualified path. (Because I’m too lazy to call Get­Full­Path­Name.) The program should open a Web page that shows where the picture was taken.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

0 comments

Discussion are closed.