Coverage for pygeodesy/css.py: 97%

236 statements  

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

1 

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

3 

4u'''Cassini-Soldner (CSS) projection. 

5 

6Classes L{CassiniSoldner}, L{Css} and L{CSSError} requiring I{Charles Karney}'s 

7U{geographiclib <https://PyPI.org/project/geographiclib>} Python package to be 

8installed. 

9''' 

10 

11from pygeodesy.basics import islistuple, neg, _xinstanceof, _xsubclassof 

12from pygeodesy.constants import _umod_360, _0_0, _0_5, _90_0 

13from pygeodesy.datums import _ellipsoidal_datum, _WGS84 

14from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase as _LLEB 

15from pygeodesy.errors import _ValueError, _xdatum, _xellipsoidal, \ 

16 _xattr, _xkwds 

17from pygeodesy.interns import _azimuth_, _COMMASPACE_, _easting_, \ 

18 _lat_, _lon_, _m_, _name_, _northing_, \ 

19 _reciprocal_, _SPACE_ 

20from pygeodesy.interns import _C_ # PYCHOK used! 

21from pygeodesy.karney import _atan2d, _copysign, _diff182, _norm2, \ 

22 _norm180, _signBit, _sincos2d, fabs 

23from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS 

24from pygeodesy.named import _name2__, _NamedBase, _NamedTuple 

25from pygeodesy.namedTuples import EasNor2Tuple, EasNor3Tuple, \ 

26 LatLon2Tuple, LatLon4Tuple, _LL4Tuple 

27from pygeodesy.props import deprecated_Property_RO, Property, \ 

28 Property_RO, _update_all 

29from pygeodesy.streprs import Fmt, _fstrENH2, _fstrLL0, _xzipairs 

30from pygeodesy.units import Bearing, Degrees, Easting, Height, _heigHt, \ 

31 Lat_, Lon_, Northing, Scalar 

32 

33# from math import fabs # from .karney 

34 

35__all__ = _ALL_LAZY.css 

36__version__ = '24.05.24' 

37 

38 

39def _CS0(cs0): 

40 '''(INTERNAL) Get/set default projection. 

41 ''' 

42 if cs0 is None: 

43 cs0 = Css._CS0 

44 if cs0 is None: 

45 Css._CS0 = cs0 = CassiniSoldner(_0_0, _0_0, name='Default') 

46 else: 

47 _xinstanceof(CassiniSoldner, cs0=cs0) 

48 return cs0 

49 

50 

51class CSSError(_ValueError): 

52 '''Cassini-Soldner (CSS) conversion or other L{Css} issue. 

53 ''' 

54 pass 

55 

56 

57class CassiniSoldner(_NamedBase): 

58 '''Cassini-Soldner projection, a Python version of I{Karney}'s C++ class U{CassiniSoldner 

59 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1CassiniSoldner.html>}. 

60 ''' 

61 _cb0 = _0_0 

62 _datum = _WGS84 # L{Datum} 

63 _geodesic = None 

64 _latlon0 = () 

65 _meridian = None 

66 _sb0 = _0_0 

67 

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

69 '''New L{CassiniSoldner} projection. 

70 

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

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

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

74 L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}). 

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

76 

77 @raise CSSError: Invalid B{C{lat}} or B{C{lon}}. 

78 ''' 

79 if datum not in (None, self._datum): 

80 self._datum = _xellipsoidal(datum=_ellipsoidal_datum(datum, **name)) 

81 if name: 

82 self.name = name 

83 

84 self.reset(lat0, lon0) 

85 

86 @Property 

87 def datum(self): 

88 '''Get the datum (L{Datum}). 

89 ''' 

90 return self._datum 

91 

92 @datum.setter # PYCHOK setter! 

93 def datum(self, datum): 

94 '''Set the datum or ellipsoid (L{Datum}, L{Ellipsoid}, L{Ellipsoid2} 

95 or L{a_f2Tuple}) or C{None} for the default. 

96 ''' 

