Coverage for pygeodesy/iters.py: 97%

203 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-05-02 14:35 -0400

1 

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

3 

4u'''Iterators with options. 

5 

6Iterator classes L{LatLon2PsxyIter} and L{PointsIter} to iterate 

7over iterables, lists, sets, tuples, etc. with optional loop-back to 

8the initial items, skipping of duplicate items and copying of the 

9iterated items. 

10''' 

11 

12from pygeodesy.basics import islistuple, issubclassof, \ 

13 len2, map2, _passarg 

14# from pygeodesy.constants import _1_0 # from .utily 

15from pygeodesy.errors import _IndexError, LenError, PointsError, \ 

16 _TypeError, _ValueError 

17from pygeodesy.interns import NN, _0_, _composite_, _few_, \ 

18 _latlon_, _points_, _too_ 

19from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS 

20from pygeodesy.named import Fmt, _Named, property_RO 

21from pygeodesy.namedTuples import Point3Tuple, Points2Tuple 

22# from pygeodesy.props import property_RO # from .named 

23# from pygeodesy.streprs import Fmt # from .named 

24from pygeodesy.units import Int, Radius 

25from pygeodesy.utily import degrees2m, _Wrap, _1_0 

26 

27__all__ = _ALL_LAZY.iters 

28__version__ = '23.12.14' 

29 

30_items_ = 'items' 

31_iterNumpy2len = 1 # adjustable for testing purposes 

32_NOTHING = object() # unique 

33 

34 

35class _BaseIter(_Named): 

36 '''(INTERNAL) Iterator over items with loop-back and de-duplication. 

37 

38 @see: Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 418+, 2022 p. 600+ 

39 ''' 

40 _closed = True 

41 _copies = () 

42 _dedup = False 

43 _Error = LenError 

44 _items = None 

45 _len = 0 

46 _loop = () 

47 _looped = False 

48 _name = _items_ 

49 _prev = _NOTHING 

50 _wrap = False 

51 

52 def __init__(self, items, loop=0, dedup=False, Error=None, name=NN): 

53 '''New iterator over an iterable of B{C{items}}. 

54 

55 @arg items: Iterable (any C{type}, except composites). 

56 @kwarg loop: Number of loop-back items, also initial enumerate and 

57 iterate index (non-negative C{int}). 

58 @kwarg dedup: Skip duplicate items (C{bool}). 

59 @kwarg Error: Error to raise (L{LenError}). 

60 @kwarg name: Optional name (C{str}). 

61 

62 @raise Error: Invalid B{C{items}} or sufficient number of B{C{items}}. 

63 

64 @raise TypeError: Composite B{C{items}}. 

65 ''' 

66 if dedup: 

67 self._dedup = True 

68 if issubclassof(Error, Exception): 

69 self._Error = Error 

70 if name: 

71 self.rename(name) 

72 

73 if islistuple(items): # range in Python 2 

74 self._items = items 

75 elif _MODS.booleans.isBoolean(items): 

76 raise _TypeError(points=_composite_) 

77# XXX if hasattr(items, 'next') or hasattr(items, '__length_hint__'): 

78# XXX # handle reversed, iter, etc. items types 

79 self._iter = iter(items) 

80 self._indx = -1 

81 if Int(loop) > 0: 

82 try: 

83 self._loop = tuple(self.next for _ in range(loop)) 

84 if self.loop != loop: 

85 raise RuntimeError # force Error 

86 except (RuntimeError, StopIteration): 

87 raise self._Error(self.name, self.loop, txt=_too_(_few_)) 

88 

89 @property_RO 

90 def copies(self): 

91 '''Get the saved copies, if any (C{tuple} or C{list}) and only I{once}. 

92 ''' 

93 cs = self._copies 

94 if cs: 

95 self._copies = () 

96 return cs 

97 

98 @property_RO 

99 def dedup(self): 

100 '''Get the de-duplication setting (C{bool}). 

101 ''' 

102 return self._dedup 

103 

104 def enumerate(self, closed=False, copies=False, dedup=False): 

