1

2# -*- coding: utf-8 -*-

3

4 u'''Albers Equal-Area projections.

5

6 Classes L{AlbersEqualArea}, L{AlbersEqualArea2}, L{AlbersEqualArea4},

7 L{AlbersEqualAreaCylindrical}, L{AlbersEqualAreaNorth}, L{AlbersEqualAreaSouth}

8 and L{AlbersError}, transcoded from I{Charles Karney}'s C++ class U{AlbersEqualArea

9<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AlbersEqualArea.html>}.

10

11 See 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

13 and the Albers Conical Equal-Area examples on pp 291-294.

14'''

15# make sure int/int division yields float quotient, see .basics

16 from __future__ import division as _; del _ # PYCHOK semicolon

17

18 from pygeodesy.basics import neg, neg_

19 from pygeodesy.constants import EPS0, EPS02, _EPSqrt as _TOL, isnear0, \

20 _0_0, _0_5, _1_0, _N_1_0, _2_0, _N_2_0, \

21 _4_0, _6_0, _90_0, _N_90_0

22 from pygeodesy.datums import _ellipsoidal_datum, _WGS84

23 from pygeodesy.errors import _ValueError, _xkwds

24 from pygeodesy.fmath import hypot, hypot1, sqrt3

25 from pygeodesy.fsums import Fsum, fsum1f_

26 from pygeodesy.interns import NN, _COMMASPACE_, _datum_, _gamma_, _k0_, \

27 _lat_, _lat1_, _lat2_, _lon_, _name_, _not_, \

28 _negative_, _scale_, _SPACE_, _x_, _y_

29 from pygeodesy.karney import _diff182, _norm180, _signBit

30 from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY

31 from pygeodesy.named import _NamedBase, _NamedTuple, _Pass

32 from pygeodesy.props import deprecated_Property_RO, Property_RO, _update_all

33 from pygeodesy.streprs import Fmt, strs, unstr

34 from pygeodesy.units import Bearing, Float_, Lat, Lat_, Lon, Meter, Scalar_

35 from pygeodesy.utily import atand, atan2d, degrees360, sincos2, \

36 sincos2d, sincos2d_

37

38 from math import atan, atan2, atanh, degrees, fabs, radians, sqrt

39

40__all__ = _ALL_LAZY.albers

41__version__ = '23.07.01'

42

43_k1_ = 'k1'

44_NUMIT = 8 # XXX 4?

45_NUMIT0 = 41 # XXX 21?

46_TERMS = 21 # XXX 16?

47_TOL0 = sqrt3(_TOL)

48

49

50 def _Ks(**name_k):

51 '''(INTERNAL) Scale C{B{k} >= EPS0}.

52 '''

53 return Scalar_(Error=AlbersError, low=EPS0, **name_k) # > 0

54

55

56 def _Lat(*lat, **Error_name_lat):

57 '''(INTERNAL) Latitude C{-90 <= B{lat} <= 90}.

58 '''

59 kwds = _xkwds(Error_name_lat, Error=AlbersError)

60 return Lat_(*lat, **kwds)

61

62

63 def _qZx(alb):

64 '''(INTERNAL) Set C{alb._qZ} and C{alb._qx}.

65 '''

66 E = alb._datum.ellipsoid

67 alb._qZ = qZ = _1_0 + E.e21 * _atanhee(_1_0, E)

68 alb._qx = qZ / (_2_0 * E.e21)

69 return qZ

70

71

72 class AlbersError(_ValueError):

73 '''An L{AlbersEqualArea}, L{AlbersEqualArea2}, L{AlbersEqualArea4},

74 L{AlbersEqualAreaCylindrical}, L{AlbersEqualAreaNorth},

75 L{AlbersEqualAreaSouth} or L{Albers7Tuple} issue.

76 '''

77 pass

78

79

80 class _AlbersBase(_NamedBase):

81 '''(INTERNAL) Base class for C{AlbersEqualArea...} projections.

82

83 @see: I{Karney}'s C++ class U{AlbersEqualArea<https://GeographicLib.SourceForge.io/

84 html/classGeographicLib_1_1AlbersEqualArea.html>}, method C{Init}.

85 '''

86 _datum = _WGS84

87 _k = NN # or _k0_ or _k1_

88 _k0 = _Ks(k0=_1_0)

89 _k0n0 = None # (INTERNAL) k0 * no

90 _k02 = _1_0 # (INTERNAL) k0**2

91 _k02n0 = None # (INTERNAL) k02 * n0

92 _lat0 = None # lat origin

93 _lat1 = None # let 1st parallel

94 _lat2 = None # lat 2nd parallel

95 _m0 = _0_0 # if polar else sqrt(m02)

96 _m02 = None # (INTERNAL) cached

97 _n0 = None # (INTERNAL) cached

