Coverage for pygeodesy/points.py: 93%

529 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-01-10 16:55 -0500

1 

2# -*- coding: utf-8 -*- 

3 

4u'''Utilities for point lists, tuples, etc. 

5 

6Functions to handle collections and sequences of C{LatLon} points 

7specified as 2-d U{NumPy<https://www.NumPy.org>}, C{arrays} or tuples 

8as C{LatLon} or as C{pseudo-x/-y} pairs. 

9 

10C{NumPy} arrays are assumed to contain rows of points with a lat-, a 

11longitude -and possibly other- values in different columns. While 

12iterating over the array rows, create an instance of a given C{LatLon} 

13class "on-the-fly" for each row with the row's lat- and longitude. 

14 

15The original C{NumPy} array is read-accessed only and never duplicated, 

16except to return a I{subset} of the original array. 

17 

18For example, to process a C{NumPy} array, wrap the array by instantiating 

19class L{Numpy2LatLon} and specifying the column index for the lat- and 

20longitude in each row. Then, pass the L{Numpy2LatLon} instance to any 

21L{pygeodesy} function or method accepting a I{points} argument. 

22 

23Similarly, class L{Tuple2LatLon} is used to instantiate a C{LatLon} from 

24each 2+tuple in a sequence of such 2+tuples using the C{ilat} lat- and 

25C{ilon} longitude index in each 2+tuple. 

26''' 

27 

28from pygeodesy.basics import isclass, isint, isscalar, issequence, \ 

29 _xdup, issubclassof, _Sequence, _xcopy, \ 

30 _xinstanceof 

31from pygeodesy.constants import EPS, EPS1, PI_2, R_M, isnear0, isnear1, \ 

32 _umod_360, _0_0, _0_5, _1_0, _2_0, _6_0, \ 

33 _90_0, _N_90_0, _180_0, _360_0 

34# from pygeodesy.datums import _spherical_datum # from .formy 

35from pygeodesy.dms import F_D, parseDMS 

36from pygeodesy.errors import CrossError, crosserrors, _IndexError, \ 

37 _IsnotError, _TypeError, _ValueError, \ 

38 _xattr, _xkwds, _xkwds_item2, _xkwds_pop2 

39from pygeodesy.fmath import favg, fdot, hypot, Fsum, fsum 

40# from pygeodesy.fsums import Fsum, fsum # from .fmath 

41from pygeodesy.formy import _bearingTo2, equirectangular4, _spherical_datum 

42from pygeodesy.interns import NN, _colinear_, _COMMASPACE_, _composite_, \ 

43 _DEQUALSPACED_, _ELLIPSIS_, _EW_, _immutable_, \ 

44 _near_, _no_, _NS_, _point_, _SPACE_, _UNDER_, \ 

45 _valid_ # _lat_, _lon_ 

46from pygeodesy.iters import LatLon2PsxyIter, PointsIter, points2 

47from pygeodesy.latlonBase import LatLonBase, _latlonheight3, \ 

48 _ALL_DOCS, _ALL_LAZY, _MODS 

49# from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS 

50from pygeodesy.named import classname, _NamedTuple, nameof, \ 

51 notImplemented, notOverloaded 

52from pygeodesy.namedTuples import Bounds2Tuple, Bounds4Tuple, LatLon2Tuple, \ 

53 NearestOn3Tuple, NearestOn5Tuple, \ 

54 Point3Tuple, Vector3Tuple, \ 

55 PhiLam2Tuple # PYCHOK shared 

56from pygeodesy.props import Property_RO, property_doc_, property_RO 

57from pygeodesy.streprs import Fmt, instr 

58from pygeodesy.units import Number_, Radius, Scalar, Scalar_ 

59from pygeodesy.utily import atan2b, degrees90, degrees180, degrees2m, \ 

60 unroll180, _unrollon, unrollPI, _Wrap, wrap180 

61 

62from math import cos, fabs, fmod as _fmod, radians, sin 

63 

64__all__ = _ALL_LAZY.points 

65__version__ = '24.10.24' 

66 

67_ilat_ = 'ilat' 

68_ilon_ = 'ilon' 

69_ncols_ = 'ncols' 

70_nrows_ = 'nrows' 

71 

72 

73class LatLon_(LatLonBase): # XXX in heights._HeightBase.height 

74 '''Low-overhead C{LatLon} class, mainly for L{Numpy2LatLon} and L{Tuple2LatLon}. 

75 ''' 

76 # __slots__ efficiency is voided if the __slots__ class attribute is 

77 # used in a subclass of a class with the traditional __dict__, @see 

78 # <https://docs.Python.org/2/reference/datamodel.html#slots> plus ... 

79 # 

80 # __slots__ must be repeated in sub-classes, @see Luciano Ramalho, 

81 # "Fluent Python", O'Reilly, 2016 p. 276+ "Problems with __slots__", 

82 # 2nd Ed, 2022 p. 390 "Summarizing the Issues with __slots__". 

83 # 

84 # __slots__ = (_lat_, _lon_, _height_, _datum_, _name_) 

85 # Property_RO = property_RO # no __dict__ with __slots__! 

86 # 

87 # In addition, both size and overhead have shrunk in recent Python: 

88 # 

89 # sys.getsizeof(LatLon_(1, 2)) is 72-88 I{with} __slots__, but 

90 # only 48-56 bytes I{without in Python 2.7.18+ and Python 3+}. 

91 # 

92 # python3 -m timeit -s "from pygeodesy... import LatLonBase as LL" "LL(0, 0)" 2.14 usec 

93 # python3 -m timeit -s "from pygeodesy import LatLon_" "LatLon_(0, 0)" 216 nsec 

94 

95 def __init__(self, latlonh, lon=None, height=0, wrap=False, datum=None, **name): 

96 '''New L{LatLon_}. 

97 

98 @note: The lat- and longitude values are taken I{as-given, 

99 un-clipped and un-validated}. 

100 

101 @see: L{latlonBase.LatLonBase} for further details. 

102 ''' 

103 if name: 

104 self.name = name 

105 

106 if lon is None: # PYCHOK no cover 

107 lat, lon, height = _latlonheight3(latlonh, height, wrap) 

108 elif wrap: # PYCHOK no cover 

109 lat, lon = _Wrap.latlonDMS2(latlonh, lon) 

110 else: # must be latNS, lonEW 

111 try: 

112 lat, lon = float(latlonh), float(lon) 

113 except (TypeError, ValueError): 

114 lat = parseDMS(latlonh, suffix=_NS_) 

115 lon = parseDMS(lon, suffix=_EW_) 

116 

117 # get the minimal __dict__, see _isLatLon_ below 

118 self._lat = lat # un-clipped and ... 

119 self._lon = lon # ... un-validated 

120 self._datum = None if datum is None else \ 

121 _spherical_datum(datum, name=self.name) 

122 self._height = height 

123 

124 def __eq__(self, other): 

125 return isinstance(other, LatLon_) and \ 

126 other.lat == self.lat and \ 

127 other.lon == self.lon 

128 

129 def __ne__(self, other): 

130 return not self.__eq__(other) 

131 

132 @Property_RO 

133 def datum(self): 

134 '''Get the C{datum} (L{Datum}) or C{None}. 

135 ''' 

136 return self._datum 

137 

138 def intermediateTo(self, other, fraction, height=None, wrap=False): 

139 '''Locate the point at a given fraction, I{linearly} between 

140 (or along) this and an other point. 

141 

142 @arg other: The other point (C{LatLon}). 

143 @arg fraction: Fraction between both points (C{float}, 

144 0.0 for this and 1.0 for the other point). 

145 @kwarg height: Optional height (C{meter}), overriding the 

146 intermediate height. 

147 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll 

148 the B{C{other}} point (C{bool}). 

149 

150 @return: Intermediate point (same C{LatLon} class). 

151 

152 @raise TypeError: Incompatible B{C{other}} C{type}. 

153 ''' 

154 f = Scalar(fraction=fraction) 

155 if isnear0(f): 

156 r = self 

157 else: 

158 r = self.others(other) 

159 if wrap or not isnear1(f): 

160 _, lat, lon = _Wrap.latlon3(self.lon, r.lat, r.lon, wrap) 

161 lat = favg(self.lat, lat, f=f) 

162 lon = favg(self.lon, lon, f=f) 

163 h = height if height is not None else \ 

164 favg(self.height, r.height, f=f) 

165 # = self._havg(r, f=f, h=height) 

166 r = self.classof(lat, lon, height=h, datum=r.datum, 

167 name=r.intermediateTo.__name__) 

168 return r 

169 

170 def toRepr(self, **kwds): 

171 '''This L{LatLon_} as a string "class(<degrees>, ...)", 

172 ignoring keyword argument C{B{std}=N/A}. 

173 

174 @see: L{latlonBase.LatLonBase.toRepr} for further details. 

175 ''' 

176 _, kwds = _xkwds_pop2(kwds, std=NotImplemented) 

177 return LatLonBase.toRepr(self, **kwds) 

178 

179 def toStr(self, form=F_D, joined=_COMMASPACE_, **m_prec_sep_s_D_M_S): # PYCHOK expected 