97 d = CassiniSoldner._datum if datum is None else \ 

98 _xellipsoidal(datum=_ellipsoidal_datum(datum, name=self.name)) 

99 if self._datum != d: 

100 self._datum = d 

101 self.geodesic = None if self._geodesic is None else self.isExact 

102 

103 def _datumatch(self, latlon): 

104 '''Check for matching datum ellipsoids. 

105 

106 @raise CSSError: Ellipsoid mismatch of B{C{latlon}} and this projection. 

107 ''' 

108 d = _xattr(latlon, datum=None) 

109 if d: 

110 _xdatum(self.datum, d, Error=CSSError) 

111 

112 @Property_RO 

113 def equatoradius(self): 

114 '''Get the ellipsoid's equatorial radius, semi-axis (C{meter}). 

115 ''' 

116 return self.geodesic.a 

117 

118 a = equatoradius 

119 

120 @Property_RO 

121 def flattening(self): 

122 '''Get the ellipsoid's flattening (C{scalar}). 

123 ''' 

124 return self.geodesic.f 

125 

126 f = flattening 

127 

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

129 '''Convert an (ellipsoidal) geodetic location to Cassini-Soldner 

130 easting and northing. 

131 

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

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

134 @kwarg name: Optional C{B{name}=NN} inlieu of this projection's 

135 name (C{str}). 

136 

137 @return: An L{EasNor2Tuple}C{(easting, northing)}. 

138 

139 @see: Methods L{CassiniSoldner.forward4}, L{CassiniSoldner.reverse} 

140 and L{CassiniSoldner.reverse4}. 

141 

142 @raise CSSError: Invalid B{C{lat}} or B{C{lon}}. 

143 ''' 

144 t = self.forward6(lat, lon, **name) 

145 return EasNor2Tuple(t.easting, t.northing, name=t.name) 

146 

147 def forward4(self, lat, lon, **name): 

148 '''Convert an (ellipsoidal) geodetic location to Cassini-Soldner 

149 easting and northing. 

150 

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

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

153 @kwarg name: Optional B{C{name}=NN} inlieu of this projection's 

154 name (C{str}). 

155 

156 @return: An L{EasNorAziRk4Tuple}C{(easting, northing, 

157 azimuth, reciprocal)}. 

158 

159 @see: Method L{CassiniSoldner.forward}, L{CassiniSoldner.forward6}, 

160 L{CassiniSoldner.reverse} and L{CassiniSoldner.reverse4}. 

161 

162 @raise CSSError: Invalid B{C{lat}} or B{C{lon}}. 

163 ''' 

164 t = self.forward6(lat, lon, **name) 

165 return EasNorAziRk4Tuple(t.easting, t.northing, 

166 t.azimuth, t.reciprocal, name=t.name) 

167 

168 def forward6(self, lat, lon, **name): 

169 '''Convert an (ellipsoidal) geodetic location to Cassini-Soldner 

170 easting and northing. 

171 

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

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

174 @kwarg name: Optional B{C{name}=NN} inlieu of this projection's 

175 name (C{str}). 

176 

177 @return: An L{EasNorAziRkEqu6Tuple}C{(easting, northing, 

178 azimuth, reciprocal, equatorarc, equatorazimuth)}. 

179 

180 @see: Method L{CassiniSoldner.forward}, L{CassiniSoldner.forward4}, 

181 L{CassiniSoldner.reverse} and L{CassiniSoldner.reverse4}. 

182 

183 @raise CSSError: Invalid B{C{lat}} or B{C{lon}}. 

184 ''' 

185 g = self.geodesic 

186 

187 lat = Lat_(lat, Error=CSSError) 

188 d, _ = _diff182(self.lon0, Lon_(lon, Error=CSSError)) # _2sum 

189 D = fabs(d) 

190 

191 r = g.Inverse(lat, -D, lat, D) 

192 z1, a = r.azi1, (r.a12 * _0_5) 

193 z2, e = r.azi2, (r.s12 * _0_5) 

