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 islistuple, issubclassof, \

13 len2, map2, _passarg

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

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

16 _TypeError, _ValueError

17 from pygeodesy.interns import NN, _0_, _composite_, _few_, \

18 _latlon_, _points_, _too_

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

20 from pygeodesy.named import Fmt, _Named, property_RO

21 from pygeodesy.namedTuples import Point3Tuple, Points2Tuple

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

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

24 from pygeodesy.units import Int, Radius

25 from pygeodesy.utily import degrees2m, _Wrap, _1_0

26

27__all__ = _ALL_LAZY.iters

28__version__ = '23.12.14'

29

30_items_ = 'items'

31_iterNumpy2len = 1 # adjustable for testing purposes

32_NOTHING = object() # unique

33

34

35 class _BaseIter(_Named):

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

37

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

51

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

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

54

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

61

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

63

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)

72

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

88

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

97

98 @property_RO

99 def dedup(self):

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

101 '''

102 return self._dedup

103

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

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

106

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

113

114 def __getitem__(self, index):

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

116

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)

128

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?

134

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

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

137

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

141

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)

146

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

156

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

172

173 def __len__(self):

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

175 '''

176 return self._len

177

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)

183

184 @property_RO

185 def looped(self):

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

187 '''

188 return self._looped

189

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)

195

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

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

198

199 def next_(self, dedup=False):

200 '''Return the next item.

201

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

203 '''

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

205

206 def _next(self, dedup):

207 '''Return the next item, regardless.

208

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)

225

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

235

236

237 class PointsIter(_BaseIter):

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

239 '''

240 _base = None

241 _Error = PointsError

242

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

244 '''New L{PointsIter} iterator.

245

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

255

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

257

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

261

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

263 self._base = base

264 if wrap:

265 self._wrap = True

266

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

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

269

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

272

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

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

275

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

280

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

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

283

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

286

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

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

289

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

297

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

307

308

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

315

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

317 dedup=False, name=_latlon_):

318 '''New L{LatLon2PsxyIter} iterator.

319

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

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

322

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

334

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

336

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)

345

346 def __getitem__(self, index):

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

348

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)

356

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

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

359

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

362

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

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

365

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

367 '''

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

369

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

373

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

376

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

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

379

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)

387

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

389 yield _p3(ll)

390

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)

400

401

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

406

407

408 def isNumpy2(obj):

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

410

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

412

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)

418

419

420 def isPoints2(obj):

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

422

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

424

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)

430

431

432 def isTuple2(obj):

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

434

435 @arg obj: The object (any).

436

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)

442

443

444 def iterNumpy2(obj):

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

446 the threshold.

447

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

449

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

456

457

458 def iterNumpy2over(n=None):

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

460

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

462

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

464

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

479

480

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

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

483

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

491

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

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

494

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

496

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

502

503 n, points = len2(points)

504

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!

512

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

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

515

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

519

520 return Points2Tuple(n, points)

521

522

523__all__ += _ALL_DOCS(_BaseIter)

524

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.