#!/usr/bin/python

# geotag_pictures.py
#       --copyright--                   Copyright 2007 (C) Tranzoa, Co. All rights reserved.    Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
#       --url--                         http://www.tranzoa.net/tzpython/
#       --email--                       pycode is the name to send to. tranzoa.com is the place to send to.
#       --bodstamps--
#       July 22, 2007           bar
#       July 23, 2007           bar     use faster gpx parser
#       July 27, 2007           bar     --tzone, etc
#       July 28, 2007           bar     set the files' time/dates, too
#       August 16, 2007         bar     sort the names of the undone files
#                                       show_info
#       August 18, 2007         bar     --max_time_diff
#       August 20, 2007         bar     sheesh. wanna get that interpolation right, or what?
#       September 11, 2007      bar     sort file names
#       October 12, 2007        bar     some routines moved to latlon from tz_gps
#       October 15, 2007        bar     better help
#                                       must have at least one location file
#       October 26, 2007        bar     uptodate with respect to tracks_from_gpx_file()
#       November 18, 2007       bar     turn on doxygen
#       November 27, 2007       bar     insert boilerplate copyright
#       December 9, 2007        bar     use parse_time_zone for off-ness-able as the help string says
#       January 4, 2008         bar     handle when the picture is taken before any of the points or at the end
#       January 12, 2008        bar     able to handle altitude of None (and interpolate it)
#                                       geotag_jpg()
#       February 9, 2008        bar     comment
#       February 27, 2008       bar     fix divide by zero if both interpolated images are the same image
#       April 27, 2008          bar     able to hard-set the lat/lon/alt
#       May 17, 2008            bar     email adr
#       October 11, 2008        bar     altitude must be an int in the exif data
#       November 16, 2008       bar     i got cute about setting a really big number - fixed now
#                                       better able to individually lat-lon 'em
#       June 15, 2011           bar     altitude is signed int
#       November 29, 2011       bar     pyflake cleanup
#       May 27, 2012            bar     doxygen namespace
#       --eodstamps--
##      \file
#       \namespace              tzpython.geotag_pictures
#
#       Geotag pictures. (jpg files, specifically)
#
#       TODO:
#
#           --change_time   option (changes the times in the files by the --time_offset value)
#
#
#


import  os
import  time

import  latlon
import  tz_gps
import  tz_jpg



MAX_TIME_DIFF_TWEEN_POINTS  =   10.0            # how many seconds is too long between two GPS points we are to interpolate the times of to derive lat/lon


def geotag_jpg(jpg, lat, lon, altitude) :
    """ Geocode this jpg file data. """

    exif    = jpg.get_exif()
    if  exif :
        if  not exif.get(                                     tz_jpg.SOFTWARE_MARKER) :
            exif.add_item(0,                                  tz_jpg.SOFTWARE_MARKER,       tz_jpg.EXIF_STRING_FORMAT, "geotag_pictures.py"    )

        if  not exif.get(    (tz_jpg.GPS_EXIF_MARKER << 16) | tz_jpg.GPS_VERSION_MARKER) :
            exif.add_item(0, (tz_jpg.GPS_EXIF_MARKER << 16) | tz_jpg.GPS_VERSION_MARKER,    tz_jpg.EXIF_BYTE_FORMAT,   [ 0, 0, 2, 2 ]          )   # if big-endian, they go the other way, I've read, but we don't care (or maybe that's the badly done EXIF_VERSION_MARKER item)

        if  not exif.get(    (tz_jpg.SUBIFD_MARKER   << 16) | tz_jpg.EXIF_VERSION_MARKER) :
            exif.add_item(0, (tz_jpg.SUBIFD_MARKER   << 16) | tz_jpg.EXIF_VERSION_MARKER,   tz_jpg.EXIF_UNDEF_FORMAT,  [ '0', '2', '2', '0' ]  )   # this is the screwy data, but it's what II type files have, at least. Don't know about MM files.

        pass


    lat = latlon.lat_lon_in_degrees_minutes_seconds(lat)
    ns  = 'N'
    if  lat[0] < 0 :
        lat[0]  = -lat[0]
        ns      = 'S'
    jpg.add_exif_item(0, tz_jpg.GPS_EXIF_MARKER << 16 | tz_jpg.GPS_LATITUDE_MARKER,       tz_jpg.EXIF_U32_RATIO_FORMAT, [[ lat[0], 1], [ lat[1], 1], [int(lat[2] * 100), 100] ] )
    jpg.add_exif_item(0, tz_jpg.GPS_EXIF_MARKER << 16 | tz_jpg.GPS_LATITUDE_REF_MARKER,   tz_jpg.EXIF_STRING_FORMAT,    ns)


    lon = latlon.lat_lon_in_degrees_minutes_seconds(lon)
    ew  = 'E'
    if  lon[0] < 0 :
        lon[0]  = -lon[0]
        ew      = 'W'
    jpg.add_exif_item(0, tz_jpg.GPS_EXIF_MARKER << 16 | tz_jpg.GPS_LONGITUDE_MARKER,      tz_jpg.EXIF_U32_RATIO_FORMAT, [[ lon[0], 1], [ lon[1], 1], [int(lon[2] * 100), 100] ] )
    jpg.add_exif_item(0, tz_jpg.GPS_EXIF_MARKER << 16 | tz_jpg.GPS_LONGITUDE_REF_MARKER,  tz_jpg.EXIF_STRING_FORMAT,    ew)


    if  altitude != None :
        altitude    = int(round(altitude))
        jpg.add_exif_item(0, tz_jpg.GPS_EXIF_MARKER << 16 | tz_jpg.GPS_ALTITUDE_MARKER,       tz_jpg.EXIF_I32_RATIO_FORMAT, [ [ altitude, 1 ] ] )
        jpg.add_exif_item(0, tz_jpg.GPS_EXIF_MARKER << 16 | tz_jpg.GPS_ALTITUDE_REF_MARKER,   tz_jpg.EXIF_BYTE_FORMAT,      [ 0 ] )

    pass





