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, len2, map2

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

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

15 _TypeError, _ValueError

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

17 _latlon_, _points_, _too_

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

19 from pygeodesy.named import Fmt, _Named, property_RO

20 from pygeodesy.namedTuples import Point3Tuple, Points2Tuple

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

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

23 from pygeodesy.units import Int, Radius

24 from pygeodesy.utily import degrees2m, _Wrap, _1_0

25

26__all__ = _ALL_LAZY.iters

27__version__ = '23.08.22'

28

29_items_ = 'items'

30_iterNumpy2len = 1 # adjustable for testing purposes

31_NOTHING = object() # unique

32

33

34 class _BaseIter(_Named):

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

36

37 @see: Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 418+, 2022 p. 600+

38 '''

39 _closed = True

40 _copies = ()

41 _dedup = False

42 _Error = LenError

43 _items = None

44 _len = 0

45 _loop = ()

46 _looped = False

47 _name = _items_

48 _prev = _NOTHING

49 _wrap = False

50

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

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

53

54 @arg items: Iterable (any C{type}, except composites).

55 @kwarg loop: Number of loop-back items, also initial enumerate and

56 iterate index (non-negative C{int}).

57 @kwarg dedup: Skip duplicate items (C{bool}).

58 @kwarg Error: Error to raise (L{LenError}).

59 @kwarg name: Optional name (C{str}).

60

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

62

63 @raise TypeError: Composite B{C{items}}.

64 '''

65 if dedup:

66 self._dedup = True

67 if issubclassof(Error, Exception):

68 self._Error = Error

69 if name:

70 self.rename(name)

71

72 if islistuple(items): # range in Python 2

73 self._items = items

74 elif _MODS.booleans.isBoolean(items):

75 raise _TypeError(points=_composite_)

76# XXX if hasattr(items, 'next') or hasattr(items, '__length_hint__'):

77# XXX # handle reversed, iter, etc. items types

78 self._iter = iter(items)

79 self._indx = -1

80 if Int(loop) > 0:

81 try:

82 self._loop = tuple(self.next for _ in range(loop))

83 if self.loop != loop:

84 raise RuntimeError # force Error

85 except (RuntimeError, StopIteration):

86 raise self._Error(self.name, self.loop, txt=_too_(_few_))

87

88 @property_RO

89 def copies(self):

90 '''Get the saved copies, if any (C{tuple} or C{list}) and only I{once}.

91 '''

92 cs = self._copies

93 if cs:

94 self._copies = ()

95 return cs

96

97 @property_RO

98 def dedup(self):

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

100 '''

101 return self._dedup

102

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

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

105

106 @kwarg closed: Loop back to the first B{C{point(s)}}.

107 @kwarg copies: Make a copy of all B{C{items}} (C{bool}).

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

109 '''

110 for item in self.iterate(closed=closed, copies=copies, dedup=dedup):

111 yield self._indx, item

112

113 def __getitem__(self, index):

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

115

116 @raise IndexError: Invalid B{C{index}}, beyond B{C{loop}}.

117 '''

118 t = self._items or self._copies or self._loop

119 try: # Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 293+, 2022 p. 408+

120 if isinstance(index, slice):

121 return t[index.start:index.stop:index.step]

122 else:

123 return t[index]

124 except IndexError as x:

125 t = Fmt.SQUARE(self.name, index)

126 raise _IndexError(str(x), txt=t, cause=x)

127

128 def __iter__(self): # PYCHOK no cover

129 '''Make this iterator C{iterable}.

130 '''

131 # Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 421, 2022 p. 604+

132 return self.iterate() # XXX or self?

133

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

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

136

137 @kwarg closed: Loop back to the first B{C{point(s)}}.

138 @kwarg copies: Make a copy of all B{C{items}} (C{bool}).

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

140

141 @raise Error: Using C{B{closed}=True} without B{C{loop}}-back.

142 '''

143 if closed and not self.loop:

144 raise self._Error(closed=closed, loop=self.loop)

145

146 if copies:

147 if self._items:

148 self._copies = self._items

149 self._items = _copy = None

150 else:

151 self._copies = list(self._loop)

152 _copy = self._copies.append

153 else: # del B{C{items}} reference

154 self._items = _copy = None

155

156 self._closed = closed

157 self._looped = False

158 if self._iter:

159 try:

160 _next_ = self.next_

161 if _copy:

162 while True:

163 item = _next_(dedup=dedup)

164 _copy(item)

165 yield item

166 else:

167 while True:

168 yield _next_(dedup=dedup)

169 except StopIteration:

170 self._iter = () # del self._iter, prevent re-iterate

171

172 def __len__(self):

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

174 '''

175 return self._len

176

177 @property_RO

178 def loop(self):

179 '''Get the B{C{loop}} setting (C{int}), C{0} for non-loop-back.

180 '''

181 return len(self._loop)

182

183 @property_RO

184 def looped(self):

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

186 '''

187 return self._looped

188

189 @property_RO

190 def next(self):

