Coverage for pygeodesy/booleans.py: 95%

1

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

3

4 u'''I{Boolean} operations on I{composite} polygons and I{clip}s.

5

6 Classes L{BooleanFHP} and L{BooleanGH} are I{composites} and

7 provide I{boolean} operations C{intersection}, C{difference},

8 C{reverse-difference}, C{sum} and C{union}.

9

10@note: A I{clip} is defined as a single, usually closed polygon,

11 a I{composite} is a collection of one or more I{clip}s.

12

13@see: U{Forster-Hormann-Popa<https://www.ScienceDirect.com/science/

14 article/pii/S259014861930007X>} and U{Greiner-Hormann

15 <http://www.Inf.USI.CH/hormann/papers/Greiner.1998.ECO.pdf>}.

16'''

17# make sure int/int division yields float quotient, see .basics

18 from __future__ import division as _; del _ # noqa: E702 ;

19

20 from pygeodesy.basics import _isin, isodd, issubclassof, map2, \

21 _xscalar, typename

22 from pygeodesy.constants import EPS, EPS2, INT0, _0_0, _0_5, _1_0

23 from pygeodesy.errors import ClipError, _IsnotError, _TypeError, \

24 _ValueError, _xattr, _xkwds_get, _xkwds_pop2

25 from pygeodesy.fmath import favg, Fdot_, fdot_, hypot, hypot2

26# from pygeodesy.fsums import fsum1 # _MODS

27# from pygeodesy.internals import typename # from .basics

28 from pygeodesy.interns import NN, _BANG_, _clipid_, _COMMASPACE_, \

29 _composite_, _DOT_, _duplicate_, _e_, \

30 _ELLIPSIS_, _few_, _height_, _lat_, _LatLon_, \

31 _lon_, _not_, _points_, _SPACE_, _too_, _X_, \

32 _x_, _B_, _d_, _R_ # PYCHOK used!

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

34 from pygeodesy.latlonBase import LatLonBase, \

35 LatLon2Tuple, Property_RO, property_RO

36 from pygeodesy.named import _name__, _Named, _NotImplemented, \

37 Fmt, pairs, unstr

38# from pygeodesy.namedTuples import LatLon2Tupe # from .latlonBase

39# from pygeodesy.points import boundsOf # _MODS

40# from pygeodesy.props import Property_RO, property_RO # from .latlonBase

41# from pygeodesy.streprs import Fmt, pairs, unstr # from .named

42 from pygeodesy.units import Height, HeightX

43 from pygeodesy.utily import fabs, _unrollon, _Wrap

44

45# from math import fabs # from .utily

46

47 __all__ = _ALL_LAZY.booleans

48 __version__ = '25.10.30'

49

50 _0EPS = EPS # near-zero, positive

51 _EPS0 = -EPS # near-zero, negative

52 _1EPS = _1_0 + EPS # near-one, over

53 _EPS1 = _1_0 - EPS # near-one, under

54 _10EPS = EPS * 10 # see ._2Abs, ._10eps

55

56 _alpha_ = 'alpha'

57 _boolean_ = 'boolean'

58 _case_ = 'case'

59 _corners_ = 'corners'

60 _open_ = 'open'

61

62

63 def _Cnum(txt, enum): # PYCHOK unused

64 return txt # NN(txt, _TILDE_, enum)

65

66

67 class _L(object): # Intersection labels

68 CROSSING = _Cnum(_X_, 1) # C++ enum

69 CROSSING_D = _Cnum(_X_ + _d_, 8)

70 CROSSINGs = (CROSSING, CROSSING_D)

71 BOUNCING = _Cnum(_B_, 2)

72 BOUNCING_D = _Cnum(_B_ + _d_, 9)

73 BOUNCINGs = (BOUNCING, BOUNCING_D) + CROSSINGs

74 LEFT_ON = _Cnum('Lo', 3)

75 ON_ON = _Cnum('oo', 5)

76 ON_LEFT = _Cnum('oL', 6)

77 ON_RIGHT = _Cnum('oR', 7)

78 RIGHT_ON = _Cnum('Ro', 4)

79 RIGHT_LEFT_ON = (RIGHT_ON, LEFT_ON)

80 # Entry/Exit flags

81 ENTRY = _Cnum(_e_, 1)

82 EXIT = _Cnum(_x_, 0)

83 Toggle = {ENTRY: EXIT,

84 EXIT: ENTRY,

85 None: None}

86

87 _L = _L() # PYCHOK singleton

88

89

90 class _RP(object): # RelativePositions

91 IS_Pm = _Cnum('Pm', 2) # C++ enum

92 IS_Pp = _Cnum('Pp', 3)

93 LEFT = _Cnum('L', 0)

94 RIGHT = _Cnum(_R_, 1)

95

96 _RP = _RP() # PYCHOK singleton

97

98 _RP2L = {(_RP.LEFT, _RP.RIGHT): _L.CROSSING,

99 (_RP.RIGHT, _RP.LEFT): _L.CROSSING,

100 (_RP.LEFT, _RP.LEFT): _L.BOUNCING,

101 (_RP.RIGHT, _RP.RIGHT): _L.BOUNCING,

102 # overlapping cases

103 (_RP.RIGHT, _RP.IS_Pp): _L.LEFT_ON,

104 (_RP.IS_Pp, _RP.RIGHT): _L.LEFT_ON,

105 (_RP.LEFT, _RP.IS_Pp): _L.RIGHT_ON,

106 (_RP.IS_Pp, _RP.LEFT): _L.RIGHT_ON,

107 (_RP.IS_Pm, _RP.IS_Pp): _L.ON_ON,

108 (_RP.IS_Pp, _RP.IS_Pm): _L.ON_ON,

109 (_RP.IS_Pm, _RP.RIGHT): _L.ON_LEFT,

110 (_RP.RIGHT, _RP.IS_Pm): _L.ON_LEFT,

111 (_RP.LEFT, _RP.IS_Pm): _L.ON_RIGHT,

112 (_RP.IS_Pm, _RP.LEFT): _L.ON_RIGHT}

113

114

115 class _LatLonBool(_Named):

116 '''(INTERNAL) Base class for L{LatLonFHP} and L{LatLonGH}.

117 '''

118 _alpha = None # point AND intersection else length

119 _checked = False # checked in phase 3 iff intersection

120 _clipid = INT0 # (polygonal) clip identifier, number

121 _dupof = None # original of a duplicate

122# _e_x_str = NN # shut up PyChecker

123 _height = Height(0) # interpolated height, usually meter

124 _linked = None # link to neighbor iff intersection

125 _next = None # link to the next vertex

126 _prev = None # link to the previous vertex

127

128 def __init__(self, lat_ll, lon=None, height=0, clipid=INT0, wrap=False, **name):

129 '''New C{LatLon[FHP|GH]} from separate C{lat}, C{lon}, C{height} and C{clipid}

130 scalars or from a previous C{LatLon[FHP|GH]}, C{Clip[FHP|GH]4Tuple} or some

131 other C{LatLon} instance.

132

133 @arg lat_ll: Latitude (C{scalar}) or a lat-/longitude (C{LatLon[FHP|GH]},

134 C{Clip[FHP|GH]4Tuple} or some other C{LatLon}).

135 @kwarg lon: Longitude (C{scalar}), required B{C{lat_ll}} is scalar,

136 ignored otherwise.

137 @kwarg height: Height (C{scalar}), conventionally C{meter}.

138 @kwarg clipid: Clip identifier (C{int}).

139 @kwarg wrap: If C{True}, wrap or I{normalize} B{C{lat}} and B{C{lon}} (C{bool}).

140 @kwarg name: Optional C{B{name}=NN} (C{str}).

141 '''

142 h, name = _xkwds_pop2(name, h=height) if name else (height, name)

143

144 if lon is None:

145 y, x = lat_ll.lat, lat_ll.lon

146 h = _xattr(lat_ll, height=h)

147 c = _xattr(lat_ll, clipid=clipid)

148 else:

149 y, x, c = lat_ll, lon, clipid

150 self.y, self.x = _Wrap.latlon(y, x) if wrap else (y, x)

151 # don't duplicate defaults

152 if self._height != h:

153 self._height = h

154 if self._clipid != c:

155 self._clipid = c

156 if name:

157 self.name = name

158

159 def __abs__(self):

160 return max(fabs(self.x), fabs(self.y))

161

162 def __eq__(self, other):

163 return other is self or bool(_other(self, other) and

164 other.x == self.x and

165 other.y == self.y)

166

167 def __ne__(self, other): # required for Python 2 # PYCHOK no cover

168 return not self.__eq__(other)

169

170 def __repr__(self):

171 '''String C{repr} of this lat-/longitude.

172 '''

173 if self._prev or self._next:

174 t = _ELLIPSIS_(self._prev, self._next)

175 t = _SPACE_(self, Fmt.ANGLE(t))

176 else:

177 t = str(self)

178 return t

179

180 def __str__(self):

181 '''String C{str} of this lat-/longitude.

182 '''

183 t = (_lat_, self.lat), (_lon_, self.lon)

184 if self._height:

185 X = _X_ if self.isintersection else NN

186 t += (_height_ + X, self._height),

187 if self._clipid:

188 t += (_clipid_, self._clipid),

189 if self._alpha is not None:

190 t += (_alpha_, self._alpha),

191# if self._dupof: # recursion risk

192# t += (_dupof_, self._dupof.name),

193 t = pairs(t, prec=8, fmt=Fmt.g, ints=True)

194 t = Fmt.PAREN(_COMMASPACE_.join(t))

195 if self._linked:

196 k = _DOT_ if self._checked else _BANG_