105 '''Yield all items, each as a 2-tuple C{(index, item)}. 

106 

107 @kwarg closed: Loop back to the first B{C{point(s)}}. 

108 @kwarg copies: Make a copy of all B{C{items}} (C{bool}). 

109 @kwarg dedup: Set de-duplication in loop-back (C{bool}). 

110 ''' 

111 for item in self.iterate(closed=closed, copies=copies, dedup=dedup): 

112 yield self._indx, item 

113 

114 def __getitem__(self, index): 

115 '''Get the item(s) at the given B{C{index}} or C{slice}. 

116 

117 @raise IndexError: Invalid B{C{index}}, beyond B{C{loop}}. 

118 ''' 

119 t = self._items or self._copies or self._loop 

120 try: # Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 293+, 2022 p. 408+ 

121 if isinstance(index, slice): 

122 return t[index.start:index.stop:index.step] 

123 else: 

124 return t[index] 

125 except IndexError as x: 

126 t = Fmt.SQUARE(self.name, index) 

127 raise _IndexError(str(x), txt=t, cause=x) 

128 

129 def __iter__(self): # PYCHOK no cover 

130 '''Make this iterator C{iterable}. 

131 ''' 

132 # Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 421, 2022 p. 604+ 

133 return self.iterate() # XXX or self? 

134 

135 def iterate(self, closed=False, copies=False, dedup=False): 

136 '''Yield all items, each as C{item}. 

137 

138 @kwarg closed: Loop back to the first B{C{point(s)}}. 

139 @kwarg copies: Make a copy of all B{C{items}} (C{bool}). 

140 @kwarg dedup: Set de-duplication in loop-back (C{bool}). 

141 

142 @raise Error: Using C{B{closed}=True} without B{C{loop}}-back. 

143 ''' 

144 if closed and not self.loop: 

145 raise self._Error(closed=closed, loop=self.loop) 

146 

147 if copies: 

148 if self._items: 

149 self._copies = self._items 

150 self._items = _copy = None 

151 else: 

152 self._copies = list(self._loop) 

153 _copy = self._copies.append 

154 else: # del B{C{items}} reference 

155 self._items = _copy = None 

156 

157 self._closed = closed 

158 self._looped = False 

159 if self._iter: 

160 try: 

161 _next_ = self.next_ 

162 if _copy: 

163 while True: 

164 item = _next_(dedup=dedup) 

165 _copy(item) 

166 yield item 

167 else: 

168 while True: 

169 yield _next_(dedup=dedup) 

170 except StopIteration: 

171 self._iter = () # del self._iter, prevent re-iterate 

172 

173 def __len__(self): 

174 '''Get the number of items seen so far. 

175 ''' 

176 return self._len 

177 

178 @property_RO 

179 def loop(self): 

180 '''Get the B{C{loop}} setting (C{int}), C{0} for non-loop-back. 

181 ''' 

182 return len(self._loop) 

183 

184 @property_RO 

185 def looped(self): 

186 '''In this C{Iter}ator in loop-back? (C{bool}). 

187 ''' 

188 return self._looped 

189 

190 @property_RO 

191 def next(self): 

192 '''Get the next item. 

193 ''' 

194 return self._next_dedup() if self._dedup else self._next(False) 

195 

196# __next__ # NO __next__ AND __iter__ ... see Luciano Ramalho, 

197# # "Fluent Python", O'Reilly, 2016 p. 426, 2022 p. 610 

198 

199 def next_(self, dedup=False): 

200 '''Return the next item. 

201 

202 @kwarg dedup: Set de-duplication for loop-back (C{bool}). 

203 ''' 

204 return self._next_dedup() if self._dedup else self._next(dedup) 

205 

206 def _next(self, dedup): 

207 '''Return the next item, regardless. 

208 

209 @arg dedup: Set de-duplication for loop-back (C{bool}). 

210 ''' 

211 try: 

212 self._indx += 1 

213 self._len = self._indx # max(_len, _indx) 

214 self._prev = item = next(self._iter) 

215 return item 

216 except StopIteration: 

217 pass 

218 if self._closed and self._loop: # loop back 

219 self._dedup = bool(dedup or self._dedup) 

220 self._indx = 0 

