def equirectangular(lat1, lon1, lat2, lon2, radius=R_M, **options): '''Compute the distance between two points using the U{Equirectangular Approximation / Projection <https://www.Movable-Type.co.UK/scripts/latlong.html#equirectangular>}. @arg lat1: Start latitude (C{degrees}). @arg lon1: Start longitude (C{degrees}). @arg lat2: End latitude (C{degrees}). @arg lon2: End longitude (C{degrees}). @kwarg radius: Mean earth radius (C{meter}). @kwarg options: Optional keyword arguments for function L{equirectangular_}. @return: Distance (C{meter}, same units as B{C{radius}}). @see: Function L{equirectangular_} for more details, the available B{C{options}}, errors, restrictions and other, approximate or accurate distance functions. ''' _, dy, dx, _ = equirectangular_(Lat(lat1, name='lat1'), Lon(lon1, name='lon1'), Lat(lat2, name='lat2'), Lon(lon2, name='lon2'), **options) # PYCHOK Distance4Tuple return degrees2m(hypot(dx, dy), radius=radius)
def __init__(self, lat, lon, height=0, name=NN): '''New C{LatLon}. @arg lat: Latitude (C{degrees} or DMS C{str} with N or S suffix). @arg lon: Longitude (C{degrees} or DMS C{str} with E or W suffix). @kwarg height: Optional height (C{meter} above or below the earth surface). @kwarg name: Optional name (C{str}). @return: New instance (C{LatLon}). @raise RangeError: Value of B{C{lat}} or B{C{lon}} outside the valid range and C{rangerrors} set to C{True}. @raise UnitError: Invalid B{C{lat}}, B{C{lon}} or B{C{height}}. @example: >>> p = LatLon(50.06632, -5.71475) >>> q = LatLon('50°03′59″N', """005°42'53"W""") ''' self._lat = Lat(lat) # parseDMS2(lat, lon) self._lon = Lon(lon) # PYCHOK LatLon2Tuple if height: # elevation self._height = Height(height) if name: self.name = name
def latlon(self, latlonh): '''Set the lat- and longitude and optionally the height. @arg latlonh: New lat-, longitude and height (2- or 3-tuple of C{degrees} and C{meter}). @raise TypeError: Height of B{C{latlonh}} not C{scalar} or B{C{latlonh}} not C{list} or C{tuple}. @raise ValueError: Invalid B{C{latlonh}} or M{len(latlonh)}. @see: Function L{parse3llh} to parse a B{C{latlonh}} string into a 3-tuple (lat, lon, h). ''' _xinstanceof(list, tuple, latlonh=latlonh) if len(latlonh) == 3: h = Height(latlonh[2], name=Fmt.SQUARE(latlonh=2)) elif len(latlonh) != 2: raise _ValueError(latlonh=latlonh) else: h = self._height lat = Lat(latlonh[0]) # parseDMS2(latlonh[0], latlonh[1]) lon = Lon(latlonh[1]) self._update(lat != self._lat or lon != self._lon or h != self._height) self._lat, self._lon, self._height = lat, lon, h
def latlon2(self, datum=None): '''Convert this WM coordinate to a lat- and longitude. @kwarg datum: Optional, ellipsoidal datum (L{Datum}, L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}) or C{None}. @return: A L{LatLon2Tuple}C{(lat, lon)}. @raise TypeError: Invalid or non-ellipsoidal B{C{datum}}. @see: Method C{toLatLon}. ''' r = self.radius x = self._x / r y = 2 * atan(exp(self._y / r)) - PI_2 if datum is not None: E = _ellipsoidal_datum(datum, name=self.name).ellipsoid if not E.isEllipsoidal: raise _IsnotError(_ellipsoidal_, datum=datum) # <https://Earth-Info.NGA.mil/GandG/wgs84/web_mercator/ # %28U%29%20NGA_SIG_0011_1.0.0_WEBMERC.pdf> y = y / r if E.e: y -= E.e * atanh(E.e * tanh(y)) # == E.es_atanh(tanh(y)) y *= E.a x *= E.a / r r = LatLon2Tuple(Lat(degrees90(y)), Lon(degrees180(x))) return self._xnamed(r)
def _to3zBll(lat, lon, cmoff=True): '''(INTERNAL) Return zone, Band and lat- and (central) longitude in degrees. @arg lat: Latitude (C{degrees}). @arg lon: Longitude (C{degrees}). @kwarg cmoff: Offset B{C{lon}} from zone's central meridian. @return: 4-Tuple (zone, Band, lat, lon). ''' z, lat, lon = _to3zll(lat, lon) # in .utmupsBase if _UTM_LAT_MIN > lat or lat >= _UTM_LAT_MAX: # [-80, 84) like Veness t = ' '.join((_outside_, _UTM_, _range_, '[%s,' % (_UTM_LAT_MIN,), '%s)' % (_UTM_LAT_MAX,))) raise RangeError(lat=degDMS(lat), txt=t) B = _Bands[int(lat + 80) >> 3] x = lon - _cmlon(z) # z before Norway/Svaldbard if abs(x) > _UTM_ZONE_OFF_MAX: t = ' '.join((_outside_, _UTM_, _zone_, str(z), 'by', degDMS(x, prec=6))) raise RangeError(lon=degDMS(lon), txt=t) if B == 'X': # and 0 <= int(lon) < 42: z = int(lon + 183) // 6 + 1 x = {32: 9, 34: 21, 36: 33}.get(z, None) if x: # Svalbard z += 1 if lon >= x else -1 elif B == 'V' and z == 31 and lon >= 3: z += 1 # SouthWestern Norway if cmoff: # lon off central meridian lon -= _cmlon(z) # z after Norway/Svaldbard return Zone(z), Band(B), Lat(lat), Lon(lon)
def lon(self, lon): '''Set the longitude. @arg lon: New longitude (C{str[E|W]} or C{degrees}). @raise ValueError: Invalid B{C{lon}}. ''' lon = Lon(lon) # parseDMS(lon, suffix=_EW_, clip=180) self._update(lon != self._lon) self._lon = lon
def elevation2(lat, lon, timeout=2.0): '''Get the geoid elevation at an C{NAD83} to C{NAVD88} location. @arg lat: Latitude (C{degrees}). @arg lon: Longitude (C{degrees}). @kwarg timeout: Optional, query timeout (seconds). @return: An L{Elevation2Tuple}C{(elevation, data_source)} or (C{None, "error"}) in case of errors. @raise ValueError: Invalid B{C{timeout}}. @note: The returned C{elevation} is C{None} if B{C{lat}} or B{C{lon}} is invalid or outside the C{Conterminous US (CONUS)}, if conversion failed or if the query timed out. The C{error} is the C{HTTP-, IO-, SSL-, Type-, URL-} or C{ValueError} as a string (C{str}). @see: U{USGS National Map<https://NationalMap.gov/epqs>}, the U{FAQ<https://www.USGS.gov/faqs/what-are-projection- horizontal-and-vertical-datum-units-and-resolution-3dep-standard-dems>}, U{geoid.py<https://Gist.GitHub.com/pyRobShrk>}, module L{geoids}, classes L{GeoidG2012B}, L{GeoidKarney} and L{GeoidPGM}. ''' try: x = _qURL( 'https://NED.USGS.gov/epqs/pqs.php', # 'https://NationalMap.gov/epqs/pqs.php' ( '%s=%.6F' % (_x_, Lon(lon)), '%s=%.6F' % (_y_, Lat(lat)), '%s=%s' % (_units_, 'Meters'), # 'Feet', capitalized 'output=xml'), # json, case_sensitive timeout=Scalar(timeout, name=_timeout_)) if x[:6] == '<?xml ': e = _xml('Elevation', x) try: e = float(e) if -1000000 < e < 1000000: return Elevation2Tuple(e, _xml('Data_Source', x)) e = 'non-CONUS %.2F' % (e, ) except (TypeError, ValueError): pass else: e = 'no %s "%s"' % ( _XML_, clips(x, limit=128, white=_SPACE_), ) except (HTTPError, IOError, TypeError, ValueError) as x: e = repr(x) e = _error(elevation2, lat, lon, e) return Elevation2Tuple(None, e)
def decode3(garef, center=True): '''Decode a C{garef} to lat-, longitude and precision. @arg garef: To be decoded (L{Garef} or C{str}). @kwarg center: If C{True} the center, otherwise the south-west, lower-left corner (C{bool}). @return: A L{LatLonPrec3Tuple}C{(lat, lon, precision)}. @raise GARSError: Invalid B{C{garef}}, INValid, non-alphanumeric or bad length B{C{garef}}. ''' def _Error(i): return GARSError(garef=_item_sq(repr(garef), i)) def _ll(chars, g, i, j, lo, hi): ll, b = 0, len(chars) for i in range(i, j): d = chars.find(g[i]) if d < 0: raise _Error(i) ll = ll * b + d if ll < lo or ll > hi: raise _Error(j) return ll def _ll2(lon, lat, g, i, m): d = _Digits.find(g[i]) if d < 1 or d > m * m: raise _Error(i) d, r = divmod(d - 1, m) lon = lon * m + r lat = lat * m + (m - 1 - d) return lon, lat g, precision = _2garstr2(garef) lon = _ll(_Digits, g, 0, _LonLen, 1, 720) + _LonOrig_M1_1 lat = _ll(_Letters, g, _LonLen, _MinLen, 0, 359) + _LatOrig_M1 if precision > 0: lon, lat = _ll2(lon, lat, g, _MinLen, _M2) if precision > 1: lon, lat = _ll2(lon, lat, g, _MinLen + 1, _M3) if center: # ll = (ll * 2 + 1) / 2 lon += 0.5 lat += 0.5 r = _Resolutions[precision] # == 1.0 / unit r = LatLonPrec3Tuple(Lat(lat * r, Error=GARSError), Lon(lon * r, Error=GARSError), precision) return _xnamed(r, nameof(garef))
def decode3(georef, center=True): '''Decode a C{georef} to lat-, longitude and precision. @arg georef: To be decoded (L{Georef} or C{str}). @kwarg center: If C{True} the center, otherwise the south-west, lower-left corner (C{bool}). @return: A L{LatLonPrec3Tuple}C{(lat, lon, precision)}. @raise WGRSError: Invalid B{C{georef}}, INValid, non-alphanumeric or odd length B{C{georef}}. ''' def _digit(ll, g, i, m): d = _Digits.find(g[i]) if d < 0 or d >= m: raise _Error(i) return ll * m + d def _Error(i): return WGRSError(Fmt.SQUARE(georef=i), georef) def _index(chars, g, i): k = chars.find(g[i]) if k < 0: raise _Error(i) return k g, precision = _2geostr2(georef) lon = _index(_LonTile, g, 0) + _LonOrig_Tile lat = _index(_LatTile, g, 1) + _LatOrig_Tile u = _1_0 if precision > 0: lon = lon * _Tile + _index(_DegChar, g, 2) lat = lat * _Tile + _index(_DegChar, g, 3) m, p = 6, precision - 1 for i in range(_BaseLen, _BaseLen + p): lon = _digit(lon, g, i, m) lat = _digit(lat, g, i + p, m) u *= m m = _Base u *= _Tile if center: lon = lon * _2_0 + _1_0 lat = lat * _2_0 + _1_0 u *= _2_0 u = _Tile / u r = LatLonPrec3Tuple(Lat(lat * u, Error=WGRSError), Lon(lon * u, Error=WGRSError), precision) return _xnamed(r, nameof(georef))
def reset(self, lat0, lon0): '''Set or reset the center point of this Cassini-Soldner projection. @arg lat0: Center point latitude (C{degrees90}). @arg lon0: Center point longitude (C{degrees180}). ''' g, M = self.datum.ellipsoid._geodesic_Math2 self._meridian = m = g.Line(Lat(lat0, name=_lat0_), Lon(lon0, name=_lon0_), 0.0, g.STANDARD | g.DISTANCE_IN) self._latlon0 = LatLon2Tuple(m.lat1, m.lon1) s, c = M.sincosd(m.lat1) # == self.lat0 == self.LatitudeOrigin() self._sb0, self._cb0 = M.norm(s * (1.0 - g.f), c)
def geoidHeight2(lat, lon, model=0, timeout=2.0): '''Get the C{NAVD88} geoid height at an C{NAD83} location. @arg lat: Latitude (C{degrees}). @arg lon: Longitude (C{degrees}). @kwarg model: Optional, geoid model ID (C{int}). @kwarg timeout: Optional, query timeout (seconds). @return: An L{GeoidHeight2Tuple}C{(height, model_name)} or C{(None, "error"}) in case of errors. @raise ValueError: Invalid B{C{timeout}}. @note: The returned C{height} is C{None} if B{C{lat}} or B{C{lon}} is invalid or outside the C{Conterminous US (CONUS)}, if the B{C{model}} was invalid, if conversion failed or if the query timed out. The C{error} is the C{HTTP-, IO-, SSL-, Type-, URL-} or C{ValueError} as a string (C{str}). @see: U{NOAA National Geodetic Survey <https://www.NGS.NOAA.gov/INFO/geodesy.shtml>}, U{Geoid<https://www.NGS.NOAA.gov/web_services/geoid.shtml>}, U{USGS10mElev.py<https://Gist.GitHub.com/pyRobShrk>}, module L{geoids}, classes L{GeoidG2012B}, L{GeoidKarney} and L{GeoidPGM}. ''' try: j = _qURL('https://Geodesy.NOAA.gov/api/geoid/ght', ('lat=%.6F' % (Lat(lat), ), 'lon=%.6F' % (Lon(lon), ), 'model=%s' % (model, ) if model else ''), timeout=Scalar(timeout, name='timeout')) # PYCHOK 5 if j[:1] == '{' and j[-1:] == '}' and j.find('"error":') > 0: d = _json(j) if isinstance(d.get('error', 'N/A'), float): h = d.get('geoidHeight', None) if h is not None: m = _str(d.get('geoidModel', 'N/A')) return GeoidHeight2Tuple(h, m) e = 'geoidHeight' else: e = 'JSON' e = 'no %s "%s"' % (e, clips(j, limit=256, white=' ')) except (HTTPError, IOError, TypeError, ValueError) as x: e = repr(x) e = _error(geoidHeight2, lat, lon, e) return GeoidHeight2Tuple(None, e)
def __init__(self, datum=Datums.WGS84, lon0=0, k0=_K0, extendp=True, name=NN): '''New L{ExactTransverseMercator} projection. @kwarg datum: The datum, ellipsoid to use (L{Datum}, L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}). @kwarg lon0: The central meridian (C{degrees180}). @kwarg k0: The central scale factor (C{float}). @kwarg extendp: Use the extended domain (C{bool}). @kwarg name: Optional name for the projection (C{str}). @raise EllipticError: No convergence. @raise ETMError: Invalid B{C{k0}}. @raise TypeError: Invalid B{C{datum}}. @raise ValueError: Invalid B{C{lon0}} or B{C{k0}}. @note: The maximum error for all 255.5K U{TMcoords.dat <https://Zenodo.org/record/32470>} tests (with C{0 <= lat <= 84} and C{0 <= lon}) is C{5.2e-08 .forward} or 52 nano-meter easting and northing and C{3.8e-13 .reverse} or 0.38 pico-degrees lat- and longitude (with Python 3.7.3, 2.7.16, PyPy6 3.5.3 and PyPy6 2.7.13, all in 64-bit on macOS 10.13.6 High Sierra). ''' if not extendp: self._extendp = False if name: self.name = name self.datum = datum self.lon0 = Lon(lon0=lon0) self.k0 = k0
def _2fllh(lat, lon, height=None): '''(INTERNAL) Convert lat, lon, height. ''' # lat, lon = parseDMS2(lat, lon) return (Lat(lat, Error=WGRSError), Lon(lon, Error=WGRSError), height)
def __new__(cls, lat, lon, azi, rk): return _NamedTuple.__new__(cls, Lat(lat, Error=CSSError), Lon(lon, Error=CSSError), Bearing(azi, Error=CSSError), Scalar(rk, Error=CSSError))
def _2fll(lat, lon, *unused): '''(INTERNAL) Convert lat, lon. ''' # lat, lon = parseDMS2(lat, lon) return (Lat(lat, Error=GARSError), Lon(lon, Error=GARSError))
def _2fll(lat, lon, *unused): '''(INTERNAL) Convert lat, lon to 2-tuple of floats. ''' # lat, lon = parseDMS2(lat, lon) return (Lat(lat, Error=GeohashError), Lon(lon, Error=GeohashError))
def lon0(self, lon0): '''Set the central meridian (C{degrees180}). @raise ValueError: Invalid B{C{lon0}}. ''' self._lon0 = _norm180(Lon(lon0, name=_lon0_))
def __new__(cls, z, B, h, lat, lon, Error=None): if Error is not None: lat = Lat(lat, Error=Error) lon = Lon(lon, Error=Error) return _NamedTuple.__new__(cls, z, B, h, lat, lon)
def __new__(cls, lat, lon, d, c, s): return _NamedTuple.__new__(cls, Lat(lat), Lon(lon), d, Scalar(c, name=_convergence_), Scalar(s, name=_scale_))