Coverage for pygeodesy / booleans.py: 95%

969 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-10-21 16:43 -0400

1 

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

3 

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

5 

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

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

8C{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 

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

19 

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

21 _xscalar, typename 

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

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

24 _ValueError, _xattr, _xkwds_get, _xkwds_pop2 

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

26# from pygeodesy.fsums import fsum1 # _MODS 

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

28from 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! 

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

34from pygeodesy.latlonBase import LatLonBase, \ 

35 LatLon2Tuple, Property_RO, property_RO 

36from 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 

42from pygeodesy.units import Height, HeightX 

43from pygeodesy.utily import fabs, _unrollon, _Wrap 

44 

45# from math import fabs # from .utily 

46 

47__all__ = _ALL_LAZY.booleans 

48__version__ = '26.10.02' 

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 

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

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

65 

66 

67class _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 

90class _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 

115class _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) # PYLINT E1120 

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 isenclosedBy(self, *composites_points, **wrap): 

238 '''I{Must be overloaded}.''' 

239 self._notOverloaded(*composites_points, **wrap) 

240 

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

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

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

244 

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

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

247 

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

249 

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

251 ''' 

252 try: 

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

254 except (AttributeError, TypeError, ValueError): 

255 raise _IsnotError(_LatLon_, other=other) 

256 

257 @property_RO 

258 def isintersection(self): 

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

260 ''' 

261 return bool(self._linked) 

262 

263 @property_RO 

264 def ispoint(self): 

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

266 ''' 

267 return self._alpha is None 

268 

269 @property_RO 

270 def lat(self): 

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

272 ''' 

273 return self.y 

274 

275 @property_RO 

276 def latlon(self): 

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

278 ''' 

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

280 

281 def _link(self, other): 

282 # Make this and an other point neighbors. 

283 # assert _other(self, other) 

284 self._linked = other 

285 other._linked = self 

286 

287 @property_RO 

288 def lon(self): 

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

290 ''' 

291 return self.x 

292 

293 def _toClas(self, Clas, clipid): 

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

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

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

297 

298 

299class LatLonFHP(_LatLonBool): 

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

301 ''' 

302 _en_ex = None 

303 _label = None 

304 _2split = None # or C{._Clip} 

305 _2xing = False 

306 

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

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

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

310 C{LatLon} instance. 

311 

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

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

314 

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

316 ''' 

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

318 

319 def __add__(self, other): 

320 _other(self, other) 

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

322 

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

324 _other(self, other) 

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

326 

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

328 _other(self, other) 

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

330 

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

332 _xscalar(other=other) 

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

334 

335# def _edge2(self): 

336# # Return the start and end point of the 

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

338# n = p = self 

339# while p.isintersection: 

340# p = p._prev 

341# if p is self: 

342# break 

343# while n.isintersection: 

344# n = n._next 

345# if n is self: 

346# break 

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

348# return p, n 

349 

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

351 if self._label: 

352 t = NN(self._label, t) 

353 if self._en_ex: 

354 t = NN(t, self._en_ex) 

355 return t 

356 

357 @property_RO 

358 def _isduplicate(self): 

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

360 p = self._dupof 

361 return bool(p and self._linked 

362 and p is not self 

363 and p == self 

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

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

366 

367# @property_RO 

368# def _isduplicated(self): 

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

370# d, v = 0, self 

371# while v: 

372# if v._dupof is self: 

373# d += 1 

374# v = v._next 

375# if v is self: 

376# break 

377# return d 

378 

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

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

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

382 pii/S0925772101000128>}? 

383 

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

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

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

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

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

389 

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

391 

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

393 article/pii/S0925772101000128>}. 

394 ''' 

395 class _Pseudo(object): 

396 # Pseudo-_CompositeBase._clips tuple 

397 

398 @property_RO 

399 def _clips(self): 

400 for cp in _Cps(_CompositeFHP, composites_points, 

401 LatLonFHP.isenclosedBy): # PYCHOK yield 

402 for c in cp._clips: 

403 yield c 

404 

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

406 

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

408 # Is this point inside a composite, excluding 

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

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

411 for c in composite._clips: 

412 if c not in excludes: 

413 w = 0 

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

415 # edge [p1,p2] must straddle y 

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

417 r = p2.x > x 

418 s = p2.y > p1.y 

419 if p1.x < x: 

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

421 else: 

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

423 if b: 

424 w += 1 if s else -1 

425 if isodd(w): 

426 i = not i 

427 return i 

428 

429 @property_RO 

430 def _prev_next2(self): 

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

432 p, n = self, self._next 

433 if self._isduplicate: # PYCHOK no cover 

434 p = self._dupof 

435 while p._isduplicate: 

436 p = p._dupof 

437 while n._isduplicate: 

438 n = n._next 

439 return p._prev, n 

440 

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

442 # Relative Position oracle 

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

444 T = _RP.IS_Pm 

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

446 T = _RP.IS_Pp 

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

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

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

450 _RP.RIGHT # PYCHOK indent 

451 else: # right turn (or straight) 

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

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

454 _RP.LEFT # PYCHOK indent 

455 return T 

456 

457 

458class LatLonGH(_LatLonBool): 

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

460 ''' 

