1

2# -*- coding: utf-8 -*-

3

4 u'''Iterators with options.

5

6 Iterator classes L{LatLon2PsxyIter} and L{PointsIter} to iterate

7 over iterables, lists, sets, tuples, etc. with optional loop-back to

8 the initial items, skipping of duplicate items and copying of the

9 iterated items.

10'''

11

12 from pygeodesy.basics import _isin, islistuple, issubclassof, \

13 len2, map2, _passarg, typename

14# from pygeodesy.constants import _1_0 # from .utily

15 from pygeodesy.errors import _IndexError, LenError, PointsError, \

16 _TypeError, _ValueError

17# from pygeodesy.internals import _passarg, typename # from .basics

18 from pygeodesy.interns import _0_, _composite_, _few_, _latlon_, \

19 _points_, _too_

20 from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS

21 from pygeodesy.named import _Named, property_RO, Fmt

22 from pygeodesy.namedTuples import Point3Tuple, Points2Tuple

23# from pygeodesy.props import property_RO # from .named

24# from pygeodesy.streprs import Fmt # from .named

25 from pygeodesy.units import Int, Radius

26 from pygeodesy.utily import degrees2m, _Wrap, _1_0

27

28__all__ = _ALL_LAZY.iters

29__version__ = '25.04.14'

30

31_items_ = 'items'

32_iterNumpy2len = 1 # adjustable for testing purposes

33_NOTHING = object() # unique

34

35

36 class _BaseIter(_Named):

37 '''(INTERNAL) Iterator over items with loop-back and de-duplication.

38

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

52

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

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

55

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}).

62

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

64

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)

73

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_))

89

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

98

99 @property_RO

100 def dedup(self):

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

102 '''

103 return self._dedup

104

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

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

107

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

114

115 def __getitem__(self, index):

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

117

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)

129

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?

135

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

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

138

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}).

142

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)

147

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

157

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

173

174 def __len__(self):

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

176 '''

177 return self._len

178

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)

184

185 @property_RO

186 def looped(self):

187 '''In this C{Iter}ator in loop-back? (C{bool}).

188 '''

189 return self._looped

190

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)

196

197# __next__ # NO __next__ AND __iter__ ... see Luciano Ramalho,

198# # "Fluent Python", O'Reilly, 2016 p. 426, 2022 p. 610

199

200 def next_(self, dedup=False):

201 '''Return the next item.

202

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

204 '''

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

206

207 def _next(self, dedup):

208 '''Return the next item, regardless.

209

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)

226

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

236

237

238 class PointsIter(_BaseIter):

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

240 '''

241 _base = None

242 _Error = PointsError

243 _name = _points_

244

245 def __init__(self, points, loop=0, base=None, dedup=False, wrap=False, **name):

246 '''New L{PointsIter} iterator.

247

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}).

257

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

259

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

261 '''

262 _BaseIter.__init__(self, points, loop=loop, dedup=dedup, **name)

263

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

265 self._base = base

266 if wrap:

267 self._wrap = True

268

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

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

271

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}).

274

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

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

277

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

282

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

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

285

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}).

288

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

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

291

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

299

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_))

309

310

311 class 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

318

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

320 dedup=False, **name):

321 '''New L{LatLon2PsxyIter} iterator.

322

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

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

325

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}).

337

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

339

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)

348

349 def __getitem__(self, index):

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

351

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)

359

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

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

362

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}).

365

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

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

368

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

370 '''

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

372

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)}.

376

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}).

379

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

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

382

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

384 '''

385 if not _isin(self._deg2m, None, _1_0):

386 _p3 = self._point3Tuple

387 else:

388 def _p3(ll): # PYCHOK redef

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

390

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

392 yield _p3(ll)

393

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)

403

404

405 def _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)

409

410

411 def isNumpy2(obj):

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

413

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

415

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, typename(isNumpy2), False)

421

422

423 def isPoints2(obj):

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

425

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

427

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, typename(isPoints2), False)

433

434

435 def isTuple2(obj):

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

437

438 @arg obj: The object (any).

439

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, typename(isTuple2), False)

445

446

447 def iterNumpy2(obj):

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

449 the threshold.

450

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

452

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

459

460

461 def iterNumpy2over(n=None):

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

463

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

465

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

467

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

482

483

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

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

486

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}).

494

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

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

497

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

499

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_)

505

506 n, points = len2(points)

507

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!

515

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

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

518

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))

522

523 return Points2Tuple(n, points)

524

525

526__all__ += _ALL_DOCS(_BaseIter)

527

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.