98 _nrho0 = _0_0 # if polar else m0 * E.a

99 _polar = False

100 _qx = None # (INTERNAL) see _qZx

101 _qZ = None # (INTERNAL) see _qZx

102 _scxi0_ = None # (INTERNAL) sec(xi) / (qZ * E.a2)

103 _sign = +1

104 _sxi0 = None # (INTERNAL) sin(xi)

105 _txi0 = None # (INTERNAL) tan(xi)

106

107 def __init__(self, sa1, ca1, sa2, ca2, k, datum, name):

108 '''(INTERNAL) New C{AlbersEqualArea...} instance.

109 '''

110 qZ = self._qZ

111 if datum not in (None, self._datum):

112 self._datum = _ellipsoidal_datum(datum, name=name)

113 qZ = _qZx(self)

114 elif qZ is None:

115 qZ = _qZx(_AlbersBase)

116 if name:

117 self.name = name

118

119 E = self.ellipsoid

120 c = min(ca1, ca2)

121 if _signBit(c):

122 raise AlbersError(clat1=ca1, clat2=ca2, txt=_negative_)

123 polar = c < EPS0 # == 0

124

125 # determine hemisphere of tangent latitude

126 if sa1 < 0: # and sa2 < 0:

127 self._sign = -1

128 # internally, tangent latitude positive

129 sa1, sa2 = neg_(sa1, sa2)

130 if sa1 > sa2: # make phi1 < phi2

131 sa1, sa2 = sa2, sa1

132 ca1, ca2 = ca2, ca1

133 if sa1 < 0: # or sa2 < 0:

134 raise AlbersError(slat1=sa1, slat2=sa2, txt=_negative_)

135 # avoid singularities at poles

136 ca1, ca2 = max(EPS0, ca1), max(EPS0, ca2)

137 ta1, ta2 = (sa1 / ca1), (sa2 / ca2)

138

139 par1 = fabs(ta1 - ta2) < EPS02 # ta1 == ta2

140 if par1 or polar:

141 ta0, C = ta2, _1_0

142 else:

143 ta0, C = self._ta0C2(ca1, sa1, ta1, ca2, sa2, ta2)

144

145 self._lat0 = _Lat(lat0=self._sign * atand(ta0))

146 self._m02 = m02 = _1_x21(E.f1 * ta0)

147 self._n0 = n0 = ta0 / hypot1(ta0)

148 if polar:

149 self._polar = True

150# self._nrho0 = self._m0 = _0_0

151 else: # m0 = nrho0 / E.a

152 self._m0 = sqrt(m02)

153 self._nrho0 = self._m0 * E.a

154 t = self._txi0 = self._txif(ta0)

155 h = hypot1(t)

156 s = self._sxi0 = t / h

157 if par1:

158 self._k0n0 = self._k02n0 = n0

159 else:

160 self._k0s(k * sqrt(C / (m02 + n0 * qZ * s)))

161 self._scxi0_ = h / (qZ * E.a2)

162

163 def _a_b_sxi3(self, *ca_sa_ta_scb_4s):

164 '''(INTERNAL) Sum of C{sm1} terms and C{sin(xi)}s for ._s1_qZ_C2.

165 '''

166 _1 = _1_0

167 a = b = s = _0_0

168 for ca, sa, ta, scb in ca_sa_ta_scb_4s:

169 cxi, sxi, _ = self._cstxif3(ta)

170 if sa > 0:

171 sa += _1

172 a += (cxi / ca)**2 * sa / (sxi + _1)

173 b += scb * ca**2 / sa

174 else:

175 sa = _1 - sa

176 a += (_1 - sxi) / sa

177 b += scb * sa

178 s += sxi

179 return a, b, s

180

181 def _azik(self, t, ta):

182 '''(INTERNAL) Compute the azimuthal scale C{_Ks(k=k)}.

183 '''

184 E = self.ellipsoid

185 t *= self._k0

186 return _Ks(k=t * hypot1(E.b_a * ta) / E.a)

187

188 def _cstxif3(self, ta):

189 '''(INTERNAL) Get 3-tuple C{(cos, sin, tan)} of M{xi(ta)}.

190 '''

191 t = self._txif(ta)

192 c = _1_0 / hypot1(t)

193 s = c * t

194 return c, s, t

195

196 @Property_RO

197 def datum(self):

198 '''Get the datum (L{Datum}).

199 '''

200 return self._datum

201

202 @Property_RO

203 def ellipsoid(self):

204 '''Get the datum's ellipsoid (L{Ellipsoid}).

205 '''

206 return self.datum.ellipsoid

207

208 @Property_RO

209 def equatoradius(self):

210 '''Get the geodesic's equatorial radius, semi-axis (C{meter}).

211 '''

212 return self.ellipsoid.a

