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
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-03 10:12 +0100
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 'x-min,y-min,x-max,y-max' """
13 return _from_string_list(s.split(','))
16def from_list(ls: list) -> Optional[gws.Extent]:
17 """Create an extent from a list ``[x-min,y-min,x-max,y-max]``."""
19 return _from_string_list(ls)
22def from_points(a: gws.Point, b: gws.Point) -> gws.Extent:
23 """Create an extent from two points."""
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 )
33def from_center(xy: gws.Point, size: gws.Size) -> gws.Extent:
34 """Create an extent with certain size from a center-point."""
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 )
44def from_box(box: str) -> Optional[gws.Extent]:
45 """Create an extent from a Postgis BOX(minx miny,maxx maxy).
47 Args:
48 box: Postgis BOX.
50 Returns:
51 An extent."""
53 if not box:
54 return None
56 m = re.match(r'^BOX\((.+?)\)$', str(box).upper())
57 if not m:
58 return None
60 return _from_string_list(m.group(1).replace(',', ' ').split(' '))
63#
66def intersection(exts: list[gws.Extent]) -> Optional[gws.Extent]:
67 """Creates an extent that is the intersection of all given extents.
69 Args:
70 exts: Extents.
72 Returns:
73 An extent.
74 """
76 if not exts:
77 return
79 res = (-math.inf, -math.inf, math.inf, math.inf)
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
93def center(e: gws.Extent) -> gws.Point:
94 """The center-point of the extent"""
96 return (
97 e[0] + (e[2] - e[0]) / 2,
98 e[1] + (e[3] - e[1]) / 2,
99 )
102def size(e: gws.Extent) -> gws.Size:
103 """The size of the extent ``(width,height)"""
105 return (
106 e[2] - e[0],
107 e[3] - e[1],
108 )
111def diagonal(e: gws.Extent) -> float:
112 """The length of the diagonal"""
114 return math.sqrt((e[2] - e[0]) ** 2 + (e[3] - e[1]) ** 2)
117def circumsquare(e: gws.Extent) -> gws.Extent:
118 """A circumscribed square of the extent."""
120 d = diagonal(e)
121 return from_center(center(e), (d, d))
124def buffer(e: gws.Extent, buf: int) -> gws.Extent:
125 """Creates an extent with buffer to another extent.
127 Args:
128 e: An extent.
129 buf: Buffer between e and the output. If buf is positive the returned extent will be bigger.
131 Returns:
132 An extent.
133 """
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 )
145def union(exts: list[gws.Extent]) -> gws.Extent:
146 """Creates the smallest extent that contains all the given extents.
148 Args:
149 exts: Extents.
151 Returns:
152 An Extent.
153 """
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
166def intersect(a: gws.Extent, b: gws.Extent) -> bool:
167 """Returns ``True`` if the extents are intersecting, otherwise ``False``."""
169 return a[0] <= b[2] and a[2] >= b[0] and a[1] <= b[3] and a[3] >= b[1]
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.
175 Args:
176 e: An extent.
177 crs_from: Input crs.
178 crs_to: Output crs.
180 Returns:
181 The transformed extent.
182 """
184 return crs_from.transform_extent(e, crs_to)
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.
190 Args:
191 e: An extent.
192 crs_to: Output crs.
194 Returns:
195 The transformed extent.
196 """
198 return gws.lib.crs.WGS84.transform_extent(e, crs_to)
201def transform_to_wgs(e: gws.Extent, crs_from: gws.Crs) -> gws.Extent:
202 """Transforms the extent to WGS84.
204 Args:
205 e: An extent.
206 crs_from: Input crs.
208 Returns:
209 The WGS84 extent.
210 """
212 return crs_from.transform_extent(e, gws.lib.crs.WGS84)
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]
220def is_valid(e: gws.Extent) -> bool:
221 """Checks if the extent is valid."""
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
232def is_valid_wgs(e: gws.Extent) -> bool:
233 """Checks if the extent is valid and within WGS84 bounds."""
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]
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]