194 if e == 0: # PYCHOK no cover 

195 z = _diff182(z1, z2)[0] * _0_5 # _2sum 

196 c = _copysign(_90_0, 90 - D) # -90 if D > 90 else 90 

197 z1, z2 = c - z, c + z 

198 if _signBit(d): 

199 a, e, z2 = neg(a), neg(e), z1 

200 

201 z = _norm180(z2) # azimuth of easting direction 

202 p = g.Line(lat, d, z, g.DISTANCE | g.GEODESICSCALE | g.LINE_OFF) 

203 # reciprocal of azimuthal northing scale 

204 rk = p.ArcPosition(neg(a), g.GEODESICSCALE).M21 

205 # rk = p._GenPosition(True, -a, g.DISTANCE)[7] 

206 

207 s, c = _sincos2d(p.azi0) # aka equatorazimuth 

208 sb1 = _copysign(c, lat) 

209 cb1 = _copysign(s, 90 - D) # -abs(s) if D > 90 else abs(s) 

210 d = _atan2d(sb1 * self._cb0 - cb1 * self._sb0, 

211 cb1 * self._cb0 + sb1 * self._sb0) 

212 n = self._meridian.ArcPosition(d, g.DISTANCE).s12 

213 # n = self._meridian._GenPosition(True, d, g.DISTANCE)[4] 

214 return EasNorAziRkEqu6Tuple(e, n, z, rk, p.a1, p.azi0, 

215 name=self._name__(name)) 

216 

217 @Property 

218 def geodesic(self): 

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

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

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

222 package is installed, otherwise an I{exact} L{GeodesicExact} instance. 

223 ''' 

224 g = self._geodesic 

225 if g is None: 

226 E = self.datum.ellipsoid 

227 try: 

228 g = E.geodesicw 

229 except ImportError: 

230 g = E.geodesicx 

231 self._geodesic = g 

232 return g 

233 

234 @geodesic.setter # PYCHOK setter! 

235 def geodesic(self, exact): 

236 '''Set this projection's geodesic (C{bool}) to L{GeodesicExact} 

237 or I{wrapped Karney}'s or C{None} for the default. 

238 

239 @raise ImportError: Package U{geographiclib<https://PyPI.org/ 

240 project/geographiclib>} not installed or 

241 not found and C{B{exact}=False}. 

242 ''' 

243 E = self.datum.ellipsoid 

244 self._geodesic = None if exact is None else ( 

245 E.geodesicx if exact else E.geodesicw) 

246 self.reset(*self.latlon0) 

247 

248 @Property_RO 

249 def isExact(self): 

250 '''Return C{True} if this projection's geodesic is L{GeodesicExact}. 

251 ''' 

252 return isinstance(self.geodesic, _MODS.geodesicx.GeodesicExact) 

253 

254 @Property_RO 

255 def lat0(self): 

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

257 ''' 

258 return self.latlon0.lat 

259 

260 @property 

261 def latlon0(self): 

262 '''Get the center lat- and longitude (L{LatLon2Tuple}C{(lat, lon)}) 

263 in (C{degrees90}, (C{degrees180}). 

264 ''' 

265 return self._latlon0 

266 

267 @latlon0.setter # PYCHOK setter! 

268 def latlon0(self, latlon0): 

269 '''Set the center lat- and longitude (ellipsoidal C{LatLon}, 

270 L{LatLon2Tuple}, L{LatLon4Tuple} or a C{tuple} or C{list} 

271 with the C{lat}- and C{lon}gitude in C{degrees}). 

272 

273 @raise CSSError: Invalid B{C{latlon0}} or ellipsoid mismatch 

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

275 ''' 

276 if islistuple(latlon0, 2): 

277 lat0, lon0 = latlon0[:2] 

278 else: 

279 try: 

280 lat0, lon0 = latlon0.lat, latlon0.lon 

281 self._datumatch(latlon0) 

282 except (AttributeError, TypeError, ValueError) as x: 

