Coverage for pygeodesy/iters.py: 96%

198 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-04-23 16:38 -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 _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, wrap90, wrap180, _1_0 

25 

26__all__ = _ALL_LAZY.iters 

27__version__ = '23.03.30' 

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", page 418+, O'Reilly, 2016. 

38 ''' 

39 _closed = True 

40 _copies = () 

41 _dedup = False 

42 _Error = LenError 

43 _items = None 

44 _len = 0 

45 _loop = () 

46 _name = _items_ 

47 _prev = _NOTHING 

48 

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

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

51 

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

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

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

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

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

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

58 

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

60 

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

62 ''' 

63 if dedup: 

64 self._dedup = True 

65 if issubclassof(Error, Exception): 

66 self._Error = Error 

67 if name: 

68 self.rename(name) 

69 

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

71 self._items = items 

72 elif _MODS.booleans.isBoolean(items): 

73 raise _TypeError(points=_composite_) 

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

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

76 self._iter = iter(items) 

77 self._indx = -1 

78 if Int(loop) > 0: 

79 try: 

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

81 if self.loop != loop: 

82 raise RuntimeError # force Error 

83 except (RuntimeError, StopIteration): 

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

85 

86 @property_RO 

87 def copies(self): 

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

89 ''' 

90 cs = self._copies 

91 if cs: 

92 self._copies = () 

93 return cs 

94 

95 @property_RO 

96 def dedup(self): 

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

98 ''' 

99 return self._dedup 

100 

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

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

103 

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

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

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

107 ''' 

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

109 yield self._indx, item 

110 

111 def __getitem__(self, index): 

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

113 

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

115 ''' 

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

117 try: # Luciano Ramalho, "Fluent Python", page 293+, O'Reilly, 2016. 

118 if isinstance(index, slice): 

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

120 else: 

121 return t[index] 

122 except IndexError as x: 

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

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

125 

126 def __iter__(self): # PYCHOK no cover 

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

128 ''' 

129 # Luciano Ramalho, "Fluent Python", page 421, O'Reilly, 2016. 

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

131 

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

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

134 

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

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

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

138 

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

140 ''' 

141 if closed and not self.loop: 

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

143 

144 if copies: 

145 if self._items: 

146 self._copies = self._items 

147 self._items = copy_ = None 

148 else: 

149 self._copies = list(self._loop) 

150 copy_ = self._copies.append 

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

152 self._items = copy_ = None 

153 

154 self._closed = closed 

155 if self._iter: 

156 try: 

157 next_ = self.next_ 

158 if copy_: 

159 while True: 

160 item = next_(dedup=dedup) 

161 copy_(item) 

162 yield item 

163 else: 

164 while True: 

165 yield next_(dedup=dedup) 

166 except StopIteration: 

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

168 

169 def __len__(self): 

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

171 ''' 

172 return self._len 

173 

174 @property_RO 

175 def loop(self): 

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

177 ''' 

178 return len(self._loop) 

179 

180 @property_RO 

181 def next(self): 

182 '''Get the next item. 

183 ''' 

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

185 

186# __next__ # NO __next__ AND __iter__ ... see Ramalho, page 426 

187 

188 def next_(self, dedup=False): 

189 '''Return the next item. 

190 

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

192 ''' 

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

194 

195 def _next(self, dedup): 

196 '''Return the next item, regardless. 

197 

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

199 ''' 

200 try: 

201 self._indx += 1 

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

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

204 return item 

205 except StopIteration: 

206 pass 

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

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

209 self._indx = 0 

210 self._iter = iter(self._loop) 

211 self._loop = () 

212 return next(self._iter) 

213 

214 def _next_dedup(self): 

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

216 ''' 

217 prev = self._prev 

218 item = self._next(True) 

219 if prev is not _NOTHING: 

220 while item == prev: 

221 item = self._next(True) 

222 return item 

223 

224 

225class PointsIter(_BaseIter): 

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

227 ''' 

228 _base = None 

229 _Error = PointsError 

230 

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

232 '''New L{PointsIter} iterator. 

233 

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

235 etc. (C{point}s). 

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

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

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

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

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

241 

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

243 

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

245 ''' 

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

247 

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

249 self._base = base 

250 

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

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

253 

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

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

256 

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

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

259 

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

