Coverage for pygeodesy/azimuthal.py: 98%

318 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-05-29 12:40 -0400

1 

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

3 

4u'''Equidistant, Equal-Area, and other Azimuthal projections. 

5 

6Classes L{Equidistant}, L{EquidistantExact}, L{EquidistantGeodSolve}, 

7L{EquidistantKarney}, L{Gnomonic}, L{GnomonicExact}, L{GnomonicKarney}, 

8L{LambertEqualArea}, L{Orthographic} and L{Stereographic}, classes 

9L{AzimuthalError}, L{Azimuthal7Tuple} and functions L{equidistant} 

10and L{gnomonic}. 

11 

12L{EquidistantExact} and L{GnomonicExact} are based on exact geodesic classes 

13L{GeodesicExact} and L{GeodesicLineExact}, Python versions of I{Charles Karney}'s 

14C++ original U{GeodesicExact<https://GeographicLib.SourceForge.io/C++/doc/ 

15classGeographicLib_1_1GeodesicExact.html>}, respectively U{GeodesicLineExact 

16<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicLineExact.html>}. 

17 

18Using L{EquidistantGeodSolve} requires I{Karney}'s utility U{GeodSolve 

19<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>} to be 

20executable and set in env variable C{PYGEODESY_GEODSOLVE}, see module 

21L{geodsolve} for more details. 

22 

23L{EquidistantKarney} and L{GnomonicKarney} require I{Karney}'s Python package 

24U{geographiclib<https://PyPI.org/project/geographiclib>} to be installed. 

25 

26Other azimuthal classes implement only (***) U{Snyder's FORMULAS FOR THE SPHERE 

27<https://Pubs.USGS.gov/pp/1395/report.pdf>} and use those for any datum, 

28spherical and ellipsoidal. The radius used for the latter is the ellipsoid's 

29I{mean radius of curvature} at the latitude of the projection center point. For 

30further justification, see the first paragraph under U{Snyder's FORMULAS FOR THE 

31ELLIPSOID, page 197<https://Pubs.USGS.gov/pp/1395/report.pdf>}. 

32 

33Page numbers in C{Snyder} references apply to U{John P. Snyder, "Map Projections 

34-- A Working Manual", 1987<https://Pubs.USGS.gov/pp/1395/report.pdf>}. 

35 

36See also U{here<https://WikiPedia.org/wiki/Azimuthal_equidistant_projection>}, 

37especially the U{Comparison of the Azimuthal equidistant projection and some 

38azimuthal projections centred on 90° N at the same scale, ordered by projection 

39altitude in Earth radii<https://WikiPedia.org/wiki/Azimuthal_equidistant_projection 

40#/media/File:Comparison_azimuthal_projections.svg>}. 

41''' 

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

43from __future__ import division as _; del _ # noqa: E702 ; 

44 

45# from pygeodesy.basics import _isin, _xinstanceof # from .ellipsoidalBase 

46from pygeodesy.constants import EPS, EPS0, EPS1, NAN, isnon0, _umod_360, \ 

47 _EPStol, _0_0, _0_1, _0_5, _1_0, _N_1_0, _2_0 

48from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase as _LLEB, \ 

49 _isin, _xinstanceof 

50from pygeodesy.datums import _spherical_datum, _WGS84 

51from pygeodesy.errors import _ValueError, _xdatum, _xkwds 

52from pygeodesy.fmath import euclid, fdot_, hypot as _hypot, Fsum 

53# from pygeodesy.fsums import Fsum # from .fmath 

54# from pygeodesy.formy import antipode # _MODS 

55# from pygeodesy.internals import typename # from .karney 

56from pygeodesy.interns import _azimuth_, _datum_, _lat_, _lon_, _scale_, \ 

57 _SPACE_, _x_, _y_ 

58from pygeodesy.karney import _norm180, typename 

59from pygeodesy.latlonBase import _MODS, LatLonBase as _LLB 

60from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _FOR_DOCS # ALL_MODS 

61from pygeodesy.named import _name__, _name2__, _NamedBase, _NamedTuple, _Pass 

62from pygeodesy.namedTuples import LatLon2Tuple, LatLon4Tuple 

63from pygeodesy.props import deprecated_Property_RO, Property_RO, \ 

64 property_doc_, _update_all 

65from pygeodesy.streprs import Fmt, _fstrLL0, unstr 

66from pygeodesy.units import Azimuth, Easting, Lat_, Lon_, Northing, \ 

67 Scalar, Scalar_ 

68from pygeodesy.utily import asin1, atan1, atan2, atan2b, atan2d, \ 

69 sincos2, sincos2d, sincos2d_ 

70 

71from math import acos, degrees, fabs, sin, sqrt 

72 

73__all__ = _ALL_LAZY.azimuthal 

74__version__ = '25.05.12' 

75 

76_EPS_K = _EPStol * _0_1 # Karney's eps_ or _EPSmin * _0_1? 

77_over_horizon_ = 'over horizon' 

78_TRIPS = 21 # numit, 4 sufficient 

79 

80 

81def _enzh4(x, y, *h): 

82 '''(INTERNAL) Return 4-tuple (easting, northing, azimuth, hypot). 

83 ''' 

84 e = Easting( x=x) 

85 n = Northing(y=y) 

86 z = atan2b(e, n) # (x, y) for azimuth from true North 

87 return e, n, z, (h[0] if h else _hypot(e, n)) 

88 

89 

90class _AzimuthalBase(_NamedBase): 

91 '''(INTERNAL) Base class for azimuthal projections. 

92 

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

94 C++/doc/classGeographicLib_1_1AzimuthalEquidistant.html>} and U{Gnomonic 

95 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Gnomonic.html>} or 

96 the C{PyGeodesy} versions thereof L{EquidistantKarney} respectively L{GnomonicKarney}. 

97 ''' 

98 _datum = _WGS84 # L{Datum} 

99 _latlon0 = LatLon2Tuple(_0_0, _0_0) # lat0, lon0 (L{LatLon2Tuple}) 

100 _sc0 = _0_0, _1_0 # 2-Tuple C{sincos2d(lat0)} 

101 

102 def __init__(self, lat0, lon0, datum=None, **name): 

103 '''New azimuthal projection. 

104 

105 @arg lat0: Latitude of the center point (C{degrees90}). 

106 @arg lon0: Longitude of the center point (C{degrees180}). 

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

108 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth 

109 radius (C{meter}). 

110 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}). 

111 

112 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}} or (spherical) B{C{datum}}. 

113 

114 @raise TypeError: Invalid B{C{datum}}. 

115 ''' 