213

214 @Property_RO

215 def flattening(self):

216 '''Get the geodesic's flattening (C{float}).

217 '''

218 return self.ellipsoid.f

219

220 def forward(self, lat, lon, lon0=0, name=NN):

221 '''Convert a geodetic location to east- and northing.

222

223 @arg lat: Latitude of the location (C{degrees}).

224 @arg lon: Longitude of the location (C{degrees}).

225 @kwarg lon0: Optional central meridian longitude (C{degrees}).

226 @kwarg name: Optional name for the location (C{str}).

227

228 @return: An L{Albers7Tuple}C{(x, y, lat, lon, gamma, scale, datum)}.

229

230 @note: The origin latitude is returned by C{property lat0}. No

231 false easting or northing is added. The value of B{C{lat}}

232 should be in the range C{[-90..90] degrees}. The returned

233 values C{x} and C{y} will be large but finite for points

234 projecting to infinity, i.e. one or both of the poles.

235 '''

236 a = self.ellipsoid.a

237 s = self._sign

238

239 k0 = self._k0

240 n0 = self._n0

241 nrho0 = self._nrho0

242 txi0 = self._txi0

243

244 sa, ca = sincos2d(s * _Lat(lat=lat))

245 ta = sa / max(EPS0, ca)

246

247 _, sxi, txi = self._cstxif3(ta)

248 dq = _Dsn(txi, txi0, sxi, self._sxi0) * \

249 (txi - txi0) * self._qZ

250 drho = a * dq / (sqrt(self._m02 - n0 * dq) + self._m0)

251

252 lon, _ = _diff182(lon0, lon)

253 x = radians(lon)

254

255 th = self._k02n0 * x

256 sth, cth = sincos2(th) # XXX sin, cos

257 if n0:

258 x = sth / n0

259 y = (_1_0 - cth) if cth < 0 else (sth**2 / (cth + _1_0))

260 y *= nrho0 / n0

261 else:

262 x *= self._k02

263 y = _0_0

264 t = nrho0 - n0 * drho

265 x = t * x / k0

266 y = s * (y + drho * cth) / k0

267

268 g = degrees360(s * th)

269 if t:

270 k0 = self._azik(t, ta)

271 return Albers7Tuple(x, y, lat, lon, g, k0, self.datum,

272 name=name or self.name)

273

274 @Property_RO

275 def ispolar(self):

276 '''Is this projection polar (C{bool})?

277 '''

278 return self._polar

279

280 isPolar = ispolar # synonym

281

282 def _k0s(self, k0):

283 '''(INTERNAL) Set C{._k0}, C{._k02}, etc.

284 '''

285 self._k0 = k = _Ks(k0=k0)

286 self._k02 = k2 = k**2

287 self._k0n0 = k * self._n0

288 self._k02n0 = k2 * self._n0

289

290 @Property_RO

291 def lat0(self):

292 '''Get the latitude of the projection origin (C{degrees}).

293

294 This is the latitude of minimum azimuthal scale and

295 equals the B{C{lat}} in the 1-parallel L{AlbersEqualArea}

296 and lies between B{C{lat1}} and B{C{lat2}} for the

297 2-parallel L{AlbersEqualArea2} and L{AlbersEqualArea4}

298 projections.

299 '''

300 return self._lat0

301

302 @Property_RO

303 def lat1(self):

304 '''Get the latitude of the first parallel (C{degrees}).

305 '''

306 return self._lat1

307

308 @Property_RO

309 def lat2(self):

310 '''Get the latitude of the second parallel (C{degrees}).

311

312 @note: The second and first parallel latitudes are the

313 same instance for 1-parallel C{AlbersEqualArea*}

314 projections.

315 '''

316 return self._lat2

317

318 @deprecated_Property_RO

319 def majoradius(self): # PYCHOK no cover

320 '''DEPRECATED, use property C{equatoradius}.'''

321 return self.equatoradius

322

323 def rescale0(self, lat, k=1): # PYCHOK no cover

324 '''Set the azimuthal scale for this projection.

325

326 @arg lat: Northern latitude (C{degrees}).

327 @arg k: Azimuthal scale at latitude B{C{lat}} (C{scalar}).

328

329 @raise AlbersError: Invalid B{C{lat}} or B{C{k}}.

330

331 @note: This allows a I{latitude of conformality} to be specified.

332 '''

333 k0 = _Ks(k=k) / self.forward(lat, _0_0).scale

334 if self._k0 != k0:

335 _update_all(self)

336 self._k0s(k0)

337

338 def reverse(self, x, y, lon0=0, name=NN, LatLon=None, **LatLon_kwds):

