Coverage for pygeodesy/iters.py: 97%

203 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-10-04 14:05 -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, len2, map2 

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

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

15 _TypeError, _ValueError 

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

17 _latlon_, _points_, _too_ 

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

19from pygeodesy.named import Fmt, _Named, property_RO 

20from pygeodesy.namedTuples import Point3Tuple, Points2Tuple 

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

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

23from pygeodesy.units import Int, Radius 

24from pygeodesy.utily import degrees2m, _Wrap, _1_0 

25 

26__all__ = _ALL_LAZY.iters 

27__version__ = '23.08.22' 

28 

29_items_ = 'items' 

30_iterNumpy2len = 1 # adjustable for testing purposes 

31_NOTHING = object() # unique 

32 

33 

34class _BaseIter(_Named): 

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

36 

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

38 ''' 

39 _closed = True 

40 _copies = () 

41 _dedup = False 

42 _Error = LenError 

43 _items = None 

44 _len = 0 

45 _loop = () 

46 _looped = False 

47 _name = _items_ 

48 _prev = _NOTHING 

49 _wrap = False 

50 

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

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

53 

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

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

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

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

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

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

60 

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

62 

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

64 ''' 

65 if dedup: 

66 self._dedup = True 

67 if issubclassof(Error, Exception): 

68 self._Error = Error 

69 if name: 

70 self.rename(name) 

71 

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

73 self._items = items 

74 elif _MODS.booleans.isBoolean(items): 

75 raise _TypeError(points=_composite_) 

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

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

78 self._iter = iter(items) 

79 self._indx = -1 

80 if Int(loop) > 0: 

81 try: 

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

83 if self.loop != loop: 

84 raise RuntimeError # force Error 

85 except (RuntimeError, StopIteration): 

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

87 

88 @property_RO 

89 def copies(self): 

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

91 ''' 

92 cs = self._copies 

93 if cs: 

94 self._copies = () 

95 return cs 

96 

97 @property_RO 

98 def dedup(self): 

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

100 ''' 

101 return self._dedup 

102 

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

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

105 

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

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

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

109 ''' 

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

111 yield self._indx, item 

112 

113 def __getitem__(self, index): 

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

115 

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

117 ''' 

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

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

120 if isinstance(index, slice): 

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

122 else: 

123 return t[index] 

124 except IndexError as x: 

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

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

127 

128 def __iter__(self): # PYCHOK no cover 

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

130 ''' 

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

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

133 

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

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

136 

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

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

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

140 

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

142 ''' 

143 if closed and not self.loop: 

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

145 

146 if copies: 

147 if self._items: 

148 self._copies = self._items 

149 self._items = _copy = None 

150 else: 

151 self._copies = list(self._loop) 

152 _copy = self._copies.append 

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

154 self._items = _copy = None 

155 

156 self._closed = closed 

157 self._looped = False 

158 if self._iter: 

159 try: 

160 _next_ = self.next_ 

161 if _copy: 

162 while True: 

163 item = _next_(dedup=dedup) 

164 _copy(item) 

165 yield item 

166 else: 

167 while True: 

168 yield _next_(dedup=dedup) 

169 except StopIteration: 

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

171 

172 def __len__(self): 

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

174 ''' 

175 return self._len 

176 

177 @property_RO 

178 def loop(self): 

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

180 ''' 

181 return len(self._loop) 

182 

183 @property_RO 

184 def looped(self): 

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

186 ''' 

187 return self._looped 

188 

189 @property_RO 

190 def next(self): 

191 '''Get the next item. 

192 ''' 

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

194 

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

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

197 

198 def next_(self, dedup=False): 

199 '''Return the next item. 

200 

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

202 ''' 

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

204 

205 def _next(self, dedup): 

206 '''Return the next item, regardless. 

207 

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

209 ''' 

210 try: 

211 self._indx += 1 

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

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

214 return item 

215 except StopIteration: 

216 pass 

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

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

219 self._indx = 0 

220 self._iter = iter(self._loop) 

221 self._loop = () 

222 self._looped = True 

223 return next(self._iter) 

224 

225 def _next_dedup(self): 

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

227 ''' 

228 prev = self._prev 

229 item = self._next(True) 

230 if prev is not _NOTHING: 

231 while item == prev: 

232 item = self._next(True) 

233 return item 

234 

235 