221 self._iter = iter(self._loop) 

222 self._loop = () 

223 self._looped = True 

224 return next(self._iter) 

225 

226 def _next_dedup(self): 

227 '''Return the next item, different from the previous one. 

228 ''' 

229 prev = self._prev 

230 item = self._next(True) 

231 if prev is not _NOTHING: 

232 while item == prev: 

233 item = self._next(True) 

234 return item 

235 

236 

237class PointsIter(_BaseIter): 

238 '''Iterator for C{points} with optional loop-back and copies. 

239 ''' 

240 _base = None 

241 _Error = PointsError 

242 

243 def __init__(self, points, loop=0, base=None, dedup=False, wrap=False, name=NN): 

244 '''New L{PointsIter} iterator. 

245 

246 @arg points: C{Iterable} or C{list}, C{sequence}, C{set}, C{tuple}, 

247 etc. (C{point}s). 

248 @kwarg loop: Number of loop-back points, also initial C{enumerate} and 

249 C{iterate} index (non-negative C{int}). 

250 @kwarg base: Optional B{C{points}} instance for type checking (C{any}). 

251 @kwarg dedup: Skip duplicate points (C{bool}). 

252 @kwarg wrap: If C{True}, wrap or I{normalize} the enum-/iterated 

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

254 @kwarg name: Optional name (C{str}). 

255 

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

257 

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

259 ''' 

260 _BaseIter.__init__(self, points, loop=loop, dedup=dedup, name=name or _points_) 

261 

262 if base and not (isNumpy2(points) or isTuple2(points)): 

263 self._base = base 

264 if wrap: 

265 self._wrap = True 

266 

267 def enumerate(self, closed=False, copies=False): # PYCHOK signature 

268 '''Iterate and yield each point as a 2-tuple C{(index, point)}. 

269 

270 @kwarg closed: Loop back to the first B{C{point(s)}}, de-dup'ed (C{bool}). 

271 @kwarg copies: Save a copy of all B{C{points}} (C{bool}). 

272 

273 @raise PointsError: Insufficient number of B{C{points}} or using 

274 C{B{closed}=True} without B{C{loop}}-back. 

275 

276 @raise TypeError: Some B{C{points}} are not B{C{base}}-compatible. 

277 ''' 

278 for p in self.iterate(closed=closed, copies=copies): 

279 yield self._indx, p 

280 

281 def iterate(self, closed=False, copies=False): # PYCHOK signature 

282 '''Iterate through all B{C{points}} starting at index C{loop}. 

283 

284 @kwarg closed: Loop back to the first B{C{point(s)}}, de-dup'ed (C{bool}). 

285 @kwarg copies: Save a copy of all B{C{points}} (C{bool}). 

286 

287 @raise PointsError: Insufficient number of B{C{points}} or using 

288 C{B{closed}=True} without B{C{loop}}-back. 

289 

290 @raise TypeError: Some B{C{points}} are not B{C{base}}-compatible. 

291 ''' 

292 if self._base: 

293 _oth = self._base.others 

294 _fmt = Fmt.SQUARE(points=0).replace 

295 else: 

296 _oth = _fmt = None 

297 

298 n = self.loop if self._iter else 0 

299 _p = _Wrap.point if self._wrap else _passarg # and _Wrap.normal is not None 

300 for p in _BaseIter.iterate(self, closed=closed, copies=copies, dedup=closed): 

301 if _oth: 

302 _oth(p, name=_fmt(_0_, str(self._indx)), up=2) 

303 yield _p(p) 

304 n += 1 

305 if n < (4 if closed else 2): 

306 raise self._Error(self.name, n, txt=_too_(_few_)) 

307 

308 

309class LatLon2PsxyIter(PointsIter): 

310 '''Iterate and convert for C{points} with optional loop-back and copies. 

311 ''' 

312 _deg2m = None 

313 _radius = None # keep degrees 

314 _wrap = True 

315 

316 def __init__(self, points, loop=0, base=None, wrap=True, radius=None, 

317 dedup=False, name=_latlon_): 

