Coverage for pygeodesy/albers.py: 97%
408 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-04 12:01 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-04 12:01 -0400
2# -*- coding: utf-8 -*-
4u'''Albers Equal-Area projections.
6Classes L{AlbersEqualArea}, L{AlbersEqualArea2}, L{AlbersEqualArea4},
7L{AlbersEqualAreaCylindrical}, L{AlbersEqualAreaNorth}, L{AlbersEqualAreaSouth}
8and L{AlbersError}, transcoded from I{Charles Karney}'s C++ class U{AlbersEqualArea
9<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AlbersEqualArea.html>}.
11See also I{Albers Equal-Area Conic Projection} in U{John P. Snyder, "Map Projections
12-- A Working Manual", 1987<https://Pubs.USGS.gov/pp/1395/report.pdf>}, pp 98-106
13and the Albers Conical Equal-Area examples on pp 291-294.
14'''
15# make sure int/int division yields float quotient, see .basics
16from __future__ import division as _; del _ # PYCHOK semicolon
18from pygeodesy.basics import _isin, neg, neg_
19from pygeodesy.constants import EPS0, EPS02, _EPSqrt as _TOL, \
20 _0_0, _0_5, _1_0, _N_1_0, _2_0, \
21 _N_2_0, _4_0, _6_0, _90_0, _N_90_0
22from pygeodesy.datums import _ellipsoidal_datum, _WGS84
23from pygeodesy.errors import _ValueError, _xkwds
24from pygeodesy.fmath import hypot, hypot1, sqrt3
25from pygeodesy.fsums import Fsum, _Fsum1f_, fsum1f_
26from pygeodesy.interns import NN, _COMMASPACE_, _datum_, _gamma_, _k0_, \
27 _lat_, _lat1_, _lat2_, _lon_, _negative_, \
28 _scale_, _SPACE_, _x_, _y_
29from pygeodesy.karney import _diff182, _norm180, _signBit
30from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY
31from pygeodesy.named import _name2__, _NamedBase, _NamedTuple, _Pass
32from pygeodesy.props import deprecated_Property_RO, Property_RO, _update_all
33from pygeodesy.streprs import Fmt, strs, unstr
34from pygeodesy.units import Bearing, Float_, Lat, Lat_, Lon, Meter, Scalar_
35from pygeodesy.utily import atan1, atan1d, atan2, degrees360, sincos2, \
36 sincos2d, sincos2d_
38from math import atanh, degrees, fabs, radians, sqrt
40__all__ = _ALL_LAZY.albers
41__version__ = '25.04.14'
43_k1_ = 'k1'
44_NUMIT = 9 # XXX 4?
45_NUMIT0 = 41 # XXX 21?
46_TERMS = 31 # XXX 16?
47_TOL0 = sqrt3(_TOL)
50def _ct2(s, c):
51 '''(INTERNAL) Avoid singularities at poles.
52 '''
53 c = max(EPS0, c)
54 return c, (s / c)
57def _Ks(**name_k):
58 '''(INTERNAL) Scale C{B{k} >= EPS0}.
59 '''
60 return Scalar_(Error=AlbersError, low=EPS0, **name_k) # > 0
63def _Lat(*lat, **Error_name_lat):
64 '''(INTERNAL) Latitude C{-90 <= B{lat} <= 90}.
65 '''
66 kwds = _xkwds(Error_name_lat, Error=AlbersError)
67 return Lat_(*lat, **kwds)
70def _qZx(albs):
71 '''(INTERNAL) Set C{albs._qZ} and C{albs._qx}.
72 '''
73 E = albs._datum.ellipsoid # _AlbersBase
74 albs._qZ = qZ = _1_0 + E.e21 * _atanheE(_1_0, E)
75 albs._qx = qZ / (_2_0 * E.e21)
76 return qZ
79class AlbersError(_ValueError):
80 '''An L{AlbersEqualArea}, L{AlbersEqualArea2}, L{AlbersEqualArea4},
81 L{AlbersEqualAreaCylindrical}, L{AlbersEqualAreaNorth},
82 L{AlbersEqualAreaSouth} or L{Albers7Tuple} issue.
83 '''
84 pass
87class _AlbersBase(_NamedBase):
88 '''(INTERNAL) Base class for C{AlbersEqualArea...} projections.
90 @see: I{Karney}'s C++ class U{AlbersEqualArea<https://GeographicLib.SourceForge.io/
91 C++/doc/classGeographicLib_1_1AlbersEqualArea.html>}, method C{Init}.
92 '''
93 _datum = _WGS84
94 _k = NN # or _k0_ or _k1_
95 _k0 = _Ks(k0=_1_0)
96# _k0n0 = None # (INTERNAL) k0 * no
97 _k02 = _1_0 # (INTERNAL) k0**2
98# _k02n0 = None # (INTERNAL) k02 * n0
99# _lat0 = None # lat origin
100 _lat1 = None # let 1st parallel
101 _lat2 = None # lat 2nd parallel
102 _m0 = _0_0 # if polar else sqrt(m02)
103# _m02 = None # (INTERNAL) cached
104# _n0 = None # (INTERNAL) cached
105 _nrho0 = _0_0 # if polar else m0 * E.a
106 _polar = False
107 _qx = None # (INTERNAL) see _qZx
108 _qZ = None # (INTERNAL) see _qZx
109# _scxi0_ = None # (INTERNAL) sec(xi) / (qZ * E.a2)
110 _sign = +1
111# _sxi0 = None # (INTERNAL) sin(xi)
112# _txi0 = None # (INTERNAL) tan(xi)
114 def __init__(self, sa1, ca1, sa2, ca2, k, datum, **name):
115 '''(INTERNAL) New C{AlbersEqualArea...} instance.
116 '''
117 qZ = self._qZ
118 if not _isin(datum, None, self._datum):
119 self._datum = _ellipsoidal_datum(datum, **name)
120 qZ = _qZx(self)
121 elif qZ is None:
122 qZ = _qZx(_AlbersBase)
123 if name:
124 self.name = name
126 E = self.ellipsoid
127 c = min(ca1, ca2)
128 if _signBit(c):
129 raise AlbersError(clat1=ca1, clat2=ca2, txt=_negative_)
130 polar = c < EPS0 # == 0
132 # determine hemisphere of tangent latitude
133 if sa1 < 0: # and sa2 < 0:
134 self._sign = -1
135 # internally, tangent latitude positive
136 sa1, sa2 = neg_(sa1, sa2)
137 if sa1 > sa2: # make phi1 < phi2
138 sa1, sa2 = sa2, sa1
139 ca1, ca2 = ca2, ca1
140 if sa1 < 0: # or sa2 < 0:
141 raise AlbersError(slat1=sa1, slat2=sa2, txt=_negative_)
142 ca1, ta1 = _ct2(sa1, ca1)
143 ca2, ta2 = _ct2(sa2, ca2)
145 par1 = fabs(ta1 - ta2) < EPS02 # ta1 == ta2
146 ta0, C = (ta2, _1_0) if par1 or polar else \
147 self._ta0C2(ca1, sa1, ta1, ca2, sa2, ta2)
149 self._lat0 = _Lat(lat0=self._sign * atan1d(ta0))
150 self._m02 = m02 = _1_x21(E.f1 * ta0)
151 self._n0 = n0 = ta0 / hypot1(ta0)
152 if polar:
153 self._polar = True
154# self._nrho0 = self._m0 = _0_0
155 else: # m0 = nrho0 / E.a
156 self._m0 = t = sqrt(m02)
157 self._nrho0 = t * E.a
158 t = self._txi0 = self._txif(ta0)
159 h = hypot1(t)
160 s = self._sxi0 = t / h
161 if par1:
162 self._k0n0 = self._k02n0 = n0
163 else:
164 self._k0s(k * sqrt(C / (m02 + n0 * qZ * s)))
165 self._scxi0_ = h / (qZ * E.a2)
167 def _a_b_sxi3(self, *ca_sa_ta_scb_4s):
168 '''(INTERNAL) Sum of C{sm1} terms and C{sin(xi)}s for ._ta0C2.
169 '''
170 _1 = _1_0
171 a = b = s = _0_0
172 for ca, sa, ta, scb in ca_sa_ta_scb_4s:
173 cxi, sxi, _ = self._cstxif3(ta)
174 if sa > 0:
175 sa += _1
176 a += (cxi / ca)**2 * sa / (sxi + _1)
177 b += scb * ca**2 / sa
178 else:
179 sa = _1 - sa
180 a += (_1 - sxi) / sa
181 b += scb * sa
182 s += sxi
183 return a, b, s
185 def _azik(self, t, ta):
186 '''(INTERNAL) Compute the azimuthal scale C{_Ks(k=k)}.
187 '''
188 E = self.ellipsoid
189 return _Ks(k=hypot1(E.b_a * ta) * self._k0 * t / E.a)
191 def _cstxif3(self, ta):
192 '''(INTERNAL) Get 3-tuple C{(cos, sin, tan)} of M{xi(ta)}.
193 '''
194 t = self._txif(ta)
195 c = _1_0 / hypot1(t)
196 s = c * t
197 return c, s, t
199 @Property_RO
200 def datum(self):
201 '''Get the datum (L{Datum}).
202 '''
203 return self._datum
205 @Property_RO
206 def ellipsoid(self):
207 '''Get the datum's ellipsoid (L{Ellipsoid}).
208 '''
209 return self.datum.ellipsoid
211 @Property_RO
212 def equatoradius(self):
213 '''Get the C{ellipsoid}'s equatorial radius, semi-axis (C{meter}).
214 '''
215 return self.ellipsoid.a
217 a = equatoradius
219 @Property_RO
220 def flattening(self):
221 '''Get the C{ellipsoid}'s flattening (C{scalar}).
222 '''
223 return self.ellipsoid.f
225 f = flattening
227 def forward(self, lat, lon, lon0=0, **name):
228 '''Convert a geodetic location to east- and northing.
230 @arg lat: Latitude of the location (C{degrees}).
231 @arg lon: Longitude of the location (C{degrees}).
232 @kwarg lon0: Optional central meridian longitude (C{degrees}).
233 @kwarg name: Optional C{B{name}=NN} for the location (C{str}).
235 @return: An L{Albers7Tuple}C{(x, y, lat, lon, gamma, scale, datum)},
236 with C{lon} offset by B{C{lon0}} and reduced C{[-180,180]}.
238 @note: The origin latitude is returned by C{property lat0}. No
239 false easting or northing is added. The value of B{C{lat}}
240 should be in the range C{[-90..90] degrees}. The returned
241 values C{x} and C{y} will be large but finite for points
242 projecting to infinity, i.e. one or both of the poles.
243 '''
244 a = self.ellipsoid.a
245 s = self._sign
247 k0 = self._k0
248 n0 = self._n0
249 nrho0 = self._nrho0
250 txi0 = self._txi0
252 _, ta = _ct2(*sincos2d(s * _Lat(lat=lat)))
254 _, sxi, txi = self._cstxif3(ta)
255 dq = _Dsn(txi, txi0, sxi, self._sxi0) * \
256 (txi - txi0) * self._qZ
257 drho = a * dq / (sqrt(self._m02 - n0 * dq) + self._m0)
259 lon, _ = _diff182(lon0, lon)
260 x = radians(lon)
261 th = self._k02n0 * x
262 sth, cth = sincos2(th) # XXX sin, cos
263 if n0:
264 x = sth / n0
265 y = (_1_0 - cth) if cth < 0 else (sth**2 / (cth + _1_0))
266 y *= nrho0 / n0
267 else:
268 x *= self._k02
269 y = _0_0
270 t = nrho0 - n0 * drho
271 x = t * x / k0
272 y = s * (y + drho * cth) / k0
274 g = degrees360(s * th)
275 if t:
276 k0 = self._azik(t, ta)
277 return Albers7Tuple(x, y, lat, lon, g, k0, self.datum,
278 name=self._name__(name))
280 @Property_RO
281 def ispolar(self):
282 '''Is this projection polar (C{bool})?
283 '''
284 return self._polar
286 isPolar = ispolar # synonym
288 def _k0s(self, k0):
289 '''(INTERNAL) Set C{._k0}, C{._k02}, etc.
290 '''
291 self._k0 = k0 = _Ks(k0=k0)
292 self._k02 = k02 = k0**2
293 self._k0n0 = k0 * self._n0
294 self._k02n0 = k02 * self._n0
296 @Property_RO
297 def lat0(self):
298 '''Get the latitude of the projection origin (C{degrees}).
300 This is the latitude of minimum azimuthal scale and
301 equals the B{C{lat}} in the 1-parallel L{AlbersEqualArea}
302 and lies between B{C{lat1}} and B{C{lat2}} for the
303 2-parallel L{AlbersEqualArea2} and L{AlbersEqualArea4}
304 projections.
305 '''
306 return self._lat0
308 @Property_RO
309 def lat1(self):
310 '''Get the latitude of the first parallel (C{degrees}).
311 '''
312 return self._lat1
314 @Property_RO
315 def lat2(self):
316 '''Get the latitude of the second parallel (C{degrees}).
318 @note: The second and first parallel latitudes are the
319 same instance for 1-parallel C{AlbersEqualArea*}
320 projections.
321 '''
322 return self._lat2
324 @deprecated_Property_RO
325 def majoradius(self): # PYCHOK no cover
326 '''DEPRECATED, use property C{equatoradius}.'''
327 return self.equatoradius
329 def rescale0(self, lat, k=1): # PYCHOK no cover
330 '''Set the azimuthal scale for this projection.
332 @arg lat: Northern latitude (C{degrees}).
333 @arg k: Azimuthal scale at latitude B{C{lat}} (C{scalar}).
335 @raise AlbersError: Invalid B{C{lat}} or B{C{k}}.
337 @note: This allows a I{latitude of conformality} to be specified.
338 '''
339 k0 = _Ks(k=k) / self.forward(lat, _0_0).scale
340 if self._k0 != k0:
341 _update_all(self)
342 self._k0s(k0)
344 def reverse(self, x, y, lon0=0, LatLon=None, **name_LatLon_kwds):
345 '''Convert an east- and northing location to geodetic lat- and longitude.
347 @arg x: Easting of the location (C{meter}).
348 @arg y: Northing of the location (C{meter}).
349 @kwarg lon0: Optional central meridian longitude (C{degrees}).
350 @kwarg LatLon: Class to use (C{LatLon}) or C{None}.
351 @kwarg name_LatLon_kwds: Optional C{B{name}=NN} for the location
352 and optional, additional B{C{LatLon}} keyword
353 arguments, ignored if C{B{LatLon} is None}.
355 @return: The geodetic (C{LatLon}) or if C{B{LatLon} is None} an
356 L{Albers7Tuple}C{(x, y, lat, lon, gamma, scale, datum)}.
358 @note: The origin latitude is returned by C{property lat0}. No
359 false easting or northing is added. The returned value of
360 C{lon} is in the range C{[-180..180] degrees} and C{lat}
361 is in the range C{[-90..90] degrees}. If the given
362 B{C{x}} or B{C{y}} point is outside the valid projected
363 space the nearest pole is returned.
364 '''
365 k0 = self._k0
366 n0 = self._n0
367 k0n0 = self._k0n0
368 s = self._sign
369 txi = self._txi0
371 x = Meter(x=x)
372 nx = k0n0 * x
373 y = Meter(y=y)
374 y_ = s * y
375 ny = k0n0 * y_
376 t = nrho0 = self._nrho0
377 y1 = nrho0 - ny
379 den = hypot(nx, y1) + nrho0 # 0 implies origin with polar aspect
380 if den:
381 drho = _Fsum1f_(x * nx, y_ * nrho0 * _N_2_0, y_ * ny).fover(den / k0)
382 # dsxia = scxi0 * dsxi
383 t += drho * n0 # k0 below
384 d_ = (nrho0 + t) * drho * self._scxi0_ # / (qZ * E.a2)
385 t_ = txi - d_
386 d_ = (txi + t_) * d_ + _1_0
387 txi = t_ / (sqrt(d_) if d_ > EPS02 else EPS0)
389 ta = self._tanf(txi)
390 lat = atan1d(s * ta)
392 th = atan2(nx, y1)
393 lon = degrees((th / self._k02n0) if n0 else (x / (y1 * k0)))
394 if lon0:
395 lon += _norm180(lon0)
396 lon = _norm180(lon)
398 n, LatLon_kwds = _name2__(name_LatLon_kwds, _or_nameof=self)
399 if LatLon is None:
400 g = degrees360(s * th)
401 if den:
402 k0 = self._azik(t, ta)
403 r = Albers7Tuple(x, y, lat, lon, g, k0, self.datum, name=n)
404 else: # PYCHOK no cover
405 kwds = _xkwds(LatLon_kwds, datum=self.datum, name=n)
406 r = LatLon(lat, lon, **kwds)
407 return r
409 @Property_RO
410 def scale0(self):
411 '''Get the central scale for the projection (C{float}).
413 This is the azimuthal scale on the latitude of origin
414 of the projection, see C{property lat0}.
415 '''
416 return self._k0
418 def _ta0(self, s1_qZ, ta0, E):
419 '''(INTERNAL) Refine C{ta0} for C{._ta0C2}.
420 '''
421 e2 = E.e2
422 e21 = E.e21
423 e22 = E.e22 # == e2 / e21
424 tol = _tol(_TOL0, ta0)
425 _Ta02 = Fsum(ta0).fsum2f_
426 _1, _2 = _1_0, _2_0
427 _4, _6 = _4_0, _6_0
428 for self._iteration in range(1, _NUMIT0): # 4 trips
429 ta02 = ta0**2
430 sca02 = ta02 + _1
431 sca0 = sqrt(sca02)
432 sa0 = ta0 / sca0
433 sa01 = sa0 + _1
434 sa02 = sa0**2
435 # sa0m = 1 - sa0 = 1 / (sec(a0) * (tan(a0) + sec(a0)))
436 sa0m = _1 / (sca0 * (ta0 + sca0)) # scb0^2 * sa0
437 sa0m1 = sa0m / (_1 - e2 * sa0)
438 sa021 = _1 - e2 * sa02
440 g = (_1 + ta02 * e21) * sa0
441 dg = (_1 + ta02 * _2) * sca02 * e21 + e2
442 D = (_1 - (_1 + sa0 * _2 * sa01) * e2) * sa0m / (e21 * sa01) # dD/dsa0
443 dD = (_2 - (_6 + sa0 * _4) * sa02 * e2) / (e21 * sa01**2)
444 BA = (_atanh1(e2 * sa0m1**2) * e21 - e2 * sa0m) * sa0m1 \
445 - (_2 + (_1 + e2) * sa0) * sa0m**2 * e22 / sa021 # B + A
446 d = (_4 - (_1 + sa02) * e2 * _2) * e22 / (sa021**2 * sca02) # dAB
447 u = fsum1f_(s1_qZ * g, -D, g * BA)
448 du = fsum1f_(s1_qZ * dg, dD, dg * BA, g * d)
449 ta0, d = _Ta02(-u / du * (sca0 * sca02))
450 if fabs(d) < tol:
451 return ta0
452 raise AlbersError(Fmt.no_convergence(d, tol), txt=repr(self))
454 def _ta0C2(self, ca1, sa1, ta1, ca2, sa2, ta2):
455 '''(INTERNAL) Compute C{ta0} and C{C} for C{.__init__}.
456 '''
457 E = self.ellipsoid
458 f1, e2 = E.f1, E.e2
459 _1 = _1_0
461 tb1 = f1 * ta1
462 tb2 = f1 * ta2
463 dtb12 = f1 * (tb1 + tb2)
464 scb12 = _1 + tb1**2
465 scb22 = _1 + tb2**2
467 dsn_2 = _Dsn(ta2, ta1, sa2, sa1) * _0_5
468 sa12 = sa1 * sa2
470 esa1_2 = (_1 - e2 * sa1**2) \
471 * (_1 - e2 * sa2**2)
472 esa12 = _1 + e2 * sa12
474 axi, bxi, sxi = self._a_b_sxi3((ca1, sa1, ta1, scb12),
475 (ca2, sa2, ta2, scb22))
477 dsxi = ((esa12 / esa1_2) + _DatanheE(sa2, sa1, E)) * dsn_2 / self._qx
478 C = _Fsum1f_(sxi * dtb12 / dsxi, scb22, scb12).fover(scb22 * scb12 * _2_0)
480 S = _Fsum1f_(sa1, sa2, sa12)
481 axi *= (S * e2 + _1).fover(S + _1, raiser=False)
482 bxi *= _Fsum1f_( sa1, sa2, esa12).fover(esa1_2) * e2 + \
483 _D2atanheE(sa1, sa2, E) * E.e21
484 s1_qZ = (axi * self._qZ - bxi) * dsn_2 / dtb12
485 ta0 = self._ta0(s1_qZ, (ta1 + ta2) * _0_5, E)
486 return ta0, C
488 def _tanf(self, txi): # in .Ellipsoid.auxAuthalic
489 '''(INTERNAL) Function M{tan-phi from tan-xi}.
490 '''
491 e2 = self.ellipsoid.e2
492 ta = txi
493 tol = _tol(_TOL, ta)
494 _Ta2 = Fsum(ta).fsum2f_
495 for self._iteration in range(1, _NUMIT): # max 2, mean 1.99
496 # dtxi / dta = (scxi / sca)^3 * 2 * (1 - e^2)
497 # / (qZ * (1 - e^2 * sa^2)^2)
498 ta2 = ta**2
499 sca2 = _1_0 + ta2
500 txia = self._txif(ta)
501 s3 = sqrt3(sca2 / (txia**2 + _1_0)) # * _1_x21(txia)
502 s3 *= (e2 * ta2 / sca2 - _1_0)**2 * self._qx
503 ta, d = _Ta2((txi - txia) * s3)
504 if fabs(d) < tol:
505 return ta
506 raise AlbersError(Fmt.no_convergence(d, tol), txt=repr(self))
508 def toRepr(self, prec=6, **unused): # PYCHOK expected
509 '''Return a string representation of this projection.
511 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).
513 @return: This projection as C{"<classname>(lat1, lat2, ...)"}
514 (C{str}).
515 '''
516 t = self.toStr(prec=prec, sep=_COMMASPACE_)
517 return Fmt.PAREN(self.classname, t)
519 def toStr(self, prec=6, sep=_SPACE_, **unused): # PYCHOK expected
520 '''Return a string representation of this projection.
522 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).
523 @kwarg sep: Separator to join (C{str}).
525 @return: This projection as C{"lat1 lat2"} (C{str}).
526 '''
527 k = self._k
528 t = (self.lat1, self.lat2, self._k0) if k is _k1_ else (
529 (self.lat1, self._k0) if k is _k0_ else
530 (self.lat1,))
531 t = strs(t, prec=prec)
532 if k:
533 t = t[:-1] + (Fmt.EQUAL(k, t[-1]),)
534 if self.datum != _WGS84:
535 t += Fmt.EQUAL(datum=self.datum),
536 if self.name:
537 t += Fmt.EQUAL(name=repr(self.name)),
538 return t if sep is None else sep.join(t)
540 def _txif(self, ta): # in .Ellipsoid.auxAuthalic
541 '''(INTERNAL) Function M{tan-xi from tan-phi}.
542 '''
543 E = self.ellipsoid
544 ca2 = _1_x21(ta)
545 sa = sqrt(ca2) * fabs(ta) # enforce odd parity
546 sa1 = _1_0 + sa
548 es1 = sa * E.e2
549 es1m1 = sa1 * (_1_0 - es1)
550 es1p1 = sa1 / (_1_0 + es1)
551 es2m1 = _1_0 - sa * es1
552 es2m1a = es2m1 * E.e21 # e2m
553 s = ca2 / (es1p1 * es2m1a) + _atanheE(ca2 / es1m1, E)
554 s *= es1m1 / es2m1a + _atanheE( es1p1, E)
555 t = _Fsum1f_( sa / es2m1, _atanheE( sa, E)).fover(sqrt(s))
556 return neg(t) if ta < 0 else t
559class AlbersEqualArea(_AlbersBase):
560 '''An Albers equal-area (authalic) projection with a single standard parallel.
562 @see: L{AlbersEqualArea2} and L{AlbersEqualArea4}.
563 '''
564 _k = _k0_
566 def __init__(self, lat, k0=1, datum=_WGS84, **name):
567 '''New L{AlbersEqualArea} projection.
569 @arg lat: Standard parallel (C{degrees}).
570 @kwarg k0: Azimuthal scale on the standard parallel (C{scalar}).
571 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},
572 L{Ellipsoid2} or L{a_f2Tuple}).
573 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}).
575 @raise AlbersError: Invalid B{C{lat}}, B{C{k0}} or no convergence.
576 '''
577 self._lat1 = self._lat2 = lat = _Lat(lat1=lat)
578 args = tuple(sincos2d(lat)) * 2 + (_Ks(k0=k0), datum)
579 _AlbersBase.__init__(self, *args, **name)
582class AlbersEqualArea2(_AlbersBase):
583 '''An Albers equal-area (authalic) projection with two standard parallels.
585 @see: L{AlbersEqualArea} and L{AlbersEqualArea4}.
586 '''
587 _k = _k1_
589 def __init__(self, lat1, lat2, k1=1, datum=_WGS84, **name):
590 '''New L{AlbersEqualArea2} projection.
592 @arg lat1: First standard parallel (C{degrees}).
593 @arg lat2: Second standard parallel (C{degrees}).
594 @kwarg k1: Azimuthal scale on the standard parallels (C{scalar}).
595 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},
596 L{Ellipsoid2} or L{a_f2Tuple}).
597 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}).
599 @raise AlbersError: Invalid B{C{lat1}}m B{C{lat2}}, B{C{k1}}
600 or no convergence.
601 '''
602 self._lat1, self._lat2 = lats = _Lat(lat1=lat1), _Lat(lat2=lat2)
603 args = tuple(sincos2d_(*lats)) + (_Ks(k1=k1), datum)
604 _AlbersBase.__init__(self, *args, **name)
607class AlbersEqualArea4(_AlbersBase):
608 '''An Albers equal-area (authalic) projection specified by the C{sin}
609 and C{cos} of both standard parallels.
611 @see: L{AlbersEqualArea} and L{AlbersEqualArea2}.
612 '''
613 _k = _k1_
615 def __init__(self, slat1, clat1, slat2, clat2, k1=1, datum=_WGS84, **name):
616 '''New L{AlbersEqualArea4} projection.
618 @arg slat1: Sine of first standard parallel (C{scalar}).
619 @arg clat1: Cosine of first standard parallel (non-negative C{scalar}).
620 @arg slat2: Sine of second standard parallel (C{scalar}).
621 @arg clat2: Cosine of second standard parallel (non-negative C{scalar}).
622 @kwarg k1: Azimuthal scale on the standard parallels (C{scalar}).
623 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},
624 L{Ellipsoid2} or L{a_f2Tuple}).
625 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}).
627 @raise AlbersError: Negative B{C{clat1}} or B{C{clat2}}, B{C{slat1}}
628 and B{C{slat2}} have opposite signs (hemispheres),
629 invalid B{C{k1}} or no convergence.
630 '''
631 def _Lat_s_c3(n, s, c):
632 r = Float_(hypot(s, c), name=n, Error=AlbersError)
633 L = _Lat( atan1d(s, c), name=n)
634 return L, (s / r), (c / r)
636 self._lat1, sa1, ca1 = _Lat_s_c3(_lat1_, slat1, clat1)
637 self._lat2, sa2, ca2 = _Lat_s_c3(_lat2_, slat2, clat2)
638 _AlbersBase.__init__(self, sa1, ca1, sa2, ca2, _Ks(k1=k1), datum, **name)
641class AlbersEqualAreaCylindrical(_AlbersBase):
642 '''An L{AlbersEqualArea} projection at C{lat=0} and C{k0=1} degenerating
643 to the cylindrical-equal-area projection.
644 '''
645 _lat1 = _lat2 = _Lat(lat1=_0_0)
647 def __init__(self, lat=_0_0, datum=_WGS84, **name):
648 '''New L{AlbersEqualAreaCylindrical} projection.
650 @kwarg lat: Standard parallel (C{0 degrees} I{fixed}).
651 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},
652 L{Ellipsoid2} or L{a_f2Tuple}).
653 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}).
654 '''
655 _xlat(lat, _0_0, AlbersEqualAreaCylindrical)
656 _AlbersBase.__init__(self, _0_0, _1_0, _0_0, _1_0, 1, datum, **name)
659class AlbersEqualAreaNorth(_AlbersBase):
660 '''An azimuthal L{AlbersEqualArea} projection at C{lat=90} and C{k0=1}
661 degenerating to the L{azimuthal} L{LambertEqualArea} projection.
662 '''
663 _lat1 = _lat2 = _Lat(lat1=_90_0)
665 def __init__(self, lat=_90_0, datum=_WGS84, **name):
666 '''New L{AlbersEqualAreaNorth} projection.
668 @kwarg lat: Standard parallel (C{90 degrees} I{fixed}).
669 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},
670 L{Ellipsoid2} or L{a_f2Tuple}).
671 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}).
672 '''
673 _xlat(lat, _90_0, AlbersEqualAreaNorth)
674 _AlbersBase.__init__(self, _1_0, _0_0, _1_0, _0_0, 1, datum, **name)
677class AlbersEqualAreaSouth(_AlbersBase):
678 '''An azimuthal L{AlbersEqualArea} projection at C{lat=-90} and C{k0=1}
679 degenerating to the L{azimuthal} L{LambertEqualArea} projection.
680 '''
681 _lat1 = _lat2 = _Lat(lat1=_N_90_0)
683 def __init__(self, lat=_N_90_0, datum=_WGS84, **name):
684 '''New L{AlbersEqualAreaSouth} projection.
686 @kwarg lat: Standard parallel (C{-90 degrees} I{fixed}).
687 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},
688 L{Ellipsoid2} or L{a_f2Tuple}).
689 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}).
690 '''
691 _xlat(lat, _N_90_0, AlbersEqualAreaSouth)
692 _AlbersBase.__init__(self, _N_1_0, _0_0, _N_1_0, _0_0, 1, datum, **name)
695class Albers7Tuple(_NamedTuple):
696 '''7-Tuple C{(x, y, lat, lon, gamma, scale, datum)}, in C{meter},
697 C{meter}, C{degrees90}, C{degrees180}, C{degrees360}, C{scalar} and
698 C{Datum} where C{(x, y)} is the projected, C{(lat, lon)} the geodetic
699 location, C{gamma} the meridian convergence at point, the bearing of
700 the y-axis measured clockwise from true North and C{scale} is the
701 azimuthal scale of the projection at point. The radial scale is
702 the reciprocal C{1 / scale}.
703 '''
704 _Names_ = (_x_, _y_, _lat_, _lon_, _gamma_, _scale_, _datum_)
705 _Units_ = ( Meter, Meter, Lat, Lon, Bearing, _Pass, _Pass)
708def _atanh1(x):
709 '''(INTERNAL) Function M{atanh(sqrt(x)) / sqrt(x) - 1}.
710 '''
711 s = fabs(x)
712 if 0 < s < _0_5: # for typical ...
713 # x < E.e^2 == 2 * E.f use ...
714 # x / 3 + x^2 / 5 + x^3 / 7 + ...
715 y, k = x, 3
716 _S2 = Fsum(y / k).fsum2f_
717 for _ in range(_TERMS): # 9 terms
718 y *= x # x**n
719 k += 2 # 2*n + 1
720 s, d = _S2(y / k)
721 if not d:
722 break
723 elif s:
724 s = sqrt(s)
725 s = (atanh(s) if x > 0 else atan1(s)) / s - _1_0
726 return s
729def _atanheE(x, E): # see Ellipsoid._es_atanh, .AuxLat._atanhee
730 '''(INTERNAL) Function M{atanhee(x)}, defined as ...
731 atanh( E.e * x) / E.e if f > 0 # oblate
732 atan (sqrt(-E.e2) * x) / sqrt(-E.e2) if f < 0 # prolate
733 x if f = 0.
734 '''
735 e = E.e # == sqrt(E.e2abs)
736 if e and x:
737 if E.f > 0: # .isOblate
738 x = atanh(x * e) / e
739 elif E.f < 0: # .isProlate
740 x = atan1(x * e) / e
741 return x
744def _DatanheE(x, y, E): # see .rhumb.ekx._DeatanhE
745 '''(INTERNAL) Function M{Datanhee(x, y)}, defined as
746 M{atanhee((x - y) / (1 - E.e^2 * x * y)) / (x - y)}.
747 '''
748 e = _1_0 - E.e2 * x * y
749 if e:
750 d = x - y
751 e = (_atanheE(d / e, E) / d) if d else (_1_0 / e)
752 return e
755def _D2atanheE(x, y, E):
756 '''(INTERNAL) Function M{D2atanhee(x, y)}, defined as
757 M{(Datanhee(1, y) - Datanhee(1, x)) / (y - x)}.
758 '''
759 s, e2 = _0_0, E.e2
760 if e2:
761 if ((fabs(x) + fabs(y)) * e2) < _0_5:
762 e = z = _1_0
763 k = 1
764 T = Fsum() # Taylor expansion
765 _T = T.Fsumf_
766 _C = Fsum().Fsum_
767 _S2 = Fsum().fsum2_
768 for _ in range(_TERMS): # 15 terms
769 T *= y; P = _T(z); z *= x # PYCHOK ;
770 T *= y; Q = _T(z); z *= x # PYCHOK ;
771 e *= e2
772 k += 2
773 s, d = _S2(_C(P, Q) * e / k)
774 if not d:
775 break
776 else: # PYCHOK no cover
777 s = _1_0 - x
778 if s:
779 s = (_DatanheE(_1_0, y, E) - _DatanheE(x, y, E)) / s
780 return s
783def _Dsn(x, y, sx, sy):
784 '''(INTERNAL) Divided differences, defined as M{Df(x, y) = (f(x) - f(y)) / (x - y)}
785 with M{sn(x) = x / sqrt(1 + x^2)}: M{Dsn(x, y) = (x + y) / ((sn(x) + sn(y)) *
786 (1 + x^2) * (1 + y^2))}.
788 @see: U{W. M. Kahan and R. J. Fateman, "Sympbolic Computation of Divided
789 Differences"<https://People.EECS.Berkeley.EDU/~fateman/papers/divdiff.pdf>},
790 U{ACM SIGSAM Bulletin 33(2), 7-28 (1999)<https://DOI.org/10.1145/334714.334716>}
791 and U{AlbersEqualArea.hpp
792 <https://GeographicLib.SourceForge.io/C++/doc/AlbersEqualArea_8hpp_source.html>}.
793 '''
794 # sx = x / hypot1(x)
795 d, t = _1_0, (x * y)
796 if t > 0:
797 s = sx + sy
798 if s:
799 t = sx * sy / t
800 d = t**2 * (x + y) / s
801 elif x != y:
802 d = (sx - sy) / (x - y)
803 return d
806def _tol(tol, x):
807 '''(INTERNAL) Converge tolerance.
808 '''
809 return tol * max(_1_0, fabs(x))
812def _1_x21(x):
813 '''(INTERNAL) Return M{1 / (x**2 + 1)}.
814 '''
815 return _1_0 / (x**2 + _1_0)
818def _xlat(lat, f, where):
819 '''(INTERNAL) check fixed C{lat}.
820 '''
821 if lat is not f and _Lat(lat=lat) != f:
822 t = unstr(where, lat=lat)
823 raise AlbersError(t, txt_not_=f)
826__all__ += _ALL_DOCS(_AlbersBase)
828# **) MIT License
829#
830# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
831#
832# Permission is hereby granted, free of charge, to any person obtaining a
833# copy of this software and associated documentation files (the "Software"),
834# to deal in the Software without restriction, including without limitation
835# the rights to use, copy, modify, merge, publish, distribute, sublicense,
836# and/or sell copies of the Software, and to permit persons to whom the
837# Software is furnished to do so, subject to the following conditions:
838#
839# The above copyright notice and this permission notice shall be included
840# in all copies or substantial portions of the Software.
841#
842# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
843# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
844# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
845# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
846# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
847# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
848# OTHER DEALINGS IN THE SOFTWARE.