Coverage for pygeodesy/utily.py: 94%

333 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-09-09 13:03 -0400

1 

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

3 

4u'''Various utility functions. 

5 

6After I{Karney}'s C++ U{Math<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Math.html>} 

7class and I{Veness}' U{Latitude/Longitude<https://www.Movable-Type.co.UK/scripts/latlong.html>} 

8and U{Vector-based geodesy<https://www.Movable-Type.co.UK/scripts/latlong-vectors.html>} 

9and published under the same MIT Licence**. 

10''' 

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

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

13 

14from pygeodesy.basics import _copysign, isinstanceof, isint, isstr 

15from pygeodesy.constants import EPS, EPS0, NAN, PI, PI2, PI_2, PI_4, PI_6, R_M, \ 

16 _M_KM, _M_NM, _M_SM, _0_0, _0_5, _1_0, _N_1_0, \ 

17 _10_0, _90_0, _180_0, _360_0, _copysign_0_0, \ 

18 _copysignINF, _float, _isfinite, isnan, isnear0, \ 

19 _over_1, _umod_360, _umod_PI2 

20from pygeodesy.errors import _ValueError, _xkwds, _xkwds_get 

21from pygeodesy.internals import _Enum, _passargs, typename 

22from pygeodesy.interns import _edge_, _radians_, _semi_circular_, _SPACE_ 

23from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS 

24from pygeodesy.units import Degrees, Degrees_, Feet, Float, Lam, Lamd, \ 

25 Meter, Meter2, Radians # Radians_ 

26 

27from math import acos, asin, asinh, atan2 as _atan2, cos, degrees, fabs, \ 

28 radians, sin, sinh, tan as _tan # pow 

29 

30__all__ = _ALL_LAZY.utily 

31__version__ = '25.09.09' 

32 

33# sqrt(3) <https://WikiPedia.org/wiki/Square_root_of_3> 

34_COS_30, _SIN_30 = 0.86602540378443864676, _0_5 # sqrt(3) / 2 

35_COS_45 = _SIN_45 = 0.70710678118654752440 # sqrt(2) / 2 

36 

37_G = _Enum( # grades per ... 

38 DEG = _float( 400.0 / _360_0), # degree 

39 RAD = _float( 400.0 / PI2)) # radian 

40 

41_M = _Enum( # meter per ... 

42 ACRE = _float( 4046.8564224), # acre, chain2m(1) * furlong2m(1), squared 

43 CHAIN = _float( 20.1168), # yard2m(1) * 22 

44 FATHOM = _float( 1.8288), # yard2m(1) * 2 or _M.NM * 1e-3 

45 FOOT = _float( 0.3048), # Int'l foot, 1 / 3.280_839_895_0131 = 10_000 / (254 * 12) 

46 FOOT_GE = _float( 0.31608), # German Fuss, 1 / 3.163_756_011_1364 

47 FOOT_FR = _float( 0.3248406), # French Pied-du-Roi or pied, 1 / 3.078_432_929_8739 

48 FOOT_US = _float( 0.3048006096012192), # US Survey foot, 1_200 / 3_937 

49 FURLONG = _float( 201.168), # furlong, 220 * yard2m(1) = 10 * m2chain(1) 

50 HA = _float(10000.0), # hectare, 100 * 100, squared 

51 KM = _M_KM, # kilo meter 

52 NM = _M_NM, # nautical mile 

53 SM = _M_SM, # statute mile 

54 TOISE = _float( 1.9490436), # French toise, 6 pieds = 6 / 3.078_432_929_8739 

55 YARD_UK = _float( 0.9144)) # yard, 254 * 12 * 3 / 10_000 = 3 * _M.FOOT 

56 

57 

58def _abs1nan(x): 

59 '''(INTERNAL) Bracket C{x}. 

60 ''' 

61 return _N_1_0 < x < _1_0 or isnan(x) 

62 

63 

64def acos1(x): 

65 '''Return C{math.acos(max(-1, min(1, B{x})))}. 

66 ''' 

67 return acos(x) if _abs1nan(x) else (PI if x < 0 else _0_0) 

68 

69 

70def acre2ha(acres): 

71 '''Convert acres to hectare. 

72 

73 @arg acres: Value in acres (C{scalar}). 

74 

75 @return: Value in C{hectare} (C{float}). 

76 

77 @raise ValueError: Invalid B{C{acres}}. 

78 ''' 

79 return m2ha(acre2m2(acres)) 

80 

81 

82def acre2m2(acres): 

83 '''Convert acres to I{square} meter. 

84 

85 @arg acres: Value in acres (C{scalar}). 

86 

87 @return: Value in C{meter^2} (C{float}). 

88 

89 @raise ValueError: Invalid B{C{acres}}. 

90 ''' 

91 return Meter2(Float(acres=acres) * _M.ACRE) 

92 

93 

94def agdf(phi): 

95 '''Inverse U{Gudermannian function 

96 <https://WikiPedia.org/wiki/Gudermannian_function>}. 

97 

98 @arg phi: Angle (C{radians}). 

99 

100 @return: Gudermannian (psi, C{float}). 

101 

102 @see: Function L{gdf}. 

103 ''' 

104 return asinh(tan(phi)) 

105 

106 

107def asin1(x): 

108 '''Return C{math.asin(max(-1, min(1, B{x})))}. 

109 ''' 

110 return asin(x) if _abs1nan(x) else _copysign(PI_2, x) 

111 

112 

113def atan1(y, x=_1_0): 

114 '''Return C{atan(B{y} / B{x})} angle in C{radians} M{[-PI/2..+PI/2]} 

115 using C{atan2} for consistency and to avoid C{ZeroDivisionError}. 

116 ''' 

117 return _atan1u(y, x, atan2) 

118 

119 

120def atan1d(y, x=_1_0): 

121 '''Return C{atan(B{y} / B{x})} angle in C{degrees} M{[-90..+90]} 

122 using C{atan2d} for consistency and to avoid C{ZeroDivisionError}. 

123 

124 @see: Function L{pygeodesy.atan2d}. 

125 ''' 

126 return _atan1u(y, x, atan2d) 

127 

128 

129def _atan1u(y, x, _2u): 

130 '''(INTERNAL) Helper for functions C{atan1} and C{atan1d}. 

131 ''' 

132 if x < 0: 

133 x = -x 

134 y = -y 

135 return _2u(y, x or _0_0) 

136 

137 

138atan2 = _atan2 

139'''Return C{atan2(B{y}, B{x})} in radians M{[-PI..+PI]}. 

140 

141 @see: I{Karney}'s C++ function U{Math.atan2d 

142 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Math.html>}. 

143''' 

144 

145 

146def atan2b(y, x): 

147 '''Return C{atan2(B{y}, B{x})} in degrees M{[0..+360], counter-clockwise}. 

148 

149 @see: Function L{pygeodesy.atan2d}. 

150 ''' 

151 b = atan2d(y, x) 

152 if b < 0: 

