1

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

3

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

5

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

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

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

15

16 from pygeodesy.basics import _args_kwds_names, _isin, map1, map2, _xinstanceof, \

17 _xsubclassof, typename # .datums

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

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

20 _N_1_0 # PYCHOK used!

21# from pygeodesy.datums import _WGS84 # from .ecef

22 from pygeodesy.ecef import _EcefBase, EcefKarney, Ecef9Tuple, _llhn4, \

23 _xyzn4, _WGS84

24 from pygeodesy.errors import _NotImplementedError, _ValueError, _xattr, \

25 _xkwds, _xkwds_get, _xkwds_pop2

26 from pygeodesy.fmath import fabs, fdot, Fdot_, fdot_, Fhorner

27 from pygeodesy.fsums import _floor, fsumf_

28# from pygeodesy.internals import typename # from .basics

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

30 _invalid_, _lat0_, _lon0_, _name_, _too_

31# from pygeodesy.lazily import _ALL_LAZY # from vector3d

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

33 ChLVYX2Tuple, Footprint5Tuple, Local9Tuple, \

34 ChLVyx2Tuple, _XyzLocals4, _XyzLocals5, Xyz4Tuple

35 from pygeodesy.named import _name__, _name2__, _NamedBase, notOverloaded

36 from pygeodesy.namedTuples import LatLon3Tuple, LatLon4Tuple, Vector3Tuple

37 from pygeodesy.props import Property, Property_RO, property_doc_, \

38 property_ROver, _update_all

39 from pygeodesy.streprs import Fmt, strs, unstr

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

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

42 wrap180, wrap360

43 from pygeodesy.vector3d import _ALL_LAZY, Vector3d

44

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

46

47__all__ = _ALL_LAZY.ltp

48__version__ = '25.05.01'

49

50_height0_ = _height_ + _0_

51_narrow_ = 'narrow'

52_wide_ = 'wide'

53

54

55 class Attitude(_NamedBase):

56 '''The pose of a plane or camera in space.

57 '''

58 _alt = Meter( alt =_0_0)

59 _roll = Degrees(roll=_0_0)

60 _tilt = Degrees(tilt=_0_0)

61 _yaw = Bearing(yaw =_0_0)

62

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

64 '''New L{Attitude}.

65

66 @kwarg alt_attitude: Altitude (C{meter}) above earth or a previous attitude

67 (L{Attitude} or L{Attitude4Tuple}) with the C{B{alt}itude},

68 B{C{tilt}}, B{C{yaw}} and B{C{roll}}.

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

70 (clockwise rotation along and around the x- or East axis), iff

71 B{C{alt_attitude}} is C{meter}, ignored otherwise.

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

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

74 iff B{C{alt_attitude}} is C{meter}, ignored otherwise.

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

76 (clockwise rotation along and around the y- or North axis), iff

77 B{C{alt_attitude}} is C{meter}, ignored otherwise.

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

79

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

81 B{C{roll}}.

82

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

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

85 '''

86 if _isHeight(alt_attitude):

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

88 else:

89 try:

90 t = alt_attitude.atyr

91 except AttributeError:

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

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

94 if v:

95 setattr(self, n, v)

96 n = _name__(name, _or_nameof=t)

97 if n:

98 self.name = n

99

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

101 def alt(self):

102 return self._alt

103

104 @alt.setter # PYCHOK setter!

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

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

107 if self._alt != a:

108 _update_all(self)

109 self._alt = a

110

111 altitude = alt

112

113 @Property_RO

114 def atyr(self):

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

116 '''

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

118

119 @Property_RO

120 def matrix(self):

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

122

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

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

125 '''

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

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

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

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