197 t = NN(t, self._e_x_str(k)) # PYCHOK expected

198 return NN(self.name, t)

199

200 def __sub__(self, other):

201 _other(self, other)

202 return type(self)(self.y - other.y, # classof

203 self.x - other.x)

204

205 def _2A(self, p2, p3):

206 # I{Signed} area of a triangle, I{doubled}.

207 x, y = self.x, self.y

208 return (p2.x - x) * (p3.y - y) - \

209 (p3.x - x) * (p2.y - y)

210

211 def _2Abs(self, p2, p3, eps=_10EPS):

212 # I{Unsigned} area of a triangle, I{doubled}

213 # or 0 if below the given threshold C{eps}.

214 a = fabs(self._2A(p2, p3))

215 return 0 if a < eps else a

216

217 @property_RO

218 def clipid(self):

219 '''Get the I{clipid} (C{int} or C{0}).

220 '''

221 return self._clipid

222

223 def _equi(self, llb, eps):

224 # Is this LLB I{equivalent} to B{C{llb}} within

225 # the given I{non-negative} tolerance B{C{eps}}?

226 return not (fabs(llb.lon - self.x) > eps or

227 fabs(llb.lat - self.y) > eps)

228

229 @property_RO

230 def height(self):

231 '''Get the I{height} (C{Height} or C{int}).

232 '''

233 h = self._height

234 return HeightX(h) if self.isintersection else (

235 Height(h) if h else _LatLonBool._height)

236

237 def isequalTo(self, other, eps=None):

238 '''Is this point equal to an B{C{other}} within a given,

239 I{non-negative} tolerance, ignoring C{height}?

240

241 @arg other: The other point (C{LatLon}).

242 @kwarg eps: Tolerance for equality (C{degrees} or C{None}).

243

244 @return: C{True} if equivalent, C{False} otherwise (C{bool}).

245

246 @raise TypeError: Invalid B{C{other}}.

247 '''

248 try:

249 return self._equi(other, _eps0(eps))

250 except (AttributeError, TypeError, ValueError):

251 raise _IsnotError(_LatLon_, other=other)

252

253 @property_RO

254 def isintersection(self):

255 '''Is this an intersection? May be C{ispoint} too!

256 '''

257 return bool(self._linked)

258

259 @property_RO

260 def ispoint(self):

261 '''Is this an I{original} point? May be C{isintersection} too!

262 '''

263 return self._alpha is None

264

265 @property_RO

266 def lat(self):

267 '''Get the latitude (C{scalar}).

268 '''

269 return self.y

270

271 @property_RO

272 def latlon(self):

273 '''Get the lat- and longitude (L{LatLon2Tuple}).

274 '''

275 return LatLon2Tuple(self.y, self.x)

276

277 def _link(self, other):

278 # Make this and an other point neighbors.

279 # assert _other(self, other)

280 self._linked = other

281 other._linked = self

282

283 @property_RO

284 def lon(self):

285 '''Get the longitude (C{scalar}).

286 '''

287 return self.x

288

289 def _toClas(self, Clas, clipid):

290 # Return this vertex as a C{Clas} instance

291 # (L{Clip[FHP|GH]4Tuple} or L{LatLon[FHP|GH]}).

292 return Clas(self.lat, self.lon, self.height, clipid)

293

294

295 class LatLonFHP(_LatLonBool):

296 '''A point or intersection in a L{BooleanFHP} clip or composite.

297 '''

298 _en_ex = None

299 _label = None

300 _2split = None # or C{._Clip}

301 _2xing = False

302

303 def __init__(self, lat_ll, lon=None, height=0, clipid=INT0, **wrap_name):

304 '''New C{LatLonFHP} from separate C{lat}, C{lon}, C{h}eight and C{clipid}

305 scalars, or from a previous L{LatLonFHP}, L{ClipFHP4Tuple} or some other

306 C{LatLon} instance.

307

308 @arg lat_ll: Latitude (C{scalar}) or a lat-/longitude (L{LatLonFHP},

309 L{ClipFHP4Tuple} or some other C{LatLon}).

310

311 @see: L{Here<_LatLonBool.__init__>} for further details.

312 '''

313 _LatLonBool.__init__(self, lat_ll, lon, height, clipid, **wrap_name)

314

315 def __add__(self, other):

316 _other(self, other)

317 return type(self)(self.y + other.y, self.x + other.x)

318

319 def __mod__(self, other): # cross product

320 _other(self, other)

321 return fdot_(self.x, other.y, -self.y, other.x)

322

323 def __mul__(self, other): # dot product

324 _other(self, other)

325 return fdot_(self.x, other.x, self.y, other.y)

326

327 def __rmul__(self, other): # scalar product

328 _xscalar(other=other)

329 return type(self)(self.y * other, self.x * other)

330

331# def _edge2(self):

332# # Return the start and end point of the

333# # edge containing I{intersection} C{v}.

334# n = p = self

335# while p.isintersection:

336# p = p._prev

337# if p is self:

338# break

339# while n.isintersection:

340# n = n._next

341# if n is self:

342# break

343# # assert p == self or not p._2Abs(self, n)

344# return p, n

345

346 def _e_x_str(self, t): # PYCHOK no cover

347 if self._label:

348 t = NN(self._label, t)

349 if self._en_ex:

350 t = NN(t, self._en_ex)

351 return t

352

353 @property_RO

354 def _isduplicate(self):

355 # Is this point a I{duplicate} intersection?

356 p = self._dupof

357 return bool(p and self._linked

358 and p is not self

359 and p == self

360# and _isin(p._alpha, None, self._alpha)

361 and _isin(self._alpha, _0_0, p._alpha))

362

363# @property_RO

364# def _isduplicated(self):

365# # Return the number of I{duplicates}?

366# d, v = 0, self

367# while v:

368# if v._dupof is self:

369# d += 1

370# v = v._next

371# if v is self:

372# break

373# return d

374

375 def isenclosedBy(self, *composites_points, **wrap):

376 '''Is this point inside one or more composites or polygons based on

377 the U{winding number<https://www.ScienceDirect.com/science/article/

378 pii/S0925772101000128>}?

379

380 @arg composites_points: Composites and/or iterables of points

381 (L{ClipFHP4Tuple}, L{ClipGH4Tuple}, L{LatLonFHP},

382 L{LatLonGH} or any C{LatLon}).

383 @kwarg wrap: Optional keyword argument C{B{wrap}=False}, if C{True},

384 wrap or I{normalize} and unroll all C{points} (C{bool}).

385

386 @raise ValueError: Some C{points} invalid.

387

388 @see: U{Algorithm 6<https://www.ScienceDirect.com/science/

389 article/pii/S0925772101000128>}.

390 '''

391 class _Pseudo(object):

392 # Pseudo-_CompositeBase._clips tuple

393

394 @property_RO

395 def _clips(self):

396 for cp in _Cps(_CompositeFHP, composites_points,

397 LatLonFHP.isenclosedBy): # PYCHOK yield

398 for c in cp._clips:

399 yield c

400

401 return self._isinside(_Pseudo(), **wrap)

402

403 def _isinside(self, composite, *excludes, **wrap):

404 # Is this point inside a composite, excluding

405 # certain C{_Clip}s? I{winding number}?

406 x, y, i = self.x, self.y, False

407 for c in composite._clips:

408 if c not in excludes:

409 w = 0

410 for p1, p2 in c._edges2(**wrap):

411 # edge [p1,p2] must straddle y

412 if (p1.y < y) is not (p2.y < y): # or ^

413 r = p2.x > x

414 s = p2.y > p1.y

415 if p1.x < x:

416 b = r and (s is (p1._2A(p2, self) > 0))

417 else:

418 b = r or (s is (p1._2A(p2, self) > 0))

419 if b:

420 w += 1 if s else -1

421 if isodd(w):

422 i = not i

423 return i

424

425 @property_RO

426 def _prev_next2(self):

427 # Adjust 2-tuple (._prev, ._next) iff a I{duplicate} intersection

428 p, n = self, self._next

429 if self._isduplicate: # PYCHOK no cover

430 p = self._dupof

431 while p._isduplicate:

432 p = p._dupof

433 while n._isduplicate:

434 n = n._next

435 return p._prev, n

436

437 def _RPoracle(self, p1, p2, p3):

438 # Relative Position oracle

439 if p1._linked is self: # or p1._linked2(self):

440 T = _RP.IS_Pm

441 elif p3._linked is self: # or p3._linked2(self):

442 T = _RP.IS_Pp

443 elif p1._2A(p2, p3) > 0: # left turn

444 T = _RP.LEFT if self._2A(p1, p2) > 0 and \

445 self._2A(p2, p3) > 0 else \

446 _RP.RIGHT # PYCHOK indent

447 else: # right turn (or straight)

448 T = _RP.RIGHT if self._2A(p1, p2) < 0 and \

449 self._2A(p2, p3) < 0 else \

450 _RP.LEFT # PYCHOK indent

451 return T

452

453

454 class LatLonGH(_LatLonBool):

455 '''A point or intersection in a L{BooleanGH} clip or composite.

456 '''

457 _entry = None # entry or exit iff intersection

458

459 def __init__(self, lat_ll, lon=None, height=0, clipid=INT0, **wrap_name):

460 '''New C{LatLonGH} from separate C{lat}, C{lon}, C{h}eight and C{clipid}

461 scalars, or from a previous L{LatLonGH}, L{ClipGH4Tuple} or some other

462 C{LatLon} instance.

463

464 @arg lat_ll: Latitude (C{scalar}) or a lat-/longitude (L{LatLonGH},

465 L{ClipGH4Tuple} or some other C{LatLon}).

466

467 @see: L{Here<_LatLonBool.__init__>} for further details.

468 '''