153 b += _360_0 

154 return b or _0_0 # unsigned-0 

155 

156 

157def atan2d(y, x, reverse=False): 

158 '''Return C{atan2(B{y}, B{x})} in degrees M{[-180..+180]}, 

159 optionally I{reversed} (by 180 degrees for C{azimuth}s). 

160 

161 @see: I{Karney}'s C++ function U{Math.atan2d 

162 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Math.html>}. 

163 ''' 

164 d = degrees(_atan2(y, x)) # preserves signed-0 

165 return _azireversed(d) if reverse else d 

166 

167 

168def _azireversed(azi): # in .rhumbBase 

169 '''(INTERNAL) Return the I{reverse} B{C{azi}} in degrees M{[-180..+180]}. 

170 ''' 

171 return azi - _copysign(_180_0, azi) 

172 

173 

174def chain2m(chains): 

175 '''Convert I{UK} chains to meter. 

176 

177 @arg chains: Value in chains (C{scalar}). 

178 

179 @return: Value in C{meter} (C{float}). 

180 

181 @raise ValueError: Invalid B{C{chains}}. 

182 ''' 

183 return Meter(Float(chains=chains) * _M.CHAIN) 

184 

185 

186def circle4(earth, lat): 

187 '''Get the equatorial or a parallel I{circle of latitude}. 

188 

189 @arg earth: The earth radius (C{meter}) or an ellipsoid or datum 

190 (L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple}). 

191 @arg lat: Geodetic latitude (C{degrees90}, C{str}). 

192 

193 @return: A L{Circle4Tuple}C{(radius, height, lat, beta)}. 

194 

195 @raise RangeError: Latitude B{C{lat}} outside valid range and 

196 L{rangerrors<pygeodesy.rangerrors>} is C{True}. 

197 

198 @raise TypeError: Invalid B{C{earth}}. 

199 

200 @raise ValueError: invalid B{C{earth}} or B{C{lat}}. 

201 ''' 

202 E = _MODS.datums._earth_ellipsoid(earth) 

203 return E.circle4(lat) 

204 

205 

206def _circle4radius(earth, lat, **radius): 

207 '''(INTERNAL) Get C{circle4(earth, lat).radius}. 

208 ''' 

209 e = _xkwds_get(radius, radius=earth) if radius else earth 

210 return e if e is R_M and not lat else circle4(e, lat).radius 

211 

212 

213def cot(rad, **raiser_kwds): 

214 '''Return the C{cotangent} of an angle in C{radians}. 

215 

216 @arg rad: Angle (C{radians}). 

217 @kwarg raiser_kwds: Use C{B{raiser}=False} to avoid 

218 ValueErrors and optional, additional 

219 ValueError keyword argments. 

220 

221 @return: C{cot(B{rad})}. 

222 

223 @raise Error: If L{pygeodesy.isnear0}C{(sin(B{rad})}. 

224 ''' 

225 try: 

226 return _cotu(*sincos2(rad)) 

227 except ZeroDivisionError: 

228 return _nonfinite(cot, rad, **raiser_kwds) 

229 

230 

231def cot_(*rads, **raiser_kwds): 

232 '''Yield the C{cotangent} of angle(s) in C{radians}. 

233 

234 @arg rads: One or more angles (each in C{radians}). 

235 

236 @return: Yield C{cot(B{rad})} for each angle. 

237 

238 @see: Function L{pygeodesy.cot} for further details. 

239 ''' 

240 try: 

241 for r in rads: 

242 yield _cotu(*sincos2(r)) 

243 except ZeroDivisionError: 

244 yield _nonfinite(cot_, r, **raiser_kwds) 

245 

246 

247def cotd(deg, **raiser_kwds): 

248 '''Return the C{cotangent} of an angle in C{degrees}. 

249 

250 @arg deg: Angle (C{degrees}). 

251 @kwarg raiser_kwds: Use C{B{raiser}=False} to avoid 

252 ValueErrors and optional, additional 

253 ValueError keyword argments. 

254 

255 @return: C{cot(B{deg})}. 

256 

257 @raise Error: If L{pygeodesy.isnear0}C{(sin(B{deg})}. 

258 ''' 

259 try: 

260 return _cotu(*sincos2d(deg)) 

261 except ZeroDivisionError: 

262 return _nonfinite(cotd, deg, **raiser_kwds) 

263 

264 

265def cotd_(*degs, **raiser_kwds): 

266 '''Yield the C{cotangent} of angle(s) in C{degrees}. 

267 

268 @arg degs: One or more angles (each in C{degrees}). 

269 

270 @return: Yield C{cotd(B{deg})} for each angle. 

271 

272 @see: Function L{pygeodesy.cotd} for further details. 

273 ''' 

274 try: 

275 for d in degs: 

276 yield _cotu(*sincos2d(d)) 

277 except ZeroDivisionError: 

278 yield _nonfinite(cotd_, d, **raiser_kwds) 

279 

280 

281def _cotu(s, c): 

282 '''(INTERNAL) Helper for functions C{cot}, C{cotd}, C{cot_} and C{cotd_}. 

283 ''' 

284 return _tanu(c, s) 

285 

286 

287def degrees90(rad): 

288 '''Convert radians to degrees and wrap M{[-90..+90)}. 

289 

290 @arg rad: Angle (C{radians}). 

291 

292 @return: Angle, wrapped (C{degrees90}). 

293 ''' 

294 return wrap90(degrees(rad)) 

295 

296 

297def degrees180(rad): 

298 '''Convert radians to degrees and wrap M{[-180..+180)}. 

299 

300 @arg rad: Angle (C{radians}). 

301 

302 @return: Angle, wrapped (C{degrees180}). 

303 ''' 

304 return wrap180(degrees(rad)) 

305 

306 

307def degrees360(rad): 

308 '''Convert radians to degrees and wrap M{[0..+360)}. 

309 

310 @arg rad: Angle (C{radians}). 

311 

312 @return: Angle, wrapped (C{degrees360}). 

313 ''' 

314 return _umod_360(degrees(rad)) 

315 

316 

317def degrees2grades(deg): 

318 '''Convert degrees to I{grades} (aka I{gons} or I{gradians}). 

319 

320 @arg deg: Angle (C{degrees}). 

321 

322 @return: Angle (C{grades}). 

323 ''' 

324 return Float(grades=Degrees(deg) * _G.DEG) 

325 

326 

327def degrees2m(deg, earth=R_M, lat=0, **radius): 

