Photo location service (and Bing)

Following up on this morning’s post, I’ve expanded my map script to include an option for using Bing Maps to display the location of a photograph, and I’ve created a Service so I can get a map by right-clicking on a photo in the Finder and choosing “Photo location” from the contextual menu.

Here’s the updated map script:

python:
 1:  #!/usr/bin/python
 2:  
 3:  import pyexiv2
 4:  import sys
 5:  import subprocess
 6:  import getopt
 7:  
 8:  usage = """Usage: map [option] file
 9:  
10:  Options:
11:    -b    use Bing instead of Google Maps
12:    -h    show this help message
13:  
14:  Get the GPS data from the given image file and open a map to that
15:  location in the user's default browser."""
16:  
17:  # Parse the options.
18:  try:
19:    options, photos = getopt.getopt(sys.argv[1:], 'bh')
20:  except getopt.GetoptError, err:
21:    print str(err)
22:    sys.exit(2)
23:  
24:  # Set the option values.
25:  engine = 'google'           # default
26:  for o, a in options:
27:    if o == '-b':
28:      engine = 'bing'
29:    else:
30:      print usage
31:      sys.exit()
32:  
33:  try:
34:    # Open the photo file.
35:    md = pyexiv2.ImageMetadata(photos[0])
36:    md.read()
37:  except:
38:    print usage
39:    sys.exit()
40:  
41:  try:
42:    # Read the GPS info.
43:    latref = md['Exif.GPSInfo.GPSLatitudeRef'].value
44:    lat = md['Exif.GPSInfo.GPSLatitude'].value
45:    lonref = md['Exif.GPSInfo.GPSLongitudeRef'].value
46:    lon = md['Exif.GPSInfo.GPSLongitude'].value
47:  except KeyError:
48:    print "No GPS data for %s" % photos[0]
49:    sys.exit(1)
50:  
51:  # Convert the latitude and longitude to signed floating point values.
52:  latitude = float(lat[0]) + float(lat[1])/60 + float(lat[2])/3600
53:  longitude = float(lon[0]) + float(lon[1])/60 + float(lon[2])/3600
54:  if latref == 'S': latitude = -latitude
55:  if lonref == 'W': longitude = -longitude
56:  
57:  # Construct the Google Maps or Bing Maps query and open it.
58:  if engine == 'bing':
59:    query = "http://www.bing.com/maps/?v=2&where1=%.6f,%.6f" % (latitude, longitude)
60:  else:
61:    query = "http://maps.google.com/maps?q=loc:%.6f,%.6f" % (latitude, longitude)
62:  subprocess.call(['open', query])

It’s quite a bit longer than before, mostly because of the option handling code in Lines 17-31 and the usage message in Lines 8-15. I’ve also separated the error handling into two try/except clauses, one for dealing with errors in opening the image file and the other for handling files with no location. (Note also that Line 49 has map return a nonzero result code if there’s no GPS info. We’ll use that in the service.)

Otherwise, it’s basically the same script. Use

map photo.jpg

to open a Google Maps view of the photo’s location in your default browser. Use

map -b photo.jpg

to open the location in Bing Maps instead.

The “Photo location” service is defined this way in Automator:

Photo location service

It’s defined only for image files selected in the Finder and runs the following shell script:

bash:
1:  for f in "$@"
2:  do
3:    ~/bin/map "$f"
4:    if [ $? -ne 0 ]; then
5:     say "No location"
6:    fi
7:  done

For every selected image file, it runs the map program. If the map command in Line 3 returns a nonzero result code, $?—which it will if the image file has no location metadata—the script will say “No location” in the user’s default voice. If you want to use this service, but find the talking a bit too cute, you can change Line 5 to

bash:
5:     osascript -e "beep 1"

and it will sound the system alert if the file has no GPS data. If you’d rather use Bing, slip a -b after the map in Line 3. Or you could make two services, one that uses Google and one that uses Bing.

With these improvements, I think I’m done. I have a tool that works the way I want on my computers. I could rewrite map to use an EXIF library that’s easier to install than pyexiv2, possibly switching to Perl or Ruby in the process, but since I managed to get pyexiv2 installed on my machines, I’d get no benefit out of doing so. But if someone else takes this code and rewrites if for another library, I’d love to hear about it.