180 '''Convert this point to a "lat, lon[, height][, name][, ...]" 

181 string, formatted in the given C{B{form}at}. 

182 

183 @see: L{latlonBase.LatLonBase.toStr} for further details. 

184 ''' 

185 t = LatLonBase.toStr(self, form=form, joined=NN, 

186 **_xkwds(m_prec_sep_s_D_M_S, m=NN)) 

187 if self.name: 

188 t += (repr(self.name),) 

189 return joined.join(t) if joined else t 

190 

191 

192def _isLatLon(inst): 

193 '''(INTERNAL) Check a C{LatLon} or C{LatLon_} instance. 

194 ''' 

195 return isinstance(inst, (LatLon_, _MODS.latlonBase.LatLonBase)) 

196 

197 

198def _isLatLon_(LL): 

199 '''(INTERNAL) Check a (sub-)class of C{LatLon_}. 

200 ''' 

201 return issubclassof(LL, LatLon_) or (isclass(LL) and 

202 all(hasattr(LL, _) for _ in LatLon_(0, 0).__dict__.keys())) 

203 

204 

205class _Basequence(_Sequence): # immutable, on purpose 

206 '''(INTERNAL) Base class. 

207 ''' 

208 _array = [] 

209 _epsilon = EPS 

210 _itemname = _point_ 

211 

212 def _contains(self, point): 

213 '''(INTERNAL) Check for a matching point. 

214 ''' 

215 return any(self._findall(point, ())) 

216 

217 def copy(self, deep=False): # PYCHOK no cover 

218 '''Make a shallow or deep copy of this instance. 

219 

220 @kwarg deep: If C{True}, make a deep, otherwise a 

221 shallow copy (C{bool}). 

222 

223 @return: The copy (C{This class}). 

224 ''' 

225 return _xcopy(self, deep=deep) 

226 

227 def _count(self, point): 

228 '''(INTERNAL) Count the number of matching points. 

229 ''' 

230 return sum(1 for _ in self._findall(point, ())) # NOT len()! 

231 

232 def dup(self, **items): # PYCHOK no cover 

233 '''Duplicate this instance, I{without replacing items}. 

234 

235 @kwarg items: No attributes (I{not allowed}). 

236 

237 @return: The duplicate (C{This class}). 

238 

239 @raise TypeError: Any B{C{items}} invalid. 

240 ''' 

241 if items: 

242 t = _SPACE_(classname(self), _immutable_) 

243 raise _TypeError(txt=t, this=self, **items) 

244 return _xdup(self) 

245 

246 @property_doc_(''' the equality tolerance (C{float}).''') 

247 def epsilon(self): 

248 '''Get the tolerance for equality tests (C{float}). 

249 ''' 

250 return self._epsilon 

251 

252 @epsilon.setter # PYCHOK setter! 

253 def epsilon(self, tol): 

254 '''Set the tolerance for equality tests (C{scalar}). 

255 

256 @raise UnitError: Non-scalar or invalid B{C{tol}}. 

257 ''' 

258 self._epsilon = Scalar_(tolerance=tol) 

259 

260 def _find(self, point, start_end): 

261 '''(INTERNAL) Find the first matching point index. 

262 ''' 

263 for i in self._findall(point, start_end): 

264 return i 

265 return -1 

266 

267 def _findall(self, point, start_end): # PYCHOK no cover 

268 '''(INTERNAL) I{Must be implemented/overloaded}.''' 

269 notImplemented(self, point, start_end) 

270 

271 def _getitem(self, index): 

272 '''(INTERNAL) Return point [index] or return a slice. 

273 ''' 

274 # Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 290+, 2022 p. 405+ 

275 if isinstance(index, slice): 

276 # XXX an numpy.[nd]array slice is a view, not a copy 

277 return self.__class__(self._array[index], **self._slicekwds()) 

278 else: 

279 return self.point(self._array[index]) 

280 

281 def _index(self, point, start_end): 

282 '''(INTERNAL) Find the first matching point index. 

283 ''' 

284 for i in self._findall(point, start_end): 

285 return i 

286 raise _IndexError(self._itemname, point, txt_not_='found') 

287 

288 @property_RO 

289 def isNumpy2(self): # PYCHOK no cover 

290 '''Is this a Numpy2 wrapper? 

291 ''' 

292 return False # isinstance(self, (Numpy2LatLon, ...)) 

293 

294 @property_RO 

295 def isPoints2(self): # PYCHOK no cover 

296 '''Is this a LatLon2 wrapper/converter? 

297 ''' 

298 return False # isinstance(self, (LatLon2psxy, ...)) 

299 

300 @property_RO 

301 def isTuple2(self): # PYCHOK no cover 

302 '''Is this a Tuple2 wrapper? 

303 ''' 

304 return False # isinstance(self, (Tuple2LatLon, ...)) 

305 

306 def _iter(self): 

307 '''(INTERNAL) Yield all points. 

308 ''' 

309 _array, _point = self._array, self.point 

310 for i in range(len(self)): 

311 yield _point(_array[i]) 

312 

313 def point(self, *attrs): # PYCHOK no cover 

314 '''I{Must be overloaded}.''' 

315 notOverloaded(self, *attrs) 

316 

317 def _range(self, start=None, end=None, step=1): 

318 '''(INTERNAL) Return the range. 

319 ''' 

320 if step > 0: 

321 if start is None: 

322 start = 0 

323 if end is None: 

324 end = len(self) 

325 elif step < 0: 

326 if start is None: 

327 start = len(self) - 1 

328 if end is None: 

329 end = -1 

330 else: 

331 raise _ValueError(step=step) 

332 return range(start, end, step) 

333 

334 def _repr(self): 

335 '''(INTERNAL) Return a string representation. 

336 ''' 

337 # XXX use Python 3+ reprlib.repr 

338 t = repr(self._array[:1]) # only first row 

339 t = _SPACE_(t[:-1], _ELLIPSIS_, Fmt.SQUARE(t[-1:], len(self))) 

340 t = _SPACE_.join(t.split()) # coalesce spaces 

341 return instr(self, t, **self._slicekwds()) 

342 

343 def _reversed(self): # PYCHOK false 

344 '''(INTERNAL) Yield all points in reverse order. 

345 ''' 

346 _array, point = self._array, self.point 

347 for i in range(len(self) - 1, -1, -1): 

348 yield point(_array[i]) 

349 

350 def _rfind(self, point, start_end): 

351 '''(INTERNAL) Find the last matching point index. 

352 ''' 

353 def _r3(start=None, end=None, step=-1): 

354 return (start, end, step) # PYCHOK returns 

355 

356 for i in self._findall(point, _r3(*start_end)): 

357 return i 

358 return -1 

359 

360 def _slicekwds(self): # PYCHOK no cover 

361 '''(INTERNAL) I{Should be overloaded}. 

362 ''' 

363 return {} 

364 

365 

366class _Array2LatLon(_Basequence): # immutable, on purpose 

367 '''(INTERNAL) Base class for Numpy2LatLon or Tuple2LatLon. 

368 ''' 

369 _array = () 

370 _ilat = 0 # row column index 

371 _ilon = 0 # row column index 

372 _LatLon = LatLon_ # default 

373 _shape = () 

374 

375 def __init__(self, array, ilat=0, ilon=1, LatLon=None, shape=()): 

376 '''Handle a C{NumPy} or C{Tuple} array as a sequence of C{LatLon} points. 

377 ''' 

378 ais = (_ilat_, ilat), (_ilon_, ilon) 

379 

380 if len(shape) != 2 or shape[0] < 1 or shape[1] < len(ais): 

381 raise _IndexError('array.shape', shape) 

382 

383 self._array = array 

384 self._shape = Shape2Tuple(shape) # *shape 

385 

386 if LatLon: # check the point class 

387 if not _isLatLon_(LatLon): 

388 raise _IsnotError(_valid_, LatLon=LatLon) 

389 self._LatLon = LatLon 

390 

391 # check the attr indices 

392 for n, (ai, i) in enumerate(ais): 

393 if not isint(i): 

394 raise _IsnotError(int, **{ai: i}) 

395 i = int(i) 

396 if not 0 <= i < shape[1]: 

397 raise _ValueError(ai, i) 

398 for aj, j in ais[:n]: 

399 if int(j) == i: 

400 raise _ValueError(_DEQUALSPACED_(ai, aj, i)) 

401 setattr(self, NN(_UNDER_, ai), i) 

402 

403 def __contains__(self, latlon): 

404 '''Check for a specific lat-/longitude. 

405 

406 @arg latlon: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple 

407 C{(lat, lon)}). 

408 

409 @return: C{True} if B{C{latlon}} is present, C{False} otherwise. 

410 

411 @raise TypeError: Invalid B{C{latlon}}. 

412 ''' 

413 return self._contains(latlon) 

414 

415 def __getitem__(self, index): 

416 '''Return row[index] as C{LatLon} or return a L{Numpy2LatLon} slice. 

417 ''' 

418 return self._getitem(index) 

419 

420 def __iter__(self): 

421 '''Yield rows as C{LatLon}. 

422 ''' 

423 return self._iter() 

424 

425 def __len__(self): 

426 '''Return the number of rows. 

427 ''' 

428 return self._shape[0] 

429 

430 def __repr__(self): 

431 '''Return a string representation. 

432 ''' 

