Coverage for pygeodesy/utmupsBase.py: 98%
268 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-25 13:15 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-25 13:15 -0400
2# -*- coding: utf-8 -*-
4u'''(INTERNAL) Private class C{UtmUpsBase}, functions and constants
5for modules L{epsg}, L{etm}, L{mgrs}, L{ups} and L{utm}.
6'''
7# make sure int/int division yields float quotient, see .basics
8from __future__ import division as _; del _ # PYCHOK semicolon
10from pygeodesy.basics import _isin, isint, isscalar, isstr, neg_, \
11 _xinstanceof, _xsubclassof
12from pygeodesy.constants import _float, _0_0, _0_5, _N_90_0, _180_0
13from pygeodesy.datums import _ellipsoidal_datum, _WGS84
14from pygeodesy.dms import degDMS, parseDMS2
15from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase as _LLEB
16from pygeodesy.errors import _or, ParseError, _parseX, _ValueError, \
17 _xattrs, _xkwds, _xkwds_not
18# from pygeodesy.fsums import Fsum # _MODS
19# from pygeodesy.internals import _name__, _under # from .named
20from pygeodesy.interns import NN, _A_, _B_, _COMMA_, _Error_, _gamma_, \
21 _n_a_, _not_, _N_, _NS_, _PLUS_, _scale_, \
22 _S_, _SPACE_, _Y_, _Z_
23from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
24from pygeodesy.named import _name__, _NamedBase, _under
25from pygeodesy.namedTuples import EasNor2Tuple, LatLonDatum5Tuple
26from pygeodesy.props import deprecated_method, property_doc_, _update_all, \
27 deprecated_property_RO, Property_RO, property_RO
28from pygeodesy.streprs import Fmt, fstr, _fstrENH2, _xzipairs
29from pygeodesy.units import Band, Easting, Lat, Northing, Phi, Scalar, Zone
30from pygeodesy.utily import atan1, _Wrap, wrap360
32from math import cos, degrees, fabs, sin, tan
34__all__ = _ALL_LAZY.utmupsBase
35__version__ = '25.04.25'
37_UPS_BANDS = _A_, _B_, _Y_, _Z_ # UPS polar bands SE, SW, NE, NW
38# _UTM_BANDS = _MODS.utm._Bands
40_UTM_LAT_MAX = _float( 84) # PYCHOK for export (C{degrees})
41_UTM_LAT_MIN = _float(-80) # PYCHOK for export (C{degrees})
43_UPS_LAT_MAX = _UTM_LAT_MAX - _0_5 # PYCHOK includes 30' UTM overlap
44_UPS_LAT_MIN = _UTM_LAT_MIN + _0_5 # PYCHOK includes 30' UTM overlap
46_UPS_LATS = {_A_: _N_90_0, _Y_: _UTM_LAT_MAX, # UPS band bottom latitudes,
47 _B_: _N_90_0, _Z_: _UTM_LAT_MAX} # PYCHOK see .Mgrs.bandLatitude
49_UTM_ZONE_MAX = 60 # PYCHOK for export
50_UTM_ZONE_MIN = 1 # PYCHOK for export
51_UTM_ZONE_OFF_MAX = 60 # PYCHOK max Central meridian offset (C{degrees})
53_UPS_ZONE = _UTM_ZONE_MIN - 1 # PYCHOK for export
54_UPS_ZONE_STR = Fmt.zone(_UPS_ZONE) # PYCHOK for export
56_UTMUPS_ZONE_INVALID = -4 # PYCHOK for export too
57_UTMUPS_ZONE_MAX = _UTM_ZONE_MAX # PYCHOK for export too, by .units.py
58_UTMUPS_ZONE_MIN = _UPS_ZONE # PYCHOK for export too, by .units.py
60# _MAX_PSEUDO_ZONE = -1
61# _MIN_PSEUDO_ZONE = -4
62# _UTMUPS_ZONE_MATCH = -3
63# _UTMUPS_ZONE_STANDARD = -1
64# _UTM = -2
67class UtmUpsBase(_NamedBase):
68 '''(INTERNAL) Base class for L{Utm} and L{Ups} coordinates.
69 '''
70 _band = NN # latitude band letter ('A..Z')
71 _Bands = NN # valid Band letters, see L{Utm} and L{Ups}
72 _datum = _WGS84 # L{Datum}
73 _easting = _0_0 # Easting, see B{C{falsed}} (C{meter})
74 _Error = None # I{Must be overloaded}, see function C{notOverloaded}
75 _falsed = True # falsed easting and northing (C{bool})
76 _gamma = None # meridian conversion (C{degrees})
77 _hemisphere = NN # hemisphere ('N' or 'S'), different from UPS pole
78 _latlon = None # cached toLatLon (C{LatLon} or C{._toLLEB})
79 _northing = _0_0 # Northing, see B{C{falsed}} (C{meter})
80 _scale = None # grid or point scale factor (C{scalar}) or C{None}
81# _scale0 = _K0 # central scale factor (C{scalar})
82 _ups = None # cached toUps (L{Ups})
83 _utm = None # cached toUtm (L{Utm})
85 def __init__(self, easting, northing, band=NN, datum=None, falsed=True,
86 gamma=None, scale=None):
87 '''(INTERNAL) New L{UtmUpsBase}.
88 '''
89 E = self._Error
90 if not E: # PYCHOK no cover
91 self._notOverloaded(callername=_under(_Error_))
93 self._easting = Easting(easting, Error=E)
94 self._northing = Northing(northing, Error=E)
96 if band:
97 self._band1(band)
99 if not _isin(datum, None, self._datum):
100 self._datum = _ellipsoidal_datum(datum) # raiser=_datum_, name=band
102 if not falsed:
103 self._falsed = False
105 if gamma is not self._gamma:
106 self._gamma = Scalar(gamma=gamma, Error=E)
107 if scale is not self._scale:
108 self._scale = Scalar(scale=scale, Error=E)
110 def __repr__(self):
111 return self.toRepr(B=True)
113 def __str__(self):
114 return self.toStr()
116 def _band1(self, band):
117 '''(INTERNAL) Re/set the latitudinal or polar band.
118 '''
119 if band:
120 _xinstanceof(str, band=band)
121# if not self._Bands: # PYCHOK no cover
122# self._notOverloaded(callername=_under('Bands'))
123 if band not in self._Bands:
124 t = _or(*sorted(set(map(repr, self._Bands))))
125 raise self._Error(band=band, txt_not_=t)
126 self._band = band
127 elif self._band: # reset
128 self._band = NN
130 @deprecated_property_RO
131 def convergence(self):
132 '''DEPRECATED, use property C{gamma}.'''
133 return self.gamma
135 @property_doc_(''' the (ellipsoidal) datum of this coordinate.''')
136 def datum(self):
137 '''Get the datum (L{Datum}).
138 '''
139 return self._datum
141 @datum.setter # PYCHOK setter!
142 def datum(self, datum):
143 '''Set the (ellipsoidal) datum L{Datum}, L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}).
144 '''
145 d = _ellipsoidal_datum(datum)
146 if self._datum != d:
147 _update_all(self)
148 self._datum = d
150 @Property_RO
151 def easting(self):
152 '''Get the easting (C{meter}).
153 '''
154 return self._easting
156 @Property_RO
157 def eastingnorthing(self):
158 '''Get easting and northing (L{EasNor2Tuple}C{(easting, northing)}).
159 '''
160 return EasNor2Tuple(self.easting, self.northing)
162 def eastingnorthing2(self, falsed=True):
163 '''Return easting and northing, falsed or unfalsed.
165 @kwarg falsed: If C{True}, return easting and northing falsed,
166 otherwise unfalsed (C{bool}).
168 @return: An L{EasNor2Tuple}C{(easting, northing)} in C{meter}.
169 '''
170 e, n = self.falsed2
171 if self.falsed and not falsed:
172 e, n = neg_(e, n)
173 elif falsed and not self.falsed:
174 pass
175 else:
176 e = n = _0_0
177 return EasNor2Tuple(Easting( e + self.easting, Error=self._Error),
178 Northing(n + self.northing, Error=self._Error))
180 @Property_RO
181 def _epsg(self):
182 '''(INTERNAL) Cache for method L{toEpsg}.
183 '''
184 return _MODS.epsg.Epsg(self)
186 @Property_RO
187 def falsed(self):
188 '''Are easting and northing falsed (C{bool})?
189 '''
190 return self._falsed
192 @Property_RO
193 def falsed2(self): # PYCHOK no cover
194 '''I{Must be overloaded}.'''
195 self._notOverloaded(self)
197 def _footpoint(self, y, lat0, makris):
198 '''(INTERNAL) Compute to foot-point latitude in C{radians}.
199 '''
200 if y is None:
201 _, y = self.eastingnorthing2(falsed=False)
202 S = _MODS.fsums.Fsum
203 E = self.datum.ellipsoid
204 P = S(E.Llat(lat0), y)
205 if E.isSpherical:
206 p = P.fover(E.a)
207 elif makris:
208 r = P.fover(E.b)
209 p = fabs(r)
210 if p:
211 e2 = E.e22 # E.e22abs?
212 e4 = E.e4
214 e1 = S(e2 / 4, -11 / 64 * e4, -1).as_iscalar
215 e2 = S(e2 / 8, -13 / 128 * e4) .as_iscalar
216 e4 *= cos(p)**2 / 8
218 s = sin(p * 2)
219 p = -p
220 U = S(e1 * p, e2 * s, e4 * p, (5 / 8 * e4) * s**2)
221 p = atan1(E.a * tan(float(U)) / E.b)
222 if r < 0: # copysign(p, y)
223 p = -p
224 else: # PyGeodetics
225 f = E.f
226 f2 = f**2
227 f3 = f**3
228 B0 = S(1, -f / 2, f2 / 16, f3 / 32) * E.a
229 r = P.fover(B0)
230 P = S(r, S(3 / 4 * f, 3 / 8 * f2, 21 / 256 * f3) * sin(r * 2),
231 S( 21 / 64 * f2, 21 / 64 * f3) * sin(r * 4),
232 151 / 768 * f3 * sin(r * 6))
233 p = float(P)
234 return p
236 @Property_RO
237 def gamma(self):
238 '''Get the meridian convergence (C{degrees}) or C{None}
239 if not available.
240 '''
241 return self._gamma
243 @property_RO
244 def hemisphere(self):
245 '''Get the hemisphere (C{str}, 'N'|'S').
246 '''
247 if not self._hemisphere:
248 self._toLLEB()
249 return self._hemisphere
251 def latFootPoint(self, northing=None, lat0=0, makris=False):
252 '''Compute the foot-point latitude in C{degrees}.
254 @arg northing: Northing (C{meter}, same units this datum's ellipsoid's axes),
255 overriding this I{unfalsed} northing.
256 @kwarg lat0: Geodetic latitude of the meridian's origin (C{degrees}).
257 @kwarg makris: If C{True}, use C{Makris}' formula, otherwise C{PyGeodetics}'.
259 @return: Foot-point latitude (C{degrees}).
261 @see: U{PyGeodetics<https://GitHub.com/paarnes/pygeodetics>}, U{FootpointLatitude
262 <https://GitHub.com/APRIL-ZJU/clins/blob/master/include/utils/gps_convert_utils.h#L143>},
263 U{Makris<https://www.TandFonline.com/doi/abs/10.1179/sre.1982.26.205.345>} and
264 U{Geomatics' Mercator, page 60<https://Geomatics.CC/legacy-files/mercator.pdf>}.
265 '''
266 return Lat(FootPoint=degrees(self._footpoint(northing, lat0, makris)))
268 def _latlon5(self, LatLon, **LatLon_kwds):
269 '''(INTERNAL) Get cached C{._toLLEB} as B{C{LatLon}} instance.
270 '''
271 ll = self._latlon
272 if LatLon is None:
273 r = LatLonDatum5Tuple(ll.lat, ll.lon, ll.datum,
274 ll.gamma, ll.scale, name=ll.name)
275 else:
276 _xsubclassof(_LLEB, LatLon=LatLon)
277 r = LatLon(ll.lat, ll.lon, **_xkwds(LatLon_kwds, datum=ll.datum, name=ll.name))
278 r = _xattrs(r, ll, _under(_gamma_), _under(_scale_))
279 return r
281 def _latlon5args(self, ll, g, k, _toBand, unfalse, *other):
282 '''(INTERNAL) See C{._toLLEB} methods, functions C{ups.toUps8} and C{utm._toXtm8}
283 '''
284 ll._gamma = g
285 ll._scale = k
286 ll._toLLEB_args = (unfalse,) + other
287 if unfalse:
288 if not self._band:
289 self._band = _toBand(ll.lat, ll.lon)
290 if not self._hemisphere:
291 self._hemisphere = _hemi(ll.lat)
292 self._latlon = ll
294 @Property_RO
295 def _lowerleft(self): # by .ellipsoidalBase._lowerleft
296 '''Get this UTM or UPS C{un}-centered (L{Utm} or L{Ups}) to its C{lowerleft}.
297 '''
298 return _lowerleft(self, 0)
300 @Property_RO
301 def _mgrs(self):
302 '''(INTERNAL) Cache for method L{toMgrs}.
303 '''
304 return _toMgrs(self)
306 @Property_RO
307 def _mgrs_lowerleft(self):
308 '''(INTERNAL) Cache for method L{toMgrs}, I{un}-centered.
309 '''
310 utmups = self._lowerleft
311 return self._mgrs if utmups is self else _toMgrs(utmups)
313 @Property_RO
314 def northing(self):
315 '''Get the northing (C{meter}).
316 '''
317 return self._northing
319 def phiFootPoint(self, northing=None, lat0=0, makris=False):
320 '''Compute the foot-point latitude in C{radians}.
322 @return: Foot-point latitude (C{radians}).
324 @see: Method L{latFootPoint<UtmUpsBase.latFootPoint>} for further details.
325 '''
326 return Phi(FootPoint=self._footpoint(northing, lat0, makris))
328 @Property_RO
329 def scale(self):
330 '''Get the grid scale (C{float}) or C{None}.
331 '''
332 return self._scale
334 @Property_RO
335 def scale0(self):
336 '''Get the central scale factor (C{float}).
337 '''
338 return self._scale0
340 @deprecated_method
341 def to2en(self, falsed=True): # PYCHOK no cover
342 '''DEPRECATED, use method C{eastingnorthing2}.
344 @return: An L{EasNor2Tuple}C{(easting, northing)}.
345 '''
346 return self.eastingnorthing2(falsed=falsed)
348 def toEpsg(self):
349 '''Determine the B{EPSG (European Petroleum Survey Group)} code.
351 @return: C{EPSG} code (C{int}).
353 @raise EPSGError: See L{Epsg}.
354 '''
355 return self._epsg
357 def _toLLEB(self, **kwds): # PYCHOK no cover
358 '''(INTERNAL) I{Must be overloaded}.'''
359 self._notOverloaded(**kwds)
361 def toMgrs(self, center=False):
362 '''Convert this UTM/UPS coordinate to an MGRS grid reference.
364 @kwarg center: If C{True}, I{un}-center this UTM or UPS to
365 its C{lowerleft} (C{bool}) or by C{B{center}
366 meter} (C{scalar}).
368 @return: The MGRS grid reference (L{Mgrs}).
370 @see: Function L{pygeodesy.toMgrs} in module L{mgrs} for more details.
372 @note: If not specified, the I{latitudinal} C{band} is computed from
373 the (geodetic) latitude and the C{datum}.
374 '''
375 return self._mgrs_lowerleft if center is True else (
376 _toMgrs(_lowerleft(self, center)) if center else
377 self._mgrs) # PYCHOK indent
379 def _toRepr(self, fmt, B, cs, prec, sep): # PYCHOK expected
380 '''(INTERNAL) Return a representation for this ETM/UTM/UPS coordinate.
381 '''
382 t = self.toStr(prec=prec, sep=None, B=B, cs=cs) # hemipole
383 T = 'ZHENCS'[:len(t)]
384 return _xzipairs(T, t, sep=sep, fmt=fmt)
386 def _toStr(self, hemipole, B, cs, prec, sep):
387 '''(INTERNAL) Return a string for this ETM/UTM/UPS coordinate.
388 '''
389 z = NN(Fmt.zone(self.zone), (self.band if B else NN)) # PYCHOK band
390 t = (z, hemipole) + _fstrENH2(self, prec, None)[0]
391 if cs:
392 prec = cs if isint(cs) else 8 # for backward compatibility
393 t += (_n_a_ if self.gamma is None else
394 degDMS(self.gamma, prec=prec, pos=_PLUS_),
395 _n_a_ if self.scale is None else
396 fstr(self.scale, prec=prec))
397 return t if sep is None else sep.join(t)
400def _hemi(lat, N=0): # in .ups, .utm
401 '''Return the hemisphere letter.
403 @arg lat: Latitude (C{degrees} or C{radians}).
404 @kwarg N: Minimal North latitude, C{0} or C{_N_}.
406 @return: C{'N'|'S'} for north-/southern hemisphere.
407 '''
408 return _S_ if lat < N else _N_
411def _lowerleft(utmups, center): # in .ellipsoidalBase._lowerleft
412 '''(INTERNAL) I{Un}-center a B{C{utmups}} to its C{lowerleft} by
413 C{B{center} meter} or by a I{guess} if B{C{center}} is C{0}.
414 '''
415 if center:
416 e = n = -center
417 else:
418 for c in (50, 500, 5000):
419 t = c * 2
420 e = int(utmups.easting % t)
421 n = int(utmups.northing % t)
422 if (e == c and _isin(n, c, c - 1)) or \
423 (n == c and _isin(e, c, c - 1)):
424 break
425 else:
426 return utmups # unchanged
428 r = _xkwds_not(None, datum=utmups.datum,
429 gamma=utmups.gamma,
430 scale=utmups.scale, name=utmups.name)
431 return utmups.classof(utmups.zone, utmups.hemisphere,
432 utmups.easting - e, utmups.northing - n,
433 band=utmups.band, falsed=utmups.falsed, **r)
436def _parseUTMUPS5(strUTMUPS, UPS, Error=ParseError, band=NN, sep=_COMMA_):
437 '''(INTERNAL) Parse a string representing a UTM or UPS coordinate
438 consisting of C{"zone[band] hemisphere/pole easting northing"}.
440 @arg strUTMUPS: A UTM or UPS coordinate (C{str}).
441 @kwarg band: Optional, default Band letter (C{str}).
442 @kwarg sep: Optional, separator to split (",").
444 @return: 5-Tuple (C{zone, hemisphere/pole, easting, northing,
445 band}).
447 @raise ParseError: Invalid B{C{strUTMUPS}}.
448 '''
449 def _UTMUPS5(strUTMUPS, UPS, band, sep):
450 u = strUTMUPS.lstrip()
451 if UPS and not u.startswith(_UPS_ZONE_STR):
452 raise ValueError(_not_(_UPS_ZONE_STR))
454 u = u.replace(sep, _SPACE_).strip().split()
455 if len(u) < 4:
456 raise ValueError(_not_(sep))
458 z, h = u[:2]
459 if h[:1].upper() not in _NS_:
460 raise ValueError(_SPACE_(h, _not_(_NS_)))
462 if z.isdigit():
463 z, B = int(z), band
464 else:
465 for i in range(len(z)):
466 if not z[i].isdigit():
467 # int('') raises ValueError
468 z, B = int(z[:i]), z[i:]
469 break
470 else:
471 raise ValueError(z)
473 e, n = map(float, u[2:4])
474 return z, h.upper(), e, n, B.upper()
476 return _parseX(_UTMUPS5, strUTMUPS, UPS, band, sep,
477 strUTMUPS=strUTMUPS, Error=Error)
480def _to4lldn(latlon, lon, datum, name, wrap=False):
481 '''(INTERNAL) Return 4-tuple (C{lat, lon, datum, name}).
482 '''
483 try:
484 # if lon is not None:
485 # raise AttributeError
486 lat, lon = float(latlon.lat), float(latlon.lon)
487 _xinstanceof(_LLEB, LatLonDatum5Tuple, latlon=latlon)
488 if wrap:
489 _Wrap.latlon(lat, lon)
490 d = datum or latlon.datum
491 except AttributeError: # TypeError
492 lat, lon = _Wrap.latlonDMS2(latlon, lon) if wrap else \
493 parseDMS2(latlon, lon) # clipped
494 d = datum or _WGS84
495 return lat, lon, d, _name__(name, _or_nameof=latlon)
498def _toMgrs(utmups):
499 '''(INTERNAL) Convert a L{Utm} or L{Ups} to an L{Mgrs} instance.
500 '''
501 return _MODS.mgrs.toMgrs(utmups, datum=utmups.datum, name=utmups.name)
504def _to3zBhp(zone, band, hemipole=NN, Error=_ValueError): # in .epsg, .ups, .utm, .utmups
505 '''Parse UTM/UPS zone, Band letter and hemisphere/pole letter.
507 @arg zone: Zone with/-out Band (C{scalar} or C{str}).
508 @kwarg band: Optional I{longitudinal/polar} Band letter (C{str}).
509 @kwarg hemipole: Optional hemisphere/pole letter (C{str}).
510 @kwarg Error: Optional error to raise, overriding the default
511 C{ValueError}.
513 @return: 3-Tuple (C{zone, Band, hemisphere/pole}) as (C{int, str,
514 'N'|'S'}) where C{zone} is C{0} for UPS or C{1..60} for
515 UTM and C{Band} is C{'A'..'Z'} I{NOT} checked for valid
516 UTM/UPS bands.
518 @raise ValueError: Invalid B{C{zone}}, B{C{band}} or B{C{hemipole}}.
519 '''
520 try:
521 B, z = band, _UTMUPS_ZONE_INVALID
522 if isscalar(zone):
523 z = int(zone)
524 elif zone and isstr(zone):
525 if zone.isdigit():
526 z = int(zone)
527 elif len(zone) > 1:
528 B = zone[-1:]
529 z = int(zone[:-1])
530 elif zone.upper() in _UPS_BANDS: # single letter
531 B = zone
532 z = _UPS_ZONE
534 if _UTMUPS_ZONE_MIN <= z <= _UTMUPS_ZONE_MAX:
535 hp = hemipole[:1].upper()
536 if hp in _NS_ or not hp:
537 z = Zone(z)
538 B = Band(B.upper())
539 if B.isalpha():
540 return z, B, (hp or _hemi(B, _N_))
541 elif not B:
542 return z, B, hp
544 raise ValueError() # _invalid_
545 except (AttributeError, IndexError, TypeError, ValueError) as x:
546 raise Error(zone=zone, band=B, hemipole=hemipole, cause=x)
549def _to3zll(lat, lon): # in .ups, .utm
550 '''Wrap lat- and longitude and determine UTM zone.
552 @arg lat: Latitude (C{degrees}).
553 @arg lon: Longitude (C{degrees}).
555 @return: 3-Tuple (C{zone, lat, lon}) as (C{int}, C{degrees90},
556 C{degrees180}) where C{zone} is C{1..60} for UTM.
557 '''
558 x = wrap360(lon + _180_0) # use wrap360 to get ...
559 z = int(x) // 6 + 1 # ... longitudinal UTM zone [1, 60] and ...
560 return Zone(z), lat, (x - _180_0) # ... -180 <= lon < 180
563__all__ += _ALL_DOCS(UtmUpsBase)
565# **) MIT License
566#
567# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
568#
569# Permission is hereby granted, free of charge, to any person obtaining a
570# copy of this software and associated documentation files (the "Software"),
571# to deal in the Software without restriction, including without limitation
572# the rights to use, copy, modify, merge, publish, distribute, sublicense,
573# and/or sell copies of the Software, and to permit persons to whom the
574# Software is furnished to do so, subject to the following conditions:
575#
576# The above copyright notice and this permission notice shall be included
577# in all copies or substantial portions of the Software.
578#
579# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
580# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
581# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
582# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
583# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
584# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
585# OTHER DEALINGS IN THE SOFTWARE.