328 '''Convert an angle to a distance along the equator or along a parallel 

329 at (geodetic) latitude. 

330 

331 @arg deg: The angle (C{degrees}). 

332 @arg earth: The earth radius (C{meter}) or an ellipsoid or datum 

333 (L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple}). 

334 @kwarg lat: Parallel latitude (C{degrees90}, C{str}). 

335 @kwarg radius: For backward compatibility C{B{radius}=B{earth}}. 

336 

337 @return: Distance (C{meter}, same units as B{C{earth}} or polar and 

338 equatorial radii) or C{0.0} for near-polar B{C{lat}}. 

339 

340 @raise RangeError: Latitude B{C{lat}} outside valid range and 

341 L{rangerrors<pygeodesy.rangerrors>} is C{True}. 

342 

343 @raise TypeError: Invalid B{C{earth}} or B{C{radius}}. 

344 

345 @raise ValueError: Invalid B{C{deg}}, B{C{earth}} or B{C{lat}}. 

346 

347 @see: Function L{radians2m} and L{m2degrees}. 

348 ''' 

349 m = _circle4radius(earth, lat, **radius) 

350 return _Radians2m(Lamd(deg=deg, clip=0), m) 

351 

352 

353def fathom2m(fathoms): 

354 '''Convert I{Imperial} fathom to meter. 

355 

356 @arg fathoms: Value in fathoms (C{scalar}). 

357 

358 @return: Value in C{meter} (C{float}). 

359 

360 @raise ValueError: Invalid B{C{fathoms}}. 

361 

362 @see: Function L{toise2m}, U{Fathom<https://WikiPedia.org/wiki/Fathom>} 

363 and U{Klafter<https://WikiPedia.org/wiki/Klafter>}. 

364 ''' 

365 return Meter(Float(fathoms=fathoms) * _M.FATHOM) 

366 

367 

368def ft2m(feet, usurvey=False, pied=False, fuss=False): 

369 '''Convert I{International}, I{US Survey}, I{French} or I{German} 

370 B{C{feet}} to C{meter}. 

371 

372 @arg feet: Value in feet (C{scalar}). 

373 @kwarg usurvey: If C{True}, convert I{US Survey} foot else ... 

374 @kwarg pied: If C{True}, convert French I{pied-du-Roi} else ... 

375 @kwarg fuss: If C{True}, convert German I{Fuss}, otherwise 

376 I{International} foot to C{meter}. 

377 

378 @return: Value in C{meter} (C{float}). 

379 

380 @raise ValueError: Invalid B{C{feet}}. 

381 ''' 

382 return Meter(Feet(feet) * (_M.FOOT_US if usurvey else 

383 (_M.FOOT_FR if pied else 

384 (_M.FOOT_GE if fuss else _M.FOOT)))) 

385 

386 

387def furlong2m(furlongs): 

388 '''Convert a furlong to meter. 

389 

390 @arg furlongs: Value in furlongs (C{scalar}). 

391 

392 @return: Value in C{meter} (C{float}). 

393 

394 @raise ValueError: Invalid B{C{furlongs}}. 

395 ''' 

396 return Meter(Float(furlongs=furlongs) * _M.FURLONG) 

397 

398 

399def gdf(psi): 

400 '''U{Gudermannian function 

401 <https://WikiPedia.org/wiki/Gudermannian_function>}. 

402 

403 @arg psi: Gudermannian (C{float}). 

404 

405 @return: Angle (C{radians}). 

406 

407 @see: Function L{agdf}. 

408 ''' 

409 return atan1(sinh(psi)) 

410 

411 

412def grades(rad): 

413 '''Convert radians to I{grades} (aka I{gons} or I{gradians}). 

414 

415 @arg rad: Angle (C{radians}). 

416 

417 @return: Angle (C{grades}). 

418 ''' 

419 return Float(grades=Radians(rad) * _G.RAD) 

420 

421 

422def grades400(rad): 

423 '''Convert radians to I{grades} (aka I{gons} or I{gradians}) and wrap M{[0..+400)}. 

424 

425 @arg rad: Angle (C{radians}). 

426 

427 @return: Angle, wrapped (C{grades}). 

428 ''' 

429 return Float(grades400=wrapPI2(rad) * _G.RAD) 

430 

431 

432def grades2degrees(gon): 

433 '''Convert I{grades} (aka I{gons} or I{gradians}) to C{degrees}. 

434 

435 @arg gon: Angle (C{grades}). 

436 

437 @return: Angle (C{degrees}). 

438 ''' 

439 return Degrees(Float(gon=gon) / _G.DEG) 

440 

441 

442def grades2radians(gon): 

443 '''Convert I{grades} (aka I{gons} or I{gradians}) to C{radians}. 

444 

445 @arg gon: Angle (C{grades}). 

446 

447 @return: Angle (C{radians}). 

448 ''' 

449 return Radians(Float(gon=gon) / _G.RAD) 

450 

451 

452def ha2acre(ha): 

453 '''Convert hectare to acre. 

454 

455 @arg ha: Value in hectare (C{scalar}). 

456 

457 @return: Value in acres (C{float}). 

458 

459 @raise ValueError: Invalid B{C{ha}}. 

460 ''' 

461 return m2acre(ha2m2(ha)) 

462 

463 

464def ha2m2(ha): 

465 '''Convert hectare to I{square} meter. 

466 

467 @arg ha: Value in hectare (C{scalar}). 

468 

469 @return: Value in C{meter^2} (C{float}). 

470 

471 @raise ValueError: Invalid B{C{ha}}. 

472 ''' 

473 return Meter2(Float(ha=ha) * _M.HA) 

474 

475 

476def hav(rad): 

477 '''Return the U{haversine<https://WikiPedia.org/wiki/Haversine_formula>} of an angle. 

478 

479 @arg rad: Angle (C{radians}). 

480 

481 @return: C{sin(B{rad} / 2)**2}. 

482 ''' 

483 return sin(rad * _0_5)**2 

484 

485 

486def km2m(km): 

487 '''Convert kilo meter to meter (m). 

488 

489 @arg km: Value in kilo meter (C{scalar}). 

490 

491 @return: Value in meter (C{float}). 

492 

493 @raise ValueError: Invalid B{C{km}}. 

494 ''' 

495 return Meter(Float(km=km) * _M.KM) 

496 

497 

498def _loneg(lon): 

499 '''(INTERNAL) "Complement" of C{lon}. 

500 ''' 

501 return _180_0 - lon 

502 

503 

504def m2acre(meter2): 

505 '''Convert I{square} meter to acres. 

506 

507 @arg meter2: Value in C{meter^2} (C{scalar}). 

508 

509 @return: Value in acres (C{float}). 

510 

511 @raise ValueError: Invalid B{C{meter2}}. 

512 ''' 

513 return Float(acre=Meter2(meter2) / _M.ACRE) 

514 

515 

516def m2chain(meter): 

517 '''Convert meter to I{UK} chains. 

518 

519 @arg meter: Value in meter (C{scalar}). 

520 

521 @return: Value in C{chains} (C{float}). 

522 

523 @raise ValueError: Invalid B{C{meter}}. 

524 ''' 

525 return Float(chain=Meter(meter) / _M.CHAIN) # * 0.049_709_695_378_986_715 

526 

527 

528def m2degrees(distance, earth=R_M, lat=0, **radius): 

