Exemplo n.º 1
0
def decode(geohash):
    '''Decode a geohash to lat-/longitude of the (approximate
       centre of) geohash cell, to reasonable precision.

       @param geohash: To be decoded (L{Geohash}).

       @return: 2-Tuple (latStr, lonStr) in (C{str}).

       @raise TypeError: The I{geohash} is not a L{Geohash}, C{LatLon}
                         or C{str}.

       @raise ValueError: Invalid or null I{geohash}.

       @example:

       >>> geohash.decode('u120fxw')  # '52.205', '0.1188'
       >>> geohash.decode('sunny')  # '23.708', '42.473'  Saudi Arabia
       >>> geohash.decode('fur')  # '69.6', '-45.7'  Greenland
       >>> geohash.decode('reef')  # '-24.87', '162.95'  Coral Sea
       >>> geohash.decode('geek')  # '65.48', '-17.75'  Iceland
    '''
    s, w, n, e = bounds(geohash)

    # round to near centre without excessive precision
    # ⌊2-log10(Δ°)⌋ decimal places, strip trailing zeros
    lat = fStr(favg(n, s), prec=int(2 - log10(n - s)))
    lon = fStr(favg(e, w), prec=int(2 - log10(e - w)))

    return lat, lon  # strings
Exemplo n.º 2
0
    def intermediateTo(self, other, fraction, height=None, wrap=False):
        '''Locate the point at given fraction between this and an
           other point.

           @param other: The other point (L{LatLon}).
           @param fraction: Fraction between both points (float, 0.0 =
                            this point, 1.0 = the other point).
           @keyword height: Optional height, overriding the fractional
                            height (C{meter}).
           @keyword wrap: Wrap and unroll longitudes (C{bool}).

           @return: Intermediate point (L{LatLon}).

           @raise TypeError: The I{other} point is not L{LatLon}.

           @example:

           >>> p1 = LatLon(52.205, 0.119)
           >>> p2 = LatLon(48.857, 2.351)
           >>> p = p1.intermediateTo(p2, 0.25)  # 51.3721°N, 000.7073°E

           @JSname: I{intermediatePointTo}.
        '''
        self.others(other)

        a1, b1 = self.to2ab()
        a2, b2 = other.to2ab()

        db, b2 = unrollPI(b1, b2, wrap=wrap)
        r = haversine_(a2, a1, db)
        sr = sin(r)
        if abs(sr) > EPS:
            cb1, cb2, ca1, ca2 = map1(cos, b1, b2, a1, a2)
            sb1, sb2, sa1, sa2 = map1(sin, b1, b2, a1, a2)

            A = sin((1 - fraction) * r) / sr
            B = sin(fraction * r) / sr

            x = A * ca1 * cb1 + B * ca2 * cb2
            y = A * ca1 * sb1 + B * ca2 * sb2
            z = A * sa1 + B * sa2

            a = atan2(z, hypot(x, y))
            b = atan2(y, x)

        else:  # points too close
            a = favg(a1, a2, f=fraction)
            b = favg(b1, b2, f=fraction)

        if height is None:
            h = self._havg(other, f=fraction)
        else:
            h = height
        return self.classof(degrees90(a), degrees180(b), height=h)
Exemplo n.º 3
0
    def latlon(self):
        '''Get the lat- and longitude of (the approximate center of)
           this geohash as a 2-tuple (lat, lon) in C{degrees}.

           B{Example:}

           >>> geohash.Geohash('geek').latlon  # 65.478515625, -17.75390625
           >>> geohash.decode('geek')  # '65.48', '-17.75'
        '''
        if not self._latlon:
            s, w, n, e = self.bounds()
            self._latlon = favg(n, s), favg(e, w)
        return self._latlon
