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 _ # PYCHOK semicolon

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, _xdatum, _xkwds

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

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.04.14'

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

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

152 '''

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

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

155 s0, c0 = self._sc0

156

157 cb *= ca

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

159 if t:

160 r = k * self.radius

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

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

163 else: # 0 or 180

164 e = n = z = _0_0

165

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

167 name=self._name__(name))

168 return t

169

170 def _forwards(self, *lls):

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

172 '''

173 _fwd = self.forward

174 for ll in lls:

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

176

177 @Property_RO

178 def lat0(self):

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

180 '''

181 return self._latlon0.lat

182

183 @property

184 def latlon0(self):

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

186 '''

187 return self._latlon0

188

189 @latlon0.setter # PYCHOK setter!

190 def latlon0(self, latlon0):

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

192

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

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

195 '''

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

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

198 if hasattr(latlon0, _datum_):

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

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

201

202 @Property_RO

203 def lon0(self):

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

205 '''

206 return self._latlon0.lon

207

208 @deprecated_Property_RO

209 def majoradius(self): # PYCHOK no cover

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

211 return self.equatoradius

212

213 @Property_RO

214 def radius(self):

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

216 '''

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

218

219 def reset(self, lat0, lon0):

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

221

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

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

224

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

226 '''

227 _update_all(self) # zap caches

228 self._reset(lat0, lon0)

229

230 def _reset(self, lat0, lon0):

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

232 '''

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

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

235 self._sc0 = sincos2d(self.lat0)

236

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

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

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

240

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

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

243 '''

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

245

246 c = _c(r / self.radius)

247 if c is None:

248 lat, lon = self.latlon0

249 k, z = _1_0, _0_0

250 else:

251 s0, c0 = self._sc0

252 sc, cc = sincos2(c)

253 k = c / sc

254 s = s0 * cc

255 if r > EPS0:

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

257 lat = degrees(asin1(s))

258 if lea or fabs(c0) > EPS:

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

260 else:

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

262 lon = _norm180(self.lon0 + d)

263

264 if LatLon is None:

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

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

267 else:

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

269 return t

270

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

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

273 .ellipsoidalBaseDI._intersects2 and .ellipsoidalBaseDI._nearestOne.

274 '''

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

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

277 return t, d

278

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

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

281 '''

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

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

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

285 _xinstanceof(B, LatLon=r)

286 return r

287

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

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

290

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

292

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

294 (C{str}).

295 '''

296 return _fstrLL0(self, prec, True)

297

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

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

300

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

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

303

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

305 '''

306 t = _fstrLL0(self, prec, False)

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

308

309

310 class AzimuthalError(_ValueError):

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

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

313 L{Azimuthal7Tuple} issue.

314 '''

315 pass

316

317

318 class Azimuthal7Tuple(_NamedTuple):

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

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

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

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

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

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

325 '''

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

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

328

329 def antipodal(self, azimuth=None):

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

331

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

333 (C{compass degrees360}).

334 '''

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

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

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

338

339

340 class Equidistant(_AzimuthalBase):

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

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

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

344

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

346 L{EquidistantGeodSolve} or L{EquidistantKarney} projection

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

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

349 '''

350 if _FOR_DOCS:

351 __init__ = _AzimuthalBase.__init__

352

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

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

355

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

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

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

359

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

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

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

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

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

365

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

367

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

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

370 '''

371 def _k_t(c):

372 k = _N_1_0 if c < 0 else _1_0

373 t = fabs(c) < EPS1

374 if t:

375 a = acos(c)

376 s = sin(a)

377 if s:

378 k = a / s

379 return k, t

380

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

382

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

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

385

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

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

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

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

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

391

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

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

394

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

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

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

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

399 perpendicular to this.

400 '''

401 def _c(c):

402 return c if c > EPS else None

403

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

405

406

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

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

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

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

411

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

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

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

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

416 radius (C{meter}).

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

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

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

420

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

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

423

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

425

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

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

428

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

430 '''

431

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

433 if E is Equidistant:

434 try:

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

436 except ImportError:

437 pass

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

439

440

441 class _AzimuthalGeodesic(_AzimuthalBase):

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

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

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

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

446 '''

447 _mask = 0

448

449 @Property_RO

450 def geodesic(self): # PYCHOK no cover

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

452 self._notOverloaded()

453

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

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