469 _LatLonBool.__init__(self, lat_ll, lon, height, clipid, **wrap_name)

470

471 def _check(self):

472 # Check-mark this vertex and its link.

473 self._checked = True

474 k = self._linked

475 if k and not k._checked:

476 k._checked = True

477

478 def _e_x_str(self, t): # PYCHOK no cover

479 return t if self._entry is None else NN(t,

480 (_e_ if self._entry else _x_))

481

482 def isenclosedBy(self, *composites_points, **wrap):

483 '''Is this point inside one or more composites or polygons based

484 on the U{even-odd-rule<https://www.ScienceDirect.com/science/

485 article/pii/S0925772101000128>}?

486

487 @arg composites_points: Composites and/or iterables of points

488 (L{ClipFHP4Tuple}, L{ClipGH4Tuple}, L{LatLonFHP},

489 L{LatLonGH} or any C{LatLon}).

490 @kwarg wrap: Optional keyword argument C{B{wrap}=False}, if C{True},

491 wrap or I{normalize} and unroll all C{points} (C{bool}).

492

493 @raise ValueError: Some B{C{points}} invalid.

494 '''

495 class _Pseudo(object):

496 # Pseudo-_CompositeBase._edges3 method

497

498 def _edges3(self, **kwds):

499 for cp in _Cps(_CompositeGH, composites_points,

500 LatLonGH.isenclosedBy): # PYCHOK yield

501 for e in cp._edges3(**kwds):

502 yield e

503

504 return self._isinside(_Pseudo(), **wrap)

505

506 def _isinside(self, composite, *bottom_top, **wrap):

507 # Is this vertex inside the composite? I{even-odd rule}?

508

509 def _x(y, p1, p2):

510 # return C{x} at given C{y} on edge [p1,p2]

511 return (y - p1.y) / (p2.y - p1.y) * (p2.x - p1.x)

512

513 # The I{even-odd} rule counts the number of edges

514 # intersecting a ray emitted from this point to

515 # east-bound infinity. When I{odd} this point lies

516 # inside, if I{even} outside.

517 y, i = self.y, False

518 if not (bottom_top and _outside(y, y, *bottom_top)):

519 x = self.x

520 for p1, p2, _ in composite._edges3(**wrap):

521 if (p1.y < y) is not (p2.y < y): # or ^

522 r = p2.x > x

523 if p1.x < x:

524 b = r and (_x(y, p1, p2) > x)

525 else:

526 b = r or (_x(y, p1, p2) > x)

527 if b:

528 i = not i

529 return i

530

531

532 class _Clip(_Named):

533 '''(INTERNAL) A I{doubly-linked} list representing a I{closed}

534 polygon of L{LatLonFHP} or L{LatLonGH} points, duplicates

535 and intersections with other clips.

536 '''

537 _composite = None

538 _dups = 0

539 _first = None

540 _id = 0

541 _identical = False

542 _noInters = False

543 _last = None

544 _LL = None

545 _len = 0

546 _pushback = False

547

548 def __init__(self, composite, clipid=INT0):

549 '''(INTERNAL) New C{_Clip}.

550 '''

551 # assert isinstance(composite, _CompositeBase)

552 if clipid in composite._clipids:

553 raise ClipError(clipid=clipid, txt=_duplicate_)

554 self._composite = composite

555 self._id = clipid

556 self._LL = composite._LL

557 composite._clips = composite._clips + (self,)

558

559 def __contains__(self, point): # PYCHOK no cover

560 '''Is the B{C{point}} in this clip?

561 '''

562 for v in self:

563 if v is point: # or ==?

564 return True

565 return False

566

567 def __eq__(self, other):

568 '''Is this clip I{equivalent} to an B{C{other}} clip,

569 do both have the same C{len}, the same points, in

570 the same order, possibly rotated?

571 '''

572 return self._equi(_other(self, other), 0)

573

574 def __ge__(self, other):

575 '''See method C{__lt__}.

576 '''

577 return not self.__lt__(other)

578

579 def __gt__(self, other):

580 '''Is this clip C{"above"} an B{C{other}} clip,

581 located or stretched farther North or East?

582 '''

583 return self._bltr4 > _other(self, other)._bltr4

584

585 def __hash__(self): # PYCHOK no cover

586 return hash(self._bltr4)

587

588 def __iter__(self):

589 '''Yield the points, duplicates and intersections.

590 '''

591 v = f = self._first

592 while v:

593 yield v

594 v = v._next

595 if v is f:

596 break

597

598 def __le__(self, other):

599 '''See method C{__gt__}.

600 '''

601 return not self.__gt__(other)

602

603 def __len__(self):

604 '''Return the number of points, duplicates and

605 intersections in this clip.

606 '''

607 return self._len

608

609 def __lt__(self, other):

610 '''Is this clip C{"below"} an B{C{other}} clip,

611 located or stretched farther South or West?

612 '''

613 return self._bltr4 < _other(self, other)._bltr4

614

615 def __ne__(self, other): # required for Python 2 # PYCHOK no cover

616 '''See method C{__eq__}.

617 '''

618 return not self.__eq__(other)

619

620 _all = __iter__

621

622 @property_RO

623 def _all_ON_ON(self):

624 # Check whether all vertices are ON_ON.

625 L_ON_ON = _L.ON_ON

626 return all(v._label is L_ON_ON for v in self)

627

628 def _append(self, y_v, *x_h_clipid):

629 # Append a point given as C{y}, C{x}, C{h}eight and

630 # C{clipid} args or as a C{LatLon[FHP|GH]}.

631 self._last = v = self._LL(y_v, *x_h_clipid) if x_h_clipid else y_v

632 self._len += 1

633 # assert v._clipid == self._id

634

635 v._next = n = self._first

636 if n is None: # set ._first

637 self._first = p = n = v

638 else: # insert before ._first

639 v._prev = p = n._prev

640 p._next = n._prev = v

641 return v

642

643# def _appendedup(self, v, clipid=0):

644# # Like C{._append}, but only append C{v} if not a

645# # duplicate of the one previously append[edup]'ed.

646# y, x, p = v.y, v.x, self._last

647# if p is None or y != p.y or x != p.x or clipid != p._clipid:

648# p = self._append(y, x, v._height, clipid)

649# if v._linked:

650# p._linked = True # to force errors

651# return p

652

653 @Property_RO

654 def _bltr4(self):

655 # Get the bounds as 4-tuple C{(bottom, left, top, right)}.

656 return map2(float, _MODS.points.boundsOf(self, wrap=False))

657

658 def _bltr4eps(self, eps):

659 # Get the ._bltr4 bounds tuple, slightly oversized.

660 if eps > 0: # > EPS

661 yb, xl, yt, xr = self._bltr4

662 yb, yt = _low_high_eps2(yb, yt, eps)

663 xl, xr = _low_high_eps2(xl, xr, eps)

664 t = yb, xl, yt, xr

665 else:

666 t = self._bltr4

667 return t

668

669 def _closed(self, raiser): # PYCHOK unused

670 # End a clip, un-close it and check C{len}.

671 p, f = self._last, self._first

672 if f and f._prev is p and p is not f and \

673 p._next is f and p == f: # PYCHOK no cover

674 # un-close the clip

675 f._prev = p = p._prev

676 p._next = f

677 self._len -= 1

678# elif f and raiser:

679# raise self._OpenClipError(p, f)

680 if len(self) < 3:

681 raise self._Error(_too_(_few_))

682

683 def _dup(self, q):

684 # Duplicate a point (or intersection) as intersection.

685 v = self._insert(q.y, q.x, q)

686 v._alpha = q._alpha or _0_0 # _0_0 replaces None

687 v._dupof = q._dupof or q

688 # assert v._prev is q

689 # assert q._next is v

690 return v

691

692 def _edges2(self, wrap=False, **unused):

693 # Yield each I{original} edge as a 2-tuple

694 # (p1, p2), a pair of C{LatLon[FHP|GH])}s.

695 p1 = p = f = self._first

696 while p:

697 p2 = p = p._next

698 if p.ispoint:

699 if wrap and p is not f:

700 p2 = _unrollon(p1, p)

701 yield p1, p2

702 p1 = p2

703 if p is f:

704 break

705

706 def _equi(self, clip, eps):

707 # Is this clip I{equivalent} to B{C{clip}} within

708 # the given I{non-negative} tolerance B{C{eps}}?

709 r, f = len(self), self._first

710 if f and r == len(clip) and self._bltr4eps(eps) \

711 == clip._bltr4eps(eps):

712 _equi = _LatLonBool._equi

713 for v in clip:

714 if _equi(f, v, eps):

715 s, n = f, v

716 for _ in range(r):

717 s, n = s._next, n._next

718 if not _equi(s, n, eps):

719 break # next v

720 else: # equivalent

721 return True

722 return False

723

724 def _Error(self, txt): # PYCHOK no cover

725 # Build a C{ClipError} instance

726 kwds = dict(len=len(self), txt=txt)

727 if self._dups:

728 kwds.update(dups=self._dups)

729 cp = self._composite

730 if self._id:

731 try:

732 i = cp._clips.index(self)

733 if i != self._id:

734 kwds.update(clip=i)

735 except ValueError:

736 pass

737 kwds[_clipid_] = self._id

738 return ClipError(cp._kind, cp.name, **kwds)

739

740 def _index(self, clips, eps):

741 # see _CompositeBase._equi

742 for i, c in enumerate(clips):

743 if c._equi(self, eps):

744 return i

745 raise ValueError(NN) # like clips.index(self)

746