339 '''Convert an east- and northing location to geodetic lat- and longitude.

340

341 @arg x: Easting of the location (C{meter}).

342 @arg y: Northing of the location (C{meter}).

343 @kwarg lon0: Optional central meridian longitude (C{degrees}).

344 @kwarg name: Optional name for the location (C{str}).

345 @kwarg LatLon: Class to use (C{LatLon}) or C{None}.

346 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword

347 arguments, ignored if C{B{LatLon} is None}.

348

349 @return: The geodetic (C{LatLon}) or if B{C{LatLon}} is C{None} an

350 L{Albers7Tuple}C{(x, y, lat, lon, gamma, scale, datum)}.

351

352 @note: The origin latitude is returned by C{property lat0}. No

353 false easting or northing is added. The returned value of

354 C{lon} is in the range C{[-180..180] degrees} and C{lat}

355 is in the range C{[-90..90] degrees}. If the given

356 B{C{x}} or B{C{y}} point is outside the valid projected

357 space the nearest pole is returned.

358 '''

359 k0 = self._k0

360 n0 = self._n0

361 k0n0 = self._k0n0

362 s = self._sign

363 txi = self._txi0

364

365 x = Meter(x=x)

366 nx = k0n0 * x

367 y = Meter(y=y)

368 y_ = s * y

369 ny = k0n0 * y_

370 t = nrho0 = self._nrho0

371 y1 = nrho0 - ny

372

373 den = drho = hypot(nx, y1) + nrho0 # 0 implies origin with polar aspect

374 if den:

375 drho = fsum1f_(x * nx, y_ * nrho0 * _N_2_0, y_ * ny) * k0 / den

376 # dsxia = scxi0 * dsxi

377 t += drho * n0

378 d_ = (nrho0 + t) * drho * self._scxi0_ # / (qZ * E.a2)

379 d_2 = (txi * _2_0 - d_) * d_ + _1_0

380 txi = (txi - d_) / (sqrt(d_2) if d_2 > EPS02 else EPS0)

381

382 ta = self._tanf(txi)

383 lat = atand(s * ta)

384

385 th = atan2(nx, y1)

386 lon = degrees((th / self._k02n0) if n0 else (x / (y1 * k0)))

387 if lon0:

388 lon += _norm180(lon0)

389 lon = _norm180(lon)

390

391 n = name or self.name

392 if LatLon is None:

393 g = degrees360(s * th)

394 if den:

395 k0 = self._azik(t, ta)

396 r = Albers7Tuple(x, y, lat, lon, g, k0, self.datum, name=n)

397 else: # PYCHOK no cover

398 kwds = _xkwds(LatLon_kwds, datum=self.datum, name=n)

399 r = LatLon(lat, lon, **kwds)

400 return r

401

402 @Property_RO

403 def scale0(self):

404 '''Get the central scale for the projection (C{float}).

405

406 This is the azimuthal scale on the latitude of origin

407 of the projection, see C{property lat0}.

408 '''

409 return self._k0

410

411 def _ta0(self, s1_qZ, ta0, E):

412 '''(INTERNAL) Refine C{ta0} for C{._ta0C2}.

413 '''

414 e2 = E.e2

415 e21 = E.e21

416 e22 = E.e22 # == e2 / e21

417 tol = _tol(_TOL0, ta0)

418 _Ta02 = Fsum(ta0).fsum2_

419 _fabs = fabs

420 _fsum1f_ = fsum1f_

421 _sqrt = sqrt

422 _1, _2 = _1_0, _2_0

423 _4, _6 = _4_0, _6_0

424 for self._iteration in range(1, _NUMIT0): # 4 trips

425 ta02 = ta0**2

426 sca02 = ta02 + _1

427 sca0 = _sqrt(sca02)

428 sa0 = ta0 / sca0

429 sa01 = sa0 + _1

430 sa02 = sa0**2

431 # sa0m = 1 - sa0 = 1 / (sec(a0) * (tan(a0) + sec(a0)))

432 sa0m = _1 / (sca0 * (ta0 + sca0)) # scb0^2 * sa0

433 sa0m1 = sa0m / (_1 - e2 * sa0)

434 sa021 = _1 - e2 * sa02

435

436 g = (_1 + ta02 * e21) * sa0

437 dg = (_1 + ta02 * _2) * sca02 * e21 + e2

438 D = (_1 - (_1 + sa0 * _2 * sa01) * e2) * sa0m / (e21 * sa01) # dD/dsa0

439 dD = (_2 - (_6 + sa0 * _4) * sa02 * e2) / (e21 * sa01**2)

440 BA = (_atanhs1(e2 * sa0m1**2) * e21 - e2 * sa0m) * sa0m1 \

441 - (_2 + (_1 + e2) * sa0) * sa0m**2 * e22 / sa021 # B + A

442 d = (_4 - (_1 + sa02) * e2 * _2) * e22 / (sa021**2 * sca02) # dAB