318 '''New L{LatLon2PsxyIter} iterator. 

319 

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

321 longitude the I{pseudo-x} coordinate, like L{LatLon2psxy}. 

322 

323 @arg points: C{Iterable} or C{list}, C{sequence}, C{set}, C{tuple}, 

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

325 @kwarg loop: Number of loop-back points, also initial C{enumerate} and 

326 C{iterate} index (non-negative C{int}). 

327 @kwarg base: Optional B{C{points}} instance for type checking (C{any}). 

328 @kwarg wrap: If C{True}, wrap or I{normalize} the enum-/iterated 

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

330 @kwarg radius: Mean earth radius (C{meter}) for conversion from 

331 C{degrees} to C{meter} (or C{radians} if C{B{radius}=1}). 

332 @kwarg dedup: Skip duplicate points (C{bool}). 

333 @kwarg name: Optional name (C{str}). 

334 

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

336 

337 @raise TypeError: Some B{C{points}} are not B{C{base}}-compatible. 

338 ''' 

339 PointsIter.__init__(self, points, loop=loop, base=base, dedup=dedup, name=name) 

340 if not wrap: 

341 self._wrap = False 

342 if radius: 

343 self._radius = r = Radius(radius) 

344 self._deg2m = degrees2m(_1_0, r) 

345 

346 def __getitem__(self, index): 

347 '''Get the point(s) at the given B{C{index}} or C{slice}. 

348 

349 @raise IndexError: Invalid B{C{index}}, beyond B{C{loop}}. 

350 ''' 

351 ll = PointsIter.__getitem__(self, index) 

352 if isinstance(index, slice): 

353 return map2(self._point3Tuple, ll) 

354 else: 

355 return self._point3Tuple(ll) 

356 

357 def enumerate(self, closed=False, copies=False): # PYCHOK signature 

358 '''Iterate and yield each point as a 2-tuple C{(index, L{Point3Tuple})}. 

359 

360 @kwarg closed: Loop back to the first B{C{point(s)}}, de-dup'ed (C{bool}). 

361 @kwarg copies: Save a copy of all B{C{points}} (C{bool}). 

362 

363 @raise PointsError: Insufficient number of B{C{points}} or using 

364 C{B{closed}=True} without B{C{loop}}-back. 

365 

366 @raise TypeError: Some B{C{points}} are not B{C{base}}-compatible. 

367 ''' 

368 return PointsIter.enumerate(self, closed=closed, copies=copies) 

369 

370 def iterate(self, closed=False, copies=False): # PYCHOK signature 

371 '''Iterate the B{C{points}} starting at index B{C{loop}} and 

372 yield each as a L{Point3Tuple}C{(x, y, ll)}. 

373 

374 @kwarg closed: Loop back to the first B{C{point(s)}}, de-dup'ed (C{bool}). 

375 @kwarg copies: Save a copy of all B{C{points}} (C{bool}). 

376 

377 @raise PointsError: Insufficient number of B{C{points}} or using 

378 C{B{closed}=True} without B{C{loop}}-back. 

379 

380 @raise TypeError: Some B{C{points}} are not B{C{base}}-compatible. 

381 ''' 

382 if self._deg2m not in (None, _1_0): 

383 _p3 = self._point3Tuple 

384 else: 

385 def _p3(ll): # PYCHOK redef 

386 return Point3Tuple(ll.lon, ll.lat, ll) 

387 

388 for ll in PointsIter.iterate(self, closed=closed, copies=copies): 

389 yield _p3(ll) 

390 

391 def _point3Tuple(self, ll): 

392 '''(INTERNAL) Create a L{Point3Tuple} for point B{C{ll}}. 

393 ''' 

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

395 d = self._deg2m 

396 if d: # convert degrees 

397 x *= d 

398 y *= d 

399 return Point3Tuple(x, y, ll) 

400 

401 

402def _imdex2(closed, n): # PYCHOK by .clipy 

403 '''(INTERNAL) Return first and second index of C{range(B{n})}. 

404 ''' 

405 return (n-1, 0) if closed else (0, 1) 

406 

407 

408def isNumpy2(obj): 

