Skip to content

Commit 226e208

Browse files
authored
Merge pull request #24 from GeoscienceAustralia/feature/check_dem_bounds_utility
adding utility function for checking bounds in a dem
2 parents 19fd459 + 181995b commit 226e208

File tree

6 files changed

+266
-68
lines changed

6 files changed

+266
-68
lines changed

dem_handler/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,14 @@
11
from dem_handler._version import __version__
2+
from pathlib import Path
3+
import typing
4+
5+
DATA_DIR = Path(__file__).parent / Path("data")
6+
REMA_GPKG_PATH = DATA_DIR / Path("REMA_Mosaic_Index_v2.gpkg")
7+
COP30_GPKG_PATH = DATA_DIR / Path("copdem_tindex_filename.gpkg")
8+
9+
REMAResolutions = typing.Literal[2, 10, 32]
10+
COPResolutions = typing.Literal[30]
11+
ValidDEMResolutions = typing.Literal[REMAResolutions, COPResolutions]
12+
13+
REMA_VALID_RESOLUTIONS = typing.get_args(REMAResolutions)
14+
COP_VALID_RESOLUTIONS = typing.get_args(COPResolutions)

dem_handler/dem/cop_glo30.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@
3535
# Create a custom type that allows use of BoundingBox or tuple(xmin, ymin, xmax, ymax)
3636
BBox = BoundingBox | tuple[float | int, float | int, float | int, float | int]
3737

38-
DATA_DIR = Path(__file__).parents[1] / Path("data")
39-
COP30_GPKG_PATH = DATA_DIR / Path("copdem_tindex_filename.gpkg")
38+
from dem_handler import COP30_GPKG_PATH
4039

4140

4241
@log_timing

dem_handler/dem/rema.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,7 @@
2424
# Create a custom type that allows use of BoundingBox or tuple(xmin, ymin, xmax, ymax)
2525
BBox = BoundingBox | tuple[float | int, float | int, float | int, float | int]
2626

27-
DATA_DIR = Path(__file__).parents[1] / Path("data")
28-
REMA_GPKG_PATH = DATA_DIR / Path("REMA_Mosaic_Index_v2.gpkg")
29-
REMA_VALID_RESOLUTIONS = [
30-
2,
31-
10,
32-
32,
33-
] # [2, 10, 32, 100, 500, 1000] It seems there are no higher resolutions in the new index
27+
from dem_handler import REMA_GPKG_PATH, REMA_VALID_RESOLUTIONS, REMAResolutions
3428

3529