443 u = _fsum1f_(s1_qZ * g, -D, g * BA)

444 du = _fsum1f_(s1_qZ * dg, dD, dg * BA, g * d)

445 ta0, d = _Ta02(-u / du * (sca0 * sca02))

446 if _fabs(d) < tol:

447 return ta0

448 raise AlbersError(Fmt.no_convergence(d, tol), txt=repr(self))

449

450 def _ta0C2(self, ca1, sa1, ta1, ca2, sa2, ta2):

451 '''(INTERNAL) Compute C{ta0} and C{C} for C{.__init__}.

452 '''

453 _1 = _1_0

454 _fsum1f_ = fsum1f_

455 E = self.ellipsoid

456 f1, e2 = E.f1, E.e2

457

458 tb1 = f1 * ta1

459 tb2 = f1 * ta2

460 dtb12 = f1 * (tb1 + tb2)

461 scb12 = _1 + tb1**2

462 scb22 = _1 + tb2**2

463

464 dsn_2 = _Dsn(ta2, ta1, sa2, sa1) * _0_5

465 sa12 = sa1 * sa2

466

467 esa1_2 = (_1 - e2 * sa1**2) \

468 * (_1 - e2 * sa2**2)

469 esa12 = _1 + e2 * sa12

470

471 axi, bxi, sxi = self._a_b_sxi3((ca1, sa1, ta1, scb12),

472 (ca2, sa2, ta2, scb22))

473

474 dsxi = (esa12 / esa1_2 + _Datanhee(sa2, sa1, E)) * dsn_2 / self._qx

475 C = _fsum1f_(sxi * dtb12 / dsxi, scb22, scb12) / (scb22 * scb12 * _2_0)

476

477 sa12 = _fsum1f_(sa1, sa2, sa12)

478 axi *= (sa12 * e2 + _1) / (sa12 + _1)

479 bxi *= _fsum1f_(sa1, sa2, esa12) * e2 / esa1_2 + E.e21 * _D2atanhee(sa1, sa2, E)

480 s1_qZ = (axi * self._qZ - bxi) * dsn_2 / dtb12

481 ta0 = self._ta0(s1_qZ, (ta1 + ta2) * _0_5, E)

482 return ta0, C

483

484 def _tanf(self, txi): # in .Ellipsoid.auxAuthalic

485 '''(INTERNAL) Function M{tan-phi from tan-xi}.

486 '''

487 tol = _tol(_TOL, txi)

488 e2 = self.ellipsoid.e2

489 qx = self._qx

490

491 ta = txi

492 _Ta2 = Fsum(ta).fsum2_

493 _fabs = fabs

494 _sqrt3 = sqrt3

495 _txif = self._txif

496 _1 = _1_0

497 for self._iteration in range(1, _NUMIT): # max 2, mean 1.99

498 # dtxi / dta = (scxi / sca)^3 * 2 * (1 - e^2)

499 # / (qZ * (1 - e^2 * sa^2)^2)

500 ta2 = ta**2

501 sca2 = _1 + ta2

502 txia = _txif(ta)

503 s3qx = _sqrt3(sca2 / (txia**2 + _1)) * qx # * _1_x21(txia)

504 eta2 = (_1 - e2 * ta2 / sca2)**2

505 ta, d = _Ta2((txi - txia) * s3qx * eta2)

506 if _fabs(d) < tol:

507 return ta

508 raise AlbersError(Fmt.no_convergence(d, tol), txt=repr(self))

509

510 def toRepr(self, prec=6, **unused): # PYCHOK expected

511 '''Return a string representation of this projection.

512

513 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).

514

515 @return: This projection as C{"<classname>(lat1, lat2, ...)"}

516 (C{str}).

517 '''

518 t = self.toStr(prec=prec, sep=_COMMASPACE_)

519 return Fmt.PAREN(self.classname, t)

520

521 def toStr(self, prec=6, sep=_SPACE_, **unused): # PYCHOK expected

522 '''Return a string representation of this projection.

523

524 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).

525 @kwarg sep: Separator to join (C{str}).

526

527 @return: This projection as C{"lat1 lat2"} (C{str}).

528 '''

529 k = self._k

530 t = (self.lat1, self.lat2, self._k0) if k is _k1_ else (

531 (self.lat1, self._k0) if k is _k0_ else

532 (self.lat1,))

533 t = strs(t, prec=prec)

534 if k:

535 t = t[:-1] + (Fmt.EQUAL(k, t[-1]),)

536 if self.datum != _WGS84:

537 t += (Fmt.EQUAL(_datum_, self.datum),)

538 if self.name:

539 t += (Fmt.EQUAL(_name_, repr(self.name)),)

540 return t if sep is None else sep.join(t)

541

542 def _txif(self, ta): # in .Ellipsoid.auxAuthalic