116 if not _isin(datum, None, self._datum): 

117 self._datum = _spherical_datum(datum, **name) 

118 if name: 

119 self.name = name 

120 

121 if lat0 or lon0: # often both 0 

122 self._reset(lat0, lon0) 

123 

124 @Property_RO 

125 def datum(self): 

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

127 ''' 

128 return self._datum 

129 

130 @Property_RO 

131 def equatoradius(self): 

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

133 ''' 

134 return self.datum.ellipsoid.a 

135 

136 a = equatoradius 

137 

138 @Property_RO 

139 def flattening(self): 

140 '''Get the geodesic's flattening (C{scalar}). 

141 ''' 

142 return self.datum.ellipsoid.f 

143 

144 f = flattening 

145 

146 def forward(self, lat, lon, **name): # PYCHOK no cover 

147 '''I{Must be overloaded}.''' 

148 self._notOverloaded(lat, lon, **name) 

149 

150 def _forward(self, lat, lon, name, _k_t_2): 

151 '''(INTERNAL) Azimuthal (spherical) forward C{lat, lon} to C{x, y}. 

152 ''' 

153 lat, lon = Lat_(lat), Lon_(lon) 

154 sa, ca, sb, cb = sincos2d_(lat, lon - self.lon0) 

155 s0, c0 = self._sc0 

156 

157 cb *= ca 

158 k, t = _k_t_2(fdot_(s0, sa, c0, cb)) 

159 if t: 

160 r = k * self.radius 

161 y = r * fdot_(c0, sa, -s0, cb) 

162 e, n, z, _ = _enzh4(r * sb * ca, y, None) 

163 else: # 0 or 180 

164 e = n = z = _0_0 

165 

166 t = Azimuthal7Tuple(e, n, lat, lon, z, k, self.datum, 

167 name=self._name__(name)) 

168 return t 

169 

170 def _forwards(self, *lls): 

171 '''(INTERNAL) One or more C{.forward} calls, see .ellipsoidalBaseDI. 

172 ''' 

173 _fwd = self.forward 

174 for ll in lls: 

175 yield _fwd(ll.lat, ll.lon) 

176 

177 @Property_RO 

178 def lat0(self): 

179 '''Get the center latitude (C{degrees90}). 

180 ''' 

181 return self._latlon0.lat 

182 

183 @property 

184 def latlon0(self): 

185 '''Get the center lat- and longitude (L{LatLon2Tuple}C{(lat, lon)}) in (C{degrees90}, C{degrees180}). 

186 ''' 

187 return self._latlon0 

188 

189 @latlon0.setter # PYCHOK setter! 

190 def latlon0(self, latlon0): 

191 '''Set the center lat- and longitude (C{LatLon}, L{LatLon2Tuple} or L{LatLon4Tuple}). 

192 

193 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}} or ellipsoidal mismatch 

194 of B{C{latlon0}} and this projection. 

195 ''' 

196 B = _LLEB if self.datum.isEllipsoidal else _LLB 

197 _xinstanceof(B, LatLon2Tuple, LatLon4Tuple, latlon0=latlon0) 

198 if hasattr(latlon0, _datum_): 

199 _xdatum(self.datum, latlon0.datum, Error=AzimuthalError) 

200 self.reset(latlon0.lat, latlon0.lon) 

201 

202 @Property_RO 

203 def lon0(self): 

204 '''Get the center longitude (C{degrees180}). 

205 ''' 

206 return self._latlon0.lon 

207 

208 @deprecated_Property_RO 

209 def majoradius(self): # PYCHOK no cover 

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

211 return self.equatoradius 

212 

213 @Property_RO 

214 def radius(self): 

215 '''Get this projection's mean radius of curvature (C{meter}). 

216 ''' 

217 return self.datum.ellipsoid.rocMean(self.lat0) 

218 

219 def reset(self, lat0, lon0): 

220 '''Set or reset the center point of this azimuthal projection. 

221 

222 @arg lat0: Center point latitude (C{degrees90}). 

223 @arg lon0: Center point longitude (C{degrees180}). 

224 

225 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}}. 

226 ''' 

227 _update_all(self) # zap caches 

228 self._reset(lat0, lon0) 

229 

230 def _reset(self, lat0, lon0): 

231 '''(INTERNAL) Update the center point. 

232 ''' 

233 self._latlon0 = LatLon2Tuple(Lat_(lat0=lat0, Error=AzimuthalError), 

234 Lon_(lon0=lon0, Error=AzimuthalError)) 

235 self._sc0 = sincos2d(self.lat0) 

236 

237 def reverse(self, x, y, **name_LatLon_and_kwds): 

238 '''I{Must be overloaded}.''' 

239 self._notOverloaded(x, y, **name_LatLon_and_kwds) # PYCHOK no cover 

240 

241 def _reverse(self, x, y, _c, lea, LatLon=None, **name_LatLon_kwds): 

242 '''(INTERNAL) Azimuthal (spherical) reverse C{x, y} to C{lat, lon}. 

243 ''' 

244 e, n, z, r = _enzh4(x, y) 

245 

246 c = _c(r / self.radius) 

247 if c is None: 

248 lat, lon = self.latlon0 

249 k, z = _1_0, _0_0 

250 else: 

251 s0, c0 = self._sc0 

252 sc, cc = sincos2(c) 

253 k = c / sc 

254 s = s0 * cc 

255 if r > EPS0: 

256 s += c0 * sc * (n / r) 

257 lat = degrees(asin1(s)) 

258 if lea or fabs(c0) > EPS: 

259 d = atan2d(e * sc, r * c0 * cc - n * s0 * sc) 

260 else: 

261 d = atan2d(e, (n if s0 < 0 else -n)) 

262 lon = _norm180(self.lon0 + d) 

263 

264 if LatLon is None: 

265 t, _ = _name2__(name_LatLon_kwds, _or_nameof=self) 

266 t = Azimuthal7Tuple(e, n, lat, lon, z, k, self.datum, name=t) 

267 else: 

268 t = self._toLatLon(lat, lon, LatLon, name_LatLon_kwds) 

269 return t 

270 

271 def _reverse2(self, x_t, *y): 

272 '''(INTERNAL) See iterating functions .ellipsoidalBaseDI._intersect3, 

273 .ellipsoidalBaseDI._intersects2 and .ellipsoidalBaseDI._nearestOne. 

274 ''' 

275 t = self.reverse(x_t, *y) if y else self.reverse(x_t.x, x_t.y) # LatLon=None 

