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

89 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-16 23:09 +0200

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 "1000,2000,20000 40000". 

12 

13 Args: 

14 s: ``"x-min,y-min,x-max,y-max"`` 

15 

16 Returns: 

17 An extent. 

18 """ 

19 

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

21 

22 

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

24 """Create an extent from a list of values. 

25 

26 Args: 

27 ls: ``[x-min,y-min,x-max,y-max]`` 

28 

29 Returns: 

30 An extent.""" 

31 

32 return _from_string_list(ls) 

33 

34 

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

36 """Create an extent from two points. 

37 

38 Args: 

39 a:``(x-min,y-min)`` 

40 b:``(x-max,y-max)`` 

41 

42 Returns: 

43 An extent.""" 

44 

45 return ( 

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

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

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

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

50 ) 

51 

52 

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

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

55 

56 Args: 

57 xy: Center-point ``(x,y)`` 

58 size: Extent's size. 

59 

60 Returns: 

61 An Extent.""" 

62 

63 return ( 

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

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

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

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

68 ) 

69 

70 

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

72 """Create an extent from a Postgis BOX(1000 2000,20000 40000). 

73 

74 Args: 

75 box: Postgis BOX. 

76 

77 Returns: 

78 An extent.""" 

79 

80 if not box: 

81 return None 

82 

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

84 if not m: 

85 return None 

86 

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

88 

89 

90# 

91 

92 

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

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

95 

96 Args: 

97 exts: Extents. 

98 

99 Returns: 

100 An extent. 

101 """ 

102 

103 if not exts: 

104 return 

105 

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

107 

108 for ext in exts: 

109 if not intersect(res, ext): 

110 return 

111 res = ( 

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

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

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

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

116 ) 

117 return res 

118 

119 

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

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

122 

123 return ( 

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

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

126 ) 

127 

128 

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

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

131 

132 return ( 

133 e[2] - e[0], 

134 e[3] - e[1], 

135 ) 

136 

137 

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

139 """The length of the diagonal""" 

140 

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

142 

143 

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

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

146 

147 d = diagonal(e) 

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

149 

150 

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

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

153 

154 Args: 

155 e: An extent. 

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

157 

158 Returns: 

159 An extent. 

160 """ 

161 

162 if buf == 0: 

163 return e 

164 return ( 

165 e[0] - buf, 

166 e[1] - buf, 

167 e[2] + buf, 

168 e[3] + buf, 

169 ) 

170 

171 

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

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

174 

175 Args: 

176 exts: Extents. 

177 

178 Returns: 

179 An Extent. 

180 """ 

181 

182 ext = exts[0] 

183 for e in exts: 

184 ext = ( 

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

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

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

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

189 ) 

190 return ext 

191 

192 

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

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

195 

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

197 

198 

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

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

201 

202 Args: 

203 e: An extent. 

204 crs_from: Input crs. 

205 crs_to: Output crs. 

206 

207 Returns: 

208 The transformed extent. 

209 """ 

210 

211 return crs_from.transform_extent(e, crs_to) 

212 

213 

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

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

216 

217 Args: 

218 e: An extent. 

219 crs_to: Output crs. 

220 

221 Returns: 

222 The transformed extent. 

223 """ 

224 

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

226 

227 

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

229 """Transforms the extent to WGS84. 

230 

231 Args: 

232 e: An extent. 

233 crs_from: Input crs. 

234 

235 Returns: 

236 The WGS84 extent. 

237 """ 

238 

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

240 

241 

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

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

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

245 

246 

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

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

249 return False 

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

251 return False 

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

253 return False 

254 return True 

255 

256 

257def is_valid_wgs(e: gws.Extent, min_size: float = None) -> bool: 

258 if not is_valid(e): 

259 return False 

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

261 if e[0] < w[0] or e[1] < w[1] or e[2] > w[2] or e[3] > w[3]: 

262 return False 

263 # work around QGIS putting absurdly high or low values into extents 

264 if min_size is not None: 

265 dx = abs(e[2] - e[0]) 

266 dy = abs(e[3] - e[1]) 

267 if dx < min_size or dy < min_size: 

268 return False 

269 return True 

270 

271 

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

273 if len(ls) != 4: 

274 return None 

275 try: 

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

277 except ValueError: 

278 return None 

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

280 return None 

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

282 return None 

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