529 '''Convert a distance to an angle along the equator or along a parallel 

530 at (geodetic) latitude. 

531 

532 @arg distance: Distance (C{meter}, same units as B{C{radius}}). 

533 @kwarg earth: Mean earth radius (C{meter}) or an ellipsoid or datum 

534 (L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple}). 

535 @kwarg lat: Parallel latitude (C{degrees90}, C{str}). 

536 @kwarg radius: For backward compatibility C{B{radius}=B{earth}}. 

537 

538 @return: Angle (C{degrees}) or C{INF} or C{NINF} for near-polar B{C{lat}}. 

539 

540 @raise RangeError: Latitude B{C{lat}} outside valid range and 

541 L{rangerrors<pygeodesy.rangerrors>} is C{True}. 

542 

543 @raise TypeError: Invalid B{C{earth}} or B{C{radius}}. 

544 

545 @raise ValueError: Invalid B{C{distance}}, B{C{earth}} or B{C{lat}}. 

546 

547 @see: Function L{m2radians} and L{degrees2m}. 

548 ''' 

549 return degrees(m2radians(distance, earth=earth, lat=lat, **radius)) 

550 

551 

552def m2fathom(meter): 

553 '''Convert meter to I{Imperial} fathoms. 

554 

555 @arg meter: Value in meter (C{scalar}). 

556 

557 @return: Value in C{fathoms} (C{float}). 

558 

559 @raise ValueError: Invalid B{C{meter}}. 

560 

561 @see: Function L{m2toise}, U{Fathom<https://WikiPedia.org/wiki/Fathom>} 

562 and U{Klafter<https://WikiPedia.org/wiki/Klafter>}. 

563 ''' 

564 return Float(fathom=Meter(meter) / _M.FATHOM) # * 0.546_806_649 

565 

566 

567def m2ft(meter, usurvey=False, pied=False, fuss=False): 

568 '''Convert meter to I{International}, I{US Survey}, I{French} or 

569 or I{German} feet (C{ft}). 

570 

571 @arg meter: Value in meter (C{scalar}). 

572 @kwarg usurvey: If C{True}, convert to I{US Survey} foot else ... 

573 @kwarg pied: If C{True}, convert to French I{pied-du-Roi} else ... 

574 @kwarg fuss: If C{True}, convert to German I{Fuss}, otherwise to 

575 I{International} foot. 

576 

577 @return: Value in C{feet} (C{float}). 

578 

579 @raise ValueError: Invalid B{C{meter}}. 

580 ''' 

581 # * 3.280_833_333_333_3333, US Survey 3_937 / 1_200 

582 # * 3.280_839_895_013_1235, Int'l 10_000 / (254 * 12) 

583 return Float(feet=Meter(meter) / (_M.FOOT_US if usurvey else 

584 (_M.FOOT_FR if pied else 

585 (_M.FOOT_GE if fuss else _M.FOOT)))) 

586 

587 

588def m2furlong(meter): 

589 '''Convert meter to furlongs. 

590 

591 @arg meter: Value in meter (C{scalar}). 

592 

593 @return: Value in C{furlongs} (C{float}). 

594 

595 @raise ValueError: Invalid B{C{meter}}. 

596 ''' 

597 return Float(furlong=Meter(meter) / _M.FURLONG) # * 0.004_970_969_54 

598 

599 

600def m2ha(meter2): 

601 '''Convert I{square} meter to hectare. 

602 

603 @arg meter2: Value in C{meter^2} (C{scalar}). 

604 

605 @return: Value in hectare (C{float}). 

606 

607 @raise ValueError: Invalid B{C{meter2}}. 

608 ''' 

609 return Float(ha=Meter2(meter2) / _M.HA) 

610 

611 

612def m2km(meter): 

613 '''Convert meter to kilo meter (Km). 

614 

615 @arg meter: Value in meter (C{scalar}). 

616 

617 @return: Value in Km (C{float}). 

618 

619 @raise ValueError: Invalid B{C{meter}}. 

620 ''' 

621 return Float(km=Meter(meter) / _M.KM) 

622 

623 

624def m2NM(meter): 

625 '''Convert meter to nautical miles (NM). 

626 

627 @arg meter: Value in meter (C{scalar}). 

628 

629 @return: Value in C{NM} (C{float}). 

630 

631 @raise ValueError: Invalid B{C{meter}}. 

632 ''' 

633 return Float(NM=Meter(meter) / _M.NM) # * 5.399_568_04e-4 

634 

635 

636def m2radians(distance, earth=R_M, lat=0, **radius): 

637 '''Convert a distance to an angle along the equator or along a parallel 

638 at (geodetic) latitude. 

639 

640 @arg distance: Distance (C{meter}, same units as B{C{radius}}). 

641 @kwarg earth: Mean earth radius (C{meter}) or an ellipsoid or datum 

642 (L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple}). 

643 @kwarg lat: Parallel latitude (C{degrees90}, C{str}). 

644 @kwarg radius: For backward compatibility C{B{radius}=B{earth}}. 

645 

646 @return: Angle (C{radians}) or C{INF} or C{NINF} for near-polar B{C{lat}}. 

647 

648 @raise RangeError: Latitude B{C{lat}} outside valid range and 

649 L{rangerrors<pygeodesy.rangerrors>} is C{True}. 

650 

651 @raise TypeError: Invalid B{C{earth}} or B{C{radius}}. 

652 

653 @raise ValueError: Invalid B{C{distance}}, B{C{earth}} or B{C{lat}}. 

654 

655 @see: Function L{m2degrees} and L{radians2m}. 

656 ''' 

657 m = _circle4radius(earth, lat, **radius) 

658 return _copysign_0_0(distance) if m < EPS0 else \ 

659 Radians(Float(distance=distance) / m) 

660 

661 

662def m2SM(meter): 

663 '''Convert meter to statute miles (SM). 

664 

665 @arg meter: Value in meter (C{scalar}). 

666 

667 @return: Value in C{SM} (C{float}). 

668 

669 @raise ValueError: Invalid B{C{meter}}. 

670 ''' 

671 return Float(SM=Meter(meter) / _M.SM) # * 6.213_699_49e-4 == 1 / 1_609.344 

672 

673 

674def m2toise(meter): 

675 '''Convert meter to French U{toises<https://WikiPedia.org/wiki/Toise>}. 

676 

677 @arg meter: Value in meter (C{scalar}). 

678 

679 @return: Value in C{toises} (C{float}). 

680 

681 @raise ValueError: Invalid B{C{meter}}. 

682 

683 @see: Function L{m2fathom}. 

684 ''' 

685 return Float(toise=Meter(meter) / _M.TOISE) # * 0.513_083_632_632_119 

686 

687 

688def m2yard(meter): 

689 '''Convert meter to I{UK} yards. 

690 

691 @arg meter: Value in meter (C{scalar}). 

692 

693 @return: Value in C{yards} (C{float}). 

694 

695 @raise ValueError: Invalid B{C{meter}}. 

696 ''' 

697 return Float(yard=Meter(meter) / _M.YARD_UK) # * 1.093_613_298_337_707_8 