191 '''Get the next item.

192 '''

193 return self._next_dedup() if self._dedup else self._next(False)

194

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

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

197

198 def next_(self, dedup=False):

199 '''Return the next item.

200

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

202 '''

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

204

205 def _next(self, dedup):

206 '''Return the next item, regardless.

207

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

209 '''

210 try:

211 self._indx += 1

212 self._len = self._indx # max(_len, _indx)

213 self._prev = item = next(self._iter)

214 return item

215 except StopIteration:

216 pass

217 if self._closed and self._loop: # loop back

218 self._dedup = bool(dedup or self._dedup)

219 self._indx = 0

220 self._iter = iter(self._loop)

221 self._loop = ()

222 self._looped = True

223 return next(self._iter)

224

225 def _next_dedup(self):

226 '''Return the next item, different from the previous one.

227 '''

228 prev = self._prev

229 item = self._next(True)

230 if prev is not _NOTHING:

231 while item == prev:

232 item = self._next(True)

233 return item

234

235

236 class PointsIter(_BaseIter):

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

238 '''

239 _base = None

240 _Error = PointsError

241

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

243 '''New L{PointsIter} iterator.

244

245 @arg points: C{Iterable} or C{list}, C{sequence}, C{set}, C{tuple},

246 etc. (C{point}s).

247 @kwarg loop: Number of loop-back points, also initial C{enumerate} and

248 C{iterate} index (non-negative C{int}).

249 @kwarg base: Optional B{C{points}} instance for type checking (C{any}).

250 @kwarg dedup: Skip duplicate points (C{bool}).

251 @kwarg wrap: If C{True}, wrap or I{normalize} the enum-/iterated

252 B{C{points}} (C{bool}).

253 @kwarg name: Optional name (C{str}).

254

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

256

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

258 '''

259 _BaseIter.__init__(self, points, loop=loop, dedup=dedup, name=name or _points_)

260

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

262 self._base = base

263 if wrap:

264 self._wrap = True

265

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

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

268

269 @kwarg closed: Loop back to the first B{C{point(s)}}, de-dup'ed (C{bool}).

270 @kwarg copies: Save a copy of all B{C{points}} (C{bool}).

271

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

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

274

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

276 '''

277 for p in self.iterate(closed=closed, copies=copies):

278 yield self._indx, p

279

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

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

282

283 @kwarg closed: Loop back to the first B{C{point(s)}}, de-dup'ed (C{bool}).

284 @kwarg copies: Save a copy of all B{C{points}} (C{bool}).

285

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

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

288

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

290 '''

291 if self._base:

292 _oth = self._base.others

293 _fmt = Fmt.SQUARE(points=0).replace

294 else:

295 _oth = _fmt = None

296

297 w = self._wrap # and _Wrap.normal is not None

298 n = self.loop if self._iter else 0

299 for p in _BaseIter.iterate(self, closed=closed, copies=copies, dedup=closed):

300 if _oth:

301 _oth(p, name=_fmt(_0_, str(self._indx)), up=2)

302 yield _Wrap.point(p) if w else p

303 n += 1

304 if n < (4 if closed else 2):

305 raise self._Error(self.name, n, txt=_too_(_few_))

306

307

308 class LatLon2PsxyIter(PointsIter):

309 '''Iterate and convert for C{points} with optional loop-back and copies.

310 '''

311 _deg2m = None

312 _radius = None # keep degrees

313 _wrap = True

314

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

316 dedup=False, name=_latlon_):

317 '''New L{LatLon2PsxyIter} iterator.

318

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

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

321

322 @arg points: C{Iterable} or C{list}, C{sequence}, C{set}, C{tuple},

323 etc. (C{LatLon}[]).

324 @kwarg loop: Number of loop-back points, also initial C{enumerate} and

325 C{iterate} index (non-negative C{int}).

326 @kwarg base: Optional B{C{points}} instance for type checking (C{any}).

327 @kwarg wrap: If C{True}, wrap or I{normalize} the enum-/iterated

328 B{C{points}} (C{bool}).

329 @kwarg radius: Mean earth radius (C{meter}) for conversion from

330 C{degrees} to C{meter} (or C{radians} if C{B{radius}=1}).

331 @kwarg dedup: Skip duplicate points (C{bool}).

332 @kwarg name: Optional name (C{str}).

333

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

335

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

337 '''

338 PointsIter.__init__(self, points, loop=loop, base=base, dedup=dedup, name=name)

339 if not wrap:

340 self._wrap = False

341 if radius:

342 self._radius = r = Radius(radius)

343 self._deg2m = degrees2m(_1_0, r)

344

345 def __getitem__(self, index):

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

347

348 @raise IndexError: Invalid B{C{index}}, beyond B{C{loop}}.

349 '''

350 ll = PointsIter.__getitem__(self, index)

351 if isinstance(index, slice):

352 return map2(self._point3Tuple, ll)

353 else:

354 return self._point3Tuple(ll)

355

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

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

358