543 '''(INTERNAL) Function M{tan-xi from tan-phi}.

544 '''

545 E = self.ellipsoid

546

547 ca2 = _1_x21(ta)

548 sa = sqrt(ca2) * fabs(ta) # enforce odd parity

549 sa1 = _1_0 + sa

550

551 es1 = sa * E.e2

552 es1m1 = sa1 * (_1_0 - es1)

553 es1p1 = sa1 / (_1_0 + es1)

554 es2m1 = _1_0 - sa * es1

555 es2m1a = es2m1 * E.e21 # e2m

556 s = sqrt((ca2 / (es1p1 * es2m1a) + _atanhee(ca2 / es1m1, E))

557 * (es1m1 / es2m1a + _atanhee(es1p1, E)))

558 t = (sa / es2m1 + _atanhee(sa, E)) / s

559 return neg(t) if ta < 0 else t

560

561

562 class AlbersEqualArea(_AlbersBase):

563 '''An Albers equal-area (authalic) projection with a single standard parallel.

564

565 @see: L{AlbersEqualArea2} and L{AlbersEqualArea4}.

566 '''

567 _k = _k0_

568

569 def __init__(self, lat, k0=1, datum=_WGS84, name=NN):

570 '''New L{AlbersEqualArea} projection.

571

572 @arg lat: Standard parallel (C{degrees}).

573 @kwarg k0: Azimuthal scale on the standard parallel (C{scalar}).

574 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},

575 L{Ellipsoid2} or L{a_f2Tuple}).

576 @kwarg name: Optional name for the projection (C{str}).

577

578 @raise AlbersError: Invalid B{C{lat}}, B{C{k0}} or no convergence.

579 '''

580 self._lat1 = self._lat2 = lat = _Lat(lat1=lat)

581 args = tuple(sincos2d(lat)) * 2 + (_Ks(k0=k0), datum, name)

582 _AlbersBase.__init__(self, *args)

583

584

585 class AlbersEqualArea2(_AlbersBase):

586 '''An Albers equal-area (authalic) projection with two standard parallels.

587

588 @see: L{AlbersEqualArea} and L{AlbersEqualArea4}.

589 '''

590 _k = _k1_

591

592 def __init__(self, lat1, lat2, k1=1, datum=_WGS84, name=NN):

593 '''New L{AlbersEqualArea2} projection.

594

595 @arg lat1: First standard parallel (C{degrees}).

596 @arg lat2: Second standard parallel (C{degrees}).

597 @kwarg k1: Azimuthal scale on the standard parallels (C{scalar}).

598 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},

599 L{Ellipsoid2} or L{a_f2Tuple}).

600 @kwarg name: Optional name for the projection (C{str}).

601

602 @raise AlbersError: Invalid B{C{lat1}}m B{C{lat2}}, B{C{k1}}

603 or no convergence.

604 '''

605 self._lat1, self._lat2 = lats = _Lat(lat1=lat1), _Lat(lat2=lat2)

606 args = tuple(sincos2d_(*lats)) + (_Ks(k1=k1), datum, name)

607 _AlbersBase.__init__(self, *args)

608

609

610 class AlbersEqualArea4(_AlbersBase):

611 '''An Albers equal-area (authalic) projection specified by the C{sin}

612 and C{cos} of both standard parallels.

613

614 @see: L{AlbersEqualArea} and L{AlbersEqualArea2}.

615 '''

616 _k = _k1_

617

618 def __init__(self, slat1, clat1, slat2, clat2, k1=1, datum=_WGS84, name=NN):

619 '''New L{AlbersEqualArea4} projection.

620

621 @arg slat1: Sine of first standard parallel (C{scalar}).

622 @arg clat1: Cosine of first standard parallel (non-negative C{scalar}).

623 @arg slat2: Sine of second standard parallel (C{scalar}).

624 @arg clat2: Cosine of second standard parallel (non-negative C{scalar}).

625 @kwarg k1: Azimuthal scale on the standard parallels (C{scalar}).

626 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},

627 L{Ellipsoid2} or L{a_f2Tuple}).

628 @kwarg name: Optional name for the projection (C{str}).

629

630 @raise AlbersError: Negative B{C{clat1}} or B{C{clat2}}, B{C{slat1}}

631 and B{C{slat2}} have opposite signs (hemispheres),

632 invalid B{C{k1}} or no convergence.

633 '''

634 def _Lat_s_c3(name, s, c):

635 r = Float_(hypot(s, c), name=name, Error=AlbersError)

636 L = _Lat(atan2d(s, c), name=name)

637 return L, (s / r), (c / r)

638

639 self._lat1, sa1, ca1 = _Lat_s_c3(_lat1_, slat1, clat1)

