Coverage for pygeodesy/css.py: 98%

235 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-04-21 13:14 -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, _xkwds 

16from pygeodesy.interns import NN, _azimuth_, _COMMASPACE_, _datum_, \ 

17 _easting_, _lat_, _lon_, _m_, _name_, \ 

18 _northing_, _reciprocal_, _SPACE_ 

19from pygeodesy.interns import _C_ # PYCHOK used! 

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

21 _norm180, _signBit, _sincos2d, fabs 

22from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS 

23from pygeodesy.named import _NamedBase, _NamedTuple, nameof 

24from pygeodesy.namedTuples import EasNor2Tuple, EasNor3Tuple, \ 

25 LatLon2Tuple, LatLon4Tuple, _LL4Tuple 

26from pygeodesy.props import deprecated_Property_RO, Property, \ 

27 Property_RO, _update_all 

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

29from pygeodesy.units import Bearing, Degrees, Easting, Height, \ 

30 Lat_, Lon_, Northing, Scalar 

31 

32# from math import fabs # from .karney 

33 

34__all__ = _ALL_LAZY.css 

35__version__ = '23.04.11' 

36 

37 

38def _CS0(cs0): 

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

40 ''' 

41 if cs0 is None: 

42 cs0 = Css._CS0 

43 if cs0 is None: 

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

45 else: 

46 _xinstanceof(CassiniSoldner, cs0=cs0) 

47 return cs0 

48 

49 

50class CSSError(_ValueError): 

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

52 ''' 

53 pass 

54 

55 

56class CassiniSoldner(_NamedBase): 

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

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

59 ''' 

60 _cb0 = _0_0 

61 _datum = _WGS84 # L{Datum} 

62 _geodesic = None 

63 _latlon0 = () 

64 _meridian = None 

65 _sb0 = _0_0 

66 

67 def __init__(self, lat0, lon0, datum=_WGS84, name=NN): 

68 '''New L{CassiniSoldner} projection. 

69 

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

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

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

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

74 @kwarg name: Optional name (C{str}). 

75 

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

77 

78 @example: 

79 

80 >>> p = CassiniSoldner(48 + 50/60.0, 2 + 20/60.0) # Paris 

81 >>> p.forward(50.9, 1.8) # Calais 

82 (-37518.854545, 230003.561828) 

83 

84 >>> p.reverse4(-38e3, 230e3) 

85 (50.899937, 1.793161, 89.580797, 0.999982) 

86 ''' 

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

88 self._datum = _xellipsoidal(datum=_ellipsoidal_datum(datum, name=name)) 

89 if name: 

90 self.name = name 

91 

92 self.reset(lat0, lon0) 

93 

94 @Property 

95 def datum(self): 

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

97 ''' 

98 return self._datum 

99 

100 @datum.setter # PYCHOK setter! 

101 def datum(self, datum): 

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

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

104 ''' 

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

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

107 if self._datum != d: 

108 self._datum = d 

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

110 

111 def _datumatch(self, latlon): 

112 '''Check for matching datum ellipsoids. 

113 

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

115 ''' 

116 d = getattr(latlon, _datum_, None) 

117 if d: 

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

119 

120 @Property_RO 

121 def equatoradius(self): 

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

123 ''' 

124 return self.geodesic.a 

125 

126 a = equatoradius 

127 

128 @Property_RO 

129 def flattening(self): 

130 '''Get the ellipsoid's flattening (C{float}). 

131 ''' 

132 return self.geodesic.f 

133 

134 f = flattening 

135 

136 def forward(self, lat, lon, name=NN): 

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

138 easting and northing. 

139 

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

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

142 @kwarg name: Name inlieu of this projection's name (C{str}). 

143 

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

145 

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

147 and L{CassiniSoldner.reverse4}. 

148 

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

150 ''' 

151 t = self.forward6(lat, lon, name=name) 

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

153 

154 def forward4(self, lat, lon, name=NN): 

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

156 easting and northing. 

157 

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

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

160 @kwarg name: Name inlieu of this projection's name (C{str}). 

161 

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

163 azimuth, reciprocal)}. 

164 

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

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

167 

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

169 ''' 

170 t = self.forward6(lat, lon, name=name) 

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

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

173 

174 def forward6(self, lat, lon, name=NN): 

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

176 easting and northing. 

177 

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

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

180 @kwarg name: Name inlieu of this projection's name (C{str}). 

181 

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

183 azimuth, reciprocal, equatorarc, equatorazimuth)}. 

184 

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

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

