#!/usr/bin/python
# -*- coding: latin1 -*-

# latlon.py
#       --copyright--                   Copyright 2007 (C) Tranzoa, Co. All rights reserved.    Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
#       --url--                         http://www.tranzoa.net/tzpython/
#       --email--                       pycode is the name to send to. tranzoa.com is the place to send to.
#       --bodstamps--
#       May 10, 2007            bar
#       July 5, 2007            bar     name typo
#       July 19, 2007           bar     feet distance
#       July 25, 2007           bar     metersPerNauticalMile
#       October 7, 2007         bar     parse lat lon (parially - needs lots of work with "s for seconds" and non-3-number stuff and "xE Ny" sorts of things)
#       October 12, 2007        bar     move lat lon routines from tz_gps to here
#       October 22, 2007        bar     more parsing stuff
#       November 15, 2007       bar     explicit lat= lon= parsing
#                                       and a lot else including returning the sign multiplier from the deg/min/sec split routines (as -0.00001 won't return a negative zero degrees)
#       November 18, 2007       bar     turn on doxygen
#       November 27, 2007       bar     insert boilerplate copyright
#       November 30, 2007       bar     note
#       December 5, 2007        bar     clean out spurious periods at the top of parsing
#                                       handle spurious commas better
#                                       tighter handling of colons and "degrees"
#                                       handle geo: url
#       December 15, 2007       bar     nsew_lat_lon_string
#       December 20, 2007       bar     limit functions
#       December 21, 2007       bar     distance_in_lat_lon_here
#       December 23, 2007       bar     slightly more exact milesPerNauticalMile
#                                       convert_lat_lon_to_x_y_z() and convert_x_y_z_to_lat_lon()
#       December 25, 2007       bar     allow long and lng
#       January 5, 2008         bar     fix calcDistance() for date-line crossings
#       March 9, 2008           bar     deg min.dec output routines
#       April 27, 2008          bar     'cause of Panaramio, add 0xa7(dos) and 0xba(unicode) to the list of degree characters
#       May 14, 2008            bar     signed_lat_lon_string()
#       May 16, 2008            bar     parse nmea lat lon values
#       May 17, 2008            bar     email adr
#       June 26, 2008           bar     an_xy_point and an_xyz_point
#       June 29, 2008           bar     better distance_from_line routine in 3d, along with ability to return the point that's closest
#       August 28, 2008         bar     round when converting to 10 thousanths of a minute
#       October 27, 2008        bar     feetPerMeter
#       April 11, 2009          bar     no 'as' named variable
#       November 18, 2009       bar     more stuff
#       January 1, 2011         bar     deal with unicode by making this file latin1 encoded
#       June 1, 2011            bar     try reversing NS/EW in case they are backward and we filtered it 'cause the NS was over 90
#                                       and do negative look-behind things instead of \b for start of nsew (note: haven't checked for all instances of them)
#       November 29, 2011       bar     pyflake cleanup
#       May 27, 2012            bar     doxygen namespace
#       July 7, 2012            bar     points are objects now
#       September 13, 2012      bar     points' arithemetic
#       October 2, 2012         bar     flat_distance_from for an xyz point
#       January 11, 2013        bar     fix doxygen namespace error
#       September 29, 2013      bar     z_angle_to()
#       November 9, 2013        bar     bring the nice name and comments here for dot product
#                                       cross product
#                                       normalize
#       October 13, 2014        bar     comment about calcDistance()
#       April 11, 2015          bar     rotate 2d point
#       August 19, 2015         bar     pyflakes
#       March 2, 2016           bar     fix some cmd line exceptions
#                                       fake things like 460012123 -910034123
#       March 31, 2016          bar     able to take utf8 strings
#       August 23, 2016         bar     better rotate() for an_xy_point()
#       April 23, 2017          bar     comment
#       April 28, 2020          bar     use math.hypot and ** when OK to do so
#       May 17, 2020            bar     distance_in_lat_lon_here - abs the values so they work in the eastern hemisphere
#       --eodstamps--
##      \file
#       \namespace              tzpython.latlon
#
#
#
#       Latitude Longitude stuff
#
#       Test output is in latlon.cmp (plus == None)
#
#       TODO:
#
#           parsing:
#
#               convert NSEW to north south east west (now that the #d #m #s fixing logic is really in)
#               then look explicitly for north south east west rather than fighting the single letter problem (which, well, now seems pretty well solved)
#
#               Common errors not handled yet:
#                   N 160, W 32             Reversing ew/ns
#                   45' 23", 101' 34"       ' and " for degrees, minutes
#
#           distance from point to plane ( https://www.khanacademy.org/math/linear-algebra/vectors-and-spaces/dot-cross-products/v/point-distance-to-plane ):
#               Plane defined by    A*x + B*y + C*z = D     where ABCD are constants and xyz is the location of any point on the plane.
#                                                           (What is a better way to get from 3 points on the plane to this ABCD representation of the plane than solving the linear equations?)
#               Point is            P.x P.y P.z
#               Distance is         ( (A * P.x) + (B * P.y) + (C * P.z) - D ) / math.sqrt(A*A + B*B + C*C)
#
#               Another way is:
#                   p1, p2, p3 are numpy array points on the plane (in [ x, y, z, ] form):
#                   n           = numpy.cross(p2 - p1, p3 - p1)
#                   n          /= numpy.linalg.norm(n)
#                   distance    = numpy.dot(xyz_point - p1, n)
#
#       BUGS:
#
#           "23 deg 17 min",        is goofed as lat/lon 23/17.
#
#

import  copy
import  math
import  re
import  sys


def lat_in_world(lat) :
    """ Return a value from -90 to +90. """

    return(min(90, max(-90, lat)))


def lat_in_world_modulo(lat) :
    """ Return a value from -90 to +90 assuming that arithmetic put a value over the pole. """

    if  -90 <= lat <= 90 :
        return(lat)

    m   = False
    if  lat < 0 :
        m   = True
        lat = -lat

    v       = int(lat)
    frac    = lat - v
    lat     = v % 360

    if   lat > 270 :
         lat = lat - 360
    elif lat > 90 :
         frac   = -frac
         lat = 180 - lat

    lat    += frac
    if  (not lat) and ((v % 180) == 90) :
        lat = 90.0

    if  lat >  90.0 :
        lat =  180.0 - lat
    elif lat < -90.0 :
        lat = -180.0 - lat

    if  m :
        lat = -lat

    return(lat)


def lon_in_world(lat) :
    """ Return a value from -180 to +180. """

    return(min(180, max(-180, lat)))


def lon_in_world_modulo(lon) :
    """ Return a value from -180 to +180 assuming that arithmetic put a value around the world. """

    if  -180 <= lon <= 180 :
        return(lon)

    m   = False
    if  lon < 0 :
        m   = True
        lon = -lon

    v       = int(lon)
    frac    = lon - v
    lon     = v % 360

    if   lon > 180 :
         lon = lon - 360

    lon    += frac
    if  (not lon) and ((v % 360) == 180) :
        lon = 180.0

    if  lon > 180.0 :
        lon = lon - 360.0

    if  m :
        lon = -lon

    return(lon)




ninety_degrees_counter_clockwise    = math.pi / 2.0

class an_xy_point(object)   :
    def __init__(me, x, y)  :
        me.x    = float(x)
        me.y    = float(y)

    def distance_from(me, p) :
        return(math.hypot(me.x - p.x, me.y - p.y))

    def distance_from_line(me, p1, p2) :
        n       = abs(((p2.x - p1.x) * (p1.y - me.y)) - ((p1.x - me.x) * (p2.y - p1.y)))
        d       = p1.distance_from(p2)

        try :
            d       = n / p1.distance_from(p2)
        except ValueError :
            return(me.distance_from(p1))
        except ZeroDivisionError :
            return(me.distance_from(p1))
        return(d)


    def rotate(me, a = ninety_degrees_counter_clockwise) :
        """ Rotate the point 90 degrees counter-clockwise around 0,0. (Note the unfortunate historical angle default. The angle is in radians, counter-clockwise. Zero angle is 3 o-clock, for None purposes.) """
        if  a is None   :
            a           = math.atan2(me.y, me.x)
        if  a          == ninety_degrees_counter_clockwise :
            me.x, me.y  = -me.y, me.x           # go fast
        else            :
            s           = math.sin(a)
            c           = math.cos(a)
            me.x, me.y  = (me.x * c) - (me.y * s), (me.x * s) + (me.y * c)
        pass


    def __str__(me) :
        return("%f,%f" % ( me.x, me.y ) )


    pass        # an_xy_point