276 d = euclid(t.lat - self.lat0, t.lon - self.lon0) # degrees 

277 return t, d 

278 

279 def _toLatLon(self, lat, lon, LatLon, name_LatLon_kwds): 

280 '''(INTERNAL) Check B{C{LatLon}} and return an instance. 

281 ''' 

282 kwds = _xkwds(name_LatLon_kwds, datum=self.datum, _or_nameof=self) 

283 r = LatLon(lat, lon, **kwds) # handle .classof 

284 B = _LLEB if self.datum.isEllipsoidal else _LLB 

285 _xinstanceof(B, LatLon=r) 

286 return r 

287 

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

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

290 

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

292 

293 @return: This projection as C{"<classname>(lat0, lon0, ...)"} 

294 (C{str}). 

295 ''' 

296 return _fstrLL0(self, prec, True) 

297 

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

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

300 

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

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

303 

304 @return: This projection as C{"lat0 lon0"} (C{str}). 

305 ''' 

306 t = _fstrLL0(self, prec, False) 

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

308 

309 

310class AzimuthalError(_ValueError): 

311 '''An azimuthal L{Equidistant}, L{EquidistantKarney}, L{Gnomonic}, 

312 L{LambertEqualArea}, L{Orthographic}, L{Stereographic} or 

313 L{Azimuthal7Tuple} issue. 

314 ''' 

315 pass 

316 

317 

318class Azimuthal7Tuple(_NamedTuple): 

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

320 C{degrees90}, C{degrees180}, compass C{degrees}, C{scalar} and C{Datum} 

321 where C{(x, y)} is the easting and northing of a projected point, C{(lat, 

322 lon)} the geodetic location, C{azimuth} the azimuth, clockwise from true 

323 North and C{scale} is the projection scale, either C{1 / reciprocal} or 

324 C{1} or C{-1} in the L{Equidistant} case. 

325 ''' 

326 _Names_ = (_x_, _y_, _lat_, _lon_, _azimuth_, _scale_, _datum_) 

327 _Units_ = ( Easting, Northing, Lat_, Lon_, Azimuth, Scalar, _Pass) 

328 

329 def antipodal(self, azimuth=None): 

330 '''Return this tuple with the antipodal C{lat} and C{lon}. 

331 

332 @kwarg azimuth: Optional azimuth, overriding the current azimuth 

333 (C{compass degrees360}). 

334 ''' 

335 a = _MODS.formy.antipode(self.lat, self.lon) # PYCHOK named 

336 z = self.azimuth if azimuth is None else Azimuth(azimuth) # PYCHOK named 

337 return _NamedTuple.dup(self, lat=a.lat, lon=a.lon, azimuth=z) 

338 

339 

340class Equidistant(_AzimuthalBase): 

341 '''Azimuthal equidistant projection for the sphere***, see U{Snyder, pp 195-197 

342 <https://Pubs.USGS.gov/pp/1395/report.pdf>} and U{MathWorld-Wolfram 

343 <https://MathWorld.Wolfram.com/AzimuthalEquidistantProjection.html>}. 

344 

345 @note: Results from this L{Equidistant} and an L{EquidistantExact}, 

346 L{EquidistantGeodSolve} or L{EquidistantKarney} projection 

347 C{may differ} by 10% or more. For an example, see method 

348 C{testDiscrepancies} in module C{testAzimuthal.py}. 

349 ''' 

350 if _FOR_DOCS: 

351 __init__ = _AzimuthalBase.__init__ 

352 

353 def forward(self, lat, lon, **name): 

354 '''Convert a geodetic location to azimuthal equidistant east- and northing. 

355 

356 @arg lat: Latitude of the location (C{degrees90}). 

357 @arg lon: Longitude of the location (C{degrees180}). 

358 @kwarg name: Optional C{B{name}=NN} for the location (C{str}). 

359 

360 @return: An L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)} 

361 with easting C{x} and northing C{y} of point in C{meter} and C{lat} 

362 and C{lon} in C{degrees} and C{azimuth} clockwise from true North. 

363 The C{scale} of the projection is C{1} in I{radial} direction and 

364 is C{1 / reciprocal} in the direction perpendicular to this. 

365 

366 @raise AzimuthalError: Invalid B{C{lat}} or B{C{lon}}. 

367 

368 @note: The C{scale} will be C{-1} if B{C{(lat, lon)}} is antipodal to the 

369 projection center C{(lat0, lon0)}. 

370 ''' 

371 def _k_t(c): 

372 k = _N_1_0 if c < 0 else _1_0 

373 t = fabs(c) < EPS1 

374 if t: 

375 a = acos(c) 

376 s = sin(a) 

377 if s: 

378 k = a / s 

379 return k, t 

380 

381 return self._forward(lat, lon, name, _k_t) 

382 

383 def reverse(self, x, y, **name_LatLon_and_kwds): 

384 '''Convert an azimuthal equidistant location to geodetic lat- and longitude. 

385 

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

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

388 @kwarg name_LatLon_and_kwds: Optional C{B{name}=NN} and class C{B{LatLon}=None} 

389 to use and optionally, additional B{C{LatLon}} keyword arguments, 

390 ignored if C{B{LatLon} is None}. 

391 

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

393 L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}. 

394 

395 @note: The C{lat} will be in the range C{[-90..90] degrees} and C{lon} 

396 in the range C{[-180..180] degrees}. The C{scale} of the 

397 projection is C{1} in I{radial} direction, C{azimuth} clockwise 

398 from true North and is C{1 / reciprocal} in the direction 

399 perpendicular to this. 

400 ''' 

401 def _c(c): 

402 return c if c > EPS else None 

403 

404 return self._reverse(x, y, _c, False, **name_LatLon_and_kwds) 

405 

406 

407def equidistant(lat0, lon0, datum=_WGS84, exact=False, geodsolve=False, **name): 

408 '''Return an L{EquidistantExact}, L{EquidistantGeodSolve} or (if I{Karney}'s 

409 U{geographiclib<https://PyPI.org/project/geographiclib>} package is 

410 installed) an L{EquidistantKarney}, otherwise an L{Equidistant} instance. 

411 

412 @arg lat0: Latitude of center point (C{degrees90}). 

413 @arg lon0: Longitude of center point (C{degrees180}). 

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

415 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth 

416 radius (C{meter}). 

417 @kwarg exact: Return an L{EquidistantExact} instance. 

418 @kwarg geodsolve: Return an L{EquidistantGeodSolve} instance. 

419 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}). 

420 