Exemplo n.º 4
0
def bounds(geohash, LatLon=None, **kwds):
    '''Returns the lower-left SW and upper-right NE corners of a geohash.

       @param geohash: To be bound (L{Geohash}).
       @keyword LatLon: Optional (sub-)class to return I{bounds}
                        (C{LatLon}) or C{None}.
       @keyword kwds: Optional keyword arguments for I{LatLon}.

       @return: 4-Tuple (S, W, N, E) of bounds lat- and longitudes in
                (C{degrees}) if I{LatLon} is C{None} otherwise 2-tuple
                (I{LatLon(S, W)}, I{LatLon(N, E)} of the bounds.

       @raise TypeError: The I{geohash} is not a L{Geohash}, C{LatLon}
                         or C{str}.

       @raise ValueError: Invalid or null I{geohash}.

       @example:

       >>> geohash.bounds('u120fxw')  #  52.20428467, 0.11810303,
                                      #  52.20565796, 0.11947632
       >>> geohash.decode('u120fxw')  # '52.205',    '0.1188'
    '''
    geohash = _2Geohash(geohash)
    if len(geohash) < 1:
        raise ValueError('%s invalid: %s' % ('geohash', geohash))

    s, n = -90, 90
    w, e = -180, 180

    d = True
    for c in geohash:  # .lower():
        try:
            i = _DecodedBase32[c]
        except KeyError:
            raise ValueError('%s invalid: %s' % ('geohash', geohash))

        for m in (16, 8, 4, 2, 1):
            if d:  # longitude
                if i & m:
                    w = favg(w, e)
                else:
                    e = favg(w, e)
            else:  # latitude
                if i & m:
                    s = favg(s, n)
                else:
                    n = favg(s, n)
            d = not d

    return _2bounds((s, w, n, e), LatLon, kwds)
Exemplo n.º 5
0
def bounds(geohash, LatLon=None, **kwds):
    '''Returns the lower-left SW and upper-right NE corners of a geohash.

       @param geohash: To be bound (L{Geohash}).
       @keyword LatLon: Optional (sub-)class to return the bounds
                        (C{LatLon}) or C{None}.
       @keyword kwds: Optional keyword arguments for B{C{LatLon}}.

       @return: A L{Bounds2Tuple}C{(latlonSW, latlonNE)} as B{C{LatLon}}
                or a L{Bounds4Tuple}C{(latS, lonW, latN, lonE)} if
                B{C{LatLon}} is C{None}.

       @raise TypeError: The B{C{geohash}} is not a L{Geohash}, C{LatLon}
                         or C{str}.

       @raise ValueError: Invalid or null B{C{geohash}}.

       @example:

       >>> geohash.bounds('u120fxw')  #  52.20428467, 0.11810303,
                                      #  52.20565796, 0.11947632
       >>> geohash.decode('u120fxw')  # '52.205',    '0.1188'
    '''
    geohash = _2Geohash(geohash)
    if len(geohash) < 1:
        raise ValueError('%s invalid: %s' % ('geohash', geohash))

    s, n = -90, 90
    w, e = -180, 180

    d = True
    for c in geohash:  # .lower():
        try:
            i = _DecodedBase32[c]
        except KeyError:
            raise ValueError('%s invalid: %s' % ('geohash', geohash))

        for m in (16, 8, 4, 2, 1):
            if d:  # longitude
                if i & m:
                    w = favg(w, e)
                else:
                    e = favg(w, e)
            else:  # latitude
                if i & m:
                    s = favg(s, n)
                else:
                    n = favg(s, n)
            d = not d

    return _2bounds(s, w, n, e, LatLon, kwds)
Exemplo n.º 6
0
def bounds(geohash):
    '''Returns the SW and NE lat-/longitude bounds of a geohash.

       @return: 4-Tuple (latS, lonW, latN, lonE) of bounds in (C{degrees}).

       @raise TypeError: The I{geohash} is not a L{Geohash}, C{LatLon}
                         or C{str}.

       @raise ValueError: Invalid or null I{geohash}.

       @example:

       >>> geohash.bounds('u120fxw')  #  52.20428467, 0.11810303,
                                      #  52.20565796, 0.11947632
       >>> geohash.decode('u120fxw')  # '52.205',    '0.1188'
    '''
    geohash = _2Geohash(geohash)
    if len(geohash) < 1:
        raise ValueError('%s invalid: %s' % ('geohash', geohash))

    latS, latN = -90, 90
    lonW, lonE = -180, 180

    e = True
    for c in geohash:  # .lower():
        try:
            i = _DecodedBase32[c]
        except KeyError:
            raise ValueError('%s invalid: %s' % ('geohash', geohash))

        for m in (16, 8, 4, 2, 1):
            if e:  # longitude
                lon = favg(lonW, lonE)
                if i & m:
                    lonW = lon
                else:
                    lonE = lon
            else:  # latitude
                lat = favg(latS, latN)
                if i & m:
                    latS = lat
                else:
                    latN = lat
            e = not e

    return latS, lonW, latN, lonE