130 return ((ca * cb, fdot_(ca, sb * sg, -sa, cg), fdot_(ca, sb * cg, sa, sg)),

131 (sa * cb, fdot_(sa, sb * sg, ca, cg), fdot_(sa, sb * cg, -ca, sg)),

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

133

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

135 def roll(self):

136 return self._roll

137

138 @roll.setter # PYCHOK setter!

139 def roll(self, roll):

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

141 if self._roll != r:

142 _update_all(self)

143 self._roll = r

144

145 bank = roll

146

147 def rotate(self, x_xyz, y=None, z=None, Vector=None, **name_Vector_kwds):

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

149

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

151 L{Vector3d} or L{Vector3Tuple}).

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

153 ignored otherwise.

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

155 ignored otherwise.

156 @kwarg Vector: Class to return transformed point (C{Cartesian}, L{Vector3d}

157 or C{Vector3Tuple}) or C{None}.

158 @kwarg name_Vector_kwds: Optional C{B{name}=NN} (C{str}) and optionally,

159 additional B{C{Vector}} keyword arguments, ignored if C{B{Vector}

160 is None}.

161

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

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

164

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

166

167 @raise TypeError: Invalid B{C{Vector}} or B{C{name_Vector_kwds}} item.

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

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 n, kwds = _name2__(name_Vector_kwds, _or_nameof=self)

181 return Vector3Tuple(x, y, z, name=n) if Vector is None else \

182 Vector(x, y, z, name=n, **kwds)

183

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

185 def tilt(self):

186 return self._tilt

187

188 @tilt.setter # PYCHOK setter!

189 def tilt(self, tilt):

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

191 if self._tilt != t:

192 _update_all(self)

193 self._tilt = t

194

195 elevation = pitch = tilt

196

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

198 '''Format this attitude as string.

199

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

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

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

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

204

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

206 '''

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

208

209 @Property_RO

210 def tyr3d(self):

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

212

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

214 '''

215 def _r2d(r):

216 return fsumf_(_N_1_0, *r)

217

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

219

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

221 def yaw(self):

222 return self._yaw

223

224 @yaw.setter # PYCHOK setter!

225 def yaw(self, yaw):

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

227 if self._yaw != y:

228 _update_all(self)

229 self._yaw = y

230

231 bearing = heading = yaw # azimuth

232

233

234 class AttitudeError(_ValueError):

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

236 '''

237 pass

238

239

240 class Frustum(_NamedBase):

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

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

243

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

245 '''

246 _h_2 = _0_0 # half hfov in degrees

247 _ltp = None # local tangent plane

248 _tan_h_2 = _0_0 # tan(_h_2)

249 _v_2 = _0_0 # half vfov in degrees

250

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

252 '''New L{Frustum}.

253

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

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

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

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

258

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

260 '''

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

262 self._v_2 = _fov_2(vfov=vfov)

263

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

265

266 if ltp:

267 self._ltp = _xLtp(ltp)

268 if name:

269 self.name # PYCHOK effect

270

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

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

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

274

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

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

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

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

279 (clockwise rotation along and around the x- or East axis) iff

280 B{C{alt_attitude}} is C{meter}, ignored otherwise.

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

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

283 iff B{C{alt_attitude}} is C{meter}, ignored otherwise.

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

285 (clockwise rotation along and around the y- or North axis) iff

286 B{C{alt_attitude}} is C{meter}, ignored otherwise.

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

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

289 frustum's C{ltp}.

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

291

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

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

294

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

296

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

298

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

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

301 over the horizon.

302

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

304 '''

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

306 # left and right corners, or swapped

307 if r < EPS: # no roll

308 r = a * tan_h_2

309 l = -r # PYCHOK l is ell

310 else: # roll

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

312 r *= -a # negate right positive

313 l *= -a # PYCHOK l is ell

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

315 return (l, y), (r, y)

316

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

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

319 sc = sincos2d(b)

320 for x, y in xy5:

321 yield Xyz4Tuple(fdot(sc, x, y),

322 fdot(sc, -x, y), z, ltp)

323

324 try:

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

326 except AttributeError:

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

328

329 a = Meter(altitude=a)

330 if a < EPS: # too low

331 raise _ValueError(altitude=a)

332 if z: # PYCHOK no cover

333 z = Meter(z=z)

334 a -= z