421 @return: An L{EquidistantExact}, L{EquidistantGeodSolve}, 

422 L{EquidistantKarney} or L{Equidistant} instance. 

423 

424 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}} or (spherical) B{C{datum}}. 

425 

426 @raise GeodesicError: Issue with L{GeodesicExact}, L{GeodesicSolve} 

427 or I{Karney}'s wrapped C{Geodesic}. 

428 

429 @raise TypeError: Invalid B{C{datum}}. 

430 ''' 

431 

432 E = EquidistantExact if exact else (EquidistantGeodSolve if geodsolve else Equidistant) 

433 if E is Equidistant: 

434 try: 

435 return EquidistantKarney(lat0, lon0, datum=datum, **name) # PYCHOK types 

436 except ImportError: 

437 pass 

438 return E(lat0, lon0, datum=datum, **name) # PYCHOK types 

439 

440 

441class _AzimuthalGeodesic(_AzimuthalBase): 

442 '''(INTERNAL) Base class for azimuthal projections using the 

443 I{wrapped} U{geodesic.Geodesic and geodesicline.GeodesicLine 

444 <https://GeographicLib.SourceForge.io/Python/doc/code.html>} or the 

445 I{exact} geodesic classes L{GeodesicExact} and L{GeodesicLineExact}. 

446 ''' 

447 _mask = 0 

448 

449 @Property_RO 

450 def geodesic(self): # PYCHOK no cover 

451 '''I{Must be overloaded}.''' 

452 self._notOverloaded() 

453 

454 def _7Tuple(self, e, n, r, name_LatLon_kwds, M=None): 

455 '''(INTERNAL) Return an C{Azimuthal7Tuple}. 

456 ''' 

457 s = M 

458 if s is None: # reciprocal, azimuthal scale 

459 s = (r.m12 / r.s12) if r.a12 > _EPS_K else _1_0 

460 z = _umod_360(r.azi2) # -180 <= r.azi2 < 180 ... 0 <= z < 360 

461 t, _ = _name2__(name_LatLon_kwds, _or_nameof=self) 

462 return Azimuthal7Tuple(e, n, r.lat2, r.lon2, z, s, self.datum, name=t) 

463 

464 

465class _EquidistantBase(_AzimuthalGeodesic): 

466 '''(INTERNAL) Base for classes L{EquidistantExact}, L{EquidistantGeodSolve} 

467 and L{EquidistantKarney}. 

468 ''' 

469 def __init__(self, lat0, lon0, datum=_WGS84, **name): 

470 '''New azimuthal L{EquidistantExact}, L{EquidistantGeodSolve} or 

471 L{EquidistantKarney} projection. 

472 ''' 

473 _AzimuthalGeodesic.__init__(self, lat0, lon0, datum=datum, **name) 

474 

475 g = self.geodesic 

476 # g.STANDARD = g.AZIMUTH | g.DISTANCE | g.LATITUDE | g.LONGITUDE 

477 self._mask = g.REDUCEDLENGTH | g.STANDARD # | g.LONG_UNROLL 

478 

479 def forward(self, lat, lon, **name): 

480 '''Convert an (ellipsoidal) geodetic location to azimuthal equidistant east- and northing. 

481 

482 @arg lat: Latitude of the location (C{degrees90}). 

483 @arg lon: Longitude of the location (C{degrees180}). 

484 @kwarg name: Optional C{B{name}=NN} for the location (C{str}). 

485 

486 @return: An L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)} 

487 with easting C{x} and northing C{y} of point in C{meter} and C{lat} 

488 and C{lon} in C{degrees} and C{azimuth} clockwise from true North. 

489 The C{scale} of the projection is C{1} in I{radial} direction and 

490 is C{1 / reciprocal} in the direction perpendicular to this. 

491 

492 @raise AzimuthalError: Invalid B{C{lat}} or B{C{lon}}. 

493 

494 @note: A call to C{.forward} followed by a call to C{.reverse} will return 

495 the original C{lat, lon} to within roundoff. 

496 ''' 

497 r = self.geodesic.Inverse(self.lat0, self.lon0, 

498 Lat_(lat), Lon_(lon), outmask=self._mask) 

499 x, y = sincos2d(r.azi1) 

500 return self._7Tuple(x * r.s12, y * r.s12, r, _name__(name)) 

501 

502 def reverse(self, x, y, LatLon=None, **name_LatLon_kwds): # PYCHOK signature 

503 '''Convert an azimuthal equidistant location to (ellipsoidal) geodetic lat- and longitude. 

504 

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

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

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

508 @kwarg name_LatLon_kwds: Optional C{B{name}=NN} and optionally, additional 

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

510 

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

512 L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}. 

513 

514 @note: The C{lat} will be in the range C{[-90..90] degrees} and C{lon} 

515 in the range C{[-180..180] degrees}. The scale of the projection 

516 is C{1} in I{radial} direction, C{azimuth} clockwise from true 

517 North and is C{1 / reciprocal} in the direction perpendicular 

518 to this. 

519 ''' 

520 e, n, z, s = _enzh4(x, y) 

521 

522 r = self.geodesic.Direct(self.lat0, self.lon0, z, s, outmask=self._mask) 

523 return self._7Tuple(e, n, r, name_LatLon_kwds) if LatLon is None else \ 

524 self._toLatLon(r.lat2, r.lon2, LatLon, name_LatLon_kwds) 

525 

526 

527class EquidistantExact(_EquidistantBase): 

528 '''Azimuthal equidistant projection, a Python version of I{Karney}'s C++ class U{AzimuthalEquidistant 

529 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AzimuthalEquidistant.html>}, 

530 based on exact geodesic classes L{GeodesicExact} and L{GeodesicLineExact}. 

531 

532 An azimuthal equidistant projection is centered at an arbitrary position on the ellipsoid. 

533 For a point in projected space C{(x, y)}, the geodesic distance from the center position 

534 is C{hypot(x, y)} and the C{azimuth} of the geodesic from the center point is C{atan2(x, y)}, 

535 clockwise from true North. 

536 

537 The C{.forward} and C{.reverse} methods also return the C{azimuth} of the geodesic at C{(x, 

538 y)} and the C{scale} in the azimuthal direction which, together with the basic properties 

539 of the projection, serve to specify completely the local affine transformation between 

540 geographic and projected coordinates. 

541 ''' 

542 def __init__(self, lat0, lon0, datum=_WGS84, **name): 

543 '''New azimuthal L{EquidistantExact} projection. 

544 

545 @arg lat0: Latitude of center point (C{degrees90}). 