747 def _insert(self, y, x, start, *end_alpha):

748 # insertVertex between points C{start} and

749 # C{end}, ordered by C{alpha} iff given.

750 v = self._LL(y, x, start._height, start._clipid)

751 n = start._next

752 if end_alpha:

753 end, alpha = end_alpha

754 v._alpha = alpha

755 v._height = favg(v._height, end._height, f=alpha)

756 # assert start is not end

757 while n is not end and n._alpha < alpha:

758 n = n._next

759 v._next = n

760 v._prev = p = n._prev

761 p._next = n._prev = v

762 self._len += 1

763# _Clip._bltr4._update(self)

764# _Clip._ishole._update(self)

765 return v

766

767 def _intersection(self, unused, q, *p1_p2_alpha):

768 # insert an intersection or make a point both

769 if p1_p2_alpha: # intersection on edge

770 v = self._insert(q.y, q.x, *p1_p2_alpha)

771 else: # intersection at point

772 v = q

773 # assert not v._linked

774 # assert v._alpha is None

775 return v

776

777 def _intersections(self):

778 # Yield all intersections, some may be points too.

779 for v in self:

780 if v.isintersection:

781 yield v

782

783 @Property_RO

784 def _ishole(self): # PYCHOK no cover

785 # Is this clip a hole inside its composite?

786 v = self._first

787 return v._isinside(self._composite, self) if v else False

788

789 @property_RO

790 def _nodups(self):

791 # Yield all non-duplicates.

792 for v in self:

793 if not v._dupof:

794 yield v

795

796 def _noXings(self, Union):

797 # Are all intersections non-CROSSINGs, -BOUNCINGs?

798 Xings = _L.BOUNCINGs if Union else _L.CROSSINGs

799 return all(v._label not in Xings for v in self._intersections())

800

801 def _OpenClipError(self, s, e): # PYCHOK no cover

802 # Return a C{CloseError} instance

803 t = NN(s, _ELLIPSIS_(_COMMASPACE_, e))

804 return self._Error(_SPACE_(_open_, t))

805

806 def _point2(self, insert):

807 # getNonIntersectionPoint and -Vertex

808 if not (insert and self._noInters):

809 for p in self._points(may_be=False): # not p._isduplicated?

810 return p, None

811 for n in self._intersections():

812 p, _ = n._prev_next2

813 k = p._linked

814 if k:

815 if n._linked not in k._prev_next2:

816 # create a pseudo-point

817 k = _0_5 * (p + n)

818 if insert:

819 k = self._insert(k.y, k.x, n._prev)

820 r = k # to remove later

821 else: # no ._prev, ._next

822 k._clipid = n._clipid

823 r = None

824 return k, r

825 return None, None

826

827 def _points(self, may_be=True):

828 # Yield all points I{in original order}, which may be intersections too.

829 for v in self:

830 if v.ispoint and (may_be or not v.isintersection):

831 yield v

832

833 def _remove2(self, v):

834 # Remove vertex C{v}.

835 # assert not v._isduplicated

836 if len(self) > 1:

837 p = v._prev

838 p._next = n = v._next

839 n._prev = p

840 if self._first is v:

841 self._first = n

842 if self._last is v:

843 self._last = p

844 self._len -= 1

845 else:

846 n = self._last = \

847 p = self._first = None

848 self._len = 0

849 return p, n

850

851 def _update_all(self): # PYCHOK no cover

852 # Zap the I{cached} properties.

853 _Clip._bltr4._update( self)

854 _Clip._ishole._update(self)

855 return self # for _special_identicals

856

857 def _Xings(self):

858 # Yield all I{un-checked} CROSSING intersections.

859 CROSSING = _L.CROSSING

860 for v in self._intersections():

861 if v._label is CROSSING and not v._checked:

862 yield v

863

864

865 class _CompositeBase(_Named):

866 '''(INTERNAL) Base class for L{BooleanFHP} and L{BooleanGH}

867 (C{_CompositeFHP} and C{_CompositeGH}).

868 '''

869 _clips = () # tuple of C{_Clips}

870 _eps = EPS # null edges

871 _kind = _corners_

872 _LL = _LatLonBool # shut up PyChecker

873 _raiser = False

874 _xtend = False

875

876 def __init__(self, lls, kind=NN, eps=EPS, **name):

877 '''(INTERNAL) See L{BooleanFHP} and L{BooleanGH}.

878 '''

879 n = _name__(name, _or_nameof=lls)

880 if n:

881 self.name = n

882 if kind:

883 self._kind = kind

884 if self._eps < eps:

885 self._eps = eps

886

887 c = _Clip(self)

888 lp = None

889 for ll in lls:

890 ll = self._LL(ll)

891 if lp is None:

892 c._id = ll._clipid # keep clipid

893 lp = c._append(ll)

894 elif ll._clipid != lp._clipid: # new clip

895 c._closed(self.raiser)

896 c = _Clip(self, ll._clipid)

897 lp = c._append(ll)

898 elif abs(ll - lp) > eps: # PYCHOK lp

899 lp = c._append(ll)

900 else:

901 c._dups += 1

902 c._closed(self.raiser)

903

904 def __contains__(self, point): # PYCHOK no cover

905 '''Is the B{C{point}} in one of the clips?

906 '''

907 for c in self._clips:

908 if point in c:

909 return True

910 return False

911

912 def __eq__(self, other):

913 '''Is this I{composite} equivalent to an B{C{other}}, i.e.

914 do both contain I{equivalent} clips in the same or in a

915 different order? Two clips are considered I{equivalent}

916 if both have the same points etc. in the same order,

917 possibly rotated.

918 '''

919 return self._equi(_other(self, other), 0)

920

921 def __iter__(self):

922 '''Yield all points, duplicates and intersections.

923 '''

924 for c in self._clips:

925 for v in c:

926 yield v

927

928 def __ne__(self, other): # required for Python 2

929 '''See method C{__eq__}.

930 '''

931 return not self.__eq__(other)

932

933 def __len__(self):

934 '''Return the I{total} number of points.

935 '''

936 return sum(map(len, self._clips)) if self._clips else 0

937

938 def __repr__(self):

939 '''String C{repr} of this composite.

940 '''

941 c = len(self._clips)

942 c = Fmt.SQUARE(c) if c > 1 else NN

943 n = Fmt.SQUARE(len(self))

944 t = Fmt.PAREN(self) # XXX not unstr

945 return NN(self.typename, c, n, t)

946

947 def __str__(self):

948 '''String C{str} of this composite.

949 '''

950 return _COMMASPACE_.join(map(str, self))

951

952 @property_RO

953 def _bottom_top_eps2(self):

954 # Get the bottom and top C{y} bounds, oversized.

955 return _min_max_eps2(min(v.y for v in self),

956 max(v.y for v in self))

957

958 def _class(self, corners, kwds, **dflts):

959 # Return a new instance

960 _g = kwds.get

961 kwds = dict((n, _g(n, v)) for n, v in dflts.items())

962 return type(self)(corners or (), **kwds)

963

964 @property_RO

965 def _clipids(self): # PYCHOK no cover

966 for c in self._clips:

967 yield c._id

968

969 def clipids(self):

970 '''Return a tuple with all C{clipid}s, I{ordered}.

971 '''

972 return tuple(self._clipids)

973

974# def _clipidups(self, other):

975# # Number common C{clipid}s between this and an C{other} composite

976# return len(set(self._clipids).intersection(set(other._clipids)))

977

978 def _edges3(self, **raiser_wrap):

979 # Yield each I{original} edge as a 3-tuple

980 # C{(LatLon[FHP|GH], LatLon[FHP|GH], _Clip)}.

981 for c in self._clips:

982 for p1, p2 in c._edges2(**raiser_wrap):

983 yield p1, p2, c

984

985 def _encloses(self, lat, lon, **wrap):

986 # see function .points.isenclosedBy

987 return self._LL(lat, lon).isenclosedBy(self, **wrap)

988

989 @property

990 def eps(self):

991 '''Get the null edges tolerance (C{degrees}, usually).

992 '''

993 return self._eps

994

995 @eps.setter # PYCHOK setter!

996 def eps(self, eps):

997 '''Set the null edges tolerance (C{degrees}, usually).

998 '''

999 self._eps = eps

1000

1001 def _10eps(self, **eps_):

1002 # Get eps for _LatLonBool._2Abs

1003 e = _xkwds_get(eps_, eps=self._eps)

1004 if e != EPS:

1005 e *= _10EPS / EPS

1006 else:

1007 e = _10EPS

1008 return e

1009

1010 def _equi(self, other, eps):

1011 # Is this composite I{equivalent} to an B{C{other}} within

1012 # the given, I{non-negative} tolerance B{C{eps}}?

1013 cs, co = self._clips, other._clips

1014 if cs and len(cs) == len(co):

1015 if eps > 0:

1016 _index = _Clip._index

1017 else:

1018 def _index(c, cs, unused):

1019 return cs.index(c)

1020 try:

1021 cs = list(sorted(cs))

1022 for c in sorted(co):

1023 cs.pop(_index(c, cs, eps))

1024 except ValueError: # from ._index

1025 pass

1026 return False if cs else True

1027 else: # both null?

1028 return False if cs or co else True

1029

1030 def _intersections(self):

1031 # Yield all intersections.

1032 for c in self._clips:

1033 for v in c._intersections():

1034 yield v

1035

1036 def isequalTo(self, other, eps=None):

