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
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
1from typing import Optional
3import math
4import re
6import gws
7import gws.lib.crs
10def from_string(s: str) -> Optional[gws.Extent]:
11 """Create an extent from a comma-separated string "1000,2000,20000 40000".
13 Args:
14 s: ``"x-min,y-min,x-max,y-max"``
16 Returns:
17 An extent.
18 """
20 return _from_string_list(s.split(','))
23def from_list(ls: list) -> Optional[gws.Extent]:
24 """Create an extent from a list of values.
26 Args:
27 ls: ``[x-min,y-min,x-max,y-max]``
29 Returns:
30 An extent."""
32 return _from_string_list(ls)
35def from_points(a: gws.Point, b: gws.Point) -> gws.Extent:
36 """Create an extent from two points.
38 Args:
39 a:``(x-min,y-min)``
40 b:``(x-max,y-max)``
42 Returns:
43 An extent."""
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 )
53def from_center(xy: gws.Point, size: gws.Size) -> gws.Extent:
54 """Create an extent with certain size from a center-point.
56 Args:
57 xy: Center-point ``(x,y)``
58 size: Extent's size.
60 Returns:
61 An Extent."""
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 )
71def from_box(box: str) -> Optional[gws.Extent]:
72 """Create an extent from a Postgis BOX(1000 2000,20000 40000).
74 Args:
75 box: Postgis BOX.
77 Returns:
78 An extent."""
80 if not box:
81 return None
83 m = re.match(r'^BOX\((.+?)\)$', str(box).upper())
84 if not m:
85 return None
87 return _from_string_list(m.group(1).replace(',', ' ').split(' '))
90#
93def intersection(exts: list[gws.Extent]) -> Optional[gws.Extent]:
94 """Creates an extent that is the intersection of all given extents.
96 Args:
97 exts: Extents.
99 Returns:
100 An extent.
101 """
103 if not exts:
104 return
106 res = (-math.inf, -math.inf, math.inf, math.inf)
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
120def center(e: gws.Extent) -> gws.Point:
121 """The center-point of the extent"""
123 return (
124 e[0] + (e[2] - e[0]) / 2,
125 e[1] + (e[3] - e[1]) / 2,
126 )
129def size(e: gws.Extent) -> gws.Size:
130 """The size of the extent ``(width,height)"""
132 return (
133 e[2] - e[0],
134 e[3] - e[1],
135 )
138def diagonal(e: gws.Extent) -> float:
139 """The length of the diagonal"""
141 return math.sqrt((e[2] - e[0]) ** 2 + (e[3] - e[1]) ** 2)
144def circumsquare(e: gws.Extent) -> gws.Extent:
145 """A circumscribed square of the extent."""
147 d = diagonal(e)
148 return from_center(center(e), (d, d))
151def buffer(e: gws.Extent, buf: int) -> gws.Extent:
152 """Creates an extent with buffer to another extent.
154 Args:
155 e: An extent.
156 buf: Buffer between e and the output. If buf is positive the returned extent will be bigger.
158 Returns:
159 An extent.
160 """
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 )
172def union(exts: list[gws.Extent]) -> gws.Extent:
173 """Creates the smallest extent that contains all the given extents.
175 Args:
176 exts: Extents.
178 Returns:
179 An Extent.
180 """
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
193def intersect(a: gws.Extent, b: gws.Extent) -> bool:
194 """Returns ``True`` if the extents are intersecting, otherwise ``False``."""
196 return a[0] <= b[2] and a[2] >= b[0] and a[1] <= b[3] and a[3] >= b[1]
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.
202 Args:
203 e: An extent.
204 crs_from: Input crs.
205 crs_to: Output crs.
207 Returns:
208 The transformed extent.
209 """
211 return crs_from.transform_extent(e, crs_to)
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.
217 Args:
218 e: An extent.
219 crs_to: Output crs.
221 Returns:
222 The transformed extent.
223 """
225 return gws.lib.crs.WGS84.transform_extent(e, crs_to)
228def transform_to_wgs(e: gws.Extent, crs_from: gws.Crs) -> gws.Extent:
229 """Transforms the extent to WGS84.
231 Args:
232 e: An extent.
233 crs_from: Input crs.
235 Returns:
236 The WGS84 extent.
237 """
239 return crs_from.transform_extent(e, gws.lib.crs.WGS84)
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]
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
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
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]