433 return self._repr() 

434 

435 def __reversed__(self): # PYCHOK false 

436 '''Yield rows as C{LatLon} in reverse order. 

437 ''' 

438 return self._reversed() 

439 

440 __str__ = __repr__ 

441 

442 def count(self, latlon): 

443 '''Count the number of rows with a specific lat-/longitude. 

444 

445 @arg latlon: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple 

446 C{(lat, lon)}). 

447 

448 @return: Count (C{int}). 

449 

450 @raise TypeError: Invalid B{C{latlon}}. 

451 ''' 

452 return self._count(latlon) 

453 

454 def find(self, latlon, *start_end): 

455 '''Find the first row with a specific lat-/longitude. 

456 

457 @arg latlon: Point (C{LatLon}) or 2-tuple (lat, lon). 

458 @arg start_end: Optional C{[start[, end]]} index (integers). 

459 

460 @return: Index or -1 if not found (C{int}). 

461 

462 @raise TypeError: Invalid B{C{latlon}}. 

463 ''' 

464 return self._find(latlon, start_end) 

465 

466 def _findall(self, latlon, start_end): 

467 '''(INTERNAL) Yield indices of all matching rows. 

468 ''' 

469 try: 

470 lat, lon = latlon.lat, latlon.lon 

471 except AttributeError: 

472 try: 

473 lat, lon = latlon 

474 except (TypeError, ValueError): 

475 raise _IsnotError(_valid_, latlon=latlon) 

476 

477 _ilat, _ilon = self._ilat, self._ilon 

478 _array, _eps = self._array, self._epsilon 

479 for i in self._range(*start_end): 

480 row = _array[i] 

481 if fabs(row[_ilat] - lat) <= _eps and \ 

482 fabs(row[_ilon] - lon) <= _eps: 

483 yield i 

484 

485 def findall(self, latlon, *start_end): 

486 '''Yield indices of all rows with a specific lat-/longitude. 

487 

488 @arg latlon: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple 

489 C{(lat, lon)}). 

490 @arg start_end: Optional C{[start[, end]]} index (C{int}). 

491 

492 @return: Indices (C{iterable}). 

493 

494 @raise TypeError: Invalid B{C{latlon}}. 

495 ''' 

496 return self._findall(latlon, start_end) 

497 

498 def index(self, latlon, *start_end): # PYCHOK Python 2- issue 

499 '''Find index of the first row with a specific lat-/longitude. 

500 

501 @arg latlon: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple 

502 C{(lat, lon)}). 

503 @arg start_end: Optional C{[start[, end]]} index (C{int}). 

504 

505 @return: Index (C{int}). 

506 

507 @raise IndexError: Point not found. 

508 

509 @raise TypeError: Invalid B{C{latlon}}. 

510 ''' 

511 return self._index(latlon, start_end) 

512 

513 @Property_RO 

514 def ilat(self): 

515 '''Get the latitudes column index (C{int}). 

516 ''' 

517 return self._ilat 

518 

519 @Property_RO 

520 def ilon(self): 

521 '''Get the longitudes column index (C{int}). 

522 ''' 

523 return self._ilon 

524 

525# next = __iter__ 

526 

527 def point(self, row): # PYCHOK *attrs 

528 '''Instantiate a point C{LatLon}. 

529 

530 @arg row: Array row (numpy.array). 

531 

532 @return: Point (C{LatLon}). 

533 ''' 

534 return self._LatLon(row[self._ilat], row[self._ilon]) 

535 

536 def rfind(self, latlon, *start_end): 

537 '''Find the last row with a specific lat-/longitude. 

538 

539 @arg latlon: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple 

540 C{(lat, lon)}). 

541 @arg start_end: Optional C{[start[, end]]} index (C{int}). 

542 

543 @note: Keyword order, first stop, then start. 

544 

545 @return: Index or -1 if not found (C{int}). 

546 

547 @raise TypeError: Invalid B{C{latlon}}. 

548 ''' 

549 return self._rfind(latlon, start_end) 

550 

551 def _slicekwds(self): 

552 '''(INTERNAL) Slice kwds. 

553 ''' 

554 return dict(ilat=self._ilat, ilon=self._ilon) 

555 

556 @Property_RO 

557 def shape(self): 

558 '''Get the shape of the C{NumPy} array or the C{Tuples} as 

559 L{Shape2Tuple}C{(nrows, ncols)}. 

560 ''' 

561 return self._shape 

562 

563 def _subset(self, indices): # PYCHOK no cover 

564 '''(INTERNAL) I{Must be implemented/overloaded}.''' 

565 notImplemented(self, indices) 

566 

567 def subset(self, indices): 

568 '''Return a subset of the C{NumPy} array. 

569 

570 @arg indices: Row indices (C{range} or C{int}[]). 

571 

572 @note: A C{subset} is different from a C{slice} in 2 ways: 

573 (a) the C{subset} is typically specified as a list of 

574 (un-)ordered indices and (b) the C{subset} allocates 

575 a new, separate C{NumPy} array while a C{slice} is 

576 just an other C{view} of the original C{NumPy} array. 

577 

578 @return: Sub-array (C{numpy.array}). 

579 

580 @raise IndexError: Out-of-range B{C{indices}} value. 

581 

582 @raise TypeError: If B{C{indices}} is not a C{range} 

583 nor an C{int}[]. 

584 ''' 

585 if not issequence(indices, tuple): # NO tuple, only list 

586 # and range work properly to get Numpy array sub-sets 

587 raise _IsnotError(_valid_, indices=type(indices)) 

588 

589 n = len(self) 

590 for i, v in enumerate(indices): 

591 if not isint(v): 

592 raise _TypeError(Fmt.SQUARE(indices=i), v) 

593 elif not 0 <= v < n: 

594 raise _IndexError(Fmt.SQUARE(indices=i), v) 

595 

596 return self._subset(indices) 

597 

598 

599class LatLon2psxy(_Basequence): 

600 '''Wrapper for C{LatLon} points as "on-the-fly" pseudo-xy coordinates. 

601 ''' 

602 _closed = False 

603 _len = 0 

604 _deg2m = None # default, keep degrees 

605 _radius = None 

606 _wrap = True 

607 

608 def __init__(self, latlons, closed=False, radius=None, wrap=True): 

609 '''Handle C{LatLon} points as pseudo-xy coordinates. 

610 

611 @note: The C{LatLon} latitude is considered the I{pseudo-y} 

612 and longitude the I{pseudo-x} coordinate, likewise 

613 for L{LatLon2Tuple}. However, 2-tuples C{(x, y)} are 

614 considered as I{(longitude, latitude)}. 

615 

616 @arg latlons: Points C{list}, C{sequence}, C{set}, C{tuple}, 

617 etc. (C{LatLon[]}). 

618 @kwarg closed: Optionally, close the polygon (C{bool}). 

619 @kwarg radius: Mean earth radius (C{meter}). 

620 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll 

621 the B{C{latlons}} points (C{bool}). 

622 

623 @raise PointsError: Insufficient number of B{C{latlons}}. 

624 

625 @raise TypeError: Some B{C{points}} are not B{C{base}}. 

626 ''' 

627 self._closed = closed 

628 self._len, self._array = points2(latlons, closed=closed) 

629 if radius: 

630 self._radius = r = Radius(radius) 

631 self._deg2m = degrees2m(_1_0, r) 

632 if not wrap: 

633 self._wrap = False 

634 

635 def __contains__(self, xy): 

636 '''Check for a matching point. 

637 

638 @arg xy: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple 

639 C{(x, y)}) in (C{degrees}. 

640 

641 @return: C{True} if B{C{xy}} is present, C{False} otherwise. 

642 

643 @raise TypeError: Invalid B{C{xy}}. 

644 ''' 

645 return self._contains(xy) 

646 

647 def __getitem__(self, index): 

648 '''Return the pseudo-xy or return a L{LatLon2psxy} slice. 

649 ''' 

650 return self._getitem(index) 

651 

652 def __iter__(self): 

653 '''Yield all pseudo-xy's. 

654 ''' 

655 return self._iter() 

656 

657 def __len__(self): 

658 '''Return the number of pseudo-xy's. 

659 ''' 

660 return self._len 

661 

662 def __repr__(self): 

663 '''Return a string representation. 

664 ''' 

665 return self._repr() 

666 

667 def __reversed__(self): # PYCHOK false 

668 '''Yield all pseudo-xy's in reverse order. 

669 ''' 

670 return self._reversed() 

671 

672 __str__ = __repr__ 

673 

674 def count(self, xy): 

675 '''Count the number of matching points. 

676 

677 @arg xy: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple 

678 C{(x, y)}) in (C{degrees}. 

679 

680 @return: Count (C{int}). 

681 

682 @raise TypeError: Invalid B{C{xy}}. 

683 ''' 

684 return self._count(xy) 

685 

686 def find(self, xy, *start_end): 

687 '''Find the first matching point. 

688 

689 @arg xy: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple 

690 C{(x, y)}) in (C{degrees}. 

691 @arg start_end: Optional C{[start[, end]]} index (C{int}). 

692 

693 @return: Index or -1 if not found (C{int}). 

694 

695 @raise TypeError: Invalid B{C{xy}}. 

696 ''' 