Exemplo n.º 7
0
    def _havg(self, other, f=0.5):
        '''(INTERNAL) Weighted, average height.

           @param other: An other point (C{LatLon}).
           @keyword f: Optional fraction (C{float}).

           @return: Average, fractional height (C{float}).
        '''
        return favg(self.height, other.height, f=f)
Exemplo n.º 8
0
    def rhumbMidpointTo(self, other, height=None):
        '''Return the (loxodromic) midpoint between this and
           an other point.

           @param other: The other point (spherical LatLon).
           @keyword height: Optional height, overriding the mean height
                            (C{meter}).

           @return: The midpoint (spherical C{LatLon}).

           @raise TypeError: The I{other} point is not spherical.

           @example:

           >>> p = LatLon(51.127, 1.338)
           >>> q = LatLon(50.964, 1.853)
           >>> m = p.rhumb_midpointTo(q)
           >>> m.toStr()  # '51.0455°N, 001.5957°E'
        '''
        self.others(other)

        # see <https://MathForum.org/library/drmath/view/51822.html>
        a1, b1 = self.to2ab()
        a2, b2 = other.to2ab()
        if abs(b2 - b1) > PI:
            b1 += PI2  # crossing anti-meridian

        a3 = favg(a1, a2)
        b3 = favg(b1, b2)

        f1 = tanPI_2_2(a1)
        if abs(f1) > EPS:
            f2 = tanPI_2_2(a2)
            f = f2 / f1
            if abs(f) > EPS:
                f = log(f)
                if abs(f) > EPS:
                    f3 = tanPI_2_2(a3)
                    b3 = fsum_(b1 * log(f2), -b2 * log(f1),
                               (b2 - b1) * log(f3)) / f

        h = self._havg(other) if height is None else height
        return self.classof(degrees90(a3), degrees180(b3), height=h)
