Coverage for pygeodesy/ltp.py: 96%

417 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-05-25 12:04 -0400

1 

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

3 

4u'''I{Local Tangent Plane} (LTP) and I{local} cartesian coordinates. 

5 

6I{Local cartesian} and I{local tangent plane} classes L{LocalCartesian}, approximations L{ChLVa} 

7and L{ChLVe} and L{Ltp}, L{ChLV}, L{LocalError}, L{Attitude} and L{Frustum}. 

8 

9@see: U{Local tangent plane coordinates<https://WikiPedia.org/wiki/Local_tangent_plane_coordinates>} 

10 and class L{LocalCartesian}, transcoded from I{Charles Karney}'s C++ classU{LocalCartesian 

11 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1LocalCartesian.html>}. 

12''' 

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

14from __future__ import division as _; del _ # PYCHOK semicolon 

15 

16from pygeodesy.basics import _args_kwds_names, issubclassof, map1, map2 # .datums 

17from pygeodesy.constants import EPS, INT0, _umod_360, _0_0, _0_01, _0_5, _1_0, \ 

18 _2_0, _60_0, _90_0, _100_0, _180_0, _3600_0, \ 

19 _N_1_0 # PYCHOK used! 

20from pygeodesy.datums import _WGS84, _xinstanceof 

21from pygeodesy.ecef import _EcefBase, EcefKarney, _llhn4, _xyzn4 

22from pygeodesy.errors import _NotImplementedError, _TypesError, _ValueError, \ 

23 _xattr, _xkwds, _xkwds_get, _xkwds_pop2 

24from pygeodesy.fmath import fabs, fdot, Fhorner 

25from pygeodesy.fsums import _floor, _Fsumf_, fsumf_, fsum1f_ 

26from pygeodesy.interns import _0_, _COMMASPACE_, _DOT_, _ecef_, _height_, _M_, \ 

27 _invalid_, _lat0_, _lon0_, _ltp_, _name_, _too_ 

28# from pygeodesy.lazily import _ALL_LAZY # from vector3d 

29from pygeodesy.ltpTuples import Attitude4Tuple, ChLVEN2Tuple, ChLV9Tuple, \ 

30 ChLVYX2Tuple, Footprint5Tuple, Local9Tuple, \ 

31 ChLVyx2Tuple, _XyzLocals4, _XyzLocals5, Xyz4Tuple 

32from pygeodesy.named import _name__, _NamedBase, notOverloaded 

33from pygeodesy.namedTuples import LatLon3Tuple, LatLon4Tuple, Vector3Tuple 

34from pygeodesy.props import Property, Property_RO, property_doc_, property_RO, \ 

35 _update_all 

36from pygeodesy.streprs import Fmt, strs, unstr 

37from pygeodesy.units import Bearing, Degrees, _isHeight, Meter 

38from pygeodesy.utily import cotd, _loneg, sincos2d, sincos2d_, tand, tand_, \ 

39 wrap180, wrap360 

40from pygeodesy.vector3d import _ALL_LAZY, Vector3d 

41 

42# from math import fabs, floor as _floor # from .fmath, .fsums 

43 

44__all__ = _ALL_LAZY.ltp 

45__version__ = '24.04.23' 

46 

47_height0_ = _height_ + _0_ 

48_narrow_ = 'narrow' 

49_wide_ = 'wide' 

50_Xyz_ = 'Xyz' 

51 

52 

53def _fov_2(**fov): 

54 # Half a field-of-view angle in C{degrees}. 

55 f = Degrees(Error=LocalError, **fov) * _0_5 

56 if EPS < f < _90_0: 

57 return f 

58 t = _invalid_ if f < 0 else _too_(_wide_ if f > EPS else _narrow_) 

59 raise LocalError(txt=t, **fov) 

60 

61 

62class Attitude(_NamedBase): 

63 '''The orientation of a plane or camera in space. 

64 ''' 

65 _alt = Meter( alt =_0_0) 

66 _roll = Degrees(roll=_0_0) 

67 _tilt = Degrees(tilt=_0_0) 

68 _yaw = Bearing(yaw =_0_0) 

69 

70 def __init__(self, alt_attitude=INT0, tilt=INT0, yaw=INT0, roll=INT0, **name): 

71 '''New L{Attitude}. 

72 

73 @kwarg alt_attitude: An altitude (C{meter}) above earth or an attitude 

74 (L{Attitude} or L{Attitude4Tuple}) with the 

75 C{B{alt}itude}, B{C{tilt}}, B{C{yaw}} and B{C{roll}}. 

76 @kwarg tilt: Pitch, elevation from horizontal (C{degrees180}), negative down 

77 (clockwise rotation along and around the x- or East axis). 

78 @kwarg yaw: Bearing, heading (compass C{degrees360}), clockwise from North 

79 (counter-clockwise rotation along and around the z- or Up axis). 

80 @kwarg roll: Roll, bank (C{degrees180}), positive to the right and down 

81 (clockwise rotation along and around the y- or North axis). 

82 @kwarg name: Optional C{B{name}=NN} C{str}). 

83 

84 @raise AttitudeError: Invalid B{C{alt_attitude}}, B{C{tilt}}, B{C{yaw}} or 

85 B{C{roll}}. 

86 

87 @see: U{Principal axes<https://WikiPedia.org/wiki/Aircraft_principal_axes>} and 

88 U{Yaw, pitch, and roll rotations<http://MSL.CS.UIUC.edu/planning/node102.html>}. 

89 ''' 

90 if _isHeight(alt_attitude): 

91 t = Attitude4Tuple(alt_attitude, tilt, yaw, roll) 

92 else: 

93 try: 

94 t = alt_attitude.atyr 

95 except AttributeError: 

96 raise AttitudeError(alt=alt_attitude, tilt=tilt, yaw=yaw, rol=roll) 

97 for n, v in t.items(): 

98 if v: 

99 setattr(self, n, v) 

100 n = _name__(name, _or_nameof=t) 

101 if n: 

102 self.name = n 

103 

104 @property_doc_(' altitude above earth in C{meter}.') 

105 def alt(self): 

106 return self._alt 

107 

108 @alt.setter # PYCHOK setter! 

109 def alt(self, alt): # PYCHOK no cover 

110 a = Meter(alt=alt, Error=AttitudeError) 

111 if self._alt != a: 

112 _update_all(self) 

113 self._alt = a 

114 

115 altitude = alt 

116 

117 @Property_RO 

118 def atyr(self): 

119 '''Return this attitude's alt[itude], tilt, yaw and roll as an L{Attitude4Tuple}. 

120 ''' 

121 return Attitude4Tuple(self.alt, self.tilt, self.yaw, self.roll, name=self.name) 

122 

123 @Property_RO 

124 def matrix(self): 

125 '''Get the 3x3 rotation matrix C{R(yaw)·R(tilt)·R(roll)}, aka I{ZYX} (C{float}, row-order). 

126 

127 @see: Matrix M of case 10 in U{Appendix A 

128 <https://ntrs.NASA.gov/api/citations/19770019231/downloads/19770019231.pdf>}. 

129 ''' 

130 _f = fsum1f_ 

131 # to follow the definitions of rotation angles alpha, beta and gamma: 

132 # negate yaw since yaw is counter-clockwise around the z-axis, swap 

133 # tilt and roll since tilt is around the x- and roll around the y-axis 

134 sa, ca, sb, cb, sg, cg = sincos2d_(-self.yaw, self.roll, self.tilt) 

135 return ((ca * cb, _f(ca * sb * sg, -sa * cg), _f(ca * sb * cg, sa * sg)), 

136 (sa * cb, _f(sa * sb * sg, ca * cg), _f(sa * sb * cg, -ca * sg)), 

137 ( -sb, cb * sg, cb * cg)) 