1037 '''Is this boolean/composite equal to an B{C{other}} within

1038 a given, I{non-negative} tolerance?

1039

1040 @arg other: The other boolean/composite (C{Boolean[FHP|GB]}).

1041 @kwarg eps: Tolerance for equality (C{degrees} or C{None}).

1042

1043 @return: C{True} if equivalent, C{False} otherwise (C{bool}).

1044

1045 @raise TypeError: Invalid B{C{other}}.

1046

1047 @see: Method C{__eq__}.

1048 '''

1049 if isinstance(other, _CompositeBase):

1050 return self._equi(other, _eps0(eps))

1051 raise _IsnotError(_boolean_, _composite_, other=other)

1052

1053 def _kwds(self, op, **more):

1054 # Get all keyword arguments as C{dict}.

1055 kwds = dict(raiser=self.raiser, eps=self.eps,

1056 name=self.name or typename(op))

1057 kwds.update(more)

1058 return kwds

1059

1060 @property_RO

1061 def _left_right_eps2(self):

1062 # Get the left and right C{x} bounds, oversized.

1063 return _min_max_eps2(min(v.x for v in self),

1064 max(v.x for v in self))

1065

1066 def _points(self, may_be=True): # PYCHOK no cover

1067 # Yield all I{original} points, which may be intersections too.

1068 for c in self._clips:

1069 for v in c._points(may_be=may_be):

1070 yield v

1071

1072 @property

1073 def raiser(self):

1074 '''Get the option to throw L{ClipError} exceptions (C{bool}).

1075 '''

1076 return self._raiser

1077

1078 @raiser.setter # PYCHOK setter!

1079 def raiser(self, throw):

1080 '''Set the option to throw L{ClipError} exceptions (C{bool}).

1081 '''

1082 self._raiser = bool(throw)

1083

1084 def _results(self, _presults, Clas, closed=False, inull=False, **eps):

1085 # Yield the dedup'd results, as L{ClipFHP4Tuple}s

1086 C = self._LL if Clas is None else Clas

1087 e = self._10eps(**eps)

1088 for clipid, ns in enumerate(_presults):

1089 f = p = v = None

1090 for n in ns:

1091 if f is None:

1092 yield n._toClas(C, clipid)

1093 f = p = n

1094 elif v is None:

1095 v = n # got f, p, v

1096 elif inull or p._2Abs(v, n, eps=e):

1097 yield v._toClas(C, clipid)

1098 p, v = v, n

1099 else: # null, colinear, ... skipped

1100 v = n

1101 if v and (inull or p._2Abs(v, f, eps=e)):

1102 yield v._toClas(C, clipid)

1103 p = v

1104 if f and p != f and closed: # close clip

1105 yield f._toClas(C, clipid)

1106

1107 def _sum(self, other, op):

1108 # Combine this and an C{other} composite

1109 LL = self._LL

1110 sp = self.copy(name=self.name or typename(op))

1111 sp._clips, sid = (), INT0 # new clips

1112 for cp in (self, other):

1113 for c in cp._clips:

1114 _ap = _Clip(sp, sid)._append

1115 for v in c._nodups:

1116 _ap(LL(v.y, v.x, v.height, sid))

1117 sid += 1

1118 return sp

1119

1120 def _sum1(self, _a_p, *args, **kwds): # in .karney, .points

1121 # Sum the area or perimeter of all clips

1122 return _MODS.fsums.fsum1((_a_p(c, *args, **kwds) for c in self._clips))

1123

1124 def _sum2(self, LL, _a_p, *args, **kwds): # in .sphericalNvector, -Trigonometry

1125 # Sum the area or perimeter of all clips

1126

1127 def _lls(clip): # convert clip to LLs

1128 _LL = LL

1129 for v in clip:

1130 yield _LL(v.lat, v.lon) # datum=Sphere

1131

1132 return _MODS.fsums.fsum1((_a_p(_lls(c), *args, **kwds) for c in self._clips))

1133

1134 def toLatLon(self, LatLon=None, closed=False, **LatLon_kwds):

1135 '''Yield all (non-duplicate) points and intersections

1136 as an instance of B{C{LatLon}}.

1137

1138 @kwarg LatLon: Class to use (C{LatLon}) or if C{None},

1139 L{LatLonFHP} or L{LatLonGH}.

1140 @kwarg closed: If C{True}, close each clip (C{bool}).

1141 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}}

1142 keyword arguments, ignore if

1143 C{B{LatLon} is None}.

1144

1145 @raise TypeError: Invalid B{C{LatLon}}.

1146

1147 @note: For intersections, C{height} is an instance

1148 of L{HeightX}, otherwise of L{Height}.

1149 '''

1150 if LatLon is None:

1151 LL, kwds = self._LL, {}

1152 elif issubclassof(LatLon, _LatLonBool, LatLonBase):

1153 LL, kwds = LatLon, LatLon_kwds

1154 else:

1155 raise _TypeError(LatLon=LatLon)

1156

1157 for c in self._clips:

1158 lf, cid = None, c._id

1159 for v in c._nodups:

1160 ll = LL(v.y, v.x, **kwds)

1161 ll._height = v.height

1162 if ll._clipid != cid:

1163 ll._clipid = cid

1164 yield ll

1165 if lf is None:

1166 lf = ll

1167 if closed and lf:

1168 yield lf

1169

1170

1171 class _CompositeFHP(_CompositeBase):

1172 '''(INTERNAL) A list of clips representing a I{composite}

1173 of L{LatLonFHP} points, duplicates and intersections

1174 with an other I{composite}.

1175 '''

1176 _LL = LatLonFHP

1177 _Union = False

1178

1179 def __init__(self, lls, raiser=False, **name_kind_eps):

1180 # New L{_CompositeFHP}.

1181 if raiser:

1182 self._raiser = True

1183 _CompositeBase.__init__(self, lls, **name_kind_eps)

1184

1185 def _classify(self):

1186 # 2) Classify intersection chains.

1187 L = _L

1188 for v in self._intersections():

1189 n, b = v, v._label

1190 if b in L.RIGHT_LEFT_ON: # next chain

1191 while True:

1192 n._label = None # _xkwds_pop(n.__dict__, _label=None)

1193 n = n._next

1194 if n is v or n._label is not L.ON_ON: # n._label and ...

1195 break

1196 a = L.LEFT_ON if n._label is L.ON_LEFT else L.RIGHT_ON

1197 v._label = n._label = L.BOUNCING_D if a is b else L.CROSSING_D

1198

1199 # 3) Copy labels

1200 for v in self._intersections():

1201 v._linked._label = v._label

1202

1203 def _clip(self, corners, Union=False, Clas=None,

1204 **closed_inull_raiser_eps):

1205 # Clip this composite with another one, C{corners},

1206 # using the Foster-Hormann-Popa's algorithm.

1207 P = self

1208 Q = self._class(corners, closed_inull_raiser_eps,

1209 eps=P._eps, raiser=False)

1210 if Union:

1211 P._Union = Q._Union = True

1212

1213 bt = Q._bottom_top_eps2

1214 lr = Q._left_right_eps2

1215 # compute and insert intersections

1216 for p1, p2, Pc in P._edges3(**closed_inull_raiser_eps):

1217 if not (_outside(p1.x, p2.x, *lr) or

1218 _outside(p1.y, p2.y, *bt)):

1219 e = _EdgeFHP(p1, p2)

1220 if e._dp2 > EPS2: # non-null edge

1221 for q1, q2, Qc in Q._edges3(**closed_inull_raiser_eps):

1222 for T, p, q in e._intersect3(q1, q2):

1223 p = Pc._intersection(T, *p)

1224 q = Qc._intersection(T, *q)

1225 # assert not p._linked

1226 # assert not q._linked

1227 p._link(q)

1228

1229 # label and classify intersections

1230 P._labelize()

1231 P._classify()

1232

1233 # check for special cases

1234 P._special_cases(Q)

1235 Q._special_cases(P)

1236 # handle identicals

1237 P._special_identicals(Q)

1238

1239 # set Entry/Exit flags

1240 P._set_entry_exits(Q)

1241 Q._set_entry_exits(P)

1242

1243 # handle splits and crossings

1244 P._splits_xings(Q)

1245

1246 # yield the results

1247 return P._results(P._presults(Q), Clas, **closed_inull_raiser_eps)

1248

1249 @property_RO

1250 def _identicals(self):

1251 # Yield all clips marked C{._identical}.

1252 for c in self._clips:

1253 if c._identical:

1254 yield c

1255

1256 def _labelize(self):

1257 # 1) Intersections classification

1258 for p in self._intersections():

1259 q = p._linked

1260 # determine local configuration at this intersection

1261 # and positions of Q- and Q+ relative to (P-, I, P+)

1262 p1, p3 = p._prev_next2

1263 q1, q3 = q._prev_next2

1264 t = (q1._RPoracle(p1, p, p3),

1265 q3._RPoracle(p1, p, p3))

1266 # check intersecting and overlapping cases

1267 p._label = _RP2L.get(t, None)

1268

1269 def _presults(self, other):

1270 # Yield the result clips, each as a generator

1271 # of the L{_LatLonFHP}s in that clip

1272 for cp in (self, other):

1273 for c in cp._clips:

1274 if c._pushback:

1275 yield c._all()

1276 for c in self._clips:

1277 for X in c._Xings():

1278 yield self._resultX(X)

1279

1280 def _resultX(self, X):

1281 # Yield the results from CROSSING C{X}.

1282 L, U, v = _L, self._Union, X

1283 while v:

1284 v._checked = True

1285 r = v # in P or Q

1286 s = L.Toggle[v._en_ex]

1287 e = (s is L.EXIT) ^ U

1288 while True:

1289 v = v._next if e else v._prev

1290 yield v

1291 v._checked = True

1292 if v._en_ex is s or v is X:

1293 break