546 @arg lon0: Longitude of center point (C{degrees180}). 

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

548 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth 

549 radius (C{meter}). 

550 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}). 

551 

552 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}} or B{C{datum}}. 

553 ''' 

554 _EquidistantBase.__init__(self, lat0, lon0, datum=datum, **name) 

555 

556 if _FOR_DOCS: 

557 forward = _EquidistantBase.forward 

558 reverse = _EquidistantBase.reverse 

559 

560 @Property_RO 

561 def geodesic(self): 

562 '''Get this projection's exact geodesic (L{GeodesicExact}). 

563 ''' 

564 return self.datum.ellipsoid.geodesicx 

565 

566 

567class EquidistantGeodSolve(_EquidistantBase): 

568 '''Azimuthal equidistant projection, a Python version of I{Karney}'s C++ class U{AzimuthalEquidistant 

569 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AzimuthalEquidistant.html>}, 

570 based on (exact) geodesic I{wrappers} L{GeodesicSolve} and L{GeodesicLineSolve} and intended 

571 I{for testing purposes only}. 

572 

573 @see: L{EquidistantExact} and module L{geodsolve}. 

574 ''' 

575 def __init__(self, lat0, lon0, datum=_WGS84, **name): 

576 '''New azimuthal L{EquidistantGeodSolve} projection. 

577 

578 @arg lat0: Latitude of center point (C{degrees90}). 

579 @arg lon0: Longitude of center point (C{degrees180}). 

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

581 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth 

582 radius (C{meter}). 

583 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}). 

584 

585 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}} or B{C{datum}}. 

586 ''' 

587 _EquidistantBase.__init__(self, lat0, lon0, datum=datum, **name) 

588 

589 if _FOR_DOCS: 

590 forward = _EquidistantBase.forward 

591 reverse = _EquidistantBase.reverse 

592 

593 @Property_RO 

594 def geodesic(self): 

595 '''Get this projection's (exact) geodesic (L{GeodesicSolve}). 

596 ''' 

597 return self.datum.ellipsoid.geodsolve 

598 

599 

600class EquidistantKarney(_EquidistantBase): 

601 '''Azimuthal equidistant projection, a Python version of I{Karney}'s C++ class U{AzimuthalEquidistant 

602 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AzimuthalEquidistant.html>}, 

603 requiring package U{geographiclib<https://PyPI.org/project/geographiclib>} to be installed. 

604 

605 @see: L{EquidistantExact}. 

606 ''' 

607 def __init__(self, lat0, lon0, datum=_WGS84, **name): 

608 '''New azimuthal L{EquidistantKarney} projection. 

609 

610 @arg lat0: Latitude of center point (C{degrees90}). 

611 @arg lon0: Longitude of center point (C{degrees180}). 

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

613 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth 

614 radius (C{meter}). 

615 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}). 

616 

617 @raise ImportError: Package U{geographiclib<https://PyPI.org/project/geographiclib>} 

618 not installed or not found. 

619 

620 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}} or B{C{datum}}. 

621 ''' 

622 _EquidistantBase.__init__(self, lat0, lon0, datum=datum, **name) 

623 

624 if _FOR_DOCS: 

625 forward = _EquidistantBase.forward 

626 reverse = _EquidistantBase.reverse 

627 

628 @Property_RO 

629 def geodesic(self): 

630 '''Get this projection's I{wrapped} U{geodesic.Geodesic 

631 <https://GeographicLib.SourceForge.io/Python/doc/code.html>}, provided 

632 I{Karney}'s U{geographiclib<https://PyPI.org/project/geographiclib>} 

633 package is installed. 

634 ''' 

635 return self.datum.ellipsoid.geodesic 

636 

637 

638_Equidistants = (Equidistant, EquidistantExact, EquidistantGeodSolve, 

639 EquidistantKarney) # PYCHOK in .ellipsoidalBaseDI 

640 

641 

642class Gnomonic(_AzimuthalBase): 

643 '''Azimuthal gnomonic projection for the sphere***, see U{Snyder, pp 164-168 

644 <https://Pubs.USGS.gov/pp/1395/report.pdf>} and U{MathWorld-Wolfram 

645 <https://MathWorld.Wolfram.com/GnomonicProjection.html>}. 

646 ''' 

647 if _FOR_DOCS: 

648 __init__ = _AzimuthalBase.__init__ 

649 

650 def forward(self, lat, lon, **name): 

651 '''Convert a geodetic location to azimuthal equidistant east- and northing. 

652 

653 @arg lat: Latitude of the location (C{degrees90}). 

654 @arg lon: Longitude of the location (C{degrees180}). 

655 @kwarg name: Optional C{B{name}=NN} for the location (C{str}). 

656 

657 @return: An L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)} 

658 with easting C{x} and northing C{y} of point in C{meter} and C{lat} 

659 and C{lon} in C{degrees} and C{azimuth} clockwise from true North. 

660 The C{scale} of the projection is C{1} in I{radial} direction and 

661 is C{1 / reciprocal} in the direction perpendicular to this. 

662 

663 @raise AzimuthalError: Invalid B{C{lat}} or B{C{lon}}. 

664 ''' 

665 def _k_t(c): 

666 t = c > EPS 

667 k = (_1_0 / c) if t else _1_0 

668 return k, t 

669 

670 return self._forward(lat, lon, name, _k_t) 

671 

672 def reverse(self, x, y, **name_LatLon_and_kwds): 

673 '''Convert an azimuthal equidistant location to geodetic lat- and longitude. 

674 

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

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

677 @kwarg name_LatLon_and_kwds: Optional C{B{name}=NN} and class C{B{LatLon}=None} 

678 for the location and optionally, additional B{C{LatLon}} keyword 

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

680 

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

682 L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}. 

683 

684 @note: The C{lat} will be in the range C{[-90..90] degrees} and C{lon} 

685 in the range C{[-180..180] degrees}. The C{scale} of the 

686 projection is C{1} in I{radial} direction, C{azimuth} clockwise 

687 from true North and C{1 / reciprocal} in the direction 

688 perpendicular to this. 

689 ''' 

690 def _c(c): 

691 return atan1(c) if c > EPS else None 

692 

693 return self._reverse(x, y, _c, False, **name_LatLon_and_kwds) 

694 

695 

696def gnomonic(lat0, lon0, datum=_WGS84, exact=False, geodsolve=False, **name): 

697 '''Return a L{GnomonicExact} or (if I{Karney}'s U{geographiclib 

698 <https://PyPI.org/project/geographiclib>} package is installed) 

699 a L{GnomonicKarney}, otherwise a L{Gnomonic} instance. 

700 

701 @arg lat0: Latitude of center point (C{degrees90}). 

702 @arg lon0: Longitude of center point (C{degrees180}). 

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

704 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth 

705 radius (C{meter}). 

706 @kwarg exact: Return a L{GnomonicExact} instance. 

707 @kwarg geodsolve: Return a L{GnomonicGeodSolve} instance. 

708 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}). 