138 

139 @property_doc_(' roll/bank in C{degrees180}, positive to the right and down.') 

140 def roll(self): 

141 return self._roll 

142 

143 @roll.setter # PYCHOK setter! 

144 def roll(self, roll): 

145 r = Degrees(roll=roll, wrap=wrap180, Error=AttitudeError) 

146 if self._roll != r: 

147 _update_all(self) 

148 self._roll = r 

149 

150 bank = roll 

151 

152 def rotate(self, x_xyz, y=None, z=None, Vector=None, **Vector_kwds): 

153 '''Transform a (local) cartesian by this attitude's matrix. 

154 

155 @arg x_xyz: X component of vector (C{scalar}) or (3-D) vector 

156 (C{Cartesian}, L{Vector3d} or L{Vector3Tuple}). 

157 @kwarg y: Y component of vector (C{scalar}), same units as B{C{x}}. 

158 @kwarg z: Z component of vector (C{scalar}), same units as B{C{x}}. 

159 @kwarg Vector: Class to return transformed point (C{Cartesian}, 

160 L{Vector3d} or C{Vector3Tuple}) or C{None}. 

161 @kwarg Vector_kwds: Optional, additional B{C{Vector}} keyword arguments, 

162 ignored if C{B{Vector} is None}. 

163 

164 @return: A B{C{Vector}} instance or a L{Vector3Tuple}C{(x, y, z)} if 

165 C{B{Vector}=None}. 

166 

167 @raise AttitudeError: Invalid B{C{x_xyz}}, B{C{y}} or B{C{z}}. 

168 

169 @see: U{Yaw, pitch, and roll rotations<http://MSL.CS.UIUC.edu/planning/node102.html>}. 

170 ''' 

171 try: 

172 try: 

173 xyz = map2(float, x_xyz.xyz) 

174 except AttributeError: 

175 xyz = map1(float, x_xyz, y, z) 

176 except (TypeError, ValueError) as x: 

177 raise AttitudeError(x_xyz=x_xyz, y=y, z=z, cause=x) 

178 

179 x, y, z = (fdot(r, *xyz) for r in self.matrix) 

180 return Vector3Tuple(x, y, z, name=self.name) if Vector is None else \ 

181 Vector(x, y, z, **_xkwds(Vector_kwds, name=self.name)) 

182 

183 @property_doc_(' tilt/pitch/elevation from horizontal in C{degrees180}, negative down.') 

184 def tilt(self): 

185 return self._tilt 

186 

187 @tilt.setter # PYCHOK setter! 

188 def tilt(self, tilt): 

189 t = Degrees(tilt=tilt, wrap=wrap180, Error=AttitudeError) 

190 if self._tilt != t: 

191 _update_all(self) 

192 self._tilt = t 

193 

194 elevation = pitch = tilt 

195 

196 def toStr(self, prec=6, sep=_COMMASPACE_, **unused): # PYCHOK signature 

197 '''Format this attitude as string. 

198 

199 @kwarg prec: The C{float} precision, number of decimal digits (0..9). 

200 Trailing zero decimals are stripped for B{C{prec}} values 

201 of 1 and above, but kept for negative B{C{prec}} values. 

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

203 

204 @return: This attitude (C{str}). 

205 ''' 

206 return self.atyr.toStr(prec=prec, sep=sep) 

207 

208 @Property_RO 

209 def tyr3d(self): 

210 '''Get this attitude's (3-D) directional vector (L{Vector3d}). 

211 

212 @see: U{Yaw, pitch, and roll rotations<http://MSL.CS.UIUC.edu/planning/node102.html>}. 

213 ''' 

214 def _r2d(r): 

215 return fsumf_(_N_1_0, *r) 

216 

217 return Vector3d(*map(_r2d, self.matrix), name__=tyr3d) 

218 

219 @property_doc_(' yaw/bearing/heading in compass C{degrees360}, clockwise from North.') 

220 def yaw(self): 

221 return self._yaw 

222 

223 @yaw.setter # PYCHOK setter! 

224 def yaw(self, yaw): 

225 y = Bearing(yaw=yaw, Error=AttitudeError) 

226 if self._yaw != y: 

227 _update_all(self) 

228 self._yaw = y 

229 

230 bearing = heading = yaw 

231 

232 

233class AttitudeError(_ValueError): 

234 '''An L{Attitude} or L{Attitude4Tuple} issue. 

235 ''' 

236 pass 

237 

238 

239class Frustum(_NamedBase): 

240 '''A rectangular pyramid, typically representing a camera's I{field-of-view} 

241 (fov) and the intersection with (or projection to) a I{local tangent plane}. 

242 

243 @see: U{Viewing frustum<https://WikiPedia.org/wiki/Viewing_frustum>}. 

244 ''' 

245 _h_2 = _0_0 # half hfov in degrees 

246 _ltp = None # local tangent plane 

247 _tan_h_2 = _0_0 # tan(_h_2) 

248 _v_2 = _0_0 # half vfov in degrees 

249 

250 def __init__(self, hfov, vfov, ltp=None, **name): 

251 '''New L{Frustum}. 

252 

253 @arg hfov: Horizontal field-of-view (C{degrees180}). 

254 @arg vfov: Vertical field-of-view (C{degrees180}). 

255 @kwarg ltp: Optional I{local tangent plane} (L{Ltp}). 

256 @kwarg name: Optional C{B{name}=NN} (C{str}). 

257 

258 @raise LocalError: Invalid B{C{hfov}} or B{C{vfov}}. 

259 ''' 

260 self._h_2 = h = _fov_2(hfov=hfov) 

261 self._v_2 = _fov_2(vfov=vfov) 

262 

263 self._tan_h_2 = tand(h, hfov_2=h) 

264 

265 if ltp: 

266 self._ltp = _xLtp(ltp) 

267 if name: 

268 self.name # PYCHOK effect 

269 

270 def footprint5(self, alt_attitude, tilt=0, yaw=0, roll=0, z=_0_0, ltp=None, **name): # MCCABE 15 

271 '''Compute the center and corners of the intersection with (or projection 

272 to) the I{local tangent plane} (LTP). 

273 

274 @arg alt_attitude: An altitude (C{meter}) above I{local tangent plane} or 

275 an attitude (L{Attitude} or L{Attitude4Tuple}) with the 

276 C{B{alt}itude}, B{C{tilt}}, B{C{yaw}} and B{C{roll}}. 

277 @kwarg tilt: Pitch, elevation from horizontal (C{degrees}), negative down 

278 (clockwise rotation along and around the x- or East axis). 

279 @kwarg yaw: Bearing, heading (compass C{degrees}), clockwise from North 

280 (counter-clockwise rotation along and around the z- or Up axis). 

281 @kwarg roll: Roll, bank (C{degrees}), positive to the right and down 

282 (clockwise rotation along and around the y- or North axis). 

283 @kwarg z: Optional height of the footprint (C{meter}) above I{local tangent plane}. 

284 @kwarg ltp: The I{local tangent plane} (L{Ltp}), overriding this 

285 frustum's C{ltp}. 

286 @kwarg name: Optional C{B{name}=NN} (C{str}). 

287 

288 @return: A L{Footprint5Tuple}C{(center, upperleft, upperight, loweright, 

289 lowerleft)} with the C{center} and 4 corners, each an L{Xyz4Tuple}. 

290 

291 @raise TypeError: Invalid B{C{ltp}}. 

292 

293 @raise UnitError: Invalid B{C{altitude}}, B{C{tilt}}, B{C{roll}} or B{C{z}}. 

294 

295 @raise ValueError: If B{C{altitude}} too low, B{C{z}} too high or B{C{tilt}} 

296 or B{C{roll}} -including B{C{vfov}} respectively B{C{hfov}}- 

297 over the horizon. 

298 

299 @see: U{Principal axes<https://WikiPedia.org/wiki/Aircraft_principal_axes>}. 

300 ''' 

