Coverage for pygeodesy/utily.py: 91%

360 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-04-25 13:15 -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 _ # PYCHOK semicolon 

13 

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

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

16 _M_KM, _M_NM, _M_SM, _0_0, _1__90, _0_5, _2__PI, \ 

17 _1_0, _N_1_0, _10_0, _90_0, _180_0, _360_0, \ 

18 _copysign_0_0, _float, _isfinite, isnan, isnear0, \ 

19 _over, _umod_360, _umod_PI2 

20from pygeodesy.errors import _ValueError, _xkwds, _ALL_LAZY, _MODS 

21from pygeodesy.internals import _passargs, typename 

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

23# from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS # from .errors 

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

25 Meter, Meter2, Radians # Radians_ 

26 

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

28 sin, tan as _tan # pow 

29 

30__all__ = _ALL_LAZY.utily 

31__version__ = '25.04.14' 

32 

33_G_DEG = _float( 400.0 / _360_0) # grades per degree 

34_G_RAD = _float( 400.0 / PI2) # grades per radian 

35_M_ACRE = _float( 4046.8564224) # square meter per acre, chain2m(1) * furlong2m(1) 

36_M_CHAIN = _float( 20.1168) # meter per yard2m(1) * 22 

37_M_FATHOM = _float( 1.8288) # meter per yard2m(1) * 2 or _M_NM * 1e-3 

38_M_FOOT = _float( 0.3048) # meter per Int'l foot, 1 / 3.280_839_895_0131 = 10_000 / (254 * 12) 

39_M_FOOT_GE = _float( 0.31608) # meter per German Fuss, 1 / 3.163_756_011_1364 

40_M_FOOT_FR = _float( 0.3248406) # meter per French Pied-du-Roi or pied, 1 / 3.078_432_929_8739 

41_M_FOOT_US = _float( 0.3048006096012192) # meter per US Survey foot, 1_200 / 3_937 

42_M_FURLONG = _float( 201.168) # meter per furlong, 220 * yard2m(1) = 10 * m2chain(1) 

43_M_HA = _float(10000.0) # square meter per hectare, 100 * 100 

44# _M_KM = _float( 1000.0) # meter per kilo meter 

45# _M_NM = _float( 1852.0) # meter per nautical mile 

46# _M_SM = _float( 1609.344) # meter per statute mile 

47_M_TOISE = _float( 1.9490436) # meter per French toise, 6 pieds = 6 / 3.078_432_929_8739 

48_M_YARD_UK = _float( 0.9144) # meter per yard, 254 * 12 * 3 / 10_000 = 3 * _M_FOOT 

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

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

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

52 

53 

54def _abs1nan(x): 

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

56 ''' 

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

58 

59 

60def acos1(x): 

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

62 ''' 

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

64 

65 

66def acre2ha(acres): 

67 '''Convert acres to hectare. 

68 

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

70 

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

72 

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

74 ''' 

75 return m2ha(acre2m2(acres)) 

76 

77 

78def acre2m2(acres): 

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

80 

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

82 

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

84 

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

86 ''' 

87 return Meter2(Float(acres=acres) * _M_ACRE) 

88 

89 

90def asin1(x): 

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

92 ''' 

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

94 

95 

96def atan1(y, x=_1_0): 

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

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

99 ''' 

100 return _atan1u(y, x, atan2) 

101 

102 

103def atan1d(y, x=_1_0): 

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

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

106 

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

108 ''' 

109 return _atan1u(y, x, atan2d) 

110 

111 

112def _atan1u(y, x, _2u): 

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

114 ''' 

115 if x < 0: 

116 x = -x 

117 y = -y 

118 return _2u(y, x or _0_0) 

119 

120 

121atan2 = _atan2 

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

123 

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

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

126''' 

127 

128 

129def atan2b(y, x): 

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

131 

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

133 ''' 

134 b = atan2d(y, x) 

135 if b < 0: 

136 b += _360_0 

137 return b or _0_0 # unsigned-0 

138 

139 

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

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

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

143 

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

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

146 ''' 

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

148 return _azireversed(d) if reverse else d 

149 

150 

151def _azireversed(azi): # in .rhumbBase 

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

153 ''' 

154 return azi - _copysign(_180_0, azi) 

155 

156 

157def chain2m(chains): 

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

159 

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

161 

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

163 

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

165 ''' 

