示例#1
0
    def latlon(self, latlonh):
        '''Set the lat- and longitude and optionally the height.

           @param latlonh: New lat-, longitude and height (2- or
                           3-tuple of C{degrees} and C{meter}).

           @raise TypeError: Height of I{latlonh} not C{scalar} or
                             I{latlonh} not C{list} or C{tuple}.

           @raise ValueError: Invalid I{latlonh} or M{len(latlonh)}.

           @see: Function L{parse3llh} to parse a I{latlonh} string
                 into a 3-tuple (lat, lon, h).
        '''
        if not isinstance(latlonh, (list, tuple)):
            raise TypeError('%s invalid: %r' % ('latlonh', latlonh))

        if len(latlonh) == 3:
            h = scalar(latlonh[2], None, name='latlonh')
        elif len(latlonh) != 2:
            raise ValueError('%s invalid: %r' % ('latlonh', latlonh))
        else:
            h = self._height

        lat, lon = parseDMS2(latlonh[0], latlonh[1])
        self._update(lat != self._lat or
                     lon != self._lon or h != self._height)
        self._lat, self._lon, self._height = lat, lon, h
示例#2
0
def upsZoneBand5(lat, lon, strict=True):
    '''Return the UTM/UPS zone number, (polar) Band letter, pole and
       clipped lat- and longitude for a given location.

       @param lat: Latitude in degrees (C{scalar} or C{str}).
       @param lon: Longitude in degrees (C{scalar} or C{str}).
       @keyword strict: Restrict B{C{lat}} to UPS ranges (C{bool}).

       @return: A L{UtmUpsLatLon5Tuple}C{(zone, band, hemipole,
                lat, lon)} where C{hemipole} is the C{'N'|'S'} pole,
                the UPS projection top/center.

       @raise RangeError: If B{C{strict}} and B{C{lat}} in the UTM
                          and not the UPS range or if B{C{lat}} or
                          B{C{lon}} outside the valid range and
                          L{rangerrors} set to C{True}.

       @raise ValueError: Invalid B{C{lat}} or B{C{lon}}.
    '''
    z, lat, lon = _to3zll(*parseDMS2(lat, lon))
    if lat < _UPS_LAT_MIN:  # includes 30' overlap
        z, B, p = _UPS_ZONE, _Band(lat, lon), 'S'

    elif lat > _UPS_LAT_MAX:  # includes 30' overlap
        z, B, p = _UPS_ZONE, _Band(lat, lon), 'N'

    elif strict:
        x = '%s [%s, %s]' % ('range', _UPS_LAT_MIN, _UPS_LAT_MAX)
        raise RangeError('%s inside UTM %s: %s' % ('lat', x, degDMS(lat)))

    else:
        B, p = '', _hemi(lat)
    return UtmUpsLatLon5Tuple(z, B, p, lat, lon)
示例#3
0
    def __init__(self, lat, lon, height=0, name=''):
        '''New C{LatLon}.

           @param lat: Latitude (C{degrees} or DMS C{str} with N or S suffix).
           @param lon: Longitude (C{degrees} or DMS C{str} with E or W suffix).
           @keyword height: Optional height (C{meter} above or below the earth surface).
           @keyword name: Optional name (C{str}).

           @return: New instance (C{LatLon}).

           @raise RangeError: Value of I{lat} or I{lon} outside the valid
                              range and I{rangerrrors} set to C{True}.

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

           @example:

           >>> p = LatLon(50.06632, -5.71475)
           >>> q = LatLon('50°03′59″N', """005°42'53"W""")
        '''
        self._lat, self._lon = parseDMS2(lat, lon)
        if height:  # elevation
            self._height = scalar(height, None, name='height')
        if name:
            self.name = name