def geotag_file(points_sorted_by_when, fn, tzone = 0, max_time_diff_tween_points = MAX_TIME_DIFF_TWEEN_POINTS, show_info = False) :

    max_time_diff_tween_points = max_time_diff_tween_points or MAX_TIME_DIFF_TWEEN_POINTS

    points  = points_sorted_by_when

    jpg = tz_jpg.a_jpg(fn)

    try :
        t   = jpg.picture_taken_time(dflt = 0.0, time_func = time.gmtime)
        t  -= tzone
    except  :
        if  show_info :
            print "except taken time", fn
            e       = sys.exc_info()
            print   e[0], e[1], e[2]
        return(False)

    if  t  <= 0 :
        if  show_info :
            print "zero time"
        if  len(points_sorted_by_when) != 1 :
            return(False)
        pass

    pi      = tz_gps.find_point_index_by_when(points, t)
    if  pi == None :
        if  show_info :
            print "point not found", time.asctime(time.gmtime(t)), time.asctime(time.gmtime(points[0].when)), time.asctime(time.gmtime(points[-1].when))
        return(False)

    # print fn, points[pi], time.asctime(time.localtime(t))         # DO NOT FORGET TO TURN ON SHOW_INFO BEFORE DOING STUFF WITH THIS (and remember that the watch can lose multiples of 6553.6 seconds inside a building or whatever.

    p       = points[max(0, pi)]                                    # handle the -1 case of the picture being before the 1st point
    w       = p.when
    if  abs(t - w) >= max_time_diff_tween_points :
        if  show_info :
            print "Point too far away in time:", fn, time.asctime(time.gmtime(w)), time.asctime(time.gmtime(t)), t - w
        return(False)

    lat     = p.lat
    lon     = p.lon
    alt     = p.altitude

    if  w  != t :
        po  = points[min(len(points) - 1, pi + 1)]
        wo  = po.when
        if  wo - w >= max_time_diff_tween_points :
            if  show_info :
                print "Too much time between points:", fn, time.asctime(time.gmtime(w)), time.asctime(time.gmtime(t)), time.asctime(time.gmtime(wo)), wo - w
            return(False)

        d       = wo - w
        if  d :
            d   = (t - w) / d
        lat    += ((po.lat - lat) * d)          # interpolate the lat/lon between the two points
        lon    += ((po.lon - lon) * d)          # !!!! use xyz logic

        if  (alt != None) and (po.altitude != None) :
            alt    += ((po.altitude - alt) * d)
        else :
            alt     = None
        pass

    geotag_jpg(jpg, lat, lon, alt)

    ftime   = os.path.getmtime(fn)
    jpg.save(fn)
    os.utime(fn, ( os.path.getatime(fn), int(ftime) ) )                 # set the file date/time to that of the original

    return(True)




helpstr     = """
Tell me .gpx GPS location file(s - ambiguous ok) and any number of ambiguous input/output picture (non-gpx (.jpg)) file names.

Options:
    --time_offset   (-)hh:mm:ss     Set the offset from GMT the camera's clock is set to.
    --show_info                     Print extra info.
    --max_time_diff    hh:mm:ss     Maximum time difference between points and picture time. (dflt: 0:0:0)

    --latlon        latlon_value    Set the latitude and longitude.
    --lat           latitude        Set the latitude.
    --lon           longitude       Set the longitude.
    --altitude      altitude        Set the altitude in meters.

"""


