Coverage for pygeodesy/booleans.py: 95%
969 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-26 09:04 -0400
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-26 09:04 -0400
2# -*- coding: utf-8 -*-
4u'''I{Boolean} operations on I{composite} polygons and I{clip}s.
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}.
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.
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 ;
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
45# from math import fabs # from .utily
47__all__ = _ALL_LAZY.booleans
48__version__ = '26.10.02'
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
56_alpha_ = 'alpha'
57_boolean_ = 'boolean'
58_case_ = 'case'
59_corners_ = 'corners'
60_open_ = 'open'
63def _Cnum(txt, enum): # PYCHOK unused
64 return txt # NN(txt, _TILDE_, enum)
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}
87_L = _L() # PYCHOK singleton
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)
96_RP = _RP() # PYCHOK singleton
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}
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
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.
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)
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
159 def __abs__(self):
160 return max(fabs(self.x), fabs(self.y))
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)
167 def __ne__(self, other): # required for Python 2 # PYCHOK no cover
168 return not self.__eq__(other)
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
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)
200 def __sub__(self, other):
201 _other(self, other)
202 return type(self)(self.y - other.y, # classof
203 self.x - other.x)
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)
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
217 @property_RO
218 def clipid(self):
219 '''Get the I{clipid} (C{int} or C{0}).
220 '''
221 return self._clipid
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)
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)
237 def isenclosedBy(self, *composites_points, **wrap):
238 '''I{Must be overloaded}.'''
239 self._notOverloaded(*composites_points, **wrap)
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}?
245 @arg other: The other point (C{LatLon}).
246 @kwarg eps: Tolerance for equality (C{degrees} or C{None}).
248 @return: C{True} if equivalent, C{False} otherwise (C{bool}).
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)
257 @property_RO
258 def isintersection(self):
259 '''Is this an intersection? May be C{ispoint} too!
260 '''
261 return bool(self._linked)
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
269 @property_RO
270 def lat(self):
271 '''Get the latitude (C{scalar}).
272 '''
273 return self.y
275 @property_RO
276 def latlon(self):
277 '''Get the lat- and longitude (L{LatLon2Tuple}).
278 '''
279 return LatLon2Tuple(self.y, self.x)
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
287 @property_RO
288 def lon(self):
289 '''Get the longitude (C{scalar}).
290 '''
291 return self.x
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)
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
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.
312 @arg lat_ll: Latitude (C{scalar}) or a lat-/longitude (L{LatLonFHP},
313 L{ClipFHP4Tuple} or some other C{LatLon}).
315 @see: L{Here<_LatLonBool.__init__>} for further details.
316 '''
317 _LatLonBool.__init__(self, lat_ll, lon, height, clipid, **wrap_name)
319 def __add__(self, other):
320 _other(self, other)
321 return type(self)(self.y + other.y, self.x + other.x)
323 def __mod__(self, other): # cross product
324 _other(self, other)
325 return fdot_(self.x, other.y, -self.y, other.x)
327 def __mul__(self, other): # dot product
328 _other(self, other)
329 return fdot_(self.x, other.x, self.y, other.y)
331 def __rmul__(self, other): # scalar product
332 _xscalar(other=other)
333 return type(self)(self.y * other, self.x * other)
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
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
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))
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
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>}?
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}).
390 @raise ValueError: Some C{points} invalid.
392 @see: U{Algorithm 6<https://www.ScienceDirect.com/science/
393 article/pii/S0925772101000128>}.
394 '''
395 class _Pseudo(object):
396 # Pseudo-_CompositeBase._clips tuple
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
405 return self._isinside(_Pseudo(), **wrap)
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
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
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
458class LatLonGH(_LatLonBool):
459 '''A point or intersection in a L{BooleanGH} clip or composite.
460 '''
461 _entry = None # entry or exit iff intersection
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.
468 @arg lat_ll: Latitude (C{scalar}) or a lat-/longitude (L{LatLonGH},
469 L{ClipGH4Tuple} or some other C{LatLon}).
471 @see: L{Here<_LatLonBool.__init__>} for further details.
472 '''
473 _LatLonBool.__init__(self, lat_ll, lon, height, clipid, **wrap_name)
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
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_))
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>}?
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}).
497 @raise ValueError: Some B{C{points}} invalid.
498 '''
499 class _Pseudo(object):
500 # Pseudo-_CompositeBase._edges3 method
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
508 return self._isinside(_Pseudo(), **wrap)
510 def _isinside(self, composite, *bottom_top, **wrap):
511 # Is this vertex inside the composite? I{even-odd rule}?
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)
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
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
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,)
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
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)
578 def __ge__(self, other):
579 '''See method C{__lt__}.
580 '''
581 return not self.__lt__(other)
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
589 def __hash__(self): # PYCHOK no cover
590 return hash(self._bltr4)
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
602 def __le__(self, other):
603 '''See method C{__gt__}.
604 '''
605 return not self.__gt__(other)
607 def __len__(self):
608 '''Return the number of points, duplicates and
609 intersections in this clip.
610 '''
611 return self._len
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
619 def __ne__(self, other): # required for Python 2 # PYCHOK no cover
620 '''See method C{__eq__}.
621 '''
622 return not self.__eq__(other)
624 _all = __iter__
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)
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
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
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
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))
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
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_))
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
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
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
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)
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)
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
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
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
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
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
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())
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))
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
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
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
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
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
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
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
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)
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
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)
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
932 def __ne__(self, other): # required for Python 2
933 '''See method C{__eq__}.
934 '''
935 return not self.__eq__(other)
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
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)
951 def __str__(self):
952 '''String C{str} of this composite.
953 '''
954 return _COMMASPACE_.join(map(str, self))
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))
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)
968 @property_RO
969 def _clipids(self): # PYCHOK no cover
970 for c in self._clips:
971 yield c._id
973 def clipids(self):
974 '''Return a tuple with all C{clipid}s, I{ordered}.
975 '''
976 return tuple(self._clipids)
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)))
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
989 def _encloses(self, lat, lon, **wrap):
990 # see function .points.isenclosedBy
991 return self._LL(lat, lon).isenclosedBy(self, **wrap)
993 @property
994 def eps(self):
995 '''Get the null edges tolerance (C{degrees}, usually).
996 '''
997 return self._eps
999 @eps.setter # PYCHOK setter!
1000 def eps(self, eps):
1001 '''Set the null edges tolerance (C{degrees}, usually).
1002 '''
1003 self._eps = eps
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
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
1034 def _intersections(self):
1035 # Yield all intersections.
1036 for c in self._clips:
1037 for v in c._intersections():
1038 yield v
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?
1044 @arg other: The other boolean/composite (C{Boolean[FHP|GB]}).
1045 @kwarg eps: Tolerance for equality (C{degrees} or C{None}).
1047 @return: C{True} if equivalent, C{False} otherwise (C{bool}).
1049 @raise TypeError: Invalid B{C{other}}.
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)
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
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))
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
1076 @property
1077 def raiser(self):
1078 '''Get the option to throw L{ClipError} exceptions (C{bool}).
1079 '''
1080 return self._raiser
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)
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)
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
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))
1128 def _sum2(self, LL, _a_p, *args, **kwds): # in .sphericalNvector, -Trigonometry
1129 # Sum the area or perimeter of all clips
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
1136 return _MODS.fsums.fsum1((_a_p(_lls(c), *args, **kwds) for c in self._clips))
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}}.
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}.
1149 @raise TypeError: Invalid B{C{LatLon}}.
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)
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
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
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)
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
1203 # 3) Copy labels
1204 for v in self._intersections():
1205 v._linked._label = v._label
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
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)
1233 # label and classify intersections
1234 P._labelize()
1235 P._classify()
1237 # check for special cases
1238 P._special_cases(Q)
1239 Q._special_cases(P)
1240 # handle identicals
1241 P._special_identicals(Q)
1243 # set Entry/Exit flags
1244 P._set_entry_exits(Q)
1245 Q._set_entry_exits(P)
1247 # handle splits and crossings
1248 P._splits_xings(Q)
1250 # yield the results
1251 return P._results(P._presults(Q), Clas, **closed_inull_raiser_eps)
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
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)
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)
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
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)
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
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
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
1376 def _splits_xings(self, other): # MCCABE 15
1377 # 5) Handle split pairs and 6) crossing candidates
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)
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
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
1412 for p, q in _links2(self._2xings, other._2xings):
1413 p._label = q._label = L.CROSSING
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
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
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)
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)
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
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
1472 # 3. yield the result(s)
1473 return S._results(S._presults(), Clas, **closed_inull_raiser_xtend_eps)
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
1482 def _kwds(self, op, **more):
1483 # Get the kwds C{dict}.
1484 return _CompositeBase._kwds(self, op, xtend=self.xtend, **more)
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)
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
1509 @property
1510 def xtend(self):
1511 '''Get the option to handle some I{degenerate cases} (C{bool}).
1512 '''
1513 return self._xtend
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)
1522class _EdgeFHP(object):
1523 # An edge between two L{LatLonFHP} points.
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)
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
1543 self._lr, \
1544 self._bt = _left_right_bottom_top_eps2(p1, p2)
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)))
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)))
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,)
1595class _EdgeGH(object):
1596 # An edge between two L{LatLonGH} points.
1598 _raiser = False
1599 _xtend = False
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)
1609 if xtend:
1610 self._xtend = True
1611 elif raiser:
1612 self._raiser = True
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)
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)
1625 @Property_RO
1626 def _hypot0(self):
1627 _, sx, _, sy = self._x_sx_y_sy
1628 return hypot(sx, sy) * _0EPS
1630 @Property_RO
1631 def _hypot2(self):
1632 _, sx, _, sy = self._x_sx_y_sy
1633 return hypot2(sx, sy)
1635 def _intersect4(self, c1, c2, parallel=True): # MCCABE 14
1636 # Yield the intersection(s) of this and another edge.
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.
1642 # @raise ClipError: Intersection unhandled.
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
1652 cx = c2.x - c1_x
1653 cy = c2.y - c1_y
1654 d = cy * sx - cx * sy
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
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
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)
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)
1704class _BooleanBase(object):
1705 # Shared C{Boolean[FHP|GH]} methods.
1707 _clips = () # PYLINT W0201
1709 def __add__(self, other):
1710 '''Sum: C{this + other} clips.
1711 '''
1712 return self._sum(_other(self, other), self.__add__) # PYCHOK OK
1714 def __and__(self, other):
1715 '''Intersection: C{this & other}.
1716 '''
1717 return self._boolean(other, False, False, self.__and__) # PYCHOK OK
1719 def __iadd__(self, other):
1720 '''In-place sum: C{this += other} clips.
1721 '''
1722 return self._inplace(self.__add__(other))
1724 def __iand__(self, other):
1725 '''In-place intersection: C{this &= other}.
1726 '''
1727 return self._inplace(self.__and__(other))
1729 def __ior__(self, other):
1730 '''In-place union: C{this |= other}.
1731 '''
1732 return self._inplace(self.__or__(other))
1734 def __or__(self, other):
1735 '''Union: C{this | other}.
1736 '''
1737 return self._boolean(other, True, True, self.__or__) # PYCHOK OK
1739 def __radd__(self, other):
1740 '''Reverse sum: C{other + this} clips.
1741 '''
1742 return _other(self, other)._sum(self, self.__radd__)
1744 def __rand__(self, other):
1745 '''Reverse intersection: C{other & this}
1746 '''
1747 return _other(self, other).__and__(self)
1749 def __ror__(self, other):
1750 '''Reverse union: C{other | this}
1751 '''
1752 return _other(self, other).__or__(self)
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
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
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.
1780 The supported operations between (composite) polygon A and B are:
1782 - C = A & B or A &= B, intersection of A and B
1784 - C = A + B or A += B, sum of A and B clips
1786 - C = A | B or A |= B, union of A and B
1788 - A == B or A != B, equivalent A and B clips
1790 - A.isequalTo(B, eps), equivalent within tolerance
1792 @see: Methods C{__eq__} and C{isequalTo}, function L{clipFHP4}
1793 and class L{BooleanGH}.
1794 '''
1795 _kind = _boolean_
1797 def __init__(self, lls, raiser=False, eps=EPS, **name):
1798 '''New L{BooleanFHP} operand for I{boolean} operation.
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)
1809 def __isub__(self, other):
1810 '''Not implemented.'''
1811 return _NotImplemented(self, other)
1813 def __rsub__(self, other):
1814 '''Not implemented.'''
1815 return _NotImplemented(self, other)
1817 def __sub__(self, other):
1818 '''Not implemented.'''
1819 return _NotImplemented(self, other)
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)
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.
1833 The supported operations between (composite) polygon A and B are:
1835 - C = A - B or A -= B, difference A less B
1837 - C = B - A or B -= A, difference B less B
1839 - C = A & B or A &= B, intersection of A and B
1841 - C = A + B or A += B, sum of A and B clips
1843 - C = A | B or A |= B, union of A and B
1845 - A == B or A != B, equivalent A and B clips
1847 - A.isequalTo(B, eps), equivalent within tolerance
1849 @note: To handle I{degenerate cases} like C{point-edge} and
1850 C{point-point} intersections, use class L{BooleanFHP}.
1852 @see: Methods C{__eq__} and C{isequalTo}, function L{clipGH4}
1853 and class L{BooleanFHP}.
1854 '''
1855 _kind = _boolean_
1857 def __init__(self, lls, raiser=True, xtend=False, eps=EPS, **name):
1858 '''New L{BooleanFHP} operand for I{boolean} operation.
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)
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)
1877 def __isub__(self, other):
1878 '''In-place difference: C{this -= other}.
1879 '''
1880 return self._inplace(self.__sub__(other))
1882 def __rsub__(self, other):
1883 ''' Reverse difference: C{other - this}
1884 '''
1885 return _other(self, other).__sub__(self)
1887 def __sub__(self, other):
1888 '''Difference: C{this - other}.
1889 '''
1890 return self._boolean(other, True, False, self.__sub__)
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)
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)
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)
1920def _eps0(eps):
1921 # Adjust C{eps} or C{0}.
1922 return eps if eps and eps > EPS else 0
1925def isBoolean(obj):
1926 '''Check for C{Boolean} composites.
1928 @arg obj: The object (any C{type}).
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)
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))
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
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)
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)
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)
1982__all__ += _ALL_DOCS(_BooleanBase, _Clip,
1983 _CompositeBase, _CompositeFHP, _CompositeGH,
1984 _LatLonBool)
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.