Coverage for pygeodesy/iters.py: 97%
203 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-08-23 12:10 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-08-23 12:10 -0400
2# -*- coding: utf-8 -*-
4u'''Iterators with options.
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'''
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
26__all__ = _ALL_LAZY.iters
27__version__ = '23.08.22'
29_items_ = 'items'
30_iterNumpy2len = 1 # adjustable for testing purposes
31_NOTHING = object() # unique
34class _BaseIter(_Named):
35 '''(INTERNAL) Iterator over items with loop-back and de-duplication.
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
51 def __init__(self, items, loop=0, dedup=False, Error=None, name=NN):
52 '''New iterator over an iterable of B{C{items}}.
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}).
61 @raise Error: Invalid B{C{items}} or sufficient number of B{C{items}}.
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)
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_))
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
97 @property_RO
98 def dedup(self):
99 '''Get the de-duplication setting (C{bool}).
100 '''
101 return self._dedup
103 def enumerate(self, closed=False, copies=False, dedup=False):
104 '''Yield all items, each as a 2-tuple C{(index, item)}.
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
113 def __getitem__(self, index):
114 '''Get the item(s) at the given B{C{index}} or C{slice}.
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)
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?
134 def iterate(self, closed=False, copies=False, dedup=False):
135 '''Yield all items, each as C{item}.
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}).
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)
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
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
172 def __len__(self):
173 '''Get the number of items seen so far.
174 '''
175 return self._len
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)
183 @property_RO
184 def looped(self):
185 '''In this C{Iter}ator in loop-back? (C{bool}).
186 '''
187 return self._looped
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)
195# __next__ # NO __next__ AND __iter__ ... see Luciano Ramalho,
196# # "Fluent Python", O'Reilly, 2016 p. 426, 2022 p. 610
198 def next_(self, dedup=False):
199 '''Return the next item.
201 @kwarg dedup: Set de-duplication for loop-back (C{bool}).
202 '''
203 return self._next_dedup() if self._dedup else self._next(dedup)
205 def _next(self, dedup):
206 '''Return the next item, regardless.
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)
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
236class PointsIter(_BaseIter):
237 '''Iterator for C{points} with optional loop-back and copies.
238 '''
239 _base = None
240 _Error = PointsError
242 def __init__(self, points, loop=0, base=None, dedup=False, wrap=False, name=NN):
243 '''New L{PointsIter} iterator.
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}).
255 @raise PointsError: Insufficient number of B{C{points}}.
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_)
261 if base and not (isNumpy2(points) or isTuple2(points)):
262 self._base = base
263 if wrap:
264 self._wrap = True
266 def enumerate(self, closed=False, copies=False): # PYCHOK signature
267 '''Iterate and yield each point as a 2-tuple C{(index, point)}.
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}).
272 @raise PointsError: Insufficient number of B{C{points}} or using
273 C{B{closed}=True} without B{C{loop}}-back.
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
280 def iterate(self, closed=False, copies=False): # PYCHOK signature
281 '''Iterate through all B{C{points}} starting at index C{loop}.
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}).
286 @raise PointsError: Insufficient number of B{C{points}} or using
287 C{B{closed}=True} without B{C{loop}}-back.
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
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_))
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
315 def __init__(self, points, loop=0, base=None, wrap=True, radius=None,
316 dedup=False, name=_latlon_):
317 '''New L{LatLon2PsxyIter} iterator.
319 @note: The C{LatLon} latitude is considered the I{pseudo-y} and
320 longitude the I{pseudo-x} coordinate, like L{LatLon2psxy}.
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}).
334 @raise PointsError: Insufficient number of B{C{points}}.
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)
345 def __getitem__(self, index):
346 '''Get the point(s) at the given B{C{index}} or C{slice}.
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)
356 def enumerate(self, closed=False, copies=False): # PYCHOK signature
357 '''Iterate and yield each point as a 2-tuple C{(index, L{Point3Tuple})}.
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}).
362 @raise PointsError: Insufficient number of B{C{points}} or using
363 C{B{closed}=True} without B{C{loop}}-back.
365 @raise TypeError: Some B{C{points}} are not B{C{base}}-compatible.
366 '''
367 return PointsIter.enumerate(self, closed=closed, copies=copies)
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)}.
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}).
376 @raise PointsError: Insufficient number of B{C{points}} or using
377 C{B{closed}=True} without B{C{loop}}-back.
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)
387 for ll in PointsIter.iterate(self, closed=closed, copies=copies):
388 yield _p3(ll)
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)
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)
407def isNumpy2(obj):
408 '''Check for a B{C{Numpy2LatLon}} points wrapper.
410 @arg obj: The object (any C{type}).
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)
419def isPoints2(obj):
420 '''Check for a B{C{LatLon2psxy}} points wrapper.
422 @arg obj: The object (any C{type}).
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)
431def isTuple2(obj):
432 '''Check for a B{C{Tuple2LatLon}} points wrapper.
434 @arg obj: The object (any).
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)
443def iterNumpy2(obj):
444 '''Iterate over Numpy2 wrappers or other sequences exceeding
445 the threshold.
447 @arg obj: Points array, list, sequence, set, etc. (any).
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
457def iterNumpy2over(n=None):
458 '''Get or set the L{iterNumpy2} threshold.
460 @kwarg n: Optional, new threshold (C{int}).
462 @return: Previous threshold (C{int}).
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
480def points2(points, closed=True, base=None, Error=PointsError):
481 '''Check a path or polygon represented by points.
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}).
491 @return: A L{Points2Tuple}C{(number, points)} with the number
492 of points and the points C{list} or C{tuple}.
494 @raise PointsError: Insufficient number of B{C{points}}.
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_)
502 n, points = len2(points)
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!
512 if n < (3 if closed else 1):
513 raise Error(points=n, txt=_too_(_few_))
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))
519 return Points2Tuple(n, points)
522__all__ += _ALL_DOCS(_BaseIter)
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.