640 self._lat2, sa2, ca2 = _Lat_s_c3(_lat2_, slat2, clat2)

641 _AlbersBase.__init__(self, sa1, ca1, sa2, ca2, _Ks(k1=k1), datum, name)

642

643

644 class AlbersEqualAreaCylindrical(_AlbersBase):

645 '''An L{AlbersEqualArea} projection at C{lat=0} and C{k0=1} degenerating

646 to the cylindrical-equal-area projection.

647 '''

648 _lat1 = _lat2 = _Lat(lat1=_0_0)

649

650 def __init__(self, lat=_0_0, datum=_WGS84, name=NN):

651 '''New L{AlbersEqualAreaCylindrical} projection.

652

653 @kwarg lat: Standard parallel (C{0 degrees} I{fixed}).

654 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},

655 L{Ellipsoid2} or L{a_f2Tuple}).

656 @kwarg name: Optional name for the projection (C{str}).

657 '''

658 _xlat(lat, _0_0, AlbersEqualAreaCylindrical)

659 _AlbersBase.__init__(self, _0_0, _1_0, _0_0, _1_0, 1, datum, name)

660

661

662 class AlbersEqualAreaNorth(_AlbersBase):

663 '''An azimuthal L{AlbersEqualArea} projection at C{lat=90} and C{k0=1}

664 degenerating to the L{azimuthal} L{LambertEqualArea} projection.

665 '''

666 _lat1 = _lat2 = _Lat(lat1=_90_0)

667

668 def __init__(self, lat=_90_0, datum=_WGS84, name=NN):

669 '''New L{AlbersEqualAreaNorth} projection.

670

671 @kwarg lat: Standard parallel (C{90 degrees} I{fixed}).

672 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},

673 L{Ellipsoid2} or L{a_f2Tuple}).

674 @kwarg name: Optional name for the projection (C{str}).

675 '''

676 _xlat(lat, _90_0, AlbersEqualAreaNorth)

677 _AlbersBase.__init__(self, _1_0, _0_0, _1_0, _0_0, 1, datum, name)

678

679

680 class AlbersEqualAreaSouth(_AlbersBase):

681 '''An azimuthal L{AlbersEqualArea} projection at C{lat=-90} and C{k0=1}

682 degenerating to the L{azimuthal} L{LambertEqualArea} projection.

683 '''

684 _lat1 = _lat2 = _Lat(lat1=_N_90_0)

685

686 def __init__(self, lat=_N_90_0, datum=_WGS84, name=NN):

687 '''New L{AlbersEqualAreaSouth} projection.

688

689 @kwarg lat: Standard parallel (C{-90 degrees} I{fixed}).

690 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},

691 L{Ellipsoid2} or L{a_f2Tuple}).

692 @kwarg name: Optional name for the projection (C{str}).

693 '''

694 _xlat(lat, _N_90_0, AlbersEqualAreaSouth)

695 _AlbersBase.__init__(self, _N_1_0, _0_0, _N_1_0, _0_0, 1, datum, name)

696

697

698 class Albers7Tuple(_NamedTuple):

699 '''7-Tuple C{(x, y, lat, lon, gamma, scale, datum)}, in C{meter},

700 C{meter}, C{degrees90}, C{degrees180}, C{degrees360}, C{scalar} and

701 C{Datum} where C{(x, y)} is the projected, C{(lat, lon)} the geodetic

702 location, C{gamma} the meridian convergence at point, the bearing of

703 the y-axis measured clockwise from true North and C{scale} is the

704 azimuthal scale of the projection at point. The radial scale is

705 the reciprocal C{1 / scale}.

706 '''

707 _Names_ = (_x_, _y_, _lat_, _lon_, _gamma_, _scale_, _datum_)

708 _Units_ = ( Meter, Meter, Lat, Lon, Bearing, _Pass, _Pass)

709

710

711 def _atanhee(x, E): # see Ellipsoid._es_atanh, .AuxLat._atanhee

712 '''(INTERNAL) Function M{atanhee(x)}, defined as ...

713 atanh( E.e * x) / E.e if f > 0 # oblate

714 atan (sqrt(-E.e2) * x) / sqrt(-E.e2) if f < 0 # prolate

715 x if f = 0.

716 '''

717 e = E.e

718 if E.f > 0: # .isOblate

719 x = atanh( x * e) / e

720 elif E.f < 0: # .isProlate

721 x = (atan2(-x * e, _N_1_0) if x < 0 else

722 atan2( x * e, _1_0)) / e

723 return x

724

725

726 def _atanhs1(x):

727 '''(INTERNAL) Function M{atanh(sqrt(x)) / sqrt(x) - 1}.

728 '''

729 s = fabs(x)

730 if s < _0_5: # for typical ...

731 # x < E.e^2 == 2 * E.f use ...