187 

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

189 ''' 

190 g = self.geodesic 

191 

192 lat = Lat_(lat, Error=CSSError) 

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

194 D = fabs(d) 

195 

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

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

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

199 if e == 0: # PYCHOK no cover 

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

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

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

203 if _signBit(d): 

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

205 

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

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

208 # reciprocal of azimuthal northing scale 

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

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

211 

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

213 sb1 = _copysign(c, lat) 

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

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

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

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

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

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

220 name=name or self.name) 

221 

222 @Property 

223 def geodesic(self): 

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

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

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

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

228 ''' 

229 g = self._geodesic 

230 if g is None: 

231 E = self.datum.ellipsoid 

232 try: 

233 g = E.geodesic 

234 except ImportError: 

235 g = E.geodesicx 

236 self._geodesic = g 

237 return g 

238 

239 @geodesic.setter # PYCHOK setter! 

240 def geodesic(self, exact): 

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

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

243 

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

245 project/geographiclib>} not installed or 

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

247 ''' 

248 E = self.datum.ellipsoid 

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

250 E.geodesicx if exact else E.geodesic) 

251 self.reset(*self.latlon0) 

252 

253 @Property_RO 

254 def isExact(self): 

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

256 ''' 

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

258 

259 @Property_RO 

260 def lat0(self): 

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

262 ''' 

263 return self.latlon0.lat 

264 

265 @property 

266 def latlon0(self): 

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

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

269 ''' 

270 return self._latlon0 

271 

272 @latlon0.setter # PYCHOK setter! 

273 def latlon0(self, latlon0): 

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

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

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

277 

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

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

280 ''' 

281 if islistuple(latlon0, 2): 

282 lat0, lon0 = latlon0[:2] 

283 else: 

284 try: 

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

286 self._datumatch(latlon0) 

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

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

289 self.reset(lat0, lon0) 

290 

291 @Property_RO 

292 def lon0(self): 

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

294 ''' 

295 return self.latlon0.lon 

296 

297 @deprecated_Property_RO 

298 def majoradius(self): # PYCHOK no cover 

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

300 return self.equatoradius 

301 

302 def reset(self, lat0, lon0): 

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

304 

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

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

307 

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

309 ''' 

310 _update_all(self) 

311 

312 g = self.geodesic 

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

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

315 g.STANDARD | g.DISTANCE_IN | g.LINE_OFF) 

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

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

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

319 

320 def reverse(self, easting, northing, name=NN, LatLon=None, **LatLon_kwds): 

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

322 lat- and longitude. 

323 

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

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

326 @kwarg name: Name inlieu of this projection's name (C{str}). 

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

328 geodetic location as (C{LatLon}) or C{None}. 

329 @kwarg LatLon_kwds: Optional (C{LatLon}) keyword arguments, 

330 ignored if C{B{LatLon} is None}. 

331 

332 @return: Geodetic location B{C{LatLon}} or if B{C{LatLon}} 

333 is C{None}, a L{LatLon2Tuple}C{(lat, lon)}. 

334 

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

336 

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

338 

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

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

341 ''' 

342 r = self.reverse4(easting, northing, name=name) 

343 if LatLon is None: 

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

345 else: 

346 _xsubclassof(_LLEB, LatLon=LatLon) 

347 kwds = _xkwds(LatLon_kwds, datum=self.datum, name=r.name) 

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

349 self._datumatch(r) 

350 return r 

351 

352 def reverse4(self, easting, northing, name=NN): 

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

354 lat- and longitude. 

355 

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

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

358 @kwarg name: Name inlieu of this projection's name (C{str}). 

359 

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

361 

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

363 and L{CassiniSoldner.forward4}. 

364 ''' 

365 g = self.geodesic 

366 n = self._meridian.Position(northing) 

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

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

369 # include z azimuth of easting direction and rk reciprocal 

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

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

372 return LatLonAziRk4Tuple(r.lat2, r.lon2, z, r.M12, name=name or self.name) 

373 

374 toLatLon = reverse # XXX not reverse4 

375 

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

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

378 

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

380 

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

382 (C{str}). 

383 ''' 

384 return _fstrLL0(self, prec, True) 

385 

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

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

388 

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

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

391 

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

393 ''' 

394 t = _fstrLL0(self, prec, False) 

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

396 

397 

398class Css(_NamedBase): 

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

400 ''' 

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

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

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

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

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

406 

407 def __init__(self, e, n, h=0, cs0=None, name=NN): 

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