301 def _xy2(a, e, h_2, tan_h_2, r): 

302 # left and right corners, or swapped 

303 if r < EPS: # no roll 

304 r = a * tan_h_2 

305 l = -r # PYCHOK l is ell 

306 else: # roll 

307 r, l = tand_(r - h_2, r + h_2, roll_hfov=r) # PYCHOK l is ell 

308 r *= -a # negate right positive 

309 l *= -a # PYCHOK l is ell 

310 y = a * cotd(e, tilt_vfov=e) 

311 return (l, y), (r, y) 

312 

313 def _xyz5(b, xy5, z, ltp): 

314 # rotate (x, y)'s by bearing, clockwise 

315 s, c = sincos2d(b) 

316 _f = fsum1f_ 

317 for x, y in xy5: 

318 yield Xyz4Tuple(_f(x * c, y * s), 

319 _f(y * c, -x * s), z, ltp) 

320 

321 try: 

322 a, t, y, r = alt_attitude.atyr 

323 except AttributeError: 

324 a, t, y, r = alt_attitude, tilt, yaw, roll 

325 

326 a = Meter(altitude=a) 

327 if a < EPS: # too low 

328 raise _ValueError(altitude=a) 

329 if z: # PYCHOK no cover 

330 z = Meter(z=z) 

331 a -= z 

332 if a < EPS: # z above a 

333 raise _ValueError(altitude_z=a) 

334 else: 

335 z = _0_0 

336 

337 b = Degrees(yaw=y, wrap=wrap360) # bearing 

338 e = -Degrees(tilt=t, wrap=wrap180) # elevation, pitch 

339 if not EPS < e < _180_0: 

340 raise _ValueError(tilt=t) 

341 if e > _90_0: 

342 e = _loneg(e) 

343 b = _umod_360(b + _180_0) 

344 

345 r = Degrees(roll=r, wrap=wrap180) # roll center 

346 x = (-a * tand(r, roll=r)) if r else _0_0 

347 y = a * cotd(e, tilt=t) # ground range 

348 if fabs(y) < EPS: 

349 y = _0_0 

350 

351 v, h, t = self._v_2, self._h_2, self._tan_h_2 

352 # center and corners, clockwise from upperleft, rolled 

353 xy5 = ((x, y),) + _xy2(a, e - v, h, t, r) \ 

354 + _xy2(a, e + v, -h, -t, r) # swapped 

355 # turn center and corners by yaw, clockwise 

356 p = self.ltp if ltp is None else ltp # None OK 

357 return Footprint5Tuple(_xyz5(b, xy5, z, p), **name) # *_xyz5 

358 

359 @Property_RO 

360 def hfov(self): 

361 '''Get the horizontal C{fov} (C{degrees}). 

362 ''' 

363 return Degrees(hfov=self._h_2 * _2_0) 

364 

365 @Property_RO 

366 def ltp(self): 

367 '''Get the I{local tangent plane} (L{Ltp}) or C{None}. 

368 ''' 

369 return self._ltp 

370 

371 def toStr(self, prec=3, fmt=Fmt.F, sep=_COMMASPACE_): # PYCHOK signature 

372 '''Convert this frustum to a "hfov, vfov, ltp" string. 

373 

374 @kwarg prec: Number of (decimal) digits, unstripped (0..8 or C{None}). 

375 @kwarg fmt: Optional, C{float} format (C{letter}). 

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

377 

378 @return: Frustum in the specified form (C{str}). 

379 ''' 

380 t = self.hfov, self.vfov 

381 if self.ltp: 

382 t += self.ltp, 

383 t = strs(t, prec=prec, fmt=fmt) 

384 return sep.join(t) if sep else t 

385 

386 @Property_RO 

387 def vfov(self): 

388 '''Get the vertical C{fov} (C{degrees}). 

389 ''' 

390 return Degrees(vfov=self._v_2 * _2_0) 

391 

392 

393class LocalError(_ValueError): 

394 '''A L{LocalCartesian} or L{Ltp} related issue. 

395 ''' 

396 pass 

397 

398 

399class LocalCartesian(_NamedBase): 

400 '''Conversion between geodetic C{(lat, lon, height)} and I{local 

401 cartesian} C{(x, y, z)} coordinates with I{geodetic} origin 

402 C{(lat0, lon0, height0)}, transcoded from I{Karney}'s C++ class 

403 U{LocalCartesian<https://GeographicLib.SourceForge.io/C++/doc/ 

404 classGeographicLib_1_1LocalCartesian.html>}. 

405 

406 The C{z} axis is normal to the ellipsoid, the C{y} axis points due 

407 North. The plane C{z = -height0} is tangent to the ellipsoid. 

408 

409 The conversions all take place via geocentric coordinates using a 

410 geocentric L{EcefKarney}, by default the WGS84 datum/ellipsoid. 

411 

412 @see: Class L{Ltp}. 

413 ''' 

414 _ecef = EcefKarney(_WGS84) 

415 _Ecef = EcefKarney 

416 _lon00 = INT0 # self.lon0 

417 _t0 = None # origin (..., lat0, lon0, height0, ...) L{Ecef9Tuple} 

418 _9Tuple = Local9Tuple 

419 

420 def __init__(self, latlonh0=INT0, lon0=INT0, height0=INT0, ecef=None, **lon00_name): 

421 '''New L{LocalCartesian} converter. 

422 

423 @kwarg latlonh0: The (geodetic) origin (C{LatLon}, L{LatLon4Tuple}, L{Ltp} 

424 L{LocalCartesian} or L{Ecef9Tuple}) or the C{scalar} 

425 latitude of the (goedetic) origin (C{degrees}). 

426 @kwarg lon0: Longitude of the (goedetic) origin (C{degrees}) for C{scalar} 

427 B{C{latlonh0}}, ignored otherwise. 

428 @kwarg height0: Optional height (C{meter}, conventionally) at the (goedetic) 

429 origin perpendicular to and above (or below) the ellipsoid's 

430 surface and for C{scalar} B{C{latlonh0}}, ignored otherwise. 

431 @kwarg ecef: An ECEF converter (L{EcefKarney} I{only}) for C{scalar} 

432 B{C{latlonh0}}, ignored otherwise. 

433 @kwarg lon00_name: Optional C{B{name}=NN} (C{str}) and keyword argument 

434 C{B{lon00}=B{lon0}} for the arbitrary I{polar} longitude 

435 (C{degrees}), see method C{reverse} and property C{lon00} 

436 for further details. 

437 

438 @raise LocalError: If B{C{latlonh0}} not C{LatLon}, L{LatLon4Tuple}, L{Ltp}, 

439 L{LocalCartesian} or L{Ecef9Tuple} or B{C{latlonh0}}, 

440 B{C{lon0}}, B{C{height0}} or B{C{lon00}} invalid. 

441 

442 @raise TypeError: Invalid B{C{ecef}} or not L{EcefKarney}. 

443 

444 @note: If BC{latlonh0} is an L{Ltp} or L{LocalCartesian}, only C{lat0}, C{lon0}, 

445 C{height0} and I{polar} C{lon00} are copied, I{not} the ECEF converter. 

446 ''' 

447 self.reset(latlonh0, lon0=lon0, height0=height0, ecef=ecef, **lon00_name) 

448 

449 def __eq__(self, other): 

450 '''Compare this and an other instance. 

451 

452 @arg other: The other ellipsoid (L{LocalCartesian} or L{Ltp}). 

453 

454 @return: C{True} if equal, C{False} otherwise. 

455 ''' 