697 return self._find(xy, start_end) 

698 

699 def _findall(self, xy, start_end): 

700 '''(INTERNAL) Yield indices of all matching points. 

701 ''' 

702 try: 

703 x, y = xy.lon, xy.lat 

704 

705 def _x_y_ll3(ll): # match LatLon 

706 return ll.lon, ll.lat, ll 

707 

708 except AttributeError: 

709 try: 

710 x, y = xy[:2] 

711 except (IndexError, TypeError, ValueError): 

712 raise _IsnotError(_valid_, xy=xy) 

713 

714 _x_y_ll3 = self.point # PYCHOK expected 

715 

716 _array, _eps = self._array, self._epsilon 

717 for i in self._range(*start_end): 

718 xi, yi, _ = _x_y_ll3(_array[i]) 

719 if fabs(xi - x) <= _eps and \ 

720 fabs(yi - y) <= _eps: 

721 yield i 

722 

723 def findall(self, xy, *start_end): 

724 '''Yield indices of all matching points. 

725 

726 @arg xy: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple 

727 C{(x, y)}) in (C{degrees}. 

728 @arg start_end: Optional C{[start[, end]]} index (C{int}). 

729 

730 @return: Indices (C{iterator}). 

731 

732 @raise TypeError: Invalid B{C{xy}}. 

733 ''' 

734 return self._findall(xy, start_end) 

735 

736 def index(self, xy, *start_end): # PYCHOK Python 2- issue 

737 '''Find the first matching point. 

738 

739 @arg xy: Point (C{LatLon}) or 2-tuple (x, y) in (C{degrees}). 

740 @arg start_end: Optional C{[start[, end]]} index (C{int}). 

741 

742 @return: Index (C{int}). 

743 

744 @raise IndexError: Point not found. 

745 

746 @raise TypeError: Invalid B{C{xy}}. 

747 ''' 

748 return self._index(xy, start_end) 

749 

750 @property_RO 

751 def isPoints2(self): 

752 '''Is this a LatLon2 wrapper/converter? 

753 ''' 

754 return True # isinstance(self, (LatLon2psxy, ...)) 

755 

756 def point(self, ll): # PYCHOK *attrs 

757 '''Create a pseudo-xy. 

758 

759 @arg ll: Point (C{LatLon}). 

760 

761 @return: An L{Point3Tuple}C{(x, y, ll)}. 

762 ''' 

763 x, y = ll.lon, ll.lat # note, x, y = lon, lat 

764 if self._wrap: 

765 y, x = _Wrap.latlon(y, x) 

766 d = self._deg2m 

767 if d: # convert degrees to meter (or radians) 

768 x *= d 

769 y *= d 

770 return Point3Tuple(x, y, ll) 

771 

772 def rfind(self, xy, *start_end): 

773 '''Find the last matching point. 

774 

775 @arg xy: Point (C{LatLon}, L{LatLon2Tuple} or 2-tuple 

776 C{(x, y)}) in (C{degrees}. 

777 @arg start_end: Optional C{[start[, end]]} index (C{int}). 

778 

779 @return: Index or -1 if not found (C{int}). 

780 

781 @raise TypeError: Invalid B{C{xy}}. 

782 ''' 

783 return self._rfind(xy, start_end) 

784 

785 def _slicekwds(self): 

786 '''(INTERNAL) Slice kwds. 

787 ''' 

788 return dict(closed=self._closed, radius=self._radius, wrap=self._wrap) 

789 

790 

791class Numpy2LatLon(_Array2LatLon): # immutable, on purpose 

792 '''Wrapper for C{NumPy} arrays as "on-the-fly" C{LatLon} points. 

793 ''' 

794 def __init__(self, array, ilat=0, ilon=1, LatLon=None): 

795 '''Handle a C{NumPy} array as a sequence of C{LatLon} points. 

796 

797 @arg array: C{NumPy} array (C{numpy.array}). 

798 @kwarg ilat: Optional index of the latitudes column (C{int}). 

799 @kwarg ilon: Optional index of the longitudes column (C{int}). 

800 @kwarg LatLon: Optional C{LatLon} class to use (L{LatLon_}). 

801 

802 @raise IndexError: If B{C{array.shape}} is not (1+, 2+). 

803 

804 @raise TypeError: If B{C{array}} is not a C{NumPy} array or 

805 C{LatLon} is not a class with C{lat} 

806 and C{lon} attributes. 

807 

808 @raise ValueError: If the B{C{ilat}} and/or B{C{ilon}} values 

809 are the same or out of range. 

810 

811 @example: 

812 

813 >>> type(array) 

814 <type 'numpy.ndarray'> # <class ...> in Python 3+ 

815 >>> points = Numpy2LatLon(array, lat=0, lon=1) 

816 >>> simply = simplifyRDP(points, ...) 

817 >>> type(simply) 

818 <type 'numpy.ndarray'> # <class ...> in Python 3+ 

819 >>> sliced = points[1:-1] 

820 >>> type(sliced) 

821 <class '...Numpy2LatLon'> 

822 ''' 

823 try: # get shape and check some other numpy.array attrs 

824 s, _, _ = array.shape, array.nbytes, array.ndim # PYCHOK expected 

825 except AttributeError: 

826 raise _IsnotError('NumPy', array=type(array)) 

827 

828 _Array2LatLon.__init__(self, array, ilat=ilat, ilon=ilon, 

829 LatLon=LatLon, shape=s) 

830 

831 @property_RO 

832 def isNumpy2(self): 

833 '''Is this a Numpy2 wrapper? 

834 ''' 

835 return True # isinstance(self, (Numpy2LatLon, ...)) 

836 

837 def _subset(self, indices): 

838 return self._array[indices] # NumPy special 

839 

840 

841class Shape2Tuple(_NamedTuple): 

842 '''2-Tuple C{(nrows, ncols)}, the number of rows and columns, 

843 both C{int}. 

844 ''' 

845 _Names_ = (_nrows_, _ncols_) 

846 _Units_ = ( Number_, Number_) 

847 

848 

849class Tuple2LatLon(_Array2LatLon): 

850 '''Wrapper for tuple sequences as "on-the-fly" C{LatLon} points. 

851 ''' 

852 def __init__(self, tuples, ilat=0, ilon=1, LatLon=None): 

853 '''Handle a list of tuples, each containing a lat- and longitude 

854 and perhaps other values as a sequence of C{LatLon} points. 

855 

856 @arg tuples: The C{list}, C{tuple} or C{sequence} of tuples (C{tuple}[]). 

857 @kwarg ilat: Optional index of the latitudes value (C{int}). 

858 @kwarg ilon: Optional index of the longitudes value (C{int}). 

859 @kwarg LatLon: Optional C{LatLon} class to use (L{LatLon_}). 

860 

861 @raise IndexError: If C{(len(B{tuples}), min(len(t) for t 

862 in B{tuples}))} is not (1+, 2+). 

863 

864 @raise TypeError: If B{C{tuples}} is not a C{list}, C{tuple} 

865 or C{sequence} or if B{C{LatLon}} is not a 

866 C{LatLon} with C{lat}, C{lon} and C{name} 

867 attributes. 

868 

869 @raise ValueError: If the B{C{ilat}} and/or B{C{ilon}} values 

870 are the same or out of range. 

871 

872 @example: 

873 

874 >>> tuples = [(0, 1), (2, 3), (4, 5)] 

875 >>> type(tuples) 

876 <type 'list'> # <class ...> in Python 3+ 

877 >>> points = Tuple2LatLon(tuples, lat=0, lon=1) 

878 >>> simply = simplifyRW(points, 0.5, ...) 

879 >>> type(simply) 

880 <type 'list'> # <class ...> in Python 3+ 

881 >>> simply 

882 [(0, 1), (4, 5)] 

883 >>> sliced = points[1:-1] 

884 >>> type(sliced) 

885 <class '...Tuple2LatLon'> 

886 >>> sliced 

887 ...Tuple2LatLon([(2, 3), ...][1], ilat=0, ilon=1) 

888 

889 >>> closest, _ = nearestOn2(LatLon_(2, 1), points, adjust=False) 

890 >>> closest 

891 LatLon_(lat=1.0, lon=2.0) 

892 

893 >>> closest, _ = nearestOn2(LatLon_(3, 2), points) 

894 >>> closest 

895 LatLon_(lat=2.001162, lon=3.001162) 

896 ''' 

897 _xinstanceof(list, tuple, tuples=tuples) 

898 s = len(tuples), min(len(_) for _ in tuples) 

899 _Array2LatLon.__init__(self, tuples, ilat=ilat, ilon=ilon, 

900 LatLon=LatLon, shape=s) 

901 

902 @property_RO 

903 def isTuple2(self): 

904 '''Is this a Tuple2 wrapper? 

905 ''' 

906 return True # isinstance(self, (Tuple2LatLon, ...)) 

907 

908 def _subset(self, indices): 

909 return type(self._array)(self._array[i] for i in indices) 

910 

911 

912def _area2(points, adjust, wrap): 

913 '''(INTERNAL) Approximate the area in radians squared, I{signed}. 

914 ''' 

915 if adjust: 

916 # approximate trapezoid by a rectangle, adjusting 