461 _entry = None # entry or exit iff intersection 

462 

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

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

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

466 C{LatLon} instance. 

467 

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

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

470 

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

472 ''' 

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

474 

475 def _check(self): 

476 # Check-mark this vertex and its link. 

477 self._checked = True 

478 k = self._linked 

479 if k and not k._checked: 

480 k._checked = True 

481 

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

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

484 (_e_ if self._entry else _x_)) 

485 

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

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

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

489 article/pii/S0925772101000128>}? 

490 

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

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

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

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

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

496 

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

498 ''' 

499 class _Pseudo(object): 

500 # Pseudo-_CompositeBase._edges3 method 

501 

502 def _edges3(self, **kwds): 

503 for cp in _Cps(_CompositeGH, composites_points, 

504 LatLonGH.isenclosedBy): # PYCHOK yield 

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

506 yield e 

507 

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

509 

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

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

512 

513 def _x(y, p1, p2): 

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

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

516 

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

518 # intersecting a ray emitted from this point to 

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

520 # inside, if I{even} outside. 

521 y, i = self.y, False 

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

523 x = self.x 

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

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

526 r = p2.x > x 

527 if p1.x < x: 

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

529 else: 

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

531 if b: 

532 i = not i 

533 return i 

534 

535 

536class _Clip(_Named): 

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

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

539 and intersections with other clips. 

540 ''' 

541 _composite = None 

542 _dups = 0 

543 _first = None 

544 _id = 0 

545 _identical = False 

546 _noInters = False 

547 _last = None 

548 _LL = None 

549 _len = 0 

550 _pushback = False 

551 

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

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

554 ''' 

555 # assert isinstance(composite, _CompositeBase) 

556 if clipid in composite._clipids: 

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

558 self._composite = composite 

559 self._id = clipid 

560 self._LL = composite._LL 

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

562 

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

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

565 ''' 

566 for v in self: 

567 if v is point: # or ==? 

568 return True 

569 return False 

570 

571 def __eq__(self, other): 

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

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

574 the same order, possibly rotated? 

575 ''' 

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

577 

578 def __ge__(self, other): 

579 '''See method C{__lt__}. 

580 ''' 

581 return not self.__lt__(other) 

582 

583 def __gt__(self, other): 

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

585 located or stretched farther North or East? 

586 ''' 

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

588 

589 def __hash__(self): # PYCHOK no cover 

590 return hash(self._bltr4) 

591 

592 def __iter__(self): 

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

594 ''' 

595 v = f = self._first 

596 while v: 

597 yield v 

598 v = v._next 

599 if v is f: 

600 break 

601 

602 def __le__(self, other): 

603 '''See method C{__gt__}. 

604 ''' 

605 return not self.__gt__(other) 

606 

607 def __len__(self): 

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

609 intersections in this clip. 

610 ''' 

611 return self._len 

612 

613 def __lt__(self, other): 

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

615 located or stretched farther South or West? 

616 ''' 

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

618 

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

620 '''See method C{__eq__}. 

621 ''' 

622 return not self.__eq__(other) 

623 

624 _all = __iter__ 

625 

626 @property_RO 

627 def _all_ON_ON(self): 

628 # Check whether all vertices are ON_ON. 

629 L_ON_ON = _L.ON_ON 

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

631 

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

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

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

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

636 self._len += 1 

637 # assert v._clipid == self._id 

638 

639 v._next = n = self._first 

640 if n is None: # set ._first 

641 self._first = p = n = v 

642 else: # insert before ._first 

643 v._prev = p = n._prev 

644 p._next = n._prev = v 

645 return v 

646 

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

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

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

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

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

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

653# if v._linked: 

654# p._linked = True # to force errors 

655# return p 

656 

657 @Property_RO 

658 def _bltr4(self): 

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

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

661 

662 def _bltr4eps(self, eps): 

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

664 if eps > 0: # > EPS 

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

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

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

668 t = yb, xl, yt, xr 

669 else: 

670 t = self._bltr4 

671 return t 

672 

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

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

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

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

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

678 # un-close the clip 

679 f._prev = p = p._prev 

680 p._next = f 

681 self._len -= 1 

682# elif f and raiser: 

683# raise self._OpenClipError(p, f) 

684 if len(self) < 3: 

685 raise self._Error(_too_(_few_)) 

686 

687 def _dup(self, q): 

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

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

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

691 v._dupof = q._dupof or q 

692 # assert v._prev is q 

693 # assert q._next is v 

694 return v 

695 

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

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

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

699 p1 = p = f = self._first 

700 while p: 

701 p2 = p = p._next 

702 if p.ispoint: 

703 if wrap and p is not f: 

