1

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

3

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

5

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

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

8 if installed, see property L{CassiniSoldner.geodesic}.

9'''

10

11 from pygeodesy.basics import _isin, islistuple, neg, _xinstanceof, \

12 _xsubclassof

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

14 from pygeodesy.datums import _ellipsoidal_datum, _WGS84

15 from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase as _LLEB

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

17 _xattr, _xkwds

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

19 _lat_, _lon_, _m_, _name_, _northing_, \

20 _reciprocal_, _SPACE_

21 from pygeodesy.interns import _C_ # PYCHOK used!

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

23 _norm180, _signBit, _sincos2d, fabs

24 from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS

25 from pygeodesy.named import _name2__, _NamedBase, _NamedTuple

26 from pygeodesy.namedTuples import EasNor2Tuple, EasNor3Tuple, \

27 LatLon2Tuple, LatLon4Tuple, _LL4Tuple

28 from pygeodesy.props import deprecated_Property_RO, Property, \

29 Property_RO, _update_all

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

31 from pygeodesy.units import Azimuth, Degrees, Easting, Height, _heigHt, \

32 Lat_, Lon_, Northing, Scalar

33

34# from math import fabs # from .karney

35

36__all__ = _ALL_LAZY.css

37__version__ = '25.04.14'

38

39

40 def _CS0(cs0):

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

42 '''

43 if cs0 is None:

44 cs0 = Css._CS0

45 if cs0 is None:

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

47 else:

48 _xinstanceof(CassiniSoldner, cs0=cs0)

49 return cs0

50

51

52 class CSSError(_ValueError):

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

54 '''

55 pass

56

57

58 class CassiniSoldner(_NamedBase):

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

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

61 '''

62 _cb0 = _0_0

63 _datum = _WGS84 # L{Datum}

64 _geodesic = None

65 _latlon0 = ()

66 _meridian = None

67 _sb0 = _0_0

68

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

70 '''New L{CassiniSoldner} projection.

71

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

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

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

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

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

77

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

79 '''

80 if not _isin(datum, None, self._datum):

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

82 if name:

83 self.name = name

84

85 self.reset(lat0, lon0)

86

87 @Property

88 def datum(self):

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

90 '''

91 return self._datum

92

93 @datum.setter # PYCHOK setter!

94 def datum(self, datum):

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

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

97 '''

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

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

100 if self._datum != d:

101 self._datum = d

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

103

104 def _datumatch(self, latlon):

105 '''Check for matching datum ellipsoids.

106

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

108 '''

109 d = _xattr(latlon, datum=None)

110 if d:

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

112

113 @Property_RO

114 def equatoradius(self):

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

116 '''

117 return self.geodesic.a

118

119 a = equatoradius

120

121 @Property_RO

122 def flattening(self):

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

124 '''

125 return self.geodesic.f

126

127 f = flattening

128

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

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

131 easting and northing.

132

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

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

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

136 name (C{str}).

137

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

139

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

141 and L{CassiniSoldner.reverse4}.

142

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

144 '''

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

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

147

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

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

150 easting and northing.

151

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

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

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

155 name (C{str}).

156

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

158 azimuth, reciprocal)}.

159

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

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

162

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

164 '''

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

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

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

168

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

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

171 easting and northing.

172

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

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

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

176 name (C{str}).

177

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

179 azimuth, reciprocal, equatorarc, equatorazimuth)}.

180

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

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

183

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

185 '''

186 g = self.geodesic

187

188 lat = Lat_(lat, Error=CSSError)

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

190 D = fabs(d)

191

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

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

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

195 if e == 0: # PYCHOK no cover

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

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

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

199 if _signBit(d):

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

201

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

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

204 # reciprocal of azimuthal northing scale

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

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

207

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

209 sb1 = _copysign(c, lat)

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

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

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

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

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

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

216 name=self._name__(name))

217

218 @Property

219 def geodesic(self):

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

221 <https://GeographicLib.SourceForge.io/Python/doc/code.html>} from

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

223 package if installed, otherwise an I{exact} L{GeodesicExact

224 <pygeodesy.geodesicx.GeodesicExact>} instance.

225 '''

226 g = self._geodesic

227 if g is None:

228 E = self.datum.ellipsoid

229 try:

230 g = E.geodesicw

231 except ImportError:

232 g = E.geodesicx

233 self._geodesic = g

234 return g

235

236 @geodesic.setter # PYCHOK setter!

237 def geodesic(self, exact):

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

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

240

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

242 project/geographiclib>} not installed or

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

244 '''

245 E = self.datum.ellipsoid

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

247 E.geodesicx if exact else E.geodesicw)

248 self.reset(*self.latlon0)

249

250 @Property_RO

251 def isExact(self):

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

253 <pygeodesy.geodesicx.GeodesicExact>}.

254 '''

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

256

257 @Property_RO

258 def lat0(self):

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