3630
@log_timing
@@ -39,7 +33,7 @@ def get_rema_dem_for_bounds(
3933
save_path: Path | str = "",
4034
rema_index_path: Path | str = REMA_GPKG_PATH,
4135
local_dem_dir: Path | str | None = None,
42-
resolution: int = 2,
36+
resolution: REMAResolutions = 2,
4337
bounds_src_crs: int = 3031,
4438
buffer_metres: int = 0,
4539
buffer_pixels: int = 0,

dem_handler/utils/spatial.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22
from dataclasses import dataclass
3+
import geopandas as gpd
34
import pyproj
45
from shapely import segmentize
56
from shapely.geometry import Polygon, box
@@ -20,6 +21,14 @@
2021

2122
logger = logging.getLogger(__name__)
2223

24+
from dem_handler import (
25+
REMA_GPKG_PATH,
26+
COP30_GPKG_PATH,
27+
REMA_VALID_RESOLUTIONS,
28+
ValidDEMResolutions,
29+
COP_VALID_RESOLUTIONS,
30+
)
31+
2332

2433
# Construct a dataclass for bounding boxes
2534
@dataclass
@@ -509,3 +518,72 @@ def crop_datasets_to_bounds(
509518
# )
510519

511520
return dem_array, dem_profile
521+
522+
523+
def check_dem_type_in_bounds(
524+
dem_type: str, resolution: ValidDEMResolutions, bounds: BBox
525+
) -> bool:
526+
"""Check if the specified dem has data within the provided bounds. The provided dem_type is matched to either the
527+
Copernicus Global 30m DEM or REMA DEM (currently implemented options). True is returned if the provided bounds
528+
intersect with any tiles of the specified DEM.
529+
530+
Parameters
531+
----------
532+
dem_type : str
533+
dem type to check. Can be variations of REMA and COP. e.g. REMA_32, REMA_10,
534+
cop30m, cop_30m, cop_glo30.
535+
resolution: ValidDEMResolutions
536+
resolution of the dem. Required to read the correct layer of the GPKG.
537+
bounds : BBox
538+
Bounds to check if data exists
539+
540+
Returns
541+
-------
542+
bool
543+
True if the bounds intersects a tile, False otherwise.
544+
545+
Raises
546+
-------
547+
ValueError
548+
If the provided dem_type cannot be matched to either the Copernicus 30m global DEM or the REMA dem.
549+
"""
550+
551+
if isinstance(bounds, BoundingBox):
552+
bounds = bounds.bounds
553+
554+
dem_type_match = dem_type.upper()
555+
if "COP" in dem_type_match and resolution in COP_VALID_RESOLUTIONS:
556+
dem_index_path = COP30_GPKG_PATH
557+
dem_type_formal = "Copernicus 30m global DEM"
558+
layer = None # only one layer
559+
elif "REMA" in dem_type_match and resolution in REMA_VALID_RESOLUTIONS:
560+
dem_index_path = REMA_GPKG_PATH
561+
dem_type_formal = "REMA DEM"
562+
layer = f"REMA_Mosaic_Index_v2_{resolution}m"
563+
else:
564+
raise ValueError(
565+
f"DEM type `{dem_type}` and resolution {resolution} could not be matched to either the Copernicus 30m global DEM or a valid REMA DEM with resolutions of {REMA_VALID_RESOLUTIONS}"
566+
)
567+
568+
logger.info(f"Checking if bounds intersect with tiles of the {dem_type_formal}")
569+
logger.info(f"Searching {COP30_GPKG_PATH}")
570+
571+
gdf = gpd.read_file(dem_index_path, layer=layer)
572+
bounding_box = shapely.geometry.box(*bounds)
573+
574+
if gdf.crs is not None:
575+
# ensure same crs
576+
bounding_box = (
577+
gpd.GeoSeries([bounding_box], crs="EPSG:4326").to_crs(gdf.crs).iloc[0]
578+
)
579+
else:
580+
logger.info('No crs found for index file. Assuming EPSG:4326"')
581+
bounding_box = gpd.GeoSeries([bounding_box], crs="EPSG:4326")
582+
# Find rows that intersect with the bounding box
583+
intersecting_tiles = gdf[gdf.intersects(bounding_box)]
584+
if len(intersecting_tiles) == 0:
585+
logger.info(f"No intersecting tiles found")
586+
return False
587+
else:
588+
logger.info(f"{len(intersecting_tiles)} intersecting tiles found")
589+
return True

tests/test_utils.py

Lines changed: 0 additions & 58 deletions
This file was deleted.

tests/utils/test_utils.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import pytest
2+
from dataclasses import dataclass
3+
from shapely.geometry import Polygon, MultiPolygon
4+
from dem_handler.utils.spatial import (
5+
BoundingBox,
6+
get_correct_bounds_from_shape_at_antimeridian,
7+
check_s1_bounds_cross_antimeridian,
8+
check_dem_type_in_bounds,
9+
)
10+
11+
# Separate lists for clarity
12+
poly1 = Polygon(
13+
[
14+
(178.576126, -71.618423),
15+
(-178.032867, -70.167343),
16+
(176.938004, -68.765106),
17+
(173.430893, -70.119957),
18+
(178.576126, -71.618423),
19+
],
20+
)
21+
poly2 = Polygon(
22+
[
23+
(179.5, -10.0), # Just west of the antimeridian
24+
(-179.8, 5.0), # Just east of the antimeridian
25+
(-178.5, 15.0), # Further east
26+
(178.0, 12.0), # West again
27+
(179.5, -10.0), # Close the ring
28+
]
29+
)
30+
poly3 = Polygon(
31+
[
32+
(170.0, -55.0), # Western point
33+
(-175.0, -50.0), # Just east of antimeridian
34+
(-160.0, -45.0), # Farther east
35+
(175.0, -40.0), # Back west
36+
(170.0, -55.0), # Close the loop
37+
]
38+
)
39+
40+
poly4 = MultiPolygon([poly1, poly2])
41+
42+
shapes = [poly1, poly2, poly3, poly4]
43+
44+
expected_bounds = [
45+
BoundingBox(-178.032867, -71.618423, 173.430893, -68.765106),
46+
BoundingBox(-178.5, -10, 178.0, 15),
47+
BoundingBox(-160, -55, 170, -40),
48+
BoundingBox(-178.032867, -71.618423, 173.430893, 15),
49+
]
50+
51+
# Combine for parameterization
52+
test_cases = list(zip(shapes, expected_bounds))
53+
54+
55+
@pytest.mark.parametrize("shape, expected_bound", test_cases)
56+
def test_get_correct_bounds_from_shape_at_antimeridian(shape, expected_bound):
57+
# assert the incorrect bounds still cross the AM
58+
assert check_s1_bounds_cross_antimeridian(shape.bounds)
59+
result = get_correct_bounds_from_shape_at_antimeridian(shape)
60+
assert result == expected_bound
61+
62+
63+
@dataclass
64+
class BoundsDEMCheckCase:
65+
dem_type: str
66+
resolution: int
67+
bounds: tuple[float, float, float, float]
68+
in_bounds: bool
69+
is_error: bool
70+
71+
72+
test_invalid_dem_type = BoundsDEMCheckCase(
73+
dem_type="spaghetti",
74+
resolution=90,
75+
bounds=(161.96252, -70.75924, 162.10388, -70.72293),
76+
in_bounds=False,
77+
is_error=True,
78+
)
79+
80+
test_invalid_dem_resolution = BoundsDEMCheckCase(
81+
dem_type="REMA",
82+
resolution=15,
83+
bounds=(161.96252, -70.75924, 162.10388, -70.72293),
84+
in_bounds=False,
85+
is_error=True,
86+
)
87+
88+
test_antarctic_bounds_with_rema = BoundsDEMCheckCase(
89+
dem_type="REMA",
90+
resolution=32,
91+
bounds=(161.96252, -70.75924, 162.10388, -70.72293),
92+
in_bounds=True,
93+
is_error=False,
94+
)
95+
96+
test_antarctic_bounds_with_cop30 = BoundsDEMCheckCase(
97+
dem_type="copernicus",
98+
resolution=30,
99+
bounds=(161.96252, -70.75924, 162.10388, -70.72293),
100+
in_bounds=True,
101+
is_error=False,
102+
)
103+
104+
test_australia_bounds_with_rema = BoundsDEMCheckCase(
105+
dem_type="REMA",
106+
resolution=10,
107+
bounds=(133.0, -25.0, 134.0, -24.0),
108+
in_bounds=False,
109+
is_error=False,
110+
)
111+
112+
test_heard_island_bounds_with_rema = BoundsDEMCheckCase(
113+
dem_type="rema_2",
114+
resolution=32,
115+
bounds=(73.2144, -53.224, 73.87, -52.962),
116+
in_bounds=False,
117+
is_error=False,
118+
)
119+
120+
test_heard_island_bounds_with_cop30 = BoundsDEMCheckCase(
121+
dem_type="cop_glo30",
122+
resolution=30,
123+
bounds=(73.2144, -53.224, 73.87, -52.962),
124+
in_bounds=True,
125+
is_error=False,
126+
)
127+
128+
test_antimeridian_with_cop30 = BoundsDEMCheckCase(
129+
dem_type="cop_glo30",
130+
resolution=30,
131+
bounds=(-178.032867, -80.618423, 173.430893, -68.765106),
132+
in_bounds=True,
133+
is_error=False,
134+
)
135+
136+
test_antimeridian_with_rema = BoundsDEMCheckCase(
137+
dem_type="rema",
138+
resolution=2,
139+
bounds=(-178.032867, -80.618423, 173.430893, -68.765106),
140+
in_bounds=True,
141+
is_error=False,
142+
)
143+
144+
test_cases = [
145+
test_invalid_dem_type,
146+
test_invalid_dem_resolution,
147+
test_antarctic_bounds_with_rema,
148+
test_antarctic_bounds_with_cop30,
149+
test_australia_bounds_with_rema,
150+
test_heard_island_bounds_with_rema,
151+
test_heard_island_bounds_with_cop30,
152+
test_antimeridian_with_cop30,
153+
test_antimeridian_with_rema,
154+
]
155+
156+
157+
@pytest.mark.parametrize("test_case", test_cases)
158+
def test_check_dem_type_in_bounds(test_case: BoundsDEMCheckCase):
159+
160+
if test_case.is_error:
161+
with pytest.raises(ValueError):
162+
check_dem_type_in_bounds(
163+
test_case.dem_type, test_case.resolution, test_case.bounds
164+
)
165+
166+
else:
167+
assert (
168+
check_dem_type_in_bounds(
169+
test_case.dem_type, test_case.resolution, test_case.bounds
170+
)
171+
== test_case.in_bounds
172+
)

0 commit comments

Comments
 (0)