1

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

3

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

5

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

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

8 installed.

9'''

10

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

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

13 from pygeodesy.datums import _ellipsoidal_datum, _WGS84

14 from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase as _LLEB

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

16 _xattr, _xkwds

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

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

19 _reciprocal_, _SPACE_

20 from pygeodesy.interns import _C_ # PYCHOK used!

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

22 _norm180, _signBit, _sincos2d, fabs

23 from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS

24 from pygeodesy.named import _name2__, _NamedBase, _NamedTuple

25 from pygeodesy.namedTuples import EasNor2Tuple, EasNor3Tuple, \

26 LatLon2Tuple, LatLon4Tuple, _LL4Tuple

27 from pygeodesy.props import deprecated_Property_RO, Property, \

28 Property_RO, _update_all

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

30 from pygeodesy.units import Azimuth, 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.07.25'

37

38

39 def _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

51 class CSSError(_ValueError):

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

53 '''

54 pass

55

56

57 class 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, **name_LatLon_kwds):

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 name_LatLon_kwds: Optional name C{B{name}=NN} (C{str}) and

324 optional, additional B{C{LatLon}} keyword arguments,

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

326

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

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

329

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

331

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

333

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

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

336 '''

337 n, kwds = _name2__(name_LatLon_kwds, _or_nameof=self)

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

339 if LatLon is None:

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

341 else:

342 _xsubclassof(_LLEB, LatLon=LatLon)

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

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

345 self._datumatch(r)

346 return r

347

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

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

350 lat- and longitude.

351

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

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

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

355 name (C{str}).

356

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

358

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

360 and L{CassiniSoldner.forward4}.

361 '''

362 g = self.geodesic

363 n = self._meridian.Position(northing)

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

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

366 # include z azimuth of easting direction and rk reciprocal

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

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

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

370

371 toLatLon = reverse # XXX not reverse4

372

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

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

375

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

377

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

379 (C{str}).

380 '''

381 return _fstrLL0(self, prec, True)

382

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

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

385

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

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

388

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

390 '''

391 t = _fstrLL0(self, prec, False)

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

393

394

395 class Css(_NamedBase):

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

397 '''

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

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

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

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

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

403

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

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

406

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

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

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

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

411 (L{CassiniSoldner}).

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

413

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

415

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

417

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

419

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

421 '''

422 self._cs0 = _CS0(cs0)

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

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

425 if h:

426 self._height = Height(h=h)

427 if name:

428 self.name = name

429

430 @Property_RO

431 def azi(self):

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

433 '''

434 return self.reverse4.azimuth

435

436 azimuth = azi

437

438 @Property

439 def cs0(self):

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

441 '''

442 return self._cs0 or Css._CS0

443

444 @cs0.setter # PYCHOK setter!

445 def cs0(self, cs0):

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

447

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

449 '''

450 cs0 = _CS0(cs0)

451 if cs0 != self._cs0:

452 _update_all(self)

453 self._cs0 = cs0

454

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

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

457#

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

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

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

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

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

463# '''

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

465# return (e, n), kwds

466#

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

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

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

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

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

472

473 @Property_RO

474 def easting(self):

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

476 '''

477 return self._easting

478

479 @Property_RO

480 def height(self):

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

482 '''

483 return self._height

484

485 @Property_RO

486 def latlon(self):

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

488 '''

489 r = self.reverse4

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

491

492 @Property_RO

493 def northing(self):

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

495 '''

496 return self._northing

497

498 @Property_RO

499 def reverse4(self):

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

501 '''

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

503

504 @Property_RO

505 def rk(self):

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

507 '''

508 return self.reverse4.reciprocal

509

510 reciprocal = rk

511

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

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

514

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

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

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

518 default height (C{meter}).

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

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

521

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

523 None}, a L{LatLon4Tuple}C{(lat, lon, height, datum)}.

524

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

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

527 B{C{LatLon_kwds}}.

528 '''

529 if LatLon:

530 _xsubclassof(_LLEB, LatLon=LatLon)

531

532 lat, lon = self.latlon

533 h = _heigHt(self, height)

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

535 inst=self, name=self.name)

536

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

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

539

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

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

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

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

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

545

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

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

548 '''

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

550 if self.name:

551 t += repr(self.name),

552 T += _name_,

553 if C:

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

555 T += _C_,

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

557

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

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

560

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

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

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

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

565

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

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

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

569 '''

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

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

572

573

574 class EasNorAziRk4Tuple(_NamedTuple):

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

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

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

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

579 '''

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

581 _Units_ = ( Easting, Northing, Azimuth, Scalar)

582

583

584 class EasNorAziRkEqu6Tuple(_NamedTuple):

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

586 equatorazimuth)} for the Cassini-Soldner location with

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

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

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

590 '''

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

592 _Units_ = EasNorAziRk4Tuple._Units_ + ( Degrees, Azimuth)

593

594

595 class LatLonAziRk4Tuple(_NamedTuple):

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

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

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

599 '''

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

601 _Units_ = ( Lat_, Lon_, Azimuth, Scalar)

602

603

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

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

606 location.

607

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

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

610 (L{CassiniSoldner}).

611 @kwarg height: Optional height for the point, overriding the default

612 height (C{meter}).

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

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

615

616 @return: The Cassini-Soldner location (B{C{Css}}) or if C{B{Css} is

617 None}, an L{EasNor3Tuple}C{(easting, northing, height)}.

618

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

620

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

622 project/geographiclib>} not installed or

623 not found.

624

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

626 '''

627 _xinstanceof(_LLEB, LatLon4Tuple, latlon=latlon)

628

629 cs = _CS0(cs0)

630 cs._datumatch(latlon)

631

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

633 h = _heigHt(latlon, height)

634 n = latlon._name__(name)

635

636 if Css is None:

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

638 else:

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

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

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

642 return r

643

644# **) MIT License

645#

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

647#

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

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

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

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

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

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

654#

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

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

657#

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

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

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

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

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

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

664# OTHER DEALINGS IN THE SOFTWARE.