261 ''' 

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

263 yield self._indx, p 

264 

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

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

267 

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

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

270 

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

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

273 

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

275 ''' 

276 if self._base: 

277 base_ = self._base.others 

278 fmt_ = Fmt.SQUARE(points=0).replace 

279 else: 

280 base_ = fmt_ = None 

281 

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

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

284 if base_: 

285 base_(p, name=fmt_(_0_, str(self._indx)), up=2) 

286 yield p 

287 n += 1 

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

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

290 

291 

292class LatLon2PsxyIter(PointsIter): 

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

294 ''' 

295 _deg2m = None 

296 _radius = None # keep degrees 

297 _wrap = True 

298 

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

300 dedup=False, name=NN): 

301 '''New L{LatLon2PsxyIter} iterator. 

302 

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

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

305 

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

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

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

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

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

311 @kwarg wrap: Wrap lat- and longitudes (C{bool}). 

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

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

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

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

316 

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

318 

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

320 ''' 

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

322 if not wrap: 

323 self._wrap = False 

324 if radius: 

325 self._radius = r = Radius(radius) 

326 self._deg2m = degrees2m(_1_0, r) 

327 

328 def __getitem__(self, index): 

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

330 

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

332 ''' 

333 ll = PointsIter.__getitem__(self, index) 

334 if isinstance(index, slice): 

335 return map2(self._point3Tuple, ll) 

336 else: 

337 return self._point3Tuple(ll) 

338 

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

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

341 

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

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

344 

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

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

347 

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

349 ''' 

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

351 

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

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

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

355 

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

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

358 

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

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

361 

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

363 ''' 

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

365 _point3Tuple = self._point3Tuple 

366 elif self._wrap: 

367 def _point3Tuple(ll): 

368 return Point3Tuple(wrap180(ll.lon), wrap90(ll.lat), ll) 

369 else: 

370 def _point3Tuple(ll): # PYCHOK redef 

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

372 

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

374 yield _point3Tuple(ll) 

375 

376 def _point3Tuple(self, ll): 

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

378 ''' 

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

380 if self._wrap: 

381 x, y = wrap180(x), wrap90(y) 

382 d = self._deg2m 

383 if d: # convert degrees 

384 x *= d 

385 y *= d 

386 return Point3Tuple(x, y, ll) 

387 

388 

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

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

391 ''' 

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

393 

394 

395def isNumpy2(obj): 

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

397 

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

399 

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

401 instance, C{False} otherwise. 

402 ''' 

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

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

405 

406 

407def isPoints2(obj): 

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

409 

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

411 

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

413 instance, C{False} otherwise. 

414 ''' 

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

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

417 

418 

419def isTuple2(obj): 

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

421 

422 @arg obj: The object (any). 

423 

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

425 instance, C{False} otherwise. 

426 ''' 

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

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

429 

430 

431def iterNumpy2(obj): 

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

433 the threshold. 

434 

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

436 

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

438 ''' 

439 try: 

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

441 except TypeError: 

442 return False 

443 

444 

445def iterNumpy2over(n=None): 

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

447 

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

449 

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

451 

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

453 ''' 

454 global _iterNumpy2len 

455 p = _iterNumpy2len 

456 if n is not None: 

457 try: 

458 i = int(n) 

459 if i > 0: 

460 _iterNumpy2len = i 

461 else: 

462 raise ValueError 

463 except (TypeError, ValueError): 

464 raise _ValueError(n=n) 

465 return p 

466 

467 

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

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

470 

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

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

473 ignoring any duplicate or closing final 

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

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

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

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

478 

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

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

481 

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

483 

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

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

486 ''' 

487 if _MODS.booleans.isBoolean(points): 

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

489 

490 n, points = len2(points) 

491 

492 if closed: 

493 # remove duplicate or closing final points 

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

495 n -= 1 

496 # XXX following line is unneeded if points 

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

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

499 

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

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

502 

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

504 for i in range(n): 

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

506 

507 return Points2Tuple(n, points) 

508 

509 

510__all__ += _ALL_DOCS(_BaseIter) 

511 

512# **) MIT License 

513# 

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

515# 

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

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

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

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

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

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

522# 

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

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

525# 

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

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

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

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

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

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

532# OTHER DEALINGS IN THE SOFTWARE.