456 return other is self or (isinstance(other, self.__class__) and 

457 other.ecef == self.ecef and 

458 other._t0 == self._t0) 

459 

460 @Property_RO 

461 def datum(self): 

462 '''Get the ECEF converter's datum (L{Datum}). 

463 ''' 

464 return self.ecef.datum 

465 

466 @Property_RO 

467 def ecef(self): 

468 '''Get the ECEF converter (L{EcefKarney}). 

469 ''' 

470 return self._ecef 

471 

472 def _ecef2local(self, ecef, Xyz, Xyz_kwds): 

473 '''(INTERNAL) Convert geocentric/geodetic to local, like I{forward}. 

474 

475 @arg ecef: Geocentric (and geodetic) (L{Ecef9Tuple}). 

476 @arg Xyz: An L{XyzLocal}, L{Enu} or L{Ned} I{class} or C{None}. 

477 @arg Xyz_kwds: B{C{Xyz}} keyword arguments, ignored if C{B{Xyz} is None}. 

478 

479 @return: An C{B{Xyz}(x, y, z, ltp, **B{Xyz_kwds}} instance or if 

480 C{B{Xyz} is None}, a L{Local9Tuple}C{(x, y, z, lat, lon, 

481 height, ltp, ecef, M)} with this C{ltp}, B{C{ecef}} 

482 (L{Ecef9Tuple}) converted to this C{datum} and C{M=None}, 

483 always. 

484 ''' 

485 ltp = self 

486 if ecef.datum != ltp.datum: 

487 ecef = ecef.toDatum(ltp.datum) 

488 x, y, z = self.M.rotate(ecef.xyz, *ltp._t0_xyz) 

489 r = Local9Tuple(x, y, z, ecef.lat, ecef.lon, ecef.height, 

490 ltp, ecef, None, name=ecef.name) 

491 if Xyz: 

492 if not issubclassof(Xyz, *_XyzLocals4): # Vector3d 

493 raise _TypesError(_Xyz_, Xyz, *_XyzLocals4) 

494 r = r.toXyz(Xyz=Xyz, **Xyz_kwds) 

495 return r 

496 

497 @Property_RO 

498 def ellipsoid(self): 

499 '''Get the ECEF converter's ellipsoid (L{Ellipsoid}). 

500 ''' 

501 return self.ecef.datum.ellipsoid 

502 

503 def forward(self, latlonh, lon=None, height=0, M=False, **name): 

504 '''Convert I{geodetic} C{(lat, lon, height)} to I{local} cartesian 

505 C{(x, y, z)}. 

506 

507 @arg latlonh: Either a C{LatLon}, L{Ltp}, L{Ecef9Tuple} or C{scalar} 

508 (geodetic) latitude (C{degrees}). 

509 @kwarg lon: Optional C{scalar} (geodetic) longitude for C{scalar} 

510 B{C{latlonh}} (C{degrees}). 

511 @kwarg height: Optional height (C{meter}, conventionally) perpendicular 

512 to and above (or below) the ellipsoid's surface. 

513 @kwarg M: Optionally, return the I{concatenated} rotation L{EcefMatrix}, 

514 iff available (C{bool}). 

515 @kwarg name: Optional C{B{name}=NN} (C{str}). 

516 

517 @return: A L{Local9Tuple}C{(x, y, z, lat, lon, height, ltp, ecef, M)} 

518 with I{local} C{x}, C{y}, C{z}, I{geodetic} C{(lat}, C{lon}, 

519 C{height}, this C{ltp}, C{ecef} (L{Ecef9Tuple}) with 

520 I{geocentric} C{x}, C{y}, C{z} (and I{geodetic} C{lat}, 

521 C{lon}, C{height}) and the I{concatenated} rotation matrix 

522 C{M} (L{EcefMatrix}) if requested. 

523 

524 @raise LocalError: If B{C{latlonh}} not C{scalar}, C{LatLon}, L{Ltp}, 

525 L{Ecef9Tuple} or invalid or if B{C{lon}} not 

526 C{scalar} for C{scalar} B{C{latlonh}} or invalid 

527 or if B{C{height}} invalid. 

528 ''' 

529 lat, lon, h, n = _llhn4(latlonh, lon, height, Error=LocalError, **name) 

530 t = self.ecef._forward(lat, lon, h, n, M=M) 

531 x, y, z = self.M.rotate(t.xyz, *self._t0_xyz) 

532 m = self.M.multiply(t.M) if M else None 

533 return self._9Tuple(x, y, z, lat, lon, h, self, t, m, name=n or self.name) 

534 

535 @Property_RO 

536 def height0(self): 

537 '''Get the origin's height (C{meter}). 

538 ''' 

539 return self._t0.height 

540 

541 @Property_RO 

542 def lat0(self): 

543 '''Get the origin's latitude (C{degrees}). 

544 ''' 

545 return self._t0.lat 

546 

547 @Property_RO 

548 def latlonheight0(self): 

549 '''Get the origin's lat-, longitude and height (L{LatLon3Tuple}C{(lat, lon, height)}). 

550 ''' 

551 return LatLon3Tuple(self.lat0, self.lon0, self.height0, name=self.name) 

552 

553 def _local2ecef(self, local, nine=False, M=False): 

554 '''(INTERNAL) Convert I{local} to geocentric/geodetic, like I{.reverse}. 

555 

556 @arg local: Local (L{XyzLocal}, L{Enu}, L{Ned}, L{Aer} or L{Local9Tuple}). 

557 @kwarg nine: Return 3- or 9-tuple (C{bool}). 

558 @kwarg M: Include the rotation matrix (C{bool}). 

559 

560 @return: A I{geocentric} 3-tuple C{(x, y, z)} or if C{B{nine}=True}, 

561 an L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)}, 

562 optionally including rotation matrix C{M} or C{None}. 

563 ''' 

564 t = self.M.unrotate(local.xyz, *self._t0_xyz) 

565 if nine: 

566 t = self.ecef.reverse(*t, M=M) 

567 return t 

568 

569 @Property_RO 

570 def lon0(self): 

571 '''Get the origin's longitude (C{degrees}). 

572 ''' 

573 return self._t0.lon 

574 

575 @Property 

576 def lon00(self): 

577 '''Get the arbitrary, I{polar} longitude (C{degrees}). 

578 ''' 

579 return self._lon00 

580 

581 @lon00.setter # PYCHOK setter! 

582 def lon00(self, lon00): 

583 '''Set the arbitrary, I{polar} longitude (C{degrees}). 

584 ''' 

585 # lon00 <https://GitHub.com/mrJean1/PyGeodesy/issues/77> 

586 self._lon00 = Degrees(lon00=lon00) 

587 

588 @Property_RO 

589 def M(self): 

590 '''Get the rotation matrix (C{EcefMatrix}). 

591 ''' 

592 return self._t0.M 

593 

594 def reset(self, latlonh0=INT0, lon0=INT0, height0=INT0, ecef=None, **lon00_name): 

595 '''Reset this converter, see L{LocalCartesian.__init__} for more details. 

596 ''' 

597 _, name = _xkwds_pop2(lon00_name, lon00=None) # PYCHOK get **name 

598 if isinstance(latlonh0, LocalCartesian): 

599 if self._t0: 

600 _update_all(self) 

601 self._ecef = latlonh0.ecef 

602 self._lon00 = latlonh0.lon00 

603 self._t0 = latlonh0._t0 

604 n = _name__(name, _or_nameof=latlonh0) 

605 else: 

606 n = _name__(name, _or_nameof=self) 