917 # the top width by the cosine of the latitudinal 

918 # average and bottom width by some fudge factor 

919 def _adjust(w, h): 

920 c = cos(h) if fabs(h) < PI_2 else _0_0 

921 return w * h * (c + 1.2876) * _0_5 

922 else: 

923 def _adjust(w, h): # PYCHOK expected 

924 return w * h 

925 

926 # setting radius=1 converts degrees to radians 

927 Ps = LatLon2PsxyIter(points, loop=1, radius=_1_0, wrap=wrap) 

928 x1, y1, ll = Ps[0] 

929 pts = [ll] # for _areaError 

930 

931 A2 = Fsum() # trapezoidal area in radians**2 

932 for p in Ps.iterate(closed=True): 

933 x2, y2, ll = p 

934 if len(pts) < 4: 

935 pts.append(ll) 

936 w, x2 = unrollPI(x1, x2, wrap=wrap and not Ps.looped) 

937 A2 += _adjust(w, (y2 + y1) * _0_5) 

938 x1, y1 = x2, y2 

939 

940 return A2.fsum(), tuple(pts) 

941 

942 

943def _areaError(pts, near_=NN): # in .ellipsoidalKarney 

944 '''(INTERNAL) Area issue. 

945 ''' 

946 t = _ELLIPSIS_(pts[:3], NN) 

947 return _ValueError(NN(near_, 'zero or polar area'), txt=t) 

948 

949 

950def areaOf(points, adjust=True, radius=R_M, wrap=True): 

951 '''Approximate the area of a polygon or composite. 

952 

953 @arg points: The polygon points or clips (C{LatLon}[], 

954 L{BooleanFHP} or L{BooleanGH}). 

955 @kwarg adjust: Adjust the wrapped, unrolled longitudinal delta 

956 by the cosine of the mean latitude (C{bool}). 

957 @kwarg radius: Mean earth radius (C{meter}) or C{None}. 

958 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll 

959 the B{C{points}} (C{bool}). 

960 

961 @return: Approximate area (I{square} C{meter}, same units as 

962 B{C{radius}} or C{radians} I{squared} if C{B{radius} 

963 is None}). 

964 

965 @raise PointsError: Insufficient number of B{C{points}} 

966 

967 @raise TypeError: Some B{C{points}} are not C{LatLon}. 

968 

969 @raise ValueError: Invalid B{C{radius}}. 

970 

971 @note: This area approximation has limited accuracy and is 

972 ill-suited for regions exceeding several hundred Km 

973 or Miles or with near-polar latitudes. 

974 

975 @see: L{sphericalNvector.areaOf}, L{sphericalTrigonometry.areaOf}, 

976 L{ellipsoidalExact.areaOf} and L{ellipsoidalKarney.areaOf}. 

977 ''' 

978 if _MODS.booleans.isBoolean(points): 

979 a = points._sum1(areaOf, adjust=adjust, radius=None, wrap=wrap) 

980 else: 

981 a, _ = _area2(points, adjust, wrap) 

982 return fabs(a if radius is None else (Radius(radius)**2 * a)) 

983 

984 

985def boundsOf(points, wrap=False, LatLon=None): # was=True 

986 '''Determine the bottom-left SW and top-right NE corners of a 

987 path or polygon. 

988 

989 @arg points: The path or polygon points (C{LatLon}[]). 

990 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll 

991 the B{C{points}} (C{bool}). 

992 @kwarg LatLon: Optional class to return the C{bounds} 

993 corners (C{LatLon}) or C{None}. 

994 

995 @return: A L{Bounds2Tuple}C{(latlonSW, latlonNE)}, each 

996 a B{C{LatLon}} or if C{B{LatLon} is None}, a 

997 L{Bounds4Tuple}C{(latS, lonW, latN, lonE)}. 

998 

999 @raise PointsError: Insufficient number of B{C{points}} 

1000 

1001 @raise TypeError: Some B{C{points}} are not C{LatLon}. 

1002 

1003 @see: Function L{quadOf}. 

1004 ''' 

1005 Ps = LatLon2PsxyIter(points, loop=1, wrap=wrap) 

1006 w, s, _ = e, n, _ = Ps[0] 

1007 

1008 v = w 

1009 for x, y, _ in Ps.iterate(closed=False): # [1:] 

1010 if wrap: 

1011 _, x = unroll180(v, x, wrap=True) 

1012 v = x 

1013 

1014 if w > x: 

1015 w = x 

1016 elif e < x: 

1017 e = x 

1018 

1019 if s > y: 

1020 s = y 

1021 elif n < y: 

1022 n = y 

1023 

1024 return Bounds4Tuple(s, w, n, e) if LatLon is None else \ 

1025 Bounds2Tuple(LatLon(s, w), LatLon(n, e)) # PYCHOK inconsistent 

1026 

1027 

1028def centroidOf(points, wrap=False, LatLon=None): # was=True 

1029 '''Determine the centroid of a polygon. 

1030 

1031 @arg points: The polygon points (C{LatLon}[]). 

1032 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the 

1033 B{C{points}} (C{bool}). 

1034 @kwarg LatLon: Optional class to return the centroid (C{LatLon}) 

1035 or C{None}. 

1036 

1037 @return: Centroid (B{C{LatLon}}) or a L{LatLon2Tuple}C{(lat, lon)} 

1038 if C{B{LatLon} is None}. 

1039 

1040 @raise PointsError: Insufficient number of B{C{points}} 

1041 

1042 @raise TypeError: Some B{C{points}} are not C{LatLon}. 

1043 

1044 @raise ValueError: The B{C{points}} enclose a pole or 

1045 near-zero area. 

1046 

1047 @see: U{Centroid<https://WikiPedia.org/wiki/Centroid#Of_a_polygon>} and 

1048 Paul Bourke's U{Calculating The Area And Centroid Of A Polygon 

1049 <https://www.SEAS.UPenn.edu/~ese502/lab-content/extra_materials/ 

1050 Polygon%20Area%20and%20Centroid.pdf>}, 1988. 

1051 ''' 

1052 A, X, Y = Fsum(), Fsum(), Fsum() 

1053 

1054 # setting radius=1 converts degrees to radians 

1055 Ps = LatLon2PsxyIter(points, loop=1, radius=_1_0, wrap=wrap) 

1056 x1, y1, ll = Ps[0] 

1057 pts = [ll] # for _areaError 

1058 for p in Ps.iterate(closed=True): 

1059 x2, y2, ll = p 

1060 if len(pts) < 4: 

1061 pts.append(ll) 

1062 if wrap and not Ps.looped: 

1063 _, x2 = unrollPI(x1, x2, wrap=True) 

1064 t = x1 * y2 - x2 * y1 

1065 A += t 

1066 X += t * (x1 + x2) 

1067 Y += t * (y1 + y2) 

1068 # XXX more elaborately: 

1069 # t1, t2 = x1 * y2, -(x2 * y1) 

1070 # A.fadd_(t1, t2) 

1071 # X.fadd_(t1 * x1, t1 * x2, t2 * x1, t2 * x2) 

1072 # Y.fadd_(t1 * y1, t1 * y2, t2 * y1, t2 * y2) 

1073 x1, y1 = x2, y2 

1074 

1075 a = A.fmul(_6_0).fover(_2_0) 

1076 if isnear0(a): 

1077 raise _areaError(pts, near_=_near_) 

1078 y, x = degrees90(Y.fover(a)), degrees180(X.fover(a)) 

1079 return LatLon2Tuple(y, x) if LatLon is None else LatLon(y, x) 

1080 

1081 

1082def _distanceTo(Error, **name_points): # .frechet, .hausdorff, .heights 

1083 '''(INTERNAL) Check all callable C{distanceTo} methods. 

1084 ''' 

1085 name, ps = _xkwds_item2(name_points) 

1086 for i, p in enumerate(ps): 

1087 if not callable(_xattr(p, distanceTo=None)): 

1088 n = _distanceTo.__name__[1:] 

1089 t = _SPACE_(_no_, callable.__name__, n) 

1090 raise Error(Fmt.SQUARE(name, i), p, txt=t) 

1091 return ps 

1092 

1093 

1094def fractional(points, fi, j=None, wrap=None, LatLon=None, Vector=None, **kwds): 