409 '''Check for a B{C{Numpy2LatLon}} points wrapper. 

410 

411 @arg obj: The object (any C{type}). 

412 

413 @return: C{True} if B{C{obj}} is a B{C{Numpy2LatLon}} 

414 instance, C{False} otherwise. 

415 ''' 

416 # isinstance(self, (Numpy2LatLon, ...)) 

417 return getattr(obj, isNumpy2.__name__, False) 

418 

419 

420def isPoints2(obj): 

421 '''Check for a B{C{LatLon2psxy}} points wrapper. 

422 

423 @arg obj: The object (any C{type}). 

424 

425 @return: C{True} if B{C{obj}} is a B{C{LatLon2psxy}} 

426 instance, C{False} otherwise. 

427 ''' 

428 # isinstance(self, (LatLon2psxy, ...)) 

429 return getattr(obj, isPoints2.__name__, False) 

430 

431 

432def isTuple2(obj): 

433 '''Check for a B{C{Tuple2LatLon}} points wrapper. 

434 

435 @arg obj: The object (any). 

436 

437 @return: C{True} if B{C{obj}} is a B{C{Tuple2LatLon}} 

438 instance, C{False} otherwise. 

439 ''' 

440 # isinstance(self, (Tuple2LatLon, ...)) 

441 return getattr(obj, isTuple2.__name__, False) 

442 

443 

444def iterNumpy2(obj): 

445 '''Iterate over Numpy2 wrappers or other sequences exceeding 

446 the threshold. 

447 

448 @arg obj: Points array, list, sequence, set, etc. (any). 

449 

450 @return: C{True} do, C{False} don't iterate. 

451 ''' 

452 try: 

453 return isNumpy2(obj) or len(obj) > _iterNumpy2len 

454 except TypeError: 

455 return False 

456 

457 

458def iterNumpy2over(n=None): 

459 '''Get or set the L{iterNumpy2} threshold. 

460 

461 @kwarg n: Optional, new threshold (C{int}). 

462 

463 @return: Previous threshold (C{int}). 

464 

465 @raise ValueError: Invalid B{C{n}}. 

466 ''' 

467 global _iterNumpy2len 

468 p = _iterNumpy2len 

469 if n is not None: 

470 try: 

471 i = int(n) 

472 if i > 0: 

473 _iterNumpy2len = i 

474 else: 

475 raise ValueError 

476 except (TypeError, ValueError): 

477 raise _ValueError(n=n) 

478 return p 

479 

480 

481def points2(points, closed=True, base=None, Error=PointsError): 

482 '''Check a path or polygon represented by points. 

483 

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

485 @kwarg closed: Optionally, consider the polygon closed, 

486 ignoring any duplicate or closing final 

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

488 @kwarg base: Optionally, check all B{C{points}} against 

489 this base class, if C{None} don't check. 

490 @kwarg Error: Exception to raise (C{ValueError}). 

491 

492 @return: A L{Points2Tuple}C{(number, points)} with the number 

493 of points and the points C{list} or C{tuple}. 

494 

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

496 

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

498 compatible or composite B{C{points}}. 

499 ''' 

500 if _MODS.booleans.isBoolean(points): 

501 raise Error(points=points, txt=_composite_) 

502 

503 n, points = len2(points) 

504 

505 if closed: 

506 # remove duplicate or closing final points 

507 while n > 1 and points[n-1] in (points[0], points[n-2]): 

508 n -= 1 

509 # XXX following line is unneeded if points 

510 # are always indexed as ... i in range(n) 

511 points = points[:n] # XXX numpy.array slice is a view! 

512 

513 if n < (3 if closed else 1): 

514 raise Error(points=n, txt=_too_(_few_)) 

515 

516 if base and not (isNumpy2(points) or isTuple2(points)): 

517 for i in range(n): 

518 base.others(points[i], name=Fmt.SQUARE(points=i)) 

519 

520 return Points2Tuple(n, points) 

521 

522 

523__all__ += _ALL_DOCS(_BaseIter) 

524 

525# **) MIT License 

526# 

527# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved. 

528# 

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

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

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

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

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

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

535# 

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

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

538# 

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

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

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

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

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

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

545# OTHER DEALINGS IN THE SOFTWARE.