示例#4
0
def upsZoneBand5(lat, lon, strict=True):
    '''Return the UTM/UPS zone number, (polar) Band letter, pole and
       clipped lat- and longitude for a given location.

       @param lat: Latitude in degrees (C{scalar} or C{str}).
       @param lon: Longitude in degrees (C{scalar} or C{str}).
       @keyword strict: Restrict I{lat} to UPS ranges (C{bool}).

       @return: 5-Tuple (C{zone, Band, hemisphere, lat, lon}) as
                (C{int, str, 'N'|'S', degrees90, degrees180}) where
                C{zone} is always C{0} for UPS and polar C{Band} is
                C{""} or C{'A'|'B'|'Y'|'Z'}.

       @raise RangeError: If I{strict} and I{lat} in UTM and not UPS
                          range or if I{lat} or I{lon} outside the valid
                          range and I{rangerrrors} set to C{True}.

       @raise ValueError: Invalid I{lat} or I{lon}.
    '''
    z, lat, lon = _to3zll(*parseDMS2(lat, lon))
    if lat < _UPS_LAT_MIN:  # includes 30' overlap
        return _UPS_ZONE, _Band(lat, lon), 'S', lat, lon

    elif lat > _UPS_LAT_MAX:  # includes 30' overlap
        return _UPS_ZONE, _Band(lat, lon), 'N', lat, lon

    elif strict:
        x = '%s [%s, %s]' % ('range', _UPS_LAT_MIN, _UPS_LAT_MAX)
        raise RangeError('%s inside UTM %s: %s' % ('lat', x, degDMS(lat)))

    return z, '', _hemi(lat), lat, lon
示例#5
0
def toOsgr(latlon, lon=None, datum=Datums.WGS84, Osgr=Osgr):
    '''Convert a lat-/longitude point to an OSGR coordinate.

       @param latlon: Latitude (degrees) or an (ellipsoidal)
                      geodetic I{LatLon} point.
       @keyword lon: Optional longitude in degrees (scalar or None).
       @keyword datum: Optional datum to convert (I{Datum}).
       @keyword Osgr: Optional Osgr class to use for the
                      OSGR coordinate (L{Osgr}).

       @return: The OSGR coordinate (L{Osgr}).

       @raise TypeError: If I{latlon} is not ellipsoidal or if
                         I{datum} conversion failed.

       @raise ValueError: Invalid I{latlon} or I{lon}.

       @example:

       >>> p = LatLon(52.65798, 1.71605)
       >>> r = toOsgr(p)  # TG 51409 13177
       >>> # for conversion of (historical) OSGB36 lat-/longitude:
       >>> r = toOsgr(52.65757, 1.71791, datum=Datums.OSGB36)
    '''
    if not isinstance(latlon, _eLLb):
        # XXX fix failing _eLLb.convertDatum()
        latlon = _eLLb(*parseDMS2(latlon, lon), datum=datum)
    elif lon is not None:
        raise ValueError('%s not %s: %r' % ('lon', None, lon))

    E = _OSGB36.ellipsoid

    ll = _ll2datum(latlon, _OSGB36, 'latlon')
    a, b = map1(radians, ll.lat, ll.lon)

    ca, sa, ta = cos(a), sin(a), tan(a)

    s = E.e2s2(sa)
    v = E.a * _F0 / sqrt(s)  # nu
    r = s / E.e12  # nu / rho == v / (v * E.e12 / s)

    x2 = r - 1  # η2

    ca3, ca5 = fpowers(ca, 5, 3)  # PYCHOK false!
    ta2, ta4 = fpowers(ta, 4, 2)  # PYCHOK false!

    vsa = v * sa
    I4 = (E.b * _F0 * _M(E.Mabcd, a) + _N0, (vsa / 2) * ca,
          (vsa / 24) * ca3 * fsum_(5, -ta2, 9 * x2),
          (vsa / 720) * ca5 * fsum_(61, ta4, -58 * ta2))

    V4 = (_E0, (v * ca), (v / 6) * ca3 * (r - ta2), (v / 120) * ca5 * fdot(
        (-18, 1, 14, -58), ta2, 5 + ta4, x2, ta2 * x2))

    d, d2, d3, d4, d5, d6 = fpowers(b - _B0, 6)  # PYCHOK false!
    n = fdot(I4, 1, d2, d4, d6)
    e = fdot(V4, 1, d, d3, d5)

    return Osgr(e, n)
示例#6
0
def _to4lldn(latlon, lon, datum, name):
    '''(INTERNAL) Return 4-tuple (C{lat, lon, datum, name}).
    '''
    try:
        # if lon is not None:
        #     raise AttributeError
        lat, lon = latlon.lat, latlon.lon
        if not isinstance(latlon, _LLEB):
            raise TypeError('%s not %s: %r' %
                            ('latlon', 'ellipsoidal', latlon))
        d = datum or latlon.datum
    except AttributeError:
        lat, lon = parseDMS2(latlon, lon)
        d = datum or Datums.WGS84
    return lat, lon, d, (name or nameof(latlon))
