Coverage for pygeodesy/ups.py: 97%
166 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-15 16:36 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-15 16:36 -0400
2# -*- coding: utf-8 -*-
4u'''I{Karney}'s Universal Polar Stereographic (UPS) projection.
6Classes L{Ups} and L{UPSError} and functions L{parseUPS5}, L{toUps8} and L{upsZoneBand5}.
8A pure Python implementation, partially transcoded from I{Karney}'s C++ class U{PolarStereographic
9<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1PolarStereographic.html>}.
11The U{UPS<https://WikiPedia.org/wiki/Universal_polar_stereographic_coordinate_system>} system is used
12in conjuction with U{UTM<https://WikiPedia.org/wiki/Universal_Transverse_Mercator_coordinate_system>}
13for locations on the polar regions of the earth. UPS covers areas south of 79.5°S and north of 83.5°N,
14slightly overlapping the UTM range from 80°S to 84°N by 30' at each end.
16Env variable C{PYGEODESY_UPS_POLES} determines the UPS zones I{at} latitude 90°S and 90°N. By default,
17the encoding follows I{Karney}'s and U{Appendix B-3 of DMA TM8358.1<https://Web.Archive.org/web/
1820161226192038/http://earth-info.nga.mil/GandG/publications/tm8358.1/pdf/TM8358_1.pdf>}, using only
19zones C{'B'} respectively C{'Z'} and digraph C{'AN'}. If C{PYGEODESY_UPS_POLES} is set to anything
20other than C{"std"}, zones C{'A'} and C{'Y'} are used for negative, west longitudes I{at} latitude
2190°S respectively 90°N (for backward compatibility).
22'''
24# from pygeodesy.basics import neg as _neg # from .dms
25from pygeodesy.constants import EPS, EPS0, _EPSmin as _Tol90, \
26 isnear90, _0_0, _0_5, _1_0, _2_0
27from pygeodesy.datums import _ellipsoidal_datum, _WGS84
28from pygeodesy.dms import degDMS, _neg, parseDMS2
29from pygeodesy.errors import RangeError, _ValueError
30from pygeodesy.fmath import hypot, hypot1, sqrt0
31# from pygeodesy.internals import _under # from .named
32from pygeodesy.interns import NN, _COMMASPACE_, _inside_, _N_, \
33 _pole_, _range_, _S_, _scale0_, \
34 _SPACE_, _std_, _to_, _UTM_
35from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _getenv
36from pygeodesy.named import nameof, _xnamed, _under
37from pygeodesy.namedTuples import EasNor2Tuple, UtmUps5Tuple, \
38 UtmUps8Tuple, UtmUpsLatLon5Tuple
39from pygeodesy.props import deprecated_method, property_doc_, \
40 Property_RO, _update_all
41# from pygeodesy.streprs import Fmt # from .utmupsBase
42from pygeodesy.units import Float, Float_, Meter, Lat
43from pygeodesy.utily import atan1d, degrees180, sincos2d
44from pygeodesy.utmupsBase import Fmt, _LLEB, _hemi, _parseUTMUPS5, _to4lldn, \
45 _to3zBhp, _to3zll, _UPS_BANDS as _Bands, \
46 _UPS_LAT_MAX, _UPS_LAT_MIN, _UPS_ZONE, \
47 _UPS_ZONE_STR, UtmUpsBase
49from math import atan2, fabs, radians, tan
51__all__ = _ALL_LAZY.ups
52__version__ = '25.05.13'
54_BZ_UPS = _getenv('PYGEODESY_UPS_POLES', _std_) == _std_
55_Falsing = Meter(2000e3) # false easting and northing (C{meter})
56_K0_UPS = Float(_K0_UPS= 0.994) # scale factor at central meridian
57_K1_UPS = Float(_K1_UPS=_1_0) # rescale point scale factor
60def _scale(E, rho, tau):
61 # compute the point scale factor, ala Karney
62 t = hypot1(tau)
63 return Float(scale=(rho / E.a) * t * sqrt0(E.e21 + E.e2 / t**2))
66def _toBand(lat, lon): # see utm._toBand
67 '''(INTERNAL) Get the I{polar} Band letter for a (lat, lon).
68 '''
69 return _Bands[(0 if lat < 0 else 2) + (0 if -180 < lon < 0 else 1)]
72class UPSError(_ValueError):
73 '''Universal Polar Stereographic (UPS) parse or other L{Ups} issue.
74 '''
75 pass
78class Ups(UtmUpsBase):
79 '''Universal Polar Stereographic (UPS) coordinate.
80 '''
81# _band = NN # polar band ('A', 'B', 'Y' or 'Z')
82 _Bands = _Bands # polar Band letters (C{tuple})
83 _Error = UPSError # Error class
84 _pole = NN # UPS projection top/center ('N' or 'S')
85# _scale = None # point scale factor (C{scalar})
86 _scale0 = _K0_UPS # central scale factor (C{scalar})
88 def __init__(self, zone=0, pole=_N_, easting=_Falsing, # PYCHOK expected
89 northing=_Falsing, band=NN, datum=_WGS84,
90 falsed=True, gamma=None, scale=None,
91 name=NN, **convergence):
92 '''New L{Ups} UPS coordinate.
94 @kwarg zone: UPS zone (C{int}, zero) or zone with/-out I{polar} Band
95 letter (C{str}, '00', '00A', '00B', '00Y' or '00Z').
96 @kwarg pole: Top/center of (stereographic) projection (C{str},
97 C{'N[orth]'} or C{'S[outh]'}).
98 @kwarg easting: Easting, see B{C{falsed}} (C{meter}).
99 @kwarg northing: Northing, see B{C{falsed}} (C{meter}).
100 @kwarg band: Optional, I{polar} Band (C{str}, 'A'|'B'|'Y'|'Z').
101 @kwarg datum: Optional, this coordinate's datum (L{Datum},
102 L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}).
103 @kwarg falsed: If C{True}, both B{C{easting}} and B{C{northing}}
104 are falsed (C{bool}).
105 @kwarg gamma: Optional, meridian convergence to save (C{degrees}).
106 @kwarg scale: Optional, computed scale factor k to save
107 (C{scalar}).
108 @kwarg name: Optional name (C{str}).
109 @kwarg convergence: DEPRECATED, use keyword argument C{B{gamma}=None}.
111 @raise TypeError: Invalid B{C{datum}}.
113 @raise UPSError: Invalid B{C{zone}}, B{C{pole}}, B{C{easting}},
114 B{C{northing}}, B{C{band}}, B{C{convergence}}
115 or B{C{scale}}.
116 '''
117 if name:
118 self.name = name
120 try:
121 z, B, p = _to3zBhp(zone, band, hemipole=pole)
122 if z != _UPS_ZONE or (B and (B not in _Bands)):
123 raise ValueError
124 except (TypeError, ValueError) as x:
125 raise UPSError(zone=zone, pole=pole, band=band, cause=x)
126 self._pole = p
127 UtmUpsBase.__init__(self, easting, northing, band=B, datum=datum, falsed=falsed,
128 gamma=gamma, scale=scale, **convergence)
130 def __eq__(self, other):
131 return isinstance(other, Ups) and other.zone == self.zone \
132 and other.pole == self.pole \
133 and other.easting == self.easting \
134 and other.northing == self.northing \
135 and other.band == self.band \
136 and other.datum == self.datum
138 @property_doc_(''' the I{polar} band.''')
139 def band(self):
140 '''Get the I{polar} band (C{'A'|'B'|'Y'|'Z'}).
141 '''
142 if not self._band:
143 self._toLLEB()
144 return self._band
146 @band.setter # PYCHOK setter!
147 def band(self, band):
148 '''Set or reset the I{polar} band letter (C{'A'|'B'|'Y'|'Z'})
149 or C{None} or C{""} to reset.
151 @raise TypeError: Invalid B{C{band}}.
153 @raise ValueError: Invalid B{C{band}}.
154 '''
155 self._band1(band)
157 @Property_RO
158 def falsed2(self):
159 '''Get the easting and northing falsing (L{EasNor2Tuple}C{(easting, northing)}).
160 '''
161 f = _Falsing if self.falsed else 0
162 return EasNor2Tuple(f, f)
164 def parse(self, strUPS, name=NN):
165 '''Parse a string to a similar L{Ups} instance.
167 @arg strUPS: The UPS coordinate (C{str}),
168 see function L{parseUPS5}.
169 @kwarg name: Optional instance name (C{str}),
170 overriding this name.
172 @return: The similar instance (L{Ups}).
174 @raise UTMError: Invalid B{C{strUPS}}.
176 @see: Functions L{parseUTM5} and L{pygeodesy.parseUTMUPS5}.
177 '''
178 return parseUPS5(strUPS, datum=self.datum, Ups=self.classof,
179 name=name or self.name)
181 @deprecated_method
182 def parseUPS(self, strUPS): # PYCHOK no cover
183 '''DEPRECATED, use method L{parse}.'''
184 return self.parse(strUPS)
186 @Property_RO
187 def pole(self):
188 '''Get the top/center of (stereographic) projection (C{'N'|'S'} or C{""}).
189 '''
190 return self._pole
192 def rescale0(self, lat, scale0=_K0_UPS):
193 '''Set the central scale factor for this UPS projection.
195 @arg lat: Northern latitude (C{degrees}).
196 @arg scale0: UPS k0 scale at B{C{lat}} latitude (C{scalar}).
198 @raise RangeError: If B{C{lat}} outside the valid range and
199 L{pygeodesy.rangerrors} set to C{True}.
201 @raise UPSError: Invalid B{C{scale}}.
202 '''
203 s0 = Float_(scale0=scale0, Error=UPSError, low=EPS) # <= 1.003 or 1.0016?
204 u = toUps8(fabs(Lat(lat)), _0_0, datum=self.datum, Ups=_Ups_K1)
205 k = s0 / u.scale
206 if self.scale0 != k:
207 _update_all(self)
208 self._band = NN # force re-compute
209 self._latlon = self._utm = None
210 self._scale0 = Float(scale0=k)
212 def toLatLon(self, LatLon=None, unfalse=True, **LatLon_kwds):
213 '''Convert this UPS coordinate to an (ellipsoidal) geodetic point.
215 @kwarg LatLon: Optional, ellipsoidal class to return the
216 geodetic point (C{LatLon}) or C{None}.
217 @kwarg unfalse: Unfalse B{C{easting}} and B{C{northing}}
218 if falsed (C{bool}).
219 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword
220 arguments, ignored if C{B{LatLon} is None}.
222 @return: This UPS coordinate (B{C{LatLon}}) or if B{C{LatLon}}
223 is C{None}, a L{LatLonDatum5Tuple}C{(lat, lon, datum,
224 gamma, scale)}.
226 @raise TypeError: If B{C{LatLon}} is not ellipsoidal.
228 @raise UPSError: Invalid meridional radius or H-value.
229 '''
230 if self._latlon and self._latlon._toLLEB_args == (unfalse,):
231 return self._latlon5(LatLon)
232 else:
233 self._toLLEB(unfalse=unfalse)
234 return self._latlon5(LatLon, **LatLon_kwds)
236 def _toLLEB(self, unfalse=True): # PYCHOK signature
237 '''(INTERNAL) Compute (ellipsoidal) lat- and longitude.
238 '''
239 E = self.datum.ellipsoid # XXX vs LatLon.datum.ellipsoid
241 x, y = self.eastingnorthing2(falsed=not unfalse)
243 r = hypot(x, y)
244 t = (r * E.es_c / (self.scale0 * E.a * _2_0)) if r > 0 else EPS0
245 t = E.es_tauf((_1_0 / t - t) * _0_5)
246 a = atan1d(t)
247 if self._pole == _N_:
248 b, g = atan2(x, -y), 1
249 else:
250 b, g, a = atan2(x, y), -1, _neg(a)
251 ll = _LLEB(a, degrees180(b), datum=self._datum, name=self.name)
253 ll._gamma = b * g
254 ll._scale = _scale(E, r, t) if r > 0 else self.scale0
255 self._latlon5args(ll, _toBand, unfalse)
257 def toRepr(self, prec=0, fmt=Fmt.SQUARE, sep=_COMMASPACE_, B=False, cs=False, **unused): # PYCHOK expected
258 '''Return a string representation of this UPS coordinate.
260 Note that UPS coordinates are rounded, not truncated (unlike
261 MGRS grid references).
263 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).
264 @kwarg fmt: Enclosing backets format (C{str}).
265 @kwarg sep: Optional separator between name:value pairs (C{str}).
266 @kwarg B: Optionally, include polar band letter (C{bool}).
267 @kwarg cs: Optionally, include gamma meridian convergence and
268 point scale factor (C{bool} or non-zero C{int} to
269 specify the precison like B{C{prec}}).
271 @return: This UPS as a string with C{00[Band] pole, easting,
272 northing, [convergence, scale]} as C{"[Z:00[Band],
273 P:N|S, E:meter, N:meter]"} plus C{", C:DMS, S:float"}
274 if B{C{cs}} is C{True}, where C{[Band]} is present and
275 C{'A'|'B'|'Y'|'Z'} only if B{C{B}} is C{True} and
276 convergence C{DMS} is in I{either} degrees, minutes
277 I{or} seconds (C{str}).
279 @note: Pseudo zone zero (C{"00"}) for UPS follows I{Karney}'s U{zone UPS
280 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1UTMUPS.html>}.
281 '''
282 return self._toRepr(fmt, B, cs, prec, sep)
284 def toStr(self, prec=0, sep=_SPACE_, B=False, cs=False): # PYCHOK expected
285 '''Return a string representation of this UPS coordinate.
287 Note that UPS coordinates are rounded, not truncated (unlike
288 MGRS grid references).
290 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).
291 @kwarg sep: Optional separator to join (C{str}) or C{None}
292 to return an unjoined C{tuple} of C{str}s.
293 @kwarg B: Optionally, include and polar band letter (C{bool}).
294 @kwarg cs: Optionally, include gamma meridian convergence and
295 point scale factor (C{bool} or non-zero C{int} to
296 specify the precison like B{C{prec}}).
298 @return: This UPS as a string with C{00[Band] pole, easting,
299 northing, [convergence, scale]} as C{"00[B] N|S
300 meter meter"} plus C{" DMS float"} if B{C{cs}} is C{True},
301 where C{[Band]} is present and C{'A'|'B'|'Y'|'Z'} only
302 if B{C{B}} is C{True} and convergence C{DMS} is in
303 I{either} degrees, minutes I{or} seconds (C{str}).
305 @note: Zone zero (C{"00"}) for UPS follows I{Karney}'s U{zone UPS
306 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1UTMUPS.html>}.
307 '''
308 return self._toStr(self.pole, B, cs, prec, sep) # PYCHOK pole
310 def toUps(self, pole=NN, **unused):
311 '''Duplicate this UPS coordinate.
313 @kwarg pole: Optional top/center of the UPS projection,
314 (C{str}, 'N[orth]'|'S[outh]').
316 @return: A copy of this UPS coordinate (L{Ups}).
318 @raise UPSError: Invalid B{C{pole}} or attempt to transfer
319 the projection top/center.
320 '''
321 if self.pole == pole or not pole:
322 return self.copy()
323 t = _SPACE_(_pole_, repr(self.pole), _to_, repr(pole))
324 raise UPSError('no transfer', txt=t)
326 def toUtm(self, zone, falsed=True, **unused):
327 '''Convert this UPS coordinate to a UTM coordinate.
329 @arg zone: The UTM zone (C{int}).
330 @kwarg falsed: False both easting and northing (C{bool}).
332 @return: The UTM coordinate (L{Utm}).
333 '''
334 u = self._utm
335 if u is None or u.zone != zone or falsed != bool(u.falsed):
336 ll = self.toLatLon(LatLon=None, unfalse=True)
337 utm = _MODS.utm
338 self._utm = u = utm.toUtm8(ll, Utm=utm.Utm, falsed=falsed,
339 name=self.name, zone=zone)
340 return u
342 @Property_RO
343 def zone(self):
344 '''Get the polar pseudo zone (C{0}), like I{Karney}'s U{zone UPS<https://
345 GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1UTMUPS.html>}.
346 '''
347 return _UPS_ZONE
350class _Ups_K1(Ups):
351 '''(INTERNAL) For method L{Ups.rescale0}.
352 '''
353 _scale0 = _K1_UPS
356def parseUPS5(strUPS, datum=_WGS84, Ups=Ups, falsed=True, name=NN):
357 '''Parse a string representing a UPS coordinate, consisting of
358 C{"[zone][band] pole easting northing"} where B{C{zone}} is
359 pseudo zone C{"00"|"0"|""} and C{band} is C{'A'|'B'|'Y'|'Z'|''}.
361 @arg strUPS: A UPS coordinate (C{str}).
362 @kwarg datum: Optional datum to use (L{Datum}).
363 @kwarg Ups: Optional class to return the UPS coordinate (L{Ups})
364 or C{None}.
365 @kwarg falsed: Both B{C{easting}} and B{C{northing}} are falsed (C{bool}).
366 @kwarg name: Optional B{C{Ups}} name (C{str}).
368 @return: The UPS coordinate (B{C{Ups}}) or a
369 L{UtmUps5Tuple}C{(zone, hemipole, easting, northing,
370 band)} if B{C{Ups}} is C{None}. The C{hemipole} is
371 the C{'N'|'S'} pole, the UPS projection top/center.
373 @raise UPSError: Invalid B{C{strUPS}}.
374 '''
375 z, p, e, n, B = _parseUTMUPS5(strUPS, _UPS_ZONE_STR, Error=UPSError)
376 if z != _UPS_ZONE or (B and B not in _Bands):
377 raise UPSError(strUPS=strUPS, zone=z, band=B)
379 r = UtmUps5Tuple(z, p, e, n, B, Error=UPSError) if Ups is None \
380 else Ups(z, p, e, n, band=B, falsed=falsed, datum=datum)
381 return _xnamed(r, name)
384def toUps8(latlon, lon=None, datum=None, Ups=Ups, pole=NN,
385 falsed=True, strict=True, name=NN):
386 '''Convert a lat-/longitude point to a UPS coordinate.
388 @arg latlon: Latitude (C{degrees}) or an (ellipsoidal)
389 geodetic C{LatLon} point.
390 @kwarg lon: Optional longitude (C{degrees}) or C{None} if
391 B{C{latlon}} is a C{LatLon}.
392 @kwarg datum: Optional datum for this UPS coordinate,
393 overriding B{C{latlon}}'s datum (C{Datum},
394 L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}).
395 @kwarg Ups: Optional class to return the UPS coordinate
396 (L{Ups}) or C{None}.
397 @kwarg pole: Optional top/center of (stereographic) projection
398 (C{str}, C{'N[orth]'} or C{'S[outh]'}).
399 @kwarg falsed: False both easting and northing (C{bool}).
400 @kwarg strict: Restrict B{C{lat}} to UPS ranges (C{bool}).
401 @kwarg name: Optional B{C{Ups}} name (C{str}).
403 @return: The UPS coordinate (B{C{Ups}}) or a
404 L{UtmUps8Tuple}C{(zone, hemipole, easting, northing,
405 band, datum, gamma, scale)} if B{C{Ups}} is C{None}.
406 The C{hemipole} is the C{'N'|'S'} pole, the UPS
407 projection top/center.
409 @raise RangeError: If B{C{strict}} and B{C{lat}} outside the valid
410 UPS bands or if B{C{lat}} or B{C{lon}} outside
411 the valid range and L{pygeodesy.rangerrors} set
412 to C{True}.
414 @raise TypeError: If B{C{latlon}} is not ellipsoidal or
415 B{C{datum}} invalid.
417 @raise ValueError: If B{C{lon}} value is missing or if B{C{latlon}}
418 is invalid.
420 @see: I{Karney}'s C++ class U{UPS
421 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1UPS.html>}.
422 '''
423 lat, lon, d, name = _to4lldn(latlon, lon, datum, name)
424 z, B, p, lat, lon = upsZoneBand5(lat, lon, strict=strict) # PYCHOK UtmUpsLatLon5Tuple
426 d = _ellipsoidal_datum(d, name=name)
427 E = d.ellipsoid
429 p = str(pole or p)[:1].upper()
430 S = p == _S_ # at south[pole]
432 a = -lat if S else lat
433 P = isnear90(a, eps90=_Tol90) # at pole
435 t = tan(radians(a))
436 T = E.es_taupf(t)
437 r = (hypot1(T) - T) if T < 0 else (_0_0 if P else _1_0 /
438 (hypot1(T) + T))
440 k0 = getattr(Ups, _under(_scale0_), _K0_UPS) # Ups is class or None
441 r *= k0 * E.a * _2_0 / E.es_c
443 k = k0 if P else _scale(E, r, t)
444 g = lon # [-180, 180) from .upsZoneBand5
445 x, y = sincos2d(g)
446 x *= r
447 y *= r
448 if S:
449 g = _neg(g)
450 else:
451 y = _neg(y)
453 if falsed:
454 x += _Falsing
455 y += _Falsing
457 n = name or nameof(latlon)
458 if Ups is None:
459 r = UtmUps8Tuple(z, p, x, y, B, d, g, k, Error=UPSError, name=n)
460 else:
461 if z != _UPS_ZONE and not strict:
462 z = _UPS_ZONE # ignore UTM zone
463 r = Ups(z, p, x, y, band=B, datum=d, falsed=falsed,
464 gamma=g, scale=k, name=n)
465 if isinstance(latlon, _LLEB) and d is latlon.datum: # see utm._toXtm8
466 r._latlon5args(latlon, _toBand, falsed) # XXX weakref(latlon)?
467 latlon._gamma = g
468 latlon._scale = k
469 else:
470 r._hemisphere = _hemi(lat)
471 if not r._band:
472 r._band = _toBand(lat, lon)
473 return r
476def upsZoneBand5(lat, lon, strict=True, name=NN):
477 '''Return the UTM/UPS zone number, I{polar} Band letter, pole and
478 clipped lat- and longitude for a given location.
480 @arg lat: Latitude in degrees (C{scalar} or C{str}).
481 @arg lon: Longitude in degrees (C{scalar} or C{str}).
482 @kwarg strict: Restrict B{C{lat}} to UPS ranges (C{bool}).
483 @kwarg name: Optional name (C{str}).
485 @return: A L{UtmUpsLatLon5Tuple}C{(zone, band, hemipole,
486 lat, lon)} where C{hemipole} is the C{'N'|'S'} pole,
487 the UPS projection top/center and C{lon} [-180..180).
489 @note: The C{lon} is set to C{0} if B{C{lat}} is C{-90} or
490 C{90}, see env variable C{PYGEODESY_UPS_POLES} in
491 module L{pygeodesy.ups}.
493 @raise RangeError: If B{C{strict}} and B{C{lat}} in the UTM
494 and not the UPS range or if B{C{lat}} or
495 B{C{lon}} outside the valid range and
496 L{pygeodesy.rangerrors} set to C{True}.
498 @raise ValueError: Invalid B{C{lat}} or B{C{lon}}.
499 '''
500 z, lat, lon = _to3zll(*parseDMS2(lat, lon))
501 if _BZ_UPS and lon < 0 and isnear90(fabs(lat), eps90=_Tol90): # DMA TM8358.1 only ...
502 lon = 0 # ... zones B and Z at 90°S and 90°N, see also GeoConvert
504 if lat < _UPS_LAT_MIN: # includes 30' overlap
505 z, B, p = _UPS_ZONE, _toBand(lat, lon), _S_
507 elif lat > _UPS_LAT_MAX: # includes 30' overlap
508 z, B, p = _UPS_ZONE, _toBand(lat, lon), _N_
510 elif strict:
511 r = _range_(_UPS_LAT_MIN, _UPS_LAT_MAX, prec=1)
512 t = _SPACE_(_inside_, _UTM_, _range_, r)
513 raise RangeError(lat=degDMS(lat), txt=t)
515 else:
516 B, p = NN, _hemi(lat)
517 return UtmUpsLatLon5Tuple(z, B, p, lat, lon, Error=UPSError, name=name)
519# **) MIT License
520#
521# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
522#
523# Permission is hereby granted, free of charge, to any person obtaining a
524# copy of this software and associated documentation files (the "Software"),
525# to deal in the Software without restriction, including without limitation
526# the rights to use, copy, modify, merge, publish, distribute, sublicense,
527# and/or sell copies of the Software, and to permit persons to whom the
528# Software is furnished to do so, subject to the following conditions:
529#
530# The above copyright notice and this permission notice shall be included
531# in all copies or substantial portions of the Software.
532#
533# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
534# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
535# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
536# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
537# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
538# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
539# OTHER DEALINGS IN THE SOFTWARE.