Setting my coordinate

Too many posts of my retuning of old scripts. This is another one, but I’ll try to make it the last one for a while.

Today I was going to use my coordinate script to set the location of several dozen photos I took with a standard, non-GPS-equipped camera. I’d taken a photo in the same location with my iPhone, and I intended to use the command

coordinate -g iphone.jpg IMG*

to take the GPS location embedded in the iPhone photo and put it in the other photos. In fact, the only reason I took the iPhone photo was to use it to extract the GPS info. Well, this scheme went agley. Apparently, I took the iPhone photo before its GPS had figured out where it was, so the coordinates in it were useless.

I shifted to my backup plan: get the location from Google Maps and use the less automated form of coordinate, where I include the latitude and longitude explicitly on the command line. Unfortunately, I had written coordinate to expect the input in format, not the decimal degrees format that Google gives you when you use its “Drop LatLng Marker” feature.1

LatLng marker

It’s not the hardest thing in the world to convert from decimal degrees to degrees, minutes, and seconds, but it seemed like something coordinate should do for me.

Now it does. Here’s the new code:

  1:  #!/usr/bin/env python
  3:  import pyexiv2
  4:  import getopt
  5:  import sys
  6:  from fractions import Fraction
  7:  from functools import partial
  8:  from math import modf
 10:  usage = """Usage: coordinate [options] [files]
 12:  Options:
 13:    -g filename    photo file with GPS coordinates
 14:    -n angle       north coordinate
 15:    -s angle       south coordinate
 16:    -e angle       east coordinate
 17:    -w angle       west coordinate
 18:    -h             show this help message
 20:  Add location metadata to each of the listed files. The location
 21:  can come from either the photo associated with the -g option or
 22:  with a -n, -s, -e, -w pair given on the command line. The angles
 23:  can be in either d.dddddd or dd:mm:ss.sss format."""
 25:  # Functions for manipulating coordinates.
 26:  def makecoord(coordstring):
 27:    """Make a coordinate list from a coordinate string.
 29:    The string can be either "d.ddddd" or "", and the
 30:    list comprises three Fractions."""
 32:    if ':' not in coordstring:
 33:      theta = float(coordstring)
 34:      m, d = modf(theta)
 35:      s, m = modf(m*60)
 36:      s = s*60
 37:      angle = [ str(x) for x in [d, m, s] ]
 38:    else:
 39:      angle = coordstring.split(':', 2)
 41:    loc = [ Fraction(x).limit_denominator(1000) for x in angle ]
 42:    return loc
 44:  def setcoord(metadata, direction, coordinate):
 45:    """Set the latitude or longitude coordinate.
 47:    Latitude is set if direction is 'N' or 'S', longitude if 'E' or 'W'.
 48:    The coordinate is a list of the form [dd, mm, ss], where the degrees,
 49:    minutes, and seconds are Fractions."""
 51:    tags = {'lat': ('Exif.GPSInfo.GPSLatitudeRef', 'Exif.GPSInfo.GPSLatitude'),
 52:            'lon': ('Exif.GPSInfo.GPSLongitudeRef', 'Exif.GPSInfo.GPSLongitude')}
 53:    if direction in ('N', 'S'):
 54:      coord = 'lat'
 55:    else:
 56:      coord = 'lon'
 57:    metadata[tags[coord][0]] = direction
 58:    metadata[tags[coord][1]] = coordinate
 61:  # Get the command line options.
 62:  try:
 63:    options, filenames = getopt.getopt(sys.argv[1:], 'g:n:s:e:w:h')
 64:  except getopt.GetoptError, err:
 65:    print str(err)
 66:    sys.exit(2)
 68:  # Set the option values.
 69:  gpsphoto = north = south = east = west = False       # defaults
 70:  for o, a in options:
 71:    if o == '-g':
 72:      gpsphoto = a
 73:    elif o == '-n':
 74:      north = makecoord(a)
 75:    elif o == '-s':
 76:      south = makecoord(a)
 77:    elif o == '-e':
 78:      east = makecoord(a)
 79:    elif o == '-w':
 80:      west = makecoord(a)
 81:    else:
 82:      print usage
 83:      sys.exit()
 86:  # Valid option combinations.
 87:  ne = (north and east) and not (south or west or gpsphoto)
 88:  nw = (north and west) and not (south or east or gpsphoto)
 89:  se = (south and east) and not (north or west or gpsphoto)
 90:  sw = (south and west) and not (north or east or gpsphoto)
 91:  gps = gpsphoto and not (north or south or east or west)
 93:  if not (ne or nw or se or sw or gps):
 94:    print "invalid location"
 95:    sys.exit()
 98:  # Create the coordinate setter functions.
 99:  if ne:
100:    setlat = partial(setcoord, direction='N', coordinate=north)
101:    setlon = partial(setcoord, direction='E', coordinate=east)
102:  elif nw:
103:    setlat = partial(setcoord, direction='N', coordinate=north)
104:    setlon = partial(setcoord, direction='W', coordinate=west)
105:  elif se:
106:    setlat = partial(setcoord, direction='S', coordinate=south)
107:    setlon = partial(setcoord, direction='E', coordinate=east)
108:  elif sw:
109:    setlat = partial(setcoord, direction='S', coordinate=south)
110:    setlon = partial(setcoord, direction='W', coordinate=west)
111:  elif gps:
112:    basemd = pyexiv2.ImageMetadata(gpsphoto)
114:    latref = basemd['Exif.GPSInfo.GPSLatitudeRef']
115:    lat = basemd['Exif.GPSInfo.GPSLatitude']
116:    lonref = basemd['Exif.GPSInfo.GPSLongitudeRef']
117:    lon = basemd['Exif.GPSInfo.GPSLongitude']
118:    setlat = partial(setcoord, direction=latref.value, coordinate=lat.value)
119:    setlon = partial(setcoord, direction=lonref.value, coordinate=lon.value)
120:  else:
121:    print "coordinate setter failed"
122:    sys.exit()
124:  # Cycle through the files.
125:  for f in filenames:
126:    md = pyexiv2.ImageMetadata(f)
128:    setlat(md)
129:    setlon(md)
130:    md.write()

The new parts are in the makecoord function, Lines 26-42. If the coordinate given on the command line doesn’t include a colon, it’s assumed to be in decimal degrees, and Lines 33-37 turn it into a three-item list of strings appropriate for the conversion to Fractions that occurs on Line 41. If the coordinate does include a colon, it’s treated as before on Line 39.

I used a new (to me) function: modf from the math library. It takes a floating point number as its argument and returns the fractional and integer parts as a tuple. Strangely, the fractional part comes before the integer part in the tuple. I assumed it was the other way around when I first wrote Lines 34-35, which led to some surprising results.

Now I can copy the coordinates from a LatLng marker and, with just a little editing on the command line, use them directly in the coordinate command:

coordinate -n 41.87658 -w 87.61912 *.jpg

I should have written the script this way in the first place.

  1. You know about that feature, right? You put the mouse pointer where you want the coordinates and right-click. A popup of options appears, one of which is “Drop LatLng Marker.” The result is the little flag you see in the screenshot.

    Update As Sven says in the comments, this isn’t a standard feature. You need to enable it in the Maps Labs settings (accessed through the gear icon in the upper right corner of the screen when you’re visiting Google Maps).