1294 if v is r: # full circle

1295 raise ClipError(full_circle=v, clipid=v._clipid)

1296 if v is not X:

1297 v = v._linked

1298 if v is X:

1299 break

1300

1301 def _set_entry_exits(self, other): # MCCABE 14

1302 # 4) Set entry/exit flags

1303 L, U = _L, self._Union

1304 for c in self._clips:

1305 n, k = c._point2(True)

1306 if n:

1307 f = n

1308 s = L.EXIT if n._isinside(other) else L.ENTRY

1309 t = L.EXIT # first_chain_vertex = True

1310 while True:

1311 if n.isintersection:

1312 b = n._label

1313 if b is L.CROSSING:

1314 n._en_ex = s

1315 s = L.Toggle[s]

1316 elif b is L.BOUNCING and ((s is L.EXIT) ^ U):

1317 n._2split = c # see ._splits_xings

1318 elif b is L.CROSSING_D:

1319 n._en_ex = s

1320 if (s is t) ^ U:

1321 n._label = L.CROSSING

1322 t = L.Toggle[t]

1323 if t is L.EXIT: # first_chain_vertex == True

1324 s = L.Toggle[s]

1325 elif b is L.BOUNCING_D:

1326 n._en_ex = s

1327 if (s is t) ^ U:

1328 n._2xing = True # see ._splits_xings

1329 s = L.Toggle[s]

1330 t = L.Toggle[t]

1331 n = n._next # _, n = n._prev_next2

1332 if n is f:

1333 break # PYCHOK attr?

1334 if k:

1335 c._remove2(k)

1336

1337 def _special_cases(self, other):

1338 # 3.5) Check special cases

1339 U = self._Union

1340 for c in self._clips:

1341 if c._noXings(U):

1342 c._noInters = True

1343 if c._all_ON_ON:

1344 c._identical = True

1345 else:

1346 p, _ = c._point2(False)

1347 if p and (p._isinside(other) ^ U):

1348 c._pushback = True

1349

1350 def _special_identicals(self, other):

1351 # 3.5) Handle identicals

1352 _u = _Clip._update_all

1353 cds = dict((c._id, _u(c)) for c in other._identicals)

1354 # assert len(cds) == len(other._identicals)

1355 if cds: # PYCHOK no cover

1356 for c in self._identicals:

1357 c._update_all()

1358 for v in c._intersections():

1359 d = cds.get(v._linked._clipid, None)

1360 if d and d._ishole is c._ishole:

1361 c._pushback = True

1362 break # next c

1363

1364 @property_RO

1365 def _2splits(self):

1366 # Yield all intersections marked C{._2split}

1367 for p in self._intersections():

1368 if p._2split:

1369 # assert isinstance(p._2split, _Clip)

1370 yield p

1371

1372 def _splits_xings(self, other): # MCCABE 15

1373 # 5) Handle split pairs and 6) crossing candidates

1374

1375 def _2A_dup2(p, P): # PYCHOK unused

1376 p1, p2 = p._prev_next2

1377 ap = p1._2A(p, p2)

1378 Pc = p._2split

1379 # assert Pc in P._clips

1380 # assert p in Pc

1381 return ap, Pc._dup(p)

1382

1383 def _links2(ps, qs): # PYCHOK P unused?

1384 # Yield each link as a 2-tuple(p, q)

1385 id_qs = set(map(id, qs))

1386 if id_qs:

1387 for p in ps:

1388 q = p._linked

1389 if q and id(q) in id_qs:

1390 yield p, q

1391

1392 L = _L

1393 E = L.ENTRY if self._Union else L.EXIT

1394 X = L.Toggle[E]

1395 for p, q in _links2(self._2splits, other._2splits):

1396 ap, pp = _2A_dup2(p, self)

1397 aq, qq = _2A_dup2(q, other)

1398 if (ap * aq) > 0: # PYCHOK no cover

1399 p._link(qq) # overwrites ...

1400 q._link(pp) # ... p-q link

1401 else:

1402 pp._link(qq)

1403 p._en_ex = q._en_ex = E

1404 pp._en_ex = qq._en_ex = X

1405 p._label = pp._label = \

1406 q._label = qq._label = L.CROSSING

1407

1408 for p, q in _links2(self._2xings, other._2xings):

1409 p._label = q._label = L.CROSSING

1410

1411 @property_RO

1412 def _2xings(self):

1413 # Yield all intersections marked C{._2xing}

1414 for p in self._intersections():

1415 if p._2xing:

1416 yield p

1417

1418

1419 class _CompositeGH(_CompositeBase):

1420 '''(INTERNAL) A list of clips representing a I{composite}

1421 of L{LatLonGH} points, duplicates and intersections

1422 with an other I{composite}.

1423 '''

1424 _LL = LatLonGH

1425 _xtend = False

1426

1427 def __init__(self, lls, raiser=False, xtend=False, **name_kind_eps):

1428 # New L{_CompositeGH}.

1429 if xtend:

1430 self._xtend = True

1431 elif raiser:

1432 self._raiser = True

1433 _CompositeBase.__init__(self, lls, **name_kind_eps)

1434

1435 def _clip(self, corners, s_entry, c_entry, Clas=None,

1436 **closed_inull_raiser_xtend_eps):

1437 # Clip this polygon with another one, C{corners}, using

1438 # Greiner/Hormann's algorithm extended to optionally

1439 # handle some so-called "degenerate cases"

1440 S = self

1441 C = self._class(corners, closed_inull_raiser_xtend_eps,

1442 raiser=False, xtend=False)

1443 bt = C._bottom_top_eps2

1444 lr = C._left_right_eps2

1445 # 1. find intersections

1446 for s1, s2, Sc in S._edges3(**closed_inull_raiser_xtend_eps):

1447 if not (_outside(s1.x, s2.x, *lr) or

1448 _outside(s1.y, s2.y, *bt)):

1449 e = _EdgeGH(s1, s2, **closed_inull_raiser_xtend_eps)

1450 if e._hypot2 > EPS2: # non-null edge

1451 for c1, c2, Cc in C._edges3(**closed_inull_raiser_xtend_eps):

1452 for y, x, sa, ca in e._intersect4(c1, c2):

1453 s = Sc._insert(y, x, s1, s2, sa)

1454 c = Cc._insert(y, x, c1, c2, ca)

1455 s._link(c)

1456

1457 # 2. identify entry/exit intersections

1458 if S._first:

1459 s_entry ^= S._first._isinside(C, *bt)

1460 for v in S._intersections():

1461 v._entry = s_entry = not s_entry

1462

1463 if C._first:

1464 c_entry ^= C._first._isinside(S)

1465 for v in C._intersections():

1466 v._entry = c_entry = not c_entry

1467

1468 # 3. yield the result(s)

1469 return S._results(S._presults(), Clas, **closed_inull_raiser_xtend_eps)

1470

1471 @property_RO

1472 def _first(self):

1473 # Get the very first vertex of the first clip

1474 for v in self:

1475 return v

1476 return None # PYCHOK no cover

1477

1478 def _kwds(self, op, **more):

1479 # Get the kwds C{dict}.

1480 return _CompositeBase._kwds(self, op, xtend=self.xtend, **more)

1481

1482 def _presults(self):

1483 # Yield the unchecked intersection(s).

1484 for c in self._clips:

1485 for v in c._intersections():

1486 if not v._checked:

1487 yield self._resultU(v)

1488

1489 def _resultU(self, v):

1490 # Yield the result from an un-checked intersection.

1491 while v and not v._checked:

1492 v._check()

1493 yield v

1494 r = v

1495 e = v._entry

1496 while True:

1497 v = v._next if e else v._prev

1498 yield v

1499 if v._linked:

1500 break

1501 if v is r:

1502 raise ClipError(full_circle=v, clipid=v._clipid)

1503 v = v._linked # switch

1504

1505 @property

1506 def xtend(self):

1507 '''Get the option to handle some I{degenerate cases} (C{bool}).

1508 '''

1509 return self._xtend

1510

1511 @xtend.setter # PYCHOK setter!

1512 def xtend(self, xtend):

1513 '''Set the option to handle some I{degenerate cases} (C{bool}).

1514 '''

1515 self._xtend = bool(xtend)

1516

1517

1518 class _EdgeFHP(object):

1519 # An edge between two L{LatLonFHP} points.

1520

1521 X_INTERSECT = _Cnum('Xi', 1) # C++ enum

1522 X_OVERLAP = _Cnum('Xo', 5)

1523 P_INTERSECT = _Cnum('Pi', 3)

1524 P_OVERLAP = _Cnum('Po', 7)

1525 Ps = (P_INTERSECT, P_OVERLAP, X_OVERLAP)

1526 Q_INTERSECT = _Cnum('Qi', 2)

1527 Q_OVERLAP = _Cnum('Qo', 6)

1528 Qs = (Q_INTERSECT, Q_OVERLAP, X_OVERLAP)

1529 V_INTERSECT = _Cnum('Vi', 4)

1530 V_OVERLAP = _Cnum('Vo', 8)

1531 Vs = (V_INTERSECT, V_OVERLAP)

1532

1533 def __init__(self, p1, p2, **unused):

1534 # New edge between points C{p1} and C{p2}, each a L{LatLonFHP}.

1535 self._p1_p2 = p1, p2

1536 self._dp = dp = p2 - p1

1537 self._dp2 = dp * dp # dot product, hypot2

1538

1539 self._lr, \

1540 self._bt = _left_right_bottom_top_eps2(p1, p2)

1541

1542 def _intersect3(self, q1, q2):

1543 # Yield intersection(s) Type or C{None}