166 return Meter(Float(chains=chains) * _M_CHAIN) 

167 

168 

169def circle4(earth, lat): 

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

171 

172 @arg earth: The earth radius (C{meter}), ellipsoid or datum 

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

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

175 

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

177 

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

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

180 

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

182 

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

184 ''' 

185 E = _MODS.datums._earth_ellipsoid(earth) 

186 return E.circle4(lat) 

187 

188 

189def cot(rad, **raiser_kwds): 

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

191 

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

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

194 ValueErrors or optionally, additional 

195 ValueError keyword argments. 

196 

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

198 

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

200 ''' 

201 try: 

202 return _cotu(*sincos2(rad), **raiser_kwds) 

203 except ZeroDivisionError: 

204 raise _valueError(cot, rad, **raiser_kwds) 

205 

206 

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

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

209 

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

211 

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

213 

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

215 ''' 

216 try: 

217 for r in rads: 

218 yield _cotu(*sincos2(r), **raiser_kwds) 

219 except ZeroDivisionError: 

220 raise _valueError(cot_, r, **raiser_kwds) 

221 

222 

223def cotd(deg, **raiser_kwds): 

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

225 

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

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

228 ValueErrors or optionally, additional 

229 ValueError keyword argments. 

230 

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

232 

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

234 ''' 

235 try: 

236 return _cotu(*sincos2d(deg), **raiser_kwds) 

237 except ZeroDivisionError: 

238 raise _valueError(cotd, deg, **raiser_kwds) 

239 

240 

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

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

243 

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

245 

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

247 

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

249 ''' 

250 try: 

251 for d in degs: 

252 yield _cotu(*sincos2d(d), **raiser_kwds) 

253 except ZeroDivisionError: 

254 raise _valueError(cotd_, d, **raiser_kwds) 

255 

256 

257def _cotu(s, c, **raiser_kwds): 

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

259 ''' 

260 return _tanu(c, s, **raiser_kwds) 

261 

262 

263def degrees90(rad): 

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

265 

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

267 

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

269 ''' 

270 return wrap90(degrees(rad)) 

271 

272 

273def degrees180(rad): 

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

275 

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

277 

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

279 ''' 

280 return wrap180(degrees(rad)) 

281 

282 

283def degrees360(rad): 

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

285 

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

287 

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

289 ''' 

290 return _umod_360(degrees(rad)) 

291 

292 

293def degrees2grades(deg): 

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

295 

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

297 

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

299 ''' 

300 return Float(grades=Degrees(deg) * _G_DEG) 

301 

302 

303def degrees2m(deg, radius=R_M, lat=0): 

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

305 at (geodetic) latitude. 

306 

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

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

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

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

311 

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

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

314 

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

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

317 

318 @raise TypeError: Invalid B{C{radius}}. 

319 

320 @raise ValueError: Invalid B{C{deg}}, B{C{radius}} or B{C{lat}}. 

321 

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

323 ''' 

324 return _Radians2m(Lamd(deg=deg, clip=0), radius, lat) 

325 

326 

327def fathom2m(fathoms): 

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

329 

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

331 

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

333 

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

335 

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

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

338 ''' 

339 return Meter(Float(fathoms=fathoms) * _M_FATHOM) 

340 

341 

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

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

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

345 

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

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

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

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

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

351 

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

353 

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

355 ''' 

356 return Meter(Feet(feet) * (_M_FOOT_US if usurvey else 

357 (_M_FOOT_FR if pied else 

358 (_M_FOOT_GE if fuss else _M_FOOT)))) 

359 

360 

361def furlong2m(furlongs): 

362 '''Convert a furlong to meter. 

363 

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

365 

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

367 

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

369 ''' 

370 return Meter(Float(furlongs=furlongs) * _M_FURLONG) 

371 

372 

373def grades(rad): 

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

375 

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

377 

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

379 ''' 

380 return Float(grades=Radians(rad) * _G_RAD) 

381 

382 

383def grades400(rad): 

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

385 

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

387 

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

389 ''' 

390 return Float(grades400=wrapPI2(rad) * _G_RAD) 

391 

392 

393def grades2degrees(gon): 

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

395 

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

397 

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

399 ''' 

400 return Degrees(Float(gon=gon) / _G_DEG) 

401 

402 

403def grades2radians(gon): 

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

405 

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

407 

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