704 p2 = _unrollon(p1, p) 

705 yield p1, p2 

706 p1 = p2 

707 if p is f: 

708 break 

709 

710 def _equi(self, clip, eps): 

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

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

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

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

715 == clip._bltr4eps(eps): 

716 _equi = _LatLonBool._equi 

717 for v in clip: 

718 if _equi(f, v, eps): 

719 s, n = f, v 

720 for _ in range(r): 

721 s, n = s._next, n._next 

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

723 break # next v 

724 else: # equivalent 

725 return True 

726 return False 

727 

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

729 # Build a C{ClipError} instance 

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

731 if self._dups: 

732 kwds.update(dups=self._dups) 

733 cp = self._composite 

734 if self._id: 

735 try: 

736 i = cp._clips.index(self) 

737 if i != self._id: 

738 kwds.update(clip=i) 

739 except ValueError: 

740 pass 

741 kwds[_clipid_] = self._id 

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

743 

744 def _index(self, clips, eps): 

745 # see _CompositeBase._equi 

746 for i, c in enumerate(clips): 

747 if c._equi(self, eps): 

748 return i 

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

750 

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

752 # insertVertex between points C{start} and 

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

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

755 n = start._next 

756 if end_alpha: 

757 end, alpha = end_alpha 

758 v._alpha = alpha 

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

760 # assert start is not end 

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

762 n = n._next 

763 v._next = n 

764 v._prev = p = n._prev 

765 p._next = n._prev = v 

766 self._len += 1 

767# _Clip._bltr4._update(self) 

768# _Clip._ishole._update(self) 

769 return v 

770 

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

772 # insert an intersection or make a point both 

773 if p1_p2_alpha: # intersection on edge 

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

775 else: # intersection at point 

776 v = q 

777 # assert not v._linked 

778 # assert v._alpha is None 

779 return v 

780 

781 def _intersections(self): 

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

783 for v in self: 

784 if v.isintersection: 

785 yield v 

786 

787 @Property_RO 

788 def _ishole(self): # PYCHOK no cover 

789 # Is this clip a hole inside its composite? 

790 v = self._first 

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

792 

793 @property_RO 

794 def _nodups(self): 

795 # Yield all non-duplicates. 

796 for v in self: 

797 if not v._dupof: 

798 yield v 

799 

800 def _noXings(self, Union): 

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

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

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

804 

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

806 # Return a C{CloseError} instance 

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

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

809 

810 def _point2(self, insert): 

811 # getNonIntersectionPoint and -Vertex 

812 if not (insert and self._noInters): 

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

814 return p, None 

815 for n in self._intersections(): 

816 p, _ = n._prev_next2 

817 k = p._linked 

818 if k: 

819 if n._linked not in k._prev_next2: 

820 # create a pseudo-point 

821 k = _0_5 * (p + n) 

822 if insert: 

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

824 r = k # to remove later 

825 else: # no ._prev, ._next 

826 k._clipid = n._clipid 

827 r = None 

828 return k, r 

829 return None, None 

830 

831 def _points(self, may_be=True): 

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

833 for v in self: 

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

835 yield v 

836 

837 def _remove2(self, v): 

838 # Remove vertex C{v}. 

839 # assert not v._isduplicated 

840 if len(self) > 1: 

841 p = v._prev 

842 p._next = n = v._next 

843 n._prev = p 

844 if self._first is v: 

845 self._first = n 

846 if self._last is v: 

847 self._last = p 

848 self._len -= 1 

849 else: 

850 n = self._last = \ 

851 p = self._first = None 

852 self._len = 0 

853 return p, n 

854 

855 def _update_all(self): # PYCHOK no cover 

856 # Zap the I{cached} properties. 

857 _Clip._bltr4._update( self) 

858 _Clip._ishole._update(self) 

859 return self # for _special_identicals 

860 

861 def _Xings(self): 

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

863 CROSSING = _L.CROSSING 

864 for v in self._intersections(): 

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

866 yield v 

867 

868 

869class _CompositeBase(_Named): 

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

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

872 ''' 

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

874 _eps = EPS # null edges 

875 _kind = _corners_ 

876 _LL = _LatLonBool # shut up PyChecker 

877 _raiser = False 

878 _xtend = False 

879 

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

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

882 ''' 

883 n = _name__(name, _or_nameof=lls) 

884 if n: 

885 self.name = n 

886 if kind: 

887 self._kind = kind 

888 if self._eps < eps: 

889 self._eps = eps 

890 

891 c = _Clip(self) 

892 lp = None 

893 for ll in lls: 

894 ll = self._LL(ll) 

895 if lp is None: 

896 c._id = ll._clipid # keep clipid 

897 lp = c._append(ll) 

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

899 c._closed(self.raiser) 

900 c = _Clip(self, ll._clipid) 

901 lp = c._append(ll) 

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

903 lp = c._append(ll) 

904 else: 

905 c._dups += 1 

906 c._closed(self.raiser) 

