#!/usr/bin/python

# hikify.py
#       --copyright--                   Copyright 2008 (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--
#       October 25, 2007        bar
#       October 27, 2007        bar     put the source track names in the output tracks
#       October 28, 2007        bar     spin routine out to tz_gps
#       May 11, 2008            bar     handle no tracks found
#                                       show_info
#                                       process input in way that allows lots of files
#       May 11, 2008            bar     change name from hikes to hikify and put in tzpython dir
#       May 12, 2008            bar     combine
#       May 13, 2008            bar     more combine
#       May 14, 2008            bar     force no_time even if cmd line logic doesn't, if we whack the speeds
#       May 16, 2008            bar     nmea input
#       May 17, 2008            bar     email adr
#       May 20, 2008            bar     map_them
#       May 31, 2008            bar     fix a couple of cmd line error msgs
#       September 2, 2008       bar     spin the include/exclude logic to a routine
#       September 3, 2008       bar     fix a blown-edit typo
#       September 7, 2008       bar     compress runs of __ to one of 'em
#                                       --sparsify
#       September 8, 2008       bar     comment
#       October 26, 2008        bar     set split output files' date/time
#       November 2, 2008        bar     note about settings and big_points
#                                       start code to do start_point and end_point
#       November 5, 2008        bar     put distance and altitude info in track descriptions
#       January 25, 2009        bar     cachew
#                                       put LF after description
#                                       move hike count printout over to right if non-zero
#       February 19, 2009       bar     --bike
#       April 30, 2009          bar     fix non-caching logic
#       November 22, 2009       bar     help typo
#       January 9, 2010         bar     cache date-time, file name, input/output crcs, and use date-time/file-name to filter before reading an input file for crc
#       March 23, 2010          bar     forget_file
#       May 8, 2010             bar     allow s on end of --forget_file
#       June 20, 2010           bar     try to put the track in time order if there are name collisions
#       April 24, 2011          bar     --merge
#                                       --output
#       July 11, 2011           bar     --join
#       July 14, 2011           bar     mess with --start_point and --end_point but don't test 'em
#                                       --snip and --snip_near
#       August 17, 2011         bar     commented-out debug printout
#       November 29, 2011       bar     pyflake cleanup
#       February 3, 2012        bar     snip 'em before doing the splits, etc. so that shortened, snipped tracks are filtered out
#       --eodstamps--
#
#
#       TODO:
#
#           Default to --first_time or --last_time (I seem to use --first_time, though the latter seems better)
#
#           Loses the first, uphill track in split Poo Poo point (_01) hike if the hike is passed through again. (sparsified)
#
#           Sparsify is too aggressive. Strips significant number of points from tracks already sparsified.
#
#           Option to set the output file's name, as a last resort before using the track name, the geoname of the first/furthest-away/both points.
#
#           Test the include/exclude/filter-value options i've not used (all but --exclude_near)
#
#           Use labels or whatever to combine tracks that are same place in same split files.
#           Get t.name in to combined tracks.
#
#           output gpx file(s)
#               update a .ini file with gpx file info for thumbnail_htm.py use
#               one track per file allowed if .ini files can't have sub-sections (i forget)
#               [file_name]
#                   date_time=
#                   md5=
#                   found=(not) date/time
#
#                   north=
#                   south=
#                   east=
#                   west=
#                   high=
#                   low=
#                   start_time=
#                   end_time=
#                   tags= word word ...
#               and/or put this stuff at the top of the gpx file in custom fields
#
#

import  sys
import  os

import  tz_gps
import  latlon
import  tz_parse_time