409 ''' 

410 return Radians(Float(gon=gon) / _G_RAD) 

411 

412 

413def ha2acre(ha): 

414 '''Convert hectare to acre. 

415 

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

417 

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

419 

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

421 ''' 

422 return m2acre(ha2m2(ha)) 

423 

424 

425def ha2m2(ha): 

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

427 

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

429 

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

431 

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

433 ''' 

434 return Meter2(Float(ha=ha) * _M_HA) 

435 

436 

437def hav(rad): 

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

439 

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

441 

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

443 ''' 

444 return sin(rad * _0_5)**2 

445 

446 

447def km2m(km): 

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

449 

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

451 

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

453 

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

455 ''' 

456 return Meter(Float(km=km) * _M_KM) 

457 

458 

459def _loneg(lon): 

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

461 ''' 

462 return _180_0 - lon 

463 

464 

465def m2acre(meter2): 

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

467 

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

469 

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

471 

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

473 ''' 

474 return Float(acre=Meter2(meter2) / _M_ACRE) 

475 

476 

477def m2chain(meter): 

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

479 

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

481 

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

483 

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

485 ''' 

486 return Float(chain=Meter(meter) / _M_CHAIN) # * 0.049_709_695_378_986_715 

487 

488 

489def m2degrees(distance, radius=R_M, lat=0): 

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

491 at (geodetic) latitude. 

492 

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

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

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

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

497 

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

499 

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

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

502 

503 @raise TypeError: Invalid B{C{radius}}. 

504 

505 @raise ValueError: Invalid B{C{distance}}, B{C{radius}} or B{C{lat}}. 

506 

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

508 ''' 

509 return degrees(m2radians(distance, radius=radius, lat=lat)) 

510 

511 

512def m2fathom(meter): 

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

514 

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

516 

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

518 

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

520 

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

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

523 ''' 

524 return Float(fathom=Meter(meter) / _M_FATHOM) # * 0.546_806_649 

525 

526 

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

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

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

530 

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

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

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

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

535 I{International} foot. 

536 

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

538 

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

540 ''' 

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

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

543 return Float(feet=Meter(meter) / (_M_FOOT_US if usurvey else 

544 (_M_FOOT_FR if pied else 

545 (_M_FOOT_GE if fuss else _M_FOOT)))) 

546 

547 

548def m2furlong(meter): 

549 '''Convert meter to furlongs. 

550 

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

552 

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

554 

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

556 ''' 

557 return Float(furlong=Meter(meter) / _M_FURLONG) # * 0.004_970_969_54 

558 

559 

560def m2ha(meter2): 

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

562 

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

564 

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

566 

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

568 ''' 

569 return Float(ha=Meter2(meter2) / _M_HA) 

570 

571 

572def m2km(meter): 

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

574 

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

576 

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

578 

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

580 ''' 

581 return Float(km=Meter(meter) / _M_KM) 

582 

583 

584def m2NM(meter): 

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

586 

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

588 

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

590 

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

592 ''' 

593 return Float(NM=Meter(meter) / _M_NM) # * 5.399_568_04e-4 

594 

595 

596def m2radians(distance, radius=R_M, lat=0): 

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

598 at (geodetic) latitude. 

599 

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

601 @kwarg radius: Mean earth radius (C{meter}, an ellipsoid or datum 

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

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

604 

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

606 

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

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

609 

610 @raise TypeError: Invalid B{C{radius}}. 

611 

612 @raise ValueError: Invalid B{C{distance}}, B{C{radius}} or B{C{lat}}. 

613 

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

615 ''' 

616 m = circle4(radius, lat).radius 

617 return INF if m < EPS0 else Radians(Float(distance=distance) / m) 

618 

619 

620def m2SM(meter): 

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

622 

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

624 

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

626 

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

628 ''' 

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

630 

631 

632def m2toise(meter): 

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

634 

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

636 

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

638 

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

640 

641 @see: Function L{m2fathom}. 

642 ''' 

643 return Float(toise=Meter(meter) / _M_TOISE) # * 0.513_083_632_632_119 

644 

645 

646def m2yard(meter): 

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

648 

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

650 

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

652 

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

654 ''' 

655 return Float(yard=Meter(meter) / _M_YARD_UK) # * 1.093_613_298_337_707_8 

656 

657 

658def NM2m(nm): 

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

660 

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

662 

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

664 

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

666 ''' 

