Skip to content

Commit 2bfe421

Browse files
adapted functions and order of outputs
1 parent 77dbdd2 commit 2bfe421

File tree

2 files changed

+101
-102
lines changed

2 files changed

+101
-102
lines changed

climada/util/coordinates.py

Lines changed: 59 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -783,12 +783,22 @@ def get_country_geometries(
783783
if country_names:
784784
if isinstance(country_names, str):
785785
country_names = [country_names]
786+
787+
# print warning if ISO code not recognized
788+
for country_name in country_names:
789+
if not country_name in nat_earth[["ISO_A3", "WB_A3", "ADM0_A3"]].values:
790+
LOGGER.warning(f"ISO code {country_name} not recognized.")
791+
786792
country_mask = np.isin(
787793
nat_earth[["ISO_A3", "WB_A3", "ADM0_A3"]].values,
788794
country_names,
789795
).any(axis=1)
790796
out = out[country_mask]
791797

798+
# exit with Value error if no country code was recognized
799+
if out.size == 0:
800+
raise ValueError(f"None of the given country codes were regocnized.")
801+
792802
if extent:
793803
if extent[1] - extent[0] > 360:
794804
raise ValueError(
@@ -1687,102 +1697,84 @@ def _ensure_utf8(val):
16871697
return admin1_info, admin1_shapes
16881698

16891699

1690-
def boundsNESW_from_global():
1700+
def global_bounding_box():
16911701
"""
1692-
Return global NESW bounds in EPSG 4326
1702+
Return global bounds in EPSG 4326
16931703
16941704
Returns
16951705
-------
1696-
list:
1697-
The calculated bounding box as [north, east, south, west] in EPSG 4326
1706+
tuple:
1707+
The global bounding box as (min_lon, min_lat, max_lon, max_lat)
16981708
"""
1699-
return [90, 180, -90, -180]
1709+
return (-180, -90, 180, 90)
17001710

17011711

1702-
def boundsNESW_from_country_codes(country_codes, rel_margin=0.2):
1712+
def get_country_bounding_box(country_names, buffer=1.0):
17031713
"""
1704-
Return NESW bounds in EPSG 4326 for the combined area defined by given country ISO codes.
1714+
Return bounding box in EPSG 4326 containing given countries.
17051715
17061716
Parameters
17071717
----------
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.
1718+
country_names : list or str
1719+
list with ISO 3166 alpha-3 codes of countries, e.g ['ZWE', 'GBR', 'VNM', 'UZB']
1720+
buffer : float, optional
1721+
Buffer to add to both sides of the bounding box. Default: 1.0.
17121722
17131723
Returns
17141724
-------
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.")
1725+
tuple
1726+
The bounding box containing all given coutries as (min_lon, min_lat, max_lon, max_lat)
1727+
"""
17381728

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)
1729+
country_geometry = get_country_geometries(country_names).geometry
1730+
longitudes, latitudes = [], []
1731+
for multipolygon in country_geometry:
1732+
for polygon in multipolygon.geoms: # Loop through each polygon
1733+
for coord in polygon.exterior.coords: # Extract exterior coordinates
1734+
longitudes.append(coord[0])
1735+
latitudes.append(coord[1])
17461736

1747-
return [north, east, south, west]
1737+
return latlon_bounds(np.array(latitudes), np.array(longitudes), buffer=buffer)
17481738

17491739

1750-
def boundsNESW_from_NESW(*, north, east, south, west, rel_margin=0.0):
1740+
def bounds_from_cardinal_bounds(*, northern, eastern, western, southern):
17511741
"""
1752-
Return NESW bounds in EPSG 4326 with relative margin from given NESW values in EPSG 4326.
1742+
Return and normalize bounding box in EPSG 4326 from given cardinal bounds.
17531743
17541744
Parameters
17551745
----------
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.
1746+
northern : (int, float)
1747+
Northern boundary of bounding box
1748+
eastern : (int, float)
1749+
Eastern boundary of bounding box
1750+
western : (int, float)
1751+
Western boundary of bounding box
1752+
southern : (int, float)
1753+
Southern boundary of bounding box
17661754
17671755
Returns
17681756
-------
1769-
list:
1770-
The calculated bounding box as [north, east, south, west] in EPSG 4326
1757+
tuple
1758+
The resulting normalized bounding box (min_lon, min_lat, max_lon, max_lat) with -180 <= min_lon < max_lon < 540
1759+
17711760
"""
17721761

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")
1762+
# latitude bounds check
1763+
if not ((90 >= northern > southern >= -90)):
1764+
raise ValueError(
1765+
"Given northern bound is below given southern bound or out of bounds"
1766+
)
17761767

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)
1768+
if not (360 >= eastern >= -180) or not (360 >= western >= -180):
1769+
raise ValueError("Given eastern/western bounds are out of range (-180, 360).")
1770+
# order eastern and western coordinates
1771+
if western > eastern:
1772+
eastern += 360
1773+
if eastern > 360 and western > 180:
1774+
eastern -= 360
1775+
western -= 360
17841776