456 '''

457 s = M

458 if s is None: # reciprocal, azimuthal scale

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

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

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

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

463

464

465 class _EquidistantBase(_AzimuthalGeodesic):

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

467 and L{EquidistantKarney}.

468 '''

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

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

471 L{EquidistantKarney} projection.

472 '''

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

474

475 g = self.geodesic

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

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

478

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

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

481

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

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

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

485

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

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

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

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

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

491

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

493

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

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

496 '''

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

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

499 x, y = sincos2d(r.azi1)

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

501

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

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

504

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

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

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

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

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

510

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

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

513

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

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

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

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

518 to this.

519 '''

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

521

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

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

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

525

526

527 class EquidistantExact(_EquidistantBase):

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

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

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

531

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

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

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

535 clockwise from true North.

536

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

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

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

540 geographic and projected coordinates.

541 '''

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

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

544

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

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

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

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

549 radius (C{meter}).

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

551

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

553 '''

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

555

556 if _FOR_DOCS:

557 forward = _EquidistantBase.forward

558 reverse = _EquidistantBase.reverse

559

560 @Property_RO

561 def geodesic(self):

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

563 '''

564 return self.datum.ellipsoid.geodesicx

565

566

567 class EquidistantGeodSolve(_EquidistantBase):

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

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

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

571 I{for testing purposes only}.

572

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

574 '''

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

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

577

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

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

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

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

582 radius (C{meter}).

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

584

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

586 '''

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

588

589 if _FOR_DOCS:

590 forward = _EquidistantBase.forward

591 reverse = _EquidistantBase.reverse

592

593 @Property_RO

594 def geodesic(self):

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

596 '''

597 return self.datum.ellipsoid.geodsolve

598

599

600 class EquidistantKarney(_EquidistantBase):

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

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

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

604

605 @see: L{EquidistantExact}.

606 '''

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

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

609

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

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

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

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

614 radius (C{meter}).

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

616

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

618 not installed or not found.

619

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

621 '''

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

623

624 if _FOR_DOCS:

625 forward = _EquidistantBase.forward

626 reverse = _EquidistantBase.reverse

627

628 @Property_RO

629 def geodesic(self):

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

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

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

633 package is installed.

634 '''

635 return self.datum.ellipsoid.geodesic

636

637

638_Equidistants = (Equidistant, EquidistantExact, EquidistantGeodSolve,

639 EquidistantKarney) # PYCHOK in .ellipsoidalBaseDI

640

641

642 class Gnomonic(_AzimuthalBase):

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

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

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

646 '''

647 if _FOR_DOCS:

648 __init__ = _AzimuthalBase.__init__

649

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

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

652

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

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

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

656

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

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

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

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

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

662

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

664 '''

665 def _k_t(c):

666 t = c > EPS

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

668 return k, t

669

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

671

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

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

674

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

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

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

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

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

680

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

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

683

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

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

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

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

688 perpendicular to this.

689 '''

690 def _c(c):

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

692

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

694

695

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

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

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

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

700

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

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

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

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

705 radius (C{meter}).

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

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

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

709

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

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

712

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

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

715

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

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

718

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

720 '''

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

722 if G is Gnomonic:

723 try:

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

725 except ImportError:

726 pass

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

728

729

730 class _GnomonicBase(_AzimuthalGeodesic):

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

732 and L{GnomonicKarney}.

733 '''

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

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

736 '''

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

738

739 g = self.geodesic

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

741

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

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

744 and northing.

745

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

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

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

749 the location lies over the horizon.

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

751

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

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

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

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

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

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

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

759

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

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

762 '''

763 self._iteration = 0

764

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

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

767 M = r.M21

768 if M > EPS0:

769 q = r.m12 / M # .M12

770 e, n = sincos2d(r.azi1)

771 e *= q

772 n *= q

773 elif raiser: # PYCHOK no cover

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

775 else: # PYCHOK no cover

776 e = n = NAN

777

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

779 t._iteraton = 0

780 return t

781

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

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

784

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

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

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

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

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

790

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

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

793

794 @raise AzimuthalError: No convergence.

795

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

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

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

799 in the direction perpendicular to this.

800 '''

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

802

803 d = a = self.equatoradius

804 s = a * atan1(q, a)

805 if q > a: # PYCHOK no cover

806 def _d(r, q):

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

808

809 q = _1_0 / q

810 else: # little == True

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

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