class an_xyz_point(object) :
    """ A point in 3-space. """

    def __init__(me, x, y, z) :
        me.x    = float(x)
        me.y    = float(y)
        me.z    = float(z)

    def flat_distance_from(me, p) :
        """ Note: See this notes about converting lat/lon to XYZ. Y isn't what you would expect in such a case and this routine won't be meaningful. """
        return(math.hypot(me.x - p.x, me.y - p.y))

    def distance_from(me, p) :
        return(math.sqrt(((me.x - p.x) ** 2) + ((me.y - p.y) ** 2) + ((me.z - p.z) ** 2)))

    def dist_from_line(me, p1, p2) :
        """ http://solvedproblems.wordpress.com/2008/03/11/distance-of-a-point-from-a-line-in-3d/ """

        ab      = p1.distance_from(p2)
        if  ab == 0.0 :
            return(me.distance_from(p1))

        ap      = p1.distance_from(me)
        bp      = p2.distance_from(me)
        s       = (ab + ap + bp) / 2.0
        try :
            d       = 2.0 * math.sqrt(s * abs(s - ab) * abs(s - ap) * abs(s - bp)) / ab
        except ValueError :
            return(me.distance_from(p1))
        except ZeroDivisionError :
            return(me.distance_from(p1))

        return(d)

    def z_angle_to(me, p) :
        """ Return the radian angle in Z to the given point. If p is lower than me, the angle is negative. Converted to degrees, the angle ranges from -90 to 90. """
        d   = me.distance_from(p)
        if  not d :
            return(None)
        return(math.asin((p.z - me.z) / d))


    def dot(me, p) :
        """
            Return the square of how far along this point's "line" the other point is if the two points are considered to be vectors from the same 0, 0, 0, location in space.

            Note: The return value is not normalized to be relative to this point's "line" length.

        """
        return((me.x * p.x) + (me.y * p.y) + (me.z * p.z))
    dot_product = dot


    def cross(me, p) :
        """ Return an_xyz_point that's orthoganal to this point and another, assuming the two points are referenced from the same 0, 0, 0 location in space. """
        return(an_xyz_point((p.y * me.z) - (p.z * me.y), (p.z * me.x) - (p.x * me.z), (p.x * me.y) - (p.y * me.x)))
    cross_product   = cross


    def normalize(me) :
        """ Normalize our location as a vector from 0, 0, 0. """
        ln          = math.sqrt((me.x ** 2) + (me.y ** 2) + (me.z ** 2))
        if  ln      :
            d       = (1.0 / ln)
            me.x   *= d
            me.y   *= d
            me.z   *= d
        pass


    def closest_point_on_line(me, p1, p2) :
        """ http://geometryalgorithms.com/Archive/algorithm_0102/algorithm_0102.htm """

        v       = an_xyz_point(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z)
        w       = an_xyz_point(me.x - p1.x, me.y - p1.y, me.z - p1.z)

        c1      = w.dot(v)
        c2      = v.dot(v)
        try :
            b   = c1 / c2
        except ZeroDivisionError :
            return(me.distance_from(p1))

        return(an_xyz_point(p1.x + b * v.x, p1.y + b * v.y, p1.z + b * v.z))

    def distance_from_line(me, p1, p2) :
        return(me.distance_from(me.closest_point_on_line(p1, p2)))


    def __mul__(me, m) :
        m       = float(m)
        me      = copy.deepcopy(me)
        me.x   *= m
        me.y   *= m
        me.z   *= m
        return(me)

    def __sub__(me, om) :
        me      = copy.deepcopy(me)
        me.x   -= om.x
        me.y   -= om.y
        me.z   -= om.z
        return(me)

    def __add__(me, om) :
        me      = copy.deepcopy(me)
        me.x   += om.x
        me.y   += om.y
        me.z   += om.z
        return(me)

    def __neg__(me) :
        me      = copy.deepcopy(me)
        me.x    = -me.x
        me.y    = -me.y
        me.z    = -me.z
        return(me)


    def __str__(me) :
        return("%f,%f,%f" % ( me.x, me.y, me.z ) )


    pass        # an_xyz_point





#       http://www.geomidpoint.com/calculation.html is the best description I found

def convert_lat_lon_to_x_y_z(lat, lon) :
    """
        Convert a latitude and longitude on the Earth's surface to X Y and Z cartesian values.

        X is -1 at 180 degrees longitude, 1 at Greenwich Meridian.
        Y is -1 toward the viewer, 1 away.
        Z is -1 at south pole (-90 degrees latitude), 1 at the north pole.

    """

    lat     = math.radians(lat)
    lon     = math.radians(lon)

    z       = math.sin(lat)
    cz      = math.cos(lat)

    y       = math.sin(lon) * cz
    x       = math.cos(lon) * cz

    return( ( x, y, z ) )


def convert_lat_lon_to_xyz_point(lat, lon) :
    """ Convert a latitude and longitude in to X Y and Z cartesian values - where 1.0 is the surface of the earth (this might change). """

    ( x, y, z)  = convert_lat_lon_to_x_y_z(lat, lon)
    return(an_xyz_point(x, y, z))



def convert_x_y_z_to_lat_lon(x, y, z) :
    """ Convert an X Y and Z point back to latitude and longitude. """

    lon     = math.atan2(y, x)

    d       = math.hypot(x, y)
    lat     = math.atan2(z, d)
    #   lat = math.asin(z)                      # note: though inside -1.0 <= z <= 1.0 this doesn't raise an exception, and it gets the right value if we're just inverting original values, consider the effects of if xyz are averaged

    return( ( math.degrees(lat), math.degrees(lon) ) )

def convert_xyz_point_to_lat_lon_xy_point(p) :
    """ Convert an X Y and Z point back to latitude and longitude. """

    ( x, y )    = convert_x_y_z_to_lat_lon(p.x, p.y, p.z)

    return(an_xy_point(x, y))



#
#   Quicky from http://www.zachary.com/s/blog/2005/01/12/python_zipcode_geo-programming
#
#   It assumes an incorrect sherical model of the earth. (Note: The net code doesn't handle -180 crossovers. It simply did lon2-lon1.)
#

nauticalMilePerLat          = 60.00721
nauticalMilePerLongitude    = 60.10793
rad                         = math.pi / 180.0           # multiply to get math.radians() and divide to get math.degrees() - still exists in case someone uses it from another module.

def calcDistance(lat1, lon1, lat2, lon2):
    """
        Caclulate Earth-surface distance between two lat lons in nautical miles.

        Note:
            lat1        = math.radians(lat1)
            lat2        = math.radians(lat2)
            lon1        = math.radians(lon1)
            lon2        = math.radians(lon2)
            distance    = math.acos((math.sin(lat1) * math.sin(lat2)) + (math.cos(lat1) * math.cos(lat2) * math.cos(lon2 - lon1))) * 6225

            But, though it's from the Internet, it's not right. Try it for lon1=0 lon2=180 lat1=lat2=0. It won't be anywhere near 10819.4274.

        Note: This routine underlies tz_gps.a_point().{flat_}distance_from().

    """

    yDistance   = (lat2 - lat1) * nauticalMilePerLat

    lon1        = lon_in_world_modulo(lon1)
    lon2        = lon_in_world_modulo(lon2)
    if    lon2  < lon1      :
        ( lon1,   lon2 )    = ( lon2, lon1 )
    ld          = min(lon2  -   lon1, lon1 + 360 - lon2)

    xDistance   = (math.cos(math.radians(lat1)) + math.cos(math.radians(lat2))) * ld * (nauticalMilePerLongitude / 2.0)

    distance    = math.hypot(yDistance, xDistance)

    return(distance)