359 @kwarg closed: Loop back to the first B{C{point(s)}}, de-dup'ed (C{bool}).

360 @kwarg copies: Save a copy of all B{C{points}} (C{bool}).

361

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

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

364

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

366 '''

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

368

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

370 '''Iterate the B{C{points}} starting at index B{C{loop}} and

371 yield each as a L{Point3Tuple}C{(x, y, ll)}.

372

373 @kwarg closed: Loop back to the first B{C{point(s)}}, de-dup'ed (C{bool}).

374 @kwarg copies: Save a copy of all B{C{points}} (C{bool}).

375

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

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

378

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

380 '''

381 if self._deg2m not in (None, _1_0):

382 _p3 = self._point3Tuple

383 else:

384 def _p3(ll): # PYCHOK redef

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

386

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

388 yield _p3(ll)

389

390 def _point3Tuple(self, ll):

391 '''(INTERNAL) Create a L{Point3Tuple} for point B{C{ll}}.

392 '''

393 x, y = ll.lon, ll.lat # note, x, y = lon, lat

394 d = self._deg2m

395 if d: # convert degrees

396 x *= d

397 y *= d

398 return Point3Tuple(x, y, ll)

399

400

401 def _imdex2(closed, n): # PYCHOK by .clipy

402 '''(INTERNAL) Return first and second index of C{range(B{n})}.

403 '''

404 return (n-1, 0) if closed else (0, 1)

405

406

407 def isNumpy2(obj):

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

409

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

411

412 @return: C{True} if B{C{obj}} is a B{C{Numpy2LatLon}}

413 instance, C{False} otherwise.

414 '''

415 # isinstance(self, (Numpy2LatLon, ...))

416 return getattr(obj, isNumpy2.__name__, False)

417

418

419 def isPoints2(obj):

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

421

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

423

424 @return: C{True} if B{C{obj}} is a B{C{LatLon2psxy}}

425 instance, C{False} otherwise.

426 '''

427 # isinstance(self, (LatLon2psxy, ...))

428 return getattr(obj, isPoints2.__name__, False)

429

430

431 def isTuple2(obj):

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

433

434 @arg obj: The object (any).

435

436 @return: C{True} if B{C{obj}} is a B{C{Tuple2LatLon}}

437 instance, C{False} otherwise.

438 '''

439 # isinstance(self, (Tuple2LatLon, ...))

440 return getattr(obj, isTuple2.__name__, False)

441

442

443 def iterNumpy2(obj):

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

445 the threshold.

446

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

448

449 @return: C{True} do, C{False} don't iterate.

450 '''

451 try:

452 return isNumpy2(obj) or len(obj) > _iterNumpy2len

453 except TypeError:

454 return False

455

456

457 def iterNumpy2over(n=None):

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

459

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

461

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

463

464 @raise ValueError: Invalid B{C{n}}.

465 '''

466 global _iterNumpy2len

467 p = _iterNumpy2len

468 if n is not None:

469 try:

470 i = int(n)

471 if i > 0:

472 _iterNumpy2len = i

473 else:

474 raise ValueError

475 except (TypeError, ValueError):

476 raise _ValueError(n=n)

477 return p

478

479

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

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

482

483 @arg points: The path or polygon points (C{LatLon}[])

484 @kwarg closed: Optionally, consider the polygon closed,

485 ignoring any duplicate or closing final

486 B{C{points}} (C{bool}).

487 @kwarg base: Optionally, check all B{C{points}} against

488 this base class, if C{None} don't check.

489 @kwarg Error: Exception to raise (C{ValueError}).

490

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

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

493

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

495

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

497 compatible or composite B{C{points}}.

498 '''

499 if _MODS.booleans.isBoolean(points):

500 raise Error(points=points, txt=_composite_)

501

502 n, points = len2(points)

503

504 if closed:

505 # remove duplicate or closing final points

506 while n > 1 and points[n-1] in (points[0], points[n-2]):

507 n -= 1

508 # XXX following line is unneeded if points

509 # are always indexed as ... i in range(n)

510 points = points[:n] # XXX numpy.array slice is a view!

511

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

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

514

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

516 for i in range(n):

517 base.others(points[i], name=Fmt.SQUARE(points=i))

518

519 return Points2Tuple(n, points)

520

521

522__all__ += _ALL_DOCS(_BaseIter)

523

524# **) MIT License

525#

526# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.

527#

528# Permission is hereby granted, free of charge, to any person obtaining a

529# copy of this software and associated documentation files (the "Software"),

530# to deal in the Software without restriction, including without limitation

531# the rights to use, copy, modify, merge, publish, distribute, sublicense,

532# and/or sell copies of the Software, and to permit persons to whom the

533# Software is furnished to do so, subject to the following conditions:

534#

535# The above copyright notice and this permission notice shall be included

536# in all copies or substantial portions of the Software.

537#

538# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS

539# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

540# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL

541# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR

542# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,

543# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR

544# OTHER DEALINGS IN THE SOFTWARE.