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