1095 '''Return the point at a given I{fractional} index. 

1096 

1097 @arg points: The points (C{LatLon}[], L{Numpy2LatLon}[], 

1098 L{Tuple2LatLon}[], C{Cartesian}[], C{Vector3d}[], 

1099 L{Vector3Tuple}[]). 

1100 @arg fi: The fractional index (L{FIx}, C{float} or C{int}). 

1101 @kwarg j: Optionally, index of the other point (C{int}). 

1102 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the 

1103 B{{points}} (C{bool}) or C{None} for a backward 

1104 compatible L{LatLon2Tuple} or B{C{LatLon}} with 

1105 averaged lat- and longitudes. Use C{True} or 

1106 C{False} to get the I{fractional} point computed 

1107 by method C{B{points}[fi].intermediateTo}. 

1108 @kwarg LatLon: Optional class to return the I{intermediate}, 

1109 I{fractional} point (C{LatLon}) or C{None}. 

1110 @kwarg Vector: Optional class to return the I{intermediate}, 

1111 I{fractional} point (C{Cartesian}, C{Vector3d}) 

1112 or C{None}. 

1113 @kwarg kwds: Optional, additional B{C{LatLon}} I{or} B{C{Vector}} 

1114 keyword arguments, ignored if both C{B{LatLon}} and 

1115 C{B{Vector}} are C{None}. 

1116 

1117 @return: A L{LatLon2Tuple}C{(lat, lon)} if B{C{wrap}}, B{C{LatLon}} 

1118 and B{C{Vector}} all are C{None}, the defaults. 

1119 

1120 An instance of B{C{LatLon}} if not C{None} I{or} an instance 

1121 of B{C{Vector}} if not C{None}. 

1122 

1123 Otherwise with B{C{wrap}} either C{True} or C{False} and 

1124 B{C{LatLon}} and B{C{Vector}} both C{None}, an instance of 

1125 B{C{points}}' (sub-)class C{intermediateTo} I{fractional}. 

1126 

1127 Summarized as follows: 

1128 

1129 >>> wrap | LatLon | Vector | returned type/value 

1130 # -------+--------+--------+--------------+------ 

1131 # | | | LatLon2Tuple | favg 

1132 # None | None | None | or** | 

1133 # | | | Vector3Tuple | favg 

1134 # None | LatLon | None | LatLon | favg 

1135 # None | None | Vector | Vector | favg 

1136 # -------+--------+--------+--------------+------ 

1137 # True | None | None | points' | .iTo 

1138 # True | LatLon | None | LatLon | .iTo 

1139 # True | None | Vector | Vector | .iTo 

1140 # -------+--------+--------+--------------+------ 

1141 # False | None | None | points' | .iTo 

1142 # False | LatLon | None | LatLon | .iTo 

1143 # False | None | Vector | Vector | .iTo 

1144 # _____ 

1145 # favg) averaged lat, lon or x, y, z values 

1146 # .iTo) value from points[fi].intermediateTo 

1147 # **) depends on base class of points[fi] 

1148 

1149 @raise IndexError: Fractional index B{C{fi}} invalid or B{C{points}} 

1150 not subscriptable or not closed. 

1151 

1152 @raise TypeError: Invalid B{C{LatLon}}, B{C{Vector}} or B{C{kwds}} 

1153 argument. 

1154 

1155 @see: Class L{FIx} and method L{FIx.fractional}. 

1156 ''' 

1157 if LatLon and Vector: # PYCHOK no cover 

1158 kwds = _xkwds(kwds, fi=fi, LatLon=LatLon, Vector=Vector) 

1159 raise _TypeError(txt__=fractional, **kwds) 

1160 w = wrap if LatLon else False # intermediateTo 

1161 try: 

1162 if not isscalar(fi) or fi < 0: 

1163 raise IndexError 

1164 n = _xattr(fi, fin=0) 

1165 p = _fractional(points, fi, j, fin=n, wrap=w) # see .units.FIx 

1166 if LatLon: 

1167 p = LatLon(p.lat, p.lon, **kwds) 

1168 elif Vector: 

1169 p = Vector(p.x, p.y, p.z, **kwds) 

1170 except (IndexError, TypeError): 

1171 raise _IndexError(fi=fi, points=points, wrap=w, txt__=fractional) 

1172 return p 

1173 

1174 

1175def _fractional(points, fi, j, fin=None, wrap=None): # in .frechet.py 

1176 '''(INTERNAL) Compute point at L{fractional} index C{fi} and C{j}. 

1177 ''' 

1178 i = int(fi) 

1179 p = points[i] 

1180 r = fi - float(i) 

1181 if r > EPS: # EPS0? 

1182 if j is None: # in .frechet.py 

1183 j = i + 1 

1184 if fin: 

1185 j %= fin 

1186 q = points[j] 

1187 if r >= EPS1: # PYCHOK no cover 

1188 p = q 

1189 elif wrap is not None: # in (True, False) 

1190 p = p.intermediateTo(q, r, wrap=wrap) 

1191 elif _isLatLon(p): # backward compatible default 

1192 p = LatLon2Tuple(favg(p.lat, q.lat, f=r), 

1193 favg(p.lon, q.lon, f=r), 

1194 name__=fractional) 

1195 else: # assume p and q are cartesian or vectorial 

1196 z = p.z if p.z is q.z else favg(p.z, q.z, f=r) 

1197 p = Vector3Tuple(favg(p.x, q.x, f=r), 

1198 favg(p.y, q.y, f=r), z, 

1199 name__=fractional) 

1200 return p 

1201 

1202 

1203def isclockwise(points, adjust=False, wrap=True): 

1204 '''Determine the direction of a path or polygon. 

1205 

1206 @arg points: The path or polygon points (C{LatLon}[]). 

1207 @kwarg adjust: Adjust the wrapped, unrolled longitudinal delta 

1208 by the cosine of the mean latitude (C{bool}). 

1209 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the 

1210 B{C{points}} (C{bool}). 

1211 

1212 @return: C{True} if B{C{points}} are clockwise, C{False} otherwise. 

1213 

1214 @raise PointsError: Insufficient number of B{C{points}} 

1215 

1216 @raise TypeError: Some B{C{points}} are not C{LatLon}. 

1217 

1218 @raise ValueError: The B{C{points}} enclose a pole or zero area. 

1219 ''' 

1220 a, pts = _area2(points, adjust, wrap) 

1221 if a > 0: # opposite of ellipsoidalExact and -Karney 

1222 return True 

1223 elif a < 0: 

1224 return False 

1225 # <https://blog.Element84.com/determining-if-a-spherical-polygon-contains-a-pole.html> 

1226 raise _areaError(pts) 

1227 

1228 

1229def isconvex(points, adjust=False, wrap=False): # was=True 

1230 '''Determine whether a polygon is convex. 

1231 

1232 @arg points: The polygon points (C{LatLon}[]). 

1233 @kwarg adjust: Adjust the wrapped, unrolled longitudinal delta 

1234 by the cosine of the mean latitude (C{bool}). 

1235 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the 

1236 B{C{points}} (C{bool}). 

1237 

1238 @return: C{True} if B{C{points}} are convex, C{False} otherwise. 

1239 

1240 @raise CrossError: Some B{C{points}} are colinear. 

1241 

1242 @raise PointsError: Insufficient number of B{C{points}} 

1243 

1244 @raise TypeError: Some B{C{points}} are not C{LatLon}. 

1245 ''' 

1246 return bool(isconvex_(points, adjust=adjust, wrap=wrap)) 

1247 

1248 

1249def isconvex_(points, adjust=False, wrap=False): # was=True 

1250 '''Determine whether a polygon is convex I{and clockwise}. 

1251 

1252 @arg points: The polygon points (C{LatLon}[]). 

1253 @kwarg adjust: Adjust the wrapped, unrolled longitudinal delta 

1254 by the cosine of the mean latitude (C{bool}). 

1255 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the 

1256 B{C{points}} (C{bool}). 

1257 

1258 @return: C{+1} if B{C{points}} are convex clockwise, C{-1} for 

1259 convex counter-clockwise B{C{points}}, C{0} otherwise. 

1260 

1261 @raise CrossError: Some B{C{points}} are colinear. 

1262 

1263 @raise PointsError: Insufficient number of B{C{points}} 

1264 

1265 @raise TypeError: Some B{C{points}} are not C{LatLon}. 

1266 ''' 

1267 if adjust: 

1268 def _unroll2(x1, x2, w, y1, y2): 

1269 x21, x2 = unroll180(x1, x2, wrap=w) 

1270 y = radians(y1 + y2) * _0_5 

1271 x21 *= cos(y) if fabs(y) < PI_2 else _0_0 

1272 return x21, x2 

1273 else: 

1274 def _unroll2(x1, x2, w, *unused): # PYCHOK expected 

1275 return unroll180(x1, x2, wrap=w) 

1276 

1277 c, s = crosserrors(), 0 

1278 

1279 Ps = LatLon2PsxyIter(points, loop=2, wrap=wrap) 

1280 x1, y1, _ = Ps[0] 

1281 x2, y2, _ = Ps[1] 

1282 

1283 x21, x2 = _unroll2(x1, x2, False, y1, y2) 

1284 for i, p in Ps.enumerate(closed=True): 

1285 x3, y3, ll = p 

1286 x32, x3 = _unroll2(x2, x3, bool(wrap and not Ps.looped), y2, y3) 

1287 

1288 # get the sign of the distance from point 

1289 # x3, y3 to the line from x1, y1 to x2, y2 

1290 # <https://WikiPedia.org/wiki/Distance_from_a_point_to_a_line> 

1291 s3 = fdot((x3, y3, x1, y1), y2 - y1, -x21, -y2, x2) 

1292 if s3 > 0: # x3, y3 on the right 

1293 if s < 0: # non-convex 

1294 return 0 

1295 s = +1 

1296 

1297 elif s3 < 0: # x3, y3 on the left 

1298 if s > 0: # non-convex 

1299 return 0 

1300 s = -1 

1301 

1302 elif c and fdot((x32, y1 - y2), y3 - y2, -x21) < 0: # PYCHOK no cover 

1303 # colinear u-turn: x3, y3 not on the 