709 

710 @return: A L{GnomonicExact}, L{GnomonicGeodSolve}, 

711 L{GnomonicKarney} or L{Gnomonic} instance. 

712 

713 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}} or 

714 (spherical) B{C{datum}}. 

715 

716 @raise GeodesicError: Issue with L{GeodesicExact}, L{GeodesicSolve} 

717 or I{Karney}'s wrapped C{Geodesic}. 

718 

719 @raise TypeError: Invalid B{C{datum}}. 

720 ''' 

721 G = GnomonicExact if exact else (GnomonicGeodSolve if geodsolve else Gnomonic) 

722 if G is Gnomonic: 

723 try: 

724 return GnomonicKarney(lat0, lon0, datum=datum, **name) # PYCHOK types 

725 except ImportError: 

726 pass 

727 return G(lat0, lon0, datum=datum, **name) # PYCHOK types 

728 

729 

730class _GnomonicBase(_AzimuthalGeodesic): 

731 '''(INTERNAL) Base for classes L{GnomonicExact}, L{GnomonicGeodSolve} 

732 and L{GnomonicKarney}. 

733 ''' 

734 def __init__(self, lat0, lon0, datum=_WGS84, **name): 

735 '''New azimuthal L{GnomonicExact} or L{GnomonicKarney} projection. 

736 ''' 

737 _AzimuthalGeodesic.__init__(self, lat0, lon0, datum=datum, **name) 

738 

739 g = self.geodesic 

740 self._mask = g.ALL # | g.LONG_UNROLL 

741 

742 def forward(self, lat, lon, raiser=True, **name): # PYCHOK signature 

743 '''Convert an (ellipsoidal) geodetic location to azimuthal gnomonic east- 

744 and northing. 

745 

746 @arg lat: Latitude of the location (C{degrees90}). 

747 @arg lon: Longitude of the location (C{degrees180}). 

748 @kwarg raiser: Do or don't throw an error (C{bool}) if 

749 the location lies over the horizon. 

750 @kwarg name: Optional C{B{name}=NN} for the location (C{str}). 

751 

752 @return: An L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)} 

753 with easting C{x} and northing C{y} in C{meter} and C{lat} and 

754 C{lon} in C{degrees} and C{azimuth} clockwise from true North. 

755 The C{scale} of the projection is C{1 / reciprocal**2} in I{radial} 

756 direction and C{1 / reciprocal} in the direction perpendicular to 

757 this. Both C{x} and C{y} will be C{NAN} if the (geodetic) location 

758 lies over the horizon and C{B{raiser}=False}. 

759 

760 @raise AzimuthalError: Invalid B{C{lat}}, B{C{lon}} or the location lies 

761 over the horizon and C{B{raiser}=True}. 

762 ''' 

763 self._iteration = 0 

764 

765 r = self.geodesic.Inverse(self.lat0, self.lon0, 

766 Lat_(lat), Lon_(lon), outmask=self._mask) 

767 M = r.M21 

768 if M > EPS0: 

769 q = r.m12 / M # .M12 

770 e, n = sincos2d(r.azi1) 

771 e *= q 

772 n *= q 

773 elif raiser: # PYCHOK no cover 

774 raise AzimuthalError(lat=lat, lon=lon, txt=_over_horizon_) 

775 else: # PYCHOK no cover 

776 e = n = NAN 

777 

778 t = self._7Tuple(e, n, r, _name__(name), M=M) 

779 t._iteraton = 0 

780 return t 

781 

782 def reverse(self, x, y, LatLon=None, **name_LatLon_kwds): # PYCHOK signature 

783 '''Convert an azimuthal gnomonic location to (ellipsoidal) geodetic lat- and longitude. 

784 

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

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

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

788 @kwarg name_LatLon_kwds: Optional C{B{name}=NN} for the location and optionally, 

789 additional B{C{LatLon}} keyword arguments, ignored if C{B{LatLon} is None}. 

790 

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

792 L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}. 

793 

794 @raise AzimuthalError: No convergence. 

795 

796 @note: The C{lat} will be in the range C{[-90..90] degrees} and C{lon} in the range 

797 C{[-180..180] degrees}. The C{azimuth} is clockwise from true North. The 

798 scale is C{1 / reciprocal**2} in C{radial} direction and C{1 / reciprocal} 

799 in the direction perpendicular to this. 

800 ''' 

801 e, n, z, q = _enzh4(x, y) 

802 

803 d = a = self.equatoradius 

804 s = a * atan1(q, a) 

805 if q > a: # PYCHOK no cover 

806 def _d(r, q): 

807 return (r.M12 - q * r.m12) * r.m12 # negated 

808 

809 q = _1_0 / q 

810 else: # little == True 

811 def _d(r, q): # PYCHOK _d 

812 return (q * r.M12 - r.m12) * r.M12 # negated 

813 

814 a *= _EPS_K 

815 m = self._mask 

816 g = self.geodesic 

817 

818 _P = g.Line(self.lat0, self.lon0, z, caps=m | g.LINE_OFF).Position 

819 _S2 = Fsum(s).fsum2f_ 

820 _abs = fabs 

821 for i in range(1, _TRIPS): 

822 r = _P(s, outmask=m) 

823 if _abs(d) < a: 

824 break 

825 s, d = _S2(_d(r, q)) 

826 else: # PYCHOK no cover 

827 self._iteration = _TRIPS 

828 raise AzimuthalError(Fmt.no_convergence(d, a), 

829 txt=unstr(self.reverse, x, y)) 

830 

831 t = self._7Tuple(e, n, r, name_LatLon_kwds, M=r.M12) if LatLon is None else \ 

832 self._toLatLon(r.lat2, r.lon2, LatLon, name_LatLon_kwds) 

833 t._iteration = self._iteration = i 

834 return t 

835 

836 

837class GnomonicExact(_GnomonicBase): 

838 '''Azimuthal gnomonic projection, a Python version of I{Karney}'s C++ class U{Gnomonic 

839 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Gnomonic.html>}, 

840 based on exact geodesic classes L{GeodesicExact} and L{GeodesicLineExact}. 

