Coverage for pygeodesy/iters.py: 97%
204 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-09 11:05 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-09 11:05 -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, \
13 map2, _passarg
14# from pygeodesy.constants import _1_0 # from .utily
15from pygeodesy.errors import _IndexError, LenError, PointsError, \
16 _TypeError, _ValueError
17# from pygeodesy.internals import _passarg # from .basics
18from pygeodesy.interns import _0_, _composite_, _few_, _latlon_, \
19 _points_, _too_
20from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
21from pygeodesy.named import _Named, property_RO, Fmt
22from pygeodesy.namedTuples import Point3Tuple, Points2Tuple
23# from pygeodesy.props import property_RO # from .named
24# from pygeodesy.streprs import Fmt # from .named
25from pygeodesy.units import Int, Radius
26from pygeodesy.utily import degrees2m, _Wrap, _1_0
28__all__ = _ALL_LAZY.iters
29__version__ = '24.06.09'
31_items_ = 'items'
32_iterNumpy2len = 1 # adjustable for testing purposes
33_NOTHING = object() # unique
36class _BaseIter(_Named):
37 '''(INTERNAL) Iterator over items with loop-back and de-duplication.
39 @see: Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 418+, 2022 p. 600+
40 '''
41 _closed = True
42 _copies = ()
43 _dedup = False
44 _Error = LenError
45 _items = None
46 _len = 0
47 _loop = ()
48 _looped = False
49 _name = _items_
50 _prev = _NOTHING
51 _wrap = False
53 def __init__(self, items, loop=0, dedup=False, Error=None, **name):
54 '''New iterator over an iterable of B{C{items}}.
56 @arg items: Iterable (any C{type}, except composites).
57 @kwarg loop: Number of loop-back items, also initial enumerate and
58 iterate index (non-negative C{int}).
59 @kwarg dedup: Skip duplicate items (C{bool}).
60 @kwarg Error: Error to raise (L{LenError}).
61 @kwarg name: Optional C{B{name}="items"} (C{str}).
63 @raise Error: Invalid B{C{items}} or sufficient number of B{C{items}}.
65 @raise TypeError: Composite B{C{items}}.
66 '''
67 if dedup:
68 self._dedup = True
69 if issubclassof(Error, Exception):
70 self._Error = Error
71 if name:
72 self.rename(name)
74 if islistuple(items): # range in Python 2
75 self._items = items
76 elif _MODS.booleans.isBoolean(items):
77 raise _TypeError(points=_composite_)
78# XXX if hasattr(items, 'next') or hasattr(items, '__length_hint__'):
79# XXX # handle reversed, iter, etc. items types
80 self._iter = iter(items)
81 self._indx = -1
82 if Int(loop) > 0:
83 try:
84 self._loop = tuple(self.next for _ in range(loop))
85 if self.loop != loop:
86 raise RuntimeError() # force Error
87 except (RuntimeError, StopIteration):
88 raise self._Error(self.name, self.loop, txt=_too_(_few_))
90 @property_RO
91 def copies(self):
92 '''Get the saved copies, if any (C{tuple} or C{list}) and only I{once}.
93 '''
94 cs = self._copies
95 if cs:
96 self._copies = ()
97 return cs
99 @property_RO
100 def dedup(self):
101 '''Get the de-duplication setting (C{bool}).
102 '''
103 return self._dedup
105 def enumerate(self, closed=False, copies=False, dedup=False):
106 '''Yield all items, each as a 2-tuple C{(index, item)}.
108 @kwarg closed: Loop back to the first B{C{point(s)}}.
109 @kwarg copies: Make a copy of all B{C{items}} (C{bool}).
110 @kwarg dedup: Set de-duplication in loop-back (C{bool}).
111 '''
112 for item in self.iterate(closed=closed, copies=copies, dedup=dedup):
113 yield self._indx, item
115 def __getitem__(self, index):
116 '''Get the item(s) at the given B{C{index}} or C{slice}.
118 @raise IndexError: Invalid B{C{index}}, beyond B{C{loop}}.
119 '''
120 t = self._items or self._copies or self._loop
121 try: # Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 293+, 2022 p. 408+
122 if isinstance(index, slice):
123 return t[index.start:index.stop:index.step]
124 else:
125 return t[index]
126 except IndexError as x:
127 t = Fmt.SQUARE(self.name, index)
128 raise _IndexError(str(x), txt=t, cause=x)
130 def __iter__(self): # PYCHOK no cover
131 '''Make this iterator C{iterable}.
132 '''
133 # Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 421, 2022 p. 604+
134 return self.iterate() # XXX or self?
136 def iterate(self, closed=False, copies=False, dedup=False):
137 '''Yield all items, each as C{item}.
139 @kwarg closed: Loop back to the first B{C{point(s)}}.
140 @kwarg copies: Make a copy of all B{C{items}} (C{bool}).
141 @kwarg dedup: Set de-duplication in loop-back (C{bool}).
143 @raise Error: Using C{B{closed}=True} without B{C{loop}}-back.
144 '''
145 if closed and not self.loop:
146 raise self._Error(closed=closed, loop=self.loop)
148 if copies:
149 if self._items:
150 self._copies = self._items
151 self._items = _copy = None
152 else:
153 self._copies = list(self._loop)
154 _copy = self._copies.append
155 else: # del B{C{items}} reference
156 self._items = _copy = None
158 self._closed = closed
159 self._looped = False
160 if self._iter:
161 try:
162 _next_ = self.next_
163 if _copy:
164 while True:
165 item = _next_(dedup=dedup)
166 _copy(item)
167 yield item
168 else:
169 while True:
170 yield _next_(dedup=dedup)
171 except StopIteration:
172 self._iter = () # del self._iter, prevent re-iterate
174 def __len__(self):
175 '''Get the number of items seen so far.
176 '''
177 return self._len
179 @property_RO
180 def loop(self):
181 '''Get the B{C{loop}} setting (C{int}), C{0} for non-loop-back.
182 '''
183 return len(self._loop)
185 @property_RO
186 def looped(self):
187 '''In this C{Iter}ator in loop-back? (C{bool}).
188 '''
189 return self._looped
191 @property_RO
192 def next(self):
193 '''Get the next item.
194 '''
195 return self._next_dedup() if self._dedup else self._next(False)
197# __next__ # NO __next__ AND __iter__ ... see Luciano Ramalho,
198# # "Fluent Python", O'Reilly, 2016 p. 426, 2022 p. 610
200 def next_(self, dedup=False):
201 '''Return the next item.
203 @kwarg dedup: Set de-duplication for loop-back (C{bool}).
204 '''
205 return self._next_dedup() if self._dedup else self._next(dedup)
207 def _next(self, dedup):
208 '''Return the next item, regardless.
210 @arg dedup: Set de-duplication for loop-back (C{bool}).
211 '''
212 try:
213 self._indx += 1
214 self._len = self._indx # max(_len, _indx)
215 self._prev = item = next(self._iter)
216 return item
217 except StopIteration:
218 pass
219 if self._closed and self._loop: # loop back
220 self._dedup = bool(dedup or self._dedup)
221 self._indx = 0
222 self._iter = iter(self._loop)
223 self._loop = ()
224 self._looped = True
225 return next(self._iter)
227 def _next_dedup(self):
228 '''Return the next item, different from the previous one.
229 '''
230 prev = self._prev
231 item = self._next(True)
232 if prev is not _NOTHING:
233 while item == prev:
234 item = self._next(True)
235 return item
238class PointsIter(_BaseIter):
239 '''Iterator for C{points} with optional loop-back and copies.
240 '''
241 _base = None
242 _Error = PointsError
243 _name = _points_
245 def __init__(self, points, loop=0, base=None, dedup=False, wrap=False, **name):
246 '''New L{PointsIter} iterator.
248 @arg points: C{Iterable} or C{list}, C{sequence}, C{set}, C{tuple},
249 etc. (C{point}s).
250 @kwarg loop: Number of loop-back points, also initial C{enumerate} and
251 C{iterate} index (non-negative C{int}).
252 @kwarg base: Optional B{C{points}} instance for type checking (C{any}).
253 @kwarg dedup: Skip duplicate points (C{bool}).
254 @kwarg wrap: If C{True}, wrap or I{normalize} the enum-/iterated
255 B{C{points}} (C{bool}).
256 @kwarg name: Optional C{B{name}="points"} (C{str}).
258 @raise PointsError: Insufficient number of B{C{points}}.
260 @raise TypeError: Some B{C{points}} are not B{C{base}}.
261 '''
262 _BaseIter.__init__(self, points, loop=loop, dedup=dedup, **name)
264 if base and not (isNumpy2(points) or isTuple2(points)):
265 self._base = base
266 if wrap:
267 self._wrap = True
269 def enumerate(self, closed=False, copies=False): # PYCHOK signature
270 '''Iterate and yield each point as a 2-tuple C{(index, point)}.
272 @kwarg closed: Loop back to the first B{C{point(s)}}, de-dup'ed (C{bool}).
273 @kwarg copies: Save a copy of all B{C{points}} (C{bool}).
275 @raise PointsError: Insufficient number of B{C{points}} or using
276 C{B{closed}=True} without B{C{loop}}-back.
278 @raise TypeError: Some B{C{points}} are not B{C{base}}-compatible.
279 '''
280 for p in self.iterate(closed=closed, copies=copies):
281 yield self._indx, p
283 def iterate(self, closed=False, copies=False): # PYCHOK signature
284 '''Iterate through all B{C{points}} starting at index C{loop}.
286 @kwarg closed: Loop back to the first B{C{point(s)}}, de-dup'ed (C{bool}).
287 @kwarg copies: Save a copy of all B{C{points}} (C{bool}).
289 @raise PointsError: Insufficient number of B{C{points}} or using
290 C{B{closed}=True} without B{C{loop}}-back.
292 @raise TypeError: Some B{C{points}} are not B{C{base}}-compatible.
293 '''
294 if self._base:
295 _oth = self._base.others
296 _fmt = Fmt.SQUARE(points=0).replace
297 else:
298 _oth = _fmt = None
300 n = self.loop if self._iter else 0
301 _p = _Wrap.point if self._wrap else _passarg # and _Wrap.normal is not None
302 for p in _BaseIter.iterate(self, closed=closed, copies=copies, dedup=closed):
303 if _oth:
304 _oth(p, name=_fmt(_0_, str(self._indx)), up=2)
305 yield _p(p)
306 n += 1
307 if n < (4 if closed else 2):
308 raise self._Error(self.name, n, txt=_too_(_few_))
311class LatLon2PsxyIter(PointsIter):
312 '''Iterate and convert for C{points} with optional loop-back and copies.
313 '''
314 _deg2m = None
315 _name = _latlon_
316 _radius = None # keep degrees
317 _wrap = True
319 def __init__(self, points, loop=0, base=None, wrap=True, radius=None,
320 dedup=False, **name):
321 '''New L{LatLon2PsxyIter} iterator.
323 @note: The C{LatLon} latitude is considered the I{pseudo-y} and
324 longitude the I{pseudo-x} coordinate, like L{LatLon2psxy}.
326 @arg points: C{Iterable} or C{list}, C{sequence}, C{set}, C{tuple},
327 etc. (C{LatLon}[]).
328 @kwarg loop: Number of loop-back points, also initial C{enumerate} and
329 C{iterate} index (non-negative C{int}).
330 @kwarg base: Optional B{C{points}} instance for type checking (C{any}).
331 @kwarg wrap: If C{True}, wrap or I{normalize} the enum-/iterated
332 B{C{points}} (C{bool}).
333 @kwarg radius: Mean earth radius (C{meter}) for conversion from
334 C{degrees} to C{meter} (or C{radians} if C{B{radius}=1}).
335 @kwarg dedup: Skip duplicate points (C{bool}).
336 @kwarg name: Optional C{B{name}="latlon"} (C{str}).
338 @raise PointsError: Insufficient number of B{C{points}}.
340 @raise TypeError: Some B{C{points}} are not B{C{base}}-compatible.
341 '''
342 PointsIter.__init__(self, points, loop=loop, base=base, dedup=dedup, **name)
343 if not wrap:
344 self._wrap = False
345 if radius:
346 self._radius = r = Radius(radius)
347 self._deg2m = degrees2m(_1_0, r)
349 def __getitem__(self, index):
350 '''Get the point(s) at the given B{C{index}} or C{slice}.
352 @raise IndexError: Invalid B{C{index}}, beyond B{C{loop}}.
353 '''
354 ll = PointsIter.__getitem__(self, index)
355 if isinstance(index, slice):
356 return map2(self._point3Tuple, ll)
357 else:
358 return self._point3Tuple(ll)
360 def enumerate(self, closed=False, copies=False): # PYCHOK signature
361 '''Iterate and yield each point as a 2-tuple C{(index, L{Point3Tuple})}.
363 @kwarg closed: Loop back to the first B{C{point(s)}}, de-dup'ed (C{bool}).
364 @kwarg copies: Save a copy of all B{C{points}} (C{bool}).
366 @raise PointsError: Insufficient number of B{C{points}} or using
367 C{B{closed}=True} without B{C{loop}}-back.
369 @raise TypeError: Some B{C{points}} are not B{C{base}}-compatible.
370 '''
371 return PointsIter.enumerate(self, closed=closed, copies=copies)
373 def iterate(self, closed=False, copies=False): # PYCHOK signature
374 '''Iterate the B{C{points}} starting at index B{C{loop}} and
375 yield each as a L{Point3Tuple}C{(x, y, ll)}.
377 @kwarg closed: Loop back to the first B{C{point(s)}}, de-dup'ed (C{bool}).
378 @kwarg copies: Save a copy of all B{C{points}} (C{bool}).
380 @raise PointsError: Insufficient number of B{C{points}} or using
381 C{B{closed}=True} without B{C{loop}}-back.
383 @raise TypeError: Some B{C{points}} are not B{C{base}}-compatible.
384 '''
385 if self._deg2m not in (None, _1_0):
386 _p3 = self._point3Tuple
387 else:
388 def _p3(ll): # PYCHOK redef
389 return Point3Tuple(ll.lon, ll.lat, ll)
391 for ll in PointsIter.iterate(self, closed=closed, copies=copies):
392 yield _p3(ll)
394 def _point3Tuple(self, ll):
395 '''(INTERNAL) Create a L{Point3Tuple} for point B{C{ll}}.
396 '''
397 x, y = ll.lon, ll.lat # note, x, y = lon, lat
398 d = self._deg2m
399 if d: # convert degrees
400 x *= d
401 y *= d
402 return Point3Tuple(x, y, ll)
405def _imdex2(closed, n): # PYCHOK by .clipy
406 '''(INTERNAL) Return first and second index of C{range(B{n})}.
407 '''
408 return (n-1, 0) if closed else (0, 1)
411def isNumpy2(obj):
412 '''Check for a B{C{Numpy2LatLon}} points wrapper.
414 @arg obj: The object (any C{type}).
416 @return: C{True} if B{C{obj}} is a B{C{Numpy2LatLon}}
417 instance, C{False} otherwise.
418 '''
419 # isinstance(self, (Numpy2LatLon, ...))
420 return getattr(obj, isNumpy2.__name__, False)
423def isPoints2(obj):
424 '''Check for a B{C{LatLon2psxy}} points wrapper.
426 @arg obj: The object (any C{type}).
428 @return: C{True} if B{C{obj}} is a B{C{LatLon2psxy}}
429 instance, C{False} otherwise.
430 '''
431 # isinstance(self, (LatLon2psxy, ...))
432 return getattr(obj, isPoints2.__name__, False)
435def isTuple2(obj):
436 '''Check for a B{C{Tuple2LatLon}} points wrapper.
438 @arg obj: The object (any).
440 @return: C{True} if B{C{obj}} is a B{C{Tuple2LatLon}}
441 instance, C{False} otherwise.
442 '''
443 # isinstance(self, (Tuple2LatLon, ...))
444 return getattr(obj, isTuple2.__name__, False)
447def iterNumpy2(obj):
448 '''Iterate over Numpy2 wrappers or other sequences exceeding
449 the threshold.
451 @arg obj: Points array, list, sequence, set, etc. (any).
453 @return: C{True} do, C{False} don't iterate.
454 '''
455 try:
456 return isNumpy2(obj) or len(obj) > _iterNumpy2len
457 except TypeError:
458 return False
461def iterNumpy2over(n=None):
462 '''Get or set the L{iterNumpy2} threshold.
464 @kwarg n: Optional, new threshold (C{int}).
466 @return: Previous threshold (C{int}).
468 @raise ValueError: Invalid B{C{n}}.
469 '''
470 global _iterNumpy2len
471 p = _iterNumpy2len
472 if n is not None:
473 try:
474 i = int(n)
475 if i > 0:
476 _iterNumpy2len = i
477 else:
478 raise ValueError
479 except (TypeError, ValueError):
480 raise _ValueError(n=n)
481 return p
484def points2(points, closed=True, base=None, Error=PointsError):
485 '''Check a path or polygon represented by points.
487 @arg points: The path or polygon points (C{LatLon}[])
488 @kwarg closed: Optionally, consider the polygon closed,
489 ignoring any duplicate or closing final
490 B{C{points}} (C{bool}).
491 @kwarg base: Optionally, check all B{C{points}} against
492 this base class, if C{None} don't check.
493 @kwarg Error: Exception to raise (C{ValueError}).
495 @return: A L{Points2Tuple}C{(number, points)} with the number
496 of points and the points C{list} or C{tuple}.
498 @raise PointsError: Insufficient number of B{C{points}}.
500 @raise TypeError: Some B{C{points}} are not B{C{base}}
501 compatible or composite B{C{points}}.
502 '''
503 if _MODS.booleans.isBoolean(points):
504 raise Error(points=points, txt=_composite_)
506 n, points = len2(points)
508 if closed:
509 # remove duplicate or closing final points
510 while n > 1 and points[n-1] in (points[0], points[n-2]):
511 n -= 1
512 # XXX following line is unneeded if points
513 # are always indexed as ... i in range(n)
514 points = points[:n] # XXX numpy.array slice is a view!
516 if n < (3 if closed else 1):
517 raise Error(points=n, txt=_too_(_few_))
519 if base and not (isNumpy2(points) or isTuple2(points)):
520 for i in range(n):
521 base.others(points[i], name=Fmt.SQUARE(points=i))
523 return Points2Tuple(n, points)
526__all__ += _ALL_DOCS(_BaseIter)
528# **) MIT License
529#
530# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
531#
532# Permission is hereby granted, free of charge, to any person obtaining a
533# copy of this software and associated documentation files (the "Software"),
534# to deal in the Software without restriction, including without limitation
535# the rights to use, copy, modify, merge, publish, distribute, sublicense,
536# and/or sell copies of the Software, and to permit persons to whom the
537# Software is furnished to do so, subject to the following conditions:
538#
539# The above copyright notice and this permission notice shall be included
540# in all copies or substantial portions of the Software.
541#
542# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
543# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
544# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
545# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
546# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
547# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
548# OTHER DEALINGS IN THE SOFTWARE.