409 

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

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

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

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

414 (L{CassiniSoldner}). 

415 @kwarg name: Optional name (C{str}). 

416 

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

418 

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

420 

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

422 

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

424 

425 @example: 

426 

427 >>> cs = Css(448251, 5411932.0001) 

428 ''' 

429 self._cs0 = _CS0(cs0) 

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

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

432 if h: 

433 self._height = Height(h=h) 

434 if name: 

435 self.name = name 

436 

437 @Property_RO 

438 def azi(self): 

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

440 ''' 

441 return self.reverse4.azimuth 

442 

443 azimuth = azi 

444 

445 @Property 

446 def cs0(self): 

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

448 ''' 

449 return self._cs0 or Css._CS0 

450 

451 @cs0.setter # PYCHOK setter! 

452 def cs0(self, cs0): 

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

454 

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

456 ''' 

457 cs0 = _CS0(cs0) 

458 if cs0 != self._cs0: 

459 _update_all(self) 

460 self._cs0 = cs0 

461 

462# def dup(self, name=NN, **e_n_h_cs0): # PYCHOK signature 

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

464# 

465# @kwarg e_n_h_cs0: Use keyword argument C{B{e}=...}, C{B{n}=...}, 

466# C{B{h}=...} and/or C{B{cs0}=...} to override 

467# the current C{easting}, C{northing} C{height} 

468# or C{cs0} projectio, respectively. 

469# ''' 

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

471# return (e, n), kwds 

472# 

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

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

475# name=name or self.name) 

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

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

478 

479 @Property_RO 

480 def easting(self): 

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

482 ''' 

483 return self._easting 

484 

485 @Property_RO 

486 def height(self): 

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

488 ''' 

489 return self._height 

490 

491 @Property_RO 

492 def latlon(self): 

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

494 ''' 

495 r = self.reverse4 

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

497 

498 @Property_RO 

499 def northing(self): 

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

501 ''' 

502 return self._northing 

503 

504 @Property_RO 

505 def reverse4(self): 

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

507 ''' 

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

509 

510 @Property_RO 

511 def rk(self): 

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

513 ''' 

514 return self.reverse4.reciprocal 

515 

516 reciprocal = rk 

517 

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

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

520 

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

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

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

524 default height (C{meter}). 

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

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

527 

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

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

530 datum)}. 

531 

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

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

534 B{C{LatLon_kwds}}. 

535 ''' 

536 if LatLon: 

537 _xsubclassof(_LLEB, LatLon=LatLon) 

538 

539 lat, lon = self.latlon 

540 h = self.height if height is None else Height(height) 

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

542 inst=self, name=self.name) 

543 

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

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

546 

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

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

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

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

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

552 

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

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

555 ''' 

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

557 if self.name: 

558 t += repr(self.name), 

559 T += _name_, 

560 if C: 

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

562 T += _C_, 

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

564 

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

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

567 

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

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

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

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

572 

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

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

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

576 ''' 

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

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

579 

580 

581class EasNorAziRk4Tuple(_NamedTuple): 

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

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

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

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

586 ''' 

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

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

589 

590 

591class EasNorAziRkEqu6Tuple(_NamedTuple): 

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

593 equatorazimuth)} for the Cassini-Soldner location with 

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

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

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

597 ''' 

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

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

600 

601 

602class LatLonAziRk4Tuple(_NamedTuple): 

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

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

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

606 ''' 

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

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

609 

610 

611def toCss(latlon, cs0=None, height=None, Css=Css, name=NN): 

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

613 location. 

614 

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

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

617 (L{CassiniSoldner}). 

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

619 default height (C{meter}). 

620 @kwarg Css: Optional class to return the location (L{Css}) or C{None}. 

621 @kwarg name: Optional B{C{Css}} name (C{str}). 

622 

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

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

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

626 

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

628 

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

630 project/geographiclib>} not installed or 

631 not found. 

632 

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

634 ''' 

635 _xinstanceof(_LLEB, LatLon4Tuple, latlon=latlon) 

636 

637 cs = _CS0(cs0) 

638 cs._datumatch(latlon) 

639 

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

641 h = latlon.height if height is None else Height(height) 

642 n = name or nameof(latlon) 

643 

644 if Css is None: 

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

646 else: 

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

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

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

650 return r 

651 

652# **) MIT License 

653# 

654# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved. 

655# 

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

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

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

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

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

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

662# 

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

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

665# 

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

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

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

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

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

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

672# OTHER DEALINGS IN THE SOFTWARE.