841 

842 @see: I{Karney}'s U{Detailed Description<https://GeographicLib.SourceForge.io/C++/doc/ 

843 classGeographicLib_1_1Gnomonic.html>}, especially the B{Warning}. 

844 ''' 

845 def __init__(self, lat0, lon0, datum=_WGS84, **name): 

846 '''New azimuthal L{GnomonicExact} projection. 

847 

848 @arg lat0: Latitude of center point (C{degrees90}). 

849 @arg lon0: Longitude of center point (C{degrees180}). 

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

851 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth 

852 radius (C{meter}). 

853 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}). 

854 

855 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}}. 

856 ''' 

857 _GnomonicBase.__init__(self, lat0, lon0, datum=datum, **name) 

858 

859 if _FOR_DOCS: 

860 forward = _GnomonicBase.forward 

861 reverse = _GnomonicBase.reverse 

862 

863 @Property_RO 

864 def geodesic(self): 

865 '''Get this projection's exact geodesic (L{GeodesicExact}). 

866 ''' 

867 return self.datum.ellipsoid.geodesicx 

868 

869 

870class GnomonicGeodSolve(_GnomonicBase): 

871 '''Azimuthal gnomonic projection, a Python version of I{Karney}'s C++ class U{Gnomonic 

872 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Gnomonic.html>}, 

873 based on (exact) geodesic I{wrappers} L{GeodesicSolve} and L{GeodesicLineSolve} and 

874 intended I{for testing purposes only}. 

875 

876 @see: L{GnomonicExact} and module L{geodsolve}. 

877 ''' 

878 def __init__(self, lat0, lon0, datum=_WGS84, **name): 

879 '''New azimuthal L{GnomonicGeodSolve} projection. 

880 

881 @arg lat0: Latitude of center point (C{degrees90}). 

882 @arg lon0: Longitude of center point (C{degrees180}). 

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

884 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth 

885 radius (C{meter}). 

886 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}). 

887 

888 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}}. 

889 ''' 

890 _GnomonicBase.__init__(self, lat0, lon0, datum=datum, **name) 

891 

892 if _FOR_DOCS: 

893 forward = _GnomonicBase.forward 

894 reverse = _GnomonicBase.reverse 

895 

896 @Property_RO 

897 def geodesic(self): 

898 '''Get this projection's (exact) geodesic (L{GeodesicSolve}). 

899 ''' 

900 return self.datum.ellipsoid.geodsolve 

901 

902 

903class GnomonicKarney(_GnomonicBase): 

904 '''Azimuthal gnomonic projection, a Python version of I{Karney}'s C++ class U{Gnomonic 

905 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Gnomonic.html>}, 

906 requiring package U{geographiclib<https://PyPI.org/project/geographiclib>} to be installed. 

907 

908 @see: L{GnomonicExact}. 

909 ''' 

910 def __init__(self, lat0, lon0, datum=_WGS84, **name): 

911 '''New azimuthal L{GnomonicKarney} projection. 

912 

913 @arg lat0: Latitude of center point (C{degrees90}). 

914 @arg lon0: Longitude of center point (C{degrees180}). 

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

916 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth 

917 radius (C{meter}). 

918 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}). 

919 

920 @raise ImportError: Package U{geographiclib<https://PyPI.org/project/geographiclib>} 

921 not installed or not found. 

922 

923 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}}. 

924 ''' 

925 _GnomonicBase.__init__(self, lat0, lon0, datum=datum, **name) 

926 

927 if _FOR_DOCS: 

928 forward = _GnomonicBase.forward 

929 reverse = _GnomonicBase.reverse 

930 

931 @Property_RO 

932 def geodesic(self): 

933 '''Get this projection's I{wrapped} U{geodesic.Geodesic 

934 <https://GeographicLib.SourceForge.io/Python/doc/code.html>}, provided 

935 I{Karney}'s U{geographiclib<https://PyPI.org/project/geographiclib>} 

936 package is installed. 

937 ''' 

938 return self.datum.ellipsoid.geodesic 

939 

940 

941class LambertEqualArea(_AzimuthalBase): 

942 '''Lambert-equal-area projection for the sphere*** (aka U{Lambert zenithal equal-area 

943 projection<https://WikiPedia.org/wiki/Lambert_azimuthal_equal-area_projection>}, see 

944 U{Snyder, pp 185-187<https://Pubs.USGS.gov/pp/1395/report.pdf>} and U{MathWorld-Wolfram 

945 <https://MathWorld.Wolfram.com/LambertAzimuthalEqual-AreaProjection.html>}. 

946 ''' 

947 if _FOR_DOCS: 

948 __init__ = _AzimuthalBase.__init__ 

949 

950 def forward(self, lat, lon, **name): 

951 '''Convert a geodetic location to azimuthal Lambert-equal-area east- and northing. 

952 

953 @arg lat: Latitude of the location (C{degrees90}). 

954 @arg lon: Longitude of the location (C{degrees180}). 

955 @kwarg name: Optional C{B{name}=NN} for the location (C{str}). 

956 

957 @return: An L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)} 

958 with easting C{x} and northing C{y} of point in C{meter} and C{lat} 

959 and C{lon} in C{degrees} and C{azimuth} clockwise from true North. 

960 The C{scale} of the projection is C{1} in I{radial} direction and 

961 is C{1 / reciprocal} in the direction perpendicular to this. 

962 

963 @raise AzimuthalError: Invalid B{C{lat}} or B{C{lon}}. 

964 ''' 

965 def _k_t(c): 

966 c += _1_0 

967 t = c > EPS0 

968 k = sqrt(_2_0 / c) if t else _1_0 

969 return k, t 

970 

971 return self._forward(lat, lon, name, _k_t) 

972 

973 def reverse(self, x, y, **name_LatLon_and_kwds): 

974 '''Convert an azimuthal Lambert-equal-area location to geodetic lat- and longitude. 

975 

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

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

978 @kwarg name_LatLon_and_kwds: Optional C{B{name}=NN} and class C{B{LatLon}=None} 

979 to use and optionally, additional B{C{LatLon}} keyword arguments, 

980 ignored if C{B{LatLon} is None}. 

981 

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

983 L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}. 

984 

985 @note: The C{lat} will be in the range C{[-90..90] degrees} and C{lon} in the 

986 range C{[-180..180] degrees}. The C{scale} of the projection is C{1} 

987 in I{radial} direction, C{azimuth} clockwise from true North and is C{1 

988 / reciprocal} in the direction perpendicular to this. 

