Coverage for gws-app / gws / lib / extent / __init__.py: 94%

82 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-03 10:12 +0100

1from typing import Optional 

2 

3import math 

4import re 

5 

6import gws 

7import gws.lib.crs 

8 

9 

10def from_string(s: str) -> Optional[gws.Extent]: 

11 """Create an extent from a comma-separated string 'x-min,y-min,x-max,y-max' """ 

12 

13 return _from_string_list(s.split(',')) 

14 

15 

16def from_list(ls: list) -> Optional[gws.Extent]: 

17 """Create an extent from a list ``[x-min,y-min,x-max,y-max]``.""" 

18 

19 return _from_string_list(ls) 

20 

21 

22def from_points(a: gws.Point, b: gws.Point) -> gws.Extent: 

23 """Create an extent from two points.""" 

24 

25 return ( 

26 min(a[0], b[0]), 

27 min(a[1], b[1]), 

28 max(a[0], b[0]), 

29 max(a[1], b[1]), 

30 ) 

31 

32 

33def from_center(xy: gws.Point, size: gws.Size) -> gws.Extent: 

34 """Create an extent with certain size from a center-point.""" 

35 

36 return ( 

37 xy[0] - size[0] / 2, 

38 xy[1] - size[1] / 2, 

39 xy[0] + size[0] / 2, 

40 xy[1] + size[1] / 2, 

41 ) 

42 

43 

44def from_box(box: str) -> Optional[gws.Extent]: 

45 """Create an extent from a Postgis BOX(minx miny,maxx maxy). 

46 

47 Args: 

48 box: Postgis BOX. 

49 

50 Returns: 

51 An extent.""" 

52 

53 if not box: 

54 return None 

55 

56 m = re.match(r'^BOX\((.+?)\)$', str(box).upper()) 

57 if not m: 

58 return None 

59 

60 return _from_string_list(m.group(1).replace(',', ' ').split(' ')) 

61 

62 

63# 

64 

65 

66def intersection(exts: list[gws.Extent]) -> Optional[gws.Extent]: 

67 """Creates an extent that is the intersection of all given extents. 

68 

69 Args: 

70 exts: Extents. 

71 

72 Returns: 

73 An extent. 

74 """ 

75 

76 if not exts: 

77 return 

78 

79 res = (-math.inf, -math.inf, math.inf, math.inf) 

80 

81 for ext in exts: 

82 if not intersect(res, ext): 

83 return 

84 res = ( 

85 max(res[0], ext[0]), 

86 max(res[1], ext[1]), 

87 min(res[2], ext[2]), 

88 min(res[3], ext[3]), 

89 ) 

90 return res 

91 

92 

93def center(e: gws.Extent) -> gws.Point: 

94 """The center-point of the extent""" 

95 

96 return ( 

97 e[0] + (e[2] - e[0]) / 2, 

98 e[1] + (e[3] - e[1]) / 2, 

99 ) 

100 

101 

102def size(e: gws.Extent) -> gws.Size: 

103 """The size of the extent ``(width,height)""" 

104 

105 return ( 

106 e[2] - e[0], 

107 e[3] - e[1], 

108 ) 

109 

110 

111def diagonal(e: gws.Extent) -> float: 

112 """The length of the diagonal""" 

113 

114 return math.sqrt((e[2] - e[0]) ** 2 + (e[3] - e[1]) ** 2) 

115 

116 

117def circumsquare(e: gws.Extent) -> gws.Extent: 

118 """A circumscribed square of the extent.""" 

119 

120 d = diagonal(e) 

121 return from_center(center(e), (d, d)) 

122 

123 

124def buffer(e: gws.Extent, buf: int) -> gws.Extent: 

125 """Creates an extent with buffer to another extent. 

126 

127 Args: 

128 e: An extent. 

129 buf: Buffer between e and the output. If buf is positive the returned extent will be bigger. 

130 

131 Returns: 

132 An extent. 

133 """ 

134 

135 if buf == 0: 

136 return e 

137 return ( 

138 e[0] - buf, 

139 e[1] - buf, 

140 e[2] + buf, 

141 e[3] + buf, 

142 ) 

143 

144 

145def union(exts: list[gws.Extent]) -> gws.Extent: 

146 """Creates the smallest extent that contains all the given extents. 

147 

148 Args: 

149 exts: Extents. 

150 

151 Returns: 

152 An Extent. 

153 """ 

154 

155 ext = exts[0] 

156 for e in exts: 

157 ext = ( 

158 min(ext[0], e[0]), 

159 min(ext[1], e[1]), 

160 max(ext[2], e[2]), 

161 max(ext[3], e[3]), 

162 ) 

163 return ext 

164 

165 

166def intersect(a: gws.Extent, b: gws.Extent) -> bool: 

167 """Returns ``True`` if the extents are intersecting, otherwise ``False``.""" 

168 

169 return a[0] <= b[2] and a[2] >= b[0] and a[1] <= b[3] and a[3] >= b[1] 

170 

171 

172def transform(e: gws.Extent, crs_from: gws.Crs, crs_to: gws.Crs) -> gws.Extent: 

173 """Transforms the extent to a different coordinate reference system. 

174 

175 Args: 

176 e: An extent. 

177 crs_from: Input crs. 

178 crs_to: Output crs. 

179 

180 Returns: 

181 The transformed extent. 

182 """ 

183 

184 return crs_from.transform_extent(e, crs_to) 

185 

186 

187def transform_from_wgs(e: gws.Extent, crs_to: gws.Crs) -> gws.Extent: 

188 """Transforms the extent in WGS84 to a different coordinate reference system. 

189 

190 Args: 

191 e: An extent. 

192 crs_to: Output crs. 

193 

194 Returns: 

195 The transformed extent. 

196 """ 

197 

198 return gws.lib.crs.WGS84.transform_extent(e, crs_to) 

199 

200 

201def transform_to_wgs(e: gws.Extent, crs_from: gws.Crs) -> gws.Extent: 

202 """Transforms the extent to WGS84. 

203 

204 Args: 

205 e: An extent. 

206 crs_from: Input crs. 

207 

208 Returns: 

209 The WGS84 extent. 

210 """ 

211 

212 return crs_from.transform_extent(e, gws.lib.crs.WGS84) 

213 

214 

215def swap_xy(e: gws.Extent) -> gws.Extent: 

216 """Swaps the x and y values of the extent""" 

217 return e[1], e[0], e[3], e[2] 

218 

219 

220def is_valid(e: gws.Extent) -> bool: 

221 """Checks if the extent is valid.""" 

222 

223 if not e or len(e) != 4: 

224 return False 

225 if not all(math.isfinite(p) for p in e): 

226 return False 

227 if e[0] >= e[2] or e[1] >= e[3]: 

228 return False 

229 return True 

230 

231 

232def is_valid_wgs(e: gws.Extent) -> bool: 

233 """Checks if the extent is valid and within WGS84 bounds.""" 

234 

235 if not is_valid(e): 

236 return False 

237 w = gws.lib.crs.WGS84.extent 

238 return e[0] >= w[0] and e[1] >= w[1] and e[2] <= w[2] and e[3] <= w[3] 

239 

240 

241def _from_string_list(ls: list) -> Optional[gws.Extent]: 

242 if len(ls) != 4: 

243 return None 

244 try: 

245 e = [float(p) for p in ls] 

246 except ValueError: 

247 return None 

248 if not all(math.isfinite(p) for p in e): 

249 return None 

250 if e[0] >= e[2] or e[1] >= e[3]: 

251 return None 

252 return e[0], e[1], e[2], e[3]