def distance_in_lat_lon_here(at_lat, at_lon, distance) :
    """
        Return how many degrees one would go in latitude if one moved the given distance.
        And how many degrees in longitude.
    """

    lat         = abs((((at_lat * nauticalMilePerLat) + distance) / nauticalMilePerLat) - at_lat)

    lon         = abs((distance / calcDistance(at_lat, at_lon, at_lat, at_lon + 1.0))   - at_lon)

    return( ( lat, lon ) )



milesPerNauticalMile        = 1.150779
metersPerNauticalMile       = 1852.0
feetPerMeter                = 3.2808399



nauticalEarthRadius         = 3438.145          # (equator 3,443.918 nmi + polar 3,432.372 nmi) / 2

if  False :
    mp1                     = an_xy_point(0,         0)
    mp2                     = an_xy_point(0.0000001, 0)
    mpd                     = calcDistance(mp1.x, mp1.y, mp2.x, mp2.y)
    xyzd                    = convert_lat_lon_to_xyz_point(mp1.x, mp1.y).distance_from(convert_lat_lon_to_xyz_point(mp2.x, mp2.y))
    nauticalEarthRadius     = mpd / xyzd        # comes out about the same
    print "radius", nauticalEarthRadius

    sys.exit(1)


def lat_lon_in_degrees_minutes_10_thousandth_minutes(ll) :
    """
        Note that the proper degrees is abs([0]) * [2], which can be negative zero
    """

    neg     =  1
    if  ll < 0.0 :
        ll  = -ll
        neg = -1

    f       = float(ll) - float(int(ll))

    m       = f * 60.0
    h       = int((m - int(m)) * 10000.0 + 0.5)

    if  neg < 0 :
        ll  = -ll

    return( [ int(ll), int(m), h, neg ] )





def lat_lon_in_degrees_minutes(ll) :
    """
        Note that the proper degrees is abs([0]) * [2], which can be negative zero
    """

    neg     =  1
    if  ll < 0.0 :
        ll  = -ll
        neg = -1

    m       = (float(ll) - float(int(ll))) * 60.0

    if  neg < 0 :
        ll  = -ll

    return([ int(ll), m, neg ] )


def lat_lon_degrees_minutes_string(ll, plus, ne, sw) :

    #   This returns a string up to 14 or 15 characters in length

    dr          = ""
    if  plus   == None :
        dr      = ne
        if  ll  < 0.0 :
            ll  = -ll
            dr  = sw
        plus    = ""
    elif plus  != "+" :
        plus    = ""

    dm          =   lat_lon_in_degrees_minutes(ll)

    if  (not dr) and (dm[0] >= 0) and (dm[2] < 0) :           # special case of when the degrees are zero to the west or south and he asked for a +- format
        return(("  -0 %8.6f'%s") % ( dm[1], dr ) )

    return(("%" + plus + "4i %8.6f'%s") % ( dm[0], dm[1], dr ) )


def lat_degrees_minutes_string(lat, plus = None) :
    return(lat_lon_degrees_minutes_string(lat, plus, " N", " S"))

def lon_degrees_minutes_string(lon, plus = None) :
    return(lat_lon_degrees_minutes_string(lon, plus, " E", " W"))






def lat_lon_in_degrees_minutes_seconds(ll) :
    """
        Note that the proper degrees is abs([0]) * [3], which can be negative zero
    """

    neg     =  1
    if  ll < 0.0 :
        ll  = -ll
        neg = -1

    m       = (float(ll) - float(int(ll))) * 60.0
    s       = (float(m)  - float(int(m ))) * 60.0

    if  neg < 0 :
        ll  = -ll

    return([ int(ll), int(m), s, neg ] )


def lat_lon_degrees_minutes_seconds_string(ll, plus, ne, sw) :

    #   This returns a string up to 17 or 16 characters in length

    dr          = ""
    if  plus   == None :
        dr      = ne
        if  ll  < 0.0 :
            ll  = -ll
            dr  = sw
        plus    = ""
    elif plus  != "+" :
        plus    = ""

    dms         =   lat_lon_in_degrees_minutes_seconds(ll)

    if  (not dr) and (dms[0] >= 0) and (dms[3] < 0) :           # special case of when the degrees are zero to the west or south and he asked for a +- format
        return(("  -0 %2u\' %6.3f\"%s") % ( dms[1], dms[2], dr ) )

    return(("%" + plus + "4i %2u\' %6.3f\"%s") % ( dms[0], dms[1], dms[2], dr ) )


def lat_degrees_minutes_seconds_string(lat, plus = None) :
    return(lat_lon_degrees_minutes_seconds_string(lat, plus, " N", " S"))

def lon_degrees_minutes_seconds_string(lon, plus = None) :
    return(lat_lon_degrees_minutes_seconds_string(lon, plus, " E", " W"))




def signed_lat_lon_string(ll) :
    return("%.8f" % ll)



def lat_lon_string(ll, plus, ne, sw) :

    #   This returns a string up to 14 or 13 characters in length

    dr          = ""
    if  plus   == None :
        dr      = ne
        if  ll  < 0.0 :
            ll  = -ll
            dr  = sw
        plus    = ""

    return(("%" + plus + ".8f%s") % ( ll, dr ) )


def lat_string(lat, plus = None) :
    return(lat_lon_string(lat, plus, " N", " S"))

def lon_string(lon, plus = None) :
    return(lat_lon_string(lon, plus, " E", " W"))


def nsew_lat_lon_string(lat, lon) :
    ts  = lat_string(lat)
    os  = lon_string(lon)

    #   kludge - this is what happens when you try to use test routines in real life
    if  (ts[len(ts) - 4:len(ts) - 2] == "00") and (os[len(os) - 4:len(os) - 2] == "00") :
        ts  = ts[0:-4] + ts[len(ts) - 2:]
        os  = os[0:-4] + os[len(os) - 2:]

    return(ts + "  " + os)





def degrees_minutes_seconds_to_lat_lon(d, m, s, sign_mult = 1) :
    if  (d < 0.0) or (sign_mult < 0) :
        m   = -m
        s   = -s

    return(d + (m / 60.0) + (s / 3600.0))






no_dots_re      = re.compile(r"(^|[^\d])\.([^\d]|$)")

#
#               Note: These are used in projects\mapping\*.py and maybe other places
#
dec_rs          = r"(?:\d+\.\d*|\.\d+|\d+)"
plus_min_dec_rs = r"[+-]?" + dec_rs

dec_only_rs     = r"(?:\d*\.\d+)"


dg_sep_rs       = unicode(r"(?:(?:d(?:eg(?:rees?)?)?|\xa7|\xb0|\xba|\xf8),?)", 'latin1')
deg_sep_rs      = r"\s*"       + dg_sep_rs
cln_deg_sep_rs  = r"\s*(?:\:|" + dg_sep_rs + ")"

mn_sep_rs       = r"(?:m(?:in(?:utes?)?)?|')"
min_sep_rs      = r"\s*"       + mn_sep_rs
cln_min_sep_rs  = r"\s*(?:\:|" + mn_sep_rs + ")"

sc_sep_rs       = r'(?:sec(?:onds?)?|")'
sec_sep_rs      = r"\s*"       + sc_sep_rs
cln_sec_sep_rs  = r"\s*(?:\:|" + sc_sep_rs + ")"
s_sec_sep_rs    = r"\s*(?:"    + sc_sep_rs + "|s)"


dms_from_re     = re.compile(r"([+-]?\d+\s*)" + deg_sep_rs + "\s*((?:[nsew])?\s*(?:\d+)\s*)" + min_sep_rs + "\s*(" + dec_rs + ")\s*" + s_sec_sep_rs)
dms_to_rs       = r"\1 deg \2 min \3 sec "

dM_from_re      = re.compile(r"([+-]?\d+\s*)" + deg_sep_rs + "\s*((?:[nsew])?\s*" + dec_only_rs + ")" + min_sep_rs + "(\s*[nsew,:]|$)")
dM_to_rs        = r"\1 deg \2 min 0 sec \3"