help_str    = """
Extract hike tracks from .gpx or .nmea track files.

python  hikify.py   (options)   ambiguous_input_files.gpx_nmea   output_file_name.gpx_kml_kmz_nmea

Options:
    --strip_spaces                          Strip leading spaces in XML type output files to make them smaller.
    --show_info                             Print extra debugging information.

    --split                                 Output each track to separate files using labels and etc. (numbered _0000...####)
    --creator        name                   Set the file/tracks creator name to the given name.    (default: "Tranzoa, Co.")

    --first_time                            Set --split files' date/time to first point's.
    --last_time                             Set --split files' date/time to last  point's.

    --cache          file_name              Read/write cache of previous computations from given file.
    --recompute                             Recompute the hikes from files the cache says have hikes. (Do no use cached hikes.)

  Filtering:
    --forget_file    "regx"                 Forget from cache and don't do hikes from matching files.

    --file_since     date/time              Only input files dated since the given date/time  (default: year 1970).
    --file_before    date/time              Only input files dated before the given date/time (default: year 2038).

    --exclude        latlon latlon          Ignore any hike with a point inside the given NW/SE or NE/SW rectangle.
    --exclude_near   latlon nautical_miles  Ignore any hike with a point within the given distance to the given point.
    --include        latlon latlon          Only output hikes with a point inside any such given NW/SE or NE/SW rectangle.
    --include_near   latlon nautical_miles  Only output hikes with a point near any such given point.
    --snip           latlon latlon          Split tracks in to separate tracks at points inside any such given NW/SE or NE/SW rectangle.
    --snip_near      latlon nautical_miles  Split tracks in to separate tracks at points near any such given point.

    Unimplemented:
                                            If ouput tracks contain points within 'nautical_miles' of either of these points:
    --start_point    latlon                 Points before first point near (--ends_distance) this point are stripped from output tracks..
    --end_point      latlon                 Points after  last  point near (--ends_distance) this point are stripped from output tracks..
                                            If only one start_ end_ point is given, both points are that point.
                                            If only one of these points is in the output track, the shortest end is clipped.

  Output Processing:
    --output         file_name              Set the output file name (instead of the last name on cmd line).
    --smooth                                Smooth the output tracks (default)
    --rough                                 Do not smooth output tracks.
    --no_times                              Strip all times, speeds, durations and heart rates from output.
    --label          latlon latlon label    Add label to tracks containing a point inside given NW/SW or NE/SW rectangle.
    --label_near     latlon NMI    label    Add label to tracks containing a point within given nautical miles from given point.
    --merge                                 Merge points recorded at same time/place.
    --combine                               Merge points so there and back become one track. (implies --smooth and --no_times).
    --sparsify                              Take out points that are midway between surrounding points.
    --join                                  Join tracks whose beginnings and ends match in time and space.
                                            (Uses --ends_distance and --max_sleep settings.)

  Hikify Filter Values (Note: These apply to 'big_points' not to the detailed points.):
    --bike                                  Use bikify settings.
    --ends_distance     meters              End-point resolution detection (--smooth to 1/4 of this value). default: 40 meters
    --min_distance      meters              Hike must go at least this far from starting point this far.    default: 450 meters
    --max_jump          meters              Track cannot have more than this distance between points        default: unlimited
    --min_duration      hh:mm:ss            Hike must be for at least this long in time.                    default: 15 minutes
    --max_sleep         hh:mm:ss            Track cannot have more than this time between points.           default: 3 minutes
    --max_speed         kilometers_per_hour Hike cannot have speeds faster than this.                       default: 15 kph
    --cutoff_distance   meters              Used for low level filtering and for sparsify.                  default: --ends_distance/4


Any number of --exclude... and --include... options may be given.

If any --include... point or rectangle is given,
only hikes with at least one point inside any of the --include... areas are output.

e.g. python hikify.py --include_near "47.0473, -121.4594" 0.9 --label_near "47.0473, -121.4594" 0.9 "Lost Lake" *.gpx i.kmz

"""





def include_exclude(hikes, includes, excludes) :
    """     Strip points and tracks from the given hikes as a function of the include and exclude lists.    """

    delcnt  = 0

    for ti in xrange(len(hikes) - 1, -1, -1) :

        t   = hikes[ti]

        if  not t.points :

            del(hikes[ti])

        else :

            #
            #                       First, strip all points that should be excluded
            #
            for p in t.points :
                for r in excludes :
                    if  r.is_inside(p) :
                        del(hikes[ti])
                        delcnt += 1
                        t   = None
                        break
                    pass
                if  not t :
                    break
                pass

            #
            #                       Then, strip any tracks with no points in the 'includes' list (if there are any points in the 'includes' list).
            #
            if  t and includes :
                ok  = False
                for p in t.points :
                    for r in includes :
                        if  r.is_inside(p) :
                            ok  = True
                            break
                        pass
                    pass
                if  not ok :
                    del(hikes[ti])
                    t       = None
                    delcnt += 1
                pass
            pass
        pass

    return(delcnt)