示例#7
0
def utmZoneBand2(lat, lon):
    '''Return the UTM zone number and UTM Band letter for a location.

       @param lat: Latitude (C{degrees}) or string.
       @param lon: Longitude (C{degrees}) or string.

       @return: 2-Tuple (zone, Band) as (int, string).

       @raise RangeError: If I{lat} is outside the valid UTM bands or if
                          I{lat} or I{lon} outside the valid range and
                          I{rangerrrors} set to C{True}.

       @raise ValueError: Invalid I{lat} or I{lon}.
    '''
    return _toZBab4(*parseDMS2(lat, lon))[:2]
示例#8
0
def toWm(latlon, lon=None, radius=R_MA, Wm=Wm, name=''):
    '''Convert a lat-/longitude point to a WM coordinate.

       @param latlon: Latitude (C{degrees}) or an (ellipsoidal or
                      spherical) geodetic C{LatLon} point.
       @keyword lon: Optional longitude (C{degrees} or C{None}).
       @keyword radius: Optional earth radius (C{meter}).
       @keyword Wm: Optional (sub-)class to return the WM coordinate
                    (L{Wm}) or C{None}.
       @keyword name: Optional name (C{str}).

       @return: The WM coordinate (B{C{Wm}}) or an
                L{EasNorRadius3Tuple}C{(easting, northing, radius)}
                if B{C{Wm}} is C{None}.

       @raise ValueError: If B{C{lon}} value is missing, if B{C{latlon}}
                          is not scalar, if B{C{latlon}} is beyond the
                          valid WM range and L{rangerrors} is set
                          to C{True} or if B{C{radius}} is invalid.

       @example:

       >>> p = LatLon(48.8582, 2.2945)  # 448251.8 5411932.7
       >>> w = toWm(p)  # 448252 5411933
       >>> p = LatLon(13.4125, 103.8667)  # 377302.4 1483034.8
       >>> w = toWm(p)  # 377302 1483035
    '''
    r, e = radius, None
    try:
        lat, lon = latlon.lat, latlon.lon
        if isinstance(latlon, _LLEB):
            r = latlon.datum.ellipsoid.a
            e = latlon.datum.ellipsoid.e
            if not name:  # use latlon.name
                name = nameof(latlon)
        lat = clipDMS(lat, _LatLimit)
    except AttributeError:
        lat, lon = parseDMS2(latlon, lon, clipLat=_LatLimit)

    s = sin(radians(lat))
    y = atanh(s)  # == log(tan((90 + lat) / 2)) == log(tanPI_2_2(radians(lat)))
    if e:
        y -= e * atanh(e * s)

    e, n = r * radians(lon), r * y
    r = EasNorRadius3Tuple(e, n, r) if Wm is None else \
                        Wm(e, n, radius=r)
    return _xnamed(r, name)
示例#9
0
def toWm(latlon, lon=None, radius=R_MA, Wm=Wm):
    '''Convert a lat-/longitude point to a WM coordinate.

       @param latlon: Latitude (degrees) or an (ellipsoidal or
                      spherical) geodetic I{LatLon} point.
       @keyword lon: Optional longitude (degrees or None).
       @keyword radius: Optional earth radius (meter).
       @keyword Wm: Optional Wm class for the WM coordinate (L{Wm}).

       @return: The WM coordinate (L{Wm}).

       @raise ValueError: If I{lon} value is missing, if I{latlon}
                          is not scalar, if I{latlon} is beyond
                          the valid WM range and L{rangerrors} is
                          set to True or if I{radius} is invalid.

       @example:

       >>> p = LatLon(48.8582, 2.2945)  # 448251.8 5411932.7
       >>> w = toWm(p)  # 448252 5411933
       >>> p = LatLon(13.4125, 103.8667)  # 377302.4 1483034.8
       >>> w = toWm(p)  # 377302 1483035
    '''
    r, e = radius, None
    try:
        lat, lon = latlon.lat, latlon.lon
        if isinstance(latlon, _eLLb):
            r = latlon.datum.ellipsoid.a
            e = latlon.datum.ellipsoid.e
        lat = clipDMS(lat, _LatLimit)
    except AttributeError:
        lat, lon = parseDMS2(latlon, lon, clipLat=_LatLimit)

    s = sin(radians(lat))
    y = atanh(s)  # == log(tan((90 + lat) / 2)) == log(tanPI_2_2(radians(lat)))
    if e:
        y -= e * atanh(e * s)
    return Wm(r * radians(lon), r * y, radius=r)