698 

699 

700def NM2m(nm): 

701 '''Convert nautical miles to meter (m). 

702 

703 @arg nm: Value in nautical miles (C{scalar}). 

704 

705 @return: Value in meter (C{float}). 

706 

707 @raise ValueError: Invalid B{C{nm}}. 

708 ''' 

709 return Meter(Float(nm=nm) * _M.NM) 

710 

711 

712def _nonfinite(where, x, raiser=True, **kwds): # PYCHOK no cover 

713 '''(INTERNAL) Raise a C{_ValueError} or return C{INF} or C{NINF}. 

714 ''' 

715 if raiser: 

716 t = typename(where) 

717 t = _MODS.streprs.Fmt.PAREN(t, x) 

718 raise _ValueError(t, **kwds) 

719 return _copysignINF(x) 

720 

721 

722def radians2m(rad, earth=R_M, lat=0, **radius): 

723 '''Convert an angle to a distance along the equator or along a parallel 

724 at (geodetic) latitude. 

725 

726 @arg rad: The angle (C{radians}). 

727 @kwarg earth: Mean earth radius (C{meter}) or an ellipsoid or datum 

728 (L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple}). 

729 @kwarg lat: Parallel latitude (C{degrees90}, C{str}). 

730 @kwarg radius: For backward compatibility C{B{radius}=B{earth}}. 

731 

732 @return: Distance (C{meter}, same units as B{C{earth}} or polar and 

733 equatorial radii) or C{0.0} for near-polar B{C{lat}}. 

734 

735 @raise RangeError: Latitude B{C{lat}} outside valid range and 

736 L{rangerrors<pygeodesy.rangerrors>} is C{True}. 

737 

738 @raise TypeError: Invalid B{C{earth}} or B{C{radius}}. 

739 

740 @raise ValueError: Invalid B{C{rad}}, B{C{earth}} or B{C{lat}}. 

741 

742 @see: Function L{degrees2m} and L{m2radians}. 

743 ''' 

744 m = _circle4radius(earth, lat, **radius) 

745 return _Radians2m(Lam(rad=rad, clip=0), m) 

746 

747 

748def _Radians2m(rad, m): 

749 '''(INTERNAL) Helper for C{degrees2m} and C{radians2m}. 

750 ''' 

751 return _copysign_0_0(rad) if m < EPS0 else (rad * m) 

752 

753 

754def radiansPI(deg): 

755 '''Convert and wrap degrees to radians M{[-PI..+PI]}. 

756 

757 @arg deg: Angle (C{degrees}). 

758 

759 @return: Radians, wrapped (C{radiansPI}) 

760 ''' 

761 return wrapPI(radians(deg)) 

762 

763 

764def radiansPI2(deg): 

765 '''Convert and wrap degrees to radians M{[0..+2PI)}. 

766 

767 @arg deg: Angle (C{degrees}). 

768 

769 @return: Radians, wrapped (C{radiansPI2}) 

770 ''' 

771 return _umod_PI2(radians(deg)) 

772 

773 

774def radiansPI_2(deg): 

775 '''Convert and wrap degrees to radians M{[-3PI/2..+PI/2]}. 

776 

777 @arg deg: Angle (C{degrees}). 

778 

779 @return: Radians, wrapped (C{radiansPI_2}) 

780 ''' 

781 return wrapPI_2(radians(deg)) 

782 

783 

784def _sin0cos2(q, r, sign): 

785 '''(INTERNAL) 2-tuple (C{sin(r), cos(r)}) in quadrant C{0 <= B{q} <= 3} and 

786 C{sin} zero I{signed} with B{C{sign}}, like Karney's U{Math.sind and .cosd 

787 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Math.html>} 

788 ''' 

789 q &= 3 # -1: 3, -2: 2, -3: 1, -4: 0 ... 

790 if r < PI_2: # Quarter turn 

791 a = fabs(r) 

792 if a: 

793 s = sin(r) 

794 if a == PI_4: 

795 s, c = _copysign(_SIN_45, s), _COS_45 

796 elif a == PI_6: 

797 s, c = _copysign(_SIN_30, s), _COS_30 

798 else: 

799 c = cos(r) 

800 else: 

801 s, c = _0_0, _1_0 

802 else: 

803 s, c = _1_0, _0_0 

804 t = s, c, -s, -c, s 

805 s = t[q] or _copysign_0_0(sign) 

806 c = t[q + 1] or _0_0 

807 return s, c 

808 

809 

810def SinCos2(x): 

811 '''Get C{sin} and C{cos} of I{typed} angle. 

812 

813 @arg x: Angle (L{Degrees}, L{Radians} or scalar C{radians}). 

814 

815 @return: 2-Tuple (C{sin(B{x})}, C{cos(B{x})}). 

816 ''' 

817 return sincos2d(x) if isinstanceof(x, Degrees, Degrees_) else ( 

818# sincos2(x) if isinstanceof(x, Radians, Radians_) else 

819 sincos2(Radians(x))) # assume C{radians} 

820 

821 

822def sincos2(rad): 

823 '''Return the C{sine} and C{cosine} of an angle in C{radians}. 

824 

825 @arg rad: Angle (C{radians}). 

826 

827 @return: 2-Tuple (C{sin(B{rad})}, C{cos(B{rad})}). 

828 

829 @see: U{GeographicLib<https://GeographicLib.SourceForge.io/C++/doc/ 

830 classGeographicLib_1_1Math.html#sincosd>} function U{sincosd 

831 <https://SourceForge.net/p/geographiclib/code/ci/release/tree/ 

832 python/geographiclib/geomath.py#l155>} and C++ U{sincosd 

833 <https://SourceForge.net/p/geographiclib/code/ci/release/tree/ 

834 include/GeographicLib/Math.hpp#l558>}. 

835 ''' 

836 if _isfinite(rad): 

837 q, r = divmod(rad, PI_2) 

838 t = _sin0cos2(int(q), r, rad) 

839 else: 

840 t = NAN, NAN 

841 return t 

842 

843 

844def sincos2_(*rads): 

845 '''Yield the C{sine} and C{cosine} of angle(s) in C{radians}. 

846 

847 @arg rads: One or more angles (C{radians}). 

848 

849 @return: Yield C{sin(B{rad})} and C{cos(B{rad})} for each angle. 

850 

851 @see: Function L{sincos2}. 

852 ''' 

853 for r in rads: 

854 s, c = sincos2(r) 

855 yield s 

856 yield c 

857 

858 

859def sincos2d(deg, adeg=_0_0): 