907 

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

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

910 ''' 

911 for c in self._clips: 

912 if point in c: 

913 return True 

914 return False 

915 

916 def __eq__(self, other): 

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

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

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

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

921 possibly rotated. 

922 ''' 

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

924 

925 def __iter__(self): 

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

927 ''' 

928 for c in self._clips: 

929 for v in c: 

930 yield v 

931 

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

933 '''See method C{__eq__}. 

934 ''' 

935 return not self.__eq__(other) 

936 

937 def __len__(self): 

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

939 ''' 

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

941 

942 def __repr__(self): 

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

944 ''' 

945 c = len(self._clips) 

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

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

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

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

950 

951 def __str__(self): 

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

953 ''' 

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

955 

956 @property_RO 

957 def _bottom_top_eps2(self): 

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

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

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

961 

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

963 # Return a new instance 

964 _g = kwds.get 

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

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

967 

968 @property_RO 

969 def _clipids(self): # PYCHOK no cover 

970 for c in self._clips: 

971 yield c._id 

972 

973 def clipids(self): 

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

975 ''' 

976 return tuple(self._clipids) 

977 

978# def _clipidups(self, other): 

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

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

981 

982 def _edges3(self, **raiser_wrap): 

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

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

985 for c in self._clips: 

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

987 yield p1, p2, c 

988 

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

990 # see function .points.isenclosedBy 

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

992 

993 @property 

994 def eps(self): 

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

996 ''' 

997 return self._eps 

998 

999 @eps.setter # PYCHOK setter! 

1000 def eps(self, eps): 

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

1002 ''' 

1003 self._eps = eps 

1004 

1005 def _10eps(self, **eps_): 

1006 # Get eps for _LatLonBool._2Abs 

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

1008 if e != EPS: 

1009 e *= _10EPS / EPS 

1010 else: 

1011 e = _10EPS 

1012 return e 

1013 

1014 def _equi(self, other, eps): 

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

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

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

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

1019 if eps > 0: 

1020 _index = _Clip._index 

1021 else: 

1022 def _index(c, cs, unused): 

1023 return cs.index(c) 

1024 try: 

1025 cs = list(sorted(cs)) 

1026 for c in sorted(co): 

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

1028 except ValueError: # from ._index 

1029 pass 

1030 return False if cs else True 

1031 else: # both null? 

1032 return False if cs or co else True 

1033 

1034 def _intersections(self): 

1035 # Yield all intersections. 

1036 for c in self._clips: 

1037 for v in c._intersections(): 

1038 yield v 

1039 

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

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

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

1043 

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

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

1046 

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

1048 

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

1050 

1051 @see: Method C{__eq__}. 

1052 ''' 

1053 if isinstance(other, _CompositeBase): 

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

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

1056 

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

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

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

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

1061 kwds.update(more) 

1062 return kwds 

1063 

1064 @property_RO 

1065 def _left_right_eps2(self): 

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

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

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

1069 

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

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

1072 for c in self._clips: 

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

1074 yield v 

1075 

1076 @property 

1077 def raiser(self): 

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

1079 ''' 

1080 return self._raiser 

1081 

1082 @raiser.setter # PYCHOK setter! 

1083 def raiser(self, throw): 

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

1085 ''' 

1086 self._raiser = bool(throw) 

1087 

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

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

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

1091 e = self._10eps(**eps) 

1092 for clipid, ns in enumerate(_presults): 

1093 f = p = v = None 

1094 for n in ns: 

1095 if f is None: 

1096 yield n._toClas(C, clipid) 

1097 f = p = n 

1098 elif v is None: 

1099 v = n # got f, p, v 

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

1101 yield v._toClas(C, clipid) 

1102 p, v = v, n 

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

1104 v = n 

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

1106 yield v._toClas(C, clipid) 

1107 p = v 

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

1109 yield f._toClas(C, clipid) 

1110 

1111 def _sum(self, other, op): 

1112 # Combine this and an C{other} composite 

1113 LL = self._LL 

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

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

1116 for cp in (self, other): 

1117 for c in cp._clips: 

1118 _ap = _Clip(sp, sid)._append 

1119 for v in c._nodups: 

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

1121 sid += 1 

1122 return sp 

1123 

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

1125 # Sum the area or perimeter of all clips 

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

1127 

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

1129 # Sum the area or perimeter of all clips 

1130 

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

1132 _LL = LL 

1133 for v in clip: 

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

1135 

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

1137 

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

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

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

1141 

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

1143 L{LatLonFHP} or L{LatLonGH}. 

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

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

1146 keyword arguments, ignore if 

1147 C{B{LatLon} is None}. 

1148 

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

1150 

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

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