607 lat0, lon0, height0, n = _llhn4(latlonh0, lon0, height0, suffix=_0_, 

608 Error=LocalError, name=n) 

609 if ecef: # PYCHOK no cover 

610 _xinstanceof(self._Ecef, ecef=ecef) 

611 _update_all(self) 

612 self._ecef = ecef 

613 elif self._t0: 

614 _update_all(self) 

615 self._t0 = self.ecef._forward(lat0, lon0, height0, n, M=True) 

616 self.lon00 = _xattr(latlonh0, lon00=_xkwds_get(lon00_name, lon00=lon0)) 

617 if n: 

618 self.rename(n) 

619 

620 def reverse(self, xyz, y=None, z=None, M=False, **lon00_name): 

621 '''Convert I{local} C{(x, y, z)} to I{geodetic} C{(lat, lon, height)}. 

622 

623 @arg xyz: A I{local} (L{XyzLocal}, L{Enu}, L{Ned}, L{Aer}, L{Local9Tuple}) or 

624 local C{x} coordinate (C{scalar}). 

625 @kwarg y: Local C{y} coordinate for C{scalar} B{C{xyz}} and B{C{z}} (C{meter}). 

626 @kwarg z: Local C{z} coordinate for C{scalar} B{C{xyz}} and B{C{y}} (C{meter}). 

627 @kwarg M: Optionally, return the I{concatenated} rotation L{EcefMatrix}, iff 

628 available (C{bool}). 

629 @kwarg lon00_name: Optional C{B{name}=NN} (C{str}) and keyword argument 

630 C{B{lon00}=B{lon0}} for the arbitrary I{polar} longitude 

631 (C{degrees}), overriding see the property C{B{lon00}=B{lon0}} 

632 value. The I{polar} longitude (C{degrees}) is returned with 

633 I{polar} latitudes C{abs(B{lat0}) == 90} for local C{B{x}=0} 

634 and C{B{y}=0} locations. 

635 

636 @return: An L{Local9Tuple}C{(x, y, z, lat, lon, height, ltp, ecef, M)} with 

637 I{local} C{x}, C{y}, C{z}, I{geodetic} C{lat}, C{lon}, C{height}, 

638 this C{ltp}, an C{ecef} (L{Ecef9Tuple}) with the I{geocentric} C{x}, 

639 C{y}, C{z} (and I{geodetic} C{lat}, C{lon}, C{height}) and the 

640 I{concatenated} rotation matrix C{M} (L{EcefMatrix}) if requested. 

641 

642 @raise LocalError: Invalid B{C{xyz}} or C{scalar} C{x} or B{C{y}} and/or B{C{z}} 

643 not C{scalar} for C{scalar} B{C{xyz}}. 

644 ''' 

645 lon00, name =_xkwds_pop2(lon00_name, lon00=self.lon00) 

646 x, y, z, n = _xyzn4(xyz, y, z, _XyzLocals5, Error=LocalError, name=name) 

647 c = self.M.unrotate((x, y, z), *self._t0_xyz) 

648 t = self.ecef.reverse(*c, M=M, lon00=lon00) 

649 m = self.M.multiply(t.M) if M else None 

650 return self._9Tuple(x, y, z, t.lat, t.lon, t.height, self, t, m, name=n or self.name) 

651 

652 @Property_RO 

653 def _t0_xyz(self): 

654 '''(INTERNAL) Get C{(x0, y0, z0)} as L{Vector3Tuple}. 

655 ''' 

656 return self._t0.xyz 

657 

658 def toStr(self, prec=9, **unused): # PYCHOK signature 

659 '''Return this L{LocalCartesian} as a string. 

660 

661 @kwarg prec: Precision, number of (decimal) digits (0..9). 

662 

663 @return: This L{LocalCartesian} representation (C{str}). 

664 ''' 

665 return self.attrs(_lat0_, _lon0_, _height0_, _M_, _ecef_, _name_, prec=prec) 

666 

667 

668class Ltp(LocalCartesian): 

669 '''A I{local tangent plan} (LTP), a sub-class of C{LocalCartesian} with 

670 (re-)configurable ECEF converter. 

671 ''' 

672 _Ecef = _EcefBase 

673 

674 def __init__(self, latlonh0=INT0, lon0=INT0, height0=INT0, ecef=None, **lon00_name): 

675 '''New C{Ltp}, see L{LocalCartesian.__init__} for more details. 

676 

677 @kwarg ecef: Optional ECEF converter (L{EcefKarney}, L{EcefFarrell21}, 

678 L{EcefFarrell22}, L{EcefSudano}, L{EcefVeness} or 

679 L{EcefYou} I{instance}), overriding the default 

680 L{EcefKarney}C{(datum=Datums.WGS84)} for C{scalar}. 

681 

682 @see: Class L{LocalCartesian<LocalCartesian.__init__>} for further details. 

683 

684 @raise TypeError: Invalid B{C{ecef}}. 

685 ''' 

686 LocalCartesian.reset(self, latlonh0, lon0=lon0, height0=height0, 

687 ecef=ecef, **lon00_name) 

688 

689 @Property 

690 def ecef(self): 

691 '''Get this LTP's ECEF converter (C{Ecef...} I{instance}). 

692 ''' 

693 return self._ecef 

694 

695 @ecef.setter # PYCHOK setter! 

696 def ecef(self, ecef): 

697 '''Set this LTP's ECEF converter (C{Ecef...} I{instance}). 

698 

699 @raise TypeError: Invalid B{C{ecef}}. 

700 ''' 

701 _xinstanceof(_EcefBase, ecef=ecef) 

702 if self._ecef != ecef: # PYCHOK no cover 

703 self.reset(self._t0) 

704 self._ecef = ecef 

705 

706 

707class _ChLV(object): 

708 '''(INTERNAL) Base class for C{ChLV*} classes. 

709 ''' 

710 _03_falsing = ChLVyx2Tuple(0.6e6, 0.2e6) 

711# _92_falsing = ChLVYX2Tuple(2.0e6, 1.0e6) # _95_ - _03_ 

712 _95_falsing = ChLVEN2Tuple(2.6e6, 1.2e6) 

713 

714 def _ChLV9Tuple(self, fw, M, name, *Y_X_h_lat_lon_h): 

715 '''(INTERNAL) Helper for C{ChLVa/e.forward} and C{.reverse}. 

716 ''' 

717 if bool(M): # PYCHOK no cover 

718 m = self.forward if fw else self.reverse # PYCHOK attr 

719 n = _DOT_(self.__class__.__name__, m.__name__) 

720 raise _NotImplementedError(unstr(n, M=M), txt=None) 

721 t = Y_X_h_lat_lon_h + (self, self._t0, None) # PYCHOK _t0 

722 return ChLV9Tuple(t, name=name) 

723 

724 @property_RO 

725 def _enh_n_h(self): 

726 '''(INTERNAL) Get C{ChLV*.reverse} args[1:4] names, I{once}. 

727 ''' 

728 _ChLV._enh_n_h = t = _args_kwds_names(_ChLV.reverse)[1:4] # overwrite property_RO 

729 # assert _args_kwds_names( ChLV.reverse)[1:4] == t 

730 # assert _args_kwds_names(ChLVa.reverse)[1:4] == t 

731 # assert _args_kwds_names(ChLVe.reverse)[1:4] == t 

732 return t 

733 

734 def forward(self, latlonh, lon=None, height=0, M=None, **name): # PYCHOK no cover 