860 '''Return the C{sine} and C{cosine} of an angle in C{degrees}. 

861 

862 @arg deg: Angle (C{degrees}). 

863 @kwarg adeg: Optional correction (C{degrees}). 

864 

865 @return: 2-Tuple (C{sin(B{deg_})}, C{cos(B{deg_})} with C{B{deg_} = 

866 B{deg} + B{adeg}}). 

867 

868 @see: U{GeographicLib<https://GeographicLib.SourceForge.io/C++/doc/ 

869 classGeographicLib_1_1Math.html#sincosd>} function U{sincosd 

870 <https://SourceForge.net/p/geographiclib/code/ci/release/tree/ 

871 python/geographiclib/geomath.py#l155>} and C++ U{sincosd 

872 <https://SourceForge.net/p/geographiclib/code/ci/release/tree/ 

873 include/GeographicLib/Math.hpp#l558>}. 

874 ''' 

875 if _isfinite(deg): 

876 q, d = divmod(deg, _90_0) 

877 if adeg: 

878 d = _MODS.karney._around(d + adeg) 

879 t = _sin0cos2(int(q), radians(d), deg) 

880 else: 

881 t = NAN, NAN 

882 return t 

883 

884 

885def sincos2d_(*degs): 

886 '''Yield the C{sine} and C{cosine} of angle(s) in C{degrees}. 

887 

888 @arg degs: One or more angles (C{degrees}). 

889 

890 @return: Yield C{sind(B{deg})} and C{cosd(B{deg})} for each angle. 

891 

892 @see: Function L{sincos2d}. 

893 ''' 

894 for d in degs: 

895 s, c = sincos2d(d) 

896 yield s 

897 yield c 

898 

899 

900def sincostan3(rad): 

901 '''Return the C{sine}, C{cosine} and C{tangent} of an angle in C{radians}. 

902 

903 @arg rad: Angle (C{radians}). 

904 

905 @return: 3-Tuple (C{sin(B{rad})}, C{cos(B{rad})}, C{tan(B{rad})}). 

906 

907 @see: Function L{sincos2}. 

908 ''' 

909 return _sincostan3(*sincos2(float(rad))) 

910 

911 

912def _sincostan3(s, c): 

913 '''(INTERNAL) Helper for C{sincostan3} and C{sincostan3d}. 

914 ''' 

915 return s, c, _over_1(s, c) # _tanu(s, c, raiser=False) 

916 

917 

918def sincostan3d(deg): 

919 '''Return the C{sine}, C{cosine} and C{tangent} of an angle in C{degrees}. 

920 

921 @arg deg: Angle (C{degrees}). 

922 

923 @return: 3-Tuple (C{sind(B{deg})}, C{cosd(B{deg})}, C{tand(B{deg})}). 

924 

925 @see: Function L{sincos2d}. 

926 ''' 

927 return _sincostan3(*sincos2d(float(deg))) 

928 

929 

930def SM2m(sm): 

931 '''Convert statute miles to meter (m). 

932 

933 @arg sm: Value in statute miles (C{scalar}). 

934 

935 @return: Value in meter (C{float}). 

936 

937 @raise ValueError: Invalid B{C{sm}}. 

938 ''' 

939 return Meter(Float(sm=sm) * _M.SM) 

940 

941 

942def tan_2(rad, **semi): # edge=1 

943 '''Compute the tangent of half angle. 

944 

945 @arg rad: Angle (C{radians}). 

946 @kwarg semi: Angle or edge name and index 

947 for semi-circular error. 

948 

949 @return: M{tan(rad / 2)} (C{float}). 

950 

951 @raise ValueError: If B{C{rad}} is semi-circular 

952 and B{C{semi}} is given. 

953 ''' 

954 # .formy.excessKarney_, .sphericalTrigonometry.areaOf 

955 if semi and isnear0(fabs(rad) - PI): 

956 for n, v in semi.items(): 

957 break 

958 n = _SPACE_(n, _radians_) if not isint(v) else \ 

959 _SPACE_(_MODS.streprs.Fmt.SQUARE(**semi), _edge_) 

960 raise _ValueError(n, rad, txt=_semi_circular_) 

961 

962 return _tan(rad * _0_5) if _isfinite(rad) else NAN 

963 

964 

965def tan(rad, **raiser_kwds): 

966 '''Return the C{tangent} of an angle in C{radians}. 

967 

968 @arg rad: Angle (C{radians}). 

969 @kwarg raiser_kwds: Use C{B{raiser}=False} to avoid 

970 ValueErrors and optional, additional 

971 ValueError keyword argments. 

972 

973 @return: C{tan(B{rad})}. 

974 

975 @raise Error: If L{pygeodesy.isnear0}C{(cos(B{rad})}. 

976 ''' 

977 try: 

978 return _tanu(*sincos2(rad)) 

979 except ZeroDivisionError: 

980 return _nonfinite(tan, rad, **raiser_kwds) 

981 

982 

983def tan_(*rads, **raiser_kwds): 

984 '''Yield the C{tangent} of angle(s) in C{radians}. 

985 

986 @arg rads: One or more angles (each in C{radians}). 

987 

988 @return: Yield C{tan(B{rad})} for each angle. 

989 

990 @see: Function L{pygeodesy.tan} for futher details. 

991 ''' 

992 try: 

993 for r in rads: 

994 yield _tanu(*sincos2(r)) 

995 except ZeroDivisionError: 

996 yield _nonfinite(tan_, r, **raiser_kwds) 

997 

998 

999def tand(deg, **raiser_kwds): 

1000 '''Return the C{tangent} of an angle in C{degrees}. 

1001 

1002 @arg deg: Angle (C{degrees}). 

1003 @kwarg raiser_kwds: Use C{B{raiser}=False} to avoid 

1004 ValueErrors and optional, additional 

1005 ValueError keyword argments. 

1006 

1007 @return: C{tan(B{deg})}. 

1008 

1009 @raise Error: If L{pygeodesy.isnear0}C{(cos(B{deg})}. 

1010 ''' 

1011 try: 

1012 return _tanu(*sincos2d(deg)) 

1013 except ZeroDivisionError: 

1014 return _nonfinite(tand, deg, **raiser_kwds) 

1015 

1016 

1017def tand_(*degs, **raiser_kwds): 

1018 '''Yield the C{tangent} of angle(s) in C{degrees}. 

1019 

1020 @arg degs: One or more angles (each in C{degrees}). 

1021 

1022 @return: Yield C{tand(B{deg})} for each angle. 

1023 

1024 @see: Function L{pygeodesy.tand} for futher details. 

1025 ''' 

1026 try: 

1027 for d in degs: 

1028 yield _tanu(*sincos2d(d)) 

1029 except ZeroDivisionError: 

1030 yield _nonfinite(tand_, d, **raiser_kwds) 

1031 

1032 

1033def tanPI_2_2(rad): 

1034 '''Compute the tangent of half angle, 90 degrees rotated. 

1035 

1036 @arg rad: Angle (C{radians}). 

1037 

1038 @return: M{tan((rad + PI/2) / 2)} (C{float}). 

1039 ''' 

1040 return _tan((rad + PI_2) * _0_5) if _isfinite(rad) else ( 

1041 NAN if isnan(rad) else _copysign(_90_0, rad)) 

1042 

1043 

1044def _tanu(s, c): 

