Coverage for pygeodesy/clipy.py: 96%

316 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-04-25 13:15 -0400

1 

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

3 

4u'''Clip a path or polygon of C{LatLon} points against a rectangular box or 

5an arbitrary (convex) region. 

6 

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 

14 

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_ 

30 

31# from math import fabs # from .fmath 

32 

33__all__ = _ALL_LAZY.clipy 

34__version__ = '25.04.14' 

35 

36_fj_ = 'fj' 

37_original_ = 'original' 

38 

39 

40def _box4(lowerleft, upperight, name): 

41 '''(INTERNAL) Get the clip box edges. 

42 

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 

53 

54 

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 

64 

65 

66def _eq(p1, p2, eps=EPS): 

67 '''(INTERNAL) Check for near-equal points. 

68 ''' 

69 return not _neq(p1, p2, eps) 

70 

71 

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 

77 

78 

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) 

93 

94 

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 

104 

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 

109 

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 

114 

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 

119 

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))) 

131 

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 

146 

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 

151 

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) 

160 

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) 

169 

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 

174 

175 

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_) 

185 

186 

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. 

190 

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}). 

196 

197 @return: Yield a L{ClipCS4Tuple}C{(start, end, i, j)} for each 

198 edge of the I{clipped} path. 

199 

200 @raise ClipError: The B{C{lowerleft}} and B{C{upperight}} corners 

201 specify an invalid clip box. 

202 

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) 

208 

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')) 

230 

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) 

235 

236 i = j 

237 

238 

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. 

243 

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_) 

249 

250 @Property_RO 

251 def isintersection(self): 

252 '''Is this an intersection? 

253 ''' 

254 return isinstance(self.height, HeightX) 

255 

256 @Property_RO 

257 def ispoint(self): 

258 '''Is this an original (polygon) point? 

259 ''' 

260 return not self.isintersection 

261 

262 

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. 

267 

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). 

276 

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}. 

280 

281 @raise ClipError: Insufficient B{C{points}} or B{C{corners}} or an open clip. 

282 

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) 

292 

293 

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. 

298 

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_ 

304 

305 

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. 

310 

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). 

321 

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}. 

325 

326 @raise ClipError: Insufficient B{C{points}} or B{C{corners}}, an open clip, 

327 a I{degenerate case} or I{unhandled} intersection. 

328 

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

330 intersections, use function L{clipFHP4}. 

331 

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) 

342 

343 

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 

361 

362 

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. 

371 

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_) 

376 

377 

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. 

381 

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}). 

387 

388 @return: Yield a L{ClipLB6Tuple}C{(start, end, i, fi, fj, j)} for 

389 each edge of the I{clipped} path. 

390 

391 @raise ClipError: The B{C{lowerleft}} and B{C{upperight}} corners 

392 specify an invalid clip box. 

393 

394 @raise PointsError: Insufficient number of B{C{points}}. 

395 

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) 

404 

405 T6 = ClipLB6Tuple 

406 fin = n if closed else None # wrapping fi [n] to [0] 

407 _LB = _LBtrim 

408 

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 

414 

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 

434 

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) 

445 

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 

455 

456 

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 

469 

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 

487 

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 

494 

495 ne = 0 # number of non-null clip edges 

496 for e in self.clipedges(): 

497 ne += 1 # non-null clip edge 

498 

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 

513 

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_)) 

523 

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 

536 

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 

552 

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 

558 

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 

569 

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) 

587 

588 

589class _SHlist(list): 

590 '''(INTERNAL) List of _SH clipped points. 

591 ''' 

592 _inull = False 

593 

594 def __init__(self, inull): 

595 self._inull = inull 

596 list.__init__(self) 

597 

598 def append(self, p): 

599 if (not self) or self._inull or _neq(p, self[-1]): 

600 list.append(self, p) 

601 

602 

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_ 

609 

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 

616 

617 

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) 

627 

628 

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. 

632 

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}). 

639 

640 @return: Yield the clipped points (C{LatLon}[]). 

641 

642 @raise ClipError: The B{C{corners}} specify a polar, zero-area, 

643 non-convex or otherwise invalid clip box or 

644 region. 

645 

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 

653 

654 

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. 

658 

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}). 

665 

666 @return: Yield a L{ClipSH3Tuple}C{(start, end, original)} for 

667 each edge of the I{clipped} polygon. 

668 

669 @raise ClipError: The B{C{corners}} specify a polar, zero-area, 

670 non-convex or otherwise invalid clip box or 

671 region. 

672 

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 

684 

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.