283 raise CSSError(latlon0=latlon0, cause=x) 

284 self.reset(lat0, lon0) 

285 

286 @Property_RO 

287 def lon0(self): 

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

289 ''' 

290 return self.latlon0.lon 

291 

292 @deprecated_Property_RO 

293 def majoradius(self): # PYCHOK no cover 

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

295 return self.equatoradius 

296 

297 def reset(self, lat0, lon0): 

298 '''Set or reset the center point of this Cassini-Soldner projection. 

299 

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

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

302 

303 @raise CSSError: Invalid B{C{lat0}} or B{C{lon0}}. 

304 ''' 

305 _update_all(self) 

306 

307 g = self.geodesic 

308 self._meridian = m = g.Line(Lat_(lat0=lat0, Error=CSSError), 

309 Lon_(lon0=lon0, Error=CSSError), _0_0, 

310 g.STANDARD | g.DISTANCE_IN | g.LINE_OFF) 

311 self._latlon0 = LatLon2Tuple(m.lat1, m.lon1) 

312 s, c = _sincos2d(m.lat1) # == self.lat0 == self.LatitudeOrigin() 

313 self._sb0, self._cb0 = _norm2(s * g.f1, c) 

314 

315 def reverse(self, easting, northing, LatLon=None, **LatLon_kwds_name): 

316 '''Convert a Cassini-Soldner location to (ellipsoidal) geodetic 

317 lat- and longitude. 

318 

319 @arg easting: Easting of the location (C{meter}). 

320 @arg northing: Northing of the location (C{meter}). 

321 @kwarg LatLon: Optional, ellipsoidal class to return the geodetic 

322 location as (C{LatLon}) or C{None}. 

323 @kwarg LatLon_kwds_name: Optional (C{LatLon}) keyword arguments, 

324 ignored if C{B{LatLon} is None} and optional 

325 C{B{name}=NN} inlieu of this this projection's 

326 name (C{str}). 

327 

328 @return: Geodetic location B{C{LatLon}} or if B{C{LatLon}} is C{None}, 

329 a L{LatLon2Tuple}C{(lat, lon)}. 

330 

331 @raise CSSError: Ellipsoidal mismatch of B{C{LatLon}} and this projection. 

332 

333 @raise TypeError: Invalid B{C{LatLon}} or B{C{LatLon_kwds}}. 

334 

335 @see: Method L{CassiniSoldner.reverse4}, L{CassiniSoldner.forward}. 

336 L{CassiniSoldner.forward4} and L{CassiniSoldner.forward6}. 

337 ''' 

338 n, kwds = _name2__(LatLon_kwds_name, _or_nameof=self) 

339 r = self.reverse4(easting, northing, name=n) 

340 if LatLon is None: 

341 r = LatLon2Tuple(r.lat, r.lon, name=r.name) # PYCHOK expected 

342 else: 

343 _xsubclassof(_LLEB, LatLon=LatLon) 

344 kwds = _xkwds(kwds, datum=self.datum, name=r.name) 

345 r = LatLon(r.lat, r.lon, **kwds) # PYCHOK expected 

346 self._datumatch(r) 

347 return r 

348 

349 def reverse4(self, easting, northing, **name): 

350 '''Convert a Cassini-Soldner location to (ellipsoidal) geodetic 

351 lat- and longitude. 

352 

353 @arg easting: Easting of the location (C{meter}). 

354 @arg northing: Northing of the location (C{meter}). 

355 @kwarg name: Optional B{C{name}=NN} inlieu of this projection's 

356 name (C{str}). 

357 

358 @return: A L{LatLonAziRk4Tuple}C{(lat, lon, azimuth, reciprocal)}. 

359 

360 @see: Method L{CassiniSoldner.reverse}, L{CassiniSoldner.forward} 

361 and L{CassiniSoldner.forward4}. 