1045 '''(INTERNAL) Helper for functions C{_cotu}, C{sincostan3}, 

1046 C{sincostan3d}, C{tan}, C{tan_}, C{tand} and C{tand_}. 

1047 ''' 

1048 if s is NAN or isnan(s): 

1049 t = NAN 

1050 elif isnear0(c): 

1051 raise ZeroDivisionError() 

1052 else: 

1053 t = _over_1(s, c) 

1054 return t 

1055 

1056 

1057def toise2m(toises): 

1058 '''Convert French U{toises<https://WikiPedia.org/wiki/Toise>} to meter. 

1059 

1060 @arg toises: Value in toises (C{scalar}). 

1061 

1062 @return: Value in C{meter} (C{float}). 

1063 

1064 @raise ValueError: Invalid B{C{toises}}. 

1065 

1066 @see: Function L{fathom2m}. 

1067 ''' 

1068 return Meter(Float(toises=toises) * _M.TOISE) 

1069 

1070 

1071def truncate(x, ndigits=None): 

1072 '''Truncate to the given number of digits. 

1073 

1074 @arg x: Value to truncate (C{scalar}). 

1075 @kwarg ndigits: Number of digits (C{int}), 

1076 aka I{precision}. 

1077 

1078 @return: Truncated B{C{x}} (C{float}). 

1079 

1080 @see: Python function C{round}. 

1081 ''' 

1082 if isint(ndigits): 

1083 p = _10_0**ndigits 

1084 x = int(x * p) / p 

1085 return x 

1086 

1087 

1088def unroll180(lon1, lon2, wrap=True): 

1089 '''Unroll longitudinal delta and wrap longitude in degrees. 

1090 

1091 @arg lon1: Start longitude (C{degrees}). 

1092 @arg lon2: End longitude (C{degrees}). 

1093 @kwarg wrap: If C{True}, wrap and unroll to the M{(-180..+180]} 

1094 range (C{bool}). 

1095 

1096 @return: 2-Tuple C{(B{lon2}-B{lon1}, B{lon2})} unrolled (C{degrees}, 

1097 C{degrees}). 

1098 

1099 @see: Capability C{LONG_UNROLL} in U{GeographicLib 

1100 <https://GeographicLib.SourceForge.io/html/python/interface.html#outmask>}. 

1101 ''' 

1102 d = lon2 - lon1 

1103 if wrap: 

1104 u = wrap180(d) 

1105 if u != d: 

1106 return u, (lon1 + u) 

1107 return d, lon2 

1108 

1109 

1110def _unrollon(p1, p2, wrap=False): # unroll180 == .karney._unroll2 

1111 '''(INTERNAL) Wrap/normalize, unroll and replace longitude. 

1112 ''' 

1113 lat, lon = p2.lat, p2.lon 

1114 if wrap and _Wrap.normal: 

1115 lat, lon = _Wrap.latlon(lat, lon) 

1116 _, lon = unroll180(p1.lon, lon, wrap=True) 

1117 if lat != p2.lat or fabs(lon - p2.lon) > EPS: 

1118 p2 = p2.dup(lat=lat, lon=wrap180(lon)) 

1119 # p2 = p2.copy(); p2.latlon = lat, wrap180(lon) 

1120 return p2 

1121 

1122 

1123def _unrollon3(p1, p2, p3, wrap=False): 

1124 '''(INTERNAL) Wrap/normalize, unroll 2 points. 

1125 ''' 

1126 w = wrap 

1127 if w: # PYCHOK no cover 

1128 w = _Wrap.normal 

1129 p2 = _unrollon(p1, p2, wrap=w) 

1130 p3 = _unrollon(p1, p3, wrap=w) 

1131 p2 = _unrollon(p2, p3) 

1132 return p2, p3, w # was wrapped? 

1133 

1134 

1135def unrollPI(rad1, rad2, wrap=True): 

1136 '''Unroll longitudinal delta and wrap longitude in radians. 

1137 

1138 @arg rad1: Start longitude (C{radians}). 

1139 @arg rad2: End longitude (C{radians}). 

1140 @kwarg wrap: If C{True}, wrap and unroll to the M{(-PI..+PI]} 

1141 range (C{bool}). 

1142 

1143 @return: 2-Tuple C{(B{rad2}-B{rad1}, B{rad2})} unrolled 

1144 (C{radians}, C{radians}). 

1145 

1146 @see: Capability C{LONG_UNROLL} in U{GeographicLib 

1147 <https://GeographicLib.SourceForge.io/html/python/interface.html#outmask>}. 

1148 ''' 

1149 r = rad2 - rad1 

1150 if wrap: 

1151 u = wrapPI(r) 

1152 if u != r: 

1153 return u, (rad1 + u) 

1154 return r, rad2 

1155 

1156 

1157class _Wrap(object): 

1158 

1159 _normal = False # default 

1160 

1161 @property 

1162 def normal(self): 

1163 '''Get the current L{normal} setting (C{True}, 

1164 C{False} or C{None}). 

1165 ''' 

1166 return self._normal 

1167 

1168 @normal.setter # PYCHOK setter! 

1169 def normal(self, setting): # PYCHOK no cover 

1170 '''Set L{normal} to C{True}, C{False} or C{None}. 

1171 ''' 

1172 m = _MODS.formy 

1173 t = {True: (m.normal, m.normal_), 

1174 False: (self.wraplatlon, self.wraphilam), 

1175 None: (_passargs, _passargs)}.get(setting, ()) 

1176 if t: 

1177 self.latlon, self.philam = t 

1178 self._normal = setting 

1179 

1180 def latlonDMS2(self, lat, lon, **DMS2_kwds): # PYCHOK no cover 

1181 if isstr(lat) or isstr(lon): 

1182 kwds = _xkwds(DMS2_kwds, clipLon=0, clipLat=0) 

1183 lat, lon = _MODS.dms.parseDMS2(lat, lon, **kwds) 

1184 return self.latlon(lat, lon) 

1185 

1186# def normalatlon(self, *latlon): 

1187# return _MODS.formy.normal(*latlon) 

1188 

1189# def normalamphi(self, *philam): 

1190# return _MODS.formy.normal_(*philam) 

1191 

1192 def wraplatlon(self, lat, lon): 

1193 return wrap90(lat), wrap180(lon) 

1194 

1195 latlon = wraplatlon # default 

1196 

1197 def latlon3(self, lon1, lat2, lon2, wrap): 

1198 if wrap: 

1199 lat2, lon2 = self.latlon(lat2, lon2) 

1200 lon21, lon2 = unroll180(lon1, lon2) 

1201 else: 

1202 lon21 = lon2 - lon1 

1203 return lon21, lat2, lon2 

1204 

1205 def _latlonop(self, wrap): 

1206 if wrap and self._normal is not None: 

1207 return self.latlon 

1208 else: 

1209 return _passargs 

1210 

1211 def wraphilam(self, phi, lam): 

1212 return wrapPI_2(phi), wrapPI(lam) 