1304 # opposite side of x2, y2 as x1, y1 

1305 t = Fmt.SQUARE(points=i) 

1306 raise CrossError(t, ll, txt=_colinear_) 

1307 

1308 x1, y1, x2, y2, x21 = x2, y2, x3, y3, x32 

1309 

1310 return s # all points on the same side 

1311 

1312 

1313def isenclosedBy(point, points, wrap=False): # MCCABE 15 

1314 '''Determine whether a point is enclosed by a polygon or composite. 

1315 

1316 @arg point: The point (C{LatLon} or 2-tuple C{(lat, lon)}). 

1317 @arg points: The polygon points or clips (C{LatLon}[], L{BooleanFHP} 

1318 or L{BooleanGH}). 

1319 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the 

1320 B{C{points}} (C{bool}). 

1321 

1322 @return: C{True} if the B{C{point}} is inside the polygon or 

1323 composite, C{False} otherwise. 

1324 

1325 @raise PointsError: Insufficient number of B{C{points}} 

1326 

1327 @raise TypeError: Some B{C{points}} are not C{LatLon}. 

1328 

1329 @raise ValueError: Invalid B{C{point}}, lat- or longitude. 

1330 

1331 @see: Functions L{pygeodesy.isconvex} and L{pygeodesy.ispolar} especially 

1332 if the B{C{points}} may enclose a pole or wrap around the earth 

1333 I{longitudinally}, methods L{sphericalNvector.LatLon.isenclosedBy}, 

1334 L{sphericalTrigonometry.LatLon.isenclosedBy} and U{MultiDop 

1335 GeogContainPt<https://GitHub.com/NASA/MultiDop>} (U{Shapiro et.al. 2009, 

1336 JTECH<https://Journals.AMetSoc.org/doi/abs/10.1175/2009JTECHA1256.1>} 

1337 and U{Potvin et al. 2012, JTECH <https://Journals.AMetSoc.org/doi/abs/ 

1338 10.1175/JTECH-D-11-00019.1>}). 

1339 ''' 

1340 try: 

1341 y0, x0 = point.lat, point.lon 

1342 except AttributeError: 

1343 try: 

1344 y0, x0 = map(float, point[:2]) 

1345 except (IndexError, TypeError, ValueError) as x: 

1346 raise _ValueError(point=point, cause=x) 

1347 

1348 if wrap: 

1349 y0, x0 = _Wrap.latlon(y0, x0) 

1350 

1351 def _dxy3(x, x2, y2, Ps): 

1352 dx, x2 = unroll180(x, x2, wrap=not Ps.looped) 

1353 return dx, x2, y2 

1354 

1355 else: 

1356 x0 = _fmod(x0, _360_0) # not x0 % 360! 

1357 x0_180_ = x0 - _180_0 

1358 x0_180 = x0 + _180_0 

1359 

1360 def _dxy3(x1, x, y, unused): # PYCHOK expected 

1361 x = _umod_360(float(x)) 

1362 if x < x0_180_: 

1363 x += _360_0 

1364 elif x >= x0_180: 

1365 x -= _360_0 

1366 return (x - x1), x, y 

1367 

1368 if _MODS.booleans.isBoolean(points): 

1369 return points._encloses(y0, x0, wrap=wrap) 

1370 

1371 Ps = LatLon2PsxyIter(points, loop=1, wrap=wrap) 

1372 p = Ps[0] 

1373 e = m = False 

1374 S = Fsum() 

1375 

1376 _, x1, y1 = _dxy3(x0, p.x, p.y, False) 

1377 for p in Ps.iterate(closed=True): 

1378 dx, x2, y2 = _dxy3(x1, p.x, p.y, Ps) 

1379 # ignore duplicate and near-duplicate pts 

1380 if fabs(dx) > EPS or fabs(y2 - y1) > EPS: 

1381 # determine if polygon edge (x1, y1)..(x2, y2) straddles 

1382 # point (lat, lon) or is on boundary, but do not count 

1383 # edges on boundary as more than one crossing 

1384 if fabs(dx) < 180 and (x1 < x0 <= x2 or x2 < x0 <= x1): 

1385 m = not m 

1386 dy = (x0 - x1) * (y2 - y1) - (y0 - y1) * dx 

1387 if (dy > 0 and dx >= 0) or (dy < 0 and dx <= 0): 

1388 e = not e 

1389 

1390 S += sin(radians(y2)) 

1391 x1, y1 = x2, y2 

1392 

1393 # An odd number of meridian crossings means, the polygon 

1394 # contains a pole. Assume it is the pole on the hemisphere 

1395 # containing the polygon mean point and if the polygon does 

1396 # contain the North Pole, flip the result. 

1397 if m and S.fsum() > 0: 

1398 e = not e 

1399 return e 

1400 

1401 

1402def ispolar(points, wrap=False): 

1403 '''Check whether a polygon encloses a pole. 

1404 

1405 @arg points: The polygon points (C{LatLon}[]). 

1406 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll 

1407 the B{C{points}} (C{bool}). 

1408 

1409 @return: C{True} if the polygon encloses a pole, C{False} 

1410 otherwise. 

1411 

1412 @raise PointsError: Insufficient number of B{C{points}} 

1413 

1414 @raise TypeError: Some B{C{points}} are not C{LatLon} or don't 

1415 have C{bearingTo2}, C{initialBearingTo} 

1416 and C{finalBearingTo} methods. 

1417 ''' 

1418 def _cds(ps, w): # iterate over course deltas 

1419 Ps = PointsIter(ps, loop=2, wrap=w) 

1420 p2, p1 = Ps[0:2] 

1421 b1, _ = _bearingTo2(p2, p1, wrap=False) 

1422 for p2 in Ps.iterate(closed=True): 

1423 if not p2.isequalTo(p1, EPS): 

1424 if w and not Ps.looped: 

1425 p2 = _unrollon(p1, p2) 

1426 b, b2 = _bearingTo2(p1, p2, wrap=False) 

1427 yield wrap180(b - b1) # (b - b1 + 540) % 360 - 180 

1428 yield wrap180(b2 - b) # (b2 - b + 540) % 360 - 180 

1429 p1, b1 = p2, b2 

1430 

1431 # summation of course deltas around pole is 0° rather than normally ±360° 

1432 # <https://blog.Element84.com/determining-if-a-spherical-polygon-contains-a-pole.html> 

1433 s = fsum(_cds(points, wrap)) 

1434 # XXX fix (intermittant) edge crossing pole - eg (85,90), (85,0), (85,-90) 

1435 return fabs(s) < 90 # "zero-ish" 

1436 

1437 

1438def luneOf(lon1, lon2, closed=False, LatLon=LatLon_, **LatLon_kwds): 

1439 '''Generate an ellipsoidal or spherical U{lune 

1440 <https://WikiPedia.org/wiki/Spherical_lune>}-shaped path or polygon. 

1441 

1442 @arg lon1: Left longitude (C{degrees90}). 

1443 @arg lon2: Right longitude (C{degrees90}). 

1444 @kwarg closed: Optionally, close the path (C{bool}). 

1445 @kwarg LatLon: Class to use (L{LatLon_}). 

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

1447 keyword arguments. 

1448 

1449 @return: A tuple of 4 or 5 B{C{LatLon}} instances outlining 

1450 the lune shape. 

1451 

1452 @see: U{Latitude-longitude quadrangle 

1453 <https://www.MathWorks.com/help/map/ref/areaquad.html>}. 

1454 ''' 

1455 t = (LatLon( _0_0, lon1, **LatLon_kwds), 

1456 LatLon( _90_0, lon1, **LatLon_kwds), 

1457 LatLon( _0_0, lon2, **LatLon_kwds), 

1458 LatLon(_N_90_0, lon2, **LatLon_kwds)) 

1459 if closed: 

1460 t += t[:1] 

1461 return t 

1462 

1463 

1464def nearestOn5(point, points, closed=False, wrap=False, adjust=True, 

1465 limit=9, **LatLon_and_kwds): 

1466 '''Locate the point on a path or polygon closest to a reference point. 

1467 

1468 The closest point on each polygon edge is either the nearest of that 

1469 edge's end points or a point in between. 

1470 

1471 @arg point: The reference point (C{LatLon}). 

1472 @arg points: The path or polygon points (C{LatLon}[]). 

1473 @kwarg closed: Optionally, close the path or polygon (C{bool}). 

1474 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the 

1475 B{C{points}} (C{bool}). 

1476 @kwarg adjust: See function L{pygeodesy.equirectangular4} (C{bool}). 

1477 @kwarg limit: See function L{pygeodesy.equirectangular4} (C{degrees}), 

1478 default C{9 degrees} is about C{1,000 Kmeter} (for mean 

1479 spherical earth radius L{R_KM}). 

1480 @kwarg LatLon_and_kwds: Optional, C{B{LatLon}=None} class to use for 

1481 the closest point and additional B{C{LatLon}} keyword 

1482 arguments, ignored if C{B{LatLon} is None} or not given. 

1483 

1484 @return: A L{NearestOn3Tuple}C{(closest, distance, angle)} with the 

1485 {closest} point (B{C{LatLon}}) or if C{B{LatLon} is None}, 

1486 a L{NearestOn5Tuple}C{(lat, lon, distance, angle, height)}. 

1487 The C{distance} is the L{pygeodesy.equirectangular} distance 

1488 between the C{closest} and reference B{C{point}} in C{degrees}. 

1489 The C{angle} from the B{C{point}} to the C{closest} is in 

1490 compass C{degrees}, like function L{pygeodesy.compassAngle}. 

1491 

1492 @raise LimitError: Lat- and/or longitudinal delta exceeds the B{C{limit}}, 

1493 see function L{pygeodesy.equirectangular4}. 

1494 

1495 @raise PointsError: Insufficient number of B{C{points}} 

1496 

1497 @raise TypeError: Some B{C{points}} are not C{LatLon}. 

1498 

1499 @note: Distances are I{approximated} by function L{pygeodesy.equirectangular4}. 

1500 For more accuracy use one of the C{LatLon.nearestOn6} methods. 

1501 

1502 @see: Function L{pygeodesy.degrees2m}. 

1503 ''' 