335 if a < EPS: # z above a

336 raise _ValueError(altitude_z=a)

337 else:

338 z = _0_0

339

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

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

342 if not EPS < e < _180_0:

343 raise _ValueError(tilt=t)

344 if e > _90_0:

345 e = _loneg(e)

346 b = _umod_360(b + _180_0)

347

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

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

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

351 if fabs(y) < EPS:

352 y = _0_0

353

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

355 # center and corners, clockwise from upperleft, rolled

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

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

358 # turn center and corners by yaw, clockwise

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

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

361

362 @Property_RO

363 def hfov(self):

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

365 '''

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

367

368 @Property_RO

369 def ltp(self):

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

371 '''

372 return self._ltp

373

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

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

376

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

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

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

380

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

382 '''

383 t = self.hfov, self.vfov

384 if self.ltp:

385 t += self.ltp,

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

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

388

389 @Property_RO

390 def vfov(self):

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

392 '''

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

394

395

396 class LocalError(_ValueError):

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

398 '''

399 pass

400

401

402 class LocalCartesian(_NamedBase):

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

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

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

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

407 classGeographicLib_1_1LocalCartesian.html>}.

408

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

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

411

412 The conversions all take place via geocentric coordinates using a

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

414

415 @see: Class L{Ltp}.

416 '''

417 _ecef = EcefKarney(_WGS84)

418 _Ecef = EcefKarney

419 _lon00 = INT0 # self.lon0

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

421 _9Tuple = Local9Tuple

422

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

424 '''New L{LocalCartesian} converter.

425

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

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

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

429 @kwarg lon0: Longitude of the (goedetic) origin (C{degrees}), required if

430 B{C{latlonh0}} is C{scalar}, ignored otherwise.

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

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

433 surface, like B{C{lon0}}.

434 @kwarg ecef: An ECEF converter (L{EcefKarney} I{only}), like B{C{lon0}}.

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

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

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

438 for further details.

439

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

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

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

443

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

445

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

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

448 '''

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

450

451 def __eq__(self, other):

452 '''Compare this and an other instance.

453

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

455

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

457 '''

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

459 other.ecef == self.ecef and

460 other._t0 == self._t0)

461

462 @Property_RO

463 def datum(self):

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

465 '''

466 return self.ecef.datum

467

468 @Property_RO

469 def ecef(self):

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

471 '''

472 return self._ecef

473

474 def _ecef2local(self, ecef, Xyz, name_Xyz_kwds): # in _EcefLocal._Ltp_ecef2local

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

476

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

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

479 @arg name_Xyz_kwds: Optional C{B{name}=NN} (C{str}) and optionally,

480 additional B{C{Xyz}} keyword arguments, ignored if C{B{Xyz}

481 is None}.

482