dm_from_rs      = r"([^\d]*\d+[^\.][^\d]*)(" + dec_rs + ")" + min_sep_rs + "([^\d]*)"
dm_from_re      = re.compile(r"^" + dm_from_rs + dm_from_rs + "$")
dm_to_rs        = r"\1\2 min 0 sec \3\4\5 min 0 sec \6"

ms_from_re      = re.compile(r"((?:\d+)\s*)m\s*(" + dec_rs + ")\s*" + s_sec_sep_rs)
ms_to_rs        = r"0 deg \1 min \2 sec "


nd_dms_from_re  = re.compile(r"(^|[^\.\d])([+-]?\d{1,3})(\d\d)(\d\d(?:\.\d*)?)")
nd_dms_to_rs    = r"\1\2 deg \3 min \4 sec "

nd_dm_from_re   = re.compile(r"(^|[^\.\d])([+-]?\d{2,3})(\d\d(?:\.\d*)?)")
nd_dm_to_rs     = r"\1\2 deg \3 min "

if  False :
    dms_rs      = r"\s*([%s])?\s*([+-]?\d+)\s*d\s*([%s])?\s*(\d+)\s*m\s*(" + dec_rs + ")\s*s\s*([%s])?"

    dms_ns      = dms_rs % ( "ns", "ns", "ns" )
    dms_ew      = dms_rs % ( "ew", "ew", "ew" )

    dms_ns_re   = re.compile(dms_ns)
    dms_ew_re   = re.compile(dms_ew)

    dms_nsew_re = re.compile(dms_ns + r"\s*[,:]?" + dms_ew)
    dms_ewns_re = re.compile(dms_ew + r"\s*[,:]?" + dms_ns)

order_o_re      = re.compile(r"^(.*)(?<![a-z])([nsew])\b.+?(?<![a-z])([nsew])\b(.*)$")


decimal_rs      = r"\s*([nsew])?\s*(" + plus_min_dec_rs + ")" + cln_deg_sep_rs + "?\s*([nsew])?"

decimal_re      = re.compile(decimal_rs)
value_re        = re.compile(decimal_rs + "(?:\s*(" + dec_rs + ")" + cln_min_sep_rs + "?(?:\s*(" + dec_rs + ")" + cln_sec_sep_rs + "?)?)?\s*([nsew])?")

nsew_re         = re.compile(r"\b[nsew]\b")

split_on_deg_re = re.compile(r"^(.*?(?:" + plus_min_dec_rs + ")?" + deg_sep_rs + ".*?)(" + plus_min_dec_rs + "" + deg_sep_rs + ".*\s*[nsew]?.*)$")

exp_ll_rs       = r"\b%s[a-z]*\s*[=:]?\s*[\"\']?\s*(" + plus_min_dec_rs + ")\s*[\"\']?"

exp_lat_re      = re.compile(exp_ll_rs % ( "la?t" ) )
exp_lon_re      = re.compile(exp_ll_rs % ( "lo?ng?" ) )

geo_re          = re.compile(r"\bgeo:(" + plus_min_dec_rs + "),(" + plus_min_dec_rs + ")")

nmea_re         = re.compile(r"(\d?\d)(\d\d)\.(\d+)\s*,\s*(N|S)\s*,\s*(\d?\d?\d)(\d\d)\.(\d+)\s*,\s*(E|W)")

xyz_re          = re.compile(r"\bX\s*=\s*(" + plus_min_dec_rs + r")\s*Y\s*=\s*(" + plus_min_dec_rs + r")\s*Z\s*=\s*(" + plus_min_dec_rs + ")", re.IGNORECASE)



def fix_lat_lon(lat, lon, order = None) :
    if  (lat == None) or (lat == None) :
        return( [ None, None ] )

    if  order == 2 :                            # lon/lat order
        ( lat, lon )    = ( lon, lat )
    elif order == None :                        # figure out the order
        if  not ( -90.0 <= lat <= 90.0) :
            ( lat, lon )    = ( lon, lat )
        pass

    if  not ( -90.0 <= lat <= 90.0) :
        return( [ None, None ] )

    if  -360.0 <= lon <  -180.0 :
        lon     = lon +   360.0                 # take Far Far Eastern Airlines (note: do not use lon_in_world_modulo() here - we're trying to ignore lon numbers that are not longitude numbers)

    if   180.0 <  lon <=  360.0 :
        lon    -=         360.0                 # go west, young man

    if  not (-180.0 <= lon <= 180.0) :
        return( [ None, None ] )

    return( [ lat, lon ] )



def string_degrees_minutes_seconds_to_lat_lon(degs, mins, secs) :
        sm      =  1
        if  degs and (degs[0] == '-') :
            sm  = -1

        return(degrees_minutes_seconds_to_lat_lon(float(degs), float(mins or 0.0), float(secs or 0.0), sm))


def lat_lon_value_parse(s) :
    g   = value_re.match(s)

    if  g :
        nsew    = (g.group(1) or "") + (g.group(3) or "") + (g.group( 6) or "")
        ll      = string_degrees_minutes_seconds_to_lat_lon(g.group(2), g.group( 4), g.group( 5))

        if  not nsew :
            return( ( ll, None ) )

        if  len(nsew) == 1 :
            if  (ll > 0.0) and ((nsew  == "s") or (nsew == 'w')) :
                ll  = -ll

            if  (nsew  == "n") or (nsew == 's') :
                return( ( ll, 1 ) )

            return( ( ll, 2 ) )

        return( ( ll, None ) )


    return( ( None, None ) )



def _g2_things(g) :
    nsew    = (g.group(1) or "") + (g.group(3) or "") + (g.group( 6) or "") + (g.group(7) or "") + (g.group(9) or "") + (g.group(12) or "")
    lat     = string_degrees_minutes_seconds_to_lat_lon(g.group(2), g.group( 4), g.group( 5))
    lon     = string_degrees_minutes_seconds_to_lat_lon(g.group(8), g.group(10), g.group(11))

    return( ( nsew, lat, lon ) )



front_nsew_re   = re.compile(r"^([nsew].*?)(\b[nsew]\b)\s*$")
def _fix_front_nsew(v1, v2) :

    g       = front_nsew_re.match(v1)
    if  g :
        v1  = g.group(1)
        v2  = g.group(2) + v2

    return( ( v1, v2 ) )


def _2_values(v1, v2, order) :
    ( lat, ao )  = lat_lon_value_parse(v1)
    if  lat != None :
        ( lon, oo )  = lat_lon_value_parse(v2)
        if  lon != None :
            if  order == None :
                if  (ao == 2) or (oo == 1) :
                    order   = 2
                pass
            ( lat, lon )    = fix_lat_lon(lat, lon, order)
            if  lat != None :
                return( [ lat, lon ] )
            pass
        pass

    return( ( None, None ) )



def _2_sorta_values(s, order) :
    g   = value_re.match(s)
    if  g :
        e   = g.end(0)
        if  (g.group(1) or "") and ((g.group(3) or "") + (g.group(6) or "")):
            e       = g.end(2)
        elif  ((g.group(1) or "") + (g.group(3) or "")) and g.group(6) :
            e       = g.end(5)

        ss          = s[g.start(0):e]
        ( lat, ao ) = lat_lon_value_parse(ss)
        if  lat != None :
            ( lat, lon )    = _2_values(ss, s[e:], order)
            if  lat != None :
                return( [ lat, lon ] )
            pass

        pass

    return( [ None, None ] )