989 ''' 

990 def _c(c): 

991 c *= _0_5 

992 return (asin1(c) * _2_0) if c > EPS else None 

993 

994 return self._reverse(x, y, _c, True, **name_LatLon_and_kwds) 

995 

996 

997class Orthographic(_AzimuthalBase): 

998 '''Orthographic projection for the sphere***, see U{Snyder, pp 148-153 

999 <https://Pubs.USGS.gov/pp/1395/report.pdf>} and U{MathWorld-Wolfram 

1000 <https://MathWorld.Wolfram.com/OrthographicProjection.html>}. 

1001 ''' 

1002 if _FOR_DOCS: 

1003 __init__ = _AzimuthalBase.__init__ 

1004 

1005 def forward(self, lat, lon, **name): 

1006 '''Convert a geodetic location to azimuthal orthographic east- and northing. 

1007 

1008 @arg lat: Latitude of the location (C{degrees90}). 

1009 @arg lon: Longitude of the location (C{degrees180}). 

1010 @kwarg name: Optional C{B{name}=NN} for the location (C{str}). 

1011 

1012 @return: An L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)} 

1013 with easting C{x} and northing C{y} of point in C{meter} and C{lat} 

1014 and C{lon} in C{degrees} and C{azimuth} clockwise from true North. 

1015 The C{scale} of the projection is C{1} in I{radial} direction and 

1016 is C{1 / reciprocal} in the direction perpendicular to this. 

1017 

1018 @raise AzimuthalError: Invalid B{C{lat}} or B{C{lon}}. 

1019 ''' 

1020 def _k_t(c): 

1021 return _1_0, (c >= 0) 

1022 

1023 return self._forward(lat, lon, name, _k_t) 

1024 

1025 def reverse(self, x, y, **name_LatLon_and_kwds): 

1026 '''Convert an azimuthal orthographic location to geodetic lat- and longitude. 

1027 

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

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

1030 @kwarg name_LatLon_and_kwds: Optional C{B{name}=NN} and class C{B{LatLon}=None} 

1031 to use and optionally, additional B{C{LatLon}} keyword arguments, 

1032 ignored if C{B{LatLon} is None}. 

1033 

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

1035 L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}. 

1036 

1037 @note: The C{lat} will be in the range C{[-90..90] degrees} and C{lon} in the 

1038 range C{[-180..180] degrees}. The C{scale} of the projection is C{1} 

1039 in I{radial} direction, C{azimuth} clockwise from true North and is C{1 

1040 / reciprocal} in the direction perpendicular to this. 

1041 ''' 

1042 def _c(c): 

1043 return asin1(c) if c > EPS else None 

1044 

1045 return self._reverse(x, y, _c, False, **name_LatLon_and_kwds) 

1046 

1047 

1048class Stereographic(_AzimuthalBase): 

1049 '''Stereographic projection for the sphere***, see U{Snyder, pp 157-160 

1050 <https://Pubs.USGS.gov/pp/1395/report.pdf>} and U{MathWorld-Wolfram 

1051 <https://MathWorld.Wolfram.com/StereographicProjection.html>}. 

1052 ''' 

1053 _k0 = _1_0 # central scale factor (C{scalar}) 

1054 _k02 = _2_0 # double ._k0 

1055 

1056 if _FOR_DOCS: 

1057 __init__ = _AzimuthalBase.__init__ 

1058 

1059 def forward(self, lat, lon, **name): 

1060 '''Convert a geodetic location to azimuthal stereographic east- and northing. 

1061 

1062 @arg lat: Latitude of the location (C{degrees90}). 

1063 @arg lon: Longitude of the location (C{degrees180}). 

1064 @kwarg name: Optional C{B{name}=NN} for the location (C{str}). 

1065 

1066 @return: An L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)} 

1067 with easting C{x} and northing C{y} of point in C{meter} and C{lat} 

1068 and C{lon} in C{degrees} and C{azimuth} clockwise from true North. 

1069 The C{scale} of the projection is C{1} in I{radial} direction and 

1070 is C{1 / reciprocal} in the direction perpendicular to this. 

1071 

1072 @raise AzimuthalError: Invalid B{C{lat}} or B{C{lon}}. 

1073 ''' 

1074 def _k_t(c): 

1075 c += _1_0 

1076 t = isnon0(c) 

1077 k = (self._k02 / c) if t else _1_0 

1078 return k, t 

1079 

1080 return self._forward(lat, lon, name, _k_t) 

1081 

1082 @property_doc_(''' optional, central scale factor (C{scalar}).''') 

1083 def k0(self): 

1084 '''Get the central scale factor (C{scalar}). 

1085 ''' 

1086 return self._k0 

1087 

1088 @k0.setter # PYCHOK setter! 

1089 def k0(self, factor): 

1090 '''Set the central scale factor (C{scalar}). 

1091 ''' 

1092 n = typename(Stereographic.k0.fget) # 'k0', name__=Stereographic.k0.fget 

1093 self._k0 = Scalar_(factor, name=n, low=EPS, high=2) # XXX high=1, 2, other? 

1094 self._k02 = self._k0 * _2_0 

1095 

1096 def reverse(self, x, y, **name_LatLon_and_kwds): 

1097 '''Convert an azimuthal stereographic location to geodetic lat- and longitude. 

1098 

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

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

1101 @kwarg name_LatLon_and_kwds: Optional C{B{name}=NN} and class C{B{LatLon}=None} 

1102 to use and optionally, additional B{C{LatLon}} keyword arguments, 

1103 ignored if C{B{LatLon} is None}. 

1104 

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

1106 L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}. 

1107 

1108 @note: The C{lat} will be in range C{[-90..90] degrees}, C{lon} in range 

1109 C{[-180..180] degrees} and C{azimuth} clockwise from true North. The 

1110 C{scale} of the projection is C{1} in I{radial} direction and is C{1 

1111 / reciprocal} in the direction perpendicular to this. 

1112 ''' 

1113 def _c(c): 

1114 return (atan2(c, self._k02) * _2_0) if c > EPS else None 

1115 

1116 return self._reverse(x, y, _c, False, **name_LatLon_and_kwds) 

1117 

1118 

1119__all__ += _ALL_DOCS(_AzimuthalBase, _AzimuthalGeodesic, _EquidistantBase, _GnomonicBase) 

1120 

1121# **) MIT License 

1122# 

1123# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved. 

1124# 

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

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

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

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

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

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

1131# 

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

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

1134# 

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

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

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

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

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

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

1141# OTHER DEALINGS IN THE SOFTWARE.