Coverage for pygeodesy/azimuthal.py: 98%
318 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-03-25 15:01 -0400
« prev ^ index » next coverage.py v7.10.7, created at 2026-03-25 15:01 -0400
2# -*- coding: utf-8 -*-
4u'''Equidistant, Equal-Area, and other Azimuthal projections.
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}.
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>}.
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.
23L{EquidistantKarney} and L{GnomonicKarney} require I{Karney}'s Python package
24U{geographiclib<https://PyPI.org/project/geographiclib>} to be installed.
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>}.
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>}.
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 ;
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, _xattr, _xdatum, _xkwds
52from pygeodesy.fmath import euclid, 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_
71from math import acos, degrees, fabs, sin, sqrt
73__all__ = _ALL_LAZY.azimuthal
74__version__ = '25.11.29'
76_EPS_K = _EPStol * _0_1 # Karney's eps_ or _EPSmin * _0_1?
77_over_horizon_ = 'over horizon'
78_TRIPS = 21 # numit, 4 sufficient
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))
90class _AzimuthalBase(_NamedBase):
91 '''(INTERNAL) Base class for azimuthal projections.
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)}
102 def __init__(self, lat0, lon0, datum=None, **name):
103 '''New azimuthal projection.
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}).
112 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}} or (spherical) B{C{datum}}.
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
121 if lat0 or lon0: # often both 0
122 self._reset(lat0, lon0)
124 @Property_RO
125 def datum(self):
126 '''Get the datum (L{Datum}).
127 '''
128 return self._datum
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
136 a = equatoradius
138 @Property_RO
139 def flattening(self):
140 '''Get the geodesic's flattening (C{scalar}).
141 '''
142 return self.datum.ellipsoid.f
144 f = flattening
146 def forward(self, lat, lon, **name): # PYCHOK no cover
147 '''I{Must be overloaded}.'''
148 self._notOverloaded(lat, lon, **name)
150 def _forward(self, lat, lon, name, _k_t):
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
157 cb *= ca
158 k, t = _k_t(c0 * cb + s0 * sa)
159 if t:
160 r = k * self.radius
161 y = r * (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
166 t = Azimuthal7Tuple(e, n, lat, lon, z, k, self.datum,
167 name=self._name__(name))
168 return t
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)
177 @Property_RO
178 def lat0(self):
179 '''Get the center latitude (C{degrees90}).
180 '''
181 return self._latlon0.lat
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
189 @latlon0.setter # PYCHOK setter!
190 def latlon0(self, latlon0):
191 '''Set the center lat- and longitude (C{LatLon}, L{LatLon2Tuple} or L{LatLon4Tuple}).
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 D = _xattr(latlon0, datum=B)
199 if D is not B:
200 _xdatum(self.datum, D, Error=AzimuthalError)
201 self.reset(latlon0.lat, latlon0.lon)
203 @Property_RO
204 def lon0(self):
205 '''Get the center longitude (C{degrees180}).
206 '''
207 return self._latlon0.lon
209 @deprecated_Property_RO
210 def majoradius(self): # PYCHOK no cover
211 '''DEPRECATED, use property C{equatoradius}.'''
212 return self.equatoradius
214 @Property_RO
215 def radius(self):
216 '''Get this projection's mean radius of curvature (C{meter}).
217 '''
218 return self.datum.ellipsoid.rocMean(self.lat0)
220 def reset(self, lat0, lon0):
221 '''Set or reset the center point of this azimuthal projection.
223 @arg lat0: Center point latitude (C{degrees90}).
224 @arg lon0: Center point longitude (C{degrees180}).
226 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}}.
227 '''
228 _update_all(self) # zap caches
229 self._reset(lat0, lon0)
231 def _reset(self, lat0, lon0):
232 '''(INTERNAL) Update the center point.
233 '''
234 self._latlon0 = LatLon2Tuple(Lat_(lat0=lat0, Error=AzimuthalError),
235 Lon_(lon0=lon0, Error=AzimuthalError))
236 self._sc0 = sincos2d(self.lat0)
238 def reverse(self, x, y, **name_LatLon_and_kwds):
239 '''I{Must be overloaded}.'''
240 self._notOverloaded(x, y, **name_LatLon_and_kwds) # PYCHOK no cover
242 def _reverse(self, x, y, _c, lea, LatLon=None, **name_LatLon_kwds):
243 '''(INTERNAL) Azimuthal (spherical) reverse C{x, y} to C{lat, lon}.
244 '''
245 e, n, z, r = _enzh4(x, y)
247 c = _c(r / self.radius)
248 if c is None:
249 lat, lon = self.latlon0
250 k, z = _1_0, _0_0
251 else:
252 s0, c0 = self._sc0
253 sc, cc = sincos2(c)
254 k = c / sc
255 s = s0 * cc
256 if r > EPS0:
257 s += c0 * sc * (n / r)
258 lat = degrees(asin1(s))
259 if lea or fabs(c0) > EPS:
260 d = atan2d(e * sc, r * c0 * cc - n * s0 * sc)
261 else:
262 d = atan2d(e, (n if s0 < 0 else -n))
263 lon = _norm180(self.lon0 + d)
265 if LatLon is None:
266 t, _ = _name2__(name_LatLon_kwds, _or_nameof=self)
267 t = Azimuthal7Tuple(e, n, lat, lon, z, k, self.datum, name=t)
268 else:
269 t = self._toLatLon(lat, lon, LatLon, name_LatLon_kwds)
270 return t
272 def _reverse2(self, x_t, *y):
273 '''(INTERNAL) See iterating functions .ellipsoidalBaseDI._intersect3,
274 .ellipsoidalBaseDI._intersects2 and .ellipsoidalBaseDI._nearestOne.
275 '''
276 t = self.reverse(x_t, *y) if y else self.reverse(x_t.x, x_t.y) # LatLon=None
277 d = euclid(t.lat - self.lat0, t.lon - self.lon0) # degrees
278 return t, d
280 def _toLatLon(self, lat, lon, LatLon, name_LatLon_kwds):
281 '''(INTERNAL) Check B{C{LatLon}} and return an instance.
282 '''
283 kwds = _xkwds(name_LatLon_kwds, datum=self.datum, _or_nameof=self)
284 r = LatLon(lat, lon, **kwds) # handle .classof
285 B = _LLEB if self.datum.isEllipsoidal else _LLB
286 _xinstanceof(B, LatLon=r)
287 return r
289 def toRepr(self, prec=6, **unused): # PYCHOK expected
290 '''Return a string representation of this projection.
292 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).
294 @return: This projection as C{"<classname>(lat0, lon0, ...)"}
295 (C{str}).
296 '''
297 return _fstrLL0(self, prec, True)
299 def toStr(self, prec=6, sep=_SPACE_, **unused): # PYCHOK expected
300 '''Return a string representation of this projection.
302 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).
303 @kwarg sep: Separator to join (C{str}).
305 @return: This projection as C{"lat0 lon0"} (C{str}).
306 '''
307 t = _fstrLL0(self, prec, False)
308 return t if sep is None else sep.join(t)
311class AzimuthalError(_ValueError):
312 '''An azimuthal L{Equidistant}, L{EquidistantKarney}, L{Gnomonic},
313 L{LambertEqualArea}, L{Orthographic}, L{Stereographic} or
314 L{Azimuthal7Tuple} issue.
315 '''
316 pass
319class Azimuthal7Tuple(_NamedTuple):
320 '''7-Tuple C{(x, y, lat, lon, azimuth, scale, datum)}, in C{meter}, C{meter},
321 C{degrees90}, C{degrees180}, compass C{degrees}, C{scalar} and C{Datum}
322 where C{(x, y)} is the easting and northing of a projected point, C{(lat,
323 lon)} the geodetic location, C{azimuth} the azimuth, clockwise from true
324 North and C{scale} is the projection scale, either C{1 / reciprocal} or
325 C{1} or C{-1} in the L{Equidistant} case.
326 '''
327 _Names_ = (_x_, _y_, _lat_, _lon_, _azimuth_, _scale_, _datum_)
328 _Units_ = ( Easting, Northing, Lat_, Lon_, Azimuth, Scalar, _Pass)
330 def antipodal(self, azimuth=None):
331 '''Return this tuple with the antipodal C{lat} and C{lon}.
333 @kwarg azimuth: Optional azimuth, overriding the current azimuth
334 (C{compass degrees360}).
335 '''
336 a = _MODS.formy.antipode(self.lat, self.lon) # PYCHOK named
337 z = self.azimuth if azimuth is None else Azimuth(azimuth) # PYCHOK named
338 return _NamedTuple.dup(self, lat=a.lat, lon=a.lon, azimuth=z)
341class Equidistant(_AzimuthalBase):
342 '''Azimuthal equidistant projection for the sphere***, see U{Snyder, pp 195-197
343 <https://Pubs.USGS.gov/pp/1395/report.pdf>} and U{MathWorld-Wolfram
344 <https://MathWorld.Wolfram.com/AzimuthalEquidistantProjection.html>}.
346 @note: Results from this L{Equidistant} and an L{EquidistantExact},
347 L{EquidistantGeodSolve} or L{EquidistantKarney} projection
348 C{may differ} by 10% or more. For an example, see method
349 C{testDiscrepancies} in module C{testAzimuthal.py}.
350 '''
351 if _FOR_DOCS:
352 __init__ = _AzimuthalBase.__init__
354 def forward(self, lat, lon, **name):
355 '''Convert a geodetic location to azimuthal equidistant east- and northing.
357 @arg lat: Latitude of the location (C{degrees90}).
358 @arg lon: Longitude of the location (C{degrees180}).
359 @kwarg name: Optional C{B{name}=NN} for the location (C{str}).
361 @return: An L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}
362 with easting C{x} and northing C{y} of point in C{meter} and C{lat}
363 and C{lon} in C{degrees} and C{azimuth} clockwise from true North.
364 The C{scale} of the projection is C{1} in I{radial} direction and
365 is C{1 / reciprocal} in the direction perpendicular to this.
367 @raise AzimuthalError: Invalid B{C{lat}} or B{C{lon}}.
369 @note: The C{scale} will be C{-1} if B{C{(lat, lon)}} is antipodal to the
370 projection center C{(lat0, lon0)}.
371 '''
372 def _k_t(c):
373 k = _N_1_0 if c < 0 else _1_0
374 t = fabs(c) < EPS1
375 if t:
376 a = acos(c)
377 s = sin(a)
378 if s:
379 k = a / s
380 return k, t
382 return self._forward(lat, lon, name, _k_t)
384 def reverse(self, x, y, **name_LatLon_and_kwds):
385 '''Convert an azimuthal equidistant location to geodetic lat- and longitude.
387 @arg x: Easting of the location (C{meter}).
388 @arg y: Northing of the location (C{meter}).
389 @kwarg name_LatLon_and_kwds: Optional C{B{name}=NN} and class C{B{LatLon}=None}
390 to use and optionally, additional B{C{LatLon}} keyword arguments,
391 ignored if C{B{LatLon} is None}.
393 @return: The geodetic (C{LatLon}) or if C{B{LatLon} is None} an
394 L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}.
396 @note: The C{lat} will be in the range C{[-90..90] degrees} and C{lon}
397 in the range C{[-180..180] degrees}. The C{scale} of the
398 projection is C{1} in I{radial} direction, C{azimuth} clockwise
399 from true North and is C{1 / reciprocal} in the direction
400 perpendicular to this.
401 '''
402 def _c(c):
403 return c if c > EPS else None
405 return self._reverse(x, y, _c, False, **name_LatLon_and_kwds)
408def equidistant(lat0, lon0, datum=_WGS84, exact=False, geodsolve=False, **name):
409 '''Return an L{EquidistantExact}, L{EquidistantGeodSolve} or (if I{Karney}'s
410 U{geographiclib<https://PyPI.org/project/geographiclib>} package is
411 installed) an L{EquidistantKarney}, otherwise an L{Equidistant} instance.
413 @arg lat0: Latitude of center point (C{degrees90}).
414 @arg lon0: Longitude of center point (C{degrees180}).
415 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},
416 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth
417 radius (C{meter}).
418 @kwarg exact: Return an L{EquidistantExact} instance.
419 @kwarg geodsolve: Return an L{EquidistantGeodSolve} instance.
420 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}).
422 @return: An L{EquidistantExact}, L{EquidistantGeodSolve},
423 L{EquidistantKarney} or L{Equidistant} instance.
425 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}} or (spherical) B{C{datum}}.
427 @raise GeodesicError: Issue with L{GeodesicExact}, L{GeodesicSolve}
428 or I{Karney}'s wrapped C{Geodesic}.
430 @raise TypeError: Invalid B{C{datum}}.
431 '''
433 E = EquidistantExact if exact else (EquidistantGeodSolve if geodsolve else Equidistant)
434 if E is Equidistant:
435 try:
436 return EquidistantKarney(lat0, lon0, datum=datum, **name) # PYCHOK types
437 except ImportError:
438 pass
439 return E(lat0, lon0, datum=datum, **name) # PYCHOK types
442class _AzimuthalGeodesic(_AzimuthalBase):
443 '''(INTERNAL) Base class for azimuthal projections using the
444 I{wrapped} U{geodesic.Geodesic and geodesicline.GeodesicLine
445 <https://GeographicLib.SourceForge.io/Python/doc/code.html>} or the
446 I{exact} geodesic classes L{GeodesicExact} and L{GeodesicLineExact}.
447 '''
448 _mask = 0
450 @Property_RO
451 def geodesic(self): # PYCHOK no cover
452 '''I{Must be overloaded}.'''
453 self._notOverloaded()
455 def _7Tuple(self, e, n, r, name_LatLon_kwds, M=None):
456 '''(INTERNAL) Return an C{Azimuthal7Tuple}.
457 '''
458 s = M
459 if s is None: # reciprocal, azimuthal scale
460 s = (r.m12 / r.s12) if r.a12 > _EPS_K else _1_0
461 z = _umod_360(r.azi2) # -180 <= r.azi2 < 180 ... 0 <= z < 360
462 t, _ = _name2__(name_LatLon_kwds, _or_nameof=self)
463 return Azimuthal7Tuple(e, n, r.lat2, r.lon2, z, s, self.datum, name=t)
466class _EquidistantBase(_AzimuthalGeodesic):
467 '''(INTERNAL) Base for classes L{EquidistantExact}, L{EquidistantGeodSolve}
468 and L{EquidistantKarney}.
469 '''
470 def __init__(self, lat0, lon0, datum=_WGS84, **name):
471 '''New azimuthal L{EquidistantExact}, L{EquidistantGeodSolve} or
472 L{EquidistantKarney} projection.
473 '''
474 _AzimuthalGeodesic.__init__(self, lat0, lon0, datum=datum, **name)
476 g = self.geodesic
477 # g.STANDARD = g.AZIMUTH | g.DISTANCE | g.LATITUDE | g.LONGITUDE
478 self._mask = g.REDUCEDLENGTH | g.STANDARD # | g.LONG_UNROLL
480 def forward(self, lat, lon, **name):
481 '''Convert an (ellipsoidal) geodetic location to azimuthal equidistant east- and northing.
483 @arg lat: Latitude of the location (C{degrees90}).
484 @arg lon: Longitude of the location (C{degrees180}).
485 @kwarg name: Optional C{B{name}=NN} for the location (C{str}).
487 @return: An L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}
488 with easting C{x} and northing C{y} of point in C{meter} and C{lat}
489 and C{lon} in C{degrees} and C{azimuth} clockwise from true North.
490 The C{scale} of the projection is C{1} in I{radial} direction and
491 is C{1 / reciprocal} in the direction perpendicular to this.
493 @raise AzimuthalError: Invalid B{C{lat}} or B{C{lon}}.
495 @note: A call to C{.forward} followed by a call to C{.reverse} will return
496 the original C{lat, lon} to within roundoff.
497 '''
498 r = self.geodesic.Inverse(self.lat0, self.lon0,
499 Lat_(lat), Lon_(lon), outmask=self._mask)
500 x, y = sincos2d(r.azi1)
501 return self._7Tuple(x * r.s12, y * r.s12, r, _name__(name))
503 def reverse(self, x, y, LatLon=None, **name_LatLon_kwds): # PYCHOK signature
504 '''Convert an azimuthal equidistant location to (ellipsoidal) geodetic lat- and longitude.
506 @arg x: Easting of the location (C{meter}).
507 @arg y: Northing of the location (C{meter}).
508 @kwarg LatLon: Class to use (C{LatLon}) or C{None}.
509 @kwarg name_LatLon_kwds: Optional C{B{name}=NN} and optionally, additional
510 B{C{LatLon}} keyword arguments, ignored if C{B{LatLon} is None}.
512 @return: The geodetic (C{LatLon}) or if C{B{LatLon} is None} an
513 L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}.
515 @note: The C{lat} will be in the range C{[-90..90] degrees} and C{lon}
516 in the range C{[-180..180] degrees}. The scale of the projection
517 is C{1} in I{radial} direction, C{azimuth} clockwise from true
518 North and is C{1 / reciprocal} in the direction perpendicular
519 to this.
520 '''
521 e, n, z, s = _enzh4(x, y)
523 r = self.geodesic.Direct(self.lat0, self.lon0, z, s, outmask=self._mask)
524 return self._7Tuple(e, n, r, name_LatLon_kwds) if LatLon is None else \
525 self._toLatLon(r.lat2, r.lon2, LatLon, name_LatLon_kwds)
528class EquidistantExact(_EquidistantBase):
529 '''Azimuthal equidistant projection, a Python version of I{Karney}'s C++ class U{AzimuthalEquidistant
530 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AzimuthalEquidistant.html>},
531 based on exact geodesic classes L{GeodesicExact} and L{GeodesicLineExact}.
533 An azimuthal equidistant projection is centered at an arbitrary position on the ellipsoid.
534 For a point in projected space C{(x, y)}, the geodesic distance from the center position
535 is C{hypot(x, y)} and the C{azimuth} of the geodesic from the center point is C{atan2(x, y)},
536 clockwise from true North.
538 The C{.forward} and C{.reverse} methods also return the C{azimuth} of the geodesic at C{(x,
539 y)} and the C{scale} in the azimuthal direction which, together with the basic properties
540 of the projection, serve to specify completely the local affine transformation between
541 geographic and projected coordinates.
542 '''
543 def __init__(self, lat0, lon0, datum=_WGS84, **name):
544 '''New azimuthal L{EquidistantExact} projection.
546 @arg lat0: Latitude of center point (C{degrees90}).
547 @arg lon0: Longitude of center point (C{degrees180}).
548 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},
549 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth
550 radius (C{meter}).
551 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}).
553 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}} or B{C{datum}}.
554 '''
555 _EquidistantBase.__init__(self, lat0, lon0, datum=datum, **name)
557 if _FOR_DOCS:
558 forward = _EquidistantBase.forward
559 reverse = _EquidistantBase.reverse
561 @Property_RO
562 def geodesic(self):
563 '''Get this projection's exact geodesic (L{GeodesicExact}).
564 '''
565 return self.datum.ellipsoid.geodesicx
568class EquidistantGeodSolve(_EquidistantBase):
569 '''Azimuthal equidistant projection, a Python version of I{Karney}'s C++ class U{AzimuthalEquidistant
570 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AzimuthalEquidistant.html>},
571 based on (exact) geodesic I{wrappers} L{GeodesicSolve} and L{GeodesicLineSolve} and intended
572 I{for testing purposes only}.
574 @see: L{EquidistantExact} and module L{geodsolve}.
575 '''
576 def __init__(self, lat0, lon0, datum=_WGS84, **name):
577 '''New azimuthal L{EquidistantGeodSolve} projection.
579 @arg lat0: Latitude of center point (C{degrees90}).
580 @arg lon0: Longitude of center point (C{degrees180}).
581 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},
582 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth
583 radius (C{meter}).
584 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}).
586 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}} or B{C{datum}}.
587 '''
588 _EquidistantBase.__init__(self, lat0, lon0, datum=datum, **name)
590 if _FOR_DOCS:
591 forward = _EquidistantBase.forward
592 reverse = _EquidistantBase.reverse
594 @Property_RO
595 def geodesic(self):
596 '''Get this projection's (exact) geodesic (L{GeodesicSolve}).
597 '''
598 return self.datum.ellipsoid.geodsolve
601class EquidistantKarney(_EquidistantBase):
602 '''Azimuthal equidistant projection, a Python version of I{Karney}'s C++ class U{AzimuthalEquidistant
603 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AzimuthalEquidistant.html>},
604 requiring package U{geographiclib<https://PyPI.org/project/geographiclib>} to be installed.
606 @see: L{EquidistantExact}.
607 '''
608 def __init__(self, lat0, lon0, datum=_WGS84, **name):
609 '''New azimuthal L{EquidistantKarney} projection.
611 @arg lat0: Latitude of center point (C{degrees90}).
612 @arg lon0: Longitude of center point (C{degrees180}).
613 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},
614 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth
615 radius (C{meter}).
616 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}).
618 @raise ImportError: Package U{geographiclib<https://PyPI.org/project/geographiclib>}
619 not installed or not found.
621 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}} or B{C{datum}}.
622 '''
623 _EquidistantBase.__init__(self, lat0, lon0, datum=datum, **name)
625 if _FOR_DOCS:
626 forward = _EquidistantBase.forward
627 reverse = _EquidistantBase.reverse
629 @Property_RO
630 def geodesic(self):
631 '''Get this projection's I{wrapped} U{geodesic.Geodesic
632 <https://GeographicLib.SourceForge.io/Python/doc/code.html>}, provided
633 I{Karney}'s U{geographiclib<https://PyPI.org/project/geographiclib>}
634 package is installed.
635 '''
636 return self.datum.ellipsoid.geodesic
639_Equidistants = (Equidistant, EquidistantExact, EquidistantGeodSolve,
640 EquidistantKarney) # PYCHOK in .ellipsoidalBaseDI
643class Gnomonic(_AzimuthalBase):
644 '''Azimuthal gnomonic projection for the sphere***, see U{Snyder, pp 164-168
645 <https://Pubs.USGS.gov/pp/1395/report.pdf>} and U{MathWorld-Wolfram
646 <https://MathWorld.Wolfram.com/GnomonicProjection.html>}.
647 '''
648 if _FOR_DOCS:
649 __init__ = _AzimuthalBase.__init__
651 def forward(self, lat, lon, **name):
652 '''Convert a geodetic location to azimuthal equidistant east- and northing.
654 @arg lat: Latitude of the location (C{degrees90}).
655 @arg lon: Longitude of the location (C{degrees180}).
656 @kwarg name: Optional C{B{name}=NN} for the location (C{str}).
658 @return: An L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}
659 with easting C{x} and northing C{y} of point in C{meter} and C{lat}
660 and C{lon} in C{degrees} and C{azimuth} clockwise from true North.
661 The C{scale} of the projection is C{1} in I{radial} direction and
662 is C{1 / reciprocal} in the direction perpendicular to this.
664 @raise AzimuthalError: Invalid B{C{lat}} or B{C{lon}}.
665 '''
666 def _k_t(c):
667 t = c > EPS
668 k = (_1_0 / c) if t else _1_0
669 return k, t
671 return self._forward(lat, lon, name, _k_t)
673 def reverse(self, x, y, **name_LatLon_and_kwds):
674 '''Convert an azimuthal equidistant location to geodetic lat- and longitude.
676 @arg x: Easting of the location (C{meter}).
677 @arg y: Northing of the location (C{meter}).
678 @kwarg name_LatLon_and_kwds: Optional C{B{name}=NN} and class C{B{LatLon}=None}
679 for the location and optionally, additional B{C{LatLon}} keyword
680 arguments, ignored if C{B{LatLon} is None}.
682 @return: The geodetic (C{LatLon}) or if C{B{LatLon} is None} an
683 L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}.
685 @note: The C{lat} will be in the range C{[-90..90] degrees} and C{lon}
686 in the range C{[-180..180] degrees}. The C{scale} of the
687 projection is C{1} in I{radial} direction, C{azimuth} clockwise
688 from true North and C{1 / reciprocal} in the direction
689 perpendicular to this.
690 '''
691 def _c(c):
692 return atan1(c) if c > EPS else None
694 return self._reverse(x, y, _c, False, **name_LatLon_and_kwds)
697def gnomonic(lat0, lon0, datum=_WGS84, exact=False, geodsolve=False, **name):
698 '''Return a L{GnomonicExact} or (if I{Karney}'s U{geographiclib
699 <https://PyPI.org/project/geographiclib>} package is installed)
700 a L{GnomonicKarney}, otherwise a L{Gnomonic} instance.
702 @arg lat0: Latitude of center point (C{degrees90}).
703 @arg lon0: Longitude of center point (C{degrees180}).
704 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},
705 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth
706 radius (C{meter}).
707 @kwarg exact: Return a L{GnomonicExact} instance.
708 @kwarg geodsolve: Return a L{GnomonicGeodSolve} instance.
709 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}).
711 @return: A L{GnomonicExact}, L{GnomonicGeodSolve},
712 L{GnomonicKarney} or L{Gnomonic} instance.
714 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}} or
715 (spherical) B{C{datum}}.
717 @raise GeodesicError: Issue with L{GeodesicExact}, L{GeodesicSolve}
718 or I{Karney}'s wrapped C{Geodesic}.
720 @raise TypeError: Invalid B{C{datum}}.
721 '''
722 G = GnomonicExact if exact else (GnomonicGeodSolve if geodsolve else Gnomonic)
723 if G is Gnomonic:
724 try:
725 return GnomonicKarney(lat0, lon0, datum=datum, **name) # PYCHOK types
726 except ImportError:
727 pass
728 return G(lat0, lon0, datum=datum, **name) # PYCHOK types
731class _GnomonicBase(_AzimuthalGeodesic):
732 '''(INTERNAL) Base for classes L{GnomonicExact}, L{GnomonicGeodSolve}
733 and L{GnomonicKarney}.
734 '''
735 def __init__(self, lat0, lon0, datum=_WGS84, **name):
736 '''New azimuthal L{GnomonicExact} or L{GnomonicKarney} projection.
737 '''
738 _AzimuthalGeodesic.__init__(self, lat0, lon0, datum=datum, **name)
740 g = self.geodesic
741 self._mask = g.ALL # | g.LONG_UNROLL
743 def forward(self, lat, lon, raiser=True, **name): # PYCHOK signature
744 '''Convert an (ellipsoidal) geodetic location to azimuthal gnomonic east-
745 and northing.
747 @arg lat: Latitude of the location (C{degrees90}).
748 @arg lon: Longitude of the location (C{degrees180}).
749 @kwarg raiser: If C{False}, do not throw an error if the location lies
750 over the horizon (C{bool}).
751 @kwarg name: Optional C{B{name}=NN} for the location (C{str}).
753 @return: An L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}
754 with easting C{x} and northing C{y} in C{meter} and C{lat} and
755 C{lon} in C{degrees} and C{azimuth} clockwise from true North.
756 The C{scale} of the projection is C{1 / reciprocal**2} in I{radial}
757 direction and C{1 / reciprocal} in the direction perpendicular to
758 this. Both C{x} and C{y} will be C{NAN} if the (geodetic) location
759 lies over the horizon and C{B{raiser}=False}.
761 @raise AzimuthalError: Invalid B{C{lat}}, B{C{lon}} or the location lies
762 over the horizon and C{B{raiser}=True}.
763 '''
764 r = self.geodesic.Inverse(self.lat0, self.lon0,
765 Lat_(lat), Lon_(lon), outmask=self._mask)
766 M = r.M21
767 if M > EPS0:
768 q = r.m12 / M # .M12
769 e, n = sincos2d(r.azi1)
770 e *= q
771 n *= q
772 elif raiser: # PYCHOK no cover
773 raise AzimuthalError(lat=lat, lon=lon, txt=_over_horizon_)
774 else: # PYCHOK no cover
775 e = n = NAN
777 t = self._7Tuple(e, n, r, _name__(name), M=M)
778 t._iteration = self._iteration = 0
779 return t
781 def reverse(self, x, y, LatLon=None, **name_LatLon_kwds): # PYCHOK signature
782 '''Convert an azimuthal gnomonic location to (ellipsoidal) geodetic lat- and longitude.
784 @arg x: Easting of the location (C{meter}).
785 @arg y: Northing of the location (C{meter}).
786 @kwarg LatLon: Class to use (C{LatLon}) or C{None}.
787 @kwarg name_LatLon_kwds: Optional C{B{name}=NN} for the location and optionally,
788 additional B{C{LatLon}} keyword arguments, ignored if C{B{LatLon} is None}.
790 @return: The geodetic (C{LatLon}) or if C{B{LatLon} is None} an
791 L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}.
793 @raise AzimuthalError: No convergence.
795 @note: The C{lat} will be in the range C{[-90..90] degrees} and C{lon} in the range
796 C{[-180..180] degrees}. The C{azimuth} is clockwise from true North. The
797 scale is C{1 / reciprocal**2} in C{radial} direction and C{1 / reciprocal}
798 in the direction perpendicular to this.
799 '''
800 e, n, z, q = _enzh4(x, y)
802 d = a = self.equatoradius
803 s = a * atan1(q, a)
804 if q > a: # PYCHOK no cover
805 def _d(r, q):
806 return (r.M12 - q * r.m12) * r.m12 # negated
808 q = _1_0 / q
809 else: # little == True
810 def _d(r, q): # PYCHOK _d
811 return (q * r.M12 - r.m12) * r.M12 # negated
813 a *= _EPS_K
814 m = self._mask
815 g = self.geodesic
817 _P = g.Line(self.lat0, self.lon0, z, caps=m | g.LINE_OFF).Position
818 _S2 = Fsum(s).fsum2f_
819 _abs = fabs
820 for i in range(1, _TRIPS):
821 r = _P(s, outmask=m)
822 if _abs(d) < a:
823 break
824 s, d = _S2(_d(r, q))
825 else: # PYCHOK no cover
826 self._iteration = _TRIPS
827 raise AzimuthalError(Fmt.no_convergence(d, a),
828 txt=unstr(self.reverse, x, y))
830 t = self._7Tuple(e, n, r, name_LatLon_kwds, M=r.M12) if LatLon is None else \
831 self._toLatLon(r.lat2, r.lon2, LatLon, name_LatLon_kwds)
832 t._iteration = self._iteration = i
833 return t
836class GnomonicExact(_GnomonicBase):
837 '''Azimuthal gnomonic projection, a Python version of I{Karney}'s C++ class U{Gnomonic
838 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Gnomonic.html>},
839 based on exact geodesic classes L{GeodesicExact} and L{GeodesicLineExact}.
841 @see: I{Karney}'s U{Detailed Description<https://GeographicLib.SourceForge.io/C++/doc/
842 classGeographicLib_1_1Gnomonic.html>}, especially the B{Warning}.
843 '''
844 def __init__(self, lat0, lon0, datum=_WGS84, **name):
845 '''New azimuthal L{GnomonicExact} projection.
847 @arg lat0: Latitude of center point (C{degrees90}).
848 @arg lon0: Longitude of center point (C{degrees180}).
849 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},
850 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth
851 radius (C{meter}).
852 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}).
854 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}}.
855 '''
856 _GnomonicBase.__init__(self, lat0, lon0, datum=datum, **name)
858 if _FOR_DOCS:
859 forward = _GnomonicBase.forward
860 reverse = _GnomonicBase.reverse
862 @Property_RO
863 def geodesic(self):
864 '''Get this projection's exact geodesic (L{GeodesicExact}).
865 '''
866 return self.datum.ellipsoid.geodesicx
869class GnomonicGeodSolve(_GnomonicBase):
870 '''Azimuthal gnomonic projection, a Python version of I{Karney}'s C++ class U{Gnomonic
871 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Gnomonic.html>},
872 based on (exact) geodesic I{wrappers} L{GeodesicSolve} and L{GeodesicLineSolve} and
873 intended I{for testing purposes only}.
875 @see: L{GnomonicExact} and module L{geodsolve}.
876 '''
877 def __init__(self, lat0, lon0, datum=_WGS84, **name):
878 '''New azimuthal L{GnomonicGeodSolve} projection.
880 @arg lat0: Latitude of center point (C{degrees90}).
881 @arg lon0: Longitude of center point (C{degrees180}).
882 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},
883 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth
884 radius (C{meter}).
885 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}).
887 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}}.
888 '''
889 _GnomonicBase.__init__(self, lat0, lon0, datum=datum, **name)
891 if _FOR_DOCS:
892 forward = _GnomonicBase.forward
893 reverse = _GnomonicBase.reverse
895 @Property_RO
896 def geodesic(self):
897 '''Get this projection's (exact) geodesic (L{GeodesicSolve}).
898 '''
899 return self.datum.ellipsoid.geodsolve
902class GnomonicKarney(_GnomonicBase):
903 '''Azimuthal gnomonic projection, a Python version of I{Karney}'s C++ class U{Gnomonic
904 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Gnomonic.html>},
905 requiring package U{geographiclib<https://PyPI.org/project/geographiclib>} to be installed.
907 @see: L{GnomonicExact}.
908 '''
909 def __init__(self, lat0, lon0, datum=_WGS84, **name):
910 '''New azimuthal L{GnomonicKarney} projection.
912 @arg lat0: Latitude of center point (C{degrees90}).
913 @arg lon0: Longitude of center point (C{degrees180}).
914 @kwarg datum: Optional datum or ellipsoid (L{Datum}, L{Ellipsoid},
915 L{Ellipsoid2} or L{a_f2Tuple}) or I{scalar} earth
916 radius (C{meter}).
917 @kwarg name: Optional C{B{name}=NN} for the projection (C{str}).
919 @raise ImportError: Package U{geographiclib<https://PyPI.org/project/geographiclib>}
920 not installed or not found.
922 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}}.
923 '''
924 _GnomonicBase.__init__(self, lat0, lon0, datum=datum, **name)
926 if _FOR_DOCS:
927 forward = _GnomonicBase.forward
928 reverse = _GnomonicBase.reverse
930 @Property_RO
931 def geodesic(self):
932 '''Get this projection's I{wrapped} U{geodesic.Geodesic
933 <https://GeographicLib.SourceForge.io/Python/doc/code.html>}, provided
934 I{Karney}'s U{geographiclib<https://PyPI.org/project/geographiclib>}
935 package is installed.
936 '''
937 return self.datum.ellipsoid.geodesic
940class LambertEqualArea(_AzimuthalBase):
941 '''Lambert-equal-area projection for the sphere*** (aka U{Lambert zenithal equal-area
942 projection<https://WikiPedia.org/wiki/Lambert_azimuthal_equal-area_projection>}, see
943 U{Snyder, pp 185-187<https://Pubs.USGS.gov/pp/1395/report.pdf>} and U{MathWorld-Wolfram
944 <https://MathWorld.Wolfram.com/LambertAzimuthalEqual-AreaProjection.html>}.
945 '''
946 if _FOR_DOCS:
947 __init__ = _AzimuthalBase.__init__
949 def forward(self, lat, lon, **name):
950 '''Convert a geodetic location to azimuthal Lambert-equal-area east- and northing.
952 @arg lat: Latitude of the location (C{degrees90}).
953 @arg lon: Longitude of the location (C{degrees180}).
954 @kwarg name: Optional C{B{name}=NN} for the location (C{str}).
956 @return: An L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}
957 with easting C{x} and northing C{y} of point in C{meter} and C{lat}
958 and C{lon} in C{degrees} and C{azimuth} clockwise from true North.
959 The C{scale} of the projection is C{1} in I{radial} direction and
960 is C{1 / reciprocal} in the direction perpendicular to this.
962 @raise AzimuthalError: Invalid B{C{lat}} or B{C{lon}}.
963 '''
964 def _k_t(c):
965 c += _1_0
966 t = c > EPS0
967 k = sqrt(_2_0 / c) if t else _1_0
968 return k, t
970 return self._forward(lat, lon, name, _k_t)
972 def reverse(self, x, y, **name_LatLon_and_kwds):
973 '''Convert an azimuthal Lambert-equal-area location to geodetic lat- and longitude.
975 @arg x: Easting of the location (C{meter}).
976 @arg y: Northing of the location (C{meter}).
977 @kwarg name_LatLon_and_kwds: Optional C{B{name}=NN} and class C{B{LatLon}=None}
978 to use and optionally, additional B{C{LatLon}} keyword arguments,
979 ignored if C{B{LatLon} is None}.
981 @return: The geodetic (C{LatLon}) or if C{B{LatLon} is None} an
982 L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}.
984 @note: The C{lat} will be in the range C{[-90..90] degrees} and C{lon} in the
985 range C{[-180..180] degrees}. The C{scale} of the projection is C{1}
986 in I{radial} direction, C{azimuth} clockwise from true North and is C{1
987 / reciprocal} in the direction perpendicular to this.
988 '''
989 def _c(c):
990 c *= _0_5
991 return (asin1(c) * _2_0) if c > EPS else None
993 return self._reverse(x, y, _c, True, **name_LatLon_and_kwds)
996class Orthographic(_AzimuthalBase):
997 '''Orthographic projection for the sphere***, see U{Snyder, pp 148-153
998 <https://Pubs.USGS.gov/pp/1395/report.pdf>} and U{MathWorld-Wolfram
999 <https://MathWorld.Wolfram.com/OrthographicProjection.html>}.
1000 '''
1001 if _FOR_DOCS:
1002 __init__ = _AzimuthalBase.__init__
1004 def forward(self, lat, lon, **name):
1005 '''Convert a geodetic location to azimuthal orthographic east- and northing.
1007 @arg lat: Latitude of the location (C{degrees90}).
1008 @arg lon: Longitude of the location (C{degrees180}).
1009 @kwarg name: Optional C{B{name}=NN} for the location (C{str}).
1011 @return: An L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}
1012 with easting C{x} and northing C{y} of point in C{meter} and C{lat}
1013 and C{lon} in C{degrees} and C{azimuth} clockwise from true North.
1014 The C{scale} of the projection is C{1} in I{radial} direction and
1015 is C{1 / reciprocal} in the direction perpendicular to this.
1017 @raise AzimuthalError: Invalid B{C{lat}} or B{C{lon}}.
1018 '''
1019 def _k_t(c):
1020 return _1_0, (c >= 0)
1022 return self._forward(lat, lon, name, _k_t)
1024 def reverse(self, x, y, **name_LatLon_and_kwds):
1025 '''Convert an azimuthal orthographic location to geodetic lat- and longitude.
1027 @arg x: Easting of the location (C{meter}).
1028 @arg y: Northing of the location (C{meter}).
1029 @kwarg name_LatLon_and_kwds: Optional C{B{name}=NN} and class C{B{LatLon}=None}
1030 to use and optionally, additional B{C{LatLon}} keyword arguments,
1031 ignored if C{B{LatLon} is None}.
1033 @return: The geodetic (C{LatLon}) or if C{B{LatLon} is None} an
1034 L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}.
1036 @note: The C{lat} will be in the range C{[-90..90] degrees} and C{lon} in the
1037 range C{[-180..180] degrees}. The C{scale} of the projection is C{1}
1038 in I{radial} direction, C{azimuth} clockwise from true North and is C{1
1039 / reciprocal} in the direction perpendicular to this.
1040 '''
1041 def _c(c):
1042 return asin1(c) if c > EPS else None
1044 return self._reverse(x, y, _c, False, **name_LatLon_and_kwds)
1047class Stereographic(_AzimuthalBase):
1048 '''Stereographic projection for the sphere***, see U{Snyder, pp 157-160
1049 <https://Pubs.USGS.gov/pp/1395/report.pdf>} and U{MathWorld-Wolfram
1050 <https://MathWorld.Wolfram.com/StereographicProjection.html>}.
1051 '''
1052 _k0 = _1_0 # central scale factor (C{scalar})
1053 _k02 = _2_0 # double ._k0
1055 if _FOR_DOCS:
1056 __init__ = _AzimuthalBase.__init__
1058 def forward(self, lat, lon, **name):
1059 '''Convert a geodetic location to azimuthal stereographic east- and northing.
1061 @arg lat: Latitude of the location (C{degrees90}).
1062 @arg lon: Longitude of the location (C{degrees180}).
1063 @kwarg name: Optional C{B{name}=NN} for the location (C{str}).
1065 @return: An L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}
1066 with easting C{x} and northing C{y} of point in C{meter} and C{lat}
1067 and C{lon} in C{degrees} and C{azimuth} clockwise from true North.
1068 The C{scale} of the projection is C{1} in I{radial} direction and
1069 is C{1 / reciprocal} in the direction perpendicular to this.
1071 @raise AzimuthalError: Invalid B{C{lat}} or B{C{lon}}.
1072 '''
1073 def _k_t(c):
1074 c += _1_0
1075 t = isnon0(c)
1076 k = (self._k02 / c) if t else _1_0
1077 return k, t
1079 return self._forward(lat, lon, name, _k_t)
1081 @property_doc_(''' optional, central scale factor (C{scalar}).''')
1082 def k0(self):
1083 '''Get the central scale factor (C{scalar}).
1084 '''
1085 return self._k0
1087 @k0.setter # PYCHOK setter!
1088 def k0(self, factor):
1089 '''Set the central scale factor (C{scalar}).
1090 '''
1091 n = typename(Stereographic.k0.fget) # 'k0', name__=Stereographic.k0.fget
1092 self._k0 = Scalar_(factor, name=n, low=EPS, high=2) # XXX high=1, 2, other?
1093 self._k02 = self._k0 * _2_0
1095 def reverse(self, x, y, **name_LatLon_and_kwds):
1096 '''Convert an azimuthal stereographic location to geodetic lat- and longitude.
1098 @arg x: Easting of the location (C{meter}).
1099 @arg y: Northing of the location (C{meter}).
1100 @kwarg name_LatLon_and_kwds: Optional C{B{name}=NN} and class C{B{LatLon}=None}
1101 to use and optionally, additional B{C{LatLon}} keyword arguments,
1102 ignored if C{B{LatLon} is None}.
1104 @return: The geodetic (C{LatLon}) or if C{B{LatLon} is None} an
1105 L{Azimuthal7Tuple}C{(x, y, lat, lon, azimuth, scale, datum)}.
1107 @note: The C{lat} will be in range C{[-90..90] degrees}, C{lon} in range
1108 C{[-180..180] degrees} and C{azimuth} clockwise from true North. The
1109 C{scale} of the projection is C{1} in I{radial} direction and is C{1
1110 / reciprocal} in the direction perpendicular to this.
1111 '''
1112 def _c(c):
1113 return (atan2(c, self._k02) * _2_0) if c > EPS else None
1115 return self._reverse(x, y, _c, False, **name_LatLon_and_kwds)
1118__all__ += _ALL_DOCS(_AzimuthalBase, _AzimuthalGeodesic, _EquidistantBase, _GnomonicBase)
1120# **) MIT License
1121#
1122# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.
1123#
1124# Permission is hereby granted, free of charge, to any person obtaining a
1125# copy of this software and associated documentation files (the "Software"),
1126# to deal in the Software without restriction, including without limitation
1127# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1128# and/or sell copies of the Software, and to permit persons to whom the
1129# Software is furnished to do so, subject to the following conditions:
1130#
1131# The above copyright notice and this permission notice shall be included
1132# in all copies or substantial portions of the Software.
1133#
1134# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1135# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1136# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1137# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1138# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1139# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1140# OTHER DEALINGS IN THE SOFTWARE.