def _parse_lat_lon(s) :
    """
        Parse a string that may have a latitude longitude or vice versa in it.

        Return an array [ lat, lon ] or [ None, None ].
    """

    g   = nmea_re.search(s)
    if  g :
        lat         = int(g.group(1)) + ((int(g.group(2)) + (int((g.group(3) + "000")[0:4]) / 10000.0)) / 60.0)
        if  g.group(4).upper() == 'S' :
            lat     = -lat

        lon         = int(g.group(5)) + ((int(g.group(6)) + (int((g.group(7) + "000")[0:4]) / 10000.0)) / 60.0)
        if  g.group(8).upper() == 'W' :
            lon     = -lon

        return( [ lat, lon ] )


    g   = geo_re.search(s)
    if  g :
        ( lat, lon )    = fix_lat_lon(float(g.group(1)), float(g.group(2)))
        if  lat != None :
            return( [ lat, lon ] )
        pass

    s       = s.lower()

    g       = xyz_re.search(s)
    if  g   :
        return(convert_x_y_z_to_lat_lon(float(g.group(1)), float(g.group(2)), float(g.group(3))))


    while True :
        ns  = no_dots_re.sub(r"\1 \2", s)
        if  ns == s :
            break
        s   = ns

    s       = s.strip(",")

    #
    #   Someone used stars for degree characters
    #
    s       = s.replace(u"*", unicode('\xb0', 'latin1'))

    s       = s.replace(u"&amp;#730;", unicode("\xb0", 'latin1'))    # doesn't seem to be needed (ah. the unk report that had this was for a bad-in-other-way value)

    #
    #   Parse out explicit "lat=" and "lon=" and "lat:" and "lon:"
    #
    lag     = exp_lat_re.search(s)
    if  lag :
        lng = exp_lon_re.search(s)
        if  lng :
            ( lat, lon )    = fix_lat_lon(float(lag.group(1)), float(lng.group(1)), 1)
            if  lat != None :
                return( [ lat, lon ] )
            pass
        pass

    if  False :
        sst = dm_from_re.sub(dm_to_rs, s)
        if  sst != s :
            print "diff"
            print "   ", s
            print "   ", sst
        pass

    s   =     dm_from_re.sub(    dm_to_rs, s)
    s   =    dms_from_re.sub(   dms_to_rs, s)
    s   =     ms_from_re.sub(    ms_to_rs, s)
    s   =     dM_from_re.sub(    dM_to_rs, s)

    if  len(nd_dms_from_re.findall(s)) == 2 :
        s   = nd_dms_from_re.sub(nd_dms_to_rs, s)
    if  len(nd_dm_from_re.findall(s))  == 2 :
        s   = nd_dm_from_re.sub(nd_dm_to_rs, s)


    s   = re.sub(r"(?<![a-z])n[a-z]*", "north", s)
    s   = re.sub(r"(?<![a-z])e[a-z]*", "east", s)
    s   = re.sub(r"(?<![a-z])w[a-z]*", "west", s)
    # s   = re.sub(r"(?<![a-z])s[a-z]*", "south", s)        # note: sec isn't south

    s   = re.sub(r"(?<![a-z])no(r(th?)?)?(?![a-z])",   "n", s)
    s   = re.sub(r"(?<![a-z])so(u(th?)?)?(?![a-z])",   "s", s)
    s   = re.sub(r"(?<![a-z])ea(st?)?(?![a-z])",       "e", s)
    s   = re.sub(r"(?<![a-z])we(st?)?(?![a-z])",       "w", s)

    if  False :
        #
        #
        #   Problem is that the "s" in "12d 34m 56s" can be confused with the "s" in "South"
        #
        #
        g           = dms_nsew_re.match(s)
        if  g :
            ( nsew, lat, lon )  = _g2_things(g)
            if  len(nsew) < 3 :
                i       = 0

                if  i < len(nsew) :
                    if  nsew[i] == 's' :
                        if  lat > 0.0 :
                            lat = -lat
                        i  += 1
                    elif nsew[i] == 'n' :
                        i  += 1
                    pass

                if  i < len(nsew) :
                    if  nsew[i] == 'w' :
                        if  lon > 0.0 :
                            lon = -lon
                        i  += 1
                    elif nsew[i] == 'e' :
                        i  += 1
                    pass

                if  i >= len(nsew) :
                    ( lat, lon ) = fix_lat_lon(lat, lon, 1)
                    if  lat != None :
                        return( [ lat, lon ] )
                    pass

                pass

            pass

        #
        #   Try #d #m #s in lon lat order
        #
        g           = dms_ewns_re.match(s)
        if  g :
            ( ewns, lon, lat )  = _g2_things(g)

            if  len(ewns) < 3 :
                i       = 0

                if  i < len(ewns) :
                    if  ewns[i] == 'w' :
                        if  lon > 0.0 :
                            lon = -lon
                        i  += 1
                    elif ewns[i] == 'e' :
                        i  += 1
                    pass

                if  i < len(ewns) :
                    if  ewns[i] == 's' :
                        if  lat > 0.0 :
                            lat = -lat
                        i  += 1
                    elif ewns[i] == 'n' :
                        i  += 1
                    pass

                if  i >= len(ewns) :
                    ( lat, lon ) = fix_lat_lon(lat, lon, 1)
                    if  lat != None :
                        return( [ lat, lon ] )
                    pass

                pass
            pass
        pass


    #
    #
    #   Let's get the order, lat/lon or lon/lat, if we can, easily
    #
    #
    order   = None
    g       = order_o_re.match(s)
    if  g :
        if  (not nsew_re.search(g.group(1))) and (not nsew_re.search(g.group(4))) :
            m1  = g.group(2)
            m2  = g.group(3)
            if    ((m1 == 'n') or (m1 == 's')) and ((m2 == 'e') or (m2 == 'w')) :
                order   = 1
            elif  ((m1 == 'e') or (m1 == 'w')) and ((m2 == 'n') or (m2 == 's')) :
                order   = 2
        pass



    #
    #
    #   Maybe the degrees numbers can be distinguished to help us split the two values apart
    #
    #
    g   = split_on_deg_re.match(s)
    if  g :
        # print "split"
        ( lat, lon )    = _2_sorta_values(s, order)
        if  lat != None :
            # print "2split", s, order
            return( [ lat, lon ] )

        # print "not 2sorta", s, order, g.group(1), g.group(2)
        ( v1, v2 )      = _fix_front_nsew(g.group(1), g.group(2))

        # print "v1", v1, "v2", v2

        ( lat, lon )    = _2_values(v1, v2, order)
        if  lat != None :
            return( [ lat, lon ] )
        pass


    #
    #
    #   If he put a single comma or / or \ or ; in, we can easily split the lat and lon apart
    #
    #
    sa  = [ vs for vs in re.split(r"[;,\\/][;,\\/\s]*", s) if vs ]
    if  len(sa) == 2 :
        ( lat, lon )    = _2_values(sa[0], sa[1], order)
        if  lat != None :
            return( [ lat, lon ] )
        pass

    s   =   s.replace(",", " ")
    s   =   s.replace(";", " ")


    if  False :
        #
        #
        #   Maybe we can find a single #d #m #s value somewhere (we'll do it first because of the ambiguity of 's')
        #
        #
        g   = dms_ns_re.search(s)
        if  g :
            # extract what's before and after the #d#m#s value
            v1  = s[0 : g.start()]
            v2  = s[g.end() : ]

            ( lat, lon )    = _2_values(v1, v2, order)
            if  lat != None :
                return( [ lat, lon ] )
            pass

        g   = dms_ew_re.search(s)
        if  g :
            # extract what's before and after the #d#m#s value
            v1  = s[0 : g.start()]
            v2  = s[g.end() : ]

            ( lat, lon )    = _2_values(v2, v1, order)
            if  lat != None :
                return( [ lat, lon ] )
            pass
        pass




    #
    #   See about just two numbers (we'll go with the order as lat lon, but will recognize one of 'em being <-90.0 or >90.0 to fix that)
    #
    va  = [ vs  for vs in re.split(r"\s", s) if vs ]
    if  len(va) == 2 :
        ( lat, lon )    = _2_values(va[0], va[1], order)
        if  lat != None :
            return( [ lat, lon ] )
        pass



    ( lat, lon )        = _2_sorta_values(s, order)
    if  lat != None :
        return( [ lat, lon ] )


    g   = decimal_re.match(s)
    if  g :
        ss          = s[g.start(0):g.end(0)]
        ( lat, ao ) = lat_lon_value_parse(ss)
        if  lat != None :
            ( lat, lon )    = _2_values(ss, s[g.end(0):], order)
            if  lat != None :
                return( [ lat, lon ] )
            pass

        pass

    return( [ None, None ] )