1213 

1214 philam = wraphilam # default 

1215 

1216 def philam3(self, lam1, phi2, lam2, wrap): 

1217 if wrap: 

1218 phi2, lam2 = self.philam(phi2, lam2) 

1219 lam21, lam2 = unrollPI(lam1, lam2) 

1220 else: 

1221 lam21 = lam2 - lam1 

1222 return lam21, phi2, lam2 

1223 

1224 def _philamop(self, wrap): 

1225 if wrap and self._normal is not None: 

1226 return self.philam 

1227 else: 

1228 return _passargs 

1229 

1230 def point(self, ll, wrap=True): # in .points._fractional, -.PointsIter.iterate, ... 

1231 '''Return C{ll} or a copy, I{normalized} or I{wrap}'d. 

1232 ''' 

1233 if wrap and self._normal is not None: 

1234 lat, lon = ll.latlon 

1235 if fabs(lon) > _180_0 or fabs(lat) > _90_0: 

1236 _n = self.latlon 

1237 ll = ll.copy(name=typename(_n)) 

1238 ll.latlon = _n(lat, lon) 

1239 return ll 

1240 

1241_Wrap = _Wrap() # PYCHOK singleton 

1242 

1243 

1244# def _wrap(angle, wrap, modulo): 

1245# '''(INTERNAL) Angle wrapper M{((wrap-modulo)..+wrap]}. 

1246# 

1247# @arg angle: Angle (C{degrees}, C{radians} or C{grades}). 

1248# @arg wrap: Range (C{degrees}, C{radians} or C{grades}). 

1249# @arg modulo: Upper limit (360 C{degrees}, PI2 C{radians} or 400 C{grades}). 

1250# 

1251# @return: The B{C{angle}}, wrapped (C{degrees}, C{radians} or C{grades}). 

1252# ''' 

1253# a = float(angle) 

1254# if not (wrap - modulo) <= a < wrap: 

1255# # math.fmod(-1.5, 3.14) == -1.5, but -1.5 % 3.14 == 1.64 

1256# # math.fmod(-1.5, 360) == -1.5, but -1.5 % 360 == 358.5 

1257# a %= modulo 

1258# if a > wrap: 

1259# a -= modulo 

1260# return a 

1261 

1262 

1263def wrap90(deg): 

1264 '''Wrap degrees to M{[-90..+90]}. 

1265 

1266 @arg deg: Angle (C{degrees}). 

1267 

1268 @return: Degrees, wrapped (C{degrees90}). 

1269 ''' 

1270 return _wrapu(wrap180(deg), _180_0, _90_0) 

1271 

1272 

1273def wrap180(deg): 

1274 '''Wrap degrees to M{[-180..+180]}. 

1275 

1276 @arg deg: Angle (C{degrees}). 

1277 

1278 @return: Degrees, wrapped (C{degrees180}). 

1279 ''' 

1280 d = float(deg) 

1281 w = _umod_360(d) 

1282 if w > _180_0: 

1283 w -= _360_0 

1284 elif d < 0 and w == _180_0: 

1285 w = -w 

1286 return w 

1287 

1288 

1289def wrap360(deg): # see .streprs._umod_360 

1290 '''Wrap degrees to M{[0..+360)}. 

1291 

1292 @arg deg: Angle (C{degrees}). 

1293 

1294 @return: Degrees, wrapped (C{degrees360}). 

1295 ''' 

1296 return _umod_360(float(deg)) 

1297 

1298 

1299def wrapPI(rad): 

1300 '''Wrap radians to M{[-PI..+PI]}. 

1301 

1302 @arg rad: Angle (C{radians}). 

1303 

1304 @return: Radians, wrapped (C{radiansPI}). 

1305 ''' 

1306 r = float(rad) 

1307 w = _umod_PI2(r) 

1308 if w > PI: 

1309 w -= PI2 

1310 elif r < 0 and w == PI: 

1311 w = -PI 

1312 return w 

1313 

1314 

1315def wrapPI2(rad): 

1316 '''Wrap radians to M{[0..+2PI)}. 

1317 

1318 @arg rad: Angle (C{radians}). 

1319 

1320 @return: Radians, wrapped (C{radiansPI2}). 

1321 ''' 

1322 return _umod_PI2(float(rad)) 

1323 

1324 

1325def wrapPI_2(rad): 

1326 '''Wrap radians to M{[-PI/2..+PI/2]}. 

1327 

1328 @arg rad: Angle (C{radians}). 

1329 

1330 @return: Radians, wrapped (C{radiansPI_2}). 

1331 ''' 

1332 return _wrapu(wrapPI(rad), PI, PI_2) 

1333 

1334 

1335# def wraplatlon(lat, lon): 

1336# '''Both C{wrap90(B{lat})} and C{wrap180(B{lon})}. 

1337# ''' 

1338# return wrap90(lat), wrap180(lon) 

1339 

1340 

1341def wrap_normal(*normal): 

1342 '''Define the operation for the keyword argument C{B{wrap}=True}, 

1343 across L{pygeodesy}: I{wrap}, I{normalize} or I{no-op}. For 

1344 backward compatibility, the default is I{wrap}. 

1345 

1346 @arg normal: If C{True}, I{normalize} lat- and longitude using 

1347 L{normal} or L{normal_}, if C{False}, I{wrap} the 

1348 lat- and longitude individually by L{wrap90} or 

1349 L{wrapPI_2} respectively L{wrap180}, L{wrapPI} or 

1350 if C{None}, leave lat- and longitude I{unchanged}. 

1351 To get the current setting, do not specify. 

1352 

1353 @return: The previous L{wrap_normal} setting (C{bool} or C{None}). 

1354 ''' 

1355 t = _Wrap.normal 

1356 if normal: 

1357 _Wrap.normal = normal[0] 

1358 return t 

1359 

1360 

1361# def wraphilam(phi, lam,): 

1362# '''Both C{wrapPI_2(B{phi})} and C{wrapPI(B{lam})}. 

1363# ''' 

1364# return wrapPI_2(phi), wrapPI(lam) 

1365 

1366 

1367def _wrapu(w, H, Q): 

1368 '''(INTERNAL) Helper for functions C{wrap180} and C{wrapPI}. 

1369 ''' 

1370 return (w - H) if w > Q else ((w + H) if w < (-Q) else w) 

1371 

1372 

1373def yard2m(yards): 

1374 '''Convert I{UK} yards to meter. 

1375 

1376 @arg yards: Value in yards (C{scalar}). 

1377 

1378 @return: Value in C{meter} (C{float}). 

1379 

1380 @raise ValueError: Invalid B{C{yards}}. 

1381 ''' 

1382 return Float(yards=yards) * _M.YARD_UK 

1383 

1384# **) MIT License 

1385# 

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

1387# 

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

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

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

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

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

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

1394# 

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

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

1397# 

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

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

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

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

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

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

1404# OTHER DEALINGS IN THE SOFTWARE.