813

814 a *= _EPS_K

815 m = self._mask

816 g = self.geodesic

817

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

819 _S2 = Fsum(s).fsum2f_

820 _abs = fabs

821 for i in range(1, _TRIPS):

822 r = _P(s, outmask=m)

823 if _abs(d) < a:

824 break

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

826 else: # PYCHOK no cover

827 self._iteration = _TRIPS

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

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

830

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

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

833 t._iteration = self._iteration = i

834 return t

835

836

837 class GnomonicExact(_GnomonicBase):

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

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

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

841

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

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

844 '''

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

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

847

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

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

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

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

852 radius (C{meter}).

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

854

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

856 '''

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

858

859 if _FOR_DOCS:

860 forward = _GnomonicBase.forward

861 reverse = _GnomonicBase.reverse

862

863 @Property_RO

864 def geodesic(self):

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

866 '''

867 return self.datum.ellipsoid.geodesicx

868

869

870 class GnomonicGeodSolve(_GnomonicBase):

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

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

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

874 intended I{for testing purposes only}.

875

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

877 '''

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

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

880

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

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

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

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

885 radius (C{meter}).

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

887

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

889 '''

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

891

892 if _FOR_DOCS:

893 forward = _GnomonicBase.forward

894 reverse = _GnomonicBase.reverse

895

896 @Property_RO

897 def geodesic(self):

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

899 '''

900 return self.datum.ellipsoid.geodsolve

901

902

903 class GnomonicKarney(_GnomonicBase):

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

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

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

907

908 @see: L{GnomonicExact}.

909 '''

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

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

912

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

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

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

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

917 radius (C{meter}).

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

919

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

921 not installed or not found.

922

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

924 '''

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

926

927 if _FOR_DOCS:

928 forward = _GnomonicBase.forward

929 reverse = _GnomonicBase.reverse

930

931 @Property_RO

932 def geodesic(self):

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

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

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

936 package is installed.

937 '''

938 return self.datum.ellipsoid.geodesic

939

940

941 class LambertEqualArea(_AzimuthalBase):

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

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

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

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

946 '''

947 if _FOR_DOCS:

948 __init__ = _AzimuthalBase.__init__

949

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

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

952

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

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

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

956

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

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

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

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

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

962

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

964 '''

965 def _k_t(c):

966 c += _1_0

967 t = c > EPS0

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

969 return k, t

970

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

972

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

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

975

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

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

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

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

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

981

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

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

984

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

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

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

988 / reciprocal} in the direction perpendicular to this.

989 '''

990 def _c(c):

991 c *= _0_5

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

993

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

995

996

997 class Orthographic(_AzimuthalBase):

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

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

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

1001 '''

1002 if _FOR_DOCS:

1003 __init__ = _AzimuthalBase.__init__

1004

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

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

1007

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

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

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

1011

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

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

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

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

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

1017

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

1019 '''

1020 def _k_t(c):

1021 return _1_0, (c >= 0)

1022

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

1024

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

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

1027

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

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

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

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

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

1033

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

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

1036

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

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

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

1040 / reciprocal} in the direction perpendicular to this.

1041 '''

1042 def _c(c):

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

1044

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

1046

1047

1048 class Stereographic(_AzimuthalBase):

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

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

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

1052 '''

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

1054 _k02 = _2_0 # double ._k0

1055

1056 if _FOR_DOCS:

1057 __init__ = _AzimuthalBase.__init__

1058

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

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

1061

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

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

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

1065

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

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

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

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

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

1071

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

1073 '''

1074 def _k_t(c):

1075 c += _1_0

1076 t = isnon0(c)

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

1078 return k, t

1079

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

1081

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

1083 def k0(self):

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

1085 '''

1086 return self._k0

1087

1088 @k0.setter # PYCHOK setter!

1089 def k0(self, factor):

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

1091 '''

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

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

1094 self._k02 = self._k0 * _2_0

1095

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

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

1098

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

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

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

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

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

1104

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

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

1107

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

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

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

1111 / reciprocal} in the direction perpendicular to this.

1112 '''

1113 def _c(c):

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

1115

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

1117

1118

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

1120

1121# **) MIT License

1122#

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

1124#

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

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

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

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

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

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

1131#

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

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

1134#

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

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

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

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

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

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

1141# OTHER DEALINGS IN THE SOFTWARE.