def _reset(self, e, e2): '''(INTERNAL) Get elliptic functions and pre-compute some frequently used values. @arg e: Eccentricity (C{float}). @arg e2: Eccentricity squared (C{float}). @raise EllipticError: No convergence. ''' # assert e2 == e**2 self._e = e self._e_PI_2 = e * PI_2 self._e_PI_4 = e * PI_4 self._e_TAYTOL = e * _TAYTOL self._1_e_90 = (1 - e) * 90 self._1_e_PI_2 = (1 - e) * PI_2 self._1_e2_PI_2 = (1 - e * 2) * PI_2 self._mu = e2 self._mu_2_1 = (e2 + 2) * 0.5 self._Eu = Elliptic(self._mu) self._Eu_cE_1_4 = self._Eu.cE * 0.25 self._Eu_cK_cE = self._Eu.cK / self._Eu.cE self._Eu_cK_PI_2 = self._Eu.cK / PI_2 self._mv = 1 - e2 self._3_mv = 3.0 / self._mv self._3_mv_e = self._3_mv / e self._Ev = Elliptic(self._mv) self._Ev_cKE_3_4 = self._Ev.cKE * 0.75 self._Ev_cKE_5_4 = self._Ev.cKE * 1.25
def _reset(self, E): '''(INTERNAL) Get elliptic functions and pre-compute some frequently used values. @arg E: An ellipsoid (L{Ellipsoid}). @raise EllipticError: No convergence. ''' e, e2 = E.e, E.e2 # assert e2 == e**2 != _0_0 self._e_PI_2 = e * PI_2 self._e_PI_4 = e * PI_4 self._e_TAYTOL = e * _TAYTOL self._1_e_90 = (_1_0 - e) * _90_0 self._1_e_PI_2 = (_1_0 - e) * PI_2 self._1_e2_PI_2 = (_1_0 - e * 2) * PI_2 self._mu = e2 self._mu_2_1 = (e2 + _2_0) * _0_5 self._Eu = Elliptic(self._mu) self._Eu_cE_1_4 = self._Eu.cE * 0.25 self._Eu_cK_cE = self._Eu.cK / self._Eu.cE self._Eu_cK_PI_2 = self._Eu.cK / PI_2 self._mv = _1_0 - e2 self._3_mv = _3_0 / self._mv self._3_mv_e = self._3_mv / e self._Ev = Elliptic(self._mv) self._Ev_cKE_3_4 = self._Ev.cKE * 0.75 self._Ev_cKE_5_4 = self._Ev.cKE * 1.25 self._iteration = 0 self._E = E self._k0_a = E.a * self.k0 # see .k0 setter
def _reset(self, e, e2): '''(INTERNAL) Get elliptic functions and pre-compute frequently used values. @raise EllipticError: No convergence. ''' # assert e2 == e**2 self._e = e # eccentricity = sqrt(f * (2 - f)) self._e_PI_2 = e * PI_2 self._e_PI_4 = e * PI_4 self._e_taytol_ = e * _TAYTOL self._1_e_90 = (1 - e) * 90 self._1_e_PI_2 = (1 - e) * PI_2 self._1_e2_PI_2 = (1 - e * 2) * PI_2 self._mu = e2 # eccentricity**2 = f * (2 - f) = 1 - (b / a)**2 self._mu_2_1 = (e2 + 2) * 0.5 self._Eu = Elliptic(self._mu) self._Eu_E = self._Eu.cE # constant self._Eu_E_1_4 = self._Eu_E * 0.25 self._Eu_K = self._Eu.cK # constant self._mv = 1 - e2 # 1 - eccentricity**2 = 1 - e2 self._3_mv = 3.0 / self._mv self._3_mv_e = self._3_mv / e self._Ev = Elliptic(self._mv) self._Ev_E = self._Ev.cE # constant self._Ev_K = self._Ev.cK # constant self._Ev_KE = self._Ev.cKE # constant self._Ev_KE_3_4 = self._Ev_KE * 0.75 self._Ev_KE_5_4 = self._Ev_KE * 1.25
class ExactTransverseMercator(_NamedBase): '''A Python version of Karney's U{TransverseMercatorExact <https://GeographicLib.SourceForge.io/html/TransverseMercatorExact_8cpp_source.html>} C++ class, a numerically exact transverse mercator projection, referred to as C{TMExact} here. @see: C{TMExact(real a, real f, real k0, bool extendp)}. ''' _a = 0 # major radius _datum = None # Datum _e = 0 # eccentricity _E = None # Ellipsoid _extendp = False _f = 0 # flattening _k0 = 1 # central scale factor _k0_a = 0 _lon0 = 0 # central meridian _trips_ = _TRIPS def __init__(self, datum=Datums.WGS84, lon0=0, k0=_K0, extendp=True, name=''): '''New L{ExactTransverseMercator} projection. @keyword datum: The datum and ellipsoid to use (C{Datum}). @keyword lon0: The central meridian (C{degrees180}). @keyword k0: The central scale factor (C{float}). @keyword extendp: Use the extended domain (C{bool}). @keyword name: Optional name for the projection (C{str}). @raise EllipticError: No convergence. @raise ETMError: Invalid B{C{k0}}. @raise TypeError: Invalid B{C{datum}}. @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 extendp: self._extendp = bool(extendp) if name: self.name = name self.datum = datum self.lon0 = lon0 self.k0 = k0 @property def datum(self): '''Get the datum (L{Datum}) or C{None}. ''' return self._datum @datum.setter # PYCHOK setter! def datum(self, datum): '''Set the datum and ellipsoid (L{Datum}). @raise EllipticError: No convergence. @raise TypeError: Invalid B{C{datum}}. ''' _TypeError(Datum, datum=datum) E = datum.ellipsoid self._reset(E.e, E.e2) self._a = E.a self._f = E.f # flattening = (a - b) / a self._datum = datum self._E = E @property_RO def extendp(self): '''Get using the extended domain (C{bool}). ''' return self._extendp @property_RO def flattening(self): '''Get the flattening (C{float}). ''' return self._f def forward(self, lat, lon, lon0=None): # MCCABE 13 '''Forward projection, from geographic to transverse Mercator. @param lat: Latitude of point (C{degrees}). @param lon: Longitude of point (C{degrees}). @keyword lon0: Central meridian of the projection (C{degrees}). @return: L{EasNorExact4Tuple}C{(easting, northing, convergence, scale)} in C{meter}, C{meter}, C{degrees} and C{scalar}. @see: C{void TMExact::Forward(real lon0, real lat, real lon, real &x, real &y, real &gamma, real &k)}. @raise EllipticError: No convergence. ''' lat = _fix90(lat) lon, _ = _diff182((self._lon0 if lon0 is None else lon0), lon) # Explicitly enforce the parity _lat = _lon = backside = False if not self.extendp: if lat < 0: _lat, lat = True, -lat if lon < 0: _lon, lon = True, -lon if lon > 90: backside = True if lat == 0: _lat = True lon = 180 - lon # u,v = coordinates for the Thompson TM, Lee 54 if lat == 90: u, v = self._Eu_K, 0 elif lat == 0 and lon == self._1_e_90: u, v = 0, self._Ev_K else: # tau = tan(phi), taup = sinh(psi) tau, lam = tan(radians(lat)), radians(lon) u, v = self._zetaInv(self._E.es_taupf(tau), lam) snu, cnu, dnu = self._Eu.sncndn(u) snv, cnv, dnv = self._Ev.sncndn(v) xi, eta, _ = self._sigma3(v, snu, cnu, dnu, snv, cnv, dnv) if backside: xi = 2 * self._Eu_E - xi y = xi * self._k0_a x = eta * self._k0_a if lat == 90: g, k = lon, self._k0 else: # Recompute (T, L) from (u, v) to improve accuracy of Scale tau, lam, d = self._zeta3( snu, cnu, dnu, snv, cnv, dnv) tau = self._E.es_tauf(tau) g, k = self._scaled(tau, d, snu, cnu, dnu, snv, cnv, dnv) if backside: g = 180 - g if _lat: y, g = -y, -g if _lon: x, g = -x, -g return EasNorExact4Tuple(x, y, g, k) @property def k0(self): '''Get the central scale factor (C{float}), aka I{C{scale0}}. ''' return self._k0 # aka scale0 @k0.setter # PYCHOK setter! def k0(self, k0): '''Set the central scale factor (C{float}), aka I{C{scale0}}. @raise EllipticError: Invalid B{C{k0}}. ''' self._k0 = float(k0) if not 0 < self._k0 <= 1: raise ETMError('%s invalid: %r' % ('k0', k0)) self._k0_a = self._k0 * self._a @property def lon0(self): '''Get the central meridian (C{degrees180}). ''' return self._lon0 @lon0.setter # PYCHOK setter! def lon0(self, lon0): '''Set the central meridian (C{degrees180}). ''' self._lon0 = _wrap180(lon0) @property_RO def majoradius(self): '''Get the major (equatorial) radius, semi-axis (C{float}). ''' return self._a def _reset(self, e, e2): '''(INTERNAL) Get elliptic functions and pre-compute frequently used values. @raise EllipticError: No convergence. ''' # assert e2 == e**2 self._e = e # eccentricity = sqrt(f * (2 - f)) self._e_PI_2 = e * PI_2 self._e_PI_4 = e * PI_4 self._e_taytol_ = e * _TAYTOL self._1_e_90 = (1 - e) * 90 self._1_e_PI_2 = (1 - e) * PI_2 self._1_e2_PI_2 = (1 - e * 2) * PI_2 self._mu = e2 # eccentricity**2 = f * (2 - f) = 1 - (b / a)**2 self._mu_2_1 = (e2 + 2) * 0.5 self._Eu = Elliptic(self._mu) self._Eu_E = self._Eu.cE # constant self._Eu_E_1_4 = self._Eu_E * 0.25 self._Eu_K = self._Eu.cK # constant self._mv = 1 - e2 # 1 - eccentricity**2 = 1 - e2 self._3_mv = 3.0 / self._mv self._3_mv_e = self._3_mv / e self._Ev = Elliptic(self._mv) self._Ev_E = self._Ev.cE # constant self._Ev_K = self._Ev.cK # constant self._Ev_KE = self._Ev.cKE # constant self._Ev_KE_3_4 = self._Ev_KE * 0.75 self._Ev_KE_5_4 = self._Ev_KE * 1.25 def reverse(self, x, y, lon0=None): '''Reverse projection, from transverse Mercator to geographic. @param x: Easting of point (C{meters}). @param y: Northing of point (C{meters}). @keyword lon0: Central meridian of the projection (C{degrees}). @return: L{LatLonExact4Tuple}C{(lat, lon, convergence, scale)} in C{degrees}, C{degrees180}, C{degrees} and C{scalar}. @see: C{void TMExact::Reverse(real lon0, real x, real y, real &lat, real &lon, real &gamma, real &k)} @raise EllipticError: No convergence. ''' # undoes the steps in .forward. xi = y / self._k0_a eta = x / self._k0_a _lat = _lon = backside = False if not self.extendp: # enforce the parity if y < 0: _lat, xi = True, -xi if x < 0: _lon, eta = True, -eta if xi > self._Eu_E: backside = True xi = 2 * self._Eu_E - xi # u,v = coordinates for the Thompson TM, Lee 54 if xi == 0 and eta == self._Ev_KE: u, v = 0, self._Ev_K else: u, v = self._sigmaInv(xi, eta) if v != 0 or u != self._Eu_K: snu, cnu, dnu = self._Eu.sncndn(u) snv, cnv, dnv = self._Ev.sncndn(v) tau, lam, d = self._zeta3( snu, cnu, dnu, snv, cnv, dnv) tau = self._E.es_tauf(tau) lat, lon = degrees(atan(tau)), degrees(lam) g, k = self._scaled(tau, d, snu, cnu, dnu, snv, cnv, dnv) else: lat, lon = 90, 0 g, k = 0, self._k0 if backside: lon, g = 180 - lon, 180 - g if _lat: lat, g = -lat, -g if _lon: lon, g = -lon, -g lat = _wrap180(lat) lon = _wrap180(lon + (self._lon0 if lon0 is None else _wrap180(lon0))) return LatLonExact4Tuple(lat, lon, g, k) def _scaled(self, tau, d2, snu, cnu, dnu, snv, cnv, dnv): ''' @note: Argument B{C{d2}} is C{_mu * cnu**2 + _mv * cnv**2} from C{._sigma3} or C{._zeta3}. @return: 2-Tuple C{(convergence, scale)}. @see: C{void TMExact::Scale(real tau, real /*lam*/, real snu, real cnu, real dnu, real snv, real cnv, real dnv, real &gamma, real &k)}. ''' mu, mv = self._mu, self._mv cnudnv = cnu * dnv # Lee 55.12 -- negated for our sign convention. g gives # the bearing (clockwise from true north) of grid north g = atan2(mv * cnv * snv * snu, cnudnv * dnu) # Lee 55.13 with nu given by Lee 9.1 -- in sqrt change # the numerator from # # (1 - snu^2 * dnv^2) to (_mv * snv^2 + cnu^2 * dnv^2) # # to maintain accuracy near phi = 90 and change the # denomintor from # (dnu^2 + dnv^2 - 1) to (_mu * cnu^2 + _mv * cnv^2) # # to maintain accuracy near phi = 0, lam = 90 * (1 - e). # Similarly rewrite sqrt term in 9.1 as # # _mv + _mu * c^2 instead of 1 - _mu * sin(phi)^2 sec2 = 1 + tau**2 # sec(phi)^2 q2 = (mv * snv**2 + cnudnv**2) / d2 k = sqrt(mv + mu / sec2) * sqrt(sec2) * sqrt(q2) return degrees(g), k * self._k0 def _sigma3(self, v, snu, cnu, dnu, snv, cnv, dnv): # PYCHOK unused ''' @return: 3-Tuple C{(xi, eta, d2)}. @see: C{void TMExact::sigma(real /*u*/, real snu, real cnu, real dnu, real v, real snv, real cnv, real dnv, real &xi, real &eta)}. @raise EllipticError: No convergence. ''' # Lee 55.4 writing # dnu^2 + dnv^2 - 1 = _mu * cnu^2 + _mv * cnv^2 d2 = self._mu * cnu**2 + self._mv * cnv**2 xi = self._Eu.fE(snu, cnu, dnu) - self._mu * snu * cnu * dnu / d2 eta = v - self._Ev.fE(snv, cnv, dnv) + self._mv * snv * cnv * dnv / d2 return xi, eta, d2 def _sigmaDwd(self, snu, cnu, dnu, snv, cnv, dnv): ''' @return: 2-Tuple C{(du, dv)}. @see: C{void TMExact::dwdsigma(real /*u*/, real snu, real cnu, real dnu, real /*v*/, real snv, real cnv, real dnv, real &du, real &dv)}. ''' snuv = snu * snv # Reciprocal of 55.9: dw / ds = dn(w)^2/_mv, # expanding complex dn(w) using A+S 16.21.4 d = self._mv * (cnv**2 + self._mu * snuv**2)**2 r = cnv * dnu * dnv i = -cnu * snuv * self._mu du = (r**2 - i**2) / d dv = 2 * r * i / d return du, dv def _sigmaInv(self, xi, eta): '''Invert C{sigma} using Newton's method. @return: 2-Tuple C{(u, v)}. @see: C{void TMExact::sigmainv(real xi, real eta, real &u, real &v)}. @raise EllipticError: No convergence. ''' u, v, trip = self._sigmaInv0(xi, eta) if not trip: U, V = Fsum(u), Fsum(v) # min iterations = 2, max = 7, mean = 3.9 for _ in range(self._trips_): # GEOGRAPHICLIB_PANIC snu, cnu, dnu = self._Eu.sncndn(u) snv, cnv, dnv = self._Ev.sncndn(v) X, E, _ = self._sigma3(v, snu, cnu, dnu, snv, cnv, dnv) dw, dv = self._sigmaDwd( snu, cnu, dnu, snv, cnv, dnv) X = xi - X E -= eta u, du = U.fsum2_(X * dw, E * dv) v, dv = V.fsum2_(X * dv, -E * dw) if trip: break trip = (du**2 + dv**2) < _TOL_10 else: raise EllipticError('no %s convergence' % ('sigmaInv',)) return u, v def _sigmaInv0(self, xi, eta): '''Starting point for C{sigmaInv}. @return: 3-Tuple C{(u, v, trip)}. @see: C{bool TMExact::sigmainv0(real xi, real eta, real &u, real &v)}. ''' trip = False if eta > self._Ev_KE_5_4 or xi < min(- self._Eu_E_1_4, eta - self._Ev_KE): # sigma as a simple pole at # w = w0 = Eu.K() + i * Ev.K() # and sigma is approximated by # sigma = (Eu.E() + i * Ev.KE()) + 1/(w - w0) x = xi - self._Eu_E y = eta - self._Ev_KE d = x**2 + y**2 u = self._Eu_K + x / d v = self._Ev_K - y / d elif eta > self._Ev_KE or (eta > self._Ev_KE_3_4 and xi < self._Eu_E_1_4): # At w = w0 = i * Ev.K(), we have # sigma = sigma0 = i * Ev.KE() # sigma' = sigma'' = 0 # # including the next term in the Taylor series gives: # sigma = sigma0 - _mv / 3 * (w - w0)^3 # # When inverting this, we map arg(w - w0) = [-pi/2, -pi/6] # to arg(sigma - sigma0) = [-pi/2, pi/2] # mapping arg = [-pi/2, -pi/6] to [-pi/2, pi/2] d = eta - self._Ev_KE r = hypot(xi, d) # Error using this guess is about 0.068 * rad^(5/3) trip = r < _TAYTOL2 # Map the range [-90, 180] in sigma space to [-90, 0] in # w space. See discussion in zetainv0 on the cut for ang. r = cbrt(r * self._3_mv) a = atan2(d - xi, xi + d) / 3.0 - PI_4 s, c = sincos2(a) u = r * c v = r * s + self._Ev_K else: # use w = sigma * Eu.K/Eu.E (correct in the limit _e -> 0) r = self._Eu_K / self._Eu_E u = xi * r v = eta * r return u, v, trip def _zeta3(self, snu, cnu, dnu, snv, cnv, dnv): ''' @return: 3-Tuple C{(taup, lambda, d2)}. @see: C{void TMExact::zeta(real /*u*/, real snu, real cnu, real dnu, real /*v*/, real snv, real cnv, real dnv, real &taup, real &lam)} ''' e = self._e # Lee 54.17 but write # atanh(snu * dnv) = asinh(snu * dnv / sqrt(cnu^2 + _mv * snu^2 * snv^2)) # atanh(_e * snu / dnv) = asinh(_e * snu / sqrt(_mu * cnu^2 + _mv * cnv^2)) d1 = cnu**2 + self._mv * (snu * snv)**2 d2 = self._mu * cnu**2 + self._mv * cnv**2 # Overflow value s.t. atan(overflow) = pi/2 t1 = t2 = copysign(_OVERFLOW, snu) if d1 > 0: t1 = snu * dnv / sqrt(d1) if d2 > 0: t2 = sinh(e * asinh(e * snu / sqrt(d2))) # psi = asinh(t1) - asinh(t2) # taup = sinh(psi) taup = t1 * hypot1(t2) - t2 * hypot1(t1) lam = (atan2( dnu * snv, cnu * cnv) - e * atan2(e * cnu * snv, dnu * cnv)) if (d1 > 0 and d2 > 0) else 0 return taup, lam, d2 def _zetaDwd(self, snu, cnu, dnu, snv, cnv, dnv): ''' @return: 2-Tuple C{(du, dv)}. @see: C{void TMExact::dwdzeta(real /*u*/, real snu, real cnu, real dnu, real /*v*/, real snv, real cnv, real dnv, real &du, real &dv)}. ''' cnu2 = cnu**2 * self._mu cnv2 = cnv**2 dnuv = dnu * dnv dnuv2 = dnuv**2 snuv = snu * snv snuv2 = snuv**2 * self._mu # Lee 54.21 but write # (1 - dnu^2 * snv^2) = (cnv^2 + _mu * snu^2 * snv^2) # (see A+S 16.21.4) d = self._mv * (cnv2 + snuv2)**2 du = cnu * dnuv * (cnv2 - snuv2) / d dv = -cnv * snuv * (cnu2 + dnuv2) / d return du, dv def _zetaInv(self, taup, lam): '''Invert C{zeta} using Newton's method. @return: 2-Tuple C{(u, v)}. @see: C{void TMExact::zetainv(real taup, real lam, real &u, real &v)}. @raise EllipticError: No convergence. ''' psi = asinh(taup) sca = 1.0 / hypot1(taup) u, v, trip = self._zetaInv0(psi, lam) if not trip: stol2 = _TOL_10 / max(psi, 1.0)**2 U, V = Fsum(u), Fsum(v) # min iterations = 2, max = 6, mean = 4.0 for _ in range(self._trips_): # GEOGRAPHICLIB_PANIC snu, cnu, dnu = self._Eu.sncndn(u) snv, cnv, dnv = self._Ev.sncndn(v) T, L, _ = self._zeta3( snu, cnu, dnu, snv, cnv, dnv) dw, dv = self._zetaDwd(snu, cnu, dnu, snv, cnv, dnv) T = (taup - T) * sca L -= lam u, du = U.fsum2_(T * dw, L * dv) v, dv = V.fsum2_(T * dv, -L * dw) if trip: break trip = (du**2 + dv**2) < stol2 else: raise EllipticError('no %s convergence' % ('zetaInv',)) return u, v def _zetaInv0(self, psi, lam): '''Starting point for C{zetaInv}. @return: 3-Tuple C{(u, v, trip)}. @see: C{bool TMExact::zetainv0(real psi, real lam, # radians real &u, real &v)}. ''' trip = False if (psi < -self._e_PI_4 and lam > self._1_e2_PI_2 and psi < lam - self._1_e_PI_2): # N.B. this branch is normally not taken because psi < 0 # is converted psi > 0 by Forward. # # There's a log singularity at w = w0 = Eu.K() + i * Ev.K(), # corresponding to the south pole, where we have, approximately # # psi = _e + i * pi/2 - _e * atanh(cos(i * (w - w0)/(1 + _mu/2))) # # Inverting this gives: h = sinh(1 - psi / self._e) a = (PI_2 - lam) / self._e s, c = sincos2(a) u = self._Eu_K - asinh(s / hypot(c, h)) * self._mu_2_1 v = self._Ev_K - atan2(c, h) * self._mu_2_1 elif (psi < self._e_PI_2 and lam > self._1_e2_PI_2): # At w = w0 = i * Ev.K(), we have # # zeta = zeta0 = i * (1 - _e) * pi/2 # zeta' = zeta'' = 0 # # including the next term in the Taylor series gives: # # zeta = zeta0 - (_mv * _e) / 3 * (w - w0)^3 # # When inverting this, we map arg(w - w0) = [-90, 0] to # arg(zeta - zeta0) = [-90, 180] d = lam - self._1_e_PI_2 r = hypot(psi, d) # Error using this guess is about 0.21 * (rad/e)^(5/3) trip = r < self._e_taytol_ # atan2(dlam-psi, psi+dlam) + 45d gives arg(zeta - zeta0) # in range [-135, 225). Subtracting 180 (since multiplier # is negative) makes range [-315, 45). Multiplying by 1/3 # (for cube root) gives range [-105, 15). In particular # the range [-90, 180] in zeta space maps to [-90, 0] in # w space as required. r = cbrt(r * self._3_mv_e) a = atan2(d - psi, psi + d) / 3.0 - PI_4 s, c = sincos2(a) u = r * c v = r * s + self._Ev_K else: # Use spherical TM, Lee 12.6 -- writing C{atanh(sin(lam) / # cosh(psi)) = asinh(sin(lam) / hypot(cos(lam), sinh(psi)))}. # This takes care of the log singularity at C{zeta = Eu.K()}, # corresponding to the north pole. s, c = sincos2(lam) h, r = sinh(psi), self._Eu_K / PI_2 # But scale to put 90, 0 on the right place u = r * atan2(h, c) v = r * asinh(s / hypot(c, h)) return u, v, trip
class ExactTransverseMercator(_NamedBase): '''A Python version of Karney's U{TransverseMercatorExact <https://GeographicLib.SourceForge.io/html/TransverseMercatorExact_8cpp_source.html>} C++ class, a numerically exact transverse mercator projection, here referred to as C{TMExact}. @see: C{U{TMExact(real a, real f, real k0, bool extendp)<https://GeographicLib.SourceForge.io/ html/classGeographicLib_1_1TransverseMercatorExact.html#a72ffcc89eee6f30a6d1f4d061518a6f1>}}. ''' _a = 0 # major radius _datum = None # Datum _e = 0 # eccentricity _E = None # Ellipsoid _extendp = True _f = 0 # flattening _iteration = 0 # ._sigmaInv and ._zetaInv _k0 = 1 # central scale factor _k0_a = 0 _lon0 = 0 # central meridian # see ._reset() below: # _e_PI_2 = _e * PI_2 # _e_PI_4 = _e * PI_4 # _e_TAYTOL = _e * _TAYTOL # _1_e_90 = (1 - _e) * 90 # _1_e_PI_2 = (1 - _e) * PI_2 # _1_e2_PI_2 = (1 - _e * 2) * PI_2 # _mu = _e**2 # _mu_2_1 = (_e**2 + 2) * 0.5 # _Eu = Elliptic(_mu) # _Eu_cE_1_4 = _Eu.cE * 0.25 # _Eu_cK_cE = _Eu.cK / _Eu.cE # _Eu_cK_PI_2 = _Eu.cK / PI_2 # _mv = 1 - _mu # _3_mv = 3.0 / _mv # _3_mv_e = _3_mv / _e # _Ev = Elliptic(_mv) # _Ev_cKE_3_4 = _Ev.cKE * 0.75 # _Ev_cKE_5_4 = _Ev.cKE * 1.25 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 = lon0 self.k0 = k0 @property_doc_(''' the datum (L{Datum}).''') def datum(self): '''Get the datum (L{Datum}) or C{None}. ''' return self._datum @datum.setter # PYCHOK setter! def datum(self, datum): '''Set the datum and ellipsoid (L{Datum}, L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}). @raise EllipticError: No convergence. @raise TypeError: Invalid B{C{datum}}. ''' d = _ellipsoidal_datum(datum, name=self.name) E = d.ellipsoid self._reset(E.e, E.e2) self._a = E.a self._f = E.f # flattening = (a - b) / a self._datum = d self._E = E @property_RO def equatoradius(self): '''Get the equatorial (major) radius, semi-axis (C{meter}). ''' return self._a majoradius = equatoradius # for backward compatibility '''DEPRECATED, use C{equatoradius}.''' @property_RO def extendp(self): '''Get using the extended domain (C{bool}). ''' return self._extendp @property_RO def flattening(self): '''Get the flattening (C{float}). ''' return self._f def forward(self, lat, lon, lon0=None): # MCCABE 13 '''Forward projection, from geographic to transverse Mercator. @arg lat: Latitude of point (C{degrees}). @arg lon: Longitude of point (C{degrees}). @kwarg lon0: Central meridian of the projection (C{degrees}). @return: L{EasNorExact4Tuple}C{(easting, northing, convergence, scale)} in C{meter}, C{meter}, C{degrees} and C{scalar}. @see: C{void TMExact::Forward(real lon0, real lat, real lon, real &x, real &y, real &gamma, real &k)}. @raise EllipticError: No convergence. ''' lat = _fix90(lat) lon, _ = _diff182((self._lon0 if lon0 is None else lon0), lon) # Explicitly enforce the parity backside = _lat = _lon = False if not self.extendp: if lat < 0: _lat, lat = True, -lat if lon < 0: _lon, lon = True, -lon if lon > 90: backside = True if lat == 0: _lat = True lon = 180 - lon # u,v = coordinates for the Thompson TM, Lee 54 if lat == 90: u = self._Eu.cK v = self._iteration = 0 elif lat == 0 and lon == self._1_e_90: u = self._iteration = 0 v = self._Ev.cK else: # tau = tan(phi), taup = sinh(psi) tau, lam = tan(radians(lat)), radians(lon) u, v = self._zetaInv(self._E.es_taupf(tau), lam) sncndn6 = self._sncndn6(u, v) xi, eta, _ = self._sigma3(v, *sncndn6) if backside: xi = 2 * self._Eu.cE - xi y = xi * self._k0_a x = eta * self._k0_a if lat == 90: g, k = lon, self._k0 else: g, k = self._zetaScaled(sncndn6, ll=False) if backside: g = 180 - g if _lat: y, g = -y, -g if _lon: x, g = -x, -g r = EasNorExact4Tuple(x, y, g, k) r._iteration = self._iteration return r @property_RO def iteration(self): '''Get the most recent C{ExactTransverseMercator.forward} or C{ExactTransverseMercator.reverse} iteration number (C{int} or C{0} if not available/applicable). ''' return self._iteration @property_doc_(''' the central scale factor (C{float}).''') def k0(self): '''Get the central scale factor (C{float}), aka I{C{scale0}}. ''' return self._k0 # aka scale0 @k0.setter # PYCHOK setter! def k0(self, k0): '''Set the central scale factor (C{float}), aka I{C{scale0}}. @raise ETMError: Invalid B{C{k0}}. ''' self._k0 = Scalar_(k0, name=_k0_, Error=ETMError, low=_TOL_10, high=1.0) # if not self._k0 > 0: # raise Scalar_.Error_(Scalar_, k0, name=_k0_, Error=ETMError) self._k0_a = self._k0 * self._a @property_doc_(''' the central meridian (C{degrees180}).''') def lon0(self): '''Get the central meridian (C{degrees180}). ''' return self._lon0 @lon0.setter # PYCHOK setter! def lon0(self, lon0): '''Set the central meridian (C{degrees180}). @raise ValueError: Invalid B{C{lon0}}. ''' self._lon0 = _norm180(Lon(lon0, name=_lon0_)) def _reset(self, e, e2): '''(INTERNAL) Get elliptic functions and pre-compute some frequently used values. @arg e: Eccentricity (C{float}). @arg e2: Eccentricity squared (C{float}). @raise EllipticError: No convergence. ''' # assert e2 == e**2 self._e = e self._e_PI_2 = e * PI_2 self._e_PI_4 = e * PI_4 self._e_TAYTOL = e * _TAYTOL self._1_e_90 = (1 - e) * 90 self._1_e_PI_2 = (1 - e) * PI_2 self._1_e2_PI_2 = (1 - e * 2) * PI_2 self._mu = e2 self._mu_2_1 = (e2 + 2) * 0.5 self._Eu = Elliptic(self._mu) self._Eu_cE_1_4 = self._Eu.cE * 0.25 self._Eu_cK_cE = self._Eu.cK / self._Eu.cE self._Eu_cK_PI_2 = self._Eu.cK / PI_2 self._mv = 1 - e2 self._3_mv = 3.0 / self._mv self._3_mv_e = self._3_mv / e self._Ev = Elliptic(self._mv) self._Ev_cKE_3_4 = self._Ev.cKE * 0.75 self._Ev_cKE_5_4 = self._Ev.cKE * 1.25 self._iteration = 0 def reverse(self, x, y, lon0=None): '''Reverse projection, from Transverse Mercator to geographic. @arg x: Easting of point (C{meters}). @arg y: Northing of point (C{meters}). @kwarg lon0: Central meridian of the projection (C{degrees}). @return: L{LatLonExact4Tuple}C{(lat, lon, convergence, scale)} in C{degrees}, C{degrees180}, C{degrees} and C{scalar}. @see: C{void TMExact::Reverse(real lon0, real x, real y, real &lat, real &lon, real &gamma, real &k)} @raise EllipticError: No convergence. ''' # undoes the steps in .forward. xi = y / self._k0_a eta = x / self._k0_a backside = _lat = _lon = False if not self.extendp: # enforce the parity if y < 0: _lat, xi = True, -xi if x < 0: _lon, eta = True, -eta if xi > self._Eu.cE: xi = 2 * self._Eu.cE - xi backside = True # u,v = coordinates for the Thompson TM, Lee 54 if xi != 0 or eta != self._Ev.cKE: u, v = self._sigmaInv(xi, eta) else: u = self._iteration = 0 v = self._Ev.cK if v != 0 or u != self._Eu.cK: g, k, lat, lon = self._zetaScaled(self._sncndn6(u, v)) else: g, k, lat, lon = 0, self._k0, 90, 0 if backside: lon, g = (180 - lon), (180 - g) if _lat: lat, g = -lat, -g if _lon: lon, g = -lon, -g lon += self._lon0 if lon0 is None else _norm180(lon0) r = LatLonExact4Tuple(_norm180(lat), _norm180(lon), g, k) r._iteration = self._iteration return r def _scaled(self, tau, d2, snu, cnu, dnu, snv, cnv, dnv): '''(INTERNAL) C{scaled}. @note: Argument B{C{d2}} is C{_mu * cnu**2 + _mv * cnv**2} from C{._sigma3} or C{._zeta3}. @return: 2-Tuple C{(convergence, scale)}. @see: C{void TMExact::Scale(real tau, real /*lam*/, real snu, real cnu, real dnu, real snv, real cnv, real dnv, real &gamma, real &k)}. ''' mu, mv = self._mu, self._mv cnudnv = cnu * dnv # Lee 55.12 -- negated for our sign convention. g gives # the bearing (clockwise from true north) of grid north g = atan2(mv * cnv * snv * snu, cnudnv * dnu) # Lee 55.13 with nu given by Lee 9.1 -- in sqrt change # the numerator from # # (1 - snu^2 * dnv^2) to (_mv * snv^2 + cnu^2 * dnv^2) # # to maintain accuracy near phi = 90 and change the # denomintor from # (dnu^2 + dnv^2 - 1) to (_mu * cnu^2 + _mv * cnv^2) # # to maintain accuracy near phi = 0, lam = 90 * (1 - e). # Similarly rewrite sqrt term in 9.1 as # # _mv + _mu * c^2 instead of 1 - _mu * sin(phi)^2 q2 = (mv * snv**2 + cnudnv**2) / d2 # originally: sec2 = 1 + tau**2 # sec(phi)^2 # k = sqrt(mv + mu / sec2) * sqrt(sec2) * sqrt(q2) # = sqrt(mv + mv * tau**2 + mu) * sqrt(q2) k = sqrt(fsum_(mu, mv, mv * tau**2)) * sqrt(q2) return degrees(g), k * self._k0 def _sigma3(self, v, snu, cnu, dnu, snv, cnv, dnv): # PYCHOK unused '''(INTERNAL) C{sigma}. @return: 3-Tuple C{(xi, eta, d2)}. @see: C{void TMExact::sigma(real /*u*/, real snu, real cnu, real dnu, real v, real snv, real cnv, real dnv, real &xi, real &eta)}. @raise EllipticError: No convergence. ''' # Lee 55.4 writing # dnu^2 + dnv^2 - 1 = _mu * cnu^2 + _mv * cnv^2 d2 = self._mu * cnu**2 + self._mv * cnv**2 xi = self._Eu.fE(snu, cnu, dnu) - self._mu * snu * cnu * dnu / d2 eta = v - self._Ev.fE(snv, cnv, dnv) + self._mv * snv * cnv * dnv / d2 return xi, eta, d2 def _sigmaDwd(self, snu, cnu, dnu, snv, cnv, dnv): '''(INTERNAL) C{sigmaDwd}. @return: 2-Tuple C{(du, dv)}. @see: C{void TMExact::dwdsigma(real /*u*/, real snu, real cnu, real dnu, real /*v*/, real snv, real cnv, real dnv, real &du, real &dv)}. ''' snuv = snu * snv # Reciprocal of 55.9: dw / ds = dn(w)^2/_mv, # expanding complex dn(w) using A+S 16.21.4 d = self._mv * (cnv**2 + self._mu * snuv**2)**2 r = cnv * dnu * dnv i = -cnu * snuv * self._mu du = (r**2 - i**2) / d dv = 2 * r * i / d return du, dv def _sigmaInv(self, xi, eta): '''(INTERNAL) Invert C{sigma} using Newton's method. @return: 2-Tuple C{(u, v)}. @see: C{void TMExact::sigmainv(real xi, real eta, real &u, real &v)}. @raise EllipticError: No convergence. ''' u, v, trip = self._sigmaInv0(xi, eta) if trip: self._iteration = 0 else: U, V = Fsum(u), Fsum(v) # min iterations = 2, max = 7, mean = 3.9 for self._iteration in range(1, _TRIPS): # GEOGRAPHICLIB_PANIC sncndn6 = self._sncndn6(u, v) X, E, _ = self._sigma3(v, *sncndn6) dw, dv = self._sigmaDwd( *sncndn6) X = xi - X E -= eta u, du = U.fsum2_(X * dw, E * dv) v, dv = V.fsum2_(X * dv, -E * dw) if trip: break trip = hypot2(du, dv) < _TOL_10 else: t = unstr(self._sigmaInv.__name__, xi, eta) raise EllipticError(_no_convergence_, txt=t) return u, v def _sigmaInv0(self, xi, eta): '''(INTERNAL) Starting point for C{sigmaInv}. @return: 3-Tuple C{(u, v, trip)}. @see: C{bool TMExact::sigmainv0(real xi, real eta, real &u, real &v)}. ''' trip = False if eta > self._Ev_cKE_5_4 or xi < min(- self._Eu_cE_1_4, eta - self._Ev.cKE): # sigma as a simple pole at # w = w0 = Eu.K() + i * Ev.K() # and sigma is approximated by # sigma = (Eu.E() + i * Ev.KE()) + 1 / (w - w0) x = xi - self._Eu.cE y = eta - self._Ev.cKE d = hypot2(x, y) u = self._Eu.cK + x / d v = self._Ev.cK - y / d elif eta > self._Ev.cKE or (xi < self._Eu_cE_1_4 and eta > self._Ev_cKE_3_4): # At w = w0 = i * Ev.K(), we have # sigma = sigma0 = i * Ev.KE() # sigma' = sigma'' = 0 # # including the next term in the Taylor series gives: # sigma = sigma0 - _mv / 3 * (w - w0)^3 # # When inverting this, we map arg(w - w0) = [-pi/2, -pi/6] # to arg(sigma - sigma0) = [-pi/2, pi/2] # mapping arg = [-pi/2, -pi/6] to [-pi/2, pi/2] d = eta - self._Ev.cKE r = hypot(xi, d) # Error using this guess is about 0.068 * rad^(5/3) trip = r < _TAYTOL2 # Map the range [-90, 180] in sigma space to [-90, 0] in # w space. See discussion in zetainv0 on the cut for ang. r = cbrt(r * self._3_mv) a = atan2(d - xi, xi + d) / 3.0 - PI_4 s, c = sincos2(a) u = r * c v = r * s + self._Ev.cK else: # use w = sigma * Eu.K/Eu.E (correct in the limit _e -> 0) u = xi * self._Eu_cK_cE v = eta * self._Eu_cK_cE return u, v, trip def _sncndn6(self, u, v): '''(INTERNAL) Get 6-tuple C{(snu, cnu, dnu, snv, cnv, dnv)}. ''' # snu, cnu, dnu = self._Eu.sncndn(u) # snv, cnv, dnv = self._Ev.sncndn(v) return self._Eu.sncndn(u) + self._Ev.sncndn(v) def toStr(self, **kwds): '''Return a C{str} representation. @arg kwds: Optional, overriding keyword arguments. ''' d = dict(name=self.name) if self.name else {} d = dict(datum=self.datum.name, lon0=self.lon0, k0=self.k0, extendp=self.extendp, **d) return _COMMA_SPACE_.join(pairs(d, **kwds)) def _zeta3(self, snu, cnu, dnu, snv, cnv, dnv): '''(INTERNAL) C{zeta}. @return: 3-Tuple C{(taup, lambda, d2)}. @see: C{void TMExact::zeta(real /*u*/, real snu, real cnu, real dnu, real /*v*/, real snv, real cnv, real dnv, real &taup, real &lam)} ''' e = self._e # Lee 54.17 but write # atanh(snu * dnv) = asinh(snu * dnv / sqrt(cnu^2 + _mv * snu^2 * snv^2)) # atanh(_e * snu / dnv) = asinh(_e * snu / sqrt(_mu * cnu^2 + _mv * cnv^2)) d1 = cnu**2 + self._mv * (snu * snv)**2 d2 = self._mu * cnu**2 + self._mv * cnv**2 # Overflow value s.t. atan(overflow) = pi/2 t1 = t2 = copysign(_OVERFLOW, snu) if d1 > 0: t1 = snu * dnv / sqrt(d1) lam = 0 if d2 > 0: t2 = sinh(e * asinh(e * snu / sqrt(d2))) if d1 > 0: lam = atan2(dnu * snv , cnu * cnv) - \ atan2(cnu * snv * e, dnu * cnv) * e # psi = asinh(t1) - asinh(t2) # taup = sinh(psi) taup = t1 * hypot1(t2) - t2 * hypot1(t1) return taup, lam, d2 def _zetaDwd(self, snu, cnu, dnu, snv, cnv, dnv): '''(INTERNAL) C{zetaDwd}. @return: 2-Tuple C{(du, dv)}. @see: C{void TMExact::dwdzeta(real /*u*/, real snu, real cnu, real dnu, real /*v*/, real snv, real cnv, real dnv, real &du, real &dv)}. ''' cnu2 = cnu**2 * self._mu cnv2 = cnv**2 dnuv = dnu * dnv dnuv2 = dnuv**2 snuv = snu * snv snuv2 = snuv**2 * self._mu # Lee 54.21 but write # (1 - dnu^2 * snv^2) = (cnv^2 + _mu * snu^2 * snv^2) # (see A+S 16.21.4) d = self._mv * (cnv2 + snuv2)**2 du = cnu * dnuv * (cnv2 - snuv2) / d dv = -cnv * snuv * (cnu2 + dnuv2) / d return du, dv def _zetaInv(self, taup, lam): '''(INTERNAL) Invert C{zeta} using Newton's method. @return: 2-Tuple C{(u, v)}. @see: C{void TMExact::zetainv(real taup, real lam, real &u, real &v)}. @raise EllipticError: No convergence. ''' psi = asinh(taup) sca = 1.0 / hypot1(taup) u, v, trip = self._zetaInv0(psi, lam) if trip: self._iteration = 0 else: stol2 = _TOL_10 / max(psi**2, 1.0) U, V = Fsum(u), Fsum(v) # min iterations = 2, max = 6, mean = 4.0 for self._iteration in range(1, _TRIPS): # GEOGRAPHICLIB_PANIC sncndn6 = self._sncndn6(u, v) T, L, _ = self._zeta3( *sncndn6) dw, dv = self._zetaDwd(*sncndn6) T = (taup - T) * sca L -= lam u, du = U.fsum2_(T * dw, L * dv) v, dv = V.fsum2_(T * dv, -L * dw) if trip: break trip = hypot2(du, dv) < stol2 else: t = unstr(self._zetaInv.__name__, taup, lam) raise EllipticError(_no_convergence_, txt=t) return u, v def _zetaInv0(self, psi, lam): '''(INTERNAL) Starting point for C{zetaInv}. @return: 3-Tuple C{(u, v, trip)}. @see: C{bool TMExact::zetainv0(real psi, real lam, # radians real &u, real &v)}. ''' trip = False if psi < -self._e_PI_4 and lam > self._1_e2_PI_2 \ and psi < (lam - self._1_e_PI_2): # N.B. this branch is normally not taken because psi < 0 # is converted psi > 0 by Forward. # # There's a log singularity at w = w0 = Eu.K() + i * Ev.K(), # corresponding to the south pole, where we have, approximately # # psi = _e + i * pi/2 - _e * atanh(cos(i * (w - w0)/(1 + _mu/2))) # # Inverting this gives: h = sinh(1 - psi / self._e) a = (PI_2 - lam) / self._e s, c = sincos2(a) u = self._Eu.cK - asinh(s / hypot(c, h)) * self._mu_2_1 v = self._Ev.cK - atan2(c, h) * self._mu_2_1 elif psi < self._e_PI_2 and lam > self._1_e2_PI_2: # At w = w0 = i * Ev.K(), we have # # zeta = zeta0 = i * (1 - _e) * pi/2 # zeta' = zeta'' = 0 # # including the next term in the Taylor series gives: # # zeta = zeta0 - (_mv * _e) / 3 * (w - w0)^3 # # When inverting this, we map arg(w - w0) = [-90, 0] to # arg(zeta - zeta0) = [-90, 180] d = lam - self._1_e_PI_2 r = hypot(psi, d) # Error using this guess is about 0.21 * (rad/e)^(5/3) trip = r < self._e_TAYTOL # atan2(dlam-psi, psi+dlam) + 45d gives arg(zeta - zeta0) # in range [-135, 225). Subtracting 180 (since multiplier # is negative) makes range [-315, 45). Multiplying by 1/3 # (for cube root) gives range [-105, 15). In particular # the range [-90, 180] in zeta space maps to [-90, 0] in # w space as required. r = cbrt(r * self._3_mv_e) a = atan2(d - psi, psi + d) / 3.0 - PI_4 s, c = sincos2(a) u = r * c v = r * s + self._Ev.cK else: # Use spherical TM, Lee 12.6 -- writing C{atanh(sin(lam) / # cosh(psi)) = asinh(sin(lam) / hypot(cos(lam), sinh(psi)))}. # This takes care of the log singularity at C{zeta = Eu.K()}, # corresponding to the north pole. s, c = sincos2(lam) h, r = sinh(psi), self._Eu_cK_PI_2 # But scale to put 90, 0 on the right place u = r * atan2(h, c) v = r * asinh(s / hypot(c, h)) return u, v, trip def _zetaScaled(self, sncndn6, ll=True): '''(INTERNAL) Recompute (T, L) from (u, v) to improve accuracy of Scale. @arg sncndn6: 6-Tuple C{(snu, cnu, dnu, snv, cnv, dnv)}. @return: 2-Tuple C{(g, k)} if B{C{ll}} is C{False} else 4-tuple C{(g, k, lat, lon)}. ''' t, lam, d2 = self._zeta3( *sncndn6) tau = self._E.es_tauf(t) r = self._scaled(tau, d2, *sncndn6) if ll: r += degrees(atan(tau)), degrees(lam) return r