667 return Meter(Float(nm=nm) * _M_NM) 

668 

669 

670def radians2m(rad, radius=R_M, lat=0): 

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

672 at (geodetic) latitude. 

673 

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

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

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

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

678 

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

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

681 

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

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

684 

685 @raise TypeError: Invalid B{C{radius}}. 

686 

687 @raise ValueError: Invalid B{C{rad}}, B{C{radius}} or B{C{lat}}. 

688 

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

690 ''' 

691 return _Radians2m(Lam(rad=rad, clip=0), radius, lat) 

692 

693 

694def _Radians2m(rad, radius, lat): 

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

696 ''' 

697 m = circle4(radius, lat).radius 

698 return _0_0 if m < EPS0 else (rad * m) 

699 

700 

701def radiansPI(deg): 

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

703 

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

705 

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

707 ''' 

708 return wrapPI(radians(deg)) 

709 

710 

711def radiansPI2(deg): 

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

713 

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

715 

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

717 ''' 

718 return _umod_PI2(radians(deg)) 

719 

720 

721def radiansPI_2(deg): 

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

723 

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

725 

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

727 ''' 

728 return wrapPI_2(radians(deg)) 

729 

730 

731def _sin0cos2(q, r, sign, a, Q): # Quarter turn 

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

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

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

735 ''' 

736 if r < PI_2: 

737 s = sin(r) 

738 if (a * 2) == Q: 

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

740 elif (a * 3) == Q: 

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

742 else: 

743 c = cos(r) 

744 else: 

745 s, c = _1_0, _0_0 

746 t = s, c, -s, -c, s 

747# q &= 3 

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

749 c = t[q + 1] or _0_0 

750 return s, c 

751 

752 

753def SinCos2(x): 

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

755 

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

757 

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

759 ''' 

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

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

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

763 

764 

765def sincos2(rad): 

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

767 

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

769 

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

771 

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

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

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

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

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

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

778 ''' 

779 if _isfinite(rad): 

780 q = int(rad * _2__PI) # int(math.floor) 

781 if q < 0: 

782 q -= 1 

783 r = rad - q * PI_2 

784 t = _sin0cos2(q & 3, r, rad, fabs(r), PI_2) 

785 else: 

786 t = NAN, NAN 

787 return t 

788 

789 

790def sincos2_(*rads): 

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

792 

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

794 

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

796 

797 @see: Function L{sincos2}. 

798 ''' 

799 for r in rads: 

800 s, c = sincos2(r) 

801 yield s 

802 yield c 

803 

804 

805def sincos2d(deg, adeg=_0_0): 

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

807 

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

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

810 

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

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

813 

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

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

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

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

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

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

820 ''' 

821 if _isfinite(deg): 

822 q = int(deg * _1__90) # int(math.floor) 

823 if q < 0: 

824 q -= 1 

825 d = deg - q * _90_0 

826 if adeg: 

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

828 t = _sin0cos2(q & 3, radians(d), deg, fabs(d), _90_0) 

829 else: 

830 t = NAN, NAN 

831 return t 

832 

833 

834def sincos2d_(*degs): 

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

836 

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

838 

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

840 

841 @see: Function L{sincos2d}. 

842 ''' 

843 for d in degs: 

844 s, c = sincos2d(d) 

845 yield s 

846 yield c 

847 

848 

849def sincostan3(rad): 

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

851 

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

853 

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

855 

856 @see: Function L{sincos2}. 

857 ''' 

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

859 

860 

861def _sincostan3(s, c): 

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

863 ''' 

864 return s, c, _tanu(s, c, raiser=False) 

865 

866 

867def sincostan3d(deg): 

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

869 

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

871 

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

873 

874 @see: Function L{sincos2d}. 

875 ''' 

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

877 

878 

879def SM2m(sm): 

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

881 

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

883 

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

885 

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

887 ''' 

888 return Meter(Float(sm=sm) * _M_SM) 

889 

890 

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

892 '''Compute the tangent of half angle. 

893 

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

895 @kwarg semi: Angle or edge name and index 

896 for semi-circular error. 

897 

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

899 

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

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

902 ''' 

903 # .formy.excessKarney_, .sphericalTrigonometry.areaOf 

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

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

906 break 

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

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

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

910 

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

912 

913 

914def tan(rad, **raiser_kwds): 

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

916 

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

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

919 ValueErrors or optionally, additional 

920 ValueError keyword argments. 

921 

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

923 

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

925 ''' 