1153 ''' 

1154 if LatLon is None: 

1155 LL, kwds = self._LL, {} 

1156 elif issubclassof(LatLon, _LatLonBool, LatLonBase): 

1157 LL, kwds = LatLon, LatLon_kwds 

1158 else: 

1159 raise _TypeError(LatLon=LatLon) 

1160 

1161 for c in self._clips: 

1162 lf, cid = None, c._id 

1163 for v in c._nodups: 

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

1165 ll._height = v.height 

1166 if ll._clipid != cid: 

1167 ll._clipid = cid 

1168 yield ll 

1169 if lf is None: 

1170 lf = ll 

1171 if closed and lf: 

1172 yield lf 

1173 

1174 

1175class _CompositeFHP(_CompositeBase): 

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

1177 of L{LatLonFHP} points, duplicates and intersections 

1178 with an other I{composite}. 

1179 ''' 

1180 _LL = LatLonFHP 

1181 _Union = False 

1182 

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

1184 # New L{_CompositeFHP}. 

1185 if raiser: 

1186 self._raiser = True 

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

1188 

1189 def _classify(self): 

1190 # 2) Classify intersection chains. 

1191 L = _L 

1192 for v in self._intersections(): 

1193 n, b = v, v._label 

1194 if b in L.RIGHT_LEFT_ON: # next chain 

1195 while True: 

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

1197 n = n._next 

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

1199 break 

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

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

1202 

1203 # 3) Copy labels 

1204 for v in self._intersections(): 

1205 v._linked._label = v._label 

1206 

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

1208 **closed_inull_raiser_eps): 

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

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

1211 P = self 

1212 Q = self._class(corners, closed_inull_raiser_eps, 

1213 eps=P._eps, raiser=False) 

1214 if Union: 

1215 P._Union = Q._Union = True 

1216 

1217 bt = Q._bottom_top_eps2 

1218 lr = Q._left_right_eps2 

1219 # compute and insert intersections 

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

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

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

1223 e = _EdgeFHP(p1, p2) 

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

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

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

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

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

1229 # assert not p._linked 

1230 # assert not q._linked 

1231 p._link(q) 

1232 

1233 # label and classify intersections 

1234 P._labelize() 

1235 P._classify() 

1236 

1237 # check for special cases 

1238 P._special_cases(Q) 

1239 Q._special_cases(P) 

1240 # handle identicals 

1241 P._special_identicals(Q) 

1242 

1243 # set Entry/Exit flags 

1244 P._set_entry_exits(Q) 

1245 Q._set_entry_exits(P) 

1246 

1247 # handle splits and crossings 

1248 P._splits_xings(Q) 

1249 

1250 # yield the results 

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

1252 

1253 @property_RO 

1254 def _identicals(self): 

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

1256 for c in self._clips: 

1257 if c._identical: 

1258 yield c 

1259 

1260 def _labelize(self): 

1261 # 1) Intersections classification 

1262 for p in self._intersections(): 

1263 q = p._linked 

1264 # determine local configuration at this intersection 

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

1266 p1, p3 = p._prev_next2 

1267 q1, q3 = q._prev_next2 

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

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

1270 # check intersecting and overlapping cases 

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

1272 

1273 def _presults(self, other): 

1274 # Yield the result clips, each as a generator 

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

1276 for cp in (self, other): 

1277 for c in cp._clips: 

1278 if c._pushback: 

1279 yield c._all() 

1280 for c in self._clips: 

1281 for X in c._Xings(): 

1282 yield self._resultX(X) 

1283 

1284 def _resultX(self, X): 

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

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

1287 while v: 

1288 v._checked = True 

1289 r = v # in P or Q 

1290 s = L.Toggle[v._en_ex] 

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

1292 while True: 

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

1294 yield v 

1295 v._checked = True 

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

1297 break 

1298 if v is r: # full circle 

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

1300 if v is not X: 

1301 v = v._linked 

1302 if v is X: 

1303 break 

1304 

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

1306 # 4) Set entry/exit flags 

1307 L, U = _L, self._Union 

1308 for c in self._clips: 

1309 n, k = c._point2(True) 

1310 if n: 

1311 f = n 

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

1313 t = L.EXIT # first_chain_vertex = True 

1314 while True: 

1315 if n.isintersection: 

1316 b = n._label 

1317 if b is L.CROSSING: 

1318 n._en_ex = s 

1319 s = L.Toggle[s] 

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

1321 n._2split = c # see ._splits_xings 

1322 elif b is L.CROSSING_D: 

1323 n._en_ex = s 

1324 if (s is t) ^ U: 

1325 n._label = L.CROSSING 

1326 t = L.Toggle[t] 

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

1328 s = L.Toggle[s] 

1329 elif b is L.BOUNCING_D: 

1330 n._en_ex = s 

1331 if (s is t) ^ U: 

1332 n._2xing = True # see ._splits_xings 

1333 s = L.Toggle[s] 

1334 t = L.Toggle[t] 

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

1336 if n is f: 

1337 break # PYCHOK attr? 

1338 if k: 

1339 c._remove2(k) 

1340 

1341 def _special_cases(self, other): 

1342 # 3.5) Check special cases 

1343 U = self._Union 