示例#10
0
def utmZoneBand5(lat, lon, cmoff=False):
    '''Return the UTM zone number, Band letter, hemisphere and
       (clipped) lat- and longitude for a given location.

       @param lat: Latitude in degrees (C{scalar} or C{str}).
       @param lon: Longitude in degrees (C{scalar} or C{str}).
       @keyword cmoff: Offset longitude from the zone's central
                       meridian (C{bool}).

       @return: A L{UtmUpsLatLon5Tuple}C{(zone, band, hemipole,
                lat, lon)} where C{hemipole} is the C{'N'|'S'}
                UTM hemisphere.

       @raise RangeError: If B{C{lat}} outside the valid UTM bands
                          or if B{C{lat}} or B{C{lon}} outside the
                          valid range and L{rangerrors} set to
                          C{True}.

       @raise ValueError: Invalid B{C{lat}} or B{C{lon}}.
    '''
    lat, lon = parseDMS2(lat, lon)
    z, B, lat, lon = _to3zBll(lat, lon, cmoff=cmoff)
    return UtmUpsLatLon5Tuple(z, B, _hemi(lat), lat, lon)
示例#11
0
def utmZoneBand5(lat, lon, cmoff=False):
    '''Return the UTM zone number, Band letter, hemisphere and
       clipped lat- and longitude for a given location.

       @param lat: Latitude in degrees (C{scalar} or C{str}).
       @param lon: Longitude in degrees (C{scalar} or C{str}).
       @keyword cmoff: Offset longitude from zone's central meridian
                       (C{bool}).

       @return: 5-Tuple (C{zone, Band, hemisphere, lat, lon}) as
                (C{int, str, 'N'|'S', degrees90, degrees180}) where
                C{zone} is C{1..60} and C{Band} is C{'C'|'D'..'W'|'X'}
                for UTM.

       @raise RangeError: If I{lat} outside the valid UTM bands or if
                          I{lat} or I{lon} outside the valid range and
                          I{rangerrrors} set to C{True}.

       @raise ValueError: Invalid I{lat} or I{lon}.
    '''
    lat, lon = parseDMS2(lat, lon)
    z, B, lat, lon = _to3zBll(lat, lon, cmoff=cmoff)
    return z, B, _hemi(lat), lat, lon
示例#12
0
def toOsgr(latlon, lon=None, datum=Datums.WGS84, Osgr=Osgr, name=''):
    '''Convert a lat-/longitude point to an OSGR coordinate.

       @param latlon: Latitude (C{degrees}) or an (ellipsoidal)
                      geodetic C{LatLon} point.
       @keyword lon: Optional longitude in degrees (scalar or C{None}).
       @keyword datum: Optional datum to convert (C{Datum}).
       @keyword Osgr: Optional (sub-)class to return the OSGR
                      coordinate (L{Osgr}) or C{None}.
       @keyword name: Optional I{Osgr} name (C{str}).

       @return: The OSGR coordinate (L{Osgr}) or 2-tuple (easting,
                northing) if I{Osgr} is C{None}.

       @raise TypeError: Non-ellipsoidal I{latlon} or I{datum}
                         conversion failed.

       @raise ValueError: Invalid I{latlon} or I{lon}.

       @example:

       >>> p = LatLon(52.65798, 1.71605)
       >>> r = toOsgr(p)  # TG 51409 13177
       >>> # for conversion of (historical) OSGB36 lat-/longitude:
       >>> r = toOsgr(52.65757, 1.71791, datum=Datums.OSGB36)
    '''
    if not isinstance(latlon, _LLEB):
        # XXX fix failing _LLEB.convertDatum()
        latlon = _LLEB(*parseDMS2(latlon, lon), datum=datum)
    elif lon is not None:
        raise ValueError('%s not %s: %r' % ('lon', None, lon))
    elif not name:  # use latlon.name
        name = _nameof(latlon) or name  # PYCHOK no effect

    E = _OSGB36.ellipsoid

    ll = _ll2datum(latlon, _OSGB36, 'latlon')
    a, b = map1(radians, ll.lat, ll.lon)

    sa, ca = sincos2(a)

    s = E.e2s2(sa)
    v = E.a * _F0 / sqrt(s)  # nu
    r = s / E.e12  # nu / rho == v / (v * E.e12 / s)

    x2 = r - 1  # η2
    ta = tan(a)

    ca3, ca5 = fpowers(ca, 5, 3)  # PYCHOK false!
    ta2, ta4 = fpowers(ta, 4, 2)  # PYCHOK false!

    vsa = v * sa
    I4 = (E.b * _F0 * _M(E.Mabcd, a) + _N0, (vsa / 2) * ca,
          (vsa / 24) * ca3 * fsum_(5, -ta2, 9 * x2),
          (vsa / 720) * ca5 * fsum_(61, ta4, -58 * ta2))

    V4 = (_E0, (v * ca), (v / 6) * ca3 * (r - ta2), (v / 120) * ca5 * fdot(
        (-18, 1, 14, -58), ta2, 5 + ta4, x2, ta2 * x2))

    d, d2, d3, d4, d5, d6 = fpowers(b - _B0, 6)  # PYCHOK false!
    n = fdot(I4, 1, d2, d4, d6)
    e = fdot(V4, 1, d, d3, d5)

    return (e, n) if Osgr is None else _xnamed(Osgr(e, n), name)