def snip_tracks(tracks, snips) :
    """     Break the tracks at the given snip points. Count on --join to put 'em back together again. """

    if  len(snips) :
        tracks  = tz_gps.make_array_of_tracks(tracks)

        for ti in xrange(len(tracks)) :

            t   = tracks[ti]

            si  = 0
            pi  = 0
            while pi < len(t.points) :
                p   = t.points[pi]
                for r in snips :
                    if  r.is_inside(p) :
                        tracks.append(tz_gps.a_track(t.points[si:pi], id_num = t.id_num, name = t.name, description = t.description, file_name = t.file_name, color = t.color, opacity = t._opacity))
                        while pi < len(t.points) :
                            if  not r.is_inside(t.points[pi]) :
                                break
                            pi += 1
                        si      = pi
                        break
                    pass
                pi             += 1
            tracks[ti].points   = t.points[si:pi]

        tracks  = tz_gps.make_array_of_tracks(tracks)           # zap any empty tracks

    return(tracks)




def clip_at_start_end_point(tracks, distance, start_point = None, end_point = None) :
    start_point = start_point or end_point
    end_point   = end_point   or start_point

    if  start_point and end_point :
        tracks  = tz_gps.make_array_of_tracks(tracks)

        for t in tracks :
            si          = 0
            for pi in xrange(len(t.points)) :
                if  t.points[pi].flat_distance_from(start_point) < distance :
                    while pi < len(t.points) :
                        if  t.points[pi].flat_distance_from(start_point) >= distance :
                            break
                        pi  += 1
                    si  = pi - 1
                    break
                pass

            ei          = len(t.points)
            for pi in xrange(len(t.points) - 1, -1, -1) :
                if  t.points[pi].flat_distance_from(end_point) < distance :
                    while pi >= 0 :
                        if  t.points[pi].flat_distance_from(end_point) >= distance :
                            break
                        pi  -= 1
                    ei  = pi + 1
                    break
                pass

            if  si or (ei < len(t.points)) :
                #
                #   Things to handle !!!!
                #       Only one point is found     (clip the shorter end of the hike)
                #       There are points, but no real hike points, between si and ei (clip the shorter of len(t.points)-si (from the tail) or ei (from the beginning))
                #       After clipping, the hike becomes too short, too brief, or doesn't get far enough away from the starting point (whack the hike)
                #

                if  si  < ei :
                    t.points    = t.points[si:ei]
                pass
            pass
        tracks      = tz_gps.make_array_of_tracks(tracks)

    return(tracks)




class   a_cache_item(object) :
    def __init__(me, fn, crc = 0, hikes = None) :
        me.fn       = os.path.splitext(os.path.basename(fn))[0]
        me.fwhen    = os.path.getmtime(fn)
        me.fsize    = os.path.getsize(fn)
        me.crc      = None
        me.hikes    = hikes or None             # None is unknown. [] means there are no hikes in this input file

    def forget(me)          :
        me.hikes            = None

    def clear(me)           :
        me.hikes            = []

    def append(me, hike)    :
        if  hike            :
            me.hikes        = me.hikes or []
            me.hikes.append(hike)
        pass

    def __str__(me)         :
        return("%s %d %d 0x%08x %s" % ( me.fn, int(me.fwhen), me.fsize, me.crc or 0, ((me.hikes == None) and "Unk") or str(len(me.hikes)) ) )

    pass            # a_cache_item




def is_in_regx_list(ra, fn) :
    for r in ra :
        if  re.search(r, fn) :
            return(True)
        pass

    return(False)




def _chk_tracks(trks) :
    trks.sort(lambda a, b : cmp(a.file_name, b.file_name))
    for ti in range(len(trks)) :
        try :
            a_cache_item(trks[ti].file_name)
        except OSError :
            print "--------------------------------------------------------------"
            print "     " + trks[ti - 1].file_name
            print "     ... No file name here..."
            print "     " + trks[ti + 1].file_name
            print "--------------------------------------------------------------"
            raise
        pass
    pass


