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, _xkwds

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

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

18 _northing_, _reciprocal_, _SPACE_

19 from pygeodesy.interns import _C_ # PYCHOK used!

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

21 _norm180, _signBit, _sincos2d, fabs

22 from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS

23 from pygeodesy.named import _NamedBase, _NamedTuple, nameof

24 from pygeodesy.namedTuples import EasNor2Tuple, EasNor3Tuple, \

25 LatLon2Tuple, LatLon4Tuple, _LL4Tuple

26 from pygeodesy.props import deprecated_Property_RO, Property, \

27 Property_RO, _update_all

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

29 from 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.03.29'

36

37

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

50 class CSSError(_ValueError):

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

52 '''

53 pass

54

55

56 class 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,Karney} U{Geodesic

225 <https://GeographicLib.SourceForge.io/C++/doc/python/code.html>},

226 provided package U{geographiclib

227 <https://PyPI.org/project/geographiclib>} is installed,

228 otherwise a L{GeodesicExact} instance.

229 '''

230 g = self._geodesic

231 if g is None:

232 E = self.datum.ellipsoid

233 try:

234 g = E.geodesic

235 except ImportError:

236 g = E.geodesicx

237 self._geodesic = g

238 return g

239

240 @geodesic.setter # PYCHOK setter!

241 def geodesic(self, exact):

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

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

244

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

246 project/geographiclib>} not installed or

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

248 '''

249 E = self.datum.ellipsoid

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

251 E.geodesicx if exact else E.geodesic)

252 self.reset(*self.latlon0)

253

254 @Property_RO

255 def isExact(self):

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

257 '''

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

259

260 @Property_RO

261 def lat0(self):

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

263 '''

264 return self.latlon0.lat

265

266 @property

267 def latlon0(self):

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

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

270 '''

271 return self._latlon0

272

273 @latlon0.setter # PYCHOK setter!

274 def latlon0(self, latlon0):

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

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

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

278

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

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

281 '''

282 if islistuple(latlon0, 2):

283 lat0, lon0 = latlon0[:2]

284 else:

285 try:

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

287 self._datumatch(latlon0)

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

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

290 self.reset(lat0, lon0)

291

292 @Property_RO

293 def lon0(self):

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

295 '''

296 return self.latlon0.lon

297

298 @deprecated_Property_RO

299 def majoradius(self): # PYCHOK no cover

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

301 return self.equatoradius

302

303 def reset(self, lat0, lon0):

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

305

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

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

308

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

310 '''

311 _update_all(self)

312

313 g = self.geodesic

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

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

316 g.STANDARD | g.DISTANCE_IN | g.LINE_OFF)

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

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

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

320

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

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

323 lat- and longitude.

324

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

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

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

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

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

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

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

332

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

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

335

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

337

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

339

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

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

342 '''

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

344 if LatLon is None:

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

346 else:

347 _xsubclassof(_LLEB, LatLon=LatLon)

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

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

350 self._datumatch(r)

351 return r

352

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

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

355 lat- and longitude.

356

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

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

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

360

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

362

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

364 and L{CassiniSoldner.forward4}.

365 '''

366 g = self.geodesic

367 n = self._meridian.Position(northing)

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

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

370 # include z azimuth of easting direction and rk reciprocal

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

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

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

374

375 toLatLon = reverse # XXX not reverse4

376

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

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

379

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

381

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

383 (C{str}).

384 '''

385 return _fstrLL0(self, prec, True)

386

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

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

389

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

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

392

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

394 '''

395 t = _fstrLL0(self, prec, False)

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

397

398

399 class Css(_NamedBase):

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

401 '''

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

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

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

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

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

407

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

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

410

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

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

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

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

415 (L{CassiniSoldner}).

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

417

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

419

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

421

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

423

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

425

426 @example:

427

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

429 '''

430 self._cs0 = _CS0(cs0)

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

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

433 if h:

434 self._height = Height(h=h)

435 if name:

436 self.name = name

437

438 @Property_RO

439 def azi(self):

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

441 '''

442 return self.reverse4.azimuth

443

444 azimuth = azi

445

446 @Property

447 def cs0(self):

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

449 '''

450 return self._cs0 or Css._CS0

451

452 @cs0.setter # PYCHOK setter!

453 def cs0(self, cs0):

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

455

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

457 '''

458 cs0 = _CS0(cs0)

459 if cs0 != self._cs0:

460 _update_all(self)

461 self._cs0 = cs0

462

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

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

465#

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

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

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

469# or C{cs0} projectio, respectively.

470# '''

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

472# return (e, n), kwds

473#

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

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

476# name=name or self.name)

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

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

479

480 @Property_RO

481 def easting(self):

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

483 '''

484 return self._easting

485

486 @Property_RO

487 def height(self):

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

489 '''

490 return self._height

491

492 @Property_RO

493 def latlon(self):

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

495 '''

496 r = self.reverse4

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

498

499 @Property_RO

500 def northing(self):

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

502 '''

503 return self._northing

504

505 @Property_RO

506 def reverse4(self):

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

508 '''

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

510

511 @Property_RO

512 def rk(self):

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

514 '''

515 return self.reverse4.reciprocal

516

517 reciprocal = rk

518

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

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

521

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

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

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

525 default height (C{meter}).

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

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

528

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

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

531 datum)}.

532

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

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

535 B{C{LatLon_kwds}}.

536 '''

537 if LatLon:

538 _xsubclassof(_LLEB, LatLon=LatLon)

539

540 lat, lon = self.latlon

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

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

543 inst=self, name=self.name)

544

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

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

547

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

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

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

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

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

553

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

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

556 '''

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

558 if self.name:

559 t += repr(self.name),

560 T += _name_,

561 if C:

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

563 T += _C_,

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

565

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

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

568

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

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

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

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

573

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

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

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

577 '''

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

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

580

581

582 class EasNorAziRk4Tuple(_NamedTuple):

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

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

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

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

587 '''

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

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

590

591

592 class EasNorAziRkEqu6Tuple(_NamedTuple):

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

594 equatorazimuth)} for the Cassini-Soldner location with

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

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

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

598 '''

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

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

601

602

603 class LatLonAziRk4Tuple(_NamedTuple):

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

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

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

607 '''

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

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

610

611

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

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

614 location.

615

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

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

618 (L{CassiniSoldner}).

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

620 default height (C{meter}).

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

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

623

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

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

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

627

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

629

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

631 project/geographiclib>} not installed or

632 not found.

633

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

635 '''

636 _xinstanceof(_LLEB, LatLon4Tuple, latlon=latlon)

637

638 cs = _CS0(cs0)

639 cs._datumatch(latlon)

640

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

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

643 n = name or nameof(latlon)

644

645 if Css is None:

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

647 else:

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

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

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

651 return r

652

653# **) MIT License

654#

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

656#

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

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

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

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

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

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

663#

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

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

666#

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

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

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

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

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

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

673# OTHER DEALINGS IN THE SOFTWARE.