Coverage for gws-app/gws/gis/extent/__init__.py: 28%

92 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-17 01:37 +0200

1from typing import Optional 

2 

3import math 

4import re 

5 

6import gws 

7import gws.gis.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_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 _check(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 _check([a[0], a[1], b[0], b[1]]) 

46 

47 

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

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

50 

51 Args: 

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

53 size: Extent's size. 

54 

55 Returns: 

56 An Extent.""" 

57 

58 return ( 

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

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

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

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

63 ) 

64 

65 

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

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

68 

69 Args: 

70 box: Postgis BOX. 

71 

72 Returns: 

73 An extent.""" 

74 

75 if not box: 

76 return None 

77 

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

79 if not m: 

80 return None 

81 

82 a, b = m.group(1).split(',') 

83 c, d = a.split(), b.split() 

84 

85 return _check([c[0], c[1], d[0], d[1]]) 

86 

87 

88# 

89 

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

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

92 

93 Args: 

94 exts: Extents. 

95 

96 Returns: 

97 An extent. 

98 """ 

99 

100 if not exts: 

101 return 

102 

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

104 

105 for ext in exts: 

106 _sort(ext) 

107 if not intersect(res, ext): 

108 return 

109 res = ( 

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

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

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

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

114 ) 

115 return res 

116 

117 

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

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

120 

121 return ( 

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

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

124 ) 

125 

126 

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

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

129 

130 return ( 

131 e[2] - e[0], 

132 e[3] - e[1], 

133 ) 

134 

135 

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

137 """The length of the diagonal""" 

138 

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

140 

141 

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

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

144 

145 d = diagonal(e) 

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

147 

148 

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

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

151 

152 Args: 

153 e: An extent. 

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

155 

156 Returns: 

157 An extent. 

158 """ 

159 

160 if buf == 0: 

161 return e 

162 e = _sort(e) 

163 return ( 

164 e[0] - buf, 

165 e[1] - buf, 

166 e[2] + buf, 

167 e[3] + buf, 

168 ) 

169 

170 

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

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

173 

174 Args: 

175 exts: Extents. 

176 

177 Returns: 

178 An Extent. 

179 """ 

180 

181 ext = exts[0] 

182 for e in exts: 

183 e = _sort(e) 

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 a = _sort(a) 

197 b = _sort(b) 

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

199 

200 

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

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

203 

204 Args: 

205 e: An extent. 

206 crs_from: Input crs. 

207 crs_to: Output crs. 

208 

209 Returns: 

210 The transformed extent. 

211 """ 

212 

213 return crs_from.transform_extent(e, crs_to) 

214 

215 

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

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

218 

219 Args: 

220 e: An extent. 

221 crs_to: Output crs. 

222 

223 Returns: 

224 The transformed extent. 

225 """ 

226 

227 return gws.gis.crs.WGS84.transform_extent(e, crs_to) 

228 

229 

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

231 """Transforms the extent to WGS84. 

232 

233 Args: 

234 e: An extent. 

235 crs_from: Input crs. 

236 

237 Returns: 

238 The WGS84 extent. 

239 """ 

240 

241 return crs_from.transform_extent(e, gws.gis.crs.WGS84) 

242 

243 

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

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

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

247 

248 

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

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

251 return False 

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

253 return False 

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

255 return False 

256 return True 

257 

258 

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

260 if not is_valid(e): 

261 return False 

262 w = gws.gis.crs.WGS84.extent 

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

264 

265 

266def _check(ls: list) -> Optional[gws.Extent]: 

267 if len(ls) != 4: 

268 return None 

269 try: 

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

271 except ValueError: 

272 return None 

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

274 return None 

275 e = _sort(e) 

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

277 return None 

278 return e 

279 

280 

281def _sort(e): 

282 # our extents are always [minx, miny, maxx, maxy] 

283 return ( 

284 min(e[0], e[2]), 

285 min(e[1], e[3]), 

286 max(e[0], e[2]), 

287 max(e[1], e[3]), 

288 )