1785-
return [north, east, south, west]
1777+
return (western, southern, eastern, northern)
17861778

17871779

17881780
def get_admin1_geometries(countries):

climada/util/test/test_coordinates.py

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2297,57 +2297,64 @@ def test_mask_raster_with_geometry(self):
22972297
class TestBoundsFromUserInput(unittest.TestCase):
22982298
"""Unit tests for the bounds_from_user_input function."""
22992299

2300-
def test_boundsNESW_from_global(self):
2300+
def global_bounding_box(self):
23012301
"""Test for 'global' area selection."""
2302-
result = u_coord.boundsNESW_from_global()
2303-
expected = [90, 180, -90, -180]
2302+
result = u_coord.global_bounding_box()
2303+
expected = (-180, -90, 180, 90)
23042304
np.testing.assert_almost_equal(result, expected)
23052305

2306-
def test_boundsNESW_from_country_codes(self):
2306+
def test_get_country_bounding_box(self):
23072307
"""Test for a list of ISO country codes."""
2308-
result = u_coord.boundsNESW_from_country_codes(
2309-
["ITA"], rel_margin=0.2
2308+
result = u_coord.get_country_bounding_box(
2309+
["ITA"], buffer=1.0
23102310
) # Testing with Italy (ITA)
23112311
# Real expected bounds for Italy (calculated or manually known)
23122312
expected = [
2313-
49.404409157600064,
2314-
20.900365510000075,
2315-
33.170049669400036,
2316-
4.219788779000066,
2313+
5.6027283120000675,
2314+
34.48924388200004,
2315+
19.517425977000073,
2316+
48.08521494500006,
23172317
] # Italy's bounding box
23182318

23192319
np.testing.assert_array_almost_equal(result, expected, decimal=4)
23202320

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
2321+
def test_bounds_from_cardinal_bounds(self):
2322+
"""Test for conversion from cardinal bounds to bounds."""
2323+
np.testing.assert_array_almost_equal(
2324+
u_coord.bounds_from_cardinal_bounds(
2325+
northern=90, southern=-20, eastern=30, western=20
2326+
),
2327+
(20, -20, 30, 90),
2328+
)
2329+
np.testing.assert_array_almost_equal(
2330+
u_coord.bounds_from_cardinal_bounds(
2331+
northern=90, southern=-20, eastern=20, western=30
2332+
),
2333+
(30, -20, 380, 90),
2334+
)
2335+
np.testing.assert_array_almost_equal(
2336+
u_coord.bounds_from_cardinal_bounds(
2337+
northern=90, southern=-20, eastern=170, western=-170
2338+
),
2339+
(-170, -20, 170, 90),
2340+
)
2341+
np.testing.assert_array_almost_equal(
2342+
u_coord.bounds_from_cardinal_bounds(
2343+
northern=90, southern=-20, eastern=-170, western=170
2344+
),
2345+
(170, -20, 190, 90),
2346+
)
2347+
np.testing.assert_array_almost_equal(
2348+
u_coord.bounds_from_cardinal_bounds(
2349+
northern=90, southern=-20, eastern=170, western=175
2350+
),
2351+
(175, -20, 530, 90),
23262352
)
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)
23342353

23352354
def test_invalid_input_string(self):
23362355
"""Test for invalid string input."""
23372356
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)
2357+
u_coord.get_bound("invalid_ISO")
23512358

23522359

23532360
# Execute Tests

0 commit comments

Comments
 (0)