362 ''' 

363 g = self.geodesic 

364 n = self._meridian.Position(northing) 

365 r = g.Direct(n.lat2, n.lon2, n.azi2 + _90_0, easting, outmask=g.STANDARD | g.GEODESICSCALE) 

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

367 # include z azimuth of easting direction and rk reciprocal 

368 # of azimuthal northing scale (see C++ member Direct() 5/6 

369 # <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geodesic.html>) 

370 return LatLonAziRk4Tuple(r.lat2, r.lon2, z, r.M12, name=self._name__(name)) 

371 

372 toLatLon = reverse # XXX not reverse4 

373 

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

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

376 

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

378 

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

380 (C{str}). 

381 ''' 

382 return _fstrLL0(self, prec, True) 

383 

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

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

386 

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

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

389 

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

391 ''' 

392 t = _fstrLL0(self, prec, False) 

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

394 

395 

396class Css(_NamedBase): 

397 '''Cassini-Soldner East-/Northing location. 

398 ''' 

399 _CS0 = None # default projection (L{CassiniSoldner}) 

400 _cs0 = None # projection (L{CassiniSoldner}) 

401 _easting = _0_0 # easting (C{float}) 

402 _height = 0 # height (C{meter}) 

403 _northing = _0_0 # northing (C{float}) 

404 

405 def __init__(self, e, n, h=0, cs0=None, **name): 

406 '''New L{Css} Cassini-Soldner position. 

407 

408 @arg e: Easting (C{meter}). 

409 @arg n: Northing (C{meter}). 

410 @kwarg h: Optional height (C{meter}). 

411 @kwarg cs0: Optional, the Cassini-Soldner projection 

412 (L{CassiniSoldner}). 

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

414 

415 @return: The Cassini-Soldner location (L{Css}). 

416 

417 @raise CSSError: If B{C{e}} or B{C{n}} is invalid. 

418 

419 @raise TypeError: If B{C{cs0}} is not L{CassiniSoldner}. 

420 

421 @raise ValueError: Invalid B{C{h}}. 

422 ''' 

423 self._cs0 = _CS0(cs0) 

424 self._easting = Easting(e, Error=CSSError) 

425 self._northing = Northing(n, Error=CSSError) 

426 if h: 

427 self._height = Height(h=h) 

428 if name: 

429 self.name = name 

430 

431 @Property_RO 

432 def azi(self): 

433 '''Get the azimuth of easting direction (C{degrees}). 

434 ''' 

435 return self.reverse4.azimuth 

436 

437 azimuth = azi 

438 

439 @Property 

440 def cs0(self): 

441 '''Get the projection (L{CassiniSoldner}). 

442 ''' 

443 return self._cs0 or Css._CS0 

444 

445 @cs0.setter # PYCHOK setter! 

446 def cs0(self, cs0): 

447 '''Set the I{Cassini-Soldner} projection (L{CassiniSoldner}). 

448 

449 @raise TypeError: Invalid B{C{cs0}}. 

450 ''' 

451 cs0 = _CS0(cs0) 

452 if cs0 != self._cs0: 

453 _update_all(self) 

454 self._cs0 = cs0 

455 

456# def dup(self, **e_n_h_cs0_name): # PYCHOK signature 

457# '''Duplicate this position with some attributes modified. 

458# 

459# @kwarg e_n_h_cs0_name: Use keyword argument C{B{e}=...}, 

460# C{B{n}=...}, C{B{h}=...} and/or C{B{cs0}=...} 

461# to override the current C{easting}, C{northing} 

462# C{height} or C{cs0} projectio, respectively and 

463# an optional C{B{name}=NN} (C{str}). 

464# ''' 

465# def _args_kwds(e=None, n=None, **kwds): 

466# return (e, n), kwds 

467# 

468# kwds = _xkwds(e_n_h_cs0, e=self.easting, n=self.northing, 

469# h=self.height, cs0=self.cs0, 

470# name=_name__(name, _or_nameof(self))) 

471# args, kwds = _args_kwds(**kwds) 

472# return self.__class__(*args, **kwds) # .classof 

473 