926 try: 

927 return _tanu(*sincos2(rad), **raiser_kwds) 

928 except ZeroDivisionError: 

929 raise _valueError(tan, rad, **raiser_kwds) 

930 

931 

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

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

934 

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

936 

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

938 

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

940 ''' 

941 try: 

942 for r in rads: 

943 yield _tanu(*sincos2(r), **raiser_kwds) 

944 except ZeroDivisionError: 

945 raise _valueError(tan_, r, **raiser_kwds) 

946 

947 

948def tand(deg, **raiser_kwds): 

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

950 

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

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

953 ValueErrors or optionally, additional 

954 ValueError keyword argments. 

955 

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

957 

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

959 ''' 

960 try: 

961 return _tanu(*sincos2d(deg), **raiser_kwds) 

962 except ZeroDivisionError: 

963 raise _valueError(tand, deg, **raiser_kwds) 

964 

965 

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

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

968 

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

970 

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

972 

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

974 ''' 

975 try: 

976 for d in degs: 

977 yield _tanu(*sincos2d(d), **raiser_kwds) 

978 except ZeroDivisionError: 

979 raise _valueError(tand_, d, **raiser_kwds) 

980 

981 

982def tanPI_2_2(rad): 

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

984 

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

986 

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

988 ''' 

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

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

991 

992 

993def _tanu(s, c, raiser=True, **unused): 

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

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

996 ''' 

997 if s is NAN or isnan(s): 

998 s = NAN 

999 elif s: 

1000 if raiser and isnear0(c): 

1001 raise ZeroDivisionError() 

1002 s = _over(s, c) if fabs(s) != fabs(c) else \ 

1003 _copysign(_1_0, (-s) if c < 0 else s) 

1004 elif c < 0: 

1005 s = -s # negate-0 

1006 return s 

1007 

1008 

1009def toise2m(toises): 

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

1011 

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

1013 

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

1015 

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

1017 

1018 @see: Function L{fathom2m}. 

1019 ''' 

1020 return Meter(Float(toises=toises) * _M_TOISE) 

1021 

1022 

1023def truncate(x, ndigits=None): 

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

1025 

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

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

1028 aka I{precision}. 

1029 

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

1031 

1032 @see: Python function C{round}. 

1033 ''' 

1034 if isint(ndigits): 

1035 p = _10_0**ndigits 

1036 x = int(x * p) / p 

1037 return x 

1038 

1039 

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

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

1042 

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

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

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

1046 range (C{bool}). 

1047 

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

1049 C{degrees}). 

1050 

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

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

1053 ''' 

1054 d = lon2 - lon1 

1055 if wrap: 

1056 u = wrap180(d) 

1057 if u != d: 

1058 return u, (lon1 + u) 

1059 return d, lon2 

1060 

1061 

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

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

1064 ''' 

1065 lat, lon = p2.lat, p2.lon 

1066 if wrap and _Wrap.normal: 

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

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

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

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

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

1072 return p2 

1073 

1074 

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

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

1077 ''' 

1078 w = wrap 

1079 if w: 

1080 w = _Wrap.normal 

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

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

1083 p2 = _unrollon(p2, p3) 

1084 return p2, p3, w # was wrapped? 

1085 

1086 

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

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

1089 

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

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

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

1093 range (C{bool}). 

1094 

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

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

1097 

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

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

1100 ''' 

1101 r = rad2 - rad1 

1102 if wrap: 

1103 u = wrapPI(r) 

1104 if u != r: 

1105 return u, (rad1 + u) 

1106 return r, rad2 

1107 

1108 

1109def _valueError(where, x, raiser=True, **kwds): 

1110 '''(INTERNAL) Return a C{_ValueError} or C{None}. 

1111 ''' 

1112 t = _MODS.streprs.Fmt.PAREN(typename(where), x) 

1113 return _ValueError(t, **kwds) if raiser else None 

1114 

1115 

1116class _Wrap(object): 

1117 

1118 _normal = False # default 

1119 

1120 @property 

1121 def normal(self): 

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

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

1124 ''' 

1125 return self._normal 

1126 

1127 @normal.setter # PYCHOK setter! 

1128 def normal(self, setting): 

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