483 @return: An C{B{Xyz}(x, y, z, ltp, **B{name_Xyz_kwds}} instance or

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

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

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

487 always.

488

489 @raise TypeError: Invalid B{C{Xyz}} or B{C{name_Xyz_kwds}} item.

490 '''

491 _xinstanceof(Ecef9Tuple, ecef=ecef)

492 if ecef.datum != self.datum:

493 ecef = ecef.toDatum(self.datum)

494 n, kwds = _name2__(name_Xyz_kwds, _or_nameof=ecef)

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

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

497 self, ecef, None, name=n)

498 if Xyz:

499 _xsubclassof(*_XyzLocals4, Xyz=Xyz) # Vector3d

500 r = r.toXyz(Xyz=Xyz, name=n, **kwds)

501 return r

502

503 @Property_RO

504 def ellipsoid(self):

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

506 '''

507 return self.ecef.datum.ellipsoid

508

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

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

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

512

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

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

515 @kwarg lon: Optional C{scalar} (geodetic) longitude (C{degrees}) iff

516 B{C{latlonh}} is C{scalar}, ignored otherwise.

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

518 to and above (or below) the ellipsoid's surface, iff

519 B{C{latlonh}} is C{scalar}, ignored othewrise.

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

521 iff available (C{bool}).

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

523

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

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

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

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

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

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

530

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

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

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

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

535 '''

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

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

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

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

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

541

542 @Property_RO

543 def height0(self):

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

545 '''

546 return self._t0.height

547

548 @Property_RO

549 def lat0(self):

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

551 '''

552 return self._t0.lat

553

554 @Property_RO

555 def latlonheight0(self):

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

557 '''

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

559

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

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

562

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

564 @kwarg nine: If C{True}, return a 9-, otherwise a 3-tuple (C{bool}).

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

566

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

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

569 rotation matrix C{M} (L{EcefMatrix}) if requested.

570 '''

571 _xinstanceof(*_XyzLocals5, local=local)

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

573 if nine:

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

575 return t

576

577 @Property_RO

578 def lon0(self):

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

580 '''

581 return self._t0.lon

582

583 @Property

584 def lon00(self):

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

586 '''

587 return self._lon00

588

589 @lon00.setter # PYCHOK setter!

590 def lon00(self, lon00):

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

592 '''

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

594 self._lon00 = Degrees(lon00=lon00)

595

596 @Property_RO

597 def M(self):

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

599 '''

600 return self._t0.M

601

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

603 '''Reset this converter, see L{LocalCartesian.__init__} for further details.

604 '''

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

606 if isinstance(latlonh0, LocalCartesian):

607 if self._t0:

608 _update_all(self)

609 self._ecef = latlonh0.ecef

610 self._lon00 = latlonh0.lon00

611 self._t0 = latlonh0._t0

612 n = _name__(name, _or_nameof=latlonh0)

613 else:

614 n = _name__(name, _or_nameof=self)

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

616 Error=LocalError, name=n)

617 if ecef: # PYCHOK no cover

618 _xinstanceof(self._Ecef, ecef=ecef)

619 _update_all(self)

620 self._ecef = ecef

621 elif self._t0:

622 _update_all(self)

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

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

625 if n:

626 self.rename(n)

627

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

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

630

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

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

633 @kwarg y: Local C{y} coordinate (C{meter}), iff B{C{xyz}} is C{scalar},

634 ignored otherwise.

635 @kwarg z: Local C{z} coordinate (C{meter}), iff B{C{xyz}} is C{scalar},

636 ignored otherwise.

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

638 available (C{bool}).

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

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

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

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

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

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

645

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

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

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

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

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

651

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

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

654 '''

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

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

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

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

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

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

661

662 @Property_RO

663 def _t0_xyz(self):

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

665 '''

666 return self._t0.xyz

667

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

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

670

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

672

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

674 '''

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

676

677

678 class Ltp(LocalCartesian):

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

680 (re-)configurable ECEF converter.

681 '''

682 _Ecef = _EcefBase

683

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

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

686

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

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

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

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

691

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

693

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

695 '''

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

697 ecef=ecef, **lon00_name)

698

699 @Property

700 def ecef(self):

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

702 '''

703 return self._ecef

704

705 @ecef.setter # PYCHOK setter!

706 def ecef(self, ecef):

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

708

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

710 '''

711 _xinstanceof(_EcefBase, ecef=ecef)

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

713 self.reset(self._t0)

714 self._ecef = ecef

715

716

717 class _ChLV(object):

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

719 '''

720 _03_falsing = ChLVyx2Tuple(0.6e6, 0.2e6)

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

722 _95_falsing = ChLVEN2Tuple(2.6e6, 1.2e6)

723

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

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

726 '''

727 if bool(M): # PYCHOK no cover

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

729 n = _DOT_(*map1(typename, type(self), m))

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

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

732 return ChLV9Tuple(t, name=name)

733

734 @property_ROver

735 def _enh_n_h(self):

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

