Coverage for pygeodesy/azimuthal.py: 98%

1

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

3

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

5

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

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

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

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

10 and L{gnomonic}.

11

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

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

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

15 classGeographicLib_1_1GeodesicExact.html>}, respectively U{GeodesicLineExact

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

17

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

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

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

21 L{geodsolve} for more details.

22

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

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

25

26 Other 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,

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

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

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

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

32

33 Page 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

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

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

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

39 altitude 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

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

44

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

46 from 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

48 from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase as _LLEB, \

49 _isin, _xinstanceof

50 from pygeodesy.datums import _spherical_datum, _WGS84

51 from pygeodesy.errors import _ValueError, _xattr, _xdatum, _xkwds

52 from 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

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

57 _SPACE_, _x_, _y_

58 from pygeodesy.karney import _norm180, typename

59 from pygeodesy.latlonBase import _MODS, LatLonBase as _LLB

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

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

62 from pygeodesy.namedTuples import LatLon2Tuple, LatLon4Tuple

63 from pygeodesy.props import deprecated_Property_RO, Property_RO, \

64 property_doc_, _update_all

65 from pygeodesy.streprs import Fmt, _fstrLL0, unstr

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

67 Scalar, Scalar_

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

69 sincos2, sincos2d, sincos2d_

70

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

72

73 __all__ = _ALL_LAZY.azimuthal

74 __version__ = '25.11.29'

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

81 def _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

90 class _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):

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(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

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 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)

202

203 @Property_RO

204 def lon0(self):

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

206 '''

207 return self._latlon0.lon

208

209 @deprecated_Property_RO

210 def majoradius(self): # PYCHOK no cover

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

212 return self.equatoradius

213

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)

219

220 def reset(self, lat0, lon0):

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

222

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

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

225

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

227 '''

228 _update_all(self) # zap caches

229 self._reset(lat0, lon0)

230

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)

237

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

241

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)

246

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)

264

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

271

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

279

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

288

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

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

291

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

293

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

295 (C{str}).

296 '''

297 return _fstrLL0(self, prec, True)

298

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

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

301

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

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

304

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)

309

310

311 class 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

317

318

319 class 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)

329

330 def antipodal(self, azimuth=None):

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

332

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)

339

340

341 class 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>}.

345

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__

353

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

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

356

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}).

360

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.

366

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

368

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

381

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

383

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

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

386

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}.

392

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)}.

395

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

404

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

406

407

408 def 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.

412

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}).

421

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

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

424

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

426

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

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

429

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

431 '''

432

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

440

441

442 class _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

449

450 @Property_RO

451 def geodesic(self): # PYCHOK no cover

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

453 self._notOverloaded()

454

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)

464

465

466 class _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)

475

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

479

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

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

482

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}).

486

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.

492

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

494

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))

502

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.

505

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}.

511

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)}.

514

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)

522

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)

526

527

528 class 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}.

532

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.

537

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.

545

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}).

552

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)

556

557 if _FOR_DOCS:

558 forward = _EquidistantBase.forward

559 reverse = _EquidistantBase.reverse

560

561 @Property_RO

562 def geodesic(self):

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

564 '''

565 return self.datum.ellipsoid.geodesicx

566

567

568 class 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}.

573

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

575 '''

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

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

578

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}).

585

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)

589

590 if _FOR_DOCS:

591 forward = _EquidistantBase.forward

592 reverse = _EquidistantBase.reverse

593

594 @Property_RO

595 def geodesic(self):

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

597 '''

598 return self.datum.ellipsoid.geodsolve

599

600

601 class 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.

605

606 @see: L{EquidistantExact}.

607 '''

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

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

610

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}).

617

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

619 not installed or not found.

620

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)

624

625 if _FOR_DOCS:

626 forward = _EquidistantBase.forward

627 reverse = _EquidistantBase.reverse

628

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

637

638

639 _Equidistants = (Equidistant, EquidistantExact, EquidistantGeodSolve,

640 EquidistantKarney) # PYCHOK in .ellipsoidalBaseDI

641

642

643 class 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__

650

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

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

653

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}).

657

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.

663

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

670

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

672

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

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

675

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}.

681

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)}.

684

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

693

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

695

696

697 def 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.

701

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}).

710

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

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

713

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

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

716

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

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

719

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

729

730

731 class _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)

739

740 g = self.geodesic

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

742

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

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

745 and northing.

746

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}).

752

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}.

760

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

776

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

778 t._iteration = self._iteration = 0

779 return t

780

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.

783

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}.

789

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)}.

792

793 @raise AzimuthalError: No convergence.

794

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)

801

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

807

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

812

813 a *= _EPS_K

814 m = self._mask

815 g = self.geodesic

816

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))

829

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

834

835

836 class 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}.

840

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.

846

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}).

853

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

855 '''

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

857

858 if _FOR_DOCS:

859 forward = _GnomonicBase.forward

860 reverse = _GnomonicBase.reverse

861

862 @Property_RO

863 def geodesic(self):

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

865 '''

866 return self.datum.ellipsoid.geodesicx

867

868

869 class 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}.

874

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

876 '''

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

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

879

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}).

886

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

888 '''

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

890

891 if _FOR_DOCS:

892 forward = _GnomonicBase.forward

893 reverse = _GnomonicBase.reverse

894

895 @Property_RO

896 def geodesic(self):

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

898 '''

899 return self.datum.ellipsoid.geodsolve

900

901

902 class 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.

906

907 @see: L{GnomonicExact}.

908 '''

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

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

911

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}).

918

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

920 not installed or not found.

921

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

923 '''

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

925

926 if _FOR_DOCS:

927 forward = _GnomonicBase.forward

928 reverse = _GnomonicBase.reverse

929

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

938

939

940 class 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__

948

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

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

951

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}).

955

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.

961

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

969

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

971

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

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

974

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}.

980

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)}.

983

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

992

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

994

995

996 class 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__

1003

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

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

1006

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}).

1010

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.

1016

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

1018 '''

1019 def _k_t(c):

1020 return _1_0, (c >= 0)

1021

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

1023

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

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

1026

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}.

1032

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)}.

1035

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

1043

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

1045

1046

1047 class 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

1054

1055 if _FOR_DOCS:

1056 __init__ = _AzimuthalBase.__init__

1057

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

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

1060

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}).

1064

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.

1070

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

1078

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

1080

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

1086

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

1094

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

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

1097

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}.

1103

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)}.

1106

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

1114

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

1116

1117

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

1119

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.