236class PointsIter(_BaseIter): 

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

238 ''' 

239 _base = None 

240 _Error = PointsError 

241 

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

243 '''New L{PointsIter} iterator. 

244 

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

246 etc. (C{point}s). 

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

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

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

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

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

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

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

254 

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

256 

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

258 ''' 

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

260 

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

262 self._base = base 

263 if wrap: 

264 self._wrap = True 

265 

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

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

268 

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

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

271 

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

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

274 

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

276 ''' 

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

278 yield self._indx, p 

279 

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

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

282 

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

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

285 

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

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

288 

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

290 ''' 

291 if self._base: 

292 _oth = self._base.others 

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

294 else: 

295 _oth = _fmt = None 

296 

297 w = self._wrap # and _Wrap.normal is not None 

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

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

300 if _oth: 

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

302 yield _Wrap.point(p) if w else p 

303 n += 1 

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

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

306 

307 

308class LatLon2PsxyIter(PointsIter): 

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

310 ''' 

311 _deg2m = None 

312 _radius = None # keep degrees 

313 _wrap = True 

314 

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

316 dedup=False, name=_latlon_): 

317 '''New L{LatLon2PsxyIter} iterator. 

318 

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

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

321 

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

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

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

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

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

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

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

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

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

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

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

333 

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

335 

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

337 ''' 

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

339 if not wrap: 

340 self._wrap = False 

341 if radius: 

342 self._radius = r = Radius(radius) 

343 self._deg2m = degrees2m(_1_0, r) 

344 

345 def __getitem__(self, index): 

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

347 

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

349 ''' 

350 ll = PointsIter.__getitem__(self, index) 

351 if isinstance(index, slice): 

352 return map2(self._point3Tuple, ll) 

353 else: 

354 return self._point3Tuple(ll) 

355 

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

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

358 

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

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

361 

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

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

364 

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

366 ''' 

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

368 

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

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

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

372 

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

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

375 

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

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

378 

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

380 ''' 

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

382 _p3 = self._point3Tuple 

383 else: 

384 def _p3(ll): # PYCHOK redef 

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

386 

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

388 yield _p3(ll) 

389 

390 def _point3Tuple(self, ll): 

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

392 ''' 

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

394 d = self._deg2m 

395 if d: # convert degrees 

396 x *= d 

397 y *= d 

398 return Point3Tuple(x, y, ll) 

399 

400 

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

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

403 ''' 

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

405 

406 

407def isNumpy2(obj): 

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

409 

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

411 

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

413 instance, C{False} otherwise. 

414 ''' 

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

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

417 

418 

419def isPoints2(obj): 

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

421 

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

423 

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

425 instance, C{False} otherwise. 

426 ''' 

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

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

429 

430 

431def isTuple2(obj): 

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

433 

434 @arg obj: The object (any). 

435 

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

437 instance, C{False} otherwise. 

438 ''' 

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

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

441 

442 

443def iterNumpy2(obj): 

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

445 the threshold. 

446 

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

448 

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

450 ''' 

451 try: 

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

453 except TypeError: 

454 return False 

455 

456 

457def iterNumpy2over(n=None): 

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

459 

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

461 

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

463 

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

465 ''' 

466 global _iterNumpy2len 

467 p = _iterNumpy2len 

468 if n is not None: 

469 try: 

470 i = int(n) 

471 if i > 0: 

472 _iterNumpy2len = i 

473 else: 

474 raise ValueError 

475 except (TypeError, ValueError): 

476 raise _ValueError(n=n) 

477 return p 

478 

479 

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

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

482 

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

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

485 ignoring any duplicate or closing final 

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

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

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

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

490 

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

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

493 

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

495 

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

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

498 ''' 

499 if _MODS.booleans.isBoolean(points): 

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

501 

502 n, points = len2(points) 

503 

504 if closed: 

505 # remove duplicate or closing final points 

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

507 n -= 1 

508 # XXX following line is unneeded if points 

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

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

511 

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

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

514 

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

516 for i in range(n): 

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

518 

519 return Points2Tuple(n, points) 

520 

521 

522__all__ += _ALL_DOCS(_BaseIter) 

523 

524# **) MIT License 

525# 

526# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved. 

527# 

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

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

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

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

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

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

534# 

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

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

537# 

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

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

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

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

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

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

544# OTHER DEALINGS IN THE SOFTWARE.