Coverage for pygeodesy/azimuthal.py: 98%
318 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-04 12:01 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-04 12:01 -0400
2# -*- coding: utf-8 -*-
4u'''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 _ # PYCHOK semicolon
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_
71from math import acos, degrees, fabs, sin, sqrt
73__all__ = _ALL_LAZY.azimuthal
74__version__ = '25.04.14'
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_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
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
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 if hasattr(latlon0, _datum_):
199 _xdatum(self.datum, latlon0.datum, Error=AzimuthalError)
200 self.reset(latlon0.lat, latlon0.lon)
202 @Property_RO
203 def lon0(self):
204 '''Get the center longitude (C{degrees180}).
205 '''
206 return self._latlon0.lon
208 @deprecated_Property_RO
209 def majoradius(self): # PYCHOK no cover
210 '''DEPRECATED, use property C{equatoradius}.'''
211 return self.equatoradius
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)
219 def reset(self, lat0, lon0):
220 '''Set or reset the center point of this azimuthal projection.
222 @arg lat0: Center point latitude (C{degrees90}).
223 @arg lon0: Center point longitude (C{degrees180}).
225 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}}.
226 '''
227 _update_all(self) # zap caches
228 self._reset(lat0, lon0)
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)
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
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)
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)
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
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
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
288 def toRepr(self, prec=6, **unused): # PYCHOK expected
289 '''Return a string representation of this projection.
291 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).
293 @return: This projection as C{"<classname>(lat0, lon0, ...)"}
294 (C{str}).
295 '''
296 return _fstrLL0(self, prec, True)
298 def toStr(self, prec=6, sep=_SPACE_, **unused): # PYCHOK expected
299 '''Return a string representation of this projection.
301 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).
302 @kwarg sep: Separator to join (C{str}).
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)
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
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)
329 def antipodal(self, azimuth=None):
330 '''Return this tuple with the antipodal C{lat} and C{lon}.
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)
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>}.
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__
353 def forward(self, lat, lon, **name):
354 '''Convert a geodetic location to azimuthal equidistant east- and northing.
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}).
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.
366 @raise AzimuthalError: Invalid B{C{lat}} or B{C{lon}}.
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
381 return self._forward(lat, lon, name, _k_t)
383 def reverse(self, x, y, **name_LatLon_and_kwds):
384 '''Convert an azimuthal equidistant location to geodetic lat- and longitude.
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}.
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)}.
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
404 return self._reverse(x, y, _c, False, **name_LatLon_and_kwds)
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.
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}).
421 @return: An L{EquidistantExact}, L{EquidistantGeodSolve},
422 L{EquidistantKarney} or L{Equidistant} instance.
424 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}} or (spherical) B{C{datum}}.
426 @raise GeodesicError: Issue with L{GeodesicExact}, L{GeodesicSolve}
427 or I{Karney}'s wrapped C{Geodesic}.
429 @raise TypeError: Invalid B{C{datum}}.
430 '''
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
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
449 @Property_RO
450 def geodesic(self): # PYCHOK no cover
451 '''I{Must be overloaded}.'''
452 self._notOverloaded()
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)
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)
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
479 def forward(self, lat, lon, **name):
480 '''Convert an (ellipsoidal) geodetic location to azimuthal equidistant east- and northing.
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}).
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.
492 @raise AzimuthalError: Invalid B{C{lat}} or B{C{lon}}.
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))
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.
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}.
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)}.
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)
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)
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}.
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.
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.
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}).
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)
556 if _FOR_DOCS:
557 forward = _EquidistantBase.forward
558 reverse = _EquidistantBase.reverse
560 @Property_RO
561 def geodesic(self):
562 '''Get this projection's exact geodesic (L{GeodesicExact}).
563 '''
564 return self.datum.ellipsoid.geodesicx
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}.
573 @see: L{EquidistantExact} and module L{geodsolve}.
574 '''
575 def __init__(self, lat0, lon0, datum=_WGS84, **name):
576 '''New azimuthal L{EquidistantGeodSolve} projection.
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}).
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)
589 if _FOR_DOCS:
590 forward = _EquidistantBase.forward
591 reverse = _EquidistantBase.reverse
593 @Property_RO
594 def geodesic(self):
595 '''Get this projection's (exact) geodesic (L{GeodesicSolve}).
596 '''
597 return self.datum.ellipsoid.geodsolve
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.
605 @see: L{EquidistantExact}.
606 '''
607 def __init__(self, lat0, lon0, datum=_WGS84, **name):
608 '''New azimuthal L{EquidistantKarney} projection.
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}).
617 @raise ImportError: Package U{geographiclib<https://PyPI.org/project/geographiclib>}
618 not installed or not found.
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)
624 if _FOR_DOCS:
625 forward = _EquidistantBase.forward
626 reverse = _EquidistantBase.reverse
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
638_Equidistants = (Equidistant, EquidistantExact, EquidistantGeodSolve,
639 EquidistantKarney) # PYCHOK in .ellipsoidalBaseDI
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__
650 def forward(self, lat, lon, **name):
651 '''Convert a geodetic location to azimuthal equidistant east- and northing.
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}).
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.
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
670 return self._forward(lat, lon, name, _k_t)
672 def reverse(self, x, y, **name_LatLon_and_kwds):
673 '''Convert an azimuthal equidistant location to geodetic lat- and longitude.
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}.
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)}.
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
693 return self._reverse(x, y, _c, False, **name_LatLon_and_kwds)
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.
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}).
710 @return: A L{GnomonicExact}, L{GnomonicGeodSolve},
711 L{GnomonicKarney} or L{Gnomonic} instance.
713 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}} or
714 (spherical) B{C{datum}}.
716 @raise GeodesicError: Issue with L{GeodesicExact}, L{GeodesicSolve}
717 or I{Karney}'s wrapped C{Geodesic}.
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
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)
739 g = self.geodesic
740 self._mask = g.ALL # | g.LONG_UNROLL
742 def forward(self, lat, lon, raiser=True, **name): # PYCHOK signature
743 '''Convert an (ellipsoidal) geodetic location to azimuthal gnomonic east-
744 and northing.
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}).
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}.
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
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
778 t = self._7Tuple(e, n, r, _name__(name), M=M)
779 t._iteraton = 0
780 return t
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.
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}.
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)}.
794 @raise AzimuthalError: No convergence.
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)
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
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
814 a *= _EPS_K
815 m = self._mask
816 g = self.geodesic
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))
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
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}.
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.
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}).
855 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}}.
856 '''
857 _GnomonicBase.__init__(self, lat0, lon0, datum=datum, **name)
859 if _FOR_DOCS:
860 forward = _GnomonicBase.forward
861 reverse = _GnomonicBase.reverse
863 @Property_RO
864 def geodesic(self):
865 '''Get this projection's exact geodesic (L{GeodesicExact}).
866 '''
867 return self.datum.ellipsoid.geodesicx
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}.
876 @see: L{GnomonicExact} and module L{geodsolve}.
877 '''
878 def __init__(self, lat0, lon0, datum=_WGS84, **name):
879 '''New azimuthal L{GnomonicGeodSolve} projection.
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}).
888 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}}.
889 '''
890 _GnomonicBase.__init__(self, lat0, lon0, datum=datum, **name)
892 if _FOR_DOCS:
893 forward = _GnomonicBase.forward
894 reverse = _GnomonicBase.reverse
896 @Property_RO
897 def geodesic(self):
898 '''Get this projection's (exact) geodesic (L{GeodesicSolve}).
899 '''
900 return self.datum.ellipsoid.geodsolve
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.
908 @see: L{GnomonicExact}.
909 '''
910 def __init__(self, lat0, lon0, datum=_WGS84, **name):
911 '''New azimuthal L{GnomonicKarney} projection.
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}).
920 @raise ImportError: Package U{geographiclib<https://PyPI.org/project/geographiclib>}
921 not installed or not found.
923 @raise AzimuthalError: Invalid B{C{lat0}} or B{C{lon0}}.
924 '''
925 _GnomonicBase.__init__(self, lat0, lon0, datum=datum, **name)
927 if _FOR_DOCS:
928 forward = _GnomonicBase.forward
929 reverse = _GnomonicBase.reverse
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
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__
950 def forward(self, lat, lon, **name):
951 '''Convert a geodetic location to azimuthal Lambert-equal-area east- and northing.
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}).
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.
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
971 return self._forward(lat, lon, name, _k_t)
973 def reverse(self, x, y, **name_LatLon_and_kwds):
974 '''Convert an azimuthal Lambert-equal-area location to geodetic lat- and longitude.
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}.
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)}.
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
994 return self._reverse(x, y, _c, True, **name_LatLon_and_kwds)
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__
1005 def forward(self, lat, lon, **name):
1006 '''Convert a geodetic location to azimuthal orthographic east- and northing.
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}).
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.
1018 @raise AzimuthalError: Invalid B{C{lat}} or B{C{lon}}.
1019 '''
1020 def _k_t(c):
1021 return _1_0, (c >= 0)
1023 return self._forward(lat, lon, name, _k_t)
1025 def reverse(self, x, y, **name_LatLon_and_kwds):
1026 '''Convert an azimuthal orthographic location to geodetic lat- and longitude.
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}.
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)}.
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
1045 return self._reverse(x, y, _c, False, **name_LatLon_and_kwds)
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
1056 if _FOR_DOCS:
1057 __init__ = _AzimuthalBase.__init__
1059 def forward(self, lat, lon, **name):
1060 '''Convert a geodetic location to azimuthal stereographic east- and northing.
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}).
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.
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
1080 return self._forward(lat, lon, name, _k_t)
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
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
1096 def reverse(self, x, y, **name_LatLon_and_kwds):
1097 '''Convert an azimuthal stereographic location to geodetic lat- and longitude.
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}.
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)}.
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
1116 return self._reverse(x, y, _c, False, **name_LatLon_and_kwds)
1119__all__ += _ALL_DOCS(_AzimuthalBase, _AzimuthalGeodesic, _EquidistantBase, _GnomonicBase)
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.