Exemplo n.º 9
0
def nearestOn4(point, points, closed=False, wrap=False, **options):
    '''Locate the point on a path or polygon closest to an other point.

       If the given point is within the extent of a polygon edge,
       the closest point is on that edge, otherwise the closest
       point is the nearest of that edge's end points.

       Distances are approximated by function L{equirectangular_},
       subject to the supplied I{options}.

       @param point: The other, reference point (C{LatLon}).
       @param points: The path or polygon points (C{LatLon}[]).
       @keyword closed: Optionally, close the path or polygon (C{bool}).
       @keyword wrap: Wrap and L{unroll180} longitudes and longitudinal
                      delta (C{bool}) in function L{equirectangular_}.
       @keyword options: Other keyword arguments for function
                         L{equirectangular_}.

       @return: 4-Tuple (lat, lon, I{distance}, I{angle}) all in
                C{degrees}.  The I{distance} is the L{equirectangular_}
                distance between the closest and the reference I{point}
                in C{degrees}.  The I{angle} from the reference I{point}
                to the closest point is in compass C{degrees360}, like
                function L{compassAngle}.

       @raise LimitError: Lat- and/or longitudinal delta exceeds
                          I{limit}, see function L{equirectangular_}.

       @raise TypeError: Some I{points} are not C{LatLon}.

       @raise ValueError: Insufficient number of I{points}.

       @see: Function L{nearestOn3}.  Use function L{degrees2m} to
             convert C{degrees} to C{meter}.
    '''
    n, points = points2(points, closed=closed)

    def _d2yx(p2, p1, u, i):
        w = wrap if (not closed or i < (n - 1)) else False
        # equirectangular_ returns a 4-tuple (distance in
        # degrees squared, delta lat, delta lon, p2.lon
        # unroll/wrap); the previous p2.lon unroll/wrap
        # is also applied to the next edge's p1.lon
        return equirectangular_(p1.lat,
                                p1.lon + u,
                                p2.lat,
                                p2.lon,
                                wrap=w,
                                **options)

    # point (x, y) on axis rotated ccw by angle a:
    #   x' = y * sin(a) + x * cos(a)
    #   y' = y * cos(a) - x * sin(a)
    #
    # distance (w) along and perpendicular (h) to
    # a line thru point (dx, dy) and the origin:
    #   w = (y * dy + x * dx) / hypot(dx, dy)
    #   h = (y * dx - x * dy) / hypot(dx, dy)
    #
    # closest point on that line thru (dx, dy):
    #   xc = dx * w / hypot(dx, dy)
    #   yc = dy * w / hypot(dx, dy)
    # or
    #   xc = dx * f
    #   yc = dy * f
    # with
    #   f = w / hypot(dx, dy)
    # or
    #   f = (y * dy + x * dx) / (dx**2 + dy**2)

    i, m = _imdex2(closed, n)
    p2 = c = points[i]
    u2 = u = 0
    d, dy, dx, _ = _d2yx(p2, point, u2, i)
    for i in range(m, n):
        p1, u1, p2 = p2, u2, points[i]
        # iff wrapped, unroll lon1 (actually previous
        # lon2) like function unroll180/-PI would've
        d21, y21, x21, u2 = _d2yx(p2, p1, u1, i)
        if d21 > EPS:
            # distance point to p1, y01 and x01 inverted
            d2, y01, x01, _ = _d2yx(point, p1, u1, n)
            if d2 > EPS:
                w2 = y01 * y21 + x01 * x21
                if w2 > 0:
                    if w2 < d21:
                        # closest is between p1 and p2, use
                        # original delta's, not y21 and x21
                        f = w2 / d21
                        p1 = LatLon_(favg(p1.lat, p2.lat, f=f),
                                     favg(p1.lon, p2.lon + u2, f=f))
                        u1 = 0
                    else:  # p2 is closest
                        p1, u1 = p2, u2
                    d2, y01, x01, _ = _d2yx(point, p1, u1, n)
            if d2 < d:  # p1 is closer, y01 and x01 inverted
                c, u, d, dy, dx = p1, u1, d2, -y01, -x01
    return c.lat, c.lon + u, hypot(dx, dy), degrees360(atan2(dx, dy))