1544 if not (_outside(q1.x, q2.x, *self._lr) or

1545 _outside(q1.y, q2.y, *self._bt)):

1546 dq = q2 - q1

1547 dq2 = dq * dq # dot product, hypot2

1548 if dq2 > EPS2: # like ._clip

1549 T, _E = None, _EdgeFHP # type(self)

1550 p1, p2 = self._p1_p2

1551 ap1 = p1._2A(q1, q2)

1552 ap2_1 = p2._2A(q1, q2) - ap1

1553 if fabs(ap2_1) > _0EPS: # non-parallel edges

1554 aq1 = q1._2A(p1, p2)

1555 aq2_1 = q2._2A(p1, p2) - aq1

1556 if fabs(aq2_1) > _0EPS:

1557 # compute and classify alpha and beta

1558 a, a_0, a_0_1, _ = _alpha4(-ap1 / ap2_1)

1559 b, b_0, b_0_1, _ = _alpha4(-aq1 / aq2_1)

1560 # distinguish intersection types

1561 T = _E.X_INTERSECT if a_0_1 and b_0_1 else (

1562 _E.P_INTERSECT if a_0_1 and b_0 else (

1563 _E.Q_INTERSECT if a_0 and b_0_1 else (

1564 _E.V_INTERSECT if a_0 and b_0 else None)))

1565

1566 elif fabs(ap1) < _0EPS: # parallel or colinear edges

1567 dp = self._dp

1568 d1 = q1 - p1

1569 # compute and classify alpha and beta

1570 a, a_0, a_0_1, _a_0_1 = _alpha4((d1 * dp) / self._dp2)

1571 b, b_0, b_0_1, _b_0_1 = _alpha4((d1 * dq) / (-dq2))

1572 # distinguish overlap type

1573 T = _E.X_OVERLAP if a_0_1 and b_0_1 else (

1574 _E.P_OVERLAP if a_0_1 and _b_0_1 else (

1575 _E.Q_OVERLAP if _a_0_1 and b_0_1 else (

1576 _E.V_OVERLAP if a_0 and b_0 else None)))

1577

1578 if T:

1579 if T is _E.X_INTERSECT:

1580 v = p1 + a * self._dp

1581 yield T, (v, p1, p2, a), (v, q1, q2, b)

1582 elif T in _E.Vs:

1583 yield T, (p1,), (q1,)

1584 else:

1585 if T in _E.Qs:

1586 yield T, (p1,), (p1, q1, q2, b)

1587 if T in _E.Ps:

1588 yield T, (q1, p1, p2, a), (q1,)

1589

1590

1591 class _EdgeGH(object):

1592 # An edge between two L{LatLonGH} points.

1593

1594 _raiser = False

1595 _xtend = False

1596

1597 def __init__(self, s1, s2, raiser=False, xtend=False, **unused):

1598 # New edge between points C{s1} and C{s2}, each a L{LatLonGH}.

1599 self._s1, self._s2 = s1, s2

1600 self._x_sx_y_sy = (s1.x, s2.x - s1.x,

1601 s1.y, s2.y - s1.y)

1602 self._lr, \

1603 self._bt = _left_right_bottom_top_eps2(s1, s2)

1604

1605 if xtend:

1606 self._xtend = True

1607 elif raiser:

1608 self._raiser = True

1609

1610 def _alpha2(self, x, y, dx, dy):

1611 # Return C{(alpha)}, see .points.nearestOn5

1612 a = Fdot_(y, dy, x, dx).fover(self._hypot2)

1613 d = Fdot_(y, dx, -x, dy).fover(self._hypot0)

1614 return a, fabs(d)

1615

1616 def _Error(self, n, *args, **kwds): # PYCHOK no cover

1617 t = _DOT_(unstr(_EdgeGH, self._s1, self._s2),

1618 unstr(_EdgeGH._intersect4, *args, **kwds))

1619 return ClipError(_case_, n, txt=t)

1620

1621 @Property_RO

1622 def _hypot0(self):

1623 _, sx, _, sy = self._x_sx_y_sy

1624 return hypot(sx, sy) * _0EPS

1625

1626 @Property_RO

1627 def _hypot2(self):

1628 _, sx, _, sy = self._x_sx_y_sy

1629 return hypot2(sx, sy)

1630

1631 def _intersect4(self, c1, c2, parallel=True): # MCCABE 14

1632 # Yield the intersection(s) of this and another edge.

1633

1634 # @return: None, 1 or 2 intersections, each a 4-Tuple

1635 # (y, x, s_alpha, c_alpha) with intersection

1636 # coordinates x and y and both alphas.

1637

1638 # @raise ClipError: Intersection unhandled.

1639

1640 # @see: U{Intersection point of two line segments

1641 # <http://PaulBourke.net/geometry/pointlineplane/>}.

1642 c1_x, c1_y = c1.x, c1.y

1643 if not (_outside(c1_x, c2.x, *self._lr) or

1644 _outside(c1_y, c2.y, *self._bt)):

1645 x, sx, \

1646 y, sy = self._x_sx_y_sy

1647

1648 cx = c2.x - c1_x

1649 cy = c2.y - c1_y

1650 d = cy * sx - cx * sy

1651

1652 if fabs(d) > _0EPS: # non-parallel edges

1653 dx = x - c1_x

1654 dy = y - c1_y

1655 ca = Fdot_(sx, dy, -sy, dx).fover(d)

1656 if _0EPS < ca < _EPS1 or (self._xtend and

1657 _EPS0 < ca < _1EPS):

1658 sa = Fdot_(cx, dy, -cy, dx).fover(d)

1659 if _0EPS < sa < _EPS1 or (self._xtend and

1660 _EPS0 < sa < _1EPS):

1661 yield (y + sa * sy), (x + sa * sx), sa, ca

1662

1663 # unhandled, "degenerate" cases 1, 2 or 3

1664 elif self._raiser and not (sa < _EPS0 or sa > _1EPS): # PYCHOK no cover

1665 raise self._Error(1, c1, c2, sa=sa) # intersection at s1 or s2

1666

1667 elif self._raiser and not (ca < _EPS0 or ca > _1EPS): # PYCHOK no cover

1668 # intersection at c1 or c2 or at c1 or c2 and s1 or s2

1669 sa = Fdot_(cx, dy, -cy, dx).fover(d)

1670 e = 2 if sa < _EPS0 or sa > _1EPS else 3

1671 raise self._Error(e, c1, c2, ca=ca)

1672

1673 elif parallel and (sx or sy) and (cx or cy): # PYCHOK no cover

1674 # non-null, parallel or colinear edges

1675 sa1, d1 = self._alpha2(c1_x - x, c1_y - y, sx, sy)

1676 sa2, d2 = self._alpha2(c2.x - x, c2.y - y, sx, sy)

1677 if max(d1, d2) < _0EPS:

1678 if self._xtend and not _outside(sa1, sa2, _EPS0, _1EPS):

1679 if sa1 > sa2: # anti-parallel

1680 sa1, sa2 = sa2, sa1

1681 ca1, ca2 = _1_0, _0_0

1682 else: # parallel

1683 ca1, ca2 = _0_0, _1_0

1684 ca = fabs((sx / cx) if cx else (sy / cy))

1685 # = hypot(sx, sy) / hypot(cx, cy)

1686 if sa1 < 0: # s1 is between c1 and c2

1687 ca *= ca1 + sa1

1688 yield y, x, ca1, _alpha1(ca)

1689 else: # c1 is between s1 and s2

1690 yield (y + sa1 * sy), (x + sa1 * sx), sa1, ca1

1691 if sa2 > 1: # s2 is between c1 and c2

1692 ca *= sa2 - _1_0

1693 yield (y + sy), (x + sx), ca2, _alpha1(ca2 - ca)

1694 else: # c2 is between s1 and s2

1695 yield (y + sa2 * sy), (x + sa2 * sx), sa2, ca2

1696 elif self._raiser and not _outside(sa1, sa2, _0_0, _1EPS):

1697 raise self._Error(4, c1, c2, d1=d1, d2=d2)

1698

1699

1700 class _BooleanBase(object):

1701 # Shared C{Boolean[FHP|GH]} methods.

1702

1703 def __add__(self, other):

1704 '''Sum: C{this + other} clips.

1705 '''

1706 return self._sum(_other(self, other), self.__add__) # PYCHOK OK

1707

1708 def __and__(self, other):

1709 '''Intersection: C{this & other}.

1710 '''

1711 return self._boolean(other, False, False, self.__and__) # PYCHOK OK

1712

1713 def __iadd__(self, other):

1714 '''In-place sum: C{this += other} clips.

1715 '''

1716 return self._inplace(self.__add__(other))

1717

1718 def __iand__(self, other):

1719 '''In-place intersection: C{this &= other}.

1720 '''

1721 return self._inplace(self.__and__(other))

1722

1723 def __ior__(self, other):

1724 '''In-place union: C{this |= other}.

1725 '''

1726 return self._inplace(self.__or__(other))

1727

1728 def __or__(self, other):

1729 '''Union: C{this | other}.

1730 '''

1731 return self._boolean(other, True, True, self.__or__) # PYCHOK OK

1732

1733 def __radd__(self, other):

1734 '''Reverse sum: C{other + this} clips.

1735 '''

1736 return _other(self, other)._sum(self, self.__radd__)

1737

1738 def __rand__(self, other):

1739 '''Reverse intersection: C{other & this}

1740 '''

1741 return _other(self, other).__and__(self)

1742

1743 def __ror__(self, other):

1744 '''Reverse union: C{other | this}

1745 '''

1746 return _other(self, other).__or__(self)

1747

1748 def _boolean4(self, other, op):