def parse_lat_lon(s) :
    """
        Parse a string that may have a latitude longitude or vice versa in it.

        Return an array [ lat, lon ] or [ None, None ].
    """

    s                   = convert_to_unicode(s)
    so                  = s
    ( lat, lon )        = _parse_lat_lon(s)
    if  lat            == None :
        s               = re.sub(r"(?i)(?<![a-z])([ns][a-z]*)(.*?)(?<![a-z])([ew][a-z]*)", r"\3\2\1", s)
        ( lat, lon )    = _parse_lat_lon(s)
    if  lat            == None :
        s               = re.sub(r"(?i)(?<![a-z])([ew][a-z]*)(.*?)(?<![a-z])([ns][a-z]*)", r"\3\2\1", s)
        ( lat, lon )    = _parse_lat_lon(s)
    if  lat            == None :
        g               = re.search(r'^([+-]?\d+)\s+,?\s*([+-]?\d+)', so.strip())
        if  g           :
            lat, lon    = _parse_lat_lon("%.3f %.3f" % ( int(g.group(1)) / 1000.0, int(g.group(2)) / 1000.0, ))     # this could be improved
        pass

    return( [ lat, lon ] )



def convert_to_unicode(s) :     # from tzlib.py
    """ Try to convert the given string to unicode as best we can guess. """
    if  not isinstance(s, unicode) :
        try :
            s   = unicode(s.decode('utf8'))     # this blows up on most latin1 chars - so we hope such is the case if the string is latin 1
        except UnicodeDecodeError :
            s   = unicode(s.decode('latin1'))   # bail out
        pass
    return(s)