Exemplo n.º 10
0
def intersection(start1,
                 end1,
                 start2,
                 end2,
                 height=None,
                 wrap=False,
                 LatLon=LatLon):
    '''Compute the intersection point of two paths both defined
       by two points or a start point and bearing from North.

       @param start1: Start point of the first path (L{LatLon}).
       @param end1: End point ofthe first path (L{LatLon}) or
                    the initial bearing at the first start point
                    (compass C{degrees360}).
       @param start2: Start point of the second path (L{LatLon}).
       @param end2: End point of the second path (L{LatLon}) or
                    the initial bearing at the second start point
                    (compass C{degrees360}).
       @keyword height: Optional height for the intersection point,
                        overriding the mean height (C{meter}).
       @keyword wrap: Wrap and unroll longitudes (C{bool}).
       @keyword LatLon: Optional (sub-)class for the intersection point
                        (L{LatLon}) or C{None}.

       @return: The intersection point (I{LatLon}) or 3-tuple
                (C{degrees90}, C{degrees180}, height) if I{LatLon}
                is C{None}.  An alternate intersection point might
                be the L{antipode} to the returned result.

       @raise TypeError: Start or end point(s) not L{LatLon}.

       @raise ValueError: Intersection is ambiguous or infinite or
                          the paths are parallel, coincident or null.

       @example:

       >>> p = LatLon(51.8853, 0.2545)
       >>> s = LatLon(49.0034, 2.5735)
       >>> i = intersection(p, 108.547, s, 32.435)  # '50.9078°N, 004.5084°E'
    '''
    _Trll.others(start1, name='start1')
    _Trll.others(start2, name='start2')

    a1, b1 = start1.to2ab()
    a2, b2 = start2.to2ab()

    db, b2 = unrollPI(b1, b2, wrap=wrap)
    r12 = haversine_(a2, a1, db)
    if abs(r12) < EPS:  # [nearly] coincident points
        a, b = map1(degrees, favg(a1, a2), favg(b1, b2))

    # see <http://www.EdWilliams.org/avform.htm#Intersection>
    elif isscalar(end1) and isscalar(end2):  # both bearings
        ca1, ca2, cr12 = map1(cos, a1, a2, r12)
        sa1, sa2, sr12 = map1(sin, a1, a2, r12)
        x1, x2 = (sr12 * ca1), (sr12 * ca2)
        if abs(x1) < EPS or abs(x2) < EPS:
            raise ValueError('intersection %s: %r vs %r' % ('parallel',
                                                            (start1, end1),
                                                            (start2, end2)))

        # handle domain error for equivalent longitudes,
        # see also functions asin_safe and acos_safe at
        # <http://www.EdWilliams.org/avform.htm#Math>
        t1, t2 = map1(acos1, (sa2 - sa1 * cr12) / x1, (sa1 - sa2 * cr12) / x2)
        if sin(db) > 0:
            t12, t21 = t1, PI2 - t2
        else:
            t12, t21 = PI2 - t1, t2

        t13, t23 = map1(radiansPI2, end1, end2)
        x1, x2 = map1(
            wrapPI,
            t13 - t12,  # angle 2-1-3
            t21 - t23)  # angle 1-2-3
        sx1, sx2 = map1(sin, x1, x2)
        if sx1 == 0 and sx2 == 0:  # max(abs(sx1), abs(sx2)) < EPS
            raise ValueError('intersection %s: %r vs %r' % ('infinite',
                                                            (start1, end1),
                                                            (start2, end2)))
        sx3 = sx1 * sx2
        #       if sx3 < 0:
        #           raise ValueError('intersection %s: %r vs %r' % ('ambiguous',
        #                            (start1, end1), (start2, end2)))
        cx1, cx2 = map1(cos, x1, x2)

        x3 = acos1(cr12 * sx3 - cx2 * cx1)
        r13 = atan2(sr12 * sx3, cx2 + cx1 * cos(x3))

        a, b = _destination2(a1, b1, r13, t13)
        # choose antipode for opposing bearings
        if _xb(a1, b1, end1, a, b, wrap) < 0 or \
           _xb(a2, b2, end2, a, b, wrap) < 0:
            a, b = antipode(a, b)

    else:  # end point(s) or bearing(s)
        x1, d1 = _x3d2(start1, end1, wrap, '1')
        x2, d2 = _x3d2(start2, end2, wrap, '2')
        x = x1.cross(x2)
        if x.length < EPS:  # [nearly] colinear or parallel paths
            raise ValueError('intersection %s: %r vs %r' % ('colinear',
                                                            (start1, end1),
                                                            (start2, end2)))
        a, b = x.to2ll()
        # choose intersection similar to sphericalNvector
        d1 = _xdot(d1, a1, b1, a, b, wrap)
        d2 = _xdot(d2, a2, b2, a, b, wrap)
        if (d1 < 0 and d2 > 0) or (d1 > 0 and d2 < 0):
            a, b = antipode(a, b)

    h = start1._havg(start2) if height is None else height
    return (a, b, h) if LatLon is None else LatLon(a, b, height=h)
