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 _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, wrap90, wrap180, _1_0

25

26__all__ = _ALL_LAZY.iters

27__version__ = '23.03.30'

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", 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

48

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

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

51

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

58

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

60

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)

69

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

85

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

94

95 @property_RO

96 def dedup(self):

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

98 '''

99 return self._dedup

100

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

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

103

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

110

111 def __getitem__(self, index):

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

113

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)

125

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?

131

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

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

134

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

138

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)

143

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

153

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

168

169 def __len__(self):

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

171 '''

172 return self._len

173

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)

179

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)

185

186# __next__ # NO __next__ AND __iter__ ... see Ramalho, page 426

187

188 def next_(self, dedup=False):

189 '''Return the next item.

190

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

192 '''

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

194

195 def _next(self, dedup):

196 '''Return the next item, regardless.

197

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)

213

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

223

224

225 class PointsIter(_BaseIter):

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

227 '''

228 _base = None

229 _Error = PointsError

230

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

232 '''New L{PointsIter} iterator.

233

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

241

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

243

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

247

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

249 self._base = base

250

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

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

253

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

256

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

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

259

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

264

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

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

267

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

270

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

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

273

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

281

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

290

291

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

298

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

300 dedup=False, name=NN):

301 '''New L{LatLon2PsxyIter} iterator.

302

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

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

305

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

316

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

318

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)

327

328 def __getitem__(self, index):

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

330

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)

338

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

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

341

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

344

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

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

347

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

349 '''

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

351

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

355

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

358

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

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

361

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)

372

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

374 yield _point3Tuple(ll)

375

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)

387

388

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

393

394

395 def isNumpy2(obj):

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

397

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

399

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)

405

406

407 def isPoints2(obj):

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

409

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

411

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)

417

418

419 def isTuple2(obj):

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

421

422 @arg obj: The object (any).

423

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)

429

430

431 def iterNumpy2(obj):

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

433 the threshold.

434

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

436

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

443

444

445 def iterNumpy2over(n=None):

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

447

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

449

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

451

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

466

467

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

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

470

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

478

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

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

481

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

483

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

489

490 n, points = len2(points)

491

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!

499

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

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

502

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

506

507 return Points2Tuple(n, points)

508

509

510__all__ += _ALL_DOCS(_BaseIter)

511

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.