Coverage for pygeodesy/iters.py: 97%
203 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-06 16:50 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-06 16:50 -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, \
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
27__all__ = _ALL_LAZY.iters
28__version__ = '23.12.14'
30_items_ = 'items'
31_iterNumpy2len = 1 # adjustable for testing purposes
32_NOTHING = object() # unique
35class _BaseIter(_Named):
36 '''(INTERNAL) Iterator over items with loop-back and de-duplication.
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
52 def __init__(self, items, loop=0, dedup=False, Error=None, name=NN):
53 '''New iterator over an iterable of B{C{items}}.
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}).
62 @raise Error: Invalid B{C{items}} or sufficient number of B{C{items}}.
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)
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_))
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
98 @property_RO
99 def dedup(self):
100 '''Get the de-duplication setting (C{bool}).
101 '''
102 return self._dedup
104 def enumerate(self, closed=False, copies=False, dedup=False):
105 '''Yield all items, each as a 2-tuple C{(index, item)}.
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
114 def __getitem__(self, index):
115 '''Get the item(s) at the given B{C{index}} or C{slice}.
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)
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?
135 def iterate(self, closed=False, copies=False, dedup=False):
136 '''Yield all items, each as C{item}.
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}).
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)
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
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
173 def __len__(self):
174 '''Get the number of items seen so far.
175 '''
176 return self._len
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)
184 @property_RO
185 def looped(self):
186 '''In this C{Iter}ator in loop-back? (C{bool}).
187 '''
188 return self._looped
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)
196# __next__ # NO __next__ AND __iter__ ... see Luciano Ramalho,
197# # "Fluent Python", O'Reilly, 2016 p. 426, 2022 p. 610
199 def next_(self, dedup=False):
200 '''Return the next item.
202 @kwarg dedup: Set de-duplication for loop-back (C{bool}).
203 '''
204 return self._next_dedup() if self._dedup else self._next(dedup)
206 def _next(self, dedup):
207 '''Return the next item, regardless.
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)
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
237class PointsIter(_BaseIter):
238 '''Iterator for C{points} with optional loop-back and copies.
239 '''
240 _base = None
241 _Error = PointsError
243 def __init__(self, points, loop=0, base=None, dedup=False, wrap=False, name=NN):
244 '''New L{PointsIter} iterator.
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}).
256 @raise PointsError: Insufficient number of B{C{points}}.
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_)
262 if base and not (isNumpy2(points) or isTuple2(points)):
263 self._base = base
264 if wrap:
265 self._wrap = True
267 def enumerate(self, closed=False, copies=False): # PYCHOK signature
268 '''Iterate and yield each point as a 2-tuple C{(index, point)}.
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}).
273 @raise PointsError: Insufficient number of B{C{points}} or using
274 C{B{closed}=True} without B{C{loop}}-back.
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
281 def iterate(self, closed=False, copies=False): # PYCHOK signature
282 '''Iterate through all B{C{points}} starting at index C{loop}.
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}).
287 @raise PointsError: Insufficient number of B{C{points}} or using
288 C{B{closed}=True} without B{C{loop}}-back.
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
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_))
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
316 def __init__(self, points, loop=0, base=None, wrap=True, radius=None,
317 dedup=False, name=_latlon_):
318 '''New L{LatLon2PsxyIter} iterator.
320 @note: The C{LatLon} latitude is considered the I{pseudo-y} and
321 longitude the I{pseudo-x} coordinate, like L{LatLon2psxy}.
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}).
335 @raise PointsError: Insufficient number of B{C{points}}.
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)
346 def __getitem__(self, index):
347 '''Get the point(s) at the given B{C{index}} or C{slice}.
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)
357 def enumerate(self, closed=False, copies=False): # PYCHOK signature
358 '''Iterate and yield each point as a 2-tuple C{(index, L{Point3Tuple})}.
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}).
363 @raise PointsError: Insufficient number of B{C{points}} or using
364 C{B{closed}=True} without B{C{loop}}-back.
366 @raise TypeError: Some B{C{points}} are not B{C{base}}-compatible.
367 '''
368 return PointsIter.enumerate(self, closed=closed, copies=copies)
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)}.
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}).
377 @raise PointsError: Insufficient number of B{C{points}} or using
378 C{B{closed}=True} without B{C{loop}}-back.
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)
388 for ll in PointsIter.iterate(self, closed=closed, copies=copies):
389 yield _p3(ll)
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)
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)
408def isNumpy2(obj):
409 '''Check for a B{C{Numpy2LatLon}} points wrapper.
411 @arg obj: The object (any C{type}).
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)
420def isPoints2(obj):
421 '''Check for a B{C{LatLon2psxy}} points wrapper.
423 @arg obj: The object (any C{type}).
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)
432def isTuple2(obj):
433 '''Check for a B{C{Tuple2LatLon}} points wrapper.
435 @arg obj: The object (any).
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)
444def iterNumpy2(obj):
445 '''Iterate over Numpy2 wrappers or other sequences exceeding
446 the threshold.
448 @arg obj: Points array, list, sequence, set, etc. (any).
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
458def iterNumpy2over(n=None):
459 '''Get or set the L{iterNumpy2} threshold.
461 @kwarg n: Optional, new threshold (C{int}).
463 @return: Previous threshold (C{int}).
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
481def points2(points, closed=True, base=None, Error=PointsError):
482 '''Check a path or polygon represented by points.
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}).
492 @return: A L{Points2Tuple}C{(number, points)} with the number
493 of points and the points C{list} or C{tuple}.
495 @raise PointsError: Insufficient number of B{C{points}}.
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_)
503 n, points = len2(points)
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!
513 if n < (3 if closed else 1):
514 raise Error(points=n, txt=_too_(_few_))
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))
520 return Points2Tuple(n, points)
523__all__ += _ALL_DOCS(_BaseIter)
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.