Coverage for pygeodesy/css.py: 97%
236 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-25 12:04 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-25 12:04 -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, \
16 _xattr, _xkwds
17from pygeodesy.interns import _azimuth_, _COMMASPACE_, _easting_, \
18 _lat_, _lon_, _m_, _name_, _northing_, \
19 _reciprocal_, _SPACE_
20from pygeodesy.interns import _C_ # PYCHOK used!
21from pygeodesy.karney import _atan2d, _copysign, _diff182, _norm2, \
22 _norm180, _signBit, _sincos2d, fabs
23from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS
24from pygeodesy.named import _name2__, _NamedBase, _NamedTuple
25from pygeodesy.namedTuples import EasNor2Tuple, EasNor3Tuple, \
26 LatLon2Tuple, LatLon4Tuple, _LL4Tuple
27from pygeodesy.props import deprecated_Property_RO, Property, \
28 Property_RO, _update_all
29from pygeodesy.streprs import Fmt, _fstrENH2, _fstrLL0, _xzipairs
30from pygeodesy.units import Bearing, Degrees, Easting, Height, _heigHt, \
31 Lat_, Lon_, Northing, Scalar
33# from math import fabs # from .karney
35__all__ = _ALL_LAZY.css
36__version__ = '24.05.24'
39def _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
51class CSSError(_ValueError):
52 '''Cassini-Soldner (CSS) conversion or other L{Css} issue.
53 '''
54 pass
57class 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
68 def __init__(self, lat0, lon0, datum=_WGS84, **name):
69 '''New L{CassiniSoldner} projection.
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}).
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
84 self.reset(lat0, lon0)
86 @Property
87 def datum(self):
88 '''Get the datum (L{Datum}).
89 '''
90 return self._datum
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
103 def _datumatch(self, latlon):
104 '''Check for matching datum ellipsoids.
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)
112 @Property_RO
113 def equatoradius(self):
114 '''Get the ellipsoid's equatorial radius, semi-axis (C{meter}).
115 '''
116 return self.geodesic.a
118 a = equatoradius
120 @Property_RO
121 def flattening(self):
122 '''Get the ellipsoid's flattening (C{scalar}).
123 '''
124 return self.geodesic.f
126 f = flattening
128 def forward(self, lat, lon, **name):
129 '''Convert an (ellipsoidal) geodetic location to Cassini-Soldner
130 easting and northing.
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}).
137 @return: An L{EasNor2Tuple}C{(easting, northing)}.
139 @see: Methods L{CassiniSoldner.forward4}, L{CassiniSoldner.reverse}
140 and L{CassiniSoldner.reverse4}.
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)
147 def forward4(self, lat, lon, **name):
148 '''Convert an (ellipsoidal) geodetic location to Cassini-Soldner
149 easting and northing.
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}).
156 @return: An L{EasNorAziRk4Tuple}C{(easting, northing,
157 azimuth, reciprocal)}.
159 @see: Method L{CassiniSoldner.forward}, L{CassiniSoldner.forward6},
160 L{CassiniSoldner.reverse} and L{CassiniSoldner.reverse4}.
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)
168 def forward6(self, lat, lon, **name):
169 '''Convert an (ellipsoidal) geodetic location to Cassini-Soldner
170 easting and northing.
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}).
177 @return: An L{EasNorAziRkEqu6Tuple}C{(easting, northing,
178 azimuth, reciprocal, equatorarc, equatorazimuth)}.
180 @see: Method L{CassiniSoldner.forward}, L{CassiniSoldner.forward4},
181 L{CassiniSoldner.reverse} and L{CassiniSoldner.reverse4}.
183 @raise CSSError: Invalid B{C{lat}} or B{C{lon}}.
184 '''
185 g = self.geodesic
187 lat = Lat_(lat, Error=CSSError)
188 d, _ = _diff182(self.lon0, Lon_(lon, Error=CSSError)) # _2sum
189 D = fabs(d)
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
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]
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))
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
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.
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)
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)
254 @Property_RO
255 def lat0(self):
256 '''Get the center latitude (C{degrees90}).
257 '''
258 return self.latlon0.lat
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
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}).
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)
286 @Property_RO
287 def lon0(self):
288 '''Get the center longitude (C{degrees180}).
289 '''
290 return self.latlon0.lon
292 @deprecated_Property_RO
293 def majoradius(self): # PYCHOK no cover
294 '''DEPRECATED, use property C{equatoradius}.'''
295 return self.equatoradius
297 def reset(self, lat0, lon0):
298 '''Set or reset the center point of this Cassini-Soldner projection.
300 @arg lat0: Center point latitude (C{degrees90}).
301 @arg lon0: Center point longitude (C{degrees180}).
303 @raise CSSError: Invalid B{C{lat0}} or B{C{lon0}}.
304 '''
305 _update_all(self)
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)
315 def reverse(self, easting, northing, LatLon=None, **LatLon_kwds_name):
316 '''Convert a Cassini-Soldner location to (ellipsoidal) geodetic
317 lat- and longitude.
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 LatLon_kwds_name: Optional (C{LatLon}) keyword arguments,
324 ignored if C{B{LatLon} is None} and optional
325 C{B{name}=NN} inlieu of this this projection's
326 name (C{str}).
328 @return: Geodetic location B{C{LatLon}} or if B{C{LatLon}} is C{None},
329 a L{LatLon2Tuple}C{(lat, lon)}.
331 @raise CSSError: Ellipsoidal mismatch of B{C{LatLon}} and this projection.
333 @raise TypeError: Invalid B{C{LatLon}} or B{C{LatLon_kwds}}.
335 @see: Method L{CassiniSoldner.reverse4}, L{CassiniSoldner.forward}.
336 L{CassiniSoldner.forward4} and L{CassiniSoldner.forward6}.
337 '''
338 n, kwds = _name2__(LatLon_kwds_name, _or_nameof=self)
339 r = self.reverse4(easting, northing, name=n)
340 if LatLon is None:
341 r = LatLon2Tuple(r.lat, r.lon, name=r.name) # PYCHOK expected
342 else:
343 _xsubclassof(_LLEB, LatLon=LatLon)
344 kwds = _xkwds(kwds, datum=self.datum, name=r.name)
345 r = LatLon(r.lat, r.lon, **kwds) # PYCHOK expected
346 self._datumatch(r)
347 return r
349 def reverse4(self, easting, northing, **name):
350 '''Convert a Cassini-Soldner location to (ellipsoidal) geodetic
351 lat- and longitude.
353 @arg easting: Easting of the location (C{meter}).
354 @arg northing: Northing of the location (C{meter}).
355 @kwarg name: Optional B{C{name}=NN} inlieu of this projection's
356 name (C{str}).
358 @return: A L{LatLonAziRk4Tuple}C{(lat, lon, azimuth, reciprocal)}.
360 @see: Method L{CassiniSoldner.reverse}, L{CassiniSoldner.forward}
361 and L{CassiniSoldner.forward4}.
362 '''
363 g = self.geodesic
364 n = self._meridian.Position(northing)
365 r = g.Direct(n.lat2, n.lon2, n.azi2 + _90_0, easting, outmask=g.STANDARD | g.GEODESICSCALE)
366 z = _umod_360(r.azi2) # -180 <= r.azi2 < 180 ... 0 <= z < 360
367 # include z azimuth of easting direction and rk reciprocal
368 # of azimuthal northing scale (see C++ member Direct() 5/6
369 # <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geodesic.html>)
370 return LatLonAziRk4Tuple(r.lat2, r.lon2, z, r.M12, name=self._name__(name))
372 toLatLon = reverse # XXX not reverse4
374 def toRepr(self, prec=6, **unused): # PYCHOK expected
375 '''Return a string representation of this projection.
377 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).
379 @return: This projection as C{"<classname>(lat0, lon0, ...)"}
380 (C{str}).
381 '''
382 return _fstrLL0(self, prec, True)
384 def toStr(self, prec=6, sep=_SPACE_, **unused): # PYCHOK expected
385 '''Return a string representation of this projection.
387 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).
388 @kwarg sep: Separator to join (C{str}).
390 @return: This projection as C{"lat0 lon0"} (C{str}).
391 '''
392 t = _fstrLL0(self, prec, False)
393 return t if sep is None else sep.join(t)
396class Css(_NamedBase):
397 '''Cassini-Soldner East-/Northing location.
398 '''
399 _CS0 = None # default projection (L{CassiniSoldner})
400 _cs0 = None # projection (L{CassiniSoldner})
401 _easting = _0_0 # easting (C{float})
402 _height = 0 # height (C{meter})
403 _northing = _0_0 # northing (C{float})
405 def __init__(self, e, n, h=0, cs0=None, **name):
406 '''New L{Css} Cassini-Soldner position.
408 @arg e: Easting (C{meter}).
409 @arg n: Northing (C{meter}).
410 @kwarg h: Optional height (C{meter}).
411 @kwarg cs0: Optional, the Cassini-Soldner projection
412 (L{CassiniSoldner}).
413 @kwarg name: Optional C{B{name}=NN} (C{str}).
415 @return: The Cassini-Soldner location (L{Css}).
417 @raise CSSError: If B{C{e}} or B{C{n}} is invalid.
419 @raise TypeError: If B{C{cs0}} is not L{CassiniSoldner}.
421 @raise ValueError: Invalid B{C{h}}.
422 '''
423 self._cs0 = _CS0(cs0)
424 self._easting = Easting(e, Error=CSSError)
425 self._northing = Northing(n, Error=CSSError)
426 if h:
427 self._height = Height(h=h)
428 if name:
429 self.name = name
431 @Property_RO
432 def azi(self):
433 '''Get the azimuth of easting direction (C{degrees}).
434 '''
435 return self.reverse4.azimuth
437 azimuth = azi
439 @Property
440 def cs0(self):
441 '''Get the projection (L{CassiniSoldner}).
442 '''
443 return self._cs0 or Css._CS0
445 @cs0.setter # PYCHOK setter!
446 def cs0(self, cs0):
447 '''Set the I{Cassini-Soldner} projection (L{CassiniSoldner}).
449 @raise TypeError: Invalid B{C{cs0}}.
450 '''
451 cs0 = _CS0(cs0)
452 if cs0 != self._cs0:
453 _update_all(self)
454 self._cs0 = cs0
456# def dup(self, **e_n_h_cs0_name): # PYCHOK signature
457# '''Duplicate this position with some attributes modified.
458#
459# @kwarg e_n_h_cs0_name: Use keyword argument C{B{e}=...},
460# C{B{n}=...}, C{B{h}=...} and/or C{B{cs0}=...}
461# to override the current C{easting}, C{northing}
462# C{height} or C{cs0} projectio, respectively and
463# an optional C{B{name}=NN} (C{str}).
464# '''
465# def _args_kwds(e=None, n=None, **kwds):
466# return (e, n), kwds
467#
468# kwds = _xkwds(e_n_h_cs0, e=self.easting, n=self.northing,
469# h=self.height, cs0=self.cs0,
470# name=_name__(name, _or_nameof(self)))
471# args, kwds = _args_kwds(**kwds)
472# return self.__class__(*args, **kwds) # .classof
474 @Property_RO
475 def easting(self):
476 '''Get the easting (C{meter}).
477 '''
478 return self._easting
480 @Property_RO
481 def height(self):
482 '''Get the height (C{meter}).
483 '''
484 return self._height
486 @Property_RO
487 def latlon(self):
488 '''Get the lat- and longitude (L{LatLon2Tuple}).
489 '''
490 r = self.reverse4
491 return LatLon2Tuple(r.lat, r.lon, name=self.name)
493 @Property_RO
494 def northing(self):
495 '''Get the northing (C{meter}).
496 '''
497 return self._northing
499 @Property_RO
500 def reverse4(self):
501 '''Get the lat, lon, azimuth and reciprocal (L{LatLonAziRk4Tuple}).
502 '''
503 return self.cs0.reverse4(self.easting, self.northing, name=self.name)
505 @Property_RO
506 def rk(self):
507 '''Get the reciprocal of azimuthal northing scale (C{scalar}).
508 '''
509 return self.reverse4.reciprocal
511 reciprocal = rk
513 def toLatLon(self, LatLon=None, height=None, **LatLon_kwds):
514 '''Convert this L{Css} to an (ellipsoidal) geodetic point.
516 @kwarg LatLon: Optional, ellipsoidal class to return the
517 geodetic point (C{LatLon}) or C{None}.
518 @kwarg height: Optional height for the point, overriding the
519 default height (C{meter}).
520 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword
521 arguments, ignored if C{B{LatLon} is None}.
523 @return: The geodetic point (B{C{LatLon}}) or if B{C{LatLon}}
524 is C{None}, a L{LatLon4Tuple}C{(lat, lon, height,
525 datum)}.
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)
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)
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.
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}).
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)
560 def toStr(self, prec=6, sep=_SPACE_, m=_m_): # PYCHOK expected
561 '''Return a string representation of this L{Css} position.
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}).
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)
576class 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, Bearing, Scalar)
586class 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, Bearing)
597class 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_, Bearing, Scalar)
606def toCss(latlon, cs0=None, height=None, Css=Css, **name):
607 '''Convert an (ellipsoidal) geodetic point to a Cassini-Soldner
608 location.
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
614 default height (C{meter}).
615 @kwarg Css: Optional class to return the location (L{Css}) or
616 C{None}.
617 @kwarg name: Optional B{C{Css}} C{B{name}=NN} (C{str}).
619 @return: The Cassini-Soldner location (B{C{Css}}) or an
620 L{EasNor3Tuple}C{(easting, northing, height)}
621 if B{C{Css}} is C{None}.
623 @raise CSSError: Ellipsoidal mismatch of B{C{latlon}} and B{C{cs0}}.
625 @raise ImportError: Package U{geographiclib<https://PyPI.org/
626 project/geographiclib>} not installed or
627 not found.
629 @raise TypeError: If B{C{latlon}} is not ellipsoidal.
630 '''
631 _xinstanceof(_LLEB, LatLon4Tuple, latlon=latlon)
633 cs = _CS0(cs0)
634 cs._datumatch(latlon)
636 c = cs.forward4(latlon.lat, latlon.lon)
637 h = _heigHt(latlon, height)
638 n = latlon._name__(name)
640 if Css is None:
641 r = EasNor3Tuple(c.easting, c.northing, h, name=n)
642 else:
643 r = Css(c.easting, c.northing, h=h, cs0=cs, name=n)
644 r._latlon = LatLon2Tuple(latlon.lat, latlon.lon, name=n)
645 r._azi, r._rk = c.azimuth, c.reciprocal
646 return r
648# **) MIT License
649#
650# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
651#
652# Permission is hereby granted, free of charge, to any person obtaining a
653# copy of this software and associated documentation files (the "Software"),
654# to deal in the Software without restriction, including without limitation
655# the rights to use, copy, modify, merge, publish, distribute, sublicense,
656# and/or sell copies of the Software, and to permit persons to whom the
657# Software is furnished to do so, subject to the following conditions:
658#
659# The above copyright notice and this permission notice shall be included
660# in all copies or substantial portions of the Software.
661#
662# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
663# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
664# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
665# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
666# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
667# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
668# OTHER DEALINGS IN THE SOFTWARE.