737 '''

738 t = _args_kwds_names(_ChLV.reverse)[1:4]

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

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

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

742 return t # overwrite property_ROver

743

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

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

746

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

748 @kwarg lon: Optional, C{scalar} (geodetic) longitude (C{degrees}) iff B{C{latlonh}} is

749 C{scalar}, ignored otherwise.

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

751 (C{meter}) iff B{C{latlonh}} and B{C{lon}} are C{scalar}, ignored otherwise.

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

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

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

755

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

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

758 C{lon} and C{height}, C{ecef} (L{Ecef9Tuple}) at I{Bern, Ch}, rotation matrix

759 C{M} and C{ltp} this C{ChLV}, C{ChLVa} or C{ChLVe} instance.

760

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

762 '''

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

764

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

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

767

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

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

770 @kwarg n: Falsed I{Swiss N_LV85} or I{x_LV03} northing (C{meter}) iff B{C{enh_}} is

771 C{scalar}, ignored otherwise.

772 @kwarg h_: I{Swiss h'} height (C{meter}) iff B{C{enh_}} and B{C{n}} are C{scalar},

773 ignored otherwise.

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

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

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

777

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

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

780 C{lon} and C{height}, C{ecef} (L{Ecef9Tuple}) at I{Bern, Ch}, rotation matrix

781 C{M} and C{ltp} this C{ChLV}, C{ChLVa} or C{ChLVe} instance.

782

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

784 '''

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

786

787 @staticmethod

788 def _falsing2(LV95):

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

790 '''

791 return _ChLV._95_falsing if _isin(LV95, True, 95) else (

792 _ChLV._03_falsing if _isin(LV95, False, 3) else ChLVYX2Tuple(0, 0))

793

794 @staticmethod

795 def _llh2abh_3(lat, lon, h):

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

797 '''

798 def _deg2ab(deg, sLL):

799 # convert degrees to arc-seconds

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

801 d = _floor(ds)

802 t = (ds - d) * p

803 m = _floor(t)

804 s = (t - m) * p

805 if swap:

806 d, s = s, d

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

808

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

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

811 return (s - sLL) / ChLV._s_ab

812

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

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

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

816 return a, b, h_

817

818 @staticmethod

819 def _YXh_2abh3(Y, X, h_):

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

821 '''

822 def _YX2ab(YX):

823 return YX * ChLV._ab_m

824

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

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

827 return a, b, h

828

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

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

831 '''

832 Y, X, h_, name = _xyzn4(enh_, n, h_, (ChLV9Tuple,),

833 _xyz_y_z_names=self._enh_n_h, **name)

834 if isinstance(enh_, ChLV9Tuple):

835 Y, X = enh_.Y, enh_.X

836 else: # isscalar(enh_)

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

838 return Y, X, h_, name

839

840

841 class ChLV(_ChLV, Ltp):

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

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

844

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

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

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

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

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

850 '''

851 _9Tuple = ChLV9Tuple

852

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

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

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

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

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

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

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

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

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

862

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

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

865

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

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

868

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

870 '''

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

872

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

874 # overloaded for the _ChLV.forward.__doc__

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

876

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

878 # overloaded for the _ChLV.reverse.__doc__

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

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

881

882 @staticmethod

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

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

885

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

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

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

889 C{LV03} falsing, otherwise leave unfalsed.

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

891

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

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

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

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

896 '''

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

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

899

900 @staticmethod

901 def isLV03(e, n):

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

903

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

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

906

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

908 LV03}, projection C{False} otherwise.

909 '''

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

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

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

913

914 @staticmethod

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

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

917

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

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

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

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

922

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

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

925 '''

926 if ChLV.isLV03(e, n):

927 return False

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

929 return True

930 elif raiser: # PYCHOK no cover

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

932 return None

933

934 @staticmethod

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

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

937

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

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

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

941 I{LV03} falsing, otherwise use method C{isLV95(B{e}, B{n})}.

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

943

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

945 respectively B{C{n}}.

946 '''

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

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

949

950

951 class ChLVa(_ChLV, LocalCartesian):

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

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

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

955

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

957 '''

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

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

960

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