Exemplo n.º 11
0
def encode(lat, lon, precision=None):
    '''Encode a lat-/longitude as a geohash, either to the specified
       or if not given, an automatically evaluated precision.

       @param lat: Latitude (C{degrees}).
       @param lon: Longitude (C{degrees}).
       @keyword precision: Optional, desired geohash length (C{int}).

       @return: The geohash (C{str}).

       @raise ValueError: Invalid I{lat}, I{lon} or I{precision}.

       @example:

       >>> geohash.encode(52.205, 0.119,   7)  # 'u120fxw'
       >>> geohash.encode(52.205, 0.119,  12)  # 'u120fxwshvkg'
       >>> geohash.encode(52.205, 0.1188, 12)  # 'u120fxws0jre'
       >>> geohash.encode(52.205, 0.1188)      # 'u120fxw'
       >>> geohash.encode(     0, 0)           # 's00000000000'
    '''
    lat, lon = _2fll(lat, lon)

    if not precision:
        # Infer precision by refining geohash until
        # it matches precision of supplied lat/lon.
        for prec in range(1, 13):
            gh = encode(lat, lon, prec)
            ll = map2(float, decode(gh))
            if abs(lat - ll[0]) < EPS and \
               abs(lon - ll[1]) < EPS:
                return gh
        prec = 12  # maximum
    else:
        try:
            prec = int(precision)
            if not 0 < prec < 13:
                raise ValueError
        except ValueError:
            raise ValueError('%s invalid: %r' % ('precision', precision))

    latS, latN = -90, 90
    lonW, lonE = -180, 180

    b = i = 0
    e, gh = True, []

    while len(gh) < prec:
        i += i
        if e:  # bisect longitude
            m = favg(lonE, lonW)
            if lon < m:
                lonE = m
            else:
                lonW = m
                i += 1
        else:  # bisect latitude
            m = favg(latN, latS)
            if lat < m:
                latN = m
            else:
                latS = m
                i += 1
        e = not e

        b += 1
        if b == 5:
            # 5 bits gives a character:
            # append it and start over
            gh.append(_GeohashBase32[i])
            b = i = 0

    return ''.join(gh)
Exemplo n.º 12
0
def intersection(start1, bearing1, start2, bearing2,
                 height=None, wrap=False, LatLon=LatLon):
    '''Compute the intersection point of two paths each defined
       by a start point and an initial bearing.

       @param start1: Start point of first path (L{LatLon}).
       @param bearing1: Initial bearing from start1 (compass C{degrees360}).
       @param start2: Start point of second path (L{LatLon}).
       @param bearing2: Initial bearing from start2 (compass C{degrees360}).
       @keyword height: Optional height for the intersection point,
                        overriding the mean height (C{meter}).
       @keyword wrap: Wrap and unroll longitudes (C{bool}).
       @keyword LatLon: Optional (sub-)class for the intersection point
                        (L{LatLon}) or C{None}.

       @return: Intersection point (L{LatLon}) or 3-tuple (C{degrees90},
                C{degrees180}, height) if C{LatLon} is C{None}.

       @raise TypeError: Point I{start1} or I{start2} is not L{LatLon}.

       @raise ValueError: Intersection is ambiguous or infinite or
                          the paths are parallel or coincident.

       @example:

       >>> p = LatLon(51.8853, 0.2545)
       >>> s = LatLon(49.0034, 2.5735)
       >>> i = intersection(p, 108.547, s, 32.435)  # '50.9078°N, 004.5084°E'
    '''
    _Trll.others(start1, name='start1')
    _Trll.others(start2, name='start2')

    # see <http://www.EdWilliams.org/avform.htm#Intersection>
    a1, b1 = start1.to2ab()
    a2, b2 = start2.to2ab()

    db, b2 = unrollPI(b1, b2, wrap=wrap)
    r12 = haversine_(a2, a1, db)
    if abs(r12) < EPS:  # [nearly] coincident points
        a, b = map1(degrees, favg(a1, a2), favg(b1, b2))

    else:
        ca1, ca2, cr12 = map1(cos, a1, a2, r12)
        sa1, sa2, sr12 = map1(sin, a1, a2, r12)
        x1, x2 = (sr12 * ca1), (sr12 * ca2)
        if abs(x1) < EPS or abs(x2) < EPS:
            raise ValueError('intersection %s: %r vs %r' % ('parallel', start1, start2))

        # handle domain error for equivalent longitudes,
        # see also functions asin_safe and acos_safe at
        # <http://www.EdWilliams.org/avform.htm#Math>
        t1, t2 = map1(acos1, (sa2 - sa1 * cr12) / x1,
                             (sa1 - sa2 * cr12) / x2)
        if sin(db) > 0:
            t12, t21 = t1, PI2 - t2
        else:
            t12, t21 = PI2 - t1, t2

        t13, t23 = map1(radiansPI2, bearing1, bearing2)
        x1, x2 = map1(wrapPI, t13 - t12,  # angle 2-1-3
                              t21 - t23)  # angle 1-2-3

        sx1, sx2 = map1(sin, x1, x2)
        if sx1 == 0 and sx2 == 0:
            raise ValueError('intersection %s: %r vs %r' % ('infinite', start1, start2))
        sx3 = sx1 * sx2
        if sx3 < 0:
            raise ValueError('intersection %s: %r vs %r' % ('ambiguous', start1, start2))
        cx1, cx2 = map1(cos, x1, x2)

        x3 = acos1(cr12 * sx3 - cx2 * cx1)
        r13 = atan2(sr12 * sx3, cx2 + cx1 * cos(x3))

        a, b = _destination2(a1, b1, r13, t13)

    h = start1._havg(start2) if height is None else height
    return (a, b, h) if LatLon is None else LatLon(a, b, height=h)