260 '''

261 return self.latlon0.lat

262

263 @property

264 def latlon0(self):

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

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

267 '''

268 return self._latlon0

269

270 @latlon0.setter # PYCHOK setter!

271 def latlon0(self, latlon0):

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

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

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

275

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

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

278 '''

279 if islistuple(latlon0, 2):

280 lat0, lon0 = latlon0[:2]

281 else:

282 try:

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

284 self._datumatch(latlon0)

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

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

287 self.reset(lat0, lon0)

288

289 @Property_RO

290 def lon0(self):

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

292 '''

293 return self.latlon0.lon

294

295 @deprecated_Property_RO

296 def majoradius(self): # PYCHOK no cover

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

298 return self.equatoradius

299

300 def reset(self, lat0, lon0):

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

302

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

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

305

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

307 '''

308 _update_all(self)

309

310 g = self.geodesic

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

312 Lon_(lon0=lon0, Error=CSSError),

313 _0_0, caps=g.STANDARD_LINE | g.LINE_OFF)

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

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

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

317

318 def reverse(self, easting, northing, LatLon=None, **name_LatLon_kwds):

319 '''Convert a Cassini-Soldner location to (ellipsoidal) geodetic lat- and longitude.

320

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

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

323 @kwarg LatLon: Optional, ellipsoidal class to return the geodetic location as

324 (C{LatLon}) or C{None}.

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

326 additional B{C{LatLon}} keyword arguments, ignored if C{B{LatLon}

327 is None}.

328

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

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

331

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

333

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

335

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

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

338 '''

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

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

341 if LatLon is None:

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

343 else:

344 _xsubclassof(_LLEB, LatLon=LatLon)

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

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

347 self._datumatch(r)

348 return r

349

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

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

352 lat- and longitude.

353

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

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

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

357 name (C{str}).

358

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

360

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

362 and L{CassiniSoldner.forward4}.

363 '''

364 g = self.geodesic

365 n = self._meridian.Position(northing)

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

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

368 # include z azimuth of easting direction and rk reciprocal

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

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

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

372

373 toLatLon = reverse # XXX not reverse4

374

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

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

377

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

379

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

381 (C{str}).

382 '''

383 return _fstrLL0(self, prec, True)

384

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

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

387

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

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

390

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

392 '''

393 t = _fstrLL0(self, prec, False)

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

395

396

397 class Css(_NamedBase):

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

399 '''

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

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

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

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

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

405

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

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

408

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

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

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

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

413 (L{CassiniSoldner}).

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

415

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

417

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

419

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

421

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

423 '''

424 self._cs0 = _CS0(cs0)

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

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

427 if h:

428 self._height = Height(h=h)

429 if name:

430 self.name = name

431

432 @Property_RO

433 def azi(self):

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

435 '''

436 return self.reverse4.azimuth

437

438 azimuth = azi

439

440 @Property

441 def cs0(self):

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

443 '''

444 return self._cs0 or Css._CS0

445

446 @cs0.setter # PYCHOK setter!

447 def cs0(self, cs0):

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

449

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

451 '''

452 cs0 = _CS0(cs0)

453 if cs0 != self._cs0:

454 _update_all(self)

455 self._cs0 = cs0

456

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

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

459#

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

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

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

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

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

465# '''

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

467# return (e, n), kwds

468#

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

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

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

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

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

474

475 @Property_RO

476 def easting(self):

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

478 '''

479 return self._easting

480

481 @Property_RO

482 def height(self):

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

484 '''

485 return self._height

486

487 @Property_RO

488 def latlon(self):

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

490 '''

491 r = self.reverse4

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

493

494 @Property_RO

495 def northing(self):

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

497 '''

498 return self._northing

499

500 @Property_RO

501 def reverse4(self):

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

503 '''

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

505

506 @Property_RO

507 def rk(self):

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

509 '''

510 return self.reverse4.reciprocal

511

512 reciprocal = rk

513

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

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

516

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

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

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

520 default height (C{meter}).

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

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

523

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

525 None}, a L{LatLon4Tuple}C{(lat, lon, height, 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

576 class 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, Azimuth, Scalar)

584

585

586 class 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, Azimuth)

595

596

597 class 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_, Azimuth, Scalar)

604

605

606 def 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 default

614 height (C{meter}).

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

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

617

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

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

620

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

622

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

624 project/geographiclib>} not installed or

625 not found.

626

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

628 '''

629 _xinstanceof(_LLEB, LatLon4Tuple, latlon=latlon)

630

631 cs = _CS0(cs0)

632 cs._datumatch(latlon)

633

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

635 h = _heigHt(latlon, height)

636 n = latlon._name__(name)

637

638 if Css is None:

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

640 else:

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

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

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

644 return r

645

646# **) MIT License

647#

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

649#

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

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

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

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

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

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

656#

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

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

659#

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

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

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

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

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

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

666# OTHER DEALINGS IN THE SOFTWARE.