#
#
#   Test code.
#
#
if __name__ == '__main__' :
    import  TZCommandLineAtFile

    del(sys.argv[0])

    TZCommandLineAtFile.expand_at_sign_command_line_files(sys.argv)

    test_parse  = True


    ############################################################
    def lls(lat, lon, plus) :
        ds      = "%14s %14s" % ( lat_string(                        lat, plus = plus), lon_string(                        lon, plus = plus) )
        dmss    = "%17s %17s" % ( lat_degrees_minutes_seconds_string(lat, plus = plus), lon_degrees_minutes_seconds_string(lon, plus = plus) )
        dms     = "%14s %14s" % ( lat_degrees_minutes_string(        lat, plus = plus), lon_degrees_minutes_string(        lon, plus = plus) )
        return( [ ds, dmss, dms ] )

    def show_latlon(vs, lat, lon) :
        vs                  = convert_to_unicode(vs)
        ( ds, dmss, dms )   = lls(lat, lon, None)
        print ("%-62s %s   %s" % ( vs, ds, dmss )).encode('utf8')
    ############################################################

    while len(sys.argv) :
        test_parse  = False

        lsa         = sys.argv.pop(0)

        if  not sys.argv    :
            ( lat, lon )    = parse_lat_lon(lsa)
            if  lat == None :
                print "NO PARSE", lsa
            else :
                show_latlon(lsa, lat, lon)
            break

        lso             = sys.argv.pop(0)

        ( lat1, lon1 )  = parse_lat_lon(lsa)
        ( lat2, lon2 )  = parse_lat_lon(lso)
        if  (lat1 is None) and (lat2 is None) :
            lsa        += "," + lso
            lat, lon    = parse_lat_lon(lsa)
            if  lat == None :
                print "NO PARSE", lsa
            else :
                show_latlon(lsa, lat, lon)
            pass
        else    :
            if  lat2 == None    :
                try             :
                    d           = float(lso)
                    lat2, lon2  = distance_in_lat_lon_here(lat1, lon1, d)
                    lat2       += lat1
                    lon2       += lon1
                except          :
                    print "Not a distance from %s : %s!" % ( show_latlon(lsa, lat1, lon1), lso, )
                    continue
                pass

            print "From %.6f, %.6f to %.6f, %.6f :" % ( lat1, lon1, lat2, lon2 )

            d   = calcDistance(lat1, lon1, lat2, lon2)
            print "%11.2f nautical miles" % ( d )

            d  *= milesPerNauticalMile
            print "%11.2f miles" % ( d )

            d  *= 5280
            print "%11.2f feet" % ( d )
        pass
    pass



    va  = [
        " 47 deg 17' 49.23\" N, 121 deg 45' 57.33\" W ",
        " 121 deg 45' 57.33\" W, 47 deg 17' 49.23\" N ",
        " 121 deg 45' 57.\" W, 47 deg 17' 49.\" N ",
        " 121 deg 45' 57.\"W, 47 deg 17' 49.\"N ",

        " 31.243333,-7.989808 ",
        " 31.243333/-7.989808 ",
        " 31.243333\\-7.989808 ",
        " 31.243333  / / -7.989808 ",
        " 31.243333\\ \\ \\-7.989808 ",
        " 31.12345N,     65.948862W ",
        " 65.948862W, 31.12345N     ",
        " 31.12345s,     65.948862e ",
        " 65.948862e, 31.12345s     ",
        "31N16 21,      65W58 36",
        "31.12345N,     65.948862W",
        "31.123456,     -65.948862",

        " 172 deg 25 min 55 sec E 34 degrees 3  minutes 2.1 seconds N",
        " 172 deg 25 min 55 sec W  S 34 degrees 3  minutes 2.1 seconds ",

        "71d 25m 55s, 134 d 3 m 2 s WEST",
        "71d 25m 55s NORTH 134 d 3 m 2 s EAST",
        "71d 25m 55s SOUTH 134 d 3 m 2 s",
        "-71d 25m 55s SOUTH -134 d 3 m 2 s",

        "71.43194444 S 134.05055556 W",
        "71.S 134. W",
        "71. S 134.W",


        "134 d 3 m 2 s WEST, 71d 25m 55s",
        "134 d 3 m 2 s EAST 71d 25m 55s NORTH",
        "134 d 3 m 2 s 71d 25m 55s SOUTH ",
        "-134 d 3 m 2 s-71d 25m 55s SOUTH ",

        "31:13:46N,     65:56:55W",
        "31:45:46.302N, 65:56:55.903W",

        unicode("12.25\xb0 E -0.25\xb0 S", 'latin1'),
        "-0 15'  0.000\"    12 15'  0.000\"",

        unicode("0.00901\xb0 0\xf8 1' 23\"", 'latin1'),

        unicode("+11\xb0 11' 11.00\", -1\xf8 2' 3.45\"", 'latin1'),

        unicode("31\37034'21\"N,   65\26058'36\"W", 'latin1'),
        unicode("N45\xf8 26.7717', W65\xb0 56.93172'", 'latin1'),

        unicode("S12.25\xf8  W -0.25\xb0", 'latin1'),
        "-12 15'  0.000\"    -0 15'  0.000\"",

        unicode("-13\xb0 0', 80 59'", 'latin1'),

        "34deg 45min 3sec N, 132deg west",

        "41.23235 -122.123124",
        " -41.23235   122.123124  ",

        "-122.123124   41.23235 ",
        " 122.123124 -41.23235     ",

        " ln =  122.123124 latitude=-41.23235     ",
        "lon:-122.123124 lt:-41.23235",
        "lng:-122.123124 lt:-41.23235",
        "longitudanal  :-122.123124 ltitudinal :-41.23235",

        'lat="50.901697" lon="-1.489356"',
        "N 36* 48.5' W 117* 46.9'",
        "  x=-0.37318314  y= -0.70835411 z =0.59914005  ",
        "34.07757008999999;-118.474374396 ",
        "624826.14 n 1450350.56 w",
        "-620000.00 n -1450000.0000",
        "-620000 n -1450000",
        "-620000. n -1450000.",
        "-6200.00 n -14500.0000",
        "-6200 n -14500 ",
        "-6200. n -14500.",

        "23 deg 17 min",
        "23.5",
        "123.5",
        "lon=123.5",
        "lat=23.5",
        "lon=23.5",

        "55 0\' 0.000\" N, 12 0\' 0.000\" E",               # cgi output
        "55 0\' 0.002\" N 12 0\' 0.001\" w",                # old cgi output
        "12 0\' 0.002\" w 55 0\' 0.001\" s",
        "12 0\' 0.002\" e 55 0\' 0.001\" s",
        "12 0\' 0.002\" e 55 0\' 0.001\" n",

        " 172 25' 55\" E 34 3' 2.1\" N",
        " 172 25' 55\" W  S 34 3' 2.1\" ",

        unicode("131\xb0 13' 14 21 32 43", 'latin1'),

        unicode("10\xb0 0.05' 20\xb0 30 40", 'latin1'),

        " .0 deg 17' 49.23\" N, .0 deg 45' 57.33\" W ",
        " .0 deg 17'   .23\" N, .0 deg 45'   .33\" W ",
        " .0 deg 17' 49.\"   N, .0 deg 45' 57.  \" W ",
        unicode("S12.25\xf8  W -.25\xb0", 'latin1'),
        unicode("S12.25\xf8  W +.25\xb0", 'latin1'),
        unicode("S.25\xf8    W +.25\xb0", 'latin1'),

        "71d 25m 55s, 134 3 2",

        "172d 25m 55s E 34 3 2 N",
        "25m 55s E 34 3 2 N",
        "25m 55sw 3m 2ss",
        "25m 55se 3m 2sn",

        "-290  34",
        " 290  34",
        " 360  90",
        "-360  90",
        "-360 -90",

        unicode("  48 degrees. 12.0556 min n    16 \xb0. 22.1729 min e ", 'latin1'),
        ". 48 degrees. 12.0556 min no   16 deg.  22.1729 min e.",

        "  48 degrees  12.0556 min nor  16 deg,  22.1729 min e ",
        unicode("  48 deg,     12.0556 min nort 16 \xf8  22.1729 min e ", 'latin1'),
        "  48 degrees, 12.0556 min n    16 deg,  22.1729 min e ",
        ", 48 degrees  12.0556 min n    16 deg,  22.1729 min e ",
        ", 48 degrees, 12.0556 min n    16 deg   22.1729 min e ",
        ", 48 degrees, 12.0556 min n    16 deg,  22.1729 min e ",
        "  48 degrees  12.0556 min n    16 deg,  22.1729 min e,",
        "  48 degrees, 12.0556 min n    16 deg   22.1729 min e,",
        "  48 degrees, 12.0556 min n    16 deg,  22.1729 min e,",
        ", 48 degrees  12.0556 min n    16 deg,  22.1729 min e,",
        ", 48 degrees, 12.0556 min n    16 deg   22.1729 min e,",
        ", 48 degrees, 12.0556 min n    16 deg,  22.1729 min e,",

        " geo:-48.345,180?blah=dj&to=l%20kjsdf",
        " geo:-48.345,180,234?blah=dj&to=l%20kjsdf",
        "geo:-48.345,180",
        "geo:+148.345,+80,234",

        ";  The ones with south in them don't work correctly - sec problem",
        " 100nORTH 10Western",
        " 100nwe 10wESTERN",
        " 100sORTH 10Eestern",
        " 100swe 10eESTERN",
        " 10Western 100nORTH ",
        " 10wESTERN 100nwe ",
        " 10Eestern 100sORTH ",
        " 10eESTERN 100swe ",

        " 100nORTH , 10Western",
        " 100nwe , 10wESTERN",
        " 100sORTH , 10Eestern",
        " 100swe , 10eESTERN",
        " 10Western , 100nORTH ",
        " 10wESTERN , 100nwe ",
        " 10Eestern , 100sORTH ",
        " 10eESTERN , 100swe ",

        " 100 nORTH 10 Western",
        " 100 nwe 10 wESTERN",
        " 100 sORTH 10 Eestern",
        " 100 swe 10 eESTERN",
        " 10 Western 100 nORTH ",
        " 10 wESTERN 100 nwe ",
        " 10 Eestern 100 sORTH ",
        " 10 eESTERN 100 swe ",

        " 100 nORTH , 10 Western",
        " 100 nwe , 10 wESTERN",
        " 100 sORTH , 10 Eestern",
        " 100 swe , 10 eESTERN",
        " 10 Western , 100 nORTH ",
        " 10 wESTERN , 100 nwe ",
        " 10 Eestern , 100 sORTH ",
        " 10 eESTERN , 100 swe ",

        "460012123 -910034123 ",

        "",
        ";  ambiguous 20, should be longitude",
        unicode("10\xb0 0.05' 20 30 40", 'latin1'),

        ]

    if  False :
        for vs  in va :

            g   = value_re.match(vs)
            if  not g :
                print "NO VALUE", vs
            else :
                print vs, "------",
                for i in xrange(1, g.lastindex + 1) :
                    print "|", g.group(i) or "*",
                print   "----------------------", vs[g.end():]
            pass
        pass

    print


    if  False :

        llvs    = [ 0, 0.0, 0.7, 2, 2.1, 45.0,
                        89.0,  89.3,  90.0,  90.3,  91.0,
                       179.0, 179.3, 180.0, 180.3, 181.0,
                       269.0, 269.3, 270.0, 270.3, 271.0,
                       359.0, 359.3, 360.0, 360.3, 361.0,
                       449.0, 449.3, 450.0, 450.3, 451.0,
                       539.0, 539.3, 540.0, 540.3, 541.0,
                       629.0, 629.3, 630.0, 630.3, 631.0,
                       719.0, 719.3, 720.0, 720.3, 721.0,
                  ]

        print
        print "lat"
        for ll in llvs :

            print
            print "lat     %7.2f" % ( float( ll) ),
            print "%7.2f %7.2f" % ( float(lat_in_world( ll)), float(lat_in_world_modulo( ll)) )

            if  not ( -90 <= lat_in_world_modulo(ll) <=  90) :
                raise "Bad lat %7.2f" % (  lat_in_world_modulo(ll) )
            if  not (-180 <= lon_in_world_modulo(ll) <= 180) :
                raise "Bad lon %7.2f" % (  lon_in_world_modulo(ll) )
            pass

        print
        print "neg lat"
        for ll in llvs :

            print
            print "lat     %7.2f" % ( float(-ll) ),
            print "%7.2f %7.2f" % ( float(lat_in_world(-ll)), float(lat_in_world_modulo(-ll)) )

            if  not ( -90 <= lat_in_world_modulo(ll) <=  90) :
                raise "Bad lat %7.2f" % (  lat_in_world_modulo(ll) )
            if  not (-180 <= lon_in_world_modulo(ll) <= 180) :
                raise "Bad lon %7.2f" % (  lon_in_world_modulo(ll) )
            pass

        print
        print "lon"
        for ll in llvs :

            print
            print "lon     %7.2f" % ( float( ll) ),
            print "%7.2f %7.2f" % ( float(lon_in_world( ll)), float(lon_in_world_modulo( ll)) )

            if  not ( -90 <= lat_in_world_modulo(ll) <=  90) :
                raise "Bad lat %7.2f" % (  lat_in_world_modulo(ll) )
            if  not (-180 <= lon_in_world_modulo(ll) <= 180) :
                raise "Bad lon %7.2f" % (  lon_in_world_modulo(ll) )
            pass

        print
        print "neg lon"
        for ll in llvs :

            print
            print "lon     %7.2f" % ( float(-ll) ),
            print "%7.2f %7.2f" % ( float(lon_in_world(-ll)), float(lon_in_world_modulo(-ll)) )


            if  not ( -90 <= lat_in_world_modulo(ll) <=  90) :
                raise "Bad lat %7.2f" % (  lat_in_world_modulo(ll) )
            if  not (-180 <= lon_in_world_modulo(ll) <= 180) :
                raise "Bad lon %7.2f" % (  lon_in_world_modulo(ll) )
            pass
        pass


    if  test_parse :

        for vs  in va :
            if  not vs :
                print
            elif vs.lstrip()[0] == ';' :
                print
                print vs
            else :
                ( lat, lon )    = parse_lat_lon(vs)

                if  lat == None :
                    ( lat, lon )    = parse_lat_lon(vs + " , lon=-179.99999")
                    if  lat != None :
                        if  abs(lon - -179.99999) >= 0.0001 :
                            lat     = None
                        else        :
                            lon     = 0.0
                        pass
                    pass

                if  lat == None :
                    ( lat, lon )    = parse_lat_lon(vs + " , lat=-89.99999")
                    if  lat != None :
                        if  abs(lat - -89.99999) >= 0.0001 :
                            lat     = None
                        else        :
                            lat     = 0.0
                        pass
                    pass

                if  lat == None :
                    ( lat, lon )    = parse_lat_lon(vs + " , -179.99999")
                    if  lat != None :
                        if  abs(lon - -179.99999) >= 0.0001 :
                            lat     = None
                        else        :
                            lon     = 0.0
                        pass
                    pass

                if  lat == None :
                    ( lat, lon )    = parse_lat_lon(vs + " , 89.99999North")
                    if  lat != None :
                        if  abs(lat - 89.99999) >= 0.0001 :
                            lat     = None
                        else        :
                            lat     = 0.0
                        pass
                    pass

                if  lat == None :
                    print "NO PARSE", vs.encode('utf8')
                else :

                    show_latlon(vs, lat, lon)



                    def check_output_s(lat, lon, plus, s) :
                        ( la, lo )      = parse_lat_lon(s)
                        if  la == None :
                                print "Error plus =", plus, "lat=", lat, la, '[' + s + ']'
                        else :
                            if  round(la  * 1000000.0) != round(lat * 1000000.0) :
                                print "Error plus =", plus, "lat=", lat, la, '[' + s + ']'
                            if  round(lo  * 1000000.0) != round(lon * 1000000.0) :
                                print "Error plus =", plus, "lon=", lon, lo, '[' + s + ']'
                            pass
                        pass

                    def check_output(lat, lon, plus) :
                        ( ds, dmss, dms )   = lls(lat, lon, plus)
                        check_output_s(lat, lon, plus, ds)
                        check_output_s(lat, lon, plus, dmss)
                        check_output_s(lat, lon, plus, dms)

                    check_output(lat, lon, None)
                    check_output(lat, lon, "")
                    check_output(lat, lon, "+")

                pass
            pass
        pass

        for vs  in va :
            if  not vs :
                pass
            elif vs.lstrip()[0] == ';' :
                pass
            else :
                pass
            pass
        pass


    lla = [
            [  -1.2,   -1.2 ],
            [  -1.2,    0.0 ],
            [  -1.2,    0.3 ],
            [  -1.2,   10.0 ],
            [  -1.2,  -10.0 ],
            [  -1.2,  -89.2 ],
            [  -1.2,  -90.0 ],
            [  -1.2,  -91.3 ],
            [  -1.2, -178.2 ],
            [  -1.2, -180.0 ],
            [  -1.2,  180.0 ],
            [  -1.2,  178.7 ],
            [  -1.2,   91.3 ],
            [  -1.2,   90.0 ],
            [  -1.2,   89.2 ],

            [   0.0,   -1.2 ],
            [   0.0,    0.0 ],
            [   0.0,    1.3 ],
            [   0.0,  -89.2 ],
            [   0.0,  -90.0 ],
            [   0.0,  -91.3 ],
            [   0.0, -178.2 ],
            [   0.0, -180.0 ],
            [   0.0,  180.0 ],
            [   0.0,  178.7 ],
            [   0.0,   91.3 ],
            [   0.0,   90.0 ],
            [   0.0,   89.2 ],

            [  -1.2,   -1.2 ],
            [  -1.2,    0.0 ],
            [  -1.2,    0.3 ],
            [  -1.2,   10.0 ],
            [  -1.2,  -10.0 ],
            [  -1.2,  -89.2 ],
            [  -1.2,  -90.0 ],
            [  -1.2,  -91.3 ],
            [  -1.2, -178.2 ],
            [  -1.2, -180.0 ],
            [  -1.2,  180.0 ],
            [  -1.2,  178.7 ],
            [  -1.2,   91.3 ],
            [  -1.2,   90.0 ],
            [  -1.2,   89.2 ],

            [ -30.2,   -1.2 ],
            [ -30.2,    0.0 ],
            [ -30.2,    0.3 ],
            [ -30.2,   10.0 ],
            [ -30.2,  -10.0 ],
            [ -30.2,  -89.2 ],
            [ -30.2,  -90.0 ],
            [ -30.2,  -91.3 ],
            [ -30.2, -178.2 ],
            [ -30.2, -180.0 ],
            [ -30.2,  180.0 ],
            [ -30.2,  178.7 ],
            [ -30.2,   91.3 ],
            [ -30.2,   90.0 ],
            [ -30.2,   89.2 ],

            [ -89.2,   -1.2 ],
            [ -89.2,    0.0 ],
            [ -89.2,    0.3 ],
            [ -89.2,   10.0 ],
            [ -89.2,  -10.0 ],
            [ -89.2,  -89.2 ],
            [ -89.2,  -90.0 ],
            [ -89.2,  -91.3 ],
            [ -89.2, -178.2 ],
            [ -89.2, -180.0 ],
            [ -89.2,  180.0 ],
            [ -89.2,  178.7 ],
            [ -89.2,   91.3 ],
            [ -89.2,   90.0 ],
            [ -89.2,   89.2 ],

            [ -90.0,   -1.2 ],
            [ -90.0,    0.0 ],
            [ -90.0,    0.3 ],
            [ -90.0,   10.0 ],
            [ -90.0,  -10.0 ],
            [ -90.0,  -89.2 ],
            [ -90.0,  -90.0 ],
            [ -90.0,  -91.3 ],
            [ -90.0, -178.2 ],
            [ -90.0, -180.0 ],
            [ -90.0,  180.0 ],
            [ -90.0,  178.7 ],
            [ -90.0,   91.3 ],
            [ -90.0,   90.0 ],
            [ -90.0,   89.2 ],

            [  30.2,   -1.2 ],
            [  30.2,    0.0 ],
            [  30.2,    0.3 ],
            [  30.2,   10.0 ],
            [  30.2,  -10.0 ],
            [  30.2,  -89.2 ],
            [  30.2,  -90.0 ],
            [  30.2,  -91.3 ],
            [  30.2, -178.2 ],
            [  30.2, -180.0 ],
            [  30.2,  180.0 ],
            [  30.2,  178.7 ],
            [  30.2,   91.3 ],
            [  30.2,   90.0 ],
            [  30.2,   89.2 ],

            [  89.2,   -1.2 ],
            [  89.2,    0.0 ],
            [  89.2,    0.3 ],
            [  89.2,   10.0 ],
            [  89.2,  -10.0 ],
            [  89.2,  -89.2 ],
            [  89.2,  -90.0 ],
            [  89.2,  -91.3 ],
            [  89.2, -178.2 ],
            [  89.2, -180.0 ],
            [  89.2,  180.0 ],
            [  89.2,  178.7 ],
            [  89.2,   91.3 ],
            [  89.2,   90.0 ],
            [  89.2,   89.2 ],

            [  90.0,   -1.2 ],
            [  90.0,    0.0 ],
            [  90.0,    0.3 ],
            [  90.0,   10.0 ],
            [  90.0,  -10.0 ],
            [  90.0,  -89.2 ],
            [  90.0,  -90.0 ],
            [  90.0,  -91.3 ],
            [  90.0, -178.2 ],
            [  90.0, -180.0 ],
            [  90.0,  180.0 ],
            [  90.0,  178.7 ],
            [  90.0,   91.3 ],
            [  90.0,   90.0 ],
            [  90.0,   89.2 ],

          ]


    for ll in lla :
        lat         = ll[0]
        lon         = ll[1]

        ( x, y, z ) = convert_lat_lon_to_x_y_z(lat, lon)

        ( ra, ro)   = convert_x_y_z_to_lat_lon(x, y, z)

        # print lat, lon, x, y, z, ra, ro

        if  abs(ra - lat) > 0.0000001 :
            print "goof lat", lat, lon, x, y, z, ra, ro
        if  abs(ro - lon) > 0.0000001 :
            print "goof lon", lat, lon, x, y, z, ra, ro
        pass

    pass



#
#
#
# eof
