pygeodesy 24.3.24__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyGeodesy-24.3.24.dist-info/METADATA +272 -0
- PyGeodesy-24.3.24.dist-info/RECORD +115 -0
- PyGeodesy-24.3.24.dist-info/WHEEL +6 -0
- PyGeodesy-24.3.24.dist-info/top_level.txt +1 -0
- pygeodesy/LICENSE +21 -0
- pygeodesy/__init__.py +615 -0
- pygeodesy/__main__.py +103 -0
- pygeodesy/albers.py +867 -0
- pygeodesy/auxilats/_CX_4.py +218 -0
- pygeodesy/auxilats/_CX_6.py +314 -0
- pygeodesy/auxilats/_CX_8.py +475 -0
- pygeodesy/auxilats/__init__.py +54 -0
- pygeodesy/auxilats/__main__.py +86 -0
- pygeodesy/auxilats/auxAngle.py +548 -0
- pygeodesy/auxilats/auxDLat.py +302 -0
- pygeodesy/auxilats/auxDST.py +296 -0
- pygeodesy/auxilats/auxLat.py +848 -0
- pygeodesy/auxilats/auxily.py +272 -0
- pygeodesy/azimuthal.py +1150 -0
- pygeodesy/basics.py +892 -0
- pygeodesy/booleans.py +2031 -0
- pygeodesy/cartesianBase.py +1062 -0
- pygeodesy/clipy.py +704 -0
- pygeodesy/constants.py +516 -0
- pygeodesy/css.py +660 -0
- pygeodesy/datums.py +752 -0
- pygeodesy/deprecated/__init__.py +61 -0
- pygeodesy/deprecated/bases.py +40 -0
- pygeodesy/deprecated/classes.py +262 -0
- pygeodesy/deprecated/consterns.py +54 -0
- pygeodesy/deprecated/datum.py +40 -0
- pygeodesy/deprecated/functions.py +375 -0
- pygeodesy/deprecated/nvector.py +48 -0
- pygeodesy/deprecated/rhumbBase.py +32 -0
- pygeodesy/deprecated/rhumbaux.py +33 -0
- pygeodesy/deprecated/rhumbsolve.py +33 -0
- pygeodesy/deprecated/rhumbx.py +33 -0
- pygeodesy/dms.py +986 -0
- pygeodesy/ecef.py +1348 -0
- pygeodesy/elevations.py +279 -0
- pygeodesy/ellipsoidalBase.py +1224 -0
- pygeodesy/ellipsoidalBaseDI.py +913 -0
- pygeodesy/ellipsoidalExact.py +343 -0
- pygeodesy/ellipsoidalGeodSolve.py +343 -0
- pygeodesy/ellipsoidalKarney.py +403 -0
- pygeodesy/ellipsoidalNvector.py +685 -0
- pygeodesy/ellipsoidalVincenty.py +590 -0
- pygeodesy/ellipsoids.py +2476 -0
- pygeodesy/elliptic.py +1198 -0
- pygeodesy/epsg.py +243 -0
- pygeodesy/errors.py +804 -0
- pygeodesy/etm.py +1190 -0
- pygeodesy/fmath.py +1013 -0
- pygeodesy/formy.py +1818 -0
- pygeodesy/frechet.py +865 -0
- pygeodesy/fstats.py +760 -0
- pygeodesy/fsums.py +1898 -0
- pygeodesy/gars.py +358 -0
- pygeodesy/geodesicw.py +581 -0
- pygeodesy/geodesicx/_C4_24.py +1699 -0
- pygeodesy/geodesicx/_C4_27.py +2395 -0
- pygeodesy/geodesicx/_C4_30.py +3301 -0
- pygeodesy/geodesicx/__init__.py +48 -0
- pygeodesy/geodesicx/__main__.py +91 -0
- pygeodesy/geodesicx/gx.py +1382 -0
- pygeodesy/geodesicx/gxarea.py +535 -0
- pygeodesy/geodesicx/gxbases.py +154 -0
- pygeodesy/geodesicx/gxline.py +669 -0
- pygeodesy/geodsolve.py +426 -0
- pygeodesy/geohash.py +914 -0
- pygeodesy/geoids.py +1884 -0
- pygeodesy/hausdorff.py +892 -0
- pygeodesy/heights.py +1155 -0
- pygeodesy/interns.py +687 -0
- pygeodesy/iters.py +545 -0
- pygeodesy/karney.py +919 -0
- pygeodesy/ktm.py +633 -0
- pygeodesy/latlonBase.py +1766 -0
- pygeodesy/lazily.py +960 -0
- pygeodesy/lcc.py +684 -0
- pygeodesy/ltp.py +1107 -0
- pygeodesy/ltpTuples.py +1563 -0
- pygeodesy/mgrs.py +721 -0
- pygeodesy/named.py +1324 -0
- pygeodesy/namedTuples.py +683 -0
- pygeodesy/nvectorBase.py +695 -0
- pygeodesy/osgr.py +781 -0
- pygeodesy/points.py +1686 -0
- pygeodesy/props.py +628 -0
- pygeodesy/resections.py +1048 -0
- pygeodesy/rhumb/__init__.py +46 -0
- pygeodesy/rhumb/aux_.py +397 -0
- pygeodesy/rhumb/bases.py +1148 -0
- pygeodesy/rhumb/ekx.py +563 -0
- pygeodesy/rhumb/solve.py +572 -0
- pygeodesy/simplify.py +647 -0
- pygeodesy/solveBase.py +472 -0
- pygeodesy/sphericalBase.py +724 -0
- pygeodesy/sphericalNvector.py +1264 -0
- pygeodesy/sphericalTrigonometry.py +1447 -0
- pygeodesy/streprs.py +627 -0
- pygeodesy/trf.py +2079 -0
- pygeodesy/triaxials.py +1484 -0
- pygeodesy/units.py +969 -0
- pygeodesy/unitsBase.py +349 -0
- pygeodesy/ups.py +538 -0
- pygeodesy/utily.py +1231 -0
- pygeodesy/utm.py +762 -0
- pygeodesy/utmups.py +318 -0
- pygeodesy/utmupsBase.py +517 -0
- pygeodesy/vector2d.py +785 -0
- pygeodesy/vector3d.py +968 -0
- pygeodesy/vector3dBase.py +1049 -0
- pygeodesy/webmercator.py +383 -0
- pygeodesy/wgrs.py +439 -0
pygeodesy/booleans.py
ADDED
|
@@ -0,0 +1,2031 @@
|
|
|
1
|
+
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
u'''I{Boolean} operations on I{composite} polygons and I{clip}s.
|
|
5
|
+
|
|
6
|
+
Classes L{BooleanFHP} and L{BooleanGH} are I{composites} and
|
|
7
|
+
provide I{boolean} operations C{intersection}, C{difference},
|
|
8
|
+
C{reverse-difference}, C{sum} and C{union}.
|
|
9
|
+
|
|
10
|
+
@note: A I{clip} is defined as a single, usually closed polygon,
|
|
11
|
+
a I{composite} is a collection of one or more I{clip}s.
|
|
12
|
+
|
|
13
|
+
@see: U{Forster-Hormann-Popa<https://www.ScienceDirect.com/science/
|
|
14
|
+
article/pii/S259014861930007X>} and U{Greiner-Hormann
|
|
15
|
+
<http://www.Inf.USI.CH/hormann/papers/Greiner.1998.ECO.pdf>}.
|
|
16
|
+
'''
|
|
17
|
+
# make sure int/int division yields float quotient, see .basics
|
|
18
|
+
from __future__ import division as _; del _ # PYCHOK semicolon
|
|
19
|
+
|
|
20
|
+
from pygeodesy.basics import isodd, issubclassof, map2, _xisscalar
|
|
21
|
+
from pygeodesy.constants import EPS, EPS2, INT0, _0_0, _0_5, _1_0
|
|
22
|
+
from pygeodesy.errors import ClipError, _IsnotError, _TypeError, \
|
|
23
|
+
_ValueError, _xattr, _xkwds_get
|
|
24
|
+
from pygeodesy.fmath import favg, hypot, hypot2
|
|
25
|
+
# from pygeodesy.fsums import fsum1 # _MODS
|
|
26
|
+
from pygeodesy.interns import NN, _BANG_, _clip_, _clipid_, _COMMASPACE_, \
|
|
27
|
+
_composite_, _DOT_, _e_, _ELLIPSIS_, _few_, \
|
|
28
|
+
_height_, _lat_,_LatLon_, _lon_, _not_, \
|
|
29
|
+
_points_, _SPACE_, _too_, _X_, _x_, \
|
|
30
|
+
_B_, _d_, _R_ # PYCHOK used!
|
|
31
|
+
from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
|
|
32
|
+
from pygeodesy.latlonBase import LatLonBase, \
|
|
33
|
+
LatLon2Tuple, Property_RO, property_RO
|
|
34
|
+
from pygeodesy.named import Fmt, _Named, _NotImplemented, pairs, unstr
|
|
35
|
+
# from pygeodesy.namedTuples import LatLon2Tupe # from .latlonBase
|
|
36
|
+
# from pygeodesy.points import boundsOf # _MODS
|
|
37
|
+
# from pygeodesy.props import Property_RO, property_RO # from .latlonBase
|
|
38
|
+
# from pygeodesy.streprs import Fmt, pairs, unstr # from .named
|
|
39
|
+
from pygeodesy.units import Height, HeightX
|
|
40
|
+
from pygeodesy.utily import fabs, _unrollon, _Wrap
|
|
41
|
+
|
|
42
|
+
# from math import fabs # from .utily
|
|
43
|
+
|
|
44
|
+
__all__ = _ALL_LAZY.booleans
|
|
45
|
+
__version__ = '24.02.06'
|
|
46
|
+
|
|
47
|
+
_0_EPS = EPS # near-zero, positive
|
|
48
|
+
_EPS_0 = -EPS # near-zero, negative
|
|
49
|
+
_1_EPS = _1_0 + EPS # near-one, over
|
|
50
|
+
_EPS_1 = _1_0 - EPS # near-one, under
|
|
51
|
+
_10EPS = EPS * 10 # see ._2Abs, ._10eps
|
|
52
|
+
|
|
53
|
+
_alpha_ = 'alpha'
|
|
54
|
+
_boolean_ = 'boolean'
|
|
55
|
+
_case_ = 'case'
|
|
56
|
+
_corners_ = 'corners'
|
|
57
|
+
_duplicate_ = 'duplicate'
|
|
58
|
+
_open_ = 'open'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _Enum(txt, enum): # PYCHOK unused
|
|
62
|
+
return txt # NN(txt, _TILDE_, enum)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class _L(object): # Intersection labels
|
|
66
|
+
CROSSING = _Enum(_X_, 1) # C++ enum
|
|
67
|
+
CROSSING_D = _Enum(_X_ + _d_, 8)
|
|
68
|
+
CROSSINGs = (CROSSING, CROSSING_D)
|
|
69
|
+
BOUNCING = _Enum(_B_, 2)
|
|
70
|
+
BOUNCING_D = _Enum(_B_ + _d_, 9)
|
|
71
|
+
BOUNCINGs = (BOUNCING, BOUNCING_D) + CROSSINGs
|
|
72
|
+
LEFT_ON = _Enum('Lo', 3)
|
|
73
|
+
ON_ON = _Enum('oo', 5)
|
|
74
|
+
ON_LEFT = _Enum('oL', 6)
|
|
75
|
+
ON_RIGHT = _Enum('oR', 7)
|
|
76
|
+
RIGHT_ON = _Enum('Ro', 4)
|
|
77
|
+
RIGHT_LEFT_ON = (RIGHT_ON, LEFT_ON)
|
|
78
|
+
# Entry/Exit flags
|
|
79
|
+
ENTRY = _Enum(_e_, 1)
|
|
80
|
+
EXIT = _Enum(_x_, 0)
|
|
81
|
+
Toggle = {ENTRY: EXIT,
|
|
82
|
+
EXIT: ENTRY,
|
|
83
|
+
None: None}
|
|
84
|
+
|
|
85
|
+
_L = _L() # PYCHOK singleton
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class _RP(object): # RelativePositions
|
|
89
|
+
IS_Pm = _Enum('Pm', 2) # C++ enum
|
|
90
|
+
IS_Pp = _Enum('Pp', 3)
|
|
91
|
+
LEFT = _Enum('L', 0)
|
|
92
|
+
RIGHT = _Enum(_R_, 1)
|
|
93
|
+
|
|
94
|
+
_RP = _RP() # PYCHOK singleton
|
|
95
|
+
|
|
96
|
+
_RP2L = {(_RP.LEFT, _RP.RIGHT): _L.CROSSING,
|
|
97
|
+
(_RP.RIGHT, _RP.LEFT): _L.CROSSING,
|
|
98
|
+
(_RP.LEFT, _RP.LEFT): _L.BOUNCING,
|
|
99
|
+
(_RP.RIGHT, _RP.RIGHT): _L.BOUNCING,
|
|
100
|
+
# overlapping cases
|
|
101
|
+
(_RP.RIGHT, _RP.IS_Pp): _L.LEFT_ON,
|
|
102
|
+
(_RP.IS_Pp, _RP.RIGHT): _L.LEFT_ON,
|
|
103
|
+
(_RP.LEFT, _RP.IS_Pp): _L.RIGHT_ON,
|
|
104
|
+
(_RP.IS_Pp, _RP.LEFT): _L.RIGHT_ON,
|
|
105
|
+
(_RP.IS_Pm, _RP.IS_Pp): _L.ON_ON,
|
|
106
|
+
(_RP.IS_Pp, _RP.IS_Pm): _L.ON_ON,
|
|
107
|
+
(_RP.IS_Pm, _RP.RIGHT): _L.ON_LEFT,
|
|
108
|
+
(_RP.RIGHT, _RP.IS_Pm): _L.ON_LEFT,
|
|
109
|
+
(_RP.LEFT, _RP.IS_Pm): _L.ON_RIGHT,
|
|
110
|
+
(_RP.IS_Pm, _RP.LEFT): _L.ON_RIGHT}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class _LatLonBool(_Named):
|
|
114
|
+
'''(INTERNAL) Base class for L{LatLonFHP} and L{LatLonGH}.
|
|
115
|
+
'''
|
|
116
|
+
_alpha = None # point AND intersection else length
|
|
117
|
+
_checked = False # checked in phase 3 iff intersection
|
|
118
|
+
_clipid = INT0 # (polygonal) clip identifier, number
|
|
119
|
+
_dupof = None # original of a duplicate
|
|
120
|
+
# _e_x_str = NN # shut up PyChecker
|
|
121
|
+
_height = Height(0) # interpolated height, usually meter
|
|
122
|
+
_linked = None # link to neighbor iff intersection
|
|
123
|
+
_next = None # link to the next vertex
|
|
124
|
+
_prev = None # link to the previous vertex
|
|
125
|
+
|
|
126
|
+
def __init__(self, lat_ll, lon=None, height=0, clipid=INT0,
|
|
127
|
+
wrap=False, name=NN):
|
|
128
|
+
'''New C{LatLon[FHP|GH]} from separate C{lat}, C{lon}, C{height}
|
|
129
|
+
and C{clipid} scalars or from a previous C{LatLon[FHP|GH]},
|
|
130
|
+
a C{Clip[FHP|GH]4Tuple} or some other C{LatLon} instance.
|
|
131
|
+
|
|
132
|
+
@arg lat_ll: Latitude (C{scalar}) or a lat/longitude
|
|
133
|
+
(C{LatLon[FHP|GH]}, aC{Clip[FHP|GH]4Tuple}
|
|
134
|
+
or some other C{LatLon}).
|
|
135
|
+
@kwarg lon: Longitude (C{scalar}), iff B{C{lat_ll}} is
|
|
136
|
+
scalar, 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}}
|
|
140
|
+
and B{C{lon}} (C{bool}).
|
|
141
|
+
@kwarg name: Optional name (C{str}).
|
|
142
|
+
'''
|
|
143
|
+
if lon is None:
|
|
144
|
+
y, x = lat_ll.lat, lat_ll.lon
|
|
145
|
+
h = _xattr(lat_ll, height=height)
|
|
146
|
+
c = _xattr(lat_ll, clipid=clipid)
|
|
147
|
+
else:
|
|
148
|
+
y, x = lat_ll, lon
|
|
149
|
+
h, c = height, clipid
|
|
150
|
+
self.y, self.x = _Wrap.latlon(y, x) if wrap else (y, x)
|
|
151
|
+
# don't duplicate defaults
|
|
152
|
+
if self._height != h:
|
|
153
|
+
self._height = h
|
|
154
|
+
if self._clipid != c:
|
|
155
|
+
self._clipid = c
|
|
156
|
+
if name:
|
|
157
|
+
self.name = name
|
|
158
|
+
|
|
159
|
+
def __abs__(self):
|
|
160
|
+
return max(fabs(self.x), fabs(self.y))
|
|
161
|
+
|
|
162
|
+
def __eq__(self, other):
|
|
163
|
+
return other is self or bool(_other(self, other) and
|
|
164
|
+
other.x == self.x and
|
|
165
|
+
other.y == self.y)
|
|
166
|
+
|
|
167
|
+
def __ne__(self, other): # required for Python 2
|
|
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 self.__class__(self.y - other.y, # classof
|
|
203
|
+
self.x - other.x)
|
|
204
|
+
|
|
205
|
+
def _2A(self, p2, p3):
|
|
206
|
+
# I{Signed} area of a triangle, I{doubled}.
|
|
207
|
+
x, y = self.x, self.y
|
|
208
|
+
return (p2.x - x) * (p3.y - y) - \
|
|
209
|
+
(p3.x - x) * (p2.y - y)
|
|
210
|
+
|
|
211
|
+
def _2Abs(self, p2, p3, eps=_10EPS):
|
|
212
|
+
# I{Unsigned} area of a triangle, I{doubled}
|
|
213
|
+
# or 0 if below the given threshold C{eps}.
|
|
214
|
+
a = fabs(self._2A(p2, p3))
|
|
215
|
+
return 0 if a < eps else a
|
|
216
|
+
|
|
217
|
+
@property_RO
|
|
218
|
+
def clipid(self):
|
|
219
|
+
'''Get the I{clipid} (C{int} or C{0}).
|
|
220
|
+
'''
|
|
221
|
+
return self._clipid
|
|
222
|
+
|
|
223
|
+
def _equi(self, llb, eps):
|
|
224
|
+
# Is this LLB I{equivalent} to B{C{llb}} within
|
|
225
|
+
# the given I{non-negative} tolerance B{C{eps}}?
|
|
226
|
+
return not (fabs(llb.lon - self.x) > eps or
|
|
227
|
+
fabs(llb.lat - self.y) > eps)
|
|
228
|
+
|
|
229
|
+
@property_RO
|
|
230
|
+
def height(self):
|
|
231
|
+
'''Get the I{height} (C{Height} or C{int}).
|
|
232
|
+
'''
|
|
233
|
+
h = self._height
|
|
234
|
+
return HeightX(h) if self.isintersection else (
|
|
235
|
+
Height(h) if h else _LatLonBool._height)
|
|
236
|
+
|
|
237
|
+
def isequalTo(self, other, eps=None):
|
|
238
|
+
'''Is this point equal to an B{C{other}} within a given,
|
|
239
|
+
I{non-negative} tolerance, ignoring C{height}?
|
|
240
|
+
|
|
241
|
+
@arg other: The other point (C{LatLon}).
|
|
242
|
+
@kwarg eps: Tolerance for equality (C{degrees} or C{None}).
|
|
243
|
+
|
|
244
|
+
@return: C{True} if equivalent, C{False} otherwise (C{bool}).
|
|
245
|
+
|
|
246
|
+
@raise TypeError: Invalid B{C{other}}.
|
|
247
|
+
'''
|
|
248
|
+
try:
|
|
249
|
+
return self._equi(other, _eps0(eps))
|
|
250
|
+
except (AttributeError, TypeError, ValueError):
|
|
251
|
+
raise _IsnotError(_LatLon_, other=other)
|
|
252
|
+
|
|
253
|
+
@property_RO
|
|
254
|
+
def isintersection(self):
|
|
255
|
+
'''Is this an intersection? May be C{ispoint} too!
|
|
256
|
+
'''
|
|
257
|
+
return bool(self._linked)
|
|
258
|
+
|
|
259
|
+
@property_RO
|
|
260
|
+
def ispoint(self):
|
|
261
|
+
'''Is this an I{original} point? May be C{isintersection} too!
|
|
262
|
+
'''
|
|
263
|
+
return self._alpha is None
|
|
264
|
+
|
|
265
|
+
@property_RO
|
|
266
|
+
def lat(self):
|
|
267
|
+
'''Get the latitude (C{scalar}).
|
|
268
|
+
'''
|
|
269
|
+
return self.y
|
|
270
|
+
|
|
271
|
+
@property_RO
|
|
272
|
+
def latlon(self):
|
|
273
|
+
'''Get the lat- and longitude (L{LatLon2Tuple}).
|
|
274
|
+
'''
|
|
275
|
+
return LatLon2Tuple(self.y, self.x)
|
|
276
|
+
|
|
277
|
+
def _link(self, other):
|
|
278
|
+
# Make this and an other point neighbors.
|
|
279
|
+
# assert _other(self, other)
|
|
280
|
+
self._linked = other
|
|
281
|
+
other._linked = self
|
|
282
|
+
|
|
283
|
+
@property_RO
|
|
284
|
+
def lon(self):
|
|
285
|
+
'''Get the longitude (C{scalar}).
|
|
286
|
+
'''
|
|
287
|
+
return self.x
|
|
288
|
+
|
|
289
|
+
def _toClas(self, Clas, clipid):
|
|
290
|
+
# Return this vertex as a C{Clas} instance
|
|
291
|
+
# (L{Clip[FHP|GH]4Tuple} or L{LatLon[FHP|GH]}).
|
|
292
|
+
return Clas(self.lat, self.lon, self.height, clipid)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class LatLonFHP(_LatLonBool):
|
|
296
|
+
'''A point or intersection in a L{BooleanFHP} clip or composite.
|
|
297
|
+
'''
|
|
298
|
+
_en_ex = None
|
|
299
|
+
_label = None
|
|
300
|
+
_2split = None # or C{._Clip}
|
|
301
|
+
_2xing = False
|
|
302
|
+
|
|
303
|
+
def __init__(self, lat_ll, *lon_h_clipid, **wrap_name):
|
|
304
|
+
'''New C{LatLonFHP} from separate C{lat}, C{lon}, C{h}eight
|
|
305
|
+
and C{clipid} scalars, or from a previous L{LatLonFHP},
|
|
306
|
+
a L{ClipFHP4Tuple} or some other C{LatLon} instance.
|
|
307
|
+
|
|
308
|
+
@arg lat_ll: Latitude (C{scalar}) or a lat/longitude
|
|
309
|
+
(L{LatLonFHP}, C{LatLon} or L{ClipFHP4Tuple}).
|
|
310
|
+
@arg lon_h_clipid: Longitude (C{scalar}), C{h}eight and
|
|
311
|
+
C{clipid} iff B{C{lat_ll}} is scalar,
|
|
312
|
+
ignored otherwise.
|
|
313
|
+
@kwarg wrap_name: Keyword arguments C{B{wrap}=False} and
|
|
314
|
+
C{B{name}=NN}. If C{B{wrap} is True}, wrap
|
|
315
|
+
or I{normalize} the lat- and longitude
|
|
316
|
+
(C{bool}). Optional B{C{name}} (C{str}).
|
|
317
|
+
'''
|
|
318
|
+
_LatLonBool.__init__(self, lat_ll, *lon_h_clipid, **wrap_name)
|
|
319
|
+
|
|
320
|
+
def __add__(self, other):
|
|
321
|
+
_other(self, other)
|
|
322
|
+
return self.__class__(self.y + other.y, self.x + other.x)
|
|
323
|
+
|
|
324
|
+
def __mod__(self, other): # cross product
|
|
325
|
+
_other(self, other)
|
|
326
|
+
return self.x * other.y - self.y * other.x
|
|
327
|
+
|
|
328
|
+
def __mul__(self, other): # dot product
|
|
329
|
+
_other(self, other)
|
|
330
|
+
return self.x * other.x + self.y * other.y
|
|
331
|
+
|
|
332
|
+
def __rmul__(self, other): # scalar product
|
|
333
|
+
_xisscalar(other=other)
|
|
334
|
+
return self.__class__(self.y * other, self.x * other)
|
|
335
|
+
|
|
336
|
+
def _e_x_str(self, t): # PYCHOK no cover
|
|
337
|
+
if self._label:
|
|
338
|
+
t = NN(self._label, t)
|
|
339
|
+
if self._en_ex:
|
|
340
|
+
t = NN(t, self._en_ex)
|
|
341
|
+
return t
|
|
342
|
+
|
|
343
|
+
@property_RO
|
|
344
|
+
def _isduplicate(self):
|
|
345
|
+
# Is this point a I{duplicate} intersection?
|
|
346
|
+
p = self._dupof
|
|
347
|
+
return bool(p and self._linked
|
|
348
|
+
and p is not self
|
|
349
|
+
and p == self
|
|
350
|
+
# and p._alpha in (None, self._alpha)
|
|
351
|
+
and self._alpha in (_0_0, p._alpha))
|
|
352
|
+
|
|
353
|
+
# @property_RO
|
|
354
|
+
# def _isduplicated(self):
|
|
355
|
+
# # Return the number of I{duplicates}?
|
|
356
|
+
# d, v = 0, self
|
|
357
|
+
# while v:
|
|
358
|
+
# if v._dupof is self:
|
|
359
|
+
# d += 1
|
|
360
|
+
# v = v._next
|
|
361
|
+
# if v is self:
|
|
362
|
+
# break
|
|
363
|
+
# return d
|
|
364
|
+
|
|
365
|
+
def isenclosedBy(self, *composites_points, **wrap):
|
|
366
|
+
'''Is this point inside one or more composites or polygons based
|
|
367
|
+
the U{winding number<https://www.ScienceDirect.com/science/
|
|
368
|
+
article/pii/S0925772101000128>}?
|
|
369
|
+
|
|
370
|
+
@arg composites_points: Composites and/or iterables of points
|
|
371
|
+
(L{ClipFHP4Tuple}, L{ClipGH4Tuple}, L{LatLonFHP},
|
|
372
|
+
L{LatLonGH} or any C{LatLon}).
|
|
373
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
|
|
374
|
+
C{points} (C{bool}).
|
|
375
|
+
|
|
376
|
+
@raise ValueError: Some C{points} invalid.
|
|
377
|
+
|
|
378
|
+
@see: U{Algorithm 6<https://www.ScienceDirect.com/science/
|
|
379
|
+
article/pii/S0925772101000128>}.
|
|
380
|
+
'''
|
|
381
|
+
class _Pseudo(object):
|
|
382
|
+
# Pseudo-_CompositeBase._clips tuple
|
|
383
|
+
|
|
384
|
+
@property_RO
|
|
385
|
+
def _clips(self):
|
|
386
|
+
for cp in _Cps(_CompositeFHP, composites_points,
|
|
387
|
+
LatLonFHP.isenclosedBy): # PYCHOK yield
|
|
388
|
+
for c in cp._clips:
|
|
389
|
+
yield c
|
|
390
|
+
|
|
391
|
+
return self._isinside(_Pseudo(), **wrap)
|
|
392
|
+
|
|
393
|
+
def _isinside(self, composite, *excludes, **wrap):
|
|
394
|
+
# Is this point inside a composite, excluding
|
|
395
|
+
# certain C{_Clip}s? I{winding number}?
|
|
396
|
+
x, y, i = self.x, self.y, False
|
|
397
|
+
for c in composite._clips:
|
|
398
|
+
if c not in excludes:
|
|
399
|
+
w = 0
|
|
400
|
+
for p1, p2 in c._edges2(**wrap):
|
|
401
|
+
# edge [p1,p2] must straddle y
|
|
402
|
+
if (p1.y < y) is not (p2.y < y): # or ^
|
|
403
|
+
r = p2.x > x
|
|
404
|
+
s = p2.y > p1.y
|
|
405
|
+
if p1.x < x:
|
|
406
|
+
b = r and (s is (p1._2A(p2, self) > 0))
|
|
407
|
+
else:
|
|
408
|
+
b = r or (s is (p1._2A(p2, self) > 0))
|
|
409
|
+
if b:
|
|
410
|
+
w += 1 if s else -1
|
|
411
|
+
if isodd(w):
|
|
412
|
+
i = not i
|
|
413
|
+
return i
|
|
414
|
+
|
|
415
|
+
@property_RO
|
|
416
|
+
def _prev_next2(self):
|
|
417
|
+
# Adjust 2-tuple (._prev, ._next) iff a I{duplicate} intersection
|
|
418
|
+
p, n = self, self._next
|
|
419
|
+
if self._isduplicate:
|
|
420
|
+
p = self._dupof
|
|
421
|
+
while p._isduplicate:
|
|
422
|
+
p = p._dupof
|
|
423
|
+
while n._isduplicate:
|
|
424
|
+
n = n._next
|
|
425
|
+
return p._prev, n
|
|
426
|
+
|
|
427
|
+
# def _edge2(self):
|
|
428
|
+
# # Return the start and end point of the
|
|
429
|
+
# # edge containing I{intersection} C{v}.
|
|
430
|
+
# n = p = self
|
|
431
|
+
# while p.isintersection:
|
|
432
|
+
# p = p._prev
|
|
433
|
+
# if p is self:
|
|
434
|
+
# break
|
|
435
|
+
# while n.isintersection:
|
|
436
|
+
# n = n._next
|
|
437
|
+
# if n is self:
|
|
438
|
+
# break
|
|
439
|
+
# # assert p == self or not p._2Abs(self, n)
|
|
440
|
+
# return p, n
|
|
441
|
+
|
|
442
|
+
def _RPoracle(self, p1, p2, p3):
|
|
443
|
+
# Relative Position oracle
|
|
444
|
+
if p1._linked is self: # or p1._linked2(self):
|
|
445
|
+
T = _RP.IS_Pm
|
|
446
|
+
elif p3._linked is self: # or p3._linked2(self):
|
|
447
|
+
T = _RP.IS_Pp
|
|
448
|
+
elif p1._2A(p2, p3) > 0: # left turn
|
|
449
|
+
T = _RP.LEFT if self._2A(p1, p2) > 0 and \
|
|
450
|
+
self._2A(p2, p3) > 0 else \
|
|
451
|
+
_RP.RIGHT # PYCHOK indent
|
|
452
|
+
else: # right turn (or straight)
|
|
453
|
+
T = _RP.RIGHT if self._2A(p1, p2) < 0 and \
|
|
454
|
+
self._2A(p2, p3) < 0 else \
|
|
455
|
+
_RP.LEFT # PYCHOK indent
|
|
456
|
+
return T
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class LatLonGH(_LatLonBool):
|
|
460
|
+
'''A point or intersection in a L{BooleanGH} clip or composite.
|
|
461
|
+
'''
|
|
462
|
+
_entry = None # entry or exit iff intersection
|
|
463
|
+
|
|
464
|
+
def __init__(self, lat_ll, *lon_h_clipid, **wrap_name):
|
|
465
|
+
'''New C{LatLonGH} from separate C{lat}, C{lon}, C{h}eight
|
|
466
|
+
and C{clipid} scalars, or from a previous L{LatLonGH},
|
|
467
|
+
L{ClipGH4Tuple} or some other C{LatLon} instance.
|
|
468
|
+
|
|
469
|
+
@arg lat_ll: Latitude (C{scalar}) or a lat/longitude
|
|
470
|
+
(L{LatLonGH}, C{LatLon} or L{ClipGH4Tuple}).
|
|
471
|
+
@arg lon_h_clipid: Longitude (C{scalar}), C{h}eight and
|
|
472
|
+
C{clipid} iff B{C{lat_ll}} is scalar,
|
|
473
|
+
ignored otherwise.
|
|
474
|
+
@kwarg wrap_name: Keyword arguments C{B{wrap}=False} and
|
|
475
|
+
C{B{name}=NN}. If C{B{wrap} is True}, wrap
|
|
476
|
+
or I{normalize} the lat- and longitude
|
|
477
|
+
(C{bool}). Optional B{C{name}} (C{str}).
|
|
478
|
+
'''
|
|
479
|
+
_LatLonBool.__init__(self, lat_ll, *lon_h_clipid, **wrap_name)
|
|
480
|
+
|
|
481
|
+
def _check(self):
|
|
482
|
+
# Check-mark this vertex and its link.
|
|
483
|
+
self._checked = True
|
|
484
|
+
k = self._linked
|
|
485
|
+
if k and not k._checked:
|
|
486
|
+
k._checked = True
|
|
487
|
+
|
|
488
|
+
def _e_x_str(self, t): # PYCHOK no cover
|
|
489
|
+
return t if self._entry is None else NN(t,
|
|
490
|
+
(_e_ if self._entry else _x_))
|
|
491
|
+
|
|
492
|
+
def isenclosedBy(self, *composites_points, **wrap):
|
|
493
|
+
'''Is this point inside one or more composites or polygons based
|
|
494
|
+
on the U{even-odd-rule<https://www.ScienceDirect.com/science/
|
|
495
|
+
article/pii/S0925772101000128>}?
|
|
496
|
+
|
|
497
|
+
@arg composites_points: Composites and/or iterables of points
|
|
498
|
+
(L{ClipFHP4Tuple}, L{ClipGH4Tuple}, L{LatLonFHP},
|
|
499
|
+
L{LatLonGH} or any C{LatLon}).
|
|
500
|
+
@kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
|
|
501
|
+
C{points} (C{bool}).
|
|
502
|
+
|
|
503
|
+
@raise ValueError: Some B{C{points}} invalid.
|
|
504
|
+
'''
|
|
505
|
+
class _Pseudo(object):
|
|
506
|
+
# Pseudo-_CompositeBase._edges3 method
|
|
507
|
+
|
|
508
|
+
def _edges3(self, **kwds):
|
|
509
|
+
for cp in _Cps(_CompositeGH, composites_points,
|
|
510
|
+
LatLonGH.isenclosedBy): # PYCHOK yield
|
|
511
|
+
for e in cp._edges3(**kwds):
|
|
512
|
+
yield e
|
|
513
|
+
|
|
514
|
+
return self._isinside(_Pseudo(), **wrap)
|
|
515
|
+
|
|
516
|
+
def _isinside(self, composite, *bottom_top, **wrap):
|
|
517
|
+
# Is this vertex inside the composite? I{even-odd rule}?
|
|
518
|
+
|
|
519
|
+
def _x(y, p1, p2):
|
|
520
|
+
# return C{x} at given C{y} on edge [p1,p2]
|
|
521
|
+
return (y - p1.y) / (p2.y - p1.y) * (p2.x - p1.x)
|
|
522
|
+
|
|
523
|
+
# The I{even-odd} rule counts the number of edges
|
|
524
|
+
# intersecting a ray emitted from this point to
|
|
525
|
+
# east-bound infinity. When I{odd} this point lies
|
|
526
|
+
# inside, if I{even} outside.
|
|
527
|
+
y, i = self.y, False
|
|
528
|
+
if not (bottom_top and _outside(y, y, *bottom_top)):
|
|
529
|
+
x = self.x
|
|
530
|
+
for p1, p2, _ in composite._edges3(**wrap):
|
|
531
|
+
if (p1.y < y) is not (p2.y < y): # or ^
|
|
532
|
+
r = p2.x > x
|
|
533
|
+
if p1.x < x:
|
|
534
|
+
b = r and (_x(y, p1, p2) > x)
|
|
535
|
+
else:
|
|
536
|
+
b = r or (_x(y, p1, p2) > x)
|
|
537
|
+
if b:
|
|
538
|
+
i = not i
|
|
539
|
+
return i
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
class _Clip(_Named):
|
|
543
|
+
'''(INTERNAL) A I{doubly-linked} list representing a I{closed}
|
|
544
|
+
polygon of L{LatLonFHP} or L{LatLonGH} points, duplicates
|
|
545
|
+
and intersections with other clips.
|
|
546
|
+
'''
|
|
547
|
+
_composite = None
|
|
548
|
+
_dups = 0
|
|
549
|
+
_first = None
|
|
550
|
+
_id = 0
|
|
551
|
+
_identical = False
|
|
552
|
+
_noInters = False
|
|
553
|
+
_last = None
|
|
554
|
+
_LL = None
|
|
555
|
+
_len = 0
|
|
556
|
+
_pushback = False
|
|
557
|
+
|
|
558
|
+
def __init__(self, composite, clipid=INT0):
|
|
559
|
+
'''(INTERNAL) New C{_Clip}.
|
|
560
|
+
'''
|
|
561
|
+
# assert isinstance(composite, _CompositeBase)
|
|
562
|
+
if clipid in composite._clipids:
|
|
563
|
+
raise ClipError(clipid=clipid, txt=_duplicate_)
|
|
564
|
+
self._composite = composite
|
|
565
|
+
self._id = clipid
|
|
566
|
+
self._LL = composite._LL
|
|
567
|
+
composite._clips = composite._clips + (self,)
|
|
568
|
+
|
|
569
|
+
def __contains__(self, point): # PYCHOK no cover
|
|
570
|
+
'''Is the B{C{point}} in this clip?
|
|
571
|
+
'''
|
|
572
|
+
for v in self:
|
|
573
|
+
if v is point: # or ==?
|
|
574
|
+
return True
|
|
575
|
+
return False
|
|
576
|
+
|
|
577
|
+
def __eq__(self, other):
|
|
578
|
+
'''Is this clip I{equivalent} to an B{C{other}} clip,
|
|
579
|
+
do both have the same C{len}, the same points, in
|
|
580
|
+
the same order, possibly rotated?
|
|
581
|
+
'''
|
|
582
|
+
return self._equi(_other(self, other), 0)
|
|
583
|
+
|
|
584
|
+
def __ge__(self, other):
|
|
585
|
+
'''See method C{__lt__}.
|
|
586
|
+
'''
|
|
587
|
+
return not self.__lt__(other)
|
|
588
|
+
|
|
589
|
+
def __gt__(self, other):
|
|
590
|
+
'''Is this clip C{"above"} an B{C{other}} clip,
|
|
591
|
+
located or stretched farther North or East?
|
|
592
|
+
'''
|
|
593
|
+
return self._bltr4 > _other(self, other)._bltr4
|
|
594
|
+
|
|
595
|
+
def __hash__(self): # PYCHOK no over
|
|
596
|
+
return hash(self._bltr4)
|
|
597
|
+
|
|
598
|
+
def __iter__(self):
|
|
599
|
+
'''Yield the points, duplicates and intersections.
|
|
600
|
+
'''
|
|
601
|
+
v = f = self._first
|
|
602
|
+
while v:
|
|
603
|
+
yield v
|
|
604
|
+
v = v._next
|
|
605
|
+
if v is f:
|
|
606
|
+
break
|
|
607
|
+
|
|
608
|
+
def __le__(self, other):
|
|
609
|
+
'''See method C{__gt__}.
|
|
610
|
+
'''
|
|
611
|
+
return not self.__gt__(other)
|
|
612
|
+
|
|
613
|
+
def __len__(self):
|
|
614
|
+
'''Return the number of points, duplicates and
|
|
615
|
+
intersections in this clip.
|
|
616
|
+
'''
|
|
617
|
+
return self._len
|
|
618
|
+
|
|
619
|
+
def __lt__(self, other):
|
|
620
|
+
'''Is this clip C{"below"} an B{C{other}} clip,
|
|
621
|
+
located or stretched farther South or West?
|
|
622
|
+
'''
|
|
623
|
+
return self._bltr4 < _other(self, other)._bltr4
|
|
624
|
+
|
|
625
|
+
def __ne__(self, other): # required for Python 2
|
|
626
|
+
'''See method C{__eq__}.
|
|
627
|
+
'''
|
|
628
|
+
return not self.__eq__(other)
|
|
629
|
+
|
|
630
|
+
_all = __iter__
|
|
631
|
+
|
|
632
|
+
@property_RO
|
|
633
|
+
def _all_ON_ON(self):
|
|
634
|
+
# Check whether all vertices are ON_ON.
|
|
635
|
+
L_ON_ON = _L.ON_ON
|
|
636
|
+
return all(v._label is L_ON_ON for v in self)
|
|
637
|
+
|
|
638
|
+
def _append(self, y_v, *x_h_clipid):
|
|
639
|
+
# Append a point given as C{y}, C{x}, C{h}eight and
|
|
640
|
+
# C{clipid} args or as a C{LatLon[FHP|GH]}.
|
|
641
|
+
self._last = v = self._LL(y_v, *x_h_clipid) if x_h_clipid else y_v
|
|
642
|
+
self._len += 1
|
|
643
|
+
# assert v._clipid == self._id
|
|
644
|
+
|
|
645
|
+
v._next = n = self._first
|
|
646
|
+
if n is None: # set ._first
|
|
647
|
+
self._first = p = n = v
|
|
648
|
+
else: # insert before ._first
|
|
649
|
+
v._prev = p = n._prev
|
|
650
|
+
p._next = n._prev = v
|
|
651
|
+
return v
|
|
652
|
+
|
|
653
|
+
# def _appendedup(self, v, clipid=0):
|
|
654
|
+
# # Like C{._append}, but only append C{v} if not a
|
|
655
|
+
# # duplicate of the one previously append[edup]'ed.
|
|
656
|
+
# y, x, p = v.y, v.x, self._last
|
|
657
|
+
# if p is None or y != p.y or x != p.x or clipid != p._clipid:
|
|
658
|
+
# p = self._append(y, x, v._height, clipid)
|
|
659
|
+
# if v._linked:
|
|
660
|
+
# p._linked = True # to force errors
|
|
661
|
+
# return p
|
|
662
|
+
|
|
663
|
+
@Property_RO
|
|
664
|
+
def _bltr4(self):
|
|
665
|
+
# Get the bounds as 4-tuple C{(bottom, left, top, right)}.
|
|
666
|
+
return map2(float, _MODS.points.boundsOf(self, wrap=False))
|
|
667
|
+
|
|
668
|
+
def _bltr4eps(self, eps):
|
|
669
|
+
# Get the ._bltr4 bounds tuple, oversized.
|
|
670
|
+
if eps > 0: # > EPS
|
|
671
|
+
yb, xl, yt, xr = self._bltr4
|
|
672
|
+
yb, yt = _low_high_eps2(yb, yt, eps)
|
|
673
|
+
xl, xr = _low_high_eps2(xl, xr, eps)
|
|
674
|
+
t = yb, xl, yt, xr
|
|
675
|
+
else:
|
|
676
|
+
t = self._bltr4
|
|
677
|
+
return t
|
|
678
|
+
|
|
679
|
+
def _closed(self, raiser): # PYCHOK unused
|
|
680
|
+
# End a clip, un-close it and check C{len}.
|
|
681
|
+
p, f = self._last, self._first
|
|
682
|
+
if f and f._prev is p and p is not f and \
|
|
683
|
+
p._next is f and p == f: # PYCHOK no cover
|
|
684
|
+
# un-close the clip
|
|
685
|
+
f._prev = p = p._prev
|
|
686
|
+
p._next = f
|
|
687
|
+
self._len -= 1
|
|
688
|
+
# elif f and raiser:
|
|
689
|
+
# raise self._OpenClipError(p, f)
|
|
690
|
+
if len(self) < 3:
|
|
691
|
+
raise self._Error(_too_(_few_))
|
|
692
|
+
|
|
693
|
+
def _dup(self, q):
|
|
694
|
+
# Duplicate a point (or intersection) as intersection.
|
|
695
|
+
v = self._insert(q.y, q.x, q)
|
|
696
|
+
v._alpha = q._alpha or _0_0 # _0_0 replaces None
|
|
697
|
+
v._dupof = q._dupof or q
|
|
698
|
+
# assert v._prev is q
|
|
699
|
+
# assert q._next is v
|
|
700
|
+
return v
|
|
701
|
+
|
|
702
|
+
def _edges2(self, wrap=False, **unused):
|
|
703
|
+
# Yield each I{original} edge as a 2-tuple
|
|
704
|
+
# (p1, p2), a pair of C{LatLon[FHP|GH])}s.
|
|
705
|
+
p1 = p = f = self._first
|
|
706
|
+
while p:
|
|
707
|
+
p2 = p = p._next
|
|
708
|
+
if p.ispoint:
|
|
709
|
+
if wrap and p is not f:
|
|
710
|
+
p2 = _unrollon(p1, p)
|
|
711
|
+
yield p1, p2
|
|
712
|
+
p1 = p2
|
|
713
|
+
if p is f:
|
|
714
|
+
break
|
|
715
|
+
|
|
716
|
+
def _equi(self, clip, eps):
|
|
717
|
+
# Is this clip I{equivalent} to B{C{clip}} within
|
|
718
|
+
# the given I{non-negative} tolerance B{C{eps}}?
|
|
719
|
+
r, f = len(self), self._first
|
|
720
|
+
if f and r == len(clip) and self._bltr4eps(eps) \
|
|
721
|
+
== clip._bltr4eps(eps):
|
|
722
|
+
_equi = _LatLonBool._equi
|
|
723
|
+
for v in clip:
|
|
724
|
+
if _equi(f, v, eps):
|
|
725
|
+
s, n = f, v
|
|
726
|
+
for _ in range(r):
|
|
727
|
+
s, n = s._next, n._next
|
|
728
|
+
if not _equi(s, n, eps):
|
|
729
|
+
break # next v
|
|
730
|
+
else: # equivalent
|
|
731
|
+
return True
|
|
732
|
+
return False
|
|
733
|
+
|
|
734
|
+
def _Error(self, txt): # PYCHOK no cover
|
|
735
|
+
# Build a C{ClipError} instance
|
|
736
|
+
kwds = dict(len=len(self), txt=txt)
|
|
737
|
+
if self._dups:
|
|
738
|
+
kwds.update(dups=self._dups)
|
|
739
|
+
cp = self._composite
|
|
740
|
+
if self._id:
|
|
741
|
+
try:
|
|
742
|
+
i = cp._clips.index(self)
|
|
743
|
+
if i != self._id:
|
|
744
|
+
kwds[_clip_] = i
|
|
745
|
+
except ValueError:
|
|
746
|
+
pass
|
|
747
|
+
kwds[_clipid_] = self._id
|
|
748
|
+
return ClipError(cp._kind, cp.name, **kwds)
|
|
749
|
+
|
|
750
|
+
def _index(self, clips, eps):
|
|
751
|
+
# see _CompositeBase._equi
|
|
752
|
+
for i, c in enumerate(clips):
|
|
753
|
+
if c._equi(self, eps):
|
|
754
|
+
return i
|
|
755
|
+
raise ValueError(NN) # like clips.index(self)
|
|
756
|
+
|
|
757
|
+
def _insert(self, y, x, start, *end_alpha):
|
|
758
|
+
# insertVertex between points C{start} and
|
|
759
|
+
# C{end}, ordered by C{alpha} iff given.
|
|
760
|
+
v = self._LL(y, x, start._height, start._clipid)
|
|
761
|
+
n = start._next
|
|
762
|
+
if end_alpha:
|
|
763
|
+
end, alpha = end_alpha
|
|
764
|
+
v._alpha = alpha
|
|
765
|
+
v._height = favg(v._height, end._height, f=alpha)
|
|
766
|
+
# assert start is not end
|
|
767
|
+
while n is not end and n._alpha < alpha:
|
|
768
|
+
n = n._next
|
|
769
|
+
v._next = n
|
|
770
|
+
v._prev = p = n._prev
|
|
771
|
+
p._next = n._prev = v
|
|
772
|
+
self._len += 1
|
|
773
|
+
# _Clip._bltr4._update(self)
|
|
774
|
+
# _Clip._ishole._update(self)
|
|
775
|
+
return v
|
|
776
|
+
|
|
777
|
+
def _intersection(self, unused, q, *p1_p2_alpha):
|
|
778
|
+
# insert an intersection or make a point both
|
|
779
|
+
if p1_p2_alpha: # intersection on edge
|
|
780
|
+
v = self._insert(q.y, q.x, *p1_p2_alpha)
|
|
781
|
+
else: # intersection at point
|
|
782
|
+
v = q
|
|
783
|
+
# assert not v._linked
|
|
784
|
+
# assert v._alpha is None
|
|
785
|
+
return v
|
|
786
|
+
|
|
787
|
+
def _intersections(self):
|
|
788
|
+
# Yield all intersections, some may be points too.
|
|
789
|
+
for v in self:
|
|
790
|
+
if v.isintersection:
|
|
791
|
+
yield v
|
|
792
|
+
|
|
793
|
+
@Property_RO
|
|
794
|
+
def _ishole(self): # PYCHOK no cover
|
|
795
|
+
# Is this clip a hole inside its composite?
|
|
796
|
+
v = self._first
|
|
797
|
+
return v._isinside(self._composite, self) if v else False
|
|
798
|
+
|
|
799
|
+
@property_RO
|
|
800
|
+
def _nodups(self):
|
|
801
|
+
# Yield all non-duplicates.
|
|
802
|
+
for v in self:
|
|
803
|
+
if not v._dupof:
|
|
804
|
+
yield v
|
|
805
|
+
|
|
806
|
+
def _noXings(self, Union):
|
|
807
|
+
# Are all intersections non-CROSSINGs, -BOUNCINGs?
|
|
808
|
+
Ls = _L.BOUNCINGs if Union else _L.CROSSINGs
|
|
809
|
+
return all(v._label not in Ls for v in self._intersections())
|
|
810
|
+
|
|
811
|
+
def _OpenClipError(self, s, e): # PYCHOK no cover
|
|
812
|
+
# Return a C{CloseError} instance
|
|
813
|
+
t = NN(s, _ELLIPSIS_(_COMMASPACE_, e))
|
|
814
|
+
return self._Error(_SPACE_(_open_, t))
|
|
815
|
+
|
|
816
|
+
def _point2(self, insert):
|
|
817
|
+
# getNonIntersectionPoint and -Vertex
|
|
818
|
+
if not (insert and self._noInters):
|
|
819
|
+
for p in self._points(may_be=False): # not p._isduplicated?
|
|
820
|
+
return p, None
|
|
821
|
+
for n in self._intersections():
|
|
822
|
+
p, _ = n._prev_next2
|
|
823
|
+
k = p._linked
|
|
824
|
+
if k:
|
|
825
|
+
if n._linked not in k._prev_next2:
|
|
826
|
+
# create a pseudo-point
|
|
827
|
+
k = _0_5 * (p + n)
|
|
828
|
+
if insert:
|
|
829
|
+
k = self._insert(k.y, k.x, n._prev)
|
|
830
|
+
r = k # to remove later
|
|
831
|
+
else: # no ._prev, ._next
|
|
832
|
+
k._clipid = n._clipid
|
|
833
|
+
r = None
|
|
834
|
+
return k, r
|
|
835
|
+
return None, None
|
|
836
|
+
|
|
837
|
+
def _points(self, may_be=True):
|
|
838
|
+
# Yield all points I{in original order}, which may be intersections too.
|
|
839
|
+
for v in self:
|
|
840
|
+
if v.ispoint and (may_be or not v.isintersection):
|
|
841
|
+
yield v
|
|
842
|
+
|
|
843
|
+
def _remove2(self, v):
|
|
844
|
+
# Remove vertex C{v}.
|
|
845
|
+
# assert not v._isduplicated
|
|
846
|
+
if len(self) > 1:
|
|
847
|
+
p = v._prev
|
|
848
|
+
p._next = n = v._next
|
|
849
|
+
n._prev = p
|
|
850
|
+
if self._first is v:
|
|
851
|
+
self._first = n
|
|
852
|
+
if self._last is v:
|
|
853
|
+
self._last = p
|
|
854
|
+
self._len -= 1
|
|
855
|
+
else:
|
|
856
|
+
n = self._last = \
|
|
857
|
+
p = self._first = None
|
|
858
|
+
self._len = 0
|
|
859
|
+
return p, n
|
|
860
|
+
|
|
861
|
+
def _update_all(self): # PYCHOK no cover
|
|
862
|
+
# Zap the I{cached} properties.
|
|
863
|
+
_Clip._bltr4._update( self)
|
|
864
|
+
_Clip._ishole._update(self)
|
|
865
|
+
return self # for _special_identicals
|
|
866
|
+
|
|
867
|
+
def _Xings(self):
|
|
868
|
+
# Yield all I{un-checked} CROSSING intersections.
|
|
869
|
+
CROSSING = _L.CROSSING
|
|
870
|
+
for v in self._intersections():
|
|
871
|
+
if v._label is CROSSING and not v._checked:
|
|
872
|
+
yield v
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
class _CompositeBase(_Named):
|
|
876
|
+
'''(INTERNAL) Base class for L{BooleanFHP} and L{BooleanGH}
|
|
877
|
+
(C{_CompositeFHP} and C{_CompositeGH}).
|
|
878
|
+
'''
|
|
879
|
+
_clips = () # tuple of C{_Clips}
|
|
880
|
+
_eps = EPS # null edges
|
|
881
|
+
_kind = _corners_
|
|
882
|
+
_LL = _LatLonBool # shut up PyChecker
|
|
883
|
+
_raiser = False
|
|
884
|
+
_xtend = False
|
|
885
|
+
|
|
886
|
+
def __init__(self, lls, name=NN, kind=NN, eps=EPS):
|
|
887
|
+
'''(INTERNAL) See L{BooleanFHP} and L{BooleanGH}.
|
|
888
|
+
'''
|
|
889
|
+
n = name or _xattr(lls, name=NN)
|
|
890
|
+
if n:
|
|
891
|
+
self.name = n
|
|
892
|
+
if kind:
|
|
893
|
+
self._kind = kind
|
|
894
|
+
if self._eps < eps:
|
|
895
|
+
self._eps = eps
|
|
896
|
+
|
|
897
|
+
c = _Clip(self)
|
|
898
|
+
lp = None
|
|
899
|
+
for ll in lls:
|
|
900
|
+
ll = self._LL(ll)
|
|
901
|
+
if lp is None:
|
|
902
|
+
c._id = ll._clipid # keep clipid
|
|
903
|
+
lp = c._append(ll)
|
|
904
|
+
elif ll._clipid != lp._clipid: # new clip
|
|
905
|
+
c._closed(self.raiser)
|
|
906
|
+
c = _Clip(self, ll._clipid)
|
|
907
|
+
lp = c._append(ll)
|
|
908
|
+
elif abs(ll - lp) > eps: # PYCHOK lp
|
|
909
|
+
lp = c._append(ll)
|
|
910
|
+
else:
|
|
911
|
+
c._dups += 1
|
|
912
|
+
c._closed(self.raiser)
|
|
913
|
+
|
|
914
|
+
def __contains__(self, point): # PYCHOK no cover
|
|
915
|
+
'''Is the B{C{point}} in one of the clips?
|
|
916
|
+
'''
|
|
917
|
+
for c in self._clips:
|
|
918
|
+
if point in c:
|
|
919
|
+
return True
|
|
920
|
+
return False
|
|
921
|
+
|
|
922
|
+
def __eq__(self, other):
|
|
923
|
+
'''Is this I{composite} equivalent to an B{C{other}}, i.e.
|
|
924
|
+
do both contain I{equivalent} clips in the same or in a
|
|
925
|
+
different order? Two clips are considered I{equivalent}
|
|
926
|
+
if both have the same points etc. in the same order,
|
|
927
|
+
possibly rotated.
|
|
928
|
+
'''
|
|
929
|
+
return self._equi(_other(self, other), 0)
|
|
930
|
+
|
|
931
|
+
def __iter__(self):
|
|
932
|
+
'''Yield all points, duplicates and intersections.
|
|
933
|
+
'''
|
|
934
|
+
for c in self._clips:
|
|
935
|
+
for v in c:
|
|
936
|
+
yield v
|
|
937
|
+
|
|
938
|
+
def __ne__(self, other): # required for Python 2
|
|
939
|
+
'''See method C{__eq__}.
|
|
940
|
+
'''
|
|
941
|
+
return not self.__eq__(other)
|
|
942
|
+
|
|
943
|
+
def __len__(self):
|
|
944
|
+
'''Return the I{total} number of points.
|
|
945
|
+
'''
|
|
946
|
+
return sum(map(len, self._clips)) if self._clips else 0
|
|
947
|
+
|
|
948
|
+
def __repr__(self):
|
|
949
|
+
'''String C{repr} of this composite.
|
|
950
|
+
'''
|
|
951
|
+
c = len(self._clips)
|
|
952
|
+
c = Fmt.SQUARE(c) if c > 1 else NN
|
|
953
|
+
n = Fmt.SQUARE(len(self))
|
|
954
|
+
t = Fmt.PAREN(self) # XXX not unstr
|
|
955
|
+
return NN(self.__class__.__name__, c, n, t)
|
|
956
|
+
|
|
957
|
+
def __str__(self):
|
|
958
|
+
'''String C{str} of this composite.
|
|
959
|
+
'''
|
|
960
|
+
return _COMMASPACE_.join(map(str, self))
|
|
961
|
+
|
|
962
|
+
@property_RO
|
|
963
|
+
def _bottom_top_eps2(self):
|
|
964
|
+
# Get the bottom and top C{y} bounds, oversized.
|
|
965
|
+
return _min_max_eps2(min(v.y for v in self),
|
|
966
|
+
max(v.y for v in self))
|
|
967
|
+
|
|
968
|
+
def _class(self, corners, kwds, **dflts):
|
|
969
|
+
# Return a new instance
|
|
970
|
+
_g = kwds.get
|
|
971
|
+
kwds = dict((n, _g(n, v)) for n, v in dflts.items())
|
|
972
|
+
return self.__class__(corners or (), **kwds)
|
|
973
|
+
|
|
974
|
+
@property_RO
|
|
975
|
+
def _clipids(self): # PYCHOK no cover
|
|
976
|
+
for c in self._clips:
|
|
977
|
+
yield c._id
|
|
978
|
+
|
|
979
|
+
def clipids(self):
|
|
980
|
+
'''Return a tuple with all C{clipid}s, I{ordered}.
|
|
981
|
+
'''
|
|
982
|
+
return tuple(self._clipids)
|
|
983
|
+
|
|
984
|
+
# def _clipidups(self, other):
|
|
985
|
+
# # Number common C{clipid}s between this and an C{other} composite
|
|
986
|
+
# return len(set(self._clipids).intersection(set(other._clipids)))
|
|
987
|
+
|
|
988
|
+
def _edges3(self, **raiser_wrap):
|
|
989
|
+
# Yield each I{original} edge as a 3-tuple
|
|
990
|
+
# C{(LatLon[FHP|GH], LatLon[FHP|GH], _Clip)}.
|
|
991
|
+
for c in self._clips:
|
|
992
|
+
for p1, p2 in c._edges2(**raiser_wrap):
|
|
993
|
+
yield p1, p2, c
|
|
994
|
+
|
|
995
|
+
def _encloses(self, lat, lon, **wrap):
|
|
996
|
+
# see function .points.isenclosedBy
|
|
997
|
+
return self._LL(lat, lon).isenclosedBy(self, **wrap)
|
|
998
|
+
|
|
999
|
+
@property
|
|
1000
|
+
def eps(self):
|
|
1001
|
+
'''Get the null edges tolerance (C{degrees}, usually).
|
|
1002
|
+
'''
|
|
1003
|
+
return self._eps
|
|
1004
|
+
|
|
1005
|
+
@eps.setter # PYCHOK setter!
|
|
1006
|
+
def eps(self, eps):
|
|
1007
|
+
'''Set the null edges tolerance (C{degrees}, usually).
|
|
1008
|
+
'''
|
|
1009
|
+
self._eps = eps
|
|
1010
|
+
|
|
1011
|
+
def _10eps(self, **eps):
|
|
1012
|
+
# Get eps for _LatLonBool._2Abs
|
|
1013
|
+
e = _xkwds_get(eps, eps=self._eps)
|
|
1014
|
+
if e != EPS:
|
|
1015
|
+
e *= _10EPS / EPS
|
|
1016
|
+
else:
|
|
1017
|
+
e = _10EPS
|
|
1018
|
+
return e
|
|
1019
|
+
|
|
1020
|
+
def _equi(self, other, eps):
|
|
1021
|
+
# Is this composite I{equivalent} to an B{C{other}} within
|
|
1022
|
+
# the given, I{non-negative} tolerance B{C{eps}}?
|
|
1023
|
+
cs, co = self._clips, other._clips
|
|
1024
|
+
if cs and len(cs) == len(co):
|
|
1025
|
+
if eps > 0:
|
|
1026
|
+
_index = _Clip._index
|
|
1027
|
+
else:
|
|
1028
|
+
def _index(c, cs, unused):
|
|
1029
|
+
return cs.index(c)
|
|
1030
|
+
try:
|
|
1031
|
+
cs = list(sorted(cs))
|
|
1032
|
+
for c in sorted(co):
|
|
1033
|
+
cs.pop(_index(c, cs, eps))
|
|
1034
|
+
except ValueError: # from ._index
|
|
1035
|
+
pass
|
|
1036
|
+
return False if cs else True
|
|
1037
|
+
else: # both null?
|
|
1038
|
+
return False if cs or co else True
|
|
1039
|
+
|
|
1040
|
+
def _intersections(self):
|
|
1041
|
+
# Yield all intersections.
|
|
1042
|
+
for c in self._clips:
|
|
1043
|
+
for v in c._intersections():
|
|
1044
|
+
yield v
|
|
1045
|
+
|
|
1046
|
+
def isequalTo(self, other, eps=None):
|
|
1047
|
+
'''Is this boolean/composite equal to an B{C{other}} within
|
|
1048
|
+
a given, I{non-negative} tolerance?
|
|
1049
|
+
|
|
1050
|
+
@arg other: The other boolean/composite (C{Boolean[FHP|GB]}).
|
|
1051
|
+
@kwarg eps: Tolerance for equality (C{degrees} or C{None}).
|
|
1052
|
+
|
|
1053
|
+
@return: C{True} if equivalent, C{False} otherwise (C{bool}).
|
|
1054
|
+
|
|
1055
|
+
@raise TypeError: Invalid B{C{other}}.
|
|
1056
|
+
|
|
1057
|
+
@see: Method C{__eq__}.
|
|
1058
|
+
'''
|
|
1059
|
+
if isinstance(other, _CompositeBase):
|
|
1060
|
+
return self._equi(other, _eps0(eps))
|
|
1061
|
+
raise _IsnotError(_boolean_, _composite_, other=other)
|
|
1062
|
+
|
|
1063
|
+
def _kwds(self, op, **more):
|
|
1064
|
+
# Get all keyword arguments as C{dict}.
|
|
1065
|
+
kwds = dict(raiser=self.raiser, eps=self.eps,
|
|
1066
|
+
name=self.name or op.__name__)
|
|
1067
|
+
kwds.update(more)
|
|
1068
|
+
return kwds
|
|
1069
|
+
|
|
1070
|
+
@property_RO
|
|
1071
|
+
def _left_right_eps2(self):
|
|
1072
|
+
# Get the left and right C{x} bounds, oversized.
|
|
1073
|
+
return _min_max_eps2(min(v.x for v in self),
|
|
1074
|
+
max(v.x for v in self))
|
|
1075
|
+
|
|
1076
|
+
def _points(self, may_be=True): # PYCHOK no cover
|
|
1077
|
+
# Yield all I{original} points, which may be intersections too.
|
|
1078
|
+
for c in self._clips:
|
|
1079
|
+
for v in c._points(may_be=may_be):
|
|
1080
|
+
yield v
|
|
1081
|
+
|
|
1082
|
+
@property
|
|
1083
|
+
def raiser(self):
|
|
1084
|
+
'''Get the option to throw L{ClipError} exceptions (C{bool}).
|
|
1085
|
+
'''
|
|
1086
|
+
return self._raiser
|
|
1087
|
+
|
|
1088
|
+
@raiser.setter # PYCHOK setter!
|
|
1089
|
+
def raiser(self, throw):
|
|
1090
|
+
'''Set the option to throw L{ClipError} exceptions (C{bool}).
|
|
1091
|
+
'''
|
|
1092
|
+
self._raiser = bool(throw)
|
|
1093
|
+
|
|
1094
|
+
def _results(self, _presults, Clas, closed=False, inull=False, **eps):
|
|
1095
|
+
# Yield the dedup'd results, as L{ClipFHP4Tuple}s
|
|
1096
|
+
C = self._LL if Clas is None else Clas
|
|
1097
|
+
e = self._10eps(**eps)
|
|
1098
|
+
for clipid, ns in enumerate(_presults):
|
|
1099
|
+
f = p = v = None
|
|
1100
|
+
for n in ns:
|
|
1101
|
+
if f is None:
|
|
1102
|
+
yield n._toClas(C, clipid)
|
|
1103
|
+
f = p = n
|
|
1104
|
+
elif v is None:
|
|
1105
|
+
v = n # got f, p, v
|
|
1106
|
+
elif inull or p._2Abs(v, n, eps=e):
|
|
1107
|
+
yield v._toClas(C, clipid)
|
|
1108
|
+
p, v = v, n
|
|
1109
|
+
else: # null, colinear, ... skipped
|
|
1110
|
+
v = n
|
|
1111
|
+
if v and (inull or p._2Abs(v, f, eps=e)):
|
|
1112
|
+
yield v._toClas(C, clipid)
|
|
1113
|
+
p = v
|
|
1114
|
+
if f and p != f and closed: # close clip
|
|
1115
|
+
yield f._toClas(C, clipid)
|
|
1116
|
+
|
|
1117
|
+
def _sum(self, other, op):
|
|
1118
|
+
# Combine this and an C{other} composite
|
|
1119
|
+
LL = self._LL
|
|
1120
|
+
sp = self.copy(name=self.name or op.__name__)
|
|
1121
|
+
sp._clips, sid = (), INT0 # new clips
|
|
1122
|
+
for cp in (self, other):
|
|
1123
|
+
for c in cp._clips:
|
|
1124
|
+
_ap = _Clip(sp, sid)._append
|
|
1125
|
+
for v in c._nodups:
|
|
1126
|
+
_ap(LL(v.y, v.x, v.height, sid))
|
|
1127
|
+
sid += 1
|
|
1128
|
+
return sp
|
|
1129
|
+
|
|
1130
|
+
def _sum1(self, _a_p, *args, **kwds): # in .karney, .points
|
|
1131
|
+
# Sum the area or perimeter of all clips
|
|
1132
|
+
return _MODS.fsums.fsum1((_a_p(c, *args, **kwds) for c in self._clips), floats=True)
|
|
1133
|
+
|
|
1134
|
+
def _sum2(self, LL, _a_p, *args, **kwds): # in .sphericalNvector, -Trigonometry
|
|
1135
|
+
# Sum the area or perimeter of all clips
|
|
1136
|
+
|
|
1137
|
+
def _lls(clip): # convert clip to LLs
|
|
1138
|
+
_LL = LL
|
|
1139
|
+
for v in clip:
|
|
1140
|
+
yield _LL(v.lat, v.lon) # datum=Sphere
|
|
1141
|
+
|
|
1142
|
+
return _MODS.fsums.fsum1((_a_p(_lls(c), *args, **kwds) for c in self._clips), floats=True)
|
|
1143
|
+
|
|
1144
|
+
def toLatLon(self, LatLon=None, closed=False, **LatLon_kwds):
|
|
1145
|
+
'''Yield all (non-duplicate) points and intersections
|
|
1146
|
+
as an instance of B{C{LatLon}}.
|
|
1147
|
+
|
|
1148
|
+
@kwarg LatLon: Class to use (C{LatLon}) or if C{None},
|
|
1149
|
+
L{LatLonFHP} or L{LatLonGH}.
|
|
1150
|
+
@kwarg closed: If C{True}, close each clip (C{bool}).
|
|
1151
|
+
@kwarg LatLon_kwds: Optional, additional B{C{LatLon}}
|
|
1152
|
+
keyword arguments, ignore if
|
|
1153
|
+
C{B{LatLon} is None}.
|
|
1154
|
+
|
|
1155
|
+
@raise TypeError: Invalid B{C{LatLon}}.
|
|
1156
|
+
|
|
1157
|
+
@note: For intersections, C{height} is an instance
|
|
1158
|
+
of L{HeightX}, otherwise of L{Height}.
|
|
1159
|
+
'''
|
|
1160
|
+
if LatLon is None:
|
|
1161
|
+
LL, kwds = self._LL, {}
|
|
1162
|
+
elif issubclassof(LatLon, _LatLonBool, LatLonBase):
|
|
1163
|
+
LL, kwds = LatLon, LatLon_kwds
|
|
1164
|
+
else:
|
|
1165
|
+
raise _TypeError(LatLon=LatLon)
|
|
1166
|
+
|
|
1167
|
+
for c in self._clips:
|
|
1168
|
+
lf, cid = None, c._id
|
|
1169
|
+
for v in c._nodups:
|
|
1170
|
+
ll = LL(v.y, v.x, **kwds)
|
|
1171
|
+
ll._height = v.height
|
|
1172
|
+
if ll._clipid != cid:
|
|
1173
|
+
ll._clipid = cid
|
|
1174
|
+
yield ll
|
|
1175
|
+
if lf is None:
|
|
1176
|
+
lf = ll
|
|
1177
|
+
if closed and lf:
|
|
1178
|
+
yield lf
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
class _CompositeFHP(_CompositeBase):
|
|
1182
|
+
'''(INTERNAL) A list of clips representing a I{composite}
|
|
1183
|
+
of L{LatLonFHP} points, duplicates and intersections
|
|
1184
|
+
with an other I{composite}.
|
|
1185
|
+
'''
|
|
1186
|
+
_LL = LatLonFHP
|
|
1187
|
+
_Union = False
|
|
1188
|
+
|
|
1189
|
+
def __init__(self, lls, raiser=False, **name_kind_eps):
|
|
1190
|
+
# New L{_CompositeFHP}.
|
|
1191
|
+
if raiser:
|
|
1192
|
+
self._raiser = True
|
|
1193
|
+
_CompositeBase.__init__(self, lls, **name_kind_eps)
|
|
1194
|
+
|
|
1195
|
+
def _classify(self):
|
|
1196
|
+
# 2) Classify intersection chains.
|
|
1197
|
+
L = _L
|
|
1198
|
+
for v in self._intersections():
|
|
1199
|
+
n, b = v, v._label
|
|
1200
|
+
if b in L.RIGHT_LEFT_ON: # next chain
|
|
1201
|
+
while True:
|
|
1202
|
+
n._label = None # n.__dict__.pop('_label')
|
|
1203
|
+
n = n._next
|
|
1204
|
+
if n is v or n._label is not L.ON_ON: # n._label and ...
|
|
1205
|
+
break
|
|
1206
|
+
a = L.LEFT_ON if n._label is L.ON_LEFT else L.RIGHT_ON
|
|
1207
|
+
v._label = n._label = L.BOUNCING_D if a is b else L.CROSSING_D
|
|
1208
|
+
|
|
1209
|
+
# 3) Copy labels
|
|
1210
|
+
for v in self._intersections():
|
|
1211
|
+
v._linked._label = v._label
|
|
1212
|
+
|
|
1213
|
+
def _clip(self, corners, Union=False, Clas=None,
|
|
1214
|
+
**closed_inull_raiser_eps):
|
|
1215
|
+
# Clip this composite with another one, C{corners},
|
|
1216
|
+
# using Foster-Hormann-Popa's algorithm.
|
|
1217
|
+
P = self
|
|
1218
|
+
Q = self._class(corners, closed_inull_raiser_eps,
|
|
1219
|
+
eps=P._eps, raiser=False)
|
|
1220
|
+
if Union:
|
|
1221
|
+
P._Union = Q._Union = True
|
|
1222
|
+
|
|
1223
|
+
bt = Q._bottom_top_eps2
|
|
1224
|
+
lr = Q._left_right_eps2
|
|
1225
|
+
# compute and insert intersections
|
|
1226
|
+
for p1, p2, Pc in P._edges3(**closed_inull_raiser_eps):
|
|
1227
|
+
if not (_outside(p1.x, p2.x, *lr) or
|
|
1228
|
+
_outside(p1.y, p2.y, *bt)):
|
|
1229
|
+
e = _EdgeFHP(p1, p2)
|
|
1230
|
+
if e._dp2 > EPS2: # non-null edge
|
|
1231
|
+
for q1, q2, Qc in Q._edges3(**closed_inull_raiser_eps):
|
|
1232
|
+
for T, p, q in e._intersect3(q1, q2):
|
|
1233
|
+
p = Pc._intersection(T, *p)
|
|
1234
|
+
q = Qc._intersection(T, *q)
|
|
1235
|
+
# assert not p._linked
|
|
1236
|
+
# assert not q._linked
|
|
1237
|
+
p._link(q)
|
|
1238
|
+
|
|
1239
|
+
# label and classify intersections
|
|
1240
|
+
P._labelize()
|
|
1241
|
+
P._classify()
|
|
1242
|
+
|
|
1243
|
+
# check for special cases
|
|
1244
|
+
P._special_cases(Q)
|
|
1245
|
+
Q._special_cases(P)
|
|
1246
|
+
# handle identicals
|
|
1247
|
+
P._special_identicals(Q)
|
|
1248
|
+
|
|
1249
|
+
# set Entry/Exit flags
|
|
1250
|
+
P._set_entry_exits(Q)
|
|
1251
|
+
Q._set_entry_exits(P)
|
|
1252
|
+
|
|
1253
|
+
# handle splits and crossings
|
|
1254
|
+
P._splits_xings(Q)
|
|
1255
|
+
|
|
1256
|
+
# yield the results
|
|
1257
|
+
return P._results(P._presults(Q), Clas, **closed_inull_raiser_eps)
|
|
1258
|
+
|
|
1259
|
+
@property_RO
|
|
1260
|
+
def _identicals(self):
|
|
1261
|
+
# Yield all clips marked C{._identical}.
|
|
1262
|
+
for c in self._clips:
|
|
1263
|
+
if c._identical:
|
|
1264
|
+
yield c
|
|
1265
|
+
|
|
1266
|
+
def _labelize(self):
|
|
1267
|
+
# 1) Intersections classification
|
|
1268
|
+
for p in self._intersections():
|
|
1269
|
+
q = p._linked
|
|
1270
|
+
# determine local configuration at this intersection
|
|
1271
|
+
# and positions of Q- and Q+ relative to (P-, I, P+)
|
|
1272
|
+
p1, p3 = p._prev_next2
|
|
1273
|
+
q1, q3 = q._prev_next2
|
|
1274
|
+
t = (q1._RPoracle(p1, p, p3),
|
|
1275
|
+
q3._RPoracle(p1, p, p3))
|
|
1276
|
+
# check intersecting and overlapping cases
|
|
1277
|
+
p._label = _RP2L.get(t, None)
|
|
1278
|
+
|
|
1279
|
+
def _presults(self, other):
|
|
1280
|
+
# Yield the result clips, each as a generator
|
|
1281
|
+
# of the L{_LatLonFHP}s in that clip
|
|
1282
|
+
for cp in (self, other):
|
|
1283
|
+
for c in cp._clips:
|
|
1284
|
+
if c._pushback:
|
|
1285
|
+
yield c._all()
|
|
1286
|
+
for c in self._clips:
|
|
1287
|
+
for X in c._Xings():
|
|
1288
|
+
yield self._resultX(X)
|
|
1289
|
+
|
|
1290
|
+
def _resultX(self, X):
|
|
1291
|
+
# Yield the results from CROSSING C{X}.
|
|
1292
|
+
L, U, v = _L, self._Union, X
|
|
1293
|
+
while v:
|
|
1294
|
+
v._checked = True
|
|
1295
|
+
r = v # in P or Q
|
|
1296
|
+
s = L.Toggle[v._en_ex]
|
|
1297
|
+
e = (s is L.EXIT) ^ U
|
|
1298
|
+
while True:
|
|
1299
|
+
v = v._next if e else v._prev
|
|
1300
|
+
yield v
|
|
1301
|
+
v._checked = True
|
|
1302
|
+
if v._en_ex is s or v is X:
|
|
1303
|
+
break
|
|
1304
|
+
if v is r: # full circle
|
|
1305
|
+
raise ClipError(full_circle=v, clipid=v._clipid)
|
|
1306
|
+
if v is not X:
|
|
1307
|
+
v = v._linked
|
|
1308
|
+
if v is X:
|
|
1309
|
+
break
|
|
1310
|
+
|
|
1311
|
+
def _set_entry_exits(self, other): # MCCABE 14
|
|
1312
|
+
# 4) Set entry/exit flags
|
|
1313
|
+
L, U = _L, self._Union
|
|
1314
|
+
for c in self._clips:
|
|
1315
|
+
n, k = c._point2(True)
|
|
1316
|
+
if n:
|
|
1317
|
+
f = n
|
|
1318
|
+
s = L.EXIT if n._isinside(other) else L.ENTRY
|
|
1319
|
+
t = L.EXIT # first_chain_vertex = True
|
|
1320
|
+
while True:
|
|
1321
|
+
if n.isintersection:
|
|
1322
|
+
b = n._label
|
|
1323
|
+
if b is L.CROSSING:
|
|
1324
|
+
n._en_ex = s
|
|
1325
|
+
s = L.Toggle[s]
|
|
1326
|
+
elif b is L.BOUNCING and ((s is L.EXIT) ^ U):
|
|
1327
|
+
n._2split = c # see ._splits_xings
|
|
1328
|
+
elif b is L.CROSSING_D:
|
|
1329
|
+
n._en_ex = s
|
|
1330
|
+
if (s is t) ^ U:
|
|
1331
|
+
n._label = L.CROSSING
|
|
1332
|
+
t = L.Toggle[t]
|
|
1333
|
+
if t is L.EXIT: # first_chain_vertex == True
|
|
1334
|
+
s = L.Toggle[s]
|
|
1335
|
+
elif b is L.BOUNCING_D:
|
|
1336
|
+
n._en_ex = s
|
|
1337
|
+
if (s is t) ^ U:
|
|
1338
|
+
n._2xing = True # see ._splits_xings
|
|
1339
|
+
s = L.Toggle[s]
|
|
1340
|
+
t = L.Toggle[t]
|
|
1341
|
+
n = n._next # _, n = n._prev_next2
|
|
1342
|
+
if n is f:
|
|
1343
|
+
break # PYCHOK attr?
|
|
1344
|
+
if k:
|
|
1345
|
+
c._remove2(k)
|
|
1346
|
+
|
|
1347
|
+
def _special_cases(self, other):
|
|
1348
|
+
# 3.5) Check special cases
|
|
1349
|
+
U = self._Union
|
|
1350
|
+
for c in self._clips:
|
|
1351
|
+
if c._noXings(U):
|
|
1352
|
+
c._noInters = True
|
|
1353
|
+
if c._all_ON_ON:
|
|
1354
|
+
c._identical = True
|
|
1355
|
+
else:
|
|
1356
|
+
p, _ = c._point2(False)
|
|
1357
|
+
if p and (p._isinside(other) ^ U):
|
|
1358
|
+
c._pushback = True
|
|
1359
|
+
|
|
1360
|
+
def _special_identicals(self, other):
|
|
1361
|
+
# 3.5) Handle identicals
|
|
1362
|
+
_u = _Clip._update_all
|
|
1363
|
+
cds = dict((c._id, _u(c)) for c in other._identicals)
|
|
1364
|
+
# assert len(cds) == len(other._identicals)
|
|
1365
|
+
if cds: # PYCHOK no cover
|
|
1366
|
+
for c in self._identicals:
|
|
1367
|
+
c._update_all()
|
|
1368
|
+
for v in c._intersections():
|
|
1369
|
+
d = cds.get(v._linked._clipid, None)
|
|
1370
|
+
if d and d._ishole is c._ishole:
|
|
1371
|
+
c._pushback = True
|
|
1372
|
+
break # next c
|
|
1373
|
+
|
|
1374
|
+
@property_RO
|
|
1375
|
+
def _2splits(self):
|
|
1376
|
+
# Yield all intersections marked C{._2split}
|
|
1377
|
+
for p in self._intersections():
|
|
1378
|
+
if p._2split:
|
|
1379
|
+
# assert isinstance(p._2split, _Clip)
|
|
1380
|
+
yield p
|
|
1381
|
+
|
|
1382
|
+
def _splits_xings(self, other): # MCCABE 15
|
|
1383
|
+
# 5) Handle split pairs and 6) crossing candidates
|
|
1384
|
+
|
|
1385
|
+
def _2A_dup2(p, P): # PYCHOK unused
|
|
1386
|
+
p1, p2 = p._prev_next2
|
|
1387
|
+
ap = p1._2A(p, p2)
|
|
1388
|
+
Pc = p._2split
|
|
1389
|
+
# assert Pc in P._clips
|
|
1390
|
+
# assert p in Pc
|
|
1391
|
+
return ap, Pc._dup(p)
|
|
1392
|
+
|
|
1393
|
+
def _links2(ps, qs): # PYCHOK P unused?
|
|
1394
|
+
# Yield each link as a 2-tuple(p, q)
|
|
1395
|
+
id_qs = set(map(id, qs))
|
|
1396
|
+
if id_qs:
|
|
1397
|
+
for p in ps:
|
|
1398
|
+
q = p._linked
|
|
1399
|
+
if q and id(q) in id_qs:
|
|
1400
|
+
yield p, q
|
|
1401
|
+
|
|
1402
|
+
L = _L
|
|
1403
|
+
E = L.ENTRY if self._Union else L.EXIT
|
|
1404
|
+
X = L.Toggle[E]
|
|
1405
|
+
for p, q in _links2(self._2splits, other._2splits):
|
|
1406
|
+
ap, pp = _2A_dup2(p, self)
|
|
1407
|
+
aq, qq = _2A_dup2(q, other)
|
|
1408
|
+
if (ap * aq) > 0: # PYCHOK no cover
|
|
1409
|
+
p._link(qq) # overwrites ...
|
|
1410
|
+
q._link(pp) # ... p-q link
|
|
1411
|
+
else:
|
|
1412
|
+
pp._link(qq)
|
|
1413
|
+
p._en_ex = q._en_ex = E
|
|
1414
|
+
pp._en_ex = qq._en_ex = X
|
|
1415
|
+
p._label = pp._label = \
|
|
1416
|
+
q._label = qq._label = L.CROSSING
|
|
1417
|
+
|
|
1418
|
+
for p, q in _links2(self._2xings, other._2xings):
|
|
1419
|
+
p._label = q._label = L.CROSSING
|
|
1420
|
+
|
|
1421
|
+
@property_RO
|
|
1422
|
+
def _2xings(self):
|
|
1423
|
+
# Yield all intersections marked C{._2xing}
|
|
1424
|
+
for p in self._intersections():
|
|
1425
|
+
if p._2xing:
|
|
1426
|
+
yield p
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
class _CompositeGH(_CompositeBase):
|
|
1430
|
+
'''(INTERNAL) A list of clips representing a I{composite}
|
|
1431
|
+
of L{LatLonGH} points, duplicates and intersections
|
|
1432
|
+
with an other I{composite}.
|
|
1433
|
+
'''
|
|
1434
|
+
_LL = LatLonGH
|
|
1435
|
+
_xtend = False
|
|
1436
|
+
|
|
1437
|
+
def __init__(self, lls, raiser=False, xtend=False, **name_kind_eps):
|
|
1438
|
+
# New L{_CompositeGH}.
|
|
1439
|
+
if xtend:
|
|
1440
|
+
self._xtend = True
|
|
1441
|
+
elif raiser:
|
|
1442
|
+
self._raiser = True
|
|
1443
|
+
_CompositeBase.__init__(self, lls, **name_kind_eps)
|
|
1444
|
+
|
|
1445
|
+
def _clip(self, corners, s_entry, c_entry, Clas=None,
|
|
1446
|
+
**closed_inull_raiser_xtend_eps):
|
|
1447
|
+
# Clip this polygon with another one, C{corners}.
|
|
1448
|
+
|
|
1449
|
+
# Core of Greiner/Hormann's algorithm, enhanced U{Correia's
|
|
1450
|
+
# <https://GitHub.com/helderco/univ-polyclip>} implementation***
|
|
1451
|
+
# and extended to optionally handle so-called "degenerate cases"
|
|
1452
|
+
S = self
|
|
1453
|
+
C = self._class(corners, closed_inull_raiser_xtend_eps,
|
|
1454
|
+
raiser=False, xtend=False)
|
|
1455
|
+
bt = C._bottom_top_eps2
|
|
1456
|
+
lr = C._left_right_eps2
|
|
1457
|
+
# 1. find intersections
|
|
1458
|
+
for s1, s2, Sc in S._edges3(**closed_inull_raiser_xtend_eps):
|
|
1459
|
+
if not (_outside(s1.x, s2.x, *lr) or
|
|
1460
|
+
_outside(s1.y, s2.y, *bt)):
|
|
1461
|
+
e = _EdgeGH(s1, s2, **closed_inull_raiser_xtend_eps)
|
|
1462
|
+
if e._hypot2 > EPS2: # non-null edge
|
|
1463
|
+
for c1, c2, Cc in C._edges3(**closed_inull_raiser_xtend_eps):
|
|
1464
|
+
for y, x, sa, ca in e._intersect4(c1, c2):
|
|
1465
|
+
s = Sc._insert(y, x, s1, s2, sa)
|
|
1466
|
+
c = Cc._insert(y, x, c1, c2, ca)
|
|
1467
|
+
s._link(c)
|
|
1468
|
+
|
|
1469
|
+
# 2. identify entry/exit intersections
|
|
1470
|
+
if S._first:
|
|
1471
|
+
s_entry ^= S._first._isinside(C, *bt)
|
|
1472
|
+
for v in S._intersections():
|
|
1473
|
+
v._entry = s_entry = not s_entry
|
|
1474
|
+
|
|
1475
|
+
if C._first:
|
|
1476
|
+
c_entry ^= C._first._isinside(S)
|
|
1477
|
+
for v in C._intersections():
|
|
1478
|
+
v._entry = c_entry = not c_entry
|
|
1479
|
+
|
|
1480
|
+
# 3. yield the result(s)
|
|
1481
|
+
return S._results(S._presults(), Clas, **closed_inull_raiser_xtend_eps)
|
|
1482
|
+
|
|
1483
|
+
@property_RO
|
|
1484
|
+
def _first(self):
|
|
1485
|
+
# Get the very first vertex of the first clip
|
|
1486
|
+
for v in self:
|
|
1487
|
+
return v
|
|
1488
|
+
return None # PYCHOK no cover
|
|
1489
|
+
|
|
1490
|
+
def _kwds(self, op, **more):
|
|
1491
|
+
# Get the kwds C{dict}.
|
|
1492
|
+
return _CompositeBase._kwds(self, op, xtend=self.xtend, **more)
|
|
1493
|
+
|
|
1494
|
+
def _presults(self):
|
|
1495
|
+
# Yield the unchecked intersection(s).
|
|
1496
|
+
for c in self._clips:
|
|
1497
|
+
for v in c._intersections():
|
|
1498
|
+
if not v._checked:
|
|
1499
|
+
yield self._resultU(v)
|
|
1500
|
+
|
|
1501
|
+
def _resultU(self, v):
|
|
1502
|
+
# Yield the result from an un-checked intersection.
|
|
1503
|
+
while v and not v._checked:
|
|
1504
|
+
v._check()
|
|
1505
|
+
yield v
|
|
1506
|
+
r = v
|
|
1507
|
+
e = v._entry
|
|
1508
|
+
while True:
|
|
1509
|
+
v = v._next if e else v._prev
|
|
1510
|
+
yield v
|
|
1511
|
+
if v._linked:
|
|
1512
|
+
break
|
|
1513
|
+
if v is r:
|
|
1514
|
+
raise ClipError(full_circle=v, clipid=v._clipid)
|
|
1515
|
+
v = v._linked # switch
|
|
1516
|
+
|
|
1517
|
+
@property
|
|
1518
|
+
def xtend(self):
|
|
1519
|
+
'''Get the option to handle I{degenerate cases} (C{bool}).
|
|
1520
|
+
'''
|
|
1521
|
+
return self._xtend
|
|
1522
|
+
|
|
1523
|
+
@xtend.setter # PYCHOK setter!
|
|
1524
|
+
def xtend(self, xtend):
|
|
1525
|
+
'''Set the option to handle I{degenerate cases} (C{bool}).
|
|
1526
|
+
'''
|
|
1527
|
+
self._xtend = bool(xtend)
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
class _EdgeFHP(object):
|
|
1531
|
+
# An edge between two L{LatLonFHP} points.
|
|
1532
|
+
|
|
1533
|
+
X_INTERSECT = _Enum('Xi', 1) # C++ enum
|
|
1534
|
+
X_OVERLAP = _Enum('Xo', 5)
|
|
1535
|
+
P_INTERSECT = _Enum('Pi', 3)
|
|
1536
|
+
P_OVERLAP = _Enum('Po', 7)
|
|
1537
|
+
Ps = (P_INTERSECT, P_OVERLAP, X_OVERLAP)
|
|
1538
|
+
Q_INTERSECT = _Enum('Qi', 2)
|
|
1539
|
+
Q_OVERLAP = _Enum('Qo', 6)
|
|
1540
|
+
Qs = (Q_INTERSECT, Q_OVERLAP, X_OVERLAP)
|
|
1541
|
+
V_INTERSECT = _Enum('Vi', 4)
|
|
1542
|
+
V_OVERLAP = _Enum('Vo', 8)
|
|
1543
|
+
Vs = (V_INTERSECT, V_OVERLAP)
|
|
1544
|
+
|
|
1545
|
+
def __init__(self, p1, p2, **unused):
|
|
1546
|
+
# New edge between points C{p1} and C{p2}, each a L{LatLonFHP}.
|
|
1547
|
+
self._p1_p2 = p1, p2
|
|
1548
|
+
self._dp = dp = p2 - p1
|
|
1549
|
+
self._dp2 = dp * dp # dot product, hypot2
|
|
1550
|
+
|
|
1551
|
+
self._lr, \
|
|
1552
|
+
self._bt = _left_right_bottom_top_eps2(p1, p2)
|
|
1553
|
+
|
|
1554
|
+
def _intersect3(self, q1, q2):
|
|
1555
|
+
# Yield intersection(s) Type or C{None}
|
|
1556
|
+
if not (_outside(q1.x, q2.x, *self._lr) or
|
|
1557
|
+
_outside(q1.y, q2.y, *self._bt)):
|
|
1558
|
+
dq = q2 - q1
|
|
1559
|
+
dq2 = dq * dq # dot product, hypot2
|
|
1560
|
+
if dq2 > EPS2: # like ._clip
|
|
1561
|
+
T, E = None, _EdgeFHP # self.__class__
|
|
1562
|
+
p1, p2 = self._p1_p2
|
|
1563
|
+
ap1 = p1._2A(q1, q2)
|
|
1564
|
+
ap2_1 = p2._2A(q1, q2) - ap1
|
|
1565
|
+
if fabs(ap2_1) > _0_EPS: # non-parallel edges
|
|
1566
|
+
aq1 = q1._2A(p1, p2)
|
|
1567
|
+
aq2_1 = q2._2A(p1, p2) - aq1
|
|
1568
|
+
if fabs(aq2_1) > _0_EPS:
|
|
1569
|
+
# compute and classify alpha and beta
|
|
1570
|
+
a, a_0, a_0_1, _ = _alpha4(-ap1 / ap2_1)
|
|
1571
|
+
b, b_0, b_0_1, _ = _alpha4(-aq1 / aq2_1)
|
|
1572
|
+
# distinguish intersection types
|
|
1573
|
+
T = E.X_INTERSECT if a_0_1 and b_0_1 else (
|
|
1574
|
+
E.P_INTERSECT if a_0_1 and b_0 else (
|
|
1575
|
+
E.Q_INTERSECT if a_0 and b_0_1 else (
|
|
1576
|
+
E.V_INTERSECT if a_0 and b_0 else None)))
|
|
1577
|
+
|
|
1578
|
+
elif fabs(ap1) < _0_EPS: # parallel or colinear edges
|
|
1579
|
+
dp = self._dp
|
|
1580
|
+
d1 = q1 - p1
|
|
1581
|
+
# compute and classify alpha and beta
|
|
1582
|
+
a, a_0, a_0_1, _a_0_1 = _alpha4((d1 * dp) / self._dp2)
|
|
1583
|
+
b, b_0, b_0_1, _b_0_1 = _alpha4((d1 * dq) / (-dq2))
|
|
1584
|
+
# distinguish overlap type
|
|
1585
|
+
T = E.X_OVERLAP if a_0_1 and b_0_1 else (
|
|
1586
|
+
E.P_OVERLAP if a_0_1 and _b_0_1 else (
|
|
1587
|
+
E.Q_OVERLAP if _a_0_1 and b_0_1 else (
|
|
1588
|
+
E.V_OVERLAP if a_0 and b_0 else None)))
|
|
1589
|
+
|
|
1590
|
+
if T:
|
|
1591
|
+
if T is E.X_INTERSECT:
|
|
1592
|
+
v = p1 + a * self._dp
|
|
1593
|
+
yield T, (v, p1, p2, a), (v, q1, q2, b)
|
|
1594
|
+
elif T in E.Vs:
|
|
1595
|
+
yield T, (p1,), (q1,)
|
|
1596
|
+
else:
|
|
1597
|
+
if T in E.Qs:
|
|
1598
|
+
yield T, (p1,), (p1, q1, q2, b)
|
|
1599
|
+
if T in E.Ps:
|
|
1600
|
+
yield T, (q1, p1, p2, a), (q1,)
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
class _EdgeGH(object):
|
|
1604
|
+
# An edge between two L{LatLonGH} points.
|
|
1605
|
+
|
|
1606
|
+
_raiser = False
|
|
1607
|
+
_xtend = False
|
|
1608
|
+
|
|
1609
|
+
def __init__(self, s1, s2, raiser=False, xtend=False, **unused):
|
|
1610
|
+
# New edge between points C{s1} and C{s2}, each a L{LatLonGH}.
|
|
1611
|
+
self._s1, self._s2 = s1, s2
|
|
1612
|
+
self._x_sx_y_sy = (s1.x, s2.x - s1.x,
|
|
1613
|
+
s1.y, s2.y - s1.y)
|
|
1614
|
+
self._lr, \
|
|
1615
|
+
self._bt = _left_right_bottom_top_eps2(s1, s2)
|
|
1616
|
+
|
|
1617
|
+
if xtend:
|
|
1618
|
+
self._xtend = True
|
|
1619
|
+
elif raiser:
|
|
1620
|
+
self._raiser = True
|
|
1621
|
+
|
|
1622
|
+
def _alpha2(self, x, y, dx, dy):
|
|
1623
|
+
# Return C{(alpha)}, see .points.nearestOn5
|
|
1624
|
+
a = (y * dy + x * dx) / self._hypot2
|
|
1625
|
+
d = (y * dx - x * dy) / self._hypot0
|
|
1626
|
+
return a, fabs(d)
|
|
1627
|
+
|
|
1628
|
+
def _Error(self, n, *args, **kwds): # PYCHOK no cover
|
|
1629
|
+
t = unstr(_EdgeGH.__name__, self._s1, self._s2)
|
|
1630
|
+
t = _DOT_(t, _EdgeGH._intersect4.__name__)
|
|
1631
|
+
t = unstr(t, *args, **kwds)
|
|
1632
|
+
return ClipError(_case_, n, txt=t)
|
|
1633
|
+
|
|
1634
|
+
@Property_RO
|
|
1635
|
+
def _hypot0(self):
|
|
1636
|
+
_, sx, _, sy = self._x_sx_y_sy
|
|
1637
|
+
return hypot(sx, sy) * _0_EPS
|
|
1638
|
+
|
|
1639
|
+
@Property_RO
|
|
1640
|
+
def _hypot2(self):
|
|
1641
|
+
_, sx, _, sy = self._x_sx_y_sy
|
|
1642
|
+
return hypot2(sx, sy)
|
|
1643
|
+
|
|
1644
|
+
def _intersect4(self, c1, c2, parallel=True): # MCCABE 14
|
|
1645
|
+
# Yield the intersection(s) of this and another edge.
|
|
1646
|
+
|
|
1647
|
+
# @return: None, 1 or 2 intersections, each a 4-Tuple
|
|
1648
|
+
# (y, x, s_alpha, c_alpha) with intersection
|
|
1649
|
+
# coordinates x and y and both alphas.
|
|
1650
|
+
|
|
1651
|
+
# @raise ClipError: Intersection unhandled.
|
|
1652
|
+
|
|
1653
|
+
# @see: U{Intersection point of two line segments
|
|
1654
|
+
# <http://PaulBourke.net/geometry/pointlineplane/>}.
|
|
1655
|
+
c1_x, c1_y = c1.x, c1.y
|
|
1656
|
+
if not (_outside(c1_x, c2.x, *self._lr) or
|
|
1657
|
+
_outside(c1_y, c2.y, *self._bt)):
|
|
1658
|
+
x, sx, \
|
|
1659
|
+
y, sy = self._x_sx_y_sy
|
|
1660
|
+
|
|
1661
|
+
cx = c2.x - c1_x
|
|
1662
|
+
cy = c2.y - c1_y
|
|
1663
|
+
d = cy * sx - cx * sy
|
|
1664
|
+
|
|
1665
|
+
if fabs(d) > _0_EPS: # non-parallel edges
|
|
1666
|
+
dx = x - c1_x
|
|
1667
|
+
dy = y - c1_y
|
|
1668
|
+
ca = (sx * dy - sy * dx) / d
|
|
1669
|
+
if _0_EPS < ca < _EPS_1 or (self._xtend and
|
|
1670
|
+
_EPS_0 < ca < _1_EPS):
|
|
1671
|
+
sa = (cx * dy - cy * dx) / d
|
|
1672
|
+
if _0_EPS < sa < _EPS_1 or (self._xtend and
|
|
1673
|
+
_EPS_0 < sa < _1_EPS):
|
|
1674
|
+
yield (y + sa * sy), (x + sa * sx), sa, ca
|
|
1675
|
+
|
|
1676
|
+
# unhandled, "degenerate" cases 1, 2 or 3
|
|
1677
|
+
elif self._raiser and not (sa < _EPS_0 or sa > _1_EPS): # PYCHOK no cover
|
|
1678
|
+
raise self._Error(1, c1, c2, sa=sa) # intersection at s1 or s2
|
|
1679
|
+
|
|
1680
|
+
elif self._raiser and not (ca < _EPS_0 or ca > _1_EPS): # PYCHOK no cover
|
|
1681
|
+
# intersection at c1 or c2 or at c1 or c2 and s1 or s2
|
|
1682
|
+
sa = (cx * dy - cy * dx) / d
|
|
1683
|
+
e = 2 if sa < _EPS_0 or sa > _1_EPS else 3
|
|
1684
|
+
raise self._Error(e, c1, c2, ca=ca)
|
|
1685
|
+
|
|
1686
|
+
elif parallel and (sx or sy) and (cx or cy): # PYCHOK no cover
|
|
1687
|
+
# non-null, parallel or colinear edges
|
|
1688
|
+
sa1, d1 = self._alpha2(c1_x - x, c1_y - y, sx, sy)
|
|
1689
|
+
sa2, d2 = self._alpha2(c2.x - x, c2.y - y, sx, sy)
|
|
1690
|
+
if max(d1, d2) < _0_EPS:
|
|
1691
|
+
if self._xtend and not _outside(sa1, sa2, _EPS_0, _1_EPS):
|
|
1692
|
+
if sa1 > sa2: # anti-parallel
|
|
1693
|
+
sa1, sa2 = sa2, sa1
|
|
1694
|
+
ca1, ca2 = _1_0, _0_0
|
|
1695
|
+
else: # parallel
|
|
1696
|
+
ca1, ca2 = _0_0, _1_0
|
|
1697
|
+
ca = fabs((sx / cx) if cx else (sy / cy))
|
|
1698
|
+
# = hypot(sx, sy) / hypot(cx, cy)
|
|
1699
|
+
if sa1 < 0: # s1 is between c1 and c2
|
|
1700
|
+
ca *= ca1 + sa1
|
|
1701
|
+
yield y, x, ca1, _alpha1(ca)
|
|
1702
|
+
else: # c1 is between s1 and s2
|
|
1703
|
+
yield (y + sa1 * sy), (x + sa1 * sx), sa1, ca1
|
|
1704
|
+
if sa2 > 1: # s2 is between c1 and c2
|
|
1705
|
+
ca *= sa2 - _1_0
|
|
1706
|
+
yield (y + sy), (x + sx), ca2, _alpha1(ca2 - ca)
|
|
1707
|
+
else: # c2 is between s1 and s2
|
|
1708
|
+
yield (y + sa2 * sy), (x + sa2 * sx), sa2, ca2
|
|
1709
|
+
elif self._raiser and not _outside(sa1, sa2, _0_0, _1_EPS):
|
|
1710
|
+
raise self._Error(4, c1, c2, d1=d1, d2=d2)
|
|
1711
|
+
|
|
1712
|
+
|
|
1713
|
+
class _BooleanBase(object):
|
|
1714
|
+
# Shared C{Boolean[FHP|GH]} methods.
|
|
1715
|
+
|
|
1716
|
+
def __add__(self, other):
|
|
1717
|
+
'''Sum: C{this + other} clips.
|
|
1718
|
+
'''
|
|
1719
|
+
return self._sum(_other(self, other), self.__add__) # PYCHOK OK
|
|
1720
|
+
|
|
1721
|
+
def __and__(self, other):
|
|
1722
|
+
'''Intersection: C{this & other}.
|
|
1723
|
+
'''
|
|
1724
|
+
return self._boolean(other, False, False, self.__and__) # PYCHOK OK
|
|
1725
|
+
|
|
1726
|
+
def __iadd__(self, other):
|
|
1727
|
+
'''In-place sum: C{this += other} clips.
|
|
1728
|
+
'''
|
|
1729
|
+
return self._inplace(self.__add__(other))
|
|
1730
|
+
|
|
1731
|
+
def __iand__(self, other):
|
|
1732
|
+
'''In-place intersection: C{this &= other}.
|
|
1733
|
+
'''
|
|
1734
|
+
return self._inplace(self.__and__(other))
|
|
1735
|
+
|
|
1736
|
+
def __ior__(self, other):
|
|
1737
|
+
'''In-place union: C{this |= other}.
|
|
1738
|
+
'''
|
|
1739
|
+
return self._inplace(self.__or__(other))
|
|
1740
|
+
|
|
1741
|
+
def __or__(self, other):
|
|
1742
|
+
'''Union: C{this | other}.
|
|
1743
|
+
'''
|
|
1744
|
+
return self._boolean(other, True, True, self.__or__) # PYCHOK OK
|
|
1745
|
+
|
|
1746
|
+
def __radd__(self, other):
|
|
1747
|
+
'''Reverse sum: C{other + this} clips.
|
|
1748
|
+
'''
|
|
1749
|
+
return _other(self, other)._sum(self, self.__radd__)
|
|
1750
|
+
|
|
1751
|
+
def __rand__(self, other):
|
|
1752
|
+
'''Reverse intersection: C{other & this}
|
|
1753
|
+
'''
|
|
1754
|
+
return _other(self, other).__and__(self)
|
|
1755
|
+
|
|
1756
|
+
def __ror__(self, other):
|
|
1757
|
+
'''Reverse union: C{other | this}
|
|
1758
|
+
'''
|
|
1759
|
+
return _other(self, other).__or__(self)
|
|
1760
|
+
|
|
1761
|
+
def _boolean4(self, other, op):
|
|
1762
|
+
# Set up a new C{Boolean[FHP|GH]}.
|
|
1763
|
+
C = self.__class__
|
|
1764
|
+
kwds = C._kwds(self, op)
|
|
1765
|
+
a = C(self, **kwds)
|
|
1766
|
+
b = _other(self, other)
|
|
1767
|
+
return a, b, C, kwds
|
|
1768
|
+
|
|
1769
|
+
def _inplace(self, r):
|
|
1770
|
+
# Replace this with a L{Boolean*} result.
|
|
1771
|
+
self._clips, r._clips = r._clips, None
|
|
1772
|
+
# if self._raiser != r._raiser:
|
|
1773
|
+
# self._raiser = r._raiser
|
|
1774
|
+
# if self._xtend != r._xtend:
|
|
1775
|
+
# self._xtend = r._xtend
|
|
1776
|
+
# if self._eps != r._eps:
|
|
1777
|
+
# self._eps = r._eps
|
|
1778
|
+
return self
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
class BooleanFHP(_CompositeFHP, _BooleanBase):
|
|
1782
|
+
'''I{Composite} class providing I{boolean} operations between two
|
|
1783
|
+
I{composites} using U{Forster-Hormann-Popa<https://www.ScienceDirect.com/
|
|
1784
|
+
science/article/pii/S259014861930007X>}'s C++ implementation, transcoded
|
|
1785
|
+
to pure Python.
|
|
1786
|
+
|
|
1787
|
+
The supported operations between (composite) polygon A and B are:
|
|
1788
|
+
|
|
1789
|
+
- C = A & B or A &= B, intersection of A and B
|
|
1790
|
+
|
|
1791
|
+
- C = A + B or A += B, sum of A and B clips
|
|
1792
|
+
|
|
1793
|
+
- C = A | B or A |= B, union of A and B
|
|
1794
|
+
|
|
1795
|
+
- A == B or A != B, equivalent A and B clips
|
|
1796
|
+
|
|
1797
|
+
- A.isequalTo(B, eps), equivalent within tolerance
|
|
1798
|
+
|
|
1799
|
+
@see: Methods C{__eq__} and C{isequalTo}, function L{clipFHP4}
|
|
1800
|
+
and class L{BooleanGH}.
|
|
1801
|
+
'''
|
|
1802
|
+
_kind = _boolean_
|
|
1803
|
+
|
|
1804
|
+
def __init__(self, lls, raiser=False, eps=EPS, name=NN):
|
|
1805
|
+
'''New L{BooleanFHP} operand for I{boolean} operation.
|
|
1806
|
+
|
|
1807
|
+
@arg lls: The polygon points and clips (iterable of L{LatLonFHP}s,
|
|
1808
|
+
L{ClipFHP4Tuple}s or other C{LatLon}s).
|
|
1809
|
+
@kwarg raiser: If C{True}, throw L{ClipError} exceptions (C{bool}).
|
|
1810
|
+
@kwarg esp: Tolerance for eliminating null edges (C{degrees}, same
|
|
1811
|
+
units as the B{C{lls}} coordinates).
|
|
1812
|
+
@kwarg name: Optional name (C{str}).
|
|
1813
|
+
'''
|
|
1814
|
+
_CompositeFHP.__init__(self, lls, raiser=raiser,
|
|
1815
|
+
eps=eps, name=name)
|
|
1816
|
+
|
|
1817
|
+
def __isub__(self, other):
|
|
1818
|
+
'''Not implemented.'''
|
|
1819
|
+
return _NotImplemented(self, other)
|
|
1820
|
+
|
|
1821
|
+
def __rsub__(self, other):
|
|
1822
|
+
'''Not implemented.'''
|
|
1823
|
+
return _NotImplemented(self, other)
|
|
1824
|
+
|
|
1825
|
+
def __sub__(self, other):
|
|
1826
|
+
'''Not implemented.'''
|
|
1827
|
+
return _NotImplemented(self, other)
|
|
1828
|
+
|
|
1829
|
+
def _boolean(self, other, Union, unused, op):
|
|
1830
|
+
# One C{BooleanFHP} operation.
|
|
1831
|
+
p, q, C, kwds = self._boolean4(other, op)
|
|
1832
|
+
r = p._clip(q, Union=Union, **kwds)
|
|
1833
|
+
return C(r, **kwds)
|
|
1834
|
+
|
|
1835
|
+
|
|
1836
|
+
class BooleanGH(_CompositeGH, _BooleanBase):
|
|
1837
|
+
'''I{Composite} class providing I{boolean} operations between two
|
|
1838
|
+
I{composites} using the U{Greiner-Hormann<http://www.Inf.USI.CH/
|
|
1839
|
+
hormann/papers/Greiner.1998.ECO.pdf>} algorithm and U{Correia
|
|
1840
|
+
<https://GitHub.com/helderco/univ-polyclip>}'s implementation,
|
|
1841
|
+
modified and extended.
|
|
1842
|
+
|
|
1843
|
+
The supported operations between (composite) polygon A and B are:
|
|
1844
|
+
|
|
1845
|
+
- C = A - B or A -= B, difference A less B
|
|
1846
|
+
|
|
1847
|
+
- C = B - A or B -= A, difference B less B
|
|
1848
|
+
|
|
1849
|
+
- C = A & B or A &= B, intersection of A and B
|
|
1850
|
+
|
|
1851
|
+
- C = A + B or A += B, sum of A and B clips
|
|
1852
|
+
|
|
1853
|
+
- C = A | B or A |= B, union of A and B
|
|
1854
|
+
|
|
1855
|
+
- A == B or A != B, equivalent A and B clips
|
|
1856
|
+
|
|
1857
|
+
- A.isequalTo(B, eps), equivalent within tolerance
|
|
1858
|
+
|
|
1859
|
+
@note: To handle I{degenerate cases} like C{point-edge} and
|
|
1860
|
+
C{point-point} intersections, use class L{BooleanFHP}.
|
|
1861
|
+
|
|
1862
|
+
@see: Methods C{__eq__} and C{isequalTo}, function L{clipGH4}
|
|
1863
|
+
and class L{BooleanFHP}.
|
|
1864
|
+
'''
|
|
1865
|
+
_kind = _boolean_
|
|
1866
|
+
|
|
1867
|
+
def __init__(self, lls, raiser=True, xtend=False, eps=EPS, name=NN):
|
|
1868
|
+
'''New L{BooleanFHP} operand for I{boolean} operation.
|
|
1869
|
+
|
|
1870
|
+
@arg lls: The polygon points and clips (iterable of L{LatLonGH}s,
|
|
1871
|
+
L{ClipGH4Tuple}s or other C{LatLon}s).
|
|
1872
|
+
@kwarg raiser: If C{True}, throw L{ClipError} exceptions (C{bool}).
|
|
1873
|
+
@kwarg xtend: If C{True}, extend edges of I{degenerate cases}, an
|
|
1874
|
+
attempt to handle the latter (C{bool}).
|
|
1875
|
+
@kwarg esp: Tolerance for eliminating null edges (C{degrees}, same
|
|
1876
|
+
units as the B{C{lls}} coordinates).
|
|
1877
|
+
@kwarg name: Optional name (C{str}).
|
|
1878
|
+
'''
|
|
1879
|
+
_CompositeGH.__init__(self, lls, raiser=raiser, xtend=xtend,
|
|
1880
|
+
eps=eps, name=name)
|
|
1881
|
+
|
|
1882
|
+
def _boolean(self, other, s_entry, c_entry, op):
|
|
1883
|
+
# One C{BooleanGH} operation.
|
|
1884
|
+
s, c, C, kwds = self._boolean4(other, op)
|
|
1885
|
+
r = s._clip(c, s_entry, c_entry, **kwds)
|
|
1886
|
+
return C(r, **kwds)
|
|
1887
|
+
|
|
1888
|
+
def __isub__(self, other):
|
|
1889
|
+
'''In-place difference: C{this -= other}.
|
|
1890
|
+
'''
|
|
1891
|
+
return self._inplace(self.__sub__(other))
|
|
1892
|
+
|
|
1893
|
+
def __rsub__(self, other):
|
|
1894
|
+
''' Reverse difference: C{other - this}
|
|
1895
|
+
'''
|
|
1896
|
+
return _other(self, other).__sub__(self)
|
|
1897
|
+
|
|
1898
|
+
def __sub__(self, other):
|
|
1899
|
+
'''Difference: C{this - other}.
|
|
1900
|
+
'''
|
|
1901
|
+
return self._boolean(other, True, False, self.__sub__)
|
|
1902
|
+
|
|
1903
|
+
|
|
1904
|
+
def _alpha1(alpha):
|
|
1905
|
+
# Return C{alpha} in C{[0..1]} range
|
|
1906
|
+
if _EPS_0 < alpha < _1_EPS:
|
|
1907
|
+
return max(_0_0, min(alpha, _1_0))
|
|
1908
|
+
t = _not_(Fmt.SQUARE(_ELLIPSIS_(0, 1)))
|
|
1909
|
+
raise ClipError(_alpha_, alpha, txt=t)
|
|
1910
|
+
|
|
1911
|
+
|
|
1912
|
+
def _alpha4(a):
|
|
1913
|
+
# Return 4-tuple (alpha, -EPS < alpha < EPS,
|
|
1914
|
+
# 0 < alpha < 1,
|
|
1915
|
+
# not 0 < alpha < 1)
|
|
1916
|
+
return (a, False, True, False) if _0_EPS < a < _EPS_1 else (
|
|
1917
|
+
(a, False, False, True) if _0_EPS < fabs(a) else
|
|
1918
|
+
(a, True, False, False))
|
|
1919
|
+
|
|
1920
|
+
|
|
1921
|
+
def _Cps(Cp, composites_points, where):
|
|
1922
|
+
# Yield composites and points as a C{Cp} composite.
|
|
1923
|
+
try:
|
|
1924
|
+
kwds = dict(kind=_points_, name=where.__name__)
|
|
1925
|
+
for cp in composites_points:
|
|
1926
|
+
yield cp if isBoolean(cp) else Cp(cp, **kwds)
|
|
1927
|
+
except (AttributeError, ClipError, TypeError, ValueError) as x:
|
|
1928
|
+
raise _ValueError(points=cp, cause=x)
|
|
1929
|
+
|
|
1930
|
+
|
|
1931
|
+
def _eps0(eps):
|
|
1932
|
+
# Adjust C{eps} or C{None}.
|
|
1933
|
+
return eps if eps and eps > EPS else 0
|
|
1934
|
+
|
|
1935
|
+
|
|
1936
|
+
def isBoolean(obj):
|
|
1937
|
+
'''Check for C{Boolean} composites.
|
|
1938
|
+
|
|
1939
|
+
@arg obj: The object (any C{type}).
|
|
1940
|
+
|
|
1941
|
+
@return: C{True} if B{C{obj}} is L{BooleanFHP},
|
|
1942
|
+
L{BooleanGH} oe some other composite,
|
|
1943
|
+
C{False} otherwise.
|
|
1944
|
+
'''
|
|
1945
|
+
return isinstance(obj, _CompositeBase)
|
|
1946
|
+
|
|
1947
|
+
|
|
1948
|
+
def _left_right_bottom_top_eps2(p1, p2):
|
|
1949
|
+
'''(INTERNAL) Return 2-tuple C{(left, right), (bottom, top)}, oversized.
|
|
1950
|
+
'''
|
|
1951
|
+
return (_min_max_eps2(p1.x, p2.x),
|
|
1952
|
+
_min_max_eps2(p1.y, p2.y))
|
|
1953
|
+
|
|
1954
|
+
|
|
1955
|
+
def _low_high_eps2(lo, hi, eps):
|
|
1956
|
+
'''(INTERNAL) Return 2-tuple C{(lo, hi)}, oversized.
|
|
1957
|
+
'''
|
|
1958
|
+
lo *= (_1_0 + eps) if lo < 0 else (_1_0 - eps)
|
|
1959
|
+
hi *= (_1_0 - eps) if hi < 0 else (_1_0 + eps)
|
|
1960
|
+
return (lo or -eps), (hi or eps)
|
|
1961
|
+
|
|
1962
|
+
|
|
1963
|
+
def _min_max_eps2(*xs):
|
|
1964
|
+
'''(INTERNAL) Return 2-tuple C{(min, max)}, oversized.
|
|
1965
|
+
'''
|
|
1966
|
+
lo, hi = min(xs), max(xs)
|
|
1967
|
+
lo *= _1_EPS if lo < 0 else _EPS_1
|
|
1968
|
+
hi *= _EPS_1 if hi < 0 else _1_EPS
|
|
1969
|
+
return (lo or _EPS_0), (hi or _0_EPS)
|
|
1970
|
+
|
|
1971
|
+
|
|
1972
|
+
def _other(this, other):
|
|
1973
|
+
'''(INTERNAL) Check for compatible C{type}s.
|
|
1974
|
+
'''
|
|
1975
|
+
C = this.__class__
|
|
1976
|
+
if isinstance(other, C):
|
|
1977
|
+
return other
|
|
1978
|
+
raise _IsnotError(C.__name__, other=other)
|
|
1979
|
+
|
|
1980
|
+
|
|
1981
|
+
def _outside(x1, x2, lo, hi):
|
|
1982
|
+
'''(INTERNAL) Is C{(x1, x2)} outside C{(lo, hi)}?
|
|
1983
|
+
'''
|
|
1984
|
+
return max(x1, x2) < lo or min(x1, x2) > hi
|
|
1985
|
+
|
|
1986
|
+
|
|
1987
|
+
__all__ += _ALL_DOCS(_BooleanBase, _Clip,
|
|
1988
|
+
_CompositeBase, _CompositeFHP, _CompositeGH,
|
|
1989
|
+
_LatLonBool)
|
|
1990
|
+
|
|
1991
|
+
# **) MIT License
|
|
1992
|
+
#
|
|
1993
|
+
# Copyright (C) 2018-2024 -- mrJean1 at Gmail -- All Rights Reserved.
|
|
1994
|
+
#
|
|
1995
|
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
1996
|
+
# copy of this software and associated documentation files (the "Software"),
|
|
1997
|
+
# to deal in the Software without restriction, including without limitation
|
|
1998
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
1999
|
+
# and/or sell copies of the Software, and to permit persons to whom the
|
|
2000
|
+
# Software is furnished to do so, subject to the following conditions:
|
|
2001
|
+
#
|
|
2002
|
+
# The above copyright notice and this permission notice shall be included
|
|
2003
|
+
# in all copies or substantial portions of the Software.
|
|
2004
|
+
#
|
|
2005
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
2006
|
+
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
2007
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
2008
|
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
2009
|
+
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
2010
|
+
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
2011
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
|
2012
|
+
|
|
2013
|
+
# ***) GNU GPL 3
|
|
2014
|
+
#
|
|
2015
|
+
# Copyright (C) 2011-2012 Helder Correia <Helder.MC@Gmail.com>
|
|
2016
|
+
#
|
|
2017
|
+
# This program is free software: you can redistribute it and/or
|
|
2018
|
+
# modify it under the terms of the GNU General Public License as
|
|
2019
|
+
# published by the Free Software Foundation, either version 3 of
|
|
2020
|
+
# the License, or any later version.
|
|
2021
|
+
#
|
|
2022
|
+
# This program is distributed in the hope that it will be useful,
|
|
2023
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
2024
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
2025
|
+
# GNU General Public License for more details.
|
|
2026
|
+
#
|
|
2027
|
+
# You should have received a copy of the GNU General Public License
|
|
2028
|
+
# along with this program. If not, see <http://www.GNU.org/licenses/>.
|
|
2029
|
+
#
|
|
2030
|
+
# You should have received the README file along with this program.
|
|
2031
|
+
# If not, see <https://GitHub.com/helderco/univ-polyclip>.
|