Exemplo n.º 13
0
def nearestOn2(point, points, radius=R_M, **options):
    '''Locate the closest point on any segment between two consecutive
       points of a path.

       If the given point is within the extent of any segment, the
       closest point is on the segment.  Otherwise the closest point
       is the nearest of the segment end points.

       Distances are approximated by function L{equirectangular_},
       subject to the supplied I{options}.

       @param point: The reference point (L{LatLon}).
       @param points: The points of the path (L{LatLon}[]).
       @keyword radius: Optional, mean earth radius (meter).
       @keyword options: Optional keyword arguments for function
                         L{equirectangular_}.

       @return: 2-Tuple (closest, distance) of the closest point
                (L{LatLon}) on the path and the distance to that
                point.  The distance is the L{equirectangular_}
                distance between the given and the closest point
                in meter, rather the units of I{radius}.

       @raise LimitError: Lat- and/or longitudinal delta exceeds
                          I{limit}, see function L{equirectangular_}.

       @raise TypeError: Some I{points} are not I{LatLon}.

       @raise ValueError: Insufficient number of I{points}.
    '''
    def _d2yx(p2, p1, u):
        # equirectangular_ returns a 4-tuple (distance in
        # degrees squared, delta lat, delta lon, p2.lon
        # unroll/wrap); the previous p2.lon unroll/wrap
        # is also applied to the next edge's p1.lon
        return equirectangular_(p1.lat, p1.lon + u, p2.lat, p2.lon, **options)

    # point (x, y) on axis rotated by angle a ccw:
    #   x' = y * sin(a) + x * cos(a)
    #   y' = y * cos(a) - x * sin(a)
    #
    # distance (w) along and perpendicular (h) to
    # a line thru point (dx, dy) and the origin:
    #   w = (y * dy + x * dx) / hypot(dx, dy)
    #   h = (y * dx - x * dy) / hypot(dx, dy)
    #
    # closest point on that line thru (dx, dy):
    #   xc = w * dx / hypot(dx, dy)
    #   yc = w * dy / hypot(dx, dy)

    n, points = point.points(points, closed=False)

    u = 0
    c = p2 = points[0]
    d, _, _, _ = _d2yx(p2, point, 0)
    for i in range(1, n):
        p1, p2 = p2, points[i]
        # iff wrapped, unroll lon1 (actually previous
        # lon2) like function unroll180/-PI would've
        d21, y21, x21, u = _d2yx(p2, p1, u)
        if d21 > EPS:
            # distance point to p1
            d2, y01, x01, _ = _d2yx(point, p1, 0)
            if d2 > EPS:
                w = y01 * y21 + x01 * x21
                if w > 0:
                    if w < d21:
                        # closest is between p1 and p2, use
                        # original delta's, not y21 and x21
                        f = w / d21
                        p1 = point.classof(favg(p1.lat, p2.lat, f=f),
                                           favg(p1.lon, p2.lon + u, f=f))
                        d2, _, _, _ = _d2yx(point, p1, 0)
                    else:  # p2 is closer
                        d2, _, _, _ = _d2yx(point, p2, u)
                        p1 = p2  # in case d2 < d
            if d2 < d:  # p1 is closer
                c, d = p1, d2

    # distance in degrees squared to meter
    d = radians(sqrt(d)) * float(radius)
    return c, d