Coverage for pygeodesy/clipy.py: 96%
316 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-25 13:15 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-25 13:15 -0400
2# -*- coding: utf-8 -*-
4u'''Clip a path or polygon of C{LatLon} points against a rectangular box or
5an arbitrary (convex) region.
7Box clip functions L{clipCS4} I{Cohen-Sutherland} and L{clipLB6} I{Liang-Barsky},
8region clip functions L{clipFHP4} I{Foster-Hormann-Popa}, L{clipGH4}
9I{Greiner-Hormann} and L{clipSH} and L{clipSH3} I{Sutherland-Hodgeman}.
10.
11'''
12# make sure int/int division yields float quotient, see .basics
13from __future__ import division as _; del _ # PYCHOK semicolon
15from pygeodesy.basics import len2, typename
16from pygeodesy.constants import EPS, _0_0, _1_0
17from pygeodesy.errors import _AssertionError, ClipError, PointsError
18from pygeodesy.fmath import fabs, Fsum
19# from pygeodesy.fsums import Fsum # from .fmath
20# from pygeodesy.internals import typename # from .basics
21from pygeodesy.interns import NN, _clipid_, _convex_, _DOT_, _end_, _few_, \
22 _fi_, _height_, _i_, _invalid_, _j_, _lat_, \
23 _lon_, _near_, _not_, _points_, _start_, _too_
24from pygeodesy.iters import _imdex2, points2
25from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS
26from pygeodesy.named import _Named, _NamedTuple, _Pass, Property_RO
27from pygeodesy.points import areaOf, boundsOf, isconvex_, LatLon_
28# from pygeodesy.props import Property_RO # from .named
29from pygeodesy.units import Bool, FIx, HeightX, Lat, Lon, Number_
31# from math import fabs # from .fmath
33__all__ = _ALL_LAZY.clipy
34__version__ = '25.04.14'
36_fj_ = 'fj'
37_original_ = 'original'
40def _box4(lowerleft, upperight, name):
41 '''(INTERNAL) Get the clip box edges.
43 @see: Class C{_Box} in .ellipsoidalBaseDI.py.
44 '''
45 try:
46 yb, yt = lowerleft.lat, upperight.lat
47 xl, xr = lowerleft.lon, upperight.lon
48 if xl > xr or yb > yt:
49 raise ValueError(_invalid_)
50 except (AttributeError, TypeError, ValueError) as x:
51 raise ClipError(name, 2, (lowerleft, upperight), cause=x)
52 return xl, yb, xr, yt
55def _4corners(corners):
56 '''(INTERNAL) Clip region or box.
57 '''
58 n, cs = len2(corners)
59 if n == 2: # make a box
60 yb, xl, yt, xr = boundsOf(cs, wrap=False)
61 cs = (LatLon_(yb, xl), LatLon_(yt, xl),
62 LatLon_(yt, xr), LatLon_(yb, xr))
63 return cs
66def _eq(p1, p2, eps=EPS):
67 '''(INTERNAL) Check for near-equal points.
68 '''
69 return not _neq(p1, p2, eps)
72def _neq(p1, p2, eps=EPS):
73 '''(INTERNAL) Check for not near-equal points.
74 '''
75 return fabs(p1.lat - p2.lat) > eps or \
76 fabs(p1.lon - p2.lon) > eps
79def _pts2(points, closed, inull):
80 '''(INTERNAL) Get the points to clip as a list.
81 '''
82 if closed and inull:
83 n, pts = len2(points)
84 # only remove the final, closing point
85 if n > 1 and _eq(pts[n-1], pts[0]):
86 n -= 1
87 pts = pts[:n]
88 if n < 2:
89 raise PointsError(points=n, txt=_too_(_few_))
90 else:
91 n, pts = points2(points, closed=closed)
92 return n, list(pts)
95class _CS(_Named):
96 '''(INTERNAL) Cohen-Sutherland line clipping.
97 '''
98 # single-bit clip codes
99 _IN = 0 # inside clip box
100 _XR = 1 # right of upperight.lon
101 _XL = 2 # left of lowerleft.lon
102 _YT = 4 # above upperight.lat
103 _YB = 8 # below lowerleft.lat
105 _dx = _0_0 # pts edge delta lon
106 _dy = _0_0 # pts edge delta lat
107 _x1 = _0_0 # pts corner
108 _y1 = _0_0 # pts corner
110 _xr = _0_0 # clip box upperight.lon
111 _xl = _0_0 # clip box lowerleft.lon
112 _yt = _0_0 # clip box upperight.lat
113 _yb = _0_0 # clip box lowerleft.lat
115 def __init__(self, lowerleft, upperight, name=__name__):
116 self._xl, self._yb, \
117 self._xr, self._yt = _box4(lowerleft, upperight, name)
118 self.name = name
120# def clip4(self, p, c): # clip point p for code c
121# if c & _CS._YB:
122# return self.lon4(p, self._yb)
123# elif c & _CS._YT:
124# return self.lon4(p, self._yt)
125# elif c & _CS._XL:
126# return self.lat4(p, self._xl)
127# elif c & _CS._XR:
128# return self.lat4(p, self._xr)
129# # should never get here
130# raise _AssertionError(self._DOT_(typename(self.clip4)))
132 def code4(self, p): # compute code for point p
133 if p.lat < self._yb:
134 c, m, b = _CS._YB, self.lon4, self._yb
135 elif p.lat > self._yt:
136 c, m, b = _CS._YT, self.lon4, self._yt
137 else:
138 c, m, b = _CS._IN, self.nop4, None
139 if p.lon < self._xl:
140 c |= _CS._XL
141 m, b = self.lat4, self._xl
142 elif p.lon > self._xr:
143 c |= _CS._XR
144 m, b = self.lat4, self._xr
145 return c, m, b, p
147 def edge(self, p1, p2): # set edge p1 to p2
148 self._y1, self._dy = p1.lat, float(p2.lat - p1.lat)
149 self._x1, self._dx = p1.lon, float(p2.lon - p1.lon)
150 return fabs(self._dx) > EPS or fabs(self._dy) > EPS
152 def lat4(self, x, p): # new lat and code at lon x
153 y = self._y1 + self._dy * float(x - self._x1) / self._dx
154 if y < self._yb: # still outside
155 return _CS._YB, self.lon4, self._yb, p
156 elif y > self._yt: # still outside
157 return _CS._YT, self.lon4, self._yt, p
158 else: # inside
159 return _CS._IN, self.nop4, None, p.classof(y, x)
161 def lon4(self, y, p): # new lon and code at lat y
162 x = self._x1 + self._dx * float(y - self._y1) / self._dy
163 if x < self._xl: # still outside
164 return _CS._XL, self.lat4, self._xl, p
165 elif x > self._xr: # still outside
166 return _CS._XR, self.lat4, self._xr, p
167 else: # inside
168 return _CS._IN, self.nop4, None, p.classof(y, x)
170 def nop4(self, b, p): # PYCHOK no cover
171 if p: # should never get here
172 raise _AssertionError(self._DOT_(typename(self.nop4)))
173 return _CS._IN, self.nop4, b, p
176class ClipCS4Tuple(_NamedTuple):
177 '''4-Tuple C{(start, end, i, j)} for each edge of a I{clipped}
178 path with the C{start} and C{end} points (C{LatLon}) of the
179 portion of the edge inside or on the clip box and the indices
180 C{i} and C{j} (C{int}) of the edge start and end points in
181 the original path.
182 '''
183 _Names_ = (_start_, _end_, _i_, _j_)
184 _Units_ = (_Pass, _Pass, Number_, Number_)
187def clipCS4(points, lowerleft, upperight, closed=False, inull=False):
188 '''Clip a path against a rectangular clip box using the U{Cohen-Sutherland
189 <https://WikiPedia.org/wiki/Cohen-Sutherland_algorithm>} algorithm.
191 @arg points: The points (C{LatLon}[]).
192 @arg lowerleft: Bottom-left corner of the clip box (C{LatLon}).
193 @arg upperight: Top-right corner of the clip box (C{LatLon}).
194 @kwarg closed: Optionally, close the path (C{bool}).
195 @kwarg inull: Optionally, retain null edges if inside (C{bool}).
197 @return: Yield a L{ClipCS4Tuple}C{(start, end, i, j)} for each
198 edge of the I{clipped} path.
200 @raise ClipError: The B{C{lowerleft}} and B{C{upperight}} corners
201 specify an invalid clip box.
203 @raise PointsError: Insufficient number of B{C{points}}.
204 '''
205 T4 = ClipCS4Tuple
206 cs = _CS(lowerleft, upperight, name=typename(clipCS4))
207 n, pts = _pts2(points, closed, inull)
209 i, m = _imdex2(closed, n)
210 cmbp = cs.code4(pts[i])
211 for j in range(m, n):
212 c1, m1, b1, p1 = cmbp
213 c2, m2, b2, p2 = cmbp = cs.code4(pts[j])
214 if c1 & c2: # edge outside
215 pass
216 elif cs.edge(p1, p2):
217 for _ in range(5):
218 if c1: # clip p1
219 c1, m1, b1, p1 = m1(b1, p1)
220 elif c2: # clip p2
221 c2, m2, b2, p2 = m2(b2, p2)
222 else: # inside
223 if inull or _neq(p1, p2):
224 yield T4(p1, p2, i, j)
225 break
226 if c1 & c2: # edge outside
227 break
228 else: # PYCHOK no cover
229 raise _AssertionError(_DOT_(cs.name, 'for_else'))
231 elif inull and not c1: # null edge
232 yield T4(p1, p1, i, j)
233 elif inull and not c2:
234 yield T4(p2, p2, i, j)
236 i = j
239class ClipFHP4Tuple(_NamedTuple):
240 '''4-Tuple C{(lat, lon, height, clipid)} for each point of the
241 L{clipFHP4} result with the C{lat}-, C{lon}gitude, C{height}
242 and C{clipid} of the polygon or clip.
244 @note: The C{height} is a L{HeightX} instance if this point is
245 an intersection, otherwise a L{Height} or C{int(0)}.
246 '''
247 _Names_ = (_lat_, _lon_, _height_, _clipid_)
248 _Units_ = ( Lat, Lon, _Pass, Number_)
250 @Property_RO
251 def isintersection(self):
252 '''Is this an intersection?
253 '''
254 return isinstance(self.height, HeightX)
256 @Property_RO
257 def ispoint(self):
258 '''Is this an original (polygon) point?
259 '''
260 return not self.isintersection
263def clipFHP4(points, corners, closed=False, inull=False, raiser=False, eps=EPS):
264 '''Clip one or more polygons against a clip region or box using U{Forster-Hormann-Popa
265 <https://www.ScienceDirect.com/science/article/pii/S259014861930007X>}'s C++
266 implementation transcoded to pure Python.
268 @arg points: The polygon points and clips (C{LatLon}[]).
269 @arg corners: Three or more points defining the clip regions (C{LatLon}[])
270 or two points to specify a single, rectangular clip box.
271 @kwarg closed: If C{True}, close each result clip (C{bool}).
272 @kwarg inull: If C{True}, retain null edges in result clips (C{bool}).
273 @kwarg raiser: If C{True}, throw L{ClipError} exceptions (C{bool}).
274 @kwarg esp: Tolerance for eliminating null edges (C{degrees}, same units
275 as the B{C{points}} and B{C{corners}} coordinates).
277 @return: Yield a L{ClipFHP4Tuple}C{(lat, lon, height, clipid)} for each
278 clipped point. The result may consist of several clips, each
279 a (closed) polygon with a unique C{clipid}.
281 @raise ClipError: Insufficient B{C{points}} or B{C{corners}} or an open clip.
283 @see: U{Forster, Hormann and Popa<https://www.ScienceDirect.com/science/
284 article/pii/S259014861930007X>}, class L{BooleanFHP} and function
285 L{clipGH4}.
286 '''
287 P = _MODS.booleans._CompositeFHP(points, kind=_points_, raiser=raiser,
288 eps=eps, name__=clipFHP4)
289 Q = _4corners(corners)
290 return P._clip(Q, Union=False, Clas=ClipFHP4Tuple, closed=closed,
291 inull=inull, raiser=P._raiser, eps=eps)
294class ClipGH4Tuple(ClipFHP4Tuple):
295 '''4-Tuple C{(lat, lon, height, clipid)} for each point of the
296 L{clipGH4} result with the C{lat}-, C{lon}gitude, C{height}
297 and C{clipid} of the polygon or clip.
299 @note: The C{height} is a L{HeightX} instance if this is
300 an intersection, otherwise a L{Height} or C{int(0)}.
301 '''
302 _Names_ = ClipFHP4Tuple._Names_
303 _Units_ = ClipFHP4Tuple._Units_
306def clipGH4(points, corners, closed=False, inull=False, raiser=True, xtend=False, eps=EPS):
307 '''Clip one or more polygons against a clip region or box using the U{Greiner-Hormann
308 <http://www.Inf.USI.CH/hormann/papers/Greiner.1998.ECO.pdf>} algorithm, U{Correia
309 <https://GitHub.com/helderco/univ-polyclip>}'s implementation modified and extended.
311 @arg points: The polygon points and clips (C{LatLon}[]).
312 @arg corners: Three or more points defining the clip regions (C{LatLon}[])
313 or two points to specify a single, rectangular clip box.
314 @kwarg closed: If C{True}, close each result clip (C{bool}).
315 @kwarg inull: If C{True}, retain null edges in result clips (C{bool}).
316 @kwarg raiser: If C{True}, throw L{ClipError} exceptions (C{bool}).
317 @kwarg xtend: If C{True}, extend edges of I{degenerate cases}, an attempt
318 to handle the latter (C{bool}).
319 @kwarg esp: Tolerance for eliminating null edges (C{degrees}, same units
320 as the B{C{points}} and B{C{corners}} coordinates).
322 @return: Yield a L{ClipGH4Tuple}C{(lat, lon, height, clipid)} for each
323 clipped point. The result may consist of several clips, each
324 a (closed) polygon with a unique C{clipid}.
326 @raise ClipError: Insufficient B{C{points}} or B{C{corners}}, an open clip,
327 a I{degenerate case} or I{unhandled} intersection.
329 @note: To handle I{degenerate cases} like C{point-edge} and C{point-point}
330 intersections, use function L{clipFHP4}.
332 @see: U{Greiner-Hormann<https://WikiPedia.org/wiki/Greiner–Hormann_clipping_algorithm>},
333 U{Ionel Daniel Stroe<https://Davis.WPI.edu/~matt/courses/clipping/>}, I{Correia}'s
334 U{univ-polyclip<https://GitHub.com/helderco/univ-polyclip>}, class L{BooleanGH}
335 and function L{clipFHP4}.
336 '''
337 S = _MODS.booleans._CompositeGH(points, raiser=raiser, xtend=xtend, eps=eps,
338 name__=clipGH4, kind=_points_)
339 C = _4corners(corners)
340 return S._clip(C, False, False, Clas=ClipGH4Tuple, closed=closed, inull=inull,
341 raiser=S._raiser, xtend=S._xtend, eps=eps)
344def _LBtrim(p, q, t):
345 # Liang-Barsky trim t[0] or t[1]
346 if p < 0:
347 r = q / p
348 if r > t[1]:
349 return False # too far above
350 elif r > t[0]:
351 t[0] = r
352 elif p > 0:
353 r = q / p
354 if r < t[0]:
355 return False # too far below
356 elif r < t[1]:
357 t[1] = r
358 elif q < 0: # vertical or horizontal
359 return False # ... outside
360 return True
363class ClipLB6Tuple(_NamedTuple):
364 '''6-Tuple C{(start, end, i, fi, fj, j)} for each edge of the
365 I{clipped} path with the C{start} and C{end} points (C{LatLon})
366 of the portion of the edge inside or on the clip box, indices
367 C{i} and C{j} (both C{int}) of the original path edge start
368 and end points and I{fractional} indices C{fi} and C{fj}
369 (both L{FIx}) of the C{start} and C{end} points along the
370 edge of the original path.
372 @see: Class L{FIx} and function L{pygeodesy.fractional}.
373 '''
374 _Names_ = (_start_, _end_, _i_, _fi_, _fj_, _j_)
375 _Units_ = (_Pass, _Pass, Number_, _Pass, _Pass, Number_)
378def clipLB6(points, lowerleft, upperight, closed=False, inull=False):
379 '''Clip a path against a rectangular clip box using the U{Liang-Barsky
380 <https://www.CSE.UNT.edu/~renka/4230/LineClipping.pdf>} algorithm.
382 @arg points: The points (C{LatLon}[]).
383 @arg lowerleft: Bottom-left corner of the clip box (C{LatLon}).
384 @arg upperight: Top-right corner of the clip box (C{LatLon}).
385 @kwarg closed: Optionally, close the path (C{bool}).
386 @kwarg inull: Optionally, retain null edges if inside (C{bool}).
388 @return: Yield a L{ClipLB6Tuple}C{(start, end, i, fi, fj, j)} for
389 each edge of the I{clipped} path.
391 @raise ClipError: The B{C{lowerleft}} and B{C{upperight}} corners
392 specify an invalid clip box.
394 @raise PointsError: Insufficient number of B{C{points}}.
396 @see: U{Liang-Barsky Line Clipping<https://www.CS.Helsinki.FI/group/goa/
397 viewing/leikkaus/intro.html>}, U{Liang-Barsky line clipping algorithm
398 <https://www.Skytopia.com/project/articles/compsci/clipping.html>} and
399 U{Liang-Barsky algorithm<https://WikiPedia.org/wiki/Liang-Barsky_algorithm>}.
400 '''
401 xl, yb, \
402 xr, yt = _box4(lowerleft, upperight, typename(clipLB6))
403 n, pts = _pts2(points, closed, inull)
405 T6 = ClipLB6Tuple
406 fin = n if closed else None # wrapping fi [n] to [0]
407 _LB = _LBtrim
409 i, m = _imdex2(closed, n)
410 for j in range(m, n):
411 p1 = pts[i]
412 y1 = p1.lat
413 x1 = p1.lon
415 p2 = pts[j]
416 dy = float(p2.lat - y1)
417 dx = float(p2.lon - x1)
418 if fabs(dx) > EPS or fabs(dy) > EPS:
419 # non-null edge pts[i]...pts[j]
420 t = [_0_0, _1_0]
421 if _LB(-dx, -xl + x1, t) and \
422 _LB( dx, xr - x1, t) and \
423 _LB(-dy, -yb + y1, t) and \
424 _LB( dy, yt - y1, t):
425 # clip edge pts[i]...pts[j]
426 # at fractions t[0] to t[1]
427 f, t = t
428 if f > _0_0: # EPS
429 p1 = p1.classof(y1 + f * dy,
430 x1 + f * dx)
431 fi = i + f
432 else:
433 fi = i
435 if (t - f) > EPS: # EPS0
436 if t < _1_0: # EPS1
437 p2 = p2.classof(y1 + t * dy,
438 x1 + t * dx)
439 fj = i + t
440 else:
441 fj = j
442 fi = FIx(fi, fin=fin)
443 fj = FIx(fj, fin=fin)
444 yield T6(p1, p2, i, fi, fj, j)
446 elif inull:
447 fi = FIx(fi, fin=fin)
448 yield T6(p1, p1, i, fi, fi, j)
449# else: # outside
450# pass
451 elif inull: # null edge
452 yield T6(p1, p2, i, FIx(i, fin=fin),
453 FIx(j, fin=fin), j)
454 i = j
457class _SH(_Named):
458 '''(INTERNAL) Sutherland-Hodgman polyon clipping.
459 '''
460 _cs = () # clip corners
461 _cw = 0 # counter-/clockwise
462 _ccw = 0 # clock-/counterwise
463 _dx = _0_0 # clip edge[e] delta lon
464 _dy = _0_0 # clip edge[e] delta lat
465 _nc = 0 # len(._cs)
466 _x1 = _0_0 # clip edge[e] lon origin
467 _xy = _0_0 # see .clipedges
468 _y1 = _0_0 # clip edge[e] lat origin
470 def __init__(self, corners, name=__name__):
471 n, cs = 0, corners
472 try: # check the clip box/region
473 cs = _4corners(cs)
474 n, cs = len2(cs)
475 n, cs = points2(cs, closed=True)
476 self._cs = cs = cs[:n]
477 self._nc = n
478 self._cw = isconvex_(cs, adjust=False, wrap=False)
479 if not self._cw:
480 raise ValueError(_not_(_convex_))
481 if areaOf(cs, adjust=True, radius=1, wrap=True) < EPS:
482 raise ValueError(NN(_near_, 'zero area'))
483 self._ccw = -self._cw
484 except (PointsError, TypeError, ValueError) as x:
485 raise ClipError(name, n, cs, cause=x)
486 self.name = name
488 def clip2(self, points, closed, inull): # MCCABE 13, clip points
489 np, pts = _pts2(points, closed, inull)
490 pcs = _SHlist(inull) # clipped points
491 _ap = pcs.append
492 _d2 = self.dot2
493 _in = self.intersect
495 ne = 0 # number of non-null clip edges
496 for e in self.clipedges():
497 ne += 1 # non-null clip edge
499 # clip points, closed always
500 d1, p1 = _d2(pts[np - 1])
501 for i in range(np):
502 d2, p2 = _d2(pts[i])
503 if d1 < 0: # p1 inside, p2 ...
504 # _ap(p1)
505 _ap(p2 if d2 < 0 else # ... in-
506 _in(p1, p2, e)) # ... outside
507 elif d2 < 0: # p1 out-, p2 inside
508 _ap(_in(p1, p2, e))
509 _ap(p2)
510# elif d1 > 0: # both outside
511# pass
512 d1, p1 = d2, p2
514 # replace points, in-place
515 pts[:] = pcs
516 pcs[:] = []
517 np = len(pts)
518 if not np: # all outside
519 break
520 else:
521 if ne < 3:
522 raise ClipError(self.name, ne, self._cs, txt=_too_(_few_))
524 if np > 1:
525 p = pts[0]
526 if closed: # close clipped pts
527 if _neq(pts[np - 1], p):
528 pts.append(p)
529 np += 1
530 elif not inull: # open clipped pts
531 while np > 0 and _eq(pts[np - 1], p):
532 pts.pop()
533 np -= 1
534 # assert len(pts) == np
535 return np, pts
537 def clipedges(self): # yield clip edge index
538 # and set self._x1, ._y1, ._dx, ._dy and
539 # ._xy for each non-null clip edge
540 nc = self._nc
541 cs = self._cs
542 c = cs[nc - 1]
543 for e in range(nc):
544 y, x, c = c.lat, c.lon, cs[e]
545 dy = float(c.lat - y)
546 dx = float(c.lon - x)
547 if fabs(dx) > EPS or fabs(dy) > EPS:
548 self._y1, self._dy = y, dy
549 self._x1, self._dx = x, dx
550 self._xy = y * dx - x * dy
551 yield e + 1
553 def clipped2(self, p): # return (clipped point [i], edge)
554 if isinstance(p, _SHlli): # intersection point
555 return p.classof(p.lat, p.lon), p.edge
556 else: # original point
557 return p, 0
559 def dot2(self, p): # dot product of point p to clip
560 # corner c1 and clip edge c1 to c2, indicating where
561 # point p is located: to the right, to the left or
562 # on top of the (extended) clip edge from c1 to c2
563 d = float(p.lat - self._y1) * self._dx - \
564 float(p.lon - self._x1) * self._dy
565 # clockwise corners, +1 means point p is to the right
566 # of, -1 means on the left of, 0 means on edge c1 to c2
567 d = self._ccw if d < 0 else (self._cw if d > 0 else 0)
568 return d, p
570 def intersect(self, p1, p2, edge): # compute intersection
571 # of polygon edge p1 to p2 and the current clip edge,
572 # where p1 and p2 are known to NOT be located on the
573 # same side of or on the current, non-null clip edge
574 # <https://StackOverflow.com/questions/563198/
575 # how-do-you-detect-where-two-line-segments-intersect>
576 y, dy = p1.lat, self._dy
577 x, dx = p1.lon, self._dx
578 fy = float(p2.lat - y)
579 fx = float(p2.lon - x)
580 d = fy * dx - fx * dy # fdot((fx, fy), dx, -dy)
581 if fabs(d) < EPS: # PYCHOK no cover
582 raise _AssertionError(self._DOT_(typename(self.intersect)))
583 d = Fsum(self._xy, -y * dx, x * dy).fover(d)
584 y += d * fy
585 x += d * fx
586 return _SHlli(y, x, p1.classof, edge)
589class _SHlist(list):
590 '''(INTERNAL) List of _SH clipped points.
591 '''
592 _inull = False
594 def __init__(self, inull):
595 self._inull = inull
596 list.__init__(self)
598 def append(self, p):
599 if (not self) or self._inull or _neq(p, self[-1]):
600 list.append(self, p)
603class _SHlli(LatLon_):
604 '''(INTERNAL) LatLon_ for _SH intersections.
605 '''
606 # __slots__ are no longer space savers, see
607 # the comments at the class .points.LatLon_
608 # __slots__ = _lat_, _lon_, 'classof', 'edge', _name_
610 def __init__(self, lat, lon, classof, edge):
611 self.lat = lat
612 self.lon = lon
613 self.classof = classof
614 self.edge = edge # clip edge
615 self.name = NN
618class ClipSH3Tuple(_NamedTuple):
619 '''3-Tuple C{(start, end, original)} for each edge of a I{clipped}
620 polygon, the C{start} and C{end} points (C{LatLon}) of the
621 portion of the edge inside or on the clip region and C{original}
622 indicates whether the edge is part of the original polygon or
623 part of the clip region (C{bool}).
624 '''
625 _Names_ = (_start_, _end_, _original_)
626 _Units_ = (_Pass, _Pass, Bool)
629def clipSH(points, corners, closed=False, inull=False):
630 '''Clip a polygon against a clip region or box using the U{Sutherland-Hodgman
631 <https://WikiPedia.org/wiki/Sutherland-Hodgman_algorithm>} algorithm.
633 @arg points: The polygon points (C{LatLon}[]).
634 @arg corners: Three or more points defining a convex clip
635 region (C{LatLon}[]) or two points to specify
636 a rectangular clip box.
637 @kwarg closed: Close the clipped points (C{bool}).
638 @kwarg inull: Optionally, include null edges (C{bool}).
640 @return: Yield the clipped points (C{LatLon}[]).
642 @raise ClipError: The B{C{corners}} specify a polar, zero-area,
643 non-convex or otherwise invalid clip box or
644 region.
646 @raise PointsError: Insufficient number of B{C{points}}.
647 '''
648 sh = _SH(corners, name=typename(clipSH))
649 n, pts = sh.clip2(points, closed, inull)
650 for i in range(n):
651 p, _ = sh.clipped2(pts[i])
652 yield p
655def clipSH3(points, corners, closed=False, inull=False):
656 '''Clip a polygon against a clip region or box using the U{Sutherland-Hodgman
657 <https://WikiPedia.org/wiki/Sutherland-Hodgman_algorithm>} algorithm.
659 @arg points: The polygon points (C{LatLon}[]).
660 @arg corners: Three or more points defining a convex clip
661 region (C{LatLon}[]) or two points to specify
662 a rectangular clip box.
663 @kwarg closed: Close the clipped points (C{bool}).
664 @kwarg inull: Optionally, include null edges (C{bool}).
666 @return: Yield a L{ClipSH3Tuple}C{(start, end, original)} for
667 each edge of the I{clipped} polygon.
669 @raise ClipError: The B{C{corners}} specify a polar, zero-area,
670 non-convex or otherwise invalid clip box or
671 region.
673 @raise PointsError: Insufficient number of B{C{points}} or B{C{corners}}.
674 '''
675 sh = _SH(corners, name=typename(clipSH3))
676 n, pts = sh.clip2(points, closed, inull)
677 if n > 1:
678 T3 = ClipSH3Tuple
679 p1, e1 = sh.clipped2(pts[0])
680 for i in range(1, n):
681 p2, e2 = sh.clipped2(pts[i])
682 yield T3(p1, p2, not bool(e1 and e2 and e1 == e2))
683 p1, e1 = p2, e2
685# **) MIT License
686#
687# Copyright (C) 2018-2025 -- mrJean1 at Gmail -- All Rights Reserved.
688#
689# Permission is hereby granted, free of charge, to any person obtaining a
690# copy of this software and associated documentation files (the "Software"),
691# to deal in the Software without restriction, including without limitation
692# the rights to use, copy, modify, merge, publish, distribute, sublicense,
693# and/or sell copies of the Software, and to permit persons to whom the
694# Software is furnished to do so, subject to the following conditions:
695#
696# The above copyright notice and this permission notice shall be included
697# in all copies or substantial portions of the Software.
698#
699# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
700# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
701# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
702# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
703# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
704# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
705# OTHER DEALINGS IN THE SOFTWARE.