Coverage for pygeodesy/css.py: 98%
235 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-04-21 13:14 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-04-21 13:14 -0400
2# -*- coding: utf-8 -*-
4u'''Cassini-Soldner (CSS) projection.
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'''
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
32# from math import fabs # from .karney
34__all__ = _ALL_LAZY.css
35__version__ = '23.04.11'
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
50class CSSError(_ValueError):
51 '''Cassini-Soldner (CSS) conversion or other L{Css} issue.
52 '''
53 pass
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
67 def __init__(self, lat0, lon0, datum=_WGS84, name=NN):
68 '''New L{CassiniSoldner} projection.
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}).
76 @raise CSSError: Invalid B{C{lat}} or B{C{lon}}.
78 @example:
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)
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
92 self.reset(lat0, lon0)
94 @Property
95 def datum(self):
96 '''Get the datum (L{Datum}).
97 '''
98 return self._datum
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
111 def _datumatch(self, latlon):
112 '''Check for matching datum ellipsoids.
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)
120 @Property_RO
121 def equatoradius(self):
122 '''Get the ellipsoid's equatorial radius, semi-axis (C{meter}).
123 '''
124 return self.geodesic.a
126 a = equatoradius
128 @Property_RO
129 def flattening(self):
130 '''Get the ellipsoid's flattening (C{float}).
131 '''
132 return self.geodesic.f
134 f = flattening
136 def forward(self, lat, lon, name=NN):
137 '''Convert an (ellipsoidal) geodetic location to Cassini-Soldner
138 easting and northing.
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}).
144 @return: An L{EasNor2Tuple}C{(easting, northing)}.
146 @see: Methods L{CassiniSoldner.forward4}, L{CassiniSoldner.reverse}
147 and L{CassiniSoldner.reverse4}.
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)
154 def forward4(self, lat, lon, name=NN):
155 '''Convert an (ellipsoidal) geodetic location to Cassini-Soldner
156 easting and northing.
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}).
162 @return: An L{EasNorAziRk4Tuple}C{(easting, northing,
163 azimuth, reciprocal)}.
165 @see: Method L{CassiniSoldner.forward}, L{CassiniSoldner.forward6},
166 L{CassiniSoldner.reverse} and L{CassiniSoldner.reverse4}.
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)
174 def forward6(self, lat, lon, name=NN):
175 '''Convert an (ellipsoidal) geodetic location to Cassini-Soldner
176 easting and northing.
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}).
182 @return: An L{EasNorAziRkEqu6Tuple}C{(easting, northing,
183 azimuth, reciprocal, equatorarc, equatorazimuth)}.
185 @see: Method L{CassiniSoldner.forward}, L{CassiniSoldner.forward4},
186 L{CassiniSoldner.reverse} and L{CassiniSoldner.reverse4}.
188 @raise CSSError: Invalid B{C{lat}} or B{C{lon}}.
189 '''
190 g = self.geodesic
192 lat = Lat_(lat, Error=CSSError)
193 d, _ = _diff182(self.lon0, Lon_(lon, Error=CSSError)) # _2sum
194 D = fabs(d)
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
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]
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)
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
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.
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)
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)
259 @Property_RO
260 def lat0(self):
261 '''Get the center latitude (C{degrees90}).
262 '''
263 return self.latlon0.lat
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
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}).
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)
291 @Property_RO
292 def lon0(self):
293 '''Get the center longitude (C{degrees180}).
294 '''
295 return self.latlon0.lon
297 @deprecated_Property_RO
298 def majoradius(self): # PYCHOK no cover
299 '''DEPRECATED, use property C{equatoradius}.'''
300 return self.equatoradius
302 def reset(self, lat0, lon0):
303 '''Set or reset the center point of this Cassini-Soldner projection.
305 @arg lat0: Center point latitude (C{degrees90}).
306 @arg lon0: Center point longitude (C{degrees180}).
308 @raise CSSError: Invalid B{C{lat0}} or B{C{lon0}}.
309 '''
310 _update_all(self)
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)
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.
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}.
332 @return: Geodetic location B{C{LatLon}} or if B{C{LatLon}}
333 is C{None}, a L{LatLon2Tuple}C{(lat, lon)}.
335 @raise CSSError: Ellipsoidal mismatch of B{C{LatLon}} and this projection.
337 @raise TypeError: Invalid B{C{LatLon}} or B{C{LatLon_kwds}}.
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
352 def reverse4(self, easting, northing, name=NN):
353 '''Convert a Cassini-Soldner location to (ellipsoidal) geodetic
354 lat- and longitude.
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}).
360 @return: A L{LatLonAziRk4Tuple}C{(lat, lon, azimuth, reciprocal)}.
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)
374 toLatLon = reverse # XXX not reverse4
376 def toRepr(self, prec=6, **unused): # PYCHOK expected
377 '''Return a string representation of this projection.
379 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).
381 @return: This projection as C{"<classname>(lat0, lon0, ...)"}
382 (C{str}).
383 '''
384 return _fstrLL0(self, prec, True)
386 def toStr(self, prec=6, sep=_SPACE_, **unused): # PYCHOK expected
387 '''Return a string representation of this projection.
389 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).
390 @kwarg sep: Separator to join (C{str}).
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)
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})
407 def __init__(self, e, n, h=0, cs0=None, name=NN):
408 '''New L{Css} Cassini-Soldner position.
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}).
417 @return: The Cassini-Soldner location (L{Css}).
419 @raise CSSError: If B{C{e}} or B{C{n}} is invalid.
421 @raise TypeError: If B{C{cs0}} is not L{CassiniSoldner}.
423 @raise ValueError: Invalid B{C{h}}.
425 @example:
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
437 @Property_RO
438 def azi(self):
439 '''Get the azimuth of easting direction (C{degrees}).
440 '''
441 return self.reverse4.azimuth
443 azimuth = azi
445 @Property
446 def cs0(self):
447 '''Get the projection (L{CassiniSoldner}).
448 '''
449 return self._cs0 or Css._CS0
451 @cs0.setter # PYCHOK setter!
452 def cs0(self, cs0):
453 '''Set the I{Cassini-Soldner} projection (L{CassiniSoldner}).
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
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
479 @Property_RO
480 def easting(self):
481 '''Get the easting (C{meter}).
482 '''
483 return self._easting
485 @Property_RO
486 def height(self):
487 '''Get the height (C{meter}).
488 '''
489 return self._height
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)
498 @Property_RO
499 def northing(self):
500 '''Get the northing (C{meter}).
501 '''
502 return self._northing
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)
510 @Property_RO
511 def rk(self):
512 '''Get the reciprocal of azimuthal northing scale (C{scalar}).
513 '''
514 return self.reverse4.reciprocal
516 reciprocal = rk
518 def toLatLon(self, LatLon=None, height=None, **LatLon_kwds):
519 '''Convert this L{Css} to an (ellipsoidal) geodetic point.
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}.
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)}.
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)
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)
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.
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}).
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)
565 def toStr(self, prec=6, sep=_SPACE_, m=_m_): # PYCHOK expected
566 '''Return a string representation of this L{Css} position.
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}).
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)
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)
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)
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)
611def toCss(latlon, cs0=None, height=None, Css=Css, name=NN):
612 '''Convert an (ellipsoidal) geodetic point to a Cassini-Soldner
613 location.
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}).
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}.
627 @raise CSSError: Ellipsoidal mismatch of B{C{latlon}} and B{C{cs0}}.
629 @raise ImportError: Package U{geographiclib<https://PyPI.org/
630 project/geographiclib>} not installed or
631 not found.
633 @raise TypeError: If B{C{latlon}} is not ellipsoidal.
634 '''
635 _xinstanceof(_LLEB, LatLon4Tuple, latlon=latlon)
637 cs = _CS0(cs0)
638 cs._datumatch(latlon)
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)
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
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.