1130 ''' 

1131 m = _MODS.formy 

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

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

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

1135 if t: 

1136 self.latlon, self.philam = t 

1137 self._normal = setting 

1138 

1139 def latlonDMS2(self, lat, lon, **DMS2_kwds): 

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

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

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

1143 return self.latlon(lat, lon) 

1144 

1145# def normalatlon(self, *latlon): 

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

1147 

1148# def normalamphi(self, *philam): 

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

1150 

1151 def wraplatlon(self, lat, lon): 

1152 return wrap90(lat), wrap180(lon) 

1153 

1154 latlon = wraplatlon # default 

1155 

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

1157 if wrap: 

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

1159 lon21, lon2 = unroll180(lon1, lon2) 

1160 else: 

1161 lon21 = lon2 - lon1 

1162 return lon21, lat2, lon2 

1163 

1164 def _latlonop(self, wrap): 

1165 if wrap and self._normal is not None: 

1166 return self.latlon 

1167 else: 

1168 return _passargs 

1169 

1170 def wraphilam(self, phi, lam): 

1171 return wrapPI_2(phi), wrapPI(lam) 

1172 

1173 philam = wraphilam # default 

1174 

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

1176 if wrap: 

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

1178 lam21, lam2 = unrollPI(lam1, lam2) 

1179 else: 

1180 lam21 = lam2 - lam1 

1181 return lam21, phi2, lam2 

1182 

1183 def _philamop(self, wrap): 

1184 if wrap and self._normal is not None: 

1185 return self.philam 

1186 else: 

1187 return _passargs 

1188 

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

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

1191 ''' 

1192 if wrap and self._normal is not None: 

1193 lat, lon = ll.latlon 

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

1195 _n = self.latlon 

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

1197 ll.latlon = _n(lat, lon) 

1198 return ll 

1199 

1200_Wrap = _Wrap() # PYCHOK singleton 

1201 

1202 

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

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

1205# 

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

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

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

1209# 

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

1211# ''' 

1212# a = float(angle) 

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

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

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

1216# a %= modulo 

1217# if a > wrap: 

1218# a -= modulo 

1219# return a 

1220 

1221 

1222def wrap90(deg): 

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

1224 

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

1226 

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

1228 ''' 

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

1230 

1231 

1232def wrap180(deg): 

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

1234 

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

1236 

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

1238 ''' 

1239 d = float(deg) 

1240 w = _umod_360(d) 

1241 if w > _180_0: 

1242 w -= _360_0 

1243 elif d < 0 and w == _180_0: 

1244 w = -w 

1245 return w 

1246 

1247 

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

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

1250 

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

1252 

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

1254 ''' 

1255 return _umod_360(float(deg)) 

1256 

1257 

1258def wrapPI(rad): 

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

1260 

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

1262 

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

1264 ''' 

1265 r = float(rad) 

1266 w = _umod_PI2(r) 

1267 if w > PI: 

1268 w -= PI2 

1269 elif r < 0 and w == PI: 

1270 w = -PI 

1271 return w 

1272 

1273 

1274def wrapPI2(rad): 

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

1276 

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

1278 

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

1280 ''' 

1281 return _umod_PI2(float(rad)) 

1282 

1283 

1284def wrapPI_2(rad): 

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

1286 

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

1288 

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

1290 ''' 

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

1292 

1293 

1294# def wraplatlon(lat, lon): 

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

1296# ''' 

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

1298 

1299 

1300def wrap_normal(*normal): 

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

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

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

1304 

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

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

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

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

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

1310 To get the current setting, do not specify. 

1311 

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

1313 ''' 

1314 t = _Wrap.normal 

1315 if normal: 

1316 _Wrap.normal = normal[0] 

1317 return t 

1318 

1319 

1320# def wraphilam(phi, lam,): 

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

1322# ''' 

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

1324 

1325 

1326def _wrapu(w, H, Q): 

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

1328 ''' 

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

1330 

1331 

1332def yard2m(yards): 

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

1334 

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

1336 

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

1338 

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

1340 ''' 

1341 return Float(yards=yards) * _M_YARD_UK 

1342 

1343# **) MIT License 

1344# 

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

1346# 

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

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

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

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

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

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

1353# 

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

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

1356# 

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

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

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

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

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

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

1363# OTHER DEALINGS IN THE SOFTWARE.