#!/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
#       May 27, 2012            bar     doxygen namespace
#       April 16, 2016          bar     set proper output file time to last point if that's what's asked for
#       May 11, 2016            bar     --bikify_file
#       July 23, 2016           bar     um. use the bikify_file settings on the first look at the file
#       May 12, 2018            bar     comment about merging
#       May 11, 2019            bar     --unforget_file
#       June 3, 2019            bar     --verbose (jeez, how many of these things do we need?)
#       July 28, 2019           bar     tell the user what file(s) we write
#       August 8, 2019          bar     when merging, toss out big points that don't have all the tracks being merged involved, sort of - can smooth things by avoiding partial matches in time - so we only have samples at times all tracks had a sample for
#       October 26, 2019        bar     print when the tracks were done
#       December 5, 2019        bar     verbose stuff
#                                       max_max param to only_big_points_from_max_tracks()
#       July 18, 2020           bar     don't let him put a file name with an extension with a --bikify_file option
#       --eodstamps--
##      \file
#       \namespace              tzpython.hikify
#
#
#       TODO:
#
#           Instead of doing dumb, time-value based merge, align the tracks in case their clocks are slightly off,
#             and interpolate points when merging them in,
#             in case the merged-to track doesn't have points for each second/time-tick of the merging track's points.
#             Test with roam and the two Canmores together to see that hikes don't have squiggles that can double the milage.
#
#           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  copy
import  sys
import  os
import  time

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.
    --quiet                                 Do not print the file names.
    --verbose                               Add one to verbosity (for informative printouts).

    --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.
    --unforget_file  "regx"                 Never mind, don't --forget_file these 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.
    --bikify_file       "regx"              Use --bike filtering for matching files.
    --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, verbose = 0) :
    """     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, whack any tracks that have an excluded point
            #
            for p in t.points :
                for r in excludes :
                    if  r.is_inside(p) :
                        del(hikes[ti])
                        delcnt += 1
                        if  verbose :
                            print "Excluding point", p, "which is inside exclude", r
                        t   = None
                        break
                    pass
                if  not t :
                    break
                pass

            #
            #                       Then, whack 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
                    if  verbose :
                        print "Excluding point that is not inside includes", p
                    pass
                pass
            pass
        pass

    return(delcnt)




def snip_tracks(tracks, snips, verbose = 0) :
    """     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)

        tlen    = len(tracks)
        for ti in xrange(tlen) :

            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) :
                        ppi     = pi
                        tracks.append(tz_gps.a_track(t.points[si:ppi], 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) :              # skip over any points near the snip point
                            if  not r.is_inside(t.points[pi]) :
                                break
                            pi += 1
                        if  verbose :
                            print "Splitting %u points off %s at %s, saving %u points at track point %s" % ( pi - ppi, t.file_name or "", r, ppi - si, p, )
                        si      = pi                            # the next track we split off will start here
                        break
                    pass
                pi             += 1
            if  verbose and (pi - si != len(t.points)) :
                print "After split(s), %u remaining points in %s" % ( pi - si, t.file_name or "", )
            t.points            = t.points[si:pi]               # we've split off tracks, so change the existing track to whatever is left outside the snip points

        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
        me.settings = None

    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()
    bsettings       = tz_gps.a_track_segmenter_settings().bike()
    bikify_files    = []
    ofile_name      = None
    smooth          = True
    sparse          = False
    show_info       = False
    quiet           = False
    verbose         = 0
    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           = []                    # array of a_cache_item's
    forget_files    = []
    unforget_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();
        for c in cache  :
            c.settings  = getattr(c, 'settings', None)
        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, [ "--quiet" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        quiet           = True

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


    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        = bsettings


    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
        bsettings.ends_distance = settings.ends_distance


    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
        bsettings.min_distance  = settings.min_distance

    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
        bsettings.max_jump      = settings.max_jump


    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)
        bsettings.min_duration  = settings.min_duration

    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)
        bsettings.max_sleep     = settings.max_sleep

    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))
        bsettings.max_speed     = settings.max_speed


    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))

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--unforget_file", "--unforget-file", "--unforgetfile", "--unforget_files", "--unforget-files", "--unforgetfiles", "--unforget", ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        unforget_files.append(sys.argv.pop(oi))


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--list_settings" ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        hs  = tz_gps.a_track_segmenter_settings().hike()
        bs  = tz_gps.a_track_segmenter_settings().bike()
        cache.sort(lambda a, b : cmp(a.fwhen, b.fwhen) or cmp(a.fsize, b.fsize))
        for c in cache  :
            s   = "hike"
            if  c.settings != hs :
                if  c.settings == bs :
                    s   = "Bike"
                elif c.settings != None :
                    s   = "Path"
                else    :
                    pc  = 0
                    ct  = 0.0
                    for trk in c.hikes :
                        pa  = list(trk.points)
                        tz_gps.fix_points_speeds(pa)
                        pc += len(pa)
                        ct += len([ pnt for pnt in pa if pnt.speed > hs.max_speed + 10 ])
                    if  pc and (ct / pc >= 0.01) :
                        s   = "BIKE"
                    pass
                pass
            if  s[0] != 'h' :
                print s, os.path.basename(c.fn)
            pass
        sys.exit(0)


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--bikify_file", "--bikify-file", "--bikifyfile", "--bikify_files", "--bikify-files", "--bikifyfiles", ] )
        if  oi < 0 :    break
        del sys.argv[oi]
        bikify_files.append(sys.argv.pop(oi))
        if  re.search(r'\.[a-zA-Z0-9]*$', bikify_files[-1]) :
            print "I won't --bikify_files that appear to have file name extensions [%s]!" % bikify_files[-1]
            sys.exit(101)
        pass


    c_by_file_name  = {}
    c_by_crc        = {}
    for c in cache  :
        if  (not is_in_regx_list(forget_files, c.fn)) or is_in_regx_list(unforget_files, c.fn) :
            c_by_file_name[c.fn]    = c
            c_by_crc[c.crc]         = c
        if  is_in_regx_list(bikify_files, c.fn) :
            c.settings  = c.settings or copy.deepcopy(bsettings)        # that we None'd the cache file's settings causes us to update them to the current values - which doesn't really matter if the cache isn't recomputed
        else            :
            c.settings  = c.settings or copy.deepcopy( settings)
        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)
    bsettings.max_jump          = settings.max_jump


    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)) or is_in_regx_list(unforget_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

        if  c.settings is None  :                                       # note: this is essentially an if the file is not in cache if the code above set the cache files' settings or it left them as they were read from cache
            if  is_in_regx_list(bikify_files, c.fn) :
                c.settings  = copy.deepcopy(bsettings)
            else            :
                c.settings  = copy.deepcopy( settings)
            pass

        c_by_file_name[c.fn]    = c
        if   cache_file_name and (c.hikes != None) and (not recompute) and (not len(c.hikes)) :
            s               = "File: %s" % fn
            s              += (" " * max(0, 70 - len(s))) + " cache-skipping."
            if  not quiet   :
                print s
            pass
        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) )
            if  not quiet   :
                print s
            _chk_tracks(c.hikes)
        else                :
            trks            = tz_gps.tracks_from_file(fn)               # read the file
            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, verbose = verbose)

                hikes       = tz_gps.extract_track_segments(hikes, c.settings, show_info = show_info)       # this does what we're running for
                _chk_tracks(hikes)

                hikes       = clip_at_start_end_point(hikes, c.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, verbose = verbose)

                _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          += "."
                if  not quiet   :
                    print s
                pass
            pass
        pass

    chikes  = tz_gps.remove_duplicate_tracks(chikes)

    cnt     = len(tracks)
    tracks  = tz_gps.remove_duplicate_tracks(tracks)
    dupecnt = (cnt - len(tracks))
    delcnt += dupecnt
    if  verbose and dupecnt :
        print "Duplicate tracks removed:", dupecnt


    _chk_tracks(tracks + chikes)


    if  delcnt :
        print "Excluded %u (snipped?) 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 in %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
                if  verbose     > 1 :
                    cnt         = tz_gps.count_points_in_tracks(tracks)
                    print "After join, %u track%s with %u point%s" % ( len(tracks), tzlib.s_except_1(tracks), cnt, tzlib.s_except_1(cnt), )
                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                                # the merge-to track's ending time
                    while tti   < len(tracks) :
                        if  when < tracks[tti].when :                                       # run tti past all tracks that start before the end of the ti'th track (remember the tracks are sorted by beginning times)
                            break
                        tti    += 1
                    if  tti     > ti + 1    :
                        pa      = tz_gps.merge_tracks_to_points(tracks[ti : tti])           # get the merged points
                        if  len(pa)         :
                            pa              = tz_gps.only_big_points_from_max_tracks(pa, tti - ti)      # this avoids tiny eddies in the track at the risk of getting rid of all the points
                        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 ]                                       # replace the separate tracks with the merged track
                        pass
                    ti         += 1
                if  verbose     > 1 :
                    cnt         = tz_gps.count_points_in_tracks(tracks)
                    print "After merge, %u track%s with %u point%s" % ( len(tracks), tzlib.s_except_1(tracks), cnt, tzlib.s_except_1(cnt), )
                pass
            # print " @@@@", len(tracks), len(tracks) and len(tracks[0].points)

            if  combine         :
                cnt             = tz_gps.count_points_in_tracks(tracks)
                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
                if  verbose     > 1 :
                    cnt         = tz_gps.count_points_in_tracks(tracks)
                    print "After combine, %u track%s with %u point%s" % ( len(tracks), tzlib.s_except_1(tracks), cnt, tzlib.s_except_1(cnt), )
                pass
            elif  smooth        :
                tracks          = tz_gps.make_big_points(tracks, cutoff_distance)
                # print " @@@@", len(tracks), len(tracks) and len(tracks[0].points), len(tracks) and tracks[0].points[0], len(tracks) and tracks[0].points[-1]
                _chk_tracks(tracks)
                tracks          = tz_gps.smooth_tracks(tracks)
                # print " @@@@", len(tracks), len(tracks) and len(tracks[0].points), len(tracks) and tracks[0].points[0], len(tracks) and tracks[0].points[-1]

            _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)
            # print " @@@@", len(stracks), len(stracks) and len(stracks[0].points), len(stracks) and stracks[0].points[0], len(stracks) and stracks[0].points[-1]
            _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)
        if  not cnt         :
            print "No points/tracks to write."
        else                :
            print "Writing %u point%s to %u track%s to..." % ( cnt, tzlib.s_except_1(cnt), len(tracks), tzlib.s_except_1(tracks) ),

            if  not split :
                print ofile_name
                tz_gps.write_gps_file(ofile_name, "hikify.py", creator, [], tracks, strip_spaces)
            else :
                ands            = ""
                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)

                    print ands, fn, time.asctime(time.localtime(t.when))
                    ands        = "  and "
                    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(p.when) ) )                        # set the file date/time to that of the 1st point
                        pass
                    pass
                pass
            pass
        pass

    _chk_tracks(tracks)

    if  cache_file_name :
        for c in c_by_file_name.values() :
            c.clear()                                   # forget old tracks for this file - we're going to reset them to however they've changed a couple lines of code down
        for t in tracks :
            c   = c_by_file_name[a_cache_item(t.file_name).fn]
            c.append(t)                                 # add this track to the file's cache hikes. 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