if  __name__ == '__main__' :

    import  cPickle
    import  glob
    import  re

    import  replace_file
    import  TZCommandLineAtFile
    import  tzlib


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


    strip_spaces    = False
    settings        = tz_gps.a_track_segmenter_settings().hike()
    ofile_name      = None
    smooth          = True
    sparse          = False
    show_info       = False
    cutoff_distance = None
    no_times        = False
    merge           = False
    combine         = False
    join_tracks     = False
    excludes        = []
    includes        = []
    snips           = []
    labels          = []
    split           = False
    map_them        = False
    creator         = "Tranzoa, Co."
    file_time       = 0
    start_point     = None
    end_point       = None
    recompute       = False
    cache_file_name = None
    cache           = []
    forget_files    = []

    file_before     = float(0x7fffFFFF)
    file_since      = float(0)


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


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


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--output", "-o", ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        ofile_name      = sys.argv.pop(oi)


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


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


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--first_time" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        file_time       = 1

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--last_time" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        file_time       = -1


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--cache_file", "--cache", ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        cache_file_name = sys.argv.pop(oi)
        if  os.path.isfile(cache_file_name) :
            fi      = open(cache_file_name, "rb")
            cache  += cPickle.load(fi)
            fi.close();
        pass

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


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


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


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


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--bike", "--bikify", "-b", ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        settings        = tz_gps.a_track_segmenter_settings().bike()


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

        cutoff_distance = float(sys.argv.pop(oi)) / latlon.metersPerNauticalMile


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


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--rough", "-r" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        smooth          = False


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--combine", "-c" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        combine         = True
        smooth          = True
        no_times        = True


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


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


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--map_them", ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        combine         = True
        smooth          = True
        no_times        = True
        map_them        = True


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--ends_distance", "-e" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        settings.ends_distance  = float(sys.argv.pop(oi)) / latlon.metersPerNauticalMile


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--min_distance", "-d" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        settings.min_distance   = float(sys.argv.pop(oi)) / latlon.metersPerNauticalMile

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--max_jump", "-j" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        settings.max_jump       = float(sys.argv.pop(oi)) / latlon.metersPerNauticalMile


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

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--max_sleep", "-z" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        settings.max_sleep      = tz_parse_time.parse_time_zone(sys.argv.pop(oi))
        if  settings.max_sleep == None :
            print "I cannot understand what the maximum lack-of-points time is!"
            sys.exit(102)
        pass

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--max_speed", "-s" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        settings.max_speed      = float(sys.argv.pop(oi))


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--exclude" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        s                       = sys.argv.pop(oi)
        ( lat, lon )            = latlon.parse_lat_lon(s)
        if  (lat == None) or (lon == None) :
            print "I cannot understand exclude of %s" % s
            sys.exit(103)
        p   = tz_gps.a_rectangle(lat, lon)
        s                       = sys.argv.pop(oi)
        ( lat, lon )            = latlon.parse_lat_lon(s)
        if  (lat == None) or (lon == None) :
            print "I cannot understand exclude lat/lon of %s" % s
            sys.exit(103)
        p.include_latlon(lat, lon)
        excludes.append(p)


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--include" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        s                       = sys.argv.pop(oi)
        ( lat, lon )            = latlon.parse_lat_lon(s)
        if  (lat == None) or (lon == None) :
            print "I cannot understand include of %s" % s
            sys.exit(103)
        p   = tz_gps.a_rectangle(lat, lon)
        s                       = sys.argv.pop(oi)
        ( lat, lon )            = latlon.parse_lat_lon(s)
        if  (lat == None) or (lon == None) :
            print "I cannot understand include lat/lon of %s" % s
            sys.exit(103)
        p.include_latlon(lat, lon)
        includes.append(p)


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--snip" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        s                       = sys.argv.pop(oi)
        ( lat, lon )            = latlon.parse_lat_lon(s)
        if  (lat == None) or (lon == None) :
            print "I cannot understand snip of %s" % s
            sys.exit(103)
        p   = tz_gps.a_rectangle(lat, lon)
        s                       = sys.argv.pop(oi)
        ( lat, lon )            = latlon.parse_lat_lon(s)
        if  (lat == None) or (lon == None) :
            print "I cannot understand snip lat/lon of %s" % s
            sys.exit(103)
        p.include_latlon(lat, lon)
        snips.append(p)


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--exclude_near", "-x" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        s                       = sys.argv.pop(oi)
        ( lat, lon )            = latlon.parse_lat_lon(s)
        if  (lat == None) or (lon == None) :
            print "I cannot understand exclude_near of %s" % s
            sys.exit(103)
        p   = tz_gps.a_circle(lat, lon, float(sys.argv.pop(oi)))
        excludes.append(p)


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--include_near", "-i" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        s                       = sys.argv.pop(oi)
        ( lat, lon )            = latlon.parse_lat_lon(s)
        if  (lat == None) or (lon == None) :
            print "I cannot understand include_near of %s" % s
            sys.exit(103)
        p   = tz_gps.a_circle(lat, lon, float(sys.argv.pop(oi)))
        includes.append(p)


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--snip_near" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        s                       = sys.argv.pop(oi)
        ( lat, lon )            = latlon.parse_lat_lon(s)
        if  (lat == None) or (lon == None) :
            print "I cannot understand snip_near of %s" % s
            sys.exit(103)
        p   = tz_gps.a_circle(lat, lon, float(sys.argv.pop(oi)))
        snips.append(p)



    while True :
        oi  = tzlib.array_find(sys.argv, [ "--start_point" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        s                       = sys.argv.pop(oi)
        ( lat, lon )            = latlon.parse_lat_lon(s)
        if  (lat == None) or (lon == None) :
            print "I cannot understand start_point of %s" % s
            sys.exit(103)
        start_point             = tz_gps.a_point(lat = lat, lon = lon)


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--end_point" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        s                       = sys.argv.pop(oi)
        ( lat, lon )            = latlon.parse_lat_lon(s)
        if  (lat == None) or (lon == None) :
            print "I cannot understand end_point of %s" % s
            sys.exit(103)
        end_point               = tz_gps.a_point(lat = lat, lon = lon)




    while True :
        oi  = tzlib.array_find(sys.argv, [ "--label" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        s                       = sys.argv.pop(oi)
        ( lat, lon )            = latlon.parse_lat_lon(s)
        if  (lat == None) or (lon == None) :
            print "I cannot understand label of %s" % s
            sys.exit(103)
        p                       = tz_gps.a_rectangle(lat, lon)
        s                       = sys.argv.pop(oi)
        ( lat, lon )            = latlon.parse_lat_lon(s)
        if  (lat == None) or (lon == None) :
            print "I cannot understand label lat/lon of %s" % s
            sys.exit(103)
        p.include_latlon(lat, lon)
        p.name                  = sys.argv.pop(oi).strip()
        labels.append(p)


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--label_near", "-l" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        s                       = sys.argv.pop(oi)
        ( lat, lon )            = latlon.parse_lat_lon(s)
        if  (lat == None) or (lon == None) :
            print "I cannot understand label_near of %s" % s
            sys.exit(103)
        p                       = tz_gps.a_circle(lat, lon, float(sys.argv.pop(oi)))
        p.name                  = sys.argv.pop(oi).strip()
        labels.append(p)



    while True :
        oi  = tzlib.array_find(sys.argv, [ "--file_since" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        file_since              = tz_parse_time.parse_time(sys.argv.pop(oi))
        if  file_since         == None :
            print "I cannot understand the file_since!"
            sys.exit(102)
        pass


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--file_before" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        file_before             = tz_parse_time.parse_time(sys.argv.pop(oi))
        if  file_before        == None :
            print "I cannot understand the file_before!"
            sys.exit(102)
        pass


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--forget_file", "--forget-file", "--forgetfile", "--forget_files", "--forget-files", "--forgetfiles", "--forget", ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        forget_files.append(sys.argv.pop(oi))



    c_by_file_name  = {}
    for c in cache  :
        if  not is_in_regx_list(forget_files, c.fn) :
            c_by_file_name[c.fn]    = c
        pass

    c_by_crc        = {}
    for c in cache  :
        if  not is_in_regx_list(forget_files, c.fn) :
            c_by_crc[c.crc]         = c
        pass


    if  cutoff_distance == None :
        cutoff_distance = 5.0 / latlon.metersPerNauticalMile            # (the original sparsify value - which we emulate by dividing cutoff_distance by two below)
        cutoff_distance = settings.ends_distance / 4.0

    labels.sort(lambda a, b : cmp(a.name, b.name))


    settings.max_jump           = min(settings.max_jump or settings.min_distance, settings.ends_distance * 10.0)


    if  not len(sys.argv) :
        print "Must have input .gpx/.nmea file(s)%s!" % (((not ofile_name) and " and output file") or "")
        sys.exit(101)


    if  not ofile_name :
        ofile_name  = sys.argv.pop(-1)
    odir        = os.path.dirname(ofile_name)
    if  odir and (not os.path.isdir(odir)) :
        os.makedirs(odir)

    fnames      = {}
    while len(sys.argv) :
        fn      = sys.argv.pop(0)
        fns     = glob.glob(fn)
        if  not fns :
            print "Found no file named %s!" % ( fn )
            sys.exit(101)
        for fn in fns :
            if  not is_in_regx_list(forget_files, fn) :
                fnames[fn]  = True
            pass
        pass

    if  not len(fnames) :
        print >>sys.stderr, "Found no input files!"
        sys.exit(101)


    if  os.path.splitext(ofile_name)[1].lower() == '.nmea' :
        if  no_times or (combine and smooth) :
            print "I cannot write NMEA files with no date/times!"
            sys.exit(109)
        pass


    delcnt      = 0
    tracks      = []
    chikes      = []
    fnames      = [ fn for fn in fnames.keys() if file_since <= os.path.getmtime(fn) <= file_before ]
    fnames.sort()

    print "%u files." % len(fnames)

    for fn in fnames :
        c       = a_cache_item(fn)
        if  cache_file_name :
            c   = c_by_file_name.get(c.fn, c)

            if  c.crc      == None :
                crc         = tzlib.blkcrc32(0xffffFFFF, tzlib.read_whole_binary_file(fn))
                c           = c_by_crc.get(crc, c)
                c.crc       = crc
                c.fsize     = os.path.getsize(fn)
                c.fwhen     = os.path.getmtime(fn)
            elif (os.path.getsize(fn) != c.fsize) or (os.path.getmtime(fn) != c.fwhen) :
                crc         = tzlib.blkcrc32(0xffffFFFF, tzlib.read_whole_binary_file(fn))
                if  crc    != c.crc :
                    # print "      %s CRC mismatch. Re-computing hikes." % fn
                    c.forget()                              # input file has changed so forget what the cache knew about hikes
                else        :
                    # print "      %s CRC match. Using cache: %s." % ( fn, ((c.hikes == None) and "Unk") or str(len(c.hikes)) )
                    pass
                c.crc       = crc
                c.fsize     = os.path.getsize(fn)
                c.fwhen     = os.path.getmtime(fn)
            else            :
                # print "      %s date/size match. Using cache: %s." % ( fn, ((c.hikes == None) and "Unk") or str(len(c.hikes)) )
                pass
            pass

        c_by_file_name[c.fn]    = c
        if   cache_file_name and (c.hikes != None) and (not len(c.hikes)) :
            s               = "File: %s" % fn
            s              += (" " * max(0, 70 - len(s))) + " cache-skipping."
            print s
        elif cache_file_name and (c.hikes != None) and (not recompute) :
            chikes         += c.hikes
            s               = "File: %s" % fn
            s              += (" " * max(0, 70 - len(s))) + " %u hike%s cached." % ( len(c.hikes), tzlib.s_except_1(c.hikes) )
            print s
            _chk_tracks(c.hikes)
        else                :
            trks            = tz_gps.tracks_from_file(fn)
            for t in trks   :
                t.file_name = fn                                        # override any super-duper logic that picks up the original file name from the data ('cause that original name may be from some file somewhere else. Or it may be wrong.)

            if  not trks    :
                print "No tracks in %s!" % fn
                c.clear()                                               # just in case of what? duped files? yep. But, the cache doesn't like files with the same base name.
            else :
                _chk_tracks(trks)
                trks        = tz_gps.sorted_tracks_by_when(trks)
                _chk_tracks(trks)
                trks        = tz_gps.remove_duplicate_tracks(trks)
                _chk_tracks(trks)

                hikes       = snip_tracks(trks, snips)

                hikes       = tz_gps.extract_track_segments(hikes, settings, show_info = show_info)
                _chk_tracks(hikes)

                hikes       = clip_at_start_end_point(hikes, settings.ends_distance, start_point, end_point)
                _chk_tracks(hikes)

                # print "@@@@", len(hikes), [ "from " + str(h.points[0]) + " to " + str(h.points[-1]) for h in hikes ]

                delcnt     += include_exclude(hikes, includes, excludes)

                _chk_tracks(hikes)

                tracks     += hikes

                s           = "File: %s %u track%s" % ( fn, len(trks), tzlib.s_except_1(trks) )

                if  hikes   :
                    s      += (" " * max(0, 70 - len(s))) + " %u hike%s" % ( len(hikes), tzlib.s_except_1(hikes) )

                s          += "."
                print       s
            pass
        pass

    chikes  = tz_gps.remove_duplicate_tracks(chikes)

    cnt     = len(tracks)
    tracks  = tz_gps.remove_duplicate_tracks(tracks)
    delcnt += (cnt - len(tracks))

    _chk_tracks(tracks + chikes)


    if  delcnt :
        print "Excluded %u track%s." % ( delcnt, tzlib.s_except_1(delcnt) )

    if  not tracks and not chikes :
        print "No tracks remaining."
    else :
        if  tracks              :
            cnt                 = tz_gps.count_points_in_tracks(tracks)
            print "Processing %u point%s to %u track%s..." % ( cnt, tzlib.s_except_1(cnt), len(tracks), tzlib.s_except_1(tracks) )

            if  join_tracks     :
                fnd             = True
                while fnd       :
                    fnd         = False
                    bwhens          = [ ( t.points[ 0], ti ) for ti, t in enumerate(tracks) ]
                    ewhens          = [ ( t.points[-1], ti ) for ti, t in enumerate(tracks) ]
                    for ep, eti in ewhens :
                        for bp, bti in bwhens :
                            if  eti != bti :
                                td  = bp.when - ep.when
                                if  (td >= 0) and (td < settings.max_sleep) and (ep.flat_distance_from(bp) < settings.ends_distance) :
                                    fnd = True
                                    tracks[eti].append_track(tracks[bti])
                                    del(tracks[bti])
                                    break
                                pass
                            pass
                        if  fnd :
                            break
                        pass
                    pass
                pass

            if  merge           :
                tracks.sort(lambda a, b : cmp(a.when, b.when))
                ti              = 0
                while ti < len(tracks) - 1 :
                    tti         = ti + 1
                    when        = tracks[ti].points[-1].when
                    while tti   < len(tracks) :
                        if  when < tracks[tti].when :           # run tti past all tracks that start before the end of this track, ti
                            break
                        tti    += 1
                    if  tti     > ti + 1 :
                        pa      = tz_gps.merge_tracks_to_points(tracks[ti : tti])
                        if  len(pa) :
                            trk             = tz_gps.a_track(pa, id_num = tracks[ti].id_num, name = tracks[ti].name, description = tracks[ti].description, file_name = tracks[ti].file_name, color = tracks[ti].color, opacity = tracks[ti]._opacity)
                            tracks[ti:tti]  = [ trk ]
                        pass

                    ti         += 1
                pass

            if  combine         :
                tracks          = tz_gps.combine_near_points_in_tracks(tracks, settings.ends_distance / 4.0, map_them, show_info)
                if  map_them    :
                    no_times    = True
                if  smooth      :
                    ncnt        = tz_gps.count_points_in_tracks(tracks)
                    spp         = 2.0
                    if  ncnt    :
                        spp     = max(spp, float(cnt) / float(ncnt))
                    tracks      = tz_gps.set_equal_durations_in_tracks(tracks, spp)
                    tracks      = tz_gps.smooth_tracks(tracks)
                    no_times    = True                                          # since we've whacked the times, we may as well whack 'em good
                pass
            elif  smooth        :
                tracks          = tz_gps.make_big_points(tracks, cutoff_distance)
                _chk_tracks(tracks)
                tracks          = tz_gps.smooth_tracks(tracks)

            _chk_tracks(tracks)

            if  no_times        :
                if  os.path.splitext(ofile_name)[1].lower() == '.nmea' :
                    print "I cannot write NMEA files with no date/times!"
                    sys.exit(109)

                tracks          = tz_gps.set_no_whens_in_tracks(tracks)         # must be done after smoothing, as smoothing uses 'when' values for interpolation
                _chk_tracks(tracks)


            for t in tracks     :
                nms             = []
                for r in labels :
                    for p in t.points :
                        if  r.is_inside(p) :
                            nms.append(r)
                            break
                        pass
                    pass
                if  nms         :
                    t.name      = " and ".join( [ p.name for p in nms ] )
                pass


            stracks                 = tz_gps.remove_redundant_points_from_tracks(tracks, cutoff_distance / 2.0)
            _chk_tracks(stracks)
            for t in stracks        :
                t.description       = re.sub(r"\n?Distance: [^\n]+\nAltitude: [^\n]+\nAltitude: .*?meters", "", t.description or "")
                t.description      += "\n"

                d                   = tz_gps.points_flat_distance(t.points)
                t.description      += "Distance: %.2f miles : %.2f kilometers" % ( d * latlon.milesPerNauticalMile, d * latlon.metersPerNauticalMile / 1000.0 )

                ainfo               = tz_gps.points_altitude_info(t.points)
                if  ainfo           :
                    t.description  += "\nAltitude: low:%5u high:%5u (%5u) loss:%4u gain:%4u feet"   % ( int(round(ainfo.lowest * latlon.feetPerMeter)), int(round(ainfo.highest * latlon.feetPerMeter)), int(round((ainfo.highest - ainfo.lowest) * latlon.feetPerMeter)), int(round(ainfo.lost * latlon.feetPerMeter)), int(round(ainfo.gained * latlon.feetPerMeter)) )
                    t.description  += "\nAltitude: low:%5u high:%5u (%5u) loss:%4u gain:%4u meters" % ( int(round(ainfo.lowest                      )), int(round(ainfo.highest                      )), int(round((ainfo.highest - ainfo.lowest)                      )), int(round(ainfo.lost                      )), int(round(ainfo.gained                      )) )
                t.description      += "\n"
                t.description      += "Source: %s\n" % t.file_name


            if  sparse :
                tracks      = stracks
            pass


        tracks             += chikes

        cnt                 = tz_gps.count_points_in_tracks(tracks)
        print "Writing %u point%s to %u track%s..." % ( cnt, tzlib.s_except_1(cnt), len(tracks), tzlib.s_except_1(tracks) )

        if  not split :
            tz_gps.write_gps_file(ofile_name, "hikify.py", creator, [], tracks, strip_spaces)
        else :
            tracks.sort(lambda a, b : cmp(a.when, b.when))
            ofns            = {}
            for t in tracks :
                ( fn, ext ) = os.path.splitext(ofile_name)

                fn         += "_" + tzlib.file_name_able((t.name or "").replace(" ", "_"))          # add the track name to the file name
                fn          = re.sub(r"_+", "_", fn)
                ( pth, fn ) = os.path.split(fn)
                fn          = fn.strip("_")
                fn          = os.path.join(pth, fn)

                ofns[fn]    = ofns.get(fn, 0) + 1
                if  fn :
                    fn     += "_"
                fn          = replace_file.find_available_N_file_name(fn + replace_file.NNNNN + ext, replace_file.NNNNN, 10, True)

                tz_gps.write_gps_file(fn,     "hikify.py", creator, [], [ t ],  strip_spaces)

                if  file_time :
                    p   = t.points[0]
                    if  file_time < 1 :
                        p   = t.points[-1]
                    if  p.when :
                        os.utime(fn, ( os.path.getatime(fn), int(t.points[0].when) ) )              # set the file date/time to that of the 1st point
                    pass
                pass
            pass
        pass

    _chk_tracks(tracks)

    if  cache_file_name :
        for c in c_by_file_name.values() :
            c.clear()
        for t in tracks :
            c_by_file_name[a_cache_item(t.file_name).fn].append(t)                                  # note: the files that were merged in to this one will be cache-skipped in the future
        pass

    if  cache_file_name :
        cache       = c_by_file_name.values()
        tfn         = cache_file_name + ".tmp"
        fo          = open(tfn, "wb")
        cPickle.dump(cache, fo)
        fo.close()
        replace_file.replace_file(cache_file_name, tfn, cache_file_name + ".bak")

    pass


#
#
# eof