962 '''

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

964

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

966 # overloaded for the _ChLV.forward.__doc__

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

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

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

970

971 Y = fdot_(211455.93, b,

972 -10938.51, b * a,

973 -0.36, b * a2,

974 -44.54, b * b2, start=72.37) # + 600_000

975 X = fdot_(308807.95, a,

976 3745.25, b2,

977 76.63, a2,

978 -194.56, b2 * a,

979 119.79, a2 * a, start=147.07) # + 200_000

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

981

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

983 # overloaded for the _ChLV.reverse.__doc__

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

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

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

987

988 lat = Fdot_(3.238272, b,

989 -0.270978, a2,

990 -0.002528, b2,

991 -0.0447, a2 * b,

992 -0.014, b2 * b, start=16.9023892).fover(ab_d)

993 lon = Fdot_(4.728982, a,

994 0.791484, a * b,

995 0.1306, a * b2,

996 -0.0436, a * a2, start=2.6779094).fover(ab_d)

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

998

999

1000 class ChLVe(_ChLV, LocalCartesian):

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

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

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

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

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

1006

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

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

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

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

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

1012 function L{pygeodesy.degrees2grades}.

1013

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

1015 '''

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

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

1018

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

1020 '''

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

1022

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

1024 # overloaded for the _ChLV.forward.__doc__

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

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

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

1028

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

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

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

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

1033

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

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

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

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

1038

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

1040 if gamma:

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

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

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

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

1045 t = t, g

1046 return t

1047

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

1049 # overloaded for the _ChLV.reverse.__doc__

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

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

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

1053

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

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

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

1057 lat = _H(a, A0, z, A2, z, A4, z, -0.4249).fover(s_d)

1058

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

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

1061 A5 = _H(b, 9.63495, 9.0243)

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

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

1064

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

1066 if gamma:

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

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

1069 U5 = _H(b, 29.631, 27.873)

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

1071 t = t, g

1072 return t

1073

1074

1075 def _fov_2(**fov):

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

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

1078 if EPS < f < _90_0:

1079 return f

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

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

1082

1083

1084 def tyr3d(tilt=INT0, yaw=INT0, roll=INT0, Vector=Vector3d, **name_Vector_kwds):

1085 '''Convert an attitude pose into a (3-D) direction vector.

1086

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

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

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

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

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

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

1093 @kwarg Vector: Class to return the direction vector (C{Cartesian},

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

1095 @kwarg name_Vector_kwds: Optional C{B{name}=NN} (C{str}) and optionally,

1096 additional B{C{Vector}} keyword arguments, ignored if C{B{Vector}

1097 is None}.

1098

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

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

1101

1102 @raise AttitudeError: Invalid B{C{tilt}}, B{C{yaw}} or B{C{roll}}.

1103

1104 @raise TypeError: Invalid B{C{Vector}} or B{C{name_Vector_kwds}}.

1105

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

1107 and function L{pygeodesy.hartzell} argument C{los}, Line-Of-Sight.

1108 '''

1109 v = Attitude4Tuple(_0_0, tilt, yaw, roll).tyr3d

1110 if Vector is not type(v):

1111 n, kwds = _name2__(name_Vector_kwds, name__=tyr3d)

1112 v = Vector3Tuple(v.x, v.y, v.z, name=n) if Vector is None else \

1113 Vector(v.x, v.y, v.z, name=n, **kwds)

1114 elif name_Vector_kwds:

1115 n, _ = _name2__(name_Vector_kwds)

1116 if n:

1117 v = v.copy(name=n)

1118 return v

1119

1120

1121 def _xLtp(ltp, *dflt):

1122 '''(INTERNAL) Validate B{C{ltp}} if not C{None} else B{C{dflt}}.

1123 '''

1124 if dflt and ltp is None:

1125 ltp = dflt[0]

1126 _xinstanceof(Ltp, LocalCartesian, ltp=ltp)

1127 return ltp

1128

1129# **) MIT License

1130#

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

1132#

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

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

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

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

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

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

1139#

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

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

1142#

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

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

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

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

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

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

1149# OTHER DEALINGS IN THE SOFTWARE.