474 @Property_RO 

475 def easting(self): 

476 '''Get the easting (C{meter}). 

477 ''' 

478 return self._easting 

479 

480 @Property_RO 

481 def height(self): 

482 '''Get the height (C{meter}). 

483 ''' 

484 return self._height 

485 

486 @Property_RO 

487 def latlon(self): 

488 '''Get the lat- and longitude (L{LatLon2Tuple}). 

489 ''' 

490 r = self.reverse4 

491 return LatLon2Tuple(r.lat, r.lon, name=self.name) 

492 

493 @Property_RO 

494 def northing(self): 

495 '''Get the northing (C{meter}). 

496 ''' 

497 return self._northing 

498 

499 @Property_RO 

500 def reverse4(self): 

501 '''Get the lat, lon, azimuth and reciprocal (L{LatLonAziRk4Tuple}). 

502 ''' 

503 return self.cs0.reverse4(self.easting, self.northing, name=self.name) 

504 

505 @Property_RO 

506 def rk(self): 

507 '''Get the reciprocal of azimuthal northing scale (C{scalar}). 

508 ''' 

509 return self.reverse4.reciprocal 

510 

511 reciprocal = rk 

512 

513 def toLatLon(self, LatLon=None, height=None, **LatLon_kwds): 

514 '''Convert this L{Css} to an (ellipsoidal) geodetic point. 

515 

516 @kwarg LatLon: Optional, ellipsoidal class to return the 

517 geodetic point (C{LatLon}) or C{None}. 

518 @kwarg height: Optional height for the point, overriding the 

519 default height (C{meter}). 

520 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword 

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

522 

523 @return: The geodetic point (B{C{LatLon}}) or if B{C{LatLon}} 

524 is C{None}, a L{LatLon4Tuple}C{(lat, lon, height, 

525 datum)}. 

526 

527 @raise TypeError: If B{C{LatLon}} or B{C{datum}} is not 

528 ellipsoidal or invalid B{C{height}} or 

529 B{C{LatLon_kwds}}. 

530 ''' 

531 if LatLon: 

532 _xsubclassof(_LLEB, LatLon=LatLon) 

533 

534 lat, lon = self.latlon 

535 h = _heigHt(self, height) 

536 return _LL4Tuple(lat, lon, h, self.cs0.datum, LatLon, LatLon_kwds, 

537 inst=self, name=self.name) 

538 

539 def toRepr(self, prec=6, fmt=Fmt.SQUARE, sep=_COMMASPACE_, m=_m_, C=False): # PYCHOK expected 

540 '''Return a string representation of this L{Css} position. 

541 

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

543 @kwarg fmt: Enclosing backets format (C{str}). 

544 @kwarg sep: Optional separator between name:values (C{str}). 

545 @kwarg m: Optional unit of the height, default meter (C{str}). 

546 @kwarg C: Optionally, include name of projection (C{bool}). 

547 

548 @return: This position as C{"[E:meter, N:meter, H:m, name:'', 

549 C:Conic.Datum]"} (C{str}). 

550 ''' 

551 t, T = _fstrENH2(self, prec, m) 

552 if self.name: 

553 t += repr(self.name), 

554 T += _name_, 

555 if C: 

556 t += self.cs0.toRepr(prec=prec), 

557 T += _C_, 

558 return _xzipairs(T, t, sep=sep, fmt=fmt) 

559 

560 def toStr(self, prec=6, sep=_SPACE_, m=_m_): # PYCHOK expected 

561 '''Return a string representation of this L{Css} position. 

562 

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

564 @kwarg sep: Optional separator to join (C{str}) or C{None} 

565 to return an unjoined C{tuple} of C{str}s. 

566 @kwarg m: Height units, default C{meter} (C{str}). 

567 

568 @return: This position as C{"easting nothing"} C{str} in 

569 C{meter} plus C{" height"} and C{'m'} if height 

570 is non-zero (C{str}). 

571 ''' 

