#!/usr/bin/python
#
# A simple wrapper around exiv2 invocations to read and write GPS data
# from and to JPEG EXIFv2.
#
# Seth Golub  http://www.sethoscope.net/geophoto/
#
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# I don't bother to provide a copy of the GNU General Public License
# along with this program, but you can get one from the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA

import os
from tempfile import NamedTemporaryFile

def _num_to_rational(num):
  # Let's only use as many scaling digits as we need (but we'll stay
  # in base 10 for simplicity).
  stringrep = ('%f' % num)
  (int_portion, frac_portion) = stringrep.rstrip('0').split('.')
  return '%s%s/%d' % (int_portion, frac_portion, pow(10,len(frac_portion)))

# TODO: check to see whether the denominator is a power of ten, and if
# so, do the conversion using strings instead of floating point math,
# to avoid introducing small error.
def _rational_to_num(fields, rfield, signfield, negsignvalue):
  if fields[rfield] == '0':
    return 0    # this appears to be a special case.  Someone is discarding the denominator.
  (numerator, denominator) = fields[rfield].split()
  value = float(numerator) / int(denominator)
  if signfield != None and fields[signfield] == negsignvalue:
    value *= -1
  return value

def _num_to_rational_cmd(num, rfield, signfield, possign, negsign):
  if num >= 0:
    sign = possign
  else:
    sign = negsign
  return ['set %s %s' % (signfield, sign),
          'set %s %s' % (rfield, _num_to_rational(abs(num)))]

def _speed_out(kph):
  return ['set Exif.GPSInfo.GPSSpeedRef K',
          'set Exif.GPSInfo.GPSSpeed %s' % _num_to_rational(abs(kph))]

def _speed_in(fields):
  num = _rational_to_num(fields, 'Exif.GPSInfo.GPSSpeed', None, None)
  unit = fields['Exif.GPSInfo.GPSSpeedRef']
  if unit == 'K':    # kilometers per hour
    return num
  elif unit == 'M':  # miles per hour
    return num * 1.609344
  elif unit == 'N':  # knots
    return num * 1.852
  return num  # this shouldn't happen, but we'll assume km/h.

def _timestamp_out(hhmmss):
  hour = int(hhmmss[0:2])
  min = int(hhmmss[2:4])
  sec = int(hhmmss[4:6])
  return ['set Exif.GPSInfo.GPSTimeStamp %s %s %s' % (_num_to_rational(hour),
                                                      _num_to_rational(min),
                                                      _num_to_rational(sec))]
def _timestamp_in(fields):
#   # If we use exiv, we have to parse it ourselves, but with EXIF.py
#   # this comes in as an IFD_Tag instance.
#   values = fields['GPS GPSTimeStamp'].values
#   return ''.join(['%02d' % int(x.num / x.den) for x in values])
  data = fields['GPSTimeStamp'].split()
  hour = int(round(float(data[0]) / int(data[1])))  # we don't assume it's hh/1
  min = int(round(float(data[2]) / int(data[3])))
  sec = int(round(float(data[4]) / int(data[5])))
  return '%02d%02d%02d' % (hour, min, sec)


class GeoExif:
  exivbin = 'exiv2'
  cl = {   # closures for reading and writing fields
    'datum'     : { 'out' : lambda x : ['set Exif.GPSInfo.GPSMapDatum %s' % x],
                    'in'  : lambda f : f['Exif.GPSInfo.GPSMapDatum'] },

    'latitude'  : { 'out' : lambda deg : _num_to_rational_cmd(deg, 'Exif.GPSInfo.GPSLatitude', 'Exif.GPSInfo.GPSLatitudeRef', 'N', 'S'),
                    'in'  : lambda fields : _rational_to_num(fields, 'Exif.GPSInfo.GPSLatitude', 'Exif.GPSInfo.GPSLatitudeRef', 'S') },

    'longitude' : { 'out' : lambda deg : _num_to_rational_cmd(deg, 'Exif.GPSInfo.GPSLongitude', 'Exif.GPSInfo.GPSLongitudeRef', 'E', 'W'),
                    'in'  : lambda fields : _rational_to_num(fields, 'Exif.GPSInfo.GPSLongitude', 'Exif.GPSInfo.GPSLongitudeRef', 'W') },

    'altitude'  : { 'out' : lambda meters : _num_to_rational_cmd(meters, 'Exif.GPSInfo.GPSAltitude', 'Exif.GPSInfo.GPSAltitudeRef', '0', '1'),
                    'in'  : lambda fields : _rational_to_num(fields, 'Exif.GPSInfo.GPSAltitude', 'Exif.GPSInfo.GPSAltitudeRef', '1') },

    'speed'     : { 'out' : lambda kph : _speed_out(kph),
                    'in'  : lambda fields : _speed_in(fields) },

    'timestamp' : { 'out' : lambda hhmmss : _timestamp_out(hhmmss),
                    'in'  : lambda fields : _timestamp_in(fields) },
    }

  def clear(self):
    self.data = {}

  def __init__(self, inputfile=None):
    self.clear()
    if inputfile:
      self.read_from_file(inputfile)

  def __str__(self):
    keys = []
    output = ''
    potential_keys = self.valid_fields()
    potential_keys.sort()
    for key in potential_keys:
      if self.data.has_key(key):
        output = '%s %s: %s\n' % (output, key, self.data[key])
    return output
  
  def valid_fields(self):
    return GeoExif.cl.keys()
    
  def set(self, key, value):
    if not GeoExif.cl.has_key(key):
      raise KeyError, 'Unknown field: %s' % key
    self.data[key] = value

  def get(self, key):
    return self.data[key]

  def has_key(self, key):
    return self.data.has_key(key)

  def apply_to_file(self, jpegfilename):
    tmpfile = NamedTemporaryFile()   # this won't work on all platforms
    tmpfile.write('set Exif.GPSInfo.GPSVersionID 2 2 0 0\n')
    for key in self.data.keys():
      try:
        for command in GeoExif.cl[key]['out'](self.data[key]):
          tmpfile.write(command)
          tmpfile.write('\n')
      except KeyError:
        pass  # It's ok to skip fields we can't handle.
    tmpfile.flush()
    os.system('%s -m %s mo %s' % (GeoExif.exivbin, tmpfile.name, jpegfilename))
    tmpfile.close()  # deletes file