1344 for c in self._clips: 

1345 if c._noXings(U): 

1346 c._noInters = True 

1347 if c._all_ON_ON: 

1348 c._identical = True 

1349 else: 

1350 p, _ = c._point2(False) 

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

1352 c._pushback = True 

1353 

1354 def _special_identicals(self, other): 

1355 # 3.5) Handle identicals 

1356 _u = _Clip._update_all 

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

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

1359 if cds: # PYCHOK no cover 

1360 for c in self._identicals: 

1361 c._update_all() 

1362 for v in c._intersections(): 

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

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

1365 c._pushback = True 

1366 break # next c 

1367 

1368 @property_RO 

1369 def _2splits(self): 

1370 # Yield all intersections marked C{._2split} 

1371 for p in self._intersections(): 

1372 if p._2split: 

1373 # assert isinstance(p._2split, _Clip) 

1374 yield p 

1375 

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

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

1378 

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

1380 p1, p2 = p._prev_next2 

1381 ap = p1._2A(p, p2) 

1382 Pc = p._2split 

1383 # assert Pc in P._clips 

1384 # assert p in Pc 

1385 return ap, Pc._dup(p) 

1386 

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

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

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

1390 if id_qs: 

1391 for p in ps: 

1392 q = p._linked 

1393 if q and id(q) in id_qs: 

1394 yield p, q 

1395 

1396 L = _L 

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

1398 X = L.Toggle[E] 

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

1400 ap, pp = _2A_dup2(p, self) 

1401 aq, qq = _2A_dup2(q, other) 

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

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

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

1405 else: 

1406 pp._link(qq) 

1407 p._en_ex = q._en_ex = E 

1408 pp._en_ex = qq._en_ex = X 

1409 p._label = pp._label = \ 

1410 q._label = qq._label = L.CROSSING 

1411 

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

1413 p._label = q._label = L.CROSSING 

1414 

1415 @property_RO 

1416 def _2xings(self): 

1417 # Yield all intersections marked C{._2xing} 

1418 for p in self._intersections(): 

1419 if p._2xing: 

1420 yield p 

1421 

1422 

1423class _CompositeGH(_CompositeBase): 

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

1425 of L{LatLonGH} points, duplicates and intersections 

1426 with an other I{composite}. 

1427 ''' 

1428 _LL = LatLonGH 

1429 _xtend = False 

1430 

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

1432 # New L{_CompositeGH}. 

1433 if xtend: 

1434 self._xtend = True 

1435 elif raiser: 

1436 self._raiser = True 

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

1438 

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

1440 **closed_inull_raiser_xtend_eps): 

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

1442 # Greiner/Hormann's algorithm extended to optionally 

1443 # handle some so-called "degenerate cases" 

1444 S = self 

1445 C = self._class(corners, closed_inull_raiser_xtend_eps, 

1446 raiser=False, xtend=False) 

1447 bt = C._bottom_top_eps2 

1448 lr = C._left_right_eps2 

1449 # 1. find intersections 

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

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

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

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

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

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

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

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

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

1459 s._link(c) 

1460 

1461 # 2. identify entry/exit intersections 

1462 if S._first: 

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

1464 for v in S._intersections(): 

1465 v._entry = s_entry = not s_entry 

1466 

1467 if C._first: 

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

1469 for v in C._intersections(): 

1470 v._entry = c_entry = not c_entry 

1471 

1472 # 3. yield the result(s) 

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

1474 

1475 @property_RO 

1476 def _first(self): 

1477 # Get the very first vertex of the first clip 

1478 for v in self: 

1479 return v 

1480 return None # PYCHOK no cover 

1481 

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

1483 # Get the kwds C{dict}. 

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

1485 

1486 def _presults(self): 

1487 # Yield the unchecked intersection(s). 

1488 for c in self._clips: 

1489 for v in c._intersections(): 

1490 if not v._checked: 

1491 yield self._resultU(v) 

1492 

1493 def _resultU(self, v): 

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

1495 while v and not v._checked: 

1496 v._check() 

1497 yield v 

1498 r = v 

1499 e = v._entry 

1500 while True: 

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

1502 yield v 

1503 if v._linked: 

1504 break 

1505 if v is r: 

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

1507 v = v._linked # switch 

1508 

1509 @property 

1510 def xtend(self): 

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

1512 ''' 

1513 return self._xtend 

1514 

1515 @xtend.setter # PYCHOK setter! 

1516 def xtend(self, xtend): 

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