735 '''Convert WGS84 geodetic to I{Swiss} projection coordinates. I{Must be overloaded}. 

736 

737 @arg latlonh: Either a C{LatLon}, L{Ltp} or C{scalar} (geodetic) latitude (C{degrees}). 

738 @kwarg lon: Optional, C{scalar} (geodetic) longitude for C{scalar} B{C{latlonh}} (C{degrees}). 

739 @kwarg height: Optional, height, vertically above (or below) the surface of the ellipsoid 

740 (C{meter}) for C{scalar} B{C{latlonh}} and B{C{lon}}. 

741 @kwarg M: If C{True}, return the I{concatenated} rotation L{EcefMatrix} iff available 

742 for C{ChLV} only, C{None} otherwise (C{bool}). 

743 @kwarg name: Optional C{B{name}=NN} (C{str}). 

744 

745 @return: A L{ChLV9Tuple}C{(Y, X, h_, lat, lon, height, ltp, ecef, M)} with the unfalsed 

746 I{Swiss Y, X} coordinates, I{Swiss h_} height, the given I{geodetic} C{lat}, 

747 C{lon} and C{height}, this C{ChLV*} instance and C{ecef} (L{Ecef9Tuple}) at 

748 I{Bern, Ch} and rotation matrix C{M}. The returned C{ltp} is this C{ChLV}, 

749 C{ChLVa} or C{ChLVe} instance. 

750 

751 @raise LocalError: Invalid or non-C{scalar} B{C{latlonh}}, B{C{lon}} or B{C{height}}. 

752 ''' 

753 notOverloaded(self, latlonh, lon=lon, height=height, M=M, **name) 

754 

755 def reverse(self, enh_, n=None, h_=0, M=None, **name): # PYCHOK no cover 

756 '''Convert I{Swiss} projection to WGS84 geodetic coordinates. 

757 

758 @arg enh_: A Swiss projection (L{ChLV9Tuple}) or the C{scalar}, falsed I{Swiss E_LV95} 

759 or I{y_LV03} easting (C{meter}). 

760 @kwarg n: Falsed I{Swiss N_LV85} or I{x_LV03} northing for C{scalar} B{C{enh_}} and 

761 B{C{h_}} (C{meter}). 

762 @kwarg h_: I{Swiss h'} height for C{scalar} B{C{enh_}} and B{C{n}} (C{meter}). 

763 @kwarg M: If C{True}, return the I{concatenated} rotation L{EcefMatrix} iff available 

764 for C{ChLV} only, C{None} otherwise (C{bool}). 

765 @kwarg name: Optional C{B{name}=NN} (C{str}). 

766 

767 @return: A L{ChLV9Tuple}C{(Y, X, h_, lat, lon, height, ltp, ecef, M)} with the unfalsed 

768 I{Swiss Y, X} coordinates, I{Swiss h_} height, the given I{geodetic} C{lat}, 

769 C{lon} and C{height}, this C{ChLV*} instance and C{ecef} (L{Ecef9Tuple}) at 

770 I{Bern, Ch} and rotation matrix C{M}. The returned C{ltp} is this C{ChLV}, 

771 C{ChLVa} or C{ChLVe} instance. 

772 

773 @raise LocalError: Invalid or non-C{scalar} B{C{enh_}}, B{C{n}} or B{C{h_}}. 

774 ''' 

775 notOverloaded(self, enh_, n=n, h_=h_, M=M, **name) 

776 

777 @staticmethod 

778 def _falsing2(LV95): 

779 '''(INTERNAL) Get the C{LV95} or C{LV03} falsing. 

780 ''' 

781 return _ChLV._95_falsing if LV95 in (True, 95) else ( 

782 _ChLV._03_falsing if LV95 in (False, 3) else ChLVYX2Tuple(0, 0)) 

783 

784 @staticmethod 

785 def _llh2abh_3(lat, lon, h): 

786 '''(INTERNAL) Helper for C{ChLVa/e.forward}. 

787 ''' 

788 def _deg2ab(deg, sLL): 

789 # convert degrees to arc-seconds 

790 def _dms(ds, p, q, swap): 

791 d = _floor(ds) 

792 t = (ds - d) * p 

793 m = _floor(t) 

794 s = (t - m) * p 

795 if swap: 

796 d, s = s, d 

797 return d + (m + s * q) * q 

798 

799 s = _dms(deg, _60_0, _0_01, False) # deg2sexag 

800 s = _dms( s, _100_0, _60_0, True) # sexag2asec 

801 return (s - sLL) / ChLV._s_ab 

802 

803 a = _deg2ab(lat, ChLV._sLat) # phi', lat_aux 

804 b = _deg2ab(lon, ChLV._sLon) # lam', lng_aux 

805 h_ = fsumf_(h, -ChLV.Bern.height, 2.73 * b, 6.94 * a) 

806 return a, b, h_ 

807 

808 @staticmethod 

809 def _YXh_2abh3(Y, X, h_): 

810 '''(INTERNAL) Helper for C{ChLVa/e.reverse}. 

811 ''' 

812 def _YX2ab(YX): 

813 return YX * ChLV._ab_m 

814 

815 a, b = map1(_YX2ab, Y, X) 

816 h = fsumf_(h_, ChLV.Bern.height, -12.6 * a, -22.64 * b) 

817 return a, b, h 

818 

819 def _YXh_n4(self, enh_, n, h_, **name): 

820 '''(INTERNAL) Helper for C{ChLV*.reverse}. 

821 ''' 

822 Y, X, h_, name = _xyzn4(enh_, n, h_, ChLV9Tuple, 

823 _xyz_y_z_names=self._enh_n_h, **name) 

824 if isinstance(enh_, ChLV9Tuple): 

825 Y, X = enh_.Y, enh_.X 

826 else: # isscalar(enh_) 

827 Y, X = ChLV.unfalse2(Y, X) # PYCHOK ChLVYX2Tuple 

828 return Y, X, h_, name 

829 

830 

831class ChLV(_ChLV, Ltp): 

832 '''Conversion between I{WGS84 geodetic} and I{Swiss} projection coordinates using 

833 L{pygeodesy.EcefKarney}'s Earth-Centered, Earth-Fixed (ECEF) methods. 

834 

835 @see: U{Swiss projection formulas<https://www.SwissTopo.admin.CH/en/maps-data-online/ 

836 calculation-services.html>}, page 7ff, U{NAVREF<https://www.SwissTopo.admin.CH/en/ 

837 maps-data-online/calculation-services/navref.html>}, U{REFRAME<https://www.SwissTopo.admin.CH/ 

838 en/maps-data-online/calculation-services/reframe.html>} and U{SwissTopo Scripts GPS WGS84 

839 <-> LV03<https://GitHub.com/ValentinMinder/Swisstopo-WGS84-LV03>}. 

840 ''' 

841 _9Tuple = ChLV9Tuple 

842 

843 _ab_d = 0.36 # a, b units per degree, ... 

844 _ab_m = 1.0e-6 # ... per meter and ... 

845 _ab_M = _1_0 # ... per 1,000 Km or 1 Mm 

846 _s_d = _3600_0 # arc-seconds per degree ... 

847 _s_ab = _s_d / _ab_d # ... and per a, b unit 

848 _sLat = 169028.66 # Bern, Ch in ... 

849 _sLon = 26782.5 # ... arc-seconds ... 

850 # lat, lon, height == 46°57'08.66", 7°26'22.50", 49.55m ("new" 46°57'07.89", 7°26'22.335") 

851 Bern = LatLon4Tuple(_sLat / _s_d, _sLon / _s_d, 49.55, _WGS84, name='Bern') 

852 

853 def __init__(self, latlonh0=Bern, **other_Ltp_kwds): 

854 '''New ECEF-based I{WGS84-Swiss} L{ChLV} converter, centered at I{Bern, Ch}. 

855 

856 @kwarg latlonh0: The I{geodetic} origin and height, overriding C{Bern, Ch}. 

857 @kwarg other_Ltp_kwds: Optional, other L{Ltp.__init__} keyword arguments. 

858 

859 @see: L{Ltp.__init__} for more information. 

860 ''' 

