Coverage for pygeodesy/iters.py: 96%
198 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-04-05 15:46 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-04-05 15:46 -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 _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
26__all__ = _ALL_LAZY.iters
27__version__ = '23.03.30'
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", 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
49 def __init__(self, items, loop=0, dedup=False, Error=None, name=NN):
50 '''New iterator over an iterable of B{C{items}}.
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}).
59 @raise Error: Invalid B{C{items}} or sufficient number of B{C{items}}.
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)
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_))
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
95 @property_RO
96 def dedup(self):
97 '''Get the de-duplication setting (C{bool}).
98 '''
99 return self._dedup
101 def enumerate(self, closed=False, copies=False, dedup=False):
102 '''Yield all items, each as a 2-tuple C{(index, item)}.
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
111 def __getitem__(self, index):
112 '''Get the item(s) at the given B{C{index}} or C{slice}.
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)
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?
132 def iterate(self, closed=False, copies=False, dedup=False):
133 '''Yield all items, each as C{item}.
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}).
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)
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
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
169 def __len__(self):
170 '''Get the number of items seen so far.
171 '''
172 return self._len
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)
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)
186# __next__ # NO __next__ AND __iter__ ... see Ramalho, page 426
188 def next_(self, dedup=False):
189 '''Return the next item.
191 @kwarg dedup: Set de-duplication for loop-back (C{bool}).
192 '''
193 return self._next_dedup() if self._dedup else self._next(dedup)
195 def _next(self, dedup):
196 '''Return the next item, regardless.
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)
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
225class PointsIter(_BaseIter):
226 '''Iterator for C{points} with optional loop-back and copies.
227 '''
228 _base = None
229 _Error = PointsError
231 def __init__(self, points, loop=0, base=None, dedup=False, name=NN):
232 '''New L{PointsIter} iterator.
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}).
242 @raise PointsError: Insufficient number of B{C{points}}.
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_)
248 if base and not (isNumpy2(points) or isTuple2(points)):
249 self._base = base
251 def enumerate(self, closed=False, copies=False): # PYCHOK signature
252 '''Iterate and yield each point as a 2-tuple C{(index, point)}.
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}).
257 @raise PointsError: Insufficient number of B{C{points}} or using
258 C{B{closed}=True} without B{C{loop}}-back.
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
265 def iterate(self, closed=False, copies=False): # PYCHOK signature
266 '''Iterate through all B{C{points}} starting at index C{loop}.
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}).
271 @raise PointsError: Insufficient number of B{C{points}} or using
272 C{B{closed}=True} without B{C{loop}}-back.
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
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_))
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
299 def __init__(self, points, loop=0, base=None, wrap=True, radius=None,
300 dedup=False, name=NN):
301 '''New L{LatLon2PsxyIter} iterator.
303 @note: The C{LatLon} latitude is considered the I{pseudo-y} and
304 longitude the I{pseudo-x} coordinate, like L{LatLon2psxy}.
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}).
317 @raise PointsError: Insufficient number of B{C{points}}.
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)
328 def __getitem__(self, index):
329 '''Get the point(s) at the given B{C{index}} or C{slice}.
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)
339 def enumerate(self, closed=False, copies=False): # PYCHOK signature
340 '''Iterate and yield each point as a 2-tuple C{(index, L{Point3Tuple})}.
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}).
345 @raise PointsError: Insufficient number of B{C{points}} or using
346 C{B{closed}=True} without B{C{loop}}-back.
348 @raise TypeError: Some B{C{points}} are not B{C{base}}-compatible.
349 '''
350 return PointsIter.enumerate(self, closed=closed, copies=copies)
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)}.
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}).
359 @raise PointsError: Insufficient number of B{C{points}} or using
360 C{B{closed}=True} without B{C{loop}}-back.
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)
373 for ll in PointsIter.iterate(self, closed=closed, copies=copies):
374 yield _point3Tuple(ll)
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)
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)
395def isNumpy2(obj):
396 '''Check for a B{C{Numpy2LatLon}} points wrapper.
398 @arg obj: The object (any C{type}).
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)
407def isPoints2(obj):
408 '''Check for a B{C{LatLon2psxy}} points wrapper.
410 @arg obj: The object (any C{type}).
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)
419def isTuple2(obj):
420 '''Check for a B{C{Tuple2LatLon}} points wrapper.
422 @arg obj: The object (any).
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)
431def iterNumpy2(obj):
432 '''Iterate over Numpy2 wrappers or other sequences exceeding
433 the threshold.
435 @arg obj: Points array, list, sequence, set, etc. (any).
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
445def iterNumpy2over(n=None):
446 '''Get or set the L{iterNumpy2} threshold.
448 @kwarg n: Optional, new threshold (C{int}).
450 @return: Previous threshold (C{int}).
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
468def points2(points, closed=True, base=None, Error=PointsError):
469 '''Check a path or polygon represented by points.
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}).
479 @return: A L{Points2Tuple}C{(number, points)} with the number
480 of points and the points C{list} or C{tuple}.
482 @raise PointsError: Insufficient number of B{C{points}}.
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_)
490 n, points = len2(points)
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!
500 if n < (3 if closed else 1):
501 raise Error(points=n, txt=_too_(_few_))
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))
507 return Points2Tuple(n, points)
510__all__ += _ALL_DOCS(_BaseIter)
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.