1504 def _d2yx4(p2, p1, u, alw): 

1505 # w = wrap if (i < (n - 1) or not closed) else False 

1506 # equirectangular4 returns a Distance4Tuple(distance 

1507 # in degrees squared, delta lat, delta lon, p2.lon 

1508 # unroll/wrap'd); the previous p2.lon unroll/wrap'd 

1509 # is also applied to the next edge's p1.lon 

1510 return equirectangular4(p1.lat, p1.lon + u, 

1511 p2.lat, p2.lon, **alw) 

1512 

1513 def _h(p): # get height or default 0 

1514 return _xattr(p, height=0) or 0 

1515 

1516 # 3-D version used in .vector3d._nearestOn2 

1517 # 

1518 # point (x, y) on axis rotated ccw by angle a: 

1519 # x' = x * cos(a) + y * sin(a) 

1520 # y' = y * cos(a) - x * sin(a) 

1521 # 

1522 # distance (w) along and (h) perpendicular to 

1523 # a line thru point (dx, dy) and the origin: 

1524 # d = hypot(dx, dy) 

1525 # w = (x * dx + y * dy) / d 

1526 # h = (y * dx - x * dy) / d 

1527 # 

1528 # closest point on that line thru (dx, dy): 

1529 # xc = dx * w / d 

1530 # yc = dy * w / d 

1531 # or 

1532 # xc = dx * f 

1533 # yc = dy * f 

1534 # with 

1535 # f = w / d 

1536 # or 

1537 # f = (y * dy + x * dx) / hypot2(dx, dy) 

1538 # 

1539 # i.e. no need for sqrt or hypot 

1540 

1541 Ps = PointsIter(points, loop=1, wrap=wrap) 

1542 p1 = c = Ps[0] 

1543 u1 = u = _0_0 

1544 kw = dict(adjust=adjust, limit=limit, wrap=False) 

1545 d, dy, dx, _ = _d2yx4(p1, point, u1, kw) 

1546 for p2 in Ps.iterate(closed=closed): 

1547 # iff wrapped, unroll lon1 (actually previous 

1548 # lon2) like function unroll180/-PI would've 

1549 if wrap: 

1550 kw.update(wrap=not (closed and Ps.looped)) 

1551 d21, y21, x21, u2 = _d2yx4(p2, p1, u1, kw) 

1552 if d21 > EPS: 

1553 # distance point to p1, y01 and x01 negated 

1554 d2, y01, x01, _ = _d2yx4(point, p1, u1, kw) 

1555 if d2 > EPS: 

1556 w2 = y01 * y21 + x01 * x21 

1557 if w2 > 0: 

1558 if w2 < d21: 

1559 # closest is between p1 and p2, use 

1560 # original delta's, not y21 and x21 

1561 f = w2 / d21 

1562 p1 = LatLon_(favg(p1.lat, p2.lat, f=f), 

1563 favg(p1.lon, p2.lon + u2, f=f), 

1564 height=favg(_h(p1), _h(p2), f=f)) 

1565 u1 = _0_0 

1566 else: # p2 is closest 

1567 p1, u1 = p2, u2 

1568 d2, y01, x01, _ = _d2yx4(point, p1, u1, kw) 

1569 if d2 < d: # p1 is closer, y01 and x01 negated 

1570 c, u, d, dy, dx = p1, u1, d2, -y01, -x01 

1571 p1, u1 = p2, u2 

1572 

1573 a = atan2b(dx, dy) # azimuth 

1574 d = hypot( dx, dy) 

1575 h = _h(c) 

1576 n = nameof(point) or nearestOn5.__name__ 

1577 if LatLon_and_kwds: 

1578 LL, kwds = _xkwds_pop2(LatLon_and_kwds, LatLon=None) 

1579 if LL is not None: 

1580 r = LL(c.lat, c.lon + u, **_xkwds(kwds, height=h, name=n)) 

1581 return NearestOn3Tuple(r, d, a, name=n) 

1582 return NearestOn5Tuple(c.lat, c.lon + u, d, a, h, name=n) # PYCHOK expected 

1583 

1584 

1585def perimeterOf(points, closed=False, adjust=True, radius=R_M, wrap=True): 

1586 '''I{Approximate} the perimeter of a path, polygon. or composite. 

1587 

1588 @arg points: The path or polygon points or clips (C{LatLon}[], 

1589 L{BooleanFHP} or L{BooleanGH}). 

1590 @kwarg closed: Optionally, close the path or polygon (C{bool}). 

1591 @kwarg adjust: Adjust the wrapped, unrolled longitudinal delta 

1592 by the cosine of the mean latitude (C{bool}). 

1593 @kwarg radius: Mean earth radius (C{meter}). 

1594 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the 

1595 B{C{points}} (C{bool}). 

1596 

1597 @return: Approximate perimeter (C{meter}, same units as 

1598 B{C{radius}}). 

1599 

1600 @raise PointsError: Insufficient number of B{C{points}} 

1601 

1602 @raise TypeError: Some B{C{points}} are not C{LatLon}. 

1603 

1604 @raise ValueError: Invalid B{C{radius}} or C{B{closed}=False} with 

1605 C{B{points}} a composite. 

1606 

1607 @note: This perimeter is based on the L{pygeodesy.equirectangular4} 

1608 distance approximation and is ill-suited for regions exceeding 

1609 several hundred Km or Miles or with near-polar latitudes. 

1610 

1611 @see: Functions L{sphericalTrigonometry.perimeterOf} and 

1612 L{ellipsoidalKarney.perimeterOf}. 

1613 ''' 

1614 def _degs(ps, c, a, w): # angular edge lengths in degrees 

1615 Ps = LatLon2PsxyIter(ps, loop=1) # wrap=w 

1616 p1, u = Ps[0], _0_0 # previous x2's unroll/wrap 

1617 for p2 in Ps.iterate(closed=c): 

1618 if w and c: 

1619 w = not Ps.looped 

1620 # apply previous x2's unroll/wrap'd to new x1 

1621 _, dy, dx, u = equirectangular4(p1.y, p1.x + u, 

1622 p2.y, p2.x, 

1623 adjust=a, limit=None, 

1624 wrap=w) # PYCHOK non-seq 

1625 yield hypot(dx, dy) 

1626 p1 = p2 

1627 

1628 if _MODS.booleans.isBoolean(points): 

1629 if not closed: 

1630 notImplemented(None, closed=closed, points=_composite_) 

1631 d = points._sum1(perimeterOf, closed=True, adjust=adjust, 

1632 radius=radius, wrap=wrap) 

1633 else: 

1634 d = fsum(_degs(points, closed, adjust, wrap)) 

1635 return degrees2m(d, radius=radius) 

1636 

1637 

1638def quadOf(latS, lonW, latN, lonE, closed=False, LatLon=LatLon_, **LatLon_kwds): 

1639 '''Generate a quadrilateral path or polygon from two points. 

1640 

1641 @arg latS: Souther-nmost latitude (C{degrees90}). 

1642 @arg lonW: Western-most longitude (C{degrees180}). 

1643 @arg latN: Norther-nmost latitude (C{degrees90}). 

1644 @arg lonE: Eastern-most longitude (C{degrees180}). 

1645 @kwarg closed: Optionally, close the path (C{bool}). 

1646 @kwarg LatLon: Class to use (L{LatLon_}). 

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

1648 keyword arguments. 

1649 

1650 @return: Return a tuple of 4 or 5 B{C{LatLon}} instances 

1651 outlining the quadrilateral. 

1652 

1653 @see: Function L{boundsOf}. 

1654 ''' 

1655 t = (LatLon(latS, lonW, **LatLon_kwds), 

1656 LatLon(latN, lonW, **LatLon_kwds), 

1657 LatLon(latN, lonE, **LatLon_kwds), 

1658 LatLon(latS, lonE, **LatLon_kwds)) 

1659 if closed: 

1660 t += t[:1] 

1661 return t 

1662 

1663 

1664__all__ += _ALL_DOCS(_Array2LatLon, _Basequence) 

1665 

1666# **) MIT License 

1667# 

1668# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved. 

1669# 

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

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

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

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

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

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

1676# 

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

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

1679# 

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

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

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

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

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

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

1686# OTHER DEALINGS IN THE SOFTWARE.