Coverage for pyguymer3/geo/bufferSrc/buffer_Polygon.py: 55%
38 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
1#!/usr/bin/env python3
3# Define function ...
4def buffer_Polygon(
5 poly,
6 dist,
7 /,
8 *,
9 debug = __debug__,
10 eps = 1.0e-12,
11 fill = 1.0,
12 fillSpace = "EuclideanSpace",
13 keepInteriors = True,
14 nAng = 9,
15 nIter = 100,
16 prefix = ".",
17 ramLimit = 1073741824,
18 simp = 0.1,
19 tol = 1.0e-10,
20):
21 """Buffer a Polygon
23 This function reads in a Polygon (with an exterior and any number of
24 interiors) that exists on the surface of the Earth and returns a
25 [Multi]Polygon of the same Polygon buffered by a constant distance (in
26 metres).
28 Parameters
29 ----------
30 poly : shapely.geometry.polygon.Polygon
31 the Polygon
32 dist : float
33 the Geodesic distance to buffer each point within the Polygon by (in
34 metres)
35 debug : bool, optional
36 print debug messages
37 eps : float, optional
38 the tolerance of the Vincenty formula iterations
39 fill : float, optional
40 the Euclidean or Geodesic distance to fill in between each point within
41 the shapes by (in degrees or metres)
42 fillSpace : str, optional
43 the geometric space to perform the filling in (either "EuclideanSpace"
44 or "GeodesicSpace")
45 keepInteriors : bool, optional
46 keep the interiors of the Polygon
47 nAng : int, optional
48 the number of angles around each point within the Polygon that are
49 calculated when buffering
50 nIter : int, optional
51 the maximum number of iterations (particularly the Vincenty formula)
52 prefix : str, optional
53 change the name of the output debugging CSVs
54 ramLimit : int, optional
55 the maximum RAM usage of each "large" array (in bytes)
56 simp : float, optional
57 how much intermediary [Multi]Polygons are simplified by; negative values
58 disable simplification (in degrees)
59 tol : float, optional
60 the Euclidean distance that defines two points as being the same (in
61 degrees)
63 Returns
64 -------
65 buffs : shapely.geometry.polygon.Polygon, shapely.geometry.multipolygon.MultiPolygon
66 the buffered Polygon
68 Notes
69 -----
70 According to the `Shapely documentation for the method object.buffer()
71 <https://shapely.readthedocs.io/en/stable/manual.html#object.buffer>`_ :
73 "Passed a distance of 0, buffer() can sometimes be used to "clean"
74 self-touching or self-crossing polygons such as the classic "bowtie".
75 Users have reported that very small distance values sometimes produce
76 cleaner results than 0. Your mileage may vary when cleaning surfaces."
78 According to the `Shapely documentation for the function
79 shapely.geometry.polygon.orient()
80 <https://shapely.readthedocs.io/en/stable/manual.html#shapely.geometry.polygon.orient>`_ :
82 "A sign of 1.0 means that the coordinates of the product's exterior ring
83 will be oriented counter-clockwise."
85 Copyright 2017 Thomas Guymer [1]_
87 References
88 ----------
89 .. [1] PyGuymer3, https://github.com/Guymer/PyGuymer3
90 """
92 # Import special modules ...
93 try:
94 import shapely
95 import shapely.geometry
96 import shapely.ops
97 except:
98 raise Exception("\"shapely\" is not installed; run \"pip install --user Shapely\"") from None
100 # Import sub-functions ...
101 from ..check import check
102 from ..fillin import fillin
103 from .buffer_LinearRing import buffer_LinearRing
105 # **************************************************************************
107 # Check argument ...
108 assert isinstance(poly, shapely.geometry.polygon.Polygon), "\"poly\" is not a Polygon"
109 if debug:
110 check(poly, prefix = prefix)
112 # Initialize list ...
113 buffs = []
115 # Check if the user wants to keep interiors ...
116 if keepInteriors:
117 # Append Polygon to list ...
118 buffs.append(poly)
119 else:
120 # Append a correctly oriented Polygon made up of just the exterior
121 # LinearRing to list ...
122 buffs.append(shapely.geometry.polygon.orient(shapely.geometry.polygon.Polygon(poly.exterior)))
124 # Append buffer of exterior LinearRing to list ...
125 # TODO: Think about finding the bounding box of the exterior ring and not
126 # bothering buffering it if is the whole Earth, i.e., the Polygon is
127 # the Earth with an interior ring. Make sure to not just use the
128 # bounding box, i.e., the exterior ring should not be more complicated
129 # than four corners too.
130 buffs.append(
131 buffer_LinearRing(
132 poly.exterior,
133 dist,
134 debug = debug,
135 eps = eps,
136 fill = fill,
137 fillSpace = fillSpace,
138 nAng = nAng,
139 nIter = nIter,
140 prefix = prefix,
141 ramLimit = ramLimit,
142 simp = simp,
143 tol = tol,
144 )
145 )
147 # Check if the user wants to keep interiors ...
148 if keepInteriors:
149 # Loop over interior LinearRings ...
150 for interior in poly.interiors:
151 # Skip if it doesn't contain any length ...
152 if interior.length < tol:
153 if debug:
154 print(f"INFO: Removing a tiny-length interior ring at ({interior.centroid.x:+.6f}°,{interior.centroid.y:+.6f}°).")
155 continue
157 # Append buffer of interior LinearRing to list ...
158 buffs.append(
159 buffer_LinearRing(
160 interior,
161 dist,
162 debug = debug,
163 eps = eps,
164 fill = fill,
165 fillSpace = fillSpace,
166 nAng = nAng,
167 nIter = nIter,
168 prefix = prefix,
169 ramLimit = ramLimit,
170 simp = simp,
171 tol = tol,
172 )
173 )
175 # Convert list of [Multi]Polygons to a (unified) [Multi]Polygon ...
176 buffs = shapely.ops.unary_union(buffs).simplify(tol)
177 if debug:
178 check(buffs, prefix = prefix)
180 # Check if the user wants to fill in the [Multi]Polygon ...
181 # NOTE: This is only needed because the "shapely.ops.unary_union()" call
182 # above includes a "simplify()".
183 if simp < 0.0 < fill:
184 # Fill in [Multi]Polygon ...
185 buffs = fillin(
186 buffs,
187 fill,
188 debug = debug,
189 eps = eps,
190 fillSpace = fillSpace,
191 nIter = nIter,
192 prefix = prefix,
193 ramLimit = ramLimit,
194 tol = tol,
195 )
196 if debug:
197 check(buffs, prefix = prefix)
199 # Check if the user wants to simplify the [Multi]Polygon ...
200 # NOTE: This is only needed because the "shapely.ops.unary_union()" call
201 # above might allow more simplification.
202 if simp > 0.0:
203 # Simplify [Multi]Polygon ...
204 buffsSimp = buffs.simplify(simp)
205 if debug:
206 check(buffsSimp, prefix = prefix)
208 # Return simplified answer ...
209 return buffsSimp
211 # Return answer ...
212 return buffs