1518 ''' 

1519 self._xtend = bool(xtend) 

1520 

1521 

1522class _EdgeFHP(object): 

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

1524 

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

1526 X_OVERLAP = _Cnum('Xo', 5) 

1527 P_INTERSECT = _Cnum('Pi', 3) 

1528 P_OVERLAP = _Cnum('Po', 7) 

1529 Ps = (P_INTERSECT, P_OVERLAP, X_OVERLAP) 

1530 Q_INTERSECT = _Cnum('Qi', 2) 

1531 Q_OVERLAP = _Cnum('Qo', 6) 

1532 Qs = (Q_INTERSECT, Q_OVERLAP, X_OVERLAP) 

1533 V_INTERSECT = _Cnum('Vi', 4) 

1534 V_OVERLAP = _Cnum('Vo', 8) 

1535 Vs = (V_INTERSECT, V_OVERLAP) 

1536 

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

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

1539 self._p1_p2 = p1, p2 

1540 self._dp = dp = p2 - p1 

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

1542 

1543 self._lr, \ 

1544 self._bt = _left_right_bottom_top_eps2(p1, p2) 

1545 

1546 def _intersect3(self, q1, q2): 

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

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

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

1550 dq = q2 - q1 

1551 dq2 = dq * dq # dot product, hypot2 

1552 if dq2 > EPS2: # like ._clip 

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

1554 p1, p2 = self._p1_p2 

1555 ap1 = p1._2A(q1, q2) 

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

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

1558 aq1 = q1._2A(p1, p2) 

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

1560 if fabs(aq2_1) > _0EPS: 

1561 # compute and classify alpha and beta 

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

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

1564 # distinguish intersection types 

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

1566 _E.P_INTERSECT if a_0_1 and b_0 else ( 

1567 _E.Q_INTERSECT if a_0 and b_0_1 else ( 

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

1569 

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

1571 dp = self._dp 

1572 d1 = q1 - p1 

1573 # compute and classify alpha and beta 

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

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

1576 # distinguish overlap type 

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

1578 _E.P_OVERLAP if a_0_1 and _b_0_1 else ( 

1579 _E.Q_OVERLAP if _a_0_1 and b_0_1 else ( 

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

1581 

1582 if T: 

1583 if T is _E.X_INTERSECT: 

1584 v = p1 + a * self._dp # PYLINT E0606 

1585 yield T, (v, p1, p2, a), (v, q1, q2, b) # PYLINT E0606 

1586 elif T in _E.Vs: 

1587 yield T, (p1,), (q1,) 

1588 else: 

1589 if T in _E.Qs: 

1590 yield T, (p1,), (p1, q1, q2, b) # PYLINT E0606 

1591 if T in _E.Ps: 

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

1593 

1594 

1595class _EdgeGH(object): 

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

1597 

1598 _raiser = False 

1599 _xtend = False 

1600 

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

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

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

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

1605 s1.y, s2.y - s1.y) 

1606 self._lr, \ 

1607 self._bt = _left_right_bottom_top_eps2(s1, s2) 

1608 

1609 if xtend: 

1610 self._xtend = True 

1611 elif raiser: 

1612 self._raiser = True 

1613 

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

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

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

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

1618 return a, fabs(d) 

1619 

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

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

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

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

1624 

1625 @Property_RO 

1626 def _hypot0(self): 

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

1628 return hypot(sx, sy) * _0EPS 

1629 

1630 @Property_RO 

1631 def _hypot2(self): 

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

1633 return hypot2(sx, sy) 

1634 

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

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

1637 

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

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

1640 # coordinates x and y and both alphas. 

1641 

1642 # @raise ClipError: Intersection unhandled. 

1643 

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

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

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

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

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

1649 x, sx, \ 

1650 y, sy = self._x_sx_y_sy 

1651 

1652 cx = c2.x - c1_x 

1653 cy = c2.y - c1_y 

1654 d = cy * sx - cx * sy 

1655 

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

1657 dx = x - c1_x 

1658 dy = y - c1_y 

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

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

1661 _EPS0 < ca < _1EPS): 

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

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

1664 _EPS0 < sa < _1EPS): 

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

1666 

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

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

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

1670 

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

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

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

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

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

1676 

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

1678 # non-null, parallel or colinear edges 

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

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

1681 if max(d1, d2) < _0EPS: 

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

1683 if sa1 > sa2: # anti-parallel 

1684 sa1, sa2 = sa2, sa1 

1685 ca1, ca2 = _1_0, _0_0 

1686 else: # parallel 

1687 ca1, ca2 = _0_0, _1_0 

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

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

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

1691 ca *= ca1 + sa1 

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

1693 else: # c1 is between s1 and s2 

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

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

1696 ca *= sa2 - _1_0 

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

1698 else: # c2 is between s1 and s2 

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

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

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

1702 

1703 

1704class _BooleanBase(object): 

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

1706 

1707 _clips = () # PYLINT W0201 

1708 

1709 def __add__(self, other): 

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

1711 ''' 

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

1713 

1714 def __and__(self, other): 

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

1716 ''' 

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

1718 

1719 def __iadd__(self, other): 

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

1721 ''' 

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

1723 

1724 def __iand__(self, other): 

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

1726 ''' 

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

1728 

1729 def __ior__(self, other): 

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

1731 ''' 

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

1733 

1734 def __or__(self, other): 

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