示例#13
0
def toUtm(latlon, lon=None, datum=None, Utm=Utm, name='', cmoff=True):
    '''Convert a lat-/longitude point to a UTM coordinate.

       @param latlon: Latitude (C{degrees}) or an (ellipsoidal)
                      geodetic C{LatLon} point.
       @keyword lon: Optional longitude (C{degrees} or C{None}).
       @keyword datum: Optional datum for this UTM coordinate,
                       overriding I{latlon}'s datum (C{Datum}).
       @keyword Utm: Optional (sub-)class to use for the UTM
                     coordinate (L{Utm}) or C{None}.
       @keyword name: Optional I{Utm} name (C{str}).
       @keyword cmoff: Offset longitude from zone's central meridian,
                       apply false easting and false northing (C{bool}).

       @return: The UTM coordinate (L{Utm}) or a 6-tuple (zone, easting,
                northing, band, convergence, scale) if I{Utm} is C{None}
                or I{cmoff} is C{False}.

       @raise TypeError: If I{latlon} is not ellipsoidal.

       @raise RangeError: If I{lat} is outside the valid UTM bands or if
                          I{lat} or I{lon} outside the valid range and
                          I{rangerrrors} set to C{True}.

       @raise ValueError: If I{lon} value is missing or if I{latlon}
                          is invalid.

       @note: Implements Karney’s method, using 8-th order Krüger series,
              giving results accurate to 5 nm (or better) for distances
              up to 3900 km from the central meridian.

       @example:

       >>> p = LatLon(48.8582, 2.2945)  # 31 N 448251.8 5411932.7
       >>> u = toUtm(p)  # 31 N 448252 5411933
       >>> p = LatLon(13.4125, 103.8667) # 48 N 377302.4 1483034.8
       >>> u = toUtm(p)  # 48 N 377302 1483035
    '''
    try:
        lat, lon = latlon.lat, latlon.lon
        if not isinstance(latlon, _LLEB):
            raise TypeError('%s not %s: %r' %
                            ('latlon', 'ellipsoidal', latlon))
        if not name:  # use latlon.name
            name = _nameof(latlon) or name  # PYCHOK no effect
        d = datum or latlon.datum
    except AttributeError:
        lat, lon = parseDMS2(latlon, lon)
        d = datum or Datums.WGS84

    E = d.ellipsoid

    z, B, a, b = _toZBab4(lat, lon, cmoff)

    # easting, northing: Karney 2011 Eq 7-14, 29, 35
    cb, sb, tb = cos(b), sin(b), tan(b)

    T = tan(a)
    T12 = hypot1(T)
    S = sinh(E.e * atanh(E.e * T / T12))

    T_ = T * hypot1(S) - S * T12
    H = hypot(T_, cb)

    y = atan2(T_, cb)  # ξ' ksi
    x = asinh(sb / H)  # η' eta

    A0 = _K0 * E.A

    Ks = _Kseries(E.AlphaKs, x, y)  # Krüger series
    y = Ks.ys(y) * A0  # ξ
    x = Ks.xs(x) * A0  # η

    if cmoff:
        # C.F.F. Karney, "Test data for the transverse Mercator projection (2009)",
        # <http://GeographicLib.SourceForge.io/html/transversemercator.html> and
        # <http://Zenodo.org/record/32470#.W4LEJS2ZON8>
        x += _FalseEasting  # make x relative to false easting
        if y < 0:
            y += _FalseNorthing  # y relative to false northing in S

    # convergence: Karney 2011 Eq 23, 24
    p_ = Ks.ps(1)
    q_ = Ks.qs(0)
    c = degrees(atan(T_ / hypot1(T_) * tb) + atan2(q_, p_))

    # scale: Karney 2011 Eq 25
    s = E.e2s(sin(a)) * T12 / H * (A0 / E.a * hypot(p_, q_))

    if cmoff and Utm is not None:
        h = 'S' if a < 0 else 'N'  # hemisphere
        return _xnamed(
            Utm(z, h, x, y, band=B, datum=d, convergence=c, scale=s), name)
    else:  # zone, easting, northing, band, convergence and scale
        return z, x, y, B, c, s
