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.
Files changed (115) hide show
  1. PyGeodesy-24.3.24.dist-info/METADATA +272 -0
  2. PyGeodesy-24.3.24.dist-info/RECORD +115 -0
  3. PyGeodesy-24.3.24.dist-info/WHEEL +6 -0
  4. PyGeodesy-24.3.24.dist-info/top_level.txt +1 -0
  5. pygeodesy/LICENSE +21 -0
  6. pygeodesy/__init__.py +615 -0
  7. pygeodesy/__main__.py +103 -0
  8. pygeodesy/albers.py +867 -0
  9. pygeodesy/auxilats/_CX_4.py +218 -0
  10. pygeodesy/auxilats/_CX_6.py +314 -0
  11. pygeodesy/auxilats/_CX_8.py +475 -0
  12. pygeodesy/auxilats/__init__.py +54 -0
  13. pygeodesy/auxilats/__main__.py +86 -0
  14. pygeodesy/auxilats/auxAngle.py +548 -0
  15. pygeodesy/auxilats/auxDLat.py +302 -0
  16. pygeodesy/auxilats/auxDST.py +296 -0
  17. pygeodesy/auxilats/auxLat.py +848 -0
  18. pygeodesy/auxilats/auxily.py +272 -0
  19. pygeodesy/azimuthal.py +1150 -0
  20. pygeodesy/basics.py +892 -0
  21. pygeodesy/booleans.py +2031 -0
  22. pygeodesy/cartesianBase.py +1062 -0
  23. pygeodesy/clipy.py +704 -0
  24. pygeodesy/constants.py +516 -0
  25. pygeodesy/css.py +660 -0
  26. pygeodesy/datums.py +752 -0
  27. pygeodesy/deprecated/__init__.py +61 -0
  28. pygeodesy/deprecated/bases.py +40 -0
  29. pygeodesy/deprecated/classes.py +262 -0
  30. pygeodesy/deprecated/consterns.py +54 -0
  31. pygeodesy/deprecated/datum.py +40 -0
  32. pygeodesy/deprecated/functions.py +375 -0
  33. pygeodesy/deprecated/nvector.py +48 -0
  34. pygeodesy/deprecated/rhumbBase.py +32 -0
  35. pygeodesy/deprecated/rhumbaux.py +33 -0
  36. pygeodesy/deprecated/rhumbsolve.py +33 -0
  37. pygeodesy/deprecated/rhumbx.py +33 -0
  38. pygeodesy/dms.py +986 -0
  39. pygeodesy/ecef.py +1348 -0
  40. pygeodesy/elevations.py +279 -0
  41. pygeodesy/ellipsoidalBase.py +1224 -0
  42. pygeodesy/ellipsoidalBaseDI.py +913 -0
  43. pygeodesy/ellipsoidalExact.py +343 -0
  44. pygeodesy/ellipsoidalGeodSolve.py +343 -0
  45. pygeodesy/ellipsoidalKarney.py +403 -0
  46. pygeodesy/ellipsoidalNvector.py +685 -0
  47. pygeodesy/ellipsoidalVincenty.py +590 -0
  48. pygeodesy/ellipsoids.py +2476 -0
  49. pygeodesy/elliptic.py +1198 -0
  50. pygeodesy/epsg.py +243 -0
  51. pygeodesy/errors.py +804 -0
  52. pygeodesy/etm.py +1190 -0
  53. pygeodesy/fmath.py +1013 -0
  54. pygeodesy/formy.py +1818 -0
  55. pygeodesy/frechet.py +865 -0
  56. pygeodesy/fstats.py +760 -0
  57. pygeodesy/fsums.py +1898 -0
  58. pygeodesy/gars.py +358 -0
  59. pygeodesy/geodesicw.py +581 -0
  60. pygeodesy/geodesicx/_C4_24.py +1699 -0
  61. pygeodesy/geodesicx/_C4_27.py +2395 -0
  62. pygeodesy/geodesicx/_C4_30.py +3301 -0
  63. pygeodesy/geodesicx/__init__.py +48 -0
  64. pygeodesy/geodesicx/__main__.py +91 -0
  65. pygeodesy/geodesicx/gx.py +1382 -0
  66. pygeodesy/geodesicx/gxarea.py +535 -0
  67. pygeodesy/geodesicx/gxbases.py +154 -0
  68. pygeodesy/geodesicx/gxline.py +669 -0
  69. pygeodesy/geodsolve.py +426 -0
  70. pygeodesy/geohash.py +914 -0
  71. pygeodesy/geoids.py +1884 -0
  72. pygeodesy/hausdorff.py +892 -0
  73. pygeodesy/heights.py +1155 -0
  74. pygeodesy/interns.py +687 -0
  75. pygeodesy/iters.py +545 -0
  76. pygeodesy/karney.py +919 -0
  77. pygeodesy/ktm.py +633 -0
  78. pygeodesy/latlonBase.py +1766 -0
  79. pygeodesy/lazily.py +960 -0
  80. pygeodesy/lcc.py +684 -0
  81. pygeodesy/ltp.py +1107 -0
  82. pygeodesy/ltpTuples.py +1563 -0
  83. pygeodesy/mgrs.py +721 -0
  84. pygeodesy/named.py +1324 -0
  85. pygeodesy/namedTuples.py +683 -0
  86. pygeodesy/nvectorBase.py +695 -0
  87. pygeodesy/osgr.py +781 -0
  88. pygeodesy/points.py +1686 -0
  89. pygeodesy/props.py +628 -0
  90. pygeodesy/resections.py +1048 -0
  91. pygeodesy/rhumb/__init__.py +46 -0
  92. pygeodesy/rhumb/aux_.py +397 -0
  93. pygeodesy/rhumb/bases.py +1148 -0
  94. pygeodesy/rhumb/ekx.py +563 -0
  95. pygeodesy/rhumb/solve.py +572 -0
  96. pygeodesy/simplify.py +647 -0
  97. pygeodesy/solveBase.py +472 -0
  98. pygeodesy/sphericalBase.py +724 -0
  99. pygeodesy/sphericalNvector.py +1264 -0
  100. pygeodesy/sphericalTrigonometry.py +1447 -0
  101. pygeodesy/streprs.py +627 -0
  102. pygeodesy/trf.py +2079 -0
  103. pygeodesy/triaxials.py +1484 -0
  104. pygeodesy/units.py +969 -0
  105. pygeodesy/unitsBase.py +349 -0
  106. pygeodesy/ups.py +538 -0
  107. pygeodesy/utily.py +1231 -0
  108. pygeodesy/utm.py +762 -0
  109. pygeodesy/utmups.py +318 -0
  110. pygeodesy/utmupsBase.py +517 -0
  111. pygeodesy/vector2d.py +785 -0
  112. pygeodesy/vector3d.py +968 -0
  113. pygeodesy/vector3dBase.py +1049 -0
  114. pygeodesy/webmercator.py +383 -0
  115. 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>.