Skip to content

Commit 77dbdd2

Browse files
added functions and tests
1 parent f198a6a commit 77dbdd2

File tree

2 files changed

+155
-0
lines changed

2 files changed

+155
-0
lines changed

climada/util/coordinates.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1687,6 +1687,104 @@ def _ensure_utf8(val):
16871687
return admin1_info, admin1_shapes
16881688

16891689

1690+
def boundsNESW_from_global():
1691+
"""
1692+
Return global NESW bounds in EPSG 4326
1693+
1694+
Returns
1695+
-------
1696+
list:
1697+
The calculated bounding box as [north, east, south, west] in EPSG 4326
1698+
"""
1699+
return [90, 180, -90, -180]
1700+
1701+
1702+
def boundsNESW_from_country_codes(country_codes, rel_margin=0.2):
1703+
"""
1704+
Return NESW bounds in EPSG 4326 for the combined area defined by given country ISO codes.
1705+
1706+
Parameters
1707+
----------
1708+
country_codes : list
1709+
A list of ISO country codes (e.g.,['ITA'], ['ITA', 'CHE']).
1710+
rel_margin : float
1711+
A relative margin to extend the bounding box in all directions. Default is 0.2.
1712+
1713+
Returns
1714+
-------
1715+
list:
1716+
The calculated bounding box as [north, east, south, west] in EPSG 4326
1717+
"""
1718+
[north, east, south, west] = [-90, -180, 90, 180]
1719+
1720+
# loop through ISO codes
1721+
for iso in country_codes:
1722+
geo = get_country_geometries(iso).to_crs(epsg=4326)
1723+
iso_west, iso_south, iso_east, iso_north = geo.total_bounds
1724+
if np.any(np.isnan([iso_west, iso_south, iso_east, iso_north])):
1725+
LOGGER.warning(
1726+
f"ISO code '{iso}' not recognized. This region will not be included."
1727+
)
1728+
continue
1729+
1730+
north = max(north, iso_north)
1731+
east = max(east, iso_east)
1732+
south = min(south, iso_south)
1733+
west = min(west, iso_west)
1734+
1735+
# no countries recognized
1736+
if [north, east, south, west] == [-90, -180, 90, 180]:
1737+
raise Exception("No ISO code was recognized.")
1738+
1739+
# add relative margin
1740+
lat_margin = rel_margin * (north - south)
1741+
lon_margin = rel_margin * (east - west)
1742+
north = min(north + lat_margin, 90)
1743+
east = min(east + lon_margin, 180)
1744+
south = max(south - lat_margin, -90)
1745+
west = max(west - lon_margin, -180)
1746+
1747+
return [north, east, south, west]
1748+
1749+
1750+
def boundsNESW_from_NESW(*, north, east, south, west, rel_margin=0.0):
1751+
"""
1752+
Return NESW bounds in EPSG 4326 with relative margin from given NESW values in EPSG 4326.
1753+
1754+
Parameters
1755+
----------
1756+
north : (float, int)
1757+
Maximal latitude in EPSG 4326.
1758+
east : (float, int)
1759+
Maximal longitute in EPSG 4326.
1760+
south : (float, int)
1761+
Minimal latitude in EPSG 4326.
1762+
west : (float, int)
1763+
Minimal longitude in EPSG 4326.
1764+
rel_margin : float
1765+
A relative margin to extend the bounding box in all directions. Default is 0.2.
1766+
1767+
Returns
1768+
-------
1769+
list:
1770+
The calculated bounding box as [north, east, south, west] in EPSG 4326
1771+
"""
1772+
1773+
# simple bounds check
1774+
if not ((90 >= north > south >= -90) and (180 >= east > west >= -180)):
1775+
raise ValueError("Given bounds are not in standard order or standard bounds")
1776+
1777+
# add relative margin
1778+
lat_margin = rel_margin * (north - south)
1779+
lon_margin = rel_margin * (east - west)
1780+
north = min(north + lat_margin, 90)
1781+
east = min(east + lon_margin, 180)
1782+
south = max(south - lat_margin, -90)
1783+
west = max(west - lon_margin, -180)
1784+
1785+
return [north, east, south, west]
1786+
1787+
16901788
def get_admin1_geometries(countries):
16911789
"""
16921790
return geometries, names and codes of admin 1 regions in given countries

climada/util/test/test_coordinates.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2294,6 +2294,62 @@ def test_mask_raster_with_geometry(self):
22942294
)
22952295

22962296

2297+
class TestBoundsFromUserInput(unittest.TestCase):
2298+
"""Unit tests for the bounds_from_user_input function."""
2299+
2300+
def test_boundsNESW_from_global(self):
2301+
"""Test for 'global' area selection."""
2302+
result = u_coord.boundsNESW_from_global()
2303+
expected = [90, 180, -90, -180]
2304+
np.testing.assert_almost_equal(result, expected)
2305+
2306+
def test_boundsNESW_from_country_codes(self):
2307+
"""Test for a list of ISO country codes."""
2308+
result = u_coord.boundsNESW_from_country_codes(
2309+
["ITA"], rel_margin=0.2
2310+
) # Testing with Italy (ITA)
2311+
# Real expected bounds for Italy (calculated or manually known)
2312+
expected = [
2313+
49.404409157600064,
2314+
20.900365510000075,
2315+
33.170049669400036,
2316+
4.219788779000066,
2317+
] # Italy's bounding box
2318+
2319+
np.testing.assert_array_almost_equal(result, expected, decimal=4)
2320+
2321+
def test_bounding_box(self):
2322+
"""Test for bounding box input with margin applied."""
2323+
[north, east, south, west] = [50, -100, 30, -120]
2324+
result = u_coord.boundsNESW_from_NESW(
2325+
north=north, south=south, west=west, east=east, rel_margin=0.1
2326+
)
2327+
expected = [
2328+
50 + 2,
2329+
-100 + 2,
2330+
30 - 2,
2331+
-120 - 2,
2332+
] # Apply margin calculation
2333+
np.testing.assert_array_almost_equal(result, expected)
2334+
2335+
def test_invalid_input_string(self):
2336+
"""Test for invalid string input."""
2337+
with self.assertRaises(Exception):
2338+
u_coord.boundsNESW_from_country_codes("DEU")
2339+
2340+
def test_empty_input(self):
2341+
"""Test for empty input."""
2342+
with self.assertRaises(Exception):
2343+
u_coord.boundsNESW_from_country_codes([])
2344+
2345+
def test_invalid_coordinate_input(self):
2346+
"""Test for str in coordinates input input."""
2347+
with self.assertRaises(ValueError):
2348+
u_coord.boundsNESW_from_NESW(north=40, south=50, east=30, west=10)
2349+
with self.assertRaises(TypeError):
2350+
u_coord.boundsNESW_from_NESW(north=40, south="20", east=30, west=10)
2351+
2352+
22972353
# Execute Tests
22982354
if __name__ == "__main__":
22992355
TESTS = unittest.TestLoader().loadTestsFromTestCase(TestFunc)
@@ -2302,4 +2358,5 @@ def test_mask_raster_with_geometry(self):
23022358
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestRasterMeta))
23032359
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestRasterIO))
23042360
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestDistance))
2361+
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestBoundsFromUserInput))
23052362
unittest.TextTestRunner(verbosity=2).run(TESTS)

0 commit comments

Comments
 (0)