861 Ltp.__init__(self, latlonh0, **_xkwds(other_Ltp_kwds, ecef=None, name=ChLV.Bern.name)) 

862 

863 def forward(self, latlonh, lon=None, height=0, M=None, **name): # PYCHOK unused M 

864 # overloaded for the _ChLV.forward.__doc__ 

865 return Ltp.forward(self, latlonh, lon=lon, height=height, M=M, **name) 

866 

867 def reverse(self, enh_, n=None, h_=0, M=None, **name): # PYCHOK signature 

868 # overloaded for the _ChLV.reverse.__doc__ 

869 Y, X, h_, n = self._YXh_n4(enh_, n, h_, **name) 

870 return Ltp.reverse(self, Y, X, h_, M=M, name=n) 

871 

872 @staticmethod 

873 def false2(Y, X, LV95=True, **name): 

874 '''Add the I{Swiss LV95} or I{LV03} falsing. 

875 

876 @arg Y: Unfalsed I{Swiss Y} easting (C{meter}). 

877 @arg X: Unfalsed I{Swiss X} northing (C{meter}). 

878 @kwarg LV95: If C{True} add C{LV95} falsing, if C{False} add 

879 C{LV03} falsing, otherwise leave unfalsed. 

880 @kwarg name: Optional C{B{name}=NN} (C{str}). 

881 

882 @return: A L{ChLVEN2Tuple}C{(E_LV95, N_LV95)} or a 

883 L{ChLVyx2Tuple}C{(y_LV03, x_LV03)} with falsed B{C{Y}} 

884 and B{C{X}}, otherwise a L{ChLVYX2Tuple}C{(Y, X)} 

885 with B{C{Y}} and B{C{X}} as-is. 

886 ''' 

887 e, n = t = _ChLV._falsing2(LV95) 

888 return t.classof(e + Y, n + X, **name) 

889 

890 @staticmethod 

891 def isLV03(e, n): 

892 '''Is C{(B{e}, B{n})} a valid I{Swiss LV03} projection? 

893 

894 @arg e: Falsed (or unfalsed) I{Swiss} easting (C{meter}). 

895 @arg n: Falsed (or unfalsed) I{Swiss} northing (C{meter}). 

896 

897 @return: C{True} if C{(B{e}, B{n})} is a valid, falsed I{Swiss 

898 LV03}, projection C{False} otherwise. 

899 ''' 

900 # @see: U{Map<https://www.SwissTopo.admin.CH/en/knowledge-facts/ 

901 # surveying-geodesy/reference-frames/local/lv95.html>} 

902 return 400.0e3 < e < 900.0e3 and 40.0e3 < n < 400.0e3 

903 

904 @staticmethod 

905 def isLV95(e, n, raiser=True): 

906 '''Is C{(B{e}, B{n})} a valid I{Swiss LV95} or I{LV03} projection? 

907 

908 @arg e: Falsed (or unfalsed) I{Swiss} easting (C{meter}). 

909 @arg n: Falsed (or unfalsed) I{Swiss} northing (C{meter}). 

910 @kwarg raiser: If C{True}, throw a L{LocalError} if B{C{e}} and 

911 B{C{n}} are invalid I{Swiss LV95} nor I{LV03}. 

912 

913 @return: C{True} or C{False} if C{(B{e}, B{n})} is a valid I{Swiss 

914 LV95} respectively I{LV03} projection, C{None} otherwise. 

915 ''' 

916 if ChLV.isLV03(e, n): 

917 return False 

918 elif ChLV.isLV03(e - 2.0e6, n - 1.0e6): # _92_falsing = _95_ - _03_ 

919 return True 

920 elif raiser: # PYCHOK no cover 

921 raise LocalError(unstr(ChLV.isLV95, e=e, n=n)) 

922 return None 

923 

924 @staticmethod 

925 def unfalse2(e, n, LV95=None, **name): 

926 '''Remove the I{Swiss LV95} or I{LV03} falsing. 

927 

928 @arg e: Falsed I{Swiss E_LV95} or I{y_LV03} easting (C{meter}). 

929 @arg n: Falsed I{Swiss N_LV95} or I{x_LV03} northing (C{meter}). 

930 @kwarg LV95: If C{True} remove I{LV95} falsing, if C{False} remove 

931 I{LV03} falsing, otherwise use method C{isLV95(B{e}, 

932 B{n})}. 

933 @kwarg name: Optional C{B{name}=NN} (C{str}). 

934 

935 @return: A L{ChLVYX2Tuple}C{(Y, X)} with the unfalsed B{C{e}} 

936 respectively B{C{n}}. 

937 ''' 

938 Y, X = _ChLV._falsing2(ChLV.isLV95(e, n) if LV95 is None else LV95) 

939 return ChLVYX2Tuple(e - Y, n - X, **name) 

940 

941 

942class ChLVa(_ChLV, LocalCartesian): 

943 '''Conversion between I{WGS84 geodetic} and I{Swiss} projection coordinates 

944 using the U{Approximate<https://www.SwissTopo.admin.CH/en/maps-data-online/ 

945 calculation-services.html>} formulas, page 13. 

946 

947 @see: Older U{references<https://GitHub.com/alphasldiallo/Swisstopo-WGS84-LV03>}. 

948 ''' 

949 def __init__(self, name=ChLV.Bern.name): 

950 '''New I{Approximate WGS84-Swiss} L{ChLVa} converter, centered at I{Bern, Ch}. 

951 

952 @kwarg name: Optional C{B{name}=Bern.name} (C{str}). 

953 ''' 

954 LocalCartesian.__init__(self, latlonh0=ChLV.Bern, name=name) 

955 

956 def forward(self, latlonh, lon=None, height=0, M=None, **name): 

957 # overloaded for the _ChLV.forward.__doc__ 

958 lat, lon, h, n = _llhn4(latlonh, lon, height, **name) 

959 a, b, h_ = _ChLV._llh2abh_3(lat, lon, h) 

960 a2, b2 = a**2, b**2 

961 

962 Y = fsumf_( 72.37, 211455.93 * b, 

963 -10938.51 * b * a, 

964 -0.36 * b * a2, 

965 -44.54 * b * b2) # + 600_000 

966 X = fsumf_(147.07, 308807.95 * a, 

967 3745.25 * b2, 

968 76.63 * a2, 

969 -194.56 * b2 * a, 

970 119.79 * a2 * a) # + 200_000 

971 return self._ChLV9Tuple(True, M, n, Y, X, h_, lat, lon, h) 

972 

973 def reverse(self, enh_, n=None, h_=0, M=None, **name): # PYCHOK signature 

974 # overloaded for the _ChLV.reverse.__doc__ 

975 Y, X, h_, n = self._YXh_n4(enh_, n, h_, **name) 

976 a, b, h = _ChLV._YXh_2abh3(Y, X, h_) 

977 ab_d, a2, b2 = ChLV._ab_d, a**2, b**2 

978 

979 lat = _Fsumf_(16.9023892, 3.238272 * b, 

980 -0.270978 * a2, 

981 -0.002528 * b2, 

982 -0.0447 * a2 * b, 

983 -0.014 * b2 * b).fover(ab_d) 

984 lon = _Fsumf_( 2.6779094, 4.728982 * a, 

985 0.791484 * a * b, 

986 0.1306 * a * b2, 

987 -0.0436 * a * a2).fover(ab_d) 

988 return self._ChLV9Tuple(False, M, n, Y, X, h_, lat, lon, h) 

989 

990 

991class ChLVe(_ChLV, LocalCartesian): 