1749 # Set up a new C{Boolean[FHP|GH]}.

1750 C = type(self)

1751 kwds = C._kwds(self, op) # PYCHOK in _Composite[FHP|GH]

1752 a = C(self, **kwds)

1753 b = _other(self, other)

1754 return a, b, C, kwds

1755

1756 def _inplace(self, r):

1757 # Replace this with a L{Boolean*} result.

1758 self._clips, r._clips = r._clips, None

1759# if self._raiser != r._raiser:

1760# self._raiser = r._raiser

1761# if self._xtend != r._xtend:

1762# self._xtend = r._xtend

1763# if self._eps != r._eps:

1764# self._eps = r._eps

1765 return self

1766

1767

1768 class BooleanFHP(_CompositeFHP, _BooleanBase):

1769 '''I{Composite} class providing I{boolean} operations between two

1770 I{composites} using U{Forster-Hormann-Popa<https://www.ScienceDirect.com/

1771 science/article/pii/S259014861930007X>}'s C++ implementation, transcoded

1772 to pure Python.

1773

1774 The supported operations between (composite) polygon A and B are:

1775

1776 - C = A & B or A &= B, intersection of A and B

1777

1778 - C = A + B or A += B, sum of A and B clips

1779

1780 - C = A | B or A |= B, union of A and B

1781

1782 - A == B or A != B, equivalent A and B clips

1783

1784 - A.isequalTo(B, eps), equivalent within tolerance

1785

1786 @see: Methods C{__eq__} and C{isequalTo}, function L{clipFHP4}

1787 and class L{BooleanGH}.

1788 '''

1789 _kind = _boolean_

1790

1791 def __init__(self, lls, raiser=False, eps=EPS, **name):

1792 '''New L{BooleanFHP} operand for I{boolean} operation.

1793

1794 @arg lls: The polygon points and clips (iterable of L{LatLonFHP}s,

1795 L{ClipFHP4Tuple}s or other C{LatLon}s).

1796 @kwarg raiser: If C{True}, throw L{ClipError} exceptions (C{bool}).

1797 @kwarg esp: Tolerance for eliminating null edges (C{degrees}, same

1798 units as the B{C{lls}} coordinates).

1799 @kwarg name: Optional C{B{name}=NN} (C{str}).

1800 '''

1801 _CompositeFHP.__init__(self, lls, raiser=raiser, eps=eps, **name)

1802

1803 def __isub__(self, other):

1804 '''Not implemented.'''

1805 return _NotImplemented(self, other)

1806

1807 def __rsub__(self, other):

1808 '''Not implemented.'''

1809 return _NotImplemented(self, other)

1810

1811 def __sub__(self, other):

1812 '''Not implemented.'''

1813 return _NotImplemented(self, other)

1814

1815 def _boolean(self, other, Union, unused, op):

1816 # One C{BooleanFHP} operation.

1817 p, q, C, kwds = self._boolean4(other, op)

1818 r = p._clip(q, Union=Union, **kwds)

1819 return C(r, **kwds)

1820

1821

1822 class BooleanGH(_CompositeGH, _BooleanBase):

1823 '''I{Composite} class providing I{boolean} operations between two

1824 I{composites} using the U{Greiner-Hormann<http://www.Inf.USI.CH/

1825 hormann/papers/Greiner.1998.ECO.pdf>} algorithm, extended.

1826

1827 The supported operations between (composite) polygon A and B are:

1828

1829 - C = A - B or A -= B, difference A less B

1830

1831 - C = B - A or B -= A, difference B less B

1832

1833 - C = A & B or A &= B, intersection of A and B

1834

1835 - C = A + B or A += B, sum of A and B clips

1836

1837 - C = A | B or A |= B, union of A and B

1838

1839 - A == B or A != B, equivalent A and B clips

1840

1841 - A.isequalTo(B, eps), equivalent within tolerance

1842

1843 @note: To handle I{degenerate cases} like C{point-edge} and

1844 C{point-point} intersections, use class L{BooleanFHP}.

1845

1846 @see: Methods C{__eq__} and C{isequalTo}, function L{clipGH4}

1847 and class L{BooleanFHP}.

1848 '''

1849 _kind = _boolean_

1850

1851 def __init__(self, lls, raiser=True, xtend=False, eps=EPS, **name):

1852 '''New L{BooleanFHP} operand for I{boolean} operation.

1853

1854 @arg lls: The polygon points and clips (iterable of L{LatLonGH}s,

1855 L{ClipGH4Tuple}s or other C{LatLon}s).

1856 @kwarg raiser: If C{True}, throw L{ClipError} exceptions (C{bool}).

1857 @kwarg xtend: If C{True}, extend edges of I{degenerate cases}, an

1858 attempt to handle the latter (C{bool}).

1859 @kwarg esp: Tolerance for eliminating null edges (C{degrees}, same

1860 units as the B{C{lls}} coordinates).

1861 @kwarg name: Optional C{B{name}=NN} (C{str}).

1862 '''

1863 _CompositeGH.__init__(self, lls, raiser=raiser, xtend=xtend, eps=eps, **name)

1864

1865 def _boolean(self, other, s_entry, c_entry, op):

1866 # One C{BooleanGH} operation.

1867 s, c, C, kwds = self._boolean4(other, op)

1868 r = s._clip(c, s_entry, c_entry, **kwds)

1869 return C(r, **kwds)

1870

1871 def __isub__(self, other):

1872 '''In-place difference: C{this -= other}.

1873 '''

1874 return self._inplace(self.__sub__(other))

1875

1876 def __rsub__(self, other):

1877 ''' Reverse difference: C{other - this}

1878 '''

1879 return _other(self, other).__sub__(self)

1880

1881 def __sub__(self, other):

1882 '''Difference: C{this - other}.

1883 '''

1884 return self._boolean(other, True, False, self.__sub__)

1885

1886

1887 def _alpha1(alpha): # PYCHOK no cover

1888 # Return C{alpha} in C{[0..1]} range

1889 if _EPS0 < alpha < _1EPS:

1890 return max(_0_0, min(alpha, _1_0))

1891 t = _not_(Fmt.SQUARE(_ELLIPSIS_(0, 1)))

1892 raise ClipError(_alpha_, alpha, txt=t)

1893

1894

1895 def _alpha4(a):

1896 # Return 4-tuple (alpha, -EPS < alpha < EPS,

1897 # 0 < alpha < 1,

1898 # not 0 < alpha < 1)

1899 a_EPS = bool(_EPS0 < a < _0EPS)

1900 a_0_1 = bool(_0EPS < a < _EPS1)

1901 return a, a_EPS, a_0_1, (not a_0_1)

1902

1903

1904 def _Cps(Cp, composites_points, where):

1905 # Yield composites and points as a C{Cp} composite.

1906 try:

1907 kwds = dict(kind=_points_, name__=where)

1908 for cp in composites_points:

1909 yield cp if isBoolean(cp) else Cp(cp, **kwds)

1910 except (AttributeError, ClipError, TypeError, ValueError) as x:

1911 raise _ValueError(points=cp, cause=x)

1912

1913

1914 def _eps0(eps):

1915 # Adjust C{eps} or C{0}.

1916 return eps if eps and eps > EPS else 0

1917

1918

1919 def isBoolean(obj):

1920 '''Check for C{Boolean} composites.

1921

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

1923

1924 @return: C{True} if B{C{obj}} is L{BooleanFHP}, L{BooleanGH}

1925 or some other composite, C{False} otherwise.

1926 '''

1927 return isinstance(obj, _CompositeBase)

1928

1929

1930 def _left_right_bottom_top_eps2(p1, p2):

1931 '''(INTERNAL) Return 2-tuple C{((left, right), (bottom, top))}, both oversized.

1932 '''

1933 return (_min_max_eps2(p1.x, p2.x),

1934 _min_max_eps2(p1.y, p2.y))

1935

1936

1937 def _low_high_eps2(lo, hi, eps):

1938 '''(INTERNAL) Return 2-tuple C{(lo, hi)}, oversized.

1939 '''

1940 # assert eps > 0

1941 lo -= fabs(eps * lo)

1942 hi += fabs(eps * hi)

1943 if lo < hi:

1944 pass

1945 elif lo > hi:

1946 lo, hi = hi, lo

1947 else:

1948 lo -= eps

1949 hi += eps

1950 return lo, hi

1951

1952

1953 def _min_max_eps2(*xs):

1954 '''(INTERNAL) Return 2-tuple C{(min, max)}, oversized.

1955 '''

1956 return _low_high_eps2(min(xs), max(xs), EPS)

1957

1958

1959 def _other(this, other):

1960 '''(INTERNAL) Check for compatible C{type}s.

1961 '''

1962 C = type(this)

1963 if isinstance(other, C):

1964 return other

1965 raise _IsnotError(C, other=other)

1966

1967

1968 def _outside(x1, x2, lo, hi):

1969 '''(INTERNAL) Are C{x1} and C{x2} outside C{(lo, hi)}?

1970 '''

1971 # assert lo <= hi

1972 return (x1 < lo or x2 > hi) if x1 > x2 else \

1973 (x2 < lo or x1 > hi)

1974

1975

1976 __all__ += _ALL_DOCS(_BooleanBase, _Clip,

1977 _CompositeBase, _CompositeFHP, _CompositeGH,

1978 _LatLonBool)

1979

1980# **) MIT License

1981#

1982# Copyright (C) 2018-2026 -- mrJean1 at Gmail -- All Rights Reserved.

1983#

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

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

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

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

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

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

1990#

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

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

1993#

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

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

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

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

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

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

2000# OTHER DEALINGS IN THE SOFTWARE.