#   def read_from_file(self, filename):
#     self.clear()
#     import EXIF
#     f = open(filename, 'rb')
#     self.data = EXIF.process_file(f)
#     for key in self.valid_fields():  # go through all possible fields
#       try:
#         self.data[key] = GeoExif.cl[key]['in'](self.data)
#       except KeyError:
#         pass  # It's ok for the Exif data to lack some fields

  def read_from_file(self, filename):
    self.clear()
    # | egrep "^Exif.GPSInfo"    # not really necessary, and foils self.alldata, which others might want
    (stdin,stdout) = os.popen2('%s -Pkv pr %s | sed "s/[^a-zA-Z0-9._-]/ /g"'
                               % (GeoExif.exivbin, filename))
    data = {}
    for line in stdout:
      fields = line.split()     # name type count value [value ...]
      key = fields[0]#[13:]      # chop off the "Exif.GPSInfo." part
      data[key] = ' '.join(fields[1:])
    self.alldata = data
    for key in self.valid_fields():  # go through all possible fields
      try:
        self.data[key] = GeoExif.cl[key]['in'](data)
      except KeyError:
        pass  # It's ok for the Exif data to lack some fields

  def delete_all(self, filename):
    (stdin,stdout) = os.popen2('%s -Pk pr %s | sed "s/[^a-zA-Z0-9._-]/ /g" | egrep "^Exif.GPSInfo"'
                               % (GeoExif.exivbin, filename))
    tmpfile = NamedTemporaryFile()   # this won't work on all platforms
    for line in stdout:
      fields = line.split()     # name type count value [value ...]
      tmpfile.write('del %s\n' % fields[0])
    tmpfile.flush()
    os.system('%s -m %s mo %s' % (GeoExif.exivbin, tmpfile.name, filename))
    tmpfile.close()  # deletes file


def main():
  from optparse import OptionParser
  from sys import stderr
  usage = "usage: %prog [opts] file1.jpg [more jpegs]"
  optparser = OptionParser(usage)
  optparser.add_option('-l', '--latitude', type='float', help='decimal degrees -90..90')
  optparser.add_option('-n', '--longitude', type='float', help='decimal degrees -180..180')
  optparser.add_option('-s', '--speed', type='float', help='km/h')
  optparser.add_option('-a', '--altitude', type='float', help='meters')
  optparser.add_option('-d', '--datum', type='string', help='e.g. WGS-84')
  optparser.add_option('-t', '--timestamp', type='string', help='hhmmss UTC')
  optparser.add_option('-c', '--copy', type='string', help='Copy all GPS tags from FILE', metavar='FILE')
  optparser.add_option('-D', '--delete', action='store_true', help='Delete all GPS tags', default=False)
  optparser.add_option('-e', '--exivbin', type='string', help='location of exiv2 binary, only needed for setting values', default='exiv2')
  optparser.add_option('-p', '--print', action='store_true', default=False)
  optparser.add_option('-v', '--verbose', action='store_true', default=False)
  (options, args) = optparser.parse_args()
  GeoExif.exivbin = options.exivbin

  if options.copy:
    geo_exif = GeoExif(options.copy)
    values_set = 1  # plausible assumption
  else:
    geo_exif = GeoExif()
    values_set = 0

  if options.delete:
    for jpeg in args:
      if options.verbose:
        stderr.write('Deleting tags from %s\n' % jpeg)
      geo_exif.delete_all(jpeg)
    return

  for key in geo_exif.valid_fields():
    if options.__dict__[key] != None:
      geo_exif.set(key, options.__dict__[key])
      values_set += 1

  if values_set > 0:
    for jpeg in args:
      geo_exif.apply_to_file(jpeg)
      if options.verbose:
        stderr.write('Updating %s\n' % jpeg)

  if values_set == 0 or options.__dict__['print']:
    for jpeg in args:
      geo_exif.read_from_file(jpeg)
      print '%s:' % jpeg
      print geo_exif

if __name__ == '__main__':
  main()