992 '''Conversion between I{WGS84 geodetic} and I{Swiss} projection coordinates 

993 using the U{Ellipsoidal approximate<https://www.SwissTopo.admin.CH/en/ 

994 maps-data-online/calculation-services.html>} formulas, pp 10-11 and U{Bolliger, 

995 J.<https://eMuseum.GGGS.CH/literatur-lv/liste-Dateien/1967_Bolliger_a.pdf>} 

996 pp 148-151 (also U{GGGS<https://eMuseum.GGGS.CH/literatur-lv/liste.htm>}). 

997 

998 @note: Methods L{ChLVe.forward} and L{ChLVe.reverse} have an additional keyword 

999 argument C{B{gamma}=False} to approximate the I{meridian convergence}. 

1000 If C{B{gamma}=True} a 2-tuple C{(t, gamma)} is returned with C{t} the 

1001 usual result (C{ChLV9Tuple}) and C{gamma}, the I{meridian convergence} 

1002 (decimal C{degrees}). To convert C{gamma} to C{grades} or C{gons}, 

1003 use function L{pygeodesy.degrees2grades}. 

1004 

1005 @see: Older U{references<https://GitHub.com/alphasldiallo/Swisstopo-WGS84-LV03>}. 

1006 ''' 

1007 def __init__(self, name=ChLV.Bern.name): 

1008 '''New I{Approximate WGS84-Swiss} L{ChLVe} converter, centered at I{Bern, Ch}. 

1009 

1010 @kwarg name: Optional C{B{name}=Bern.name} (C{str}). 

1011 ''' 

1012 LocalCartesian.__init__(self, latlonh0=ChLV.Bern, name=name) 

1013 

1014 def forward(self, latlonh, lon=None, height=0, M=None, gamma=False, **name): # PYCHOK gamma 

1015 # overloaded for the _ChLV.forward.__doc__ 

1016 lat, lon, h, n = _llhn4(latlonh, lon, height, **name) 

1017 a, b, h_ = _ChLV._llh2abh_3(lat, lon, h) 

1018 ab_M, z, _H = ChLV._ab_M, 0, Fhorner 

1019 

1020 B1 = _H(a, 211428.533991, -10939.608605, -2.658213, -8.539078, -0.00345, -0.007992) 

1021 B3 = _H(a, -44.232717, 4.291740, -0.309883, 0.013924) 

1022 B5 = _H(a, 0.019784, -0.004277) 

1023 Y = _H(b, z, B1, z, B3, z, B5).fover(ab_M) # 1,000 Km! 

1024 

1025 B0 = _H(a, z, 308770.746371, 75.028131, 120.435227, 0.009488, 0.070332, -0.00001) 

1026 B2 = _H(a, 3745.408911, -193.792705, 4.340858, -0.376174, 0.004053) 

1027 B4 = _H(a, -0.734684, 0.144466, -0.011842) 

1028 B6 = 0.000488 

1029 X = _H(b, B0, z, B2, z, B4, z, B6).fover(ab_M) # 1,000 Km! 

1030 

1031 t = self._ChLV9Tuple(True, M, n, Y, X, h_, lat, lon, h) 

1032 if gamma: 

1033 U1 = _H(a, 2255515.207166, 2642.456961, 1.284180, 2.577486, 0.001165) 

1034 U3 = _H(a, -412.991934, 64.106344, -2.679566, 0.123833) 

1035 U5 = _H(a, 0.204129, -0.037725) 

1036 g = _H(b, z, U1, z, U3, z, U5).fover(ChLV._ab_m) # * ChLV._ab_d degrees? 

1037 t = t, g 

1038 return t 

1039 

1040 def reverse(self, enh_, n=None, h_=0, M=None, gamma=False, **name): # PYCHOK gamma 

1041 # overloaded for the _ChLV.reverse.__doc__ 

1042 Y, X, h_, n = self._YXh_n4(enh_, n, h_, **name) 

1043 a, b, h = _ChLV._YXh_2abh3(Y, X, h_) 

1044 s_d, _H, z = ChLV._s_d, Fhorner, 0 

1045 

1046 A0 = _H(b, ChLV._sLat, 32386.4877666, -25.486822, -132.457771, 0.48747, 0.81305, -0.0069) 

1047 A2 = _H(b, -2713.537919, -450.442705, -75.53194, -14.63049, -2.7604) 

1048 A4 = _H(b, 24.42786, 13.20703, 4.7476) 

1049 A6 = -0.4249 

1050 lat = _H(a, A0, z, A2, z, A4, z, A6).fover(s_d) 

1051 

1052 A1 = _H(b, 47297.3056722, 7925.714783, 1328.129667, 255.02202, 48.17474, 9.0243) 

1053 A3 = _H(b, -442.709889, -255.02202, -96.34947, -30.0808) 

1054 A5 = _H(b, 9.63495, 9.0243) 

1055 lon = _H(a, ChLV._sLon, A1, z, A3, z, A5).fover(s_d) 

1056 # == (ChLV._sLon + a * (A1 + a**2 * (A3 + a**2 * A5))) / s_d 

1057 

1058 t = self._ChLV9Tuple(False, M, n, Y, X, h_, lat, lon, h) 

1059 if gamma: 

1060 U1 = _H(b, 106679.792202, 17876.57022, 4306.5241, 794.87772, 148.1545, 27.8725) 

1061 U3 = _H(b, -1435.508, -794.8777, -296.309, -92.908) 

1062 U5 = _H(b, 29.631, 27.873) 

1063 g = _H(a, z, U1, z, U3, z, U5).fover(ChLV._s_ab) # degrees 

1064 t = t, g 

1065 return t 

1066 

1067 

1068def tyr3d(tilt=INT0, yaw=INT0, roll=INT0, Vector=Vector3d, **Vector_kwds): 

1069 '''Convert an attitude oriention into a (3-D) direction vector. 

1070 

1071 @kwarg tilt: Pitch, elevation from horizontal (C{degrees}), negative down 

1072 (clockwise rotation along and around the x-axis). 

1073 @kwarg yaw: Bearing, heading (compass C{degrees360}), clockwise from North 

1074 (counter-clockwise rotation along and around the z-axis). 

1075 @kwarg roll: Roll, bank (C{degrees}), positive to the right and down 

1076 (clockwise rotation along and around the y-axis). 

1077 

1078 @return: A named B{C{Vector}} instance or if B{C{Vector}} is C{None}, 

1079 a named L{Vector3Tuple}C{(x, y, z)}. 

1080 

1081 @see: U{Yaw, pitch, and roll rotations<http://MSL.CS.UIUC.edu/planning/node102.html>} 

1082 and function L{pygeodesy.hartzell} argument C{los}. 

1083 ''' 

1084 d = Attitude4Tuple(_0_0, tilt, yaw, roll).tyr3d 

1085 return d if Vector is type(d) else ( 

1086 Vector3Tuple(d.x, d.y, d.z, name=d.name) if Vector is None else 

1087 Vector(d.x, d.y, d.z, **_xkwds(Vector_kwds, name=d.name))) # PYCHOK indent 

1088 

1089 

1090def _xLtp(ltp, *dflt): 

1091 '''(INTERNAL) Validate B{C{ltp}}. 

1092 ''' 

1093 if dflt and ltp is None: 

1094 ltp = dflt[0] 

1095 if isinstance(ltp, (LocalCartesian, Ltp)): 

1096 return ltp 

1097 raise _TypesError(_ltp_, ltp, Ltp, LocalCartesian) 

1098 

1099# **) MIT License 

1100# 

1101# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved. 

1102# 

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

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

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

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

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

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

1109# 

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

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

1112# 

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

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

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

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

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

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

1119# OTHER DEALINGS IN THE SOFTWARE.