572 t, _ = _fstrENH2(self, prec, m) 

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

574 

575 

576class EasNorAziRk4Tuple(_NamedTuple): 

577 '''4-Tuple C{(easting, northing, azimuth, reciprocal)} for the 

578 Cassini-Soldner location with C{easting} and C{northing} in 

579 C{meters} and the C{azimuth} of easting direction and 

580 C{reciprocal} of azimuthal northing scale, both in C{degrees}. 

581 ''' 

582 _Names_ = (_easting_, _northing_, _azimuth_, _reciprocal_) 

583 _Units_ = ( Easting, Northing, Bearing, Scalar) 

584 

585 

586class EasNorAziRkEqu6Tuple(_NamedTuple): 

587 '''6-Tuple C{(easting, northing, azimuth, reciprocal, equatorarc, 

588 equatorazimuth)} for the Cassini-Soldner location with 

589 C{easting} and C{northing} in C{meters} and the C{azimuth} of 

590 easting direction, C{reciprocal} of azimuthal northing scale, 

591 C{equatorarc} and C{equatorazimuth}, all in C{degrees}. 

592 ''' 

593 _Names_ = EasNorAziRk4Tuple._Names_ + ('equatorarc', 'equatorazimuth') 

594 _Units_ = EasNorAziRk4Tuple._Units_ + ( Degrees, Bearing) 

595 

596 

597class LatLonAziRk4Tuple(_NamedTuple): 

598 '''4-Tuple C{(lat, lon, azimuth, reciprocal)}, all in C{degrees} 

599 where C{azimuth} is the azimuth of easting direction and 

600 C{reciprocal} the reciprocal of azimuthal northing scale. 

601 ''' 

602 _Names_ = (_lat_, _lon_, _azimuth_, _reciprocal_) 

603 _Units_ = ( Lat_, Lon_, Bearing, Scalar) 

604 

605 

606def toCss(latlon, cs0=None, height=None, Css=Css, **name): 

607 '''Convert an (ellipsoidal) geodetic point to a Cassini-Soldner 

608 location. 

609 

610 @arg latlon: Ellipsoidal point (C{LatLon} or L{LatLon4Tuple}). 

611 @kwarg cs0: Optional, the Cassini-Soldner projection to use 

612 (L{CassiniSoldner}). 

613 @kwarg height: Optional height for the point, overriding the 

614 default height (C{meter}). 

615 @kwarg Css: Optional class to return the location (L{Css}) or 

616 C{None}. 

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

618 

619 @return: The Cassini-Soldner location (B{C{Css}}) or an 

620 L{EasNor3Tuple}C{(easting, northing, height)} 

621 if B{C{Css}} is C{None}. 

622 

623 @raise CSSError: Ellipsoidal mismatch of B{C{latlon}} and B{C{cs0}}. 

624 

625 @raise ImportError: Package U{geographiclib<https://PyPI.org/ 

626 project/geographiclib>} not installed or 

627 not found. 

628 

629 @raise TypeError: If B{C{latlon}} is not ellipsoidal. 

630 ''' 

631 _xinstanceof(_LLEB, LatLon4Tuple, latlon=latlon) 

632 

633 cs = _CS0(cs0) 

634 cs._datumatch(latlon) 

635 

636 c = cs.forward4(latlon.lat, latlon.lon) 

637 h = _heigHt(latlon, height) 

638 n = latlon._name__(name) 

639 

640 if Css is None: 

641 r = EasNor3Tuple(c.easting, c.northing, h, name=n) 

642 else: 

643 r = Css(c.easting, c.northing, h=h, cs0=cs, name=n) 

644 r._latlon = LatLon2Tuple(latlon.lat, latlon.lon, name=n) 

645 r._azi, r._rk = c.azimuth, c.reciprocal 

646 return r 

647 

648# **) MIT License 

649# 

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

651# 

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

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

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

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

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

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

658# 

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

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

661# 

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

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

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

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

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

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

668# OTHER DEALINGS IN THE SOFTWARE.