if  __name__ == '__main__' :

    import  glob
    import  sys


    import  TZCommandLineAtFile
    import  tzlib
    import  tz_parse_time


    del(sys.argv[0])
    TZCommandLineAtFile.expand_at_sign_command_line_files(sys.argv)


    tzone       = 0
    show_info   = False
    max_time_d  = None
    lat         = None
    lon         = None
    alt         = None


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--help", "-h", "-?", "/?" ] )
        if  oi < 0 :    break
        print helpstr
        sys.exit(254)


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--tzone", "--time_offset", "-t"])
        if  oi < 0 :    break
        del sys.argv[oi]
        tzone   = tz_parse_time.parse_time_zone(sys.argv.pop(oi))
        if  tzone == None :
            print "I cannot understand the timezone/offset!"
            sys.exit(102)
        pass

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--max_time_diff", "-d"] )
        if  oi < 0 :    break
        del sys.argv[oi]
        max_time_d      = tz_parse_time.parse_time_zone(sys.argv.pop(oi))
        if  max_time_d  < 1.0 :
            print "Maximum time difference 'tween GPS locations must be >= 1.0 seconds!"
            sys.exit(104)
        pass


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--show_info", "-s", "-i"] )
        if  oi < 0 :    break
        del sys.argv[oi]
        show_info       = True


    sys.argv.reverse()
    a       = len(sys.argv) - 1
    while a >= 0 :
        ag  = sys.argv[a]

        oi  = tzlib.array_find([ "--latitude", "--lat", "--la" ], [ ag ] )
        if  oi >= 0 :
            del sys.argv[a]
            a  -= 1
            if  a < 0 :
                print "No latitude given!"
                sys.exit(105)
            ag  = sys.argv[a]
            del sys.argv[a]
            a  -= 1
            [ lat, ln ]  = latlon.parse_lat_lon(ag + ", 0 degrees E")
            if  lat     == None :
                print "I cannot understand latitude of", ag, "!"
                sys.exit(105)
            a  -= 1
            continue

        oi  = tzlib.array_find([ "--longitude", "--lon", "--lg" ], [ ag ] )
        if  oi >= 0 :
            del sys.argv[a]
            a  -= 1
            if  a < 0 :
                print "No longitude given!"
                sys.exit(105)
            ag  = sys.argv[a]
            del sys.argv[a]
            a  -= 1
            [ ln, lon ]  = latlon.parse_lat_lon("0 degrees N, " + ag)
            if  lon     == None :
                print "I cannot understand longitude of", ag, "!"
                sys.exit(105)
            continue

        oi  = tzlib.array_find([ "--latlon", "--ll", "-l" ], [ ag ] )
        if  oi >= 0 :
            del sys.argv[a]
            a  -= 1
            if  a < 0 :
                print "No lat/lon given!"
                sys.exit(105)
            ag  = sys.argv[a]
            del sys.argv[a]
            a  -= 1
            [ lat, lon ]    = latlon.parse_lat_lon(ag)
            if  (lat == None) or (lon == None) :
                print "I cannot understand lat/lon of", ag, "!"
                sys.exit(105)
            continue

        oi  = tzlib.array_find([ "--altitude", "--alt", "-a" ], [ ag ] )
        if  oi >= 0 :
            del sys.argv[a]
            a  -= 1
            if  a < 0 :
                print "No altitude given!"
                sys.exit(105)
            ag  = sys.argv[a]
            del sys.argv[a]
            a  -= 1
            try :
                alt = float(ag)
            except ValueError :
                print "I cannot understand altitude of", ag, "!"
                sys.exit(105)
            continue

        a  -= 1
    sys.argv.reverse()



    fnames  = {}
    while len(sys.argv) :
        fn  = sys.argv.pop(0)
        fns = tzlib.make_dictionary(glob.glob(fn))
        if  not fns :
            print "Cannot find file(s):", fn
            sys.exit(103)
        fnames.update(fns)


    points  = []


    kys     = fnames.keys()
    kys.sort()
    for fn  in kys :
        if  os.path.splitext(fn)[1].lower() == ".gpx" :
            del(fnames[fn])

            tracks      = tz_gps.tracks_from_gpx_file(fn)
            if  not tracks :
                fi      = open(fn, "r")
                fd      = tz_gps.a_gpx(fi)                      # try the slow way
                fi.close()
                tracks  = fd.get_tracks()
                del(fd)
            if  tracks :
                for t in tracks :
                    points += t.points
                del(tracks)
            else :
                print "No GPS locations in", fn
            pass
        pass

    if  not points :
        if  (lat == None) or (lon == None) :
            print "I found no GPS points given in any GPS location file!"
            sys.exit(108)

        points      = [ tz_gps.a_point(lat = lat, lon = lon, altitude = alt) ]
        tracks      = tz_gps.color_tracks( [ tz_gps.a_track(points, 0) ] )
        max_time_d  = 8000000000.0

    elif  (lat != None) or (lon != None) or (alt != None) :
        print "I cannot handle lat/lon/alt values if I am given a GPX file!"
        sys.exit(106)


    fnames  = fnames.keys()
    fnames.sort()

    points.sort(tz_gps.cmp_points_by_when)

    if  False :
        print len(points), "points"
        print points[0]
        print points[len(points) / 2]
        print points[-1]

    not_done_files  = []

    for fn in fnames :
        if  not geotag_file(points, fn, tzone, max_time_diff_tween_points = max_time_d, show_info = show_info) :
            not_done_files.append(fn)
        elif  tz_jpg.set_file_date_time_from_exif(fn, tzone) <= 0 :
            print "Could not set date/time for", fn
        pass

    if  not_done_files :
        print "The following files were not geotagged:"
        not_done_files.sort()
        for fn in not_done_files :
            print fn
        pass

    pass

#
#
#
# eof