732 # x / 3 + x^2 / 5 + x^3 / 7 + ...

733 y, k = x, 3

734 _S2 = Fsum(y / k).fsum2_

735 for _ in range(_TERMS): # 9 terms

736 y *= x # x**n

737 k += 2 # 2*n + 1

738 s, d = _S2(y / k)

739 if not d:

740 break

741 else:

742 s = sqrt(s)

743 s = (atanh(s) if x > 0 else atan(s)) / s - _1_0

744 return s

745

746

747 def _Datanhee(x, y, E): # see .rhumbx._DeatanhE

748 '''(INTERNAL) Function M{Datanhee(x, y)}, defined as

749 M{atanhee((x - y) / (1 - E.e^2 * x * y)) / (x - y)}.

750 '''

751 d = _1_0 - E.e2 * x * y

752 if d:

753 d = _1_0 / d

754 t = x - y

755 if t:

756 d = _atanhee(t * d, E) / t

757 else:

758 raise AlbersError(x=x, y=y, txt=_Datanhee.__name__)

759 return d

760

761

762 def _D2atanhee(x, y, E):

763 '''(INTERNAL) Function M{D2atanhee(x, y)}, defined as

764 M{(Datanhee(1, y) - Datanhee(1, x)) / (y - x)}.

765 '''

766 e2 = E.e2

767 if ((fabs(x) + fabs(y)) * e2) < _0_5:

768 e = z = _1_0

769 k = 1

770 T = Fsum() # Taylor expansion

771 _T = T.fsum_

772 _C = Fsum().fsum_

773 _S2 = Fsum().fsum2_

774 for _ in range(_TERMS): # 15 terms

775 T *= y; p = _T(z); z *= x # PYCHOK ;

776 T *= y; t = _T(z); z *= x # PYCHOK ;

777 e *= e2

778 k += 2

779 s, d = _S2(e * _C(p, t) / k)

780 if not d:

781 break

782 else: # PYCHOK no cover

783 d = _1_0 - x

784 if isnear0(d):

785 raise AlbersError(x=x, y=y, txt=_D2atanhee.__name__)

786 s = (_Datanhee(_1_0, y, E) - _Datanhee(x, y, E)) / d

787 return s

788

789

790 def _Dsn(x, y, sx, sy):

791 '''(INTERNAL) Divided differences, defined as M{Df(x, y) = (f(x) - f(y)) / (x - y)}

792 with M{sn(x) = x / sqrt(1 + x^2)}: M{Dsn(x, y) = (x + y) / ((sn(x) + sn(y)) *

793 (1 + x^2) * (1 + y^2))}.

794

795 @see: U{W. M. Kahan and R. J. Fateman, "Sympbolic Computation of Divided

796 Differences"<https://People.EECS.Berkeley.EDU/~fateman/papers/divdiff.pdf>},

797 U{ACM SIGSAM Bulletin 33(2), 7-28 (1999)<https://DOI.org/10.1145/334714.334716>}

798 and U{AlbersEqualArea.hpp

799 <https://GeographicLib.SourceForge.io/C++/doc/AlbersEqualArea_8hpp_source.html>}.

800 '''

801 # sx = x / hypot1(x)

802 d, t = _1_0, (x * y)

803 if t > 0:

804 s = sx + sy

805 if s:

806 t = sx * sy / t

807 d = t**2 * (x + y) / s

808 elif x != y:

809 d = (sx - sy) / (x - y)

810 return d

811

812

813 def _tol(tol, x):

814 '''(INTERNAL) Converge tolerance.

815 '''

816 return tol * max(_1_0, fabs(x))

817

818

819 def _1_x21(x):

820 '''(INTERNAL) Return M{1 / (x**2 + 1)}.

821 '''

822 return _1_0 / (x**2 + _1_0)

823

824

825 def _xlat(lat, f, where):

826 '''(INTERNAL) check fixed C{lat}.

827 '''

828 if lat is not f and _Lat(lat=lat) != f:

829 t = unstr(where.__name__, lat=lat)

830 raise AlbersError(t, txt=_not_(f))

831

832

833__all__ += _ALL_DOCS(_AlbersBase)

834

835# **) MIT License

836#

837# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.

838#

839# Permission is hereby granted, free of charge, to any person obtaining a

840# copy of this software and associated documentation files (the "Software"),

841# to deal in the Software without restriction, including without limitation

842# the rights to use, copy, modify, merge, publish, distribute, sublicense,

843# and/or sell copies of the Software, and to permit persons to whom the

844# Software is furnished to do so, subject to the following conditions:

845#

846# The above copyright notice and this permission notice shall be included

847# in all copies or substantial portions of the Software.

848#

849# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS

850# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

851# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL

852# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR

853# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,

854# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR

855# OTHER DEALINGS IN THE SOFTWARE.