1736 ''' 

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

1738 

1739 def __radd__(self, other): 

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

1741 ''' 

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

1743 

1744 def __rand__(self, other): 

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

1746 ''' 

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

1748 

1749 def __ror__(self, other): 

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

1751 ''' 

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

1753 

1754 def _boolean4(self, other, op): 

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

1756 C = type(self) 

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

1758 a = C(self, **kwds) 

1759 b = _other(self, other) 

1760 return a, b, C, kwds 

1761 

1762 def _inplace(self, r): 

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

1764 self._clips, r._clips = r._clips, None 

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

1766# self._raiser = r._raiser 

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

1768# self._xtend = r._xtend 

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

1770# self._eps = r._eps 

1771 return self 

1772 

1773 

1774class BooleanFHP(_CompositeFHP, _BooleanBase): 

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

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

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

1778 to pure Python. 

1779 

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

1781 

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

1783 

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

1785 

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

1787 

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

1789 

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

1791 

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

1793 and class L{BooleanGH}. 

1794 ''' 

1795 _kind = _boolean_ 

1796 

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

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

1799 

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

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

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

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

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

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

1806 ''' 

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

1808 

1809 def __isub__(self, other): 

1810 '''Not implemented.''' 

1811 return _NotImplemented(self, other) 

1812 

1813 def __rsub__(self, other): 

1814 '''Not implemented.''' 

1815 return _NotImplemented(self, other) 

1816 

1817 def __sub__(self, other): 

1818 '''Not implemented.''' 

1819 return _NotImplemented(self, other) 

1820 

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

1822 # One C{BooleanFHP} operation. 

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

1824 r = p._clip(q, Union=Union, **kwds) # PYLINT E1101 

1825 return C(r, **kwds) 

1826 

1827 

1828class BooleanGH(_CompositeGH, _BooleanBase): 

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

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

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

1832 

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

1834 

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

1836 

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

1838 

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

1840 

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

1842 

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

1844 

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

1846 

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

1848 

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

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

1851 

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

1853 and class L{BooleanFHP}. 

1854 ''' 

1855 _kind = _boolean_ 

1856 

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

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

1859 

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

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

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

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

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

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

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

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

1868 ''' 

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

1870 

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

1872 # One C{BooleanGH} operation. 

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

1874 r = s._clip(c, s_entry, c_entry, **kwds) # PYLINT E1101 

1875 return C(r, **kwds) 

1876 

1877 def __isub__(self, other): 

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

1879 ''' 

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

1881 

1882 def __rsub__(self, other): 

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

1884 ''' 

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

1886 

1887 def __sub__(self, other): 

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

1889 ''' 

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

1891 

1892 

1893def _alpha1(alpha): # PYCHOK no cover 

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

1895 if _EPS0 < alpha < _1EPS: 

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

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

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

1899 

1900 

1901def _alpha4(a): 

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

1903 # 0 < alpha < 1, 

1904 # not 0 < alpha < 1) 

1905 a_EPS = bool(_EPS0 < a < _0EPS) 

1906 a_0_1 = bool(_0EPS < a < _EPS1) 

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

1908 

1909 

1910def _Cps(Cp, composites_points, where): 

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

1912 try: 

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

1914 for cp in composites_points: 

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

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

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

1918 

1919 

1920def _eps0(eps): 

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

1922 return eps if eps and eps > EPS else 0 

1923 

1924 

1925def isBoolean(obj): 

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

1927 

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

1929 

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

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

1932 ''' 

1933 return isinstance(obj, _CompositeBase) 

1934 

1935 

1936def _left_right_bottom_top_eps2(p1, p2): 

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

1938 ''' 

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

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

1941 

1942 

1943def _low_high_eps2(lo, hi, eps): 

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

1945 ''' 

1946 # assert eps > 0 

1947 lo -= fabs(eps * lo) 

1948 hi += fabs(eps * hi) 

1949 if lo < hi: 

1950 pass 

1951 elif lo > hi: 

1952 lo, hi = hi, lo 

1953 else: 

1954 lo -= eps 

1955 hi += eps 

1956 return lo, hi 

1957 

1958 

1959def _min_max_eps2(*xs): 

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

1961 ''' 

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

1963 

1964 

1965def _other(this, other): 

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

1967 ''' 

1968 C = type(this) 

1969 if isinstance(other, C): 

1970 return other 

1971 raise _IsnotError(C, other=other) 

1972 

1973 

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

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

1976 ''' 

1977 # assert lo <= hi 

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

1979 (x2 < lo or x1 > hi) 

1980 

1981 

1982__all__ += _ALL_DOCS(_BooleanBase, _Clip, 

1983 _CompositeBase, _CompositeFHP, _CompositeGH, 

1984 _LatLonBool) 

1985 

1986# **) MIT License 

1987# 

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

1989# 

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

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

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

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

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

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

1996# 

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

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

1999# 

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

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

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

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

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

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

2006# OTHER DEALINGS IN THE SOFTWARE.