示例#14
0
def _2fll(lat, lon, *unused):
    '''(INTERNAL) Convert lat, lon to 2-tuple of floats.
    '''
    return parseDMS2(lat, lon)
示例#15
0
def _2fll(lat, lon, *unused):
    '''(INTERNAL) Convert lat, lon.
    '''
    return parseDMS2(lat, lon)
示例#16
0
def _2fllh(lat, lon, height=None):
    '''(INTERNAL) Convert lat, lon, height.
    '''
    return parseDMS2(lat, lon) + (height,)
示例#17
0
def toUtm(latlon, lon=None, datum=None, Utm=Utm):
    '''Convert a lat-/longitude point to a UTM coordinate.

       @note: Implements Karney’s method, using 6-th order Krüger
       series, giving results accurate to 5 nm for distances up to
       3900 km from the central meridian.

       @param latlon: Latitude (degrees) or an (ellipsoidal)
                      geodetic I{LatLon} point.
       @keyword lon: Optional longitude (degrees or None).
       @keyword datum: Optional datum for this UTM coordinate,
                       overriding latlon's datum (I{Datum}).
       @keyword Utm: Optional I{Utm} class to usefor the UTM
                     coordinate (L{Utm}).

       @return: The UTM coordinate (L{Utm}).

       @raise TypeError: If I{latlon} is not ellipsoidal.

       @raise ValueError: If I{lon} value is missing, if I{latlon}
                          is not scalar or I{latlon} is outside
                          the valid UTM bands.

       @example:

       >>> p = LatLon(48.8582, 2.2945)  # 31 N 448251.8 5411932.7
       >>> u = toUtm(p)  # 31 N 448252 5411933
       >>> p = LatLon(13.4125, 103.8667) # 48 N 377302.4 1483034.8
       >>> u = toUtm(p)  # 48 N 377302 1483035
    '''
    try:
        lat, lon = latlon.lat, latlon.lon
        if not isinstance(latlon, _eLLb):
            raise TypeError('%s not %s: %r' %
                            ('latlon', 'ellipsoidal', latlon))
        d = datum or latlon.datum
    except AttributeError:
        lat, lon = parseDMS2(latlon, lon)
        d = datum or Datums.WGS84

    E = d.ellipsoid

    z, B, a, b = _toZBll(lat, lon)
    h = 'S' if a < 0 else 'N'  # hemisphere

    # easting, northing: Karney 2011 Eq 7-14, 29, 35
    cb, sb, tb = cos(b), sin(b), tan(b)

    T = tan(a)
    T12 = hypot1(T)
    S = sinh(E.e * atanh(E.e * T / T12))

    T_ = T * hypot1(S) - S * T12
    H = hypot(T_, cb)

    y = atan2(T_, cb)  # ξ' ksi
    x = asinh(sb / H)  # η' eta

    A0 = _K0 * E.A

    A6 = _K6s(E.Alpha6, x, y)  # 6th-order Krüger series, 1-origin
    y = A6.ys(y) * A0  # ξ
    x = A6.xs(x) * A0  # η

    x += _FalseEasting  # make x relative to false easting
    if y < 0:
        y += _FalseNorthing  # y relative to false northing in S

    # convergence: Karney 2011 Eq 23, 24
    p_ = A6.ps(1)
    q_ = A6.qs(0)
    c = degrees(atan(T_ / hypot1(T_) * tb) + atan2(q_, p_))

    # scale: Karney 2011 Eq 25
    s = E.e2s(sin(a)) * T12 / H * (A0 / E.a * hypot(p_, q_))

    return Utm(z, h, x, y, band=B, datum=d, convergence=c, scale=s)