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

1#!/usr/bin/env python3 

2 

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 

22 

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

27 

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) 

62 

63 Returns 

64 ------- 

65 buffs : shapely.geometry.polygon.Polygon, shapely.geometry.multipolygon.MultiPolygon 

66 the buffered Polygon 

67 

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>`_ : 

72 

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

77 

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>`_ : 

81 

82 "A sign of 1.0 means that the coordinates of the product's exterior ring 

83 will be oriented counter-clockwise." 

84 

85 Copyright 2017 Thomas Guymer [1]_ 

86 

87 References 

88 ---------- 

89 .. [1] PyGuymer3, https://github.com/Guymer/PyGuymer3 

90 """ 

91 

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 

99 

100 # Import sub-functions ... 

101 from ..check import check 

102 from ..fillin import fillin 

103 from .buffer_LinearRing import buffer_LinearRing 

104 

105 # ************************************************************************** 

106 

107 # Check argument ... 

108 assert isinstance(poly, shapely.geometry.polygon.Polygon), "\"poly\" is not a Polygon" 

109 if debug: 

110 check(poly, prefix = prefix) 

111 

112 # Initialize list ... 

113 buffs = [] 

114 

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

123 

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 ) 

146 

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 

156 

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 ) 

174 

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) 

179 

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) 

198 

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) 

207 

208 # Return simplified answer ... 

209 return buffsSimp 

210 

211 # Return answer ... 

212 return buffs