Skip to content

Commit c2e538d

Browse files
authored
Merge pull request #19 from quantifyearth/mwd-make-hybrid-elevation-map
Add scripts to help build the hybrid elevation map
2 parents ad4fdf8 + 3bba4a5 commit c2e538d

File tree

4 files changed

+185
-0
lines changed

4 files changed

+185
-0
lines changed

prepare_layers/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Preparing input rasters for STAR
2+
3+
## Elevation
4+
5+
STAR uses [FABDEM v1.2](https://data.bris.ac.uk/data/dataset/s5hqmjcdj8yo2ibzi9b4ew3sn) for its elevation layer, however this raster needs to be processed before it can be used:
6+
7+
* There are some errata tiles to correct mistakes in the area of Florida's Forgotten Coast. Unfortunately these are only available at time of writing via a [Google Drive link](https://drive.google.com/file/d/1DIAaheKT-thuWPhzXWpfVnTqRn13nqvi/view?usp=sharing).
8+
* FABDEM was created when certain tiles in the Copernicus GLO DEM were not available, leaving a gap around Azerbaijan and nearby countries.
9+
10+
FABDEM itself is very large and slow to download, and so we leave that as an exercise for the user rather than automating it as part of the pipeline. Once that has downloaded and the google drive link has been followed and the tiles expanded, we provide two scripts to complete the job:
11+
12+
* `fetch_cglo.py` - this will fetch the missing tiles from Copernicus GLO that are now available.
13+
* `make_hybrid_elevation_map.py` - this takes three inputs: a folder with the FABDEM v1.2 tiles, a folder with the errata files from Google Drive, and a folder with the additional CGLO tiles, and outputs the compiled hybrid elevation map. Note that this will be around 500 GB in size.

prepare_layers/fetch_cglo.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# STAR uses FABDEM, which is based on the Copernicus GLO 30 Digital Elevation Model (CGLO), but
2+
# was built at a time when CGLO was missing certain areas of the globe. This script downloads those
3+
# extra areas between 40.7371 to 50.9321 degrees longitude and 37.9296 to 45.7696 latitude.
4+
import argparse
5+
from pathlib import Path
6+
7+
import boto3
8+
from botocore import UNSIGNED
9+
from botocore.config import Config
10+
11+
def download_copernicus_dem_tiles(
12+
min_lon: float,
13+
max_lon: float,
14+
min_lat: float,
15+
max_lat: float,
16+
output_dir: Path,
17+
) -> tuple[list[str],list[str]]:
18+
output_dir.mkdir(parents=True, exist_ok=True)
19+
20+
s3 = boto3.client(
21+
's3',
22+
endpoint_url='https://opentopography.s3.sdsc.edu',
23+
config=Config(signature_version=UNSIGNED)
24+
)
25+
26+
bucket = 'raster'
27+
28+
lon_tiles = range(int(min_lon), int(max_lon) + 1)
29+
lat_tiles = range(int(min_lat), int(max_lat) + 1)
30+
31+
downloaded = []
32+
failed = []
33+
34+
for lat in lat_tiles:
35+
for lon in lon_tiles:
36+
lat_prefix = 'N' if lat >= 0 else 'S'
37+
lon_prefix = 'E' if lon >= 0 else 'W'
38+
lat_str = f"{abs(lat):02d}"
39+
lon_str = f"{abs(lon):03d}"
40+
41+
tile_name = f"Copernicus_DSM_10_{lat_prefix}{lat_str}_00_{lon_prefix}{lon_str}_00_DEM"
42+
s3_key = f"COP30/COP30_hh/{tile_name}.tif"
43+
local_path = f"{output_dir}/{tile_name}.tif"
44+
45+
try:
46+
print(f"Downloading {tile_name}.tif...")
47+
s3.download_file(bucket, s3_key, local_path)
48+
downloaded.append(tile_name)
49+
print(f" ✓ Saved to {local_path}")
50+
except Exception as e: # pylint: disable=W0718
51+
print(f" ✗ Failed: {e}")
52+
failed.append(tile_name)
53+
54+
print(f"\n{'='*60}")
55+
print(f"Downloaded: {len(downloaded)} tiles")
56+
if failed:
57+
print(f"Failed: {len(failed)} tiles")
58+
print("Failed tiles:", failed)
59+
60+
return downloaded, failed
61+
62+
def main() -> None:
63+
parser = argparse.ArgumentParser(description="Convert IUCN crosswalk to minimal common format.")
64+
parser.add_argument(
65+
'--output',
66+
type=Path,
67+
help='Destination folder for tiles',
68+
required=True,
69+
dest='output_dir',
70+
)
71+
args = parser.parse_args()
72+
73+
min_lon = 40.7371
74+
max_lon = 50.9321
75+
min_lat = 37.9296
76+
max_lat = 45.7696
77+
78+
download_copernicus_dem_tiles(min_lon, max_lon, min_lat, max_lat, args.output_dir)
79+
80+
if __name__ == "__main__":
81+
main()
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import argparse
2+
from pathlib import Path
3+
from alive_progress import alive_bar # type: ignore
4+
5+
import yirgacheffe as yg
6+
from yirgacheffe.layers import RescaledRasterLayer
7+
8+
def resize_cglo(
9+
projection: yg.MapProjection,
10+
cglo_path: Path,
11+
output_path: Path,
12+
) -> None:
13+
with yg.read_rasters(list(cglo_path.glob("*.tif"))) as cglo_30:
14+
rescaled = RescaledRasterLayer(cglo_30, projection, nearest_neighbour=False)
15+
with alive_bar(manual=True) as bar:
16+
rescaled.to_geotiff(output_path, parallelism=True, callback=bar)
17+
18+
def make_hybrid_elevation_map(
19+
fabdem_path: Path,
20+
fabdem_patch_path: Path,
21+
cglo_path: Path,
22+
output_path: Path,
23+
) -> None:
24+
output_path.parent.mkdir(parents=True, exist_ok=True)
25+
26+
tmpdir = output_path.parent
27+
28+
# The CGLO files are at a different resolution to FABDEM, so we first
29+
# need to scale them.
30+
resized_cglo_path = tmpdir / "cglo.tif"
31+
if not resized_cglo_path.exists():
32+
# Get the map projection and pixel scale for fabdem
33+
fabdem_example_tile = list(fabdem_path.glob("*.tif"))[0]
34+
with yg.read_raster(fabdem_example_tile) as example:
35+
projection = example.map_projection
36+
37+
resize_cglo(projection, cglo_path, resized_cglo_path)
38+
39+
# Now we build up a large group layer, and rely on the fact that
40+
# Yirgacheffe will render earlier layers over later layers
41+
file_list = list(fabdem_patch_path.glob("*.tif")) + \
42+
list(fabdem_path.glob("*.tif")) + \
43+
[resized_cglo_path]
44+
45+
full_layer = yg.read_rasters(file_list)
46+
47+
with alive_bar(manual=True) as bar:
48+
full_layer.to_geotiff(output_path, parallelism=256, callback=bar)
49+
50+
def main() -> None:
51+
parser = argparse.ArgumentParser(description="Convert IUCN crosswalk to minimal common format.")
52+
parser.add_argument(
53+
'--fabdem_tiles',
54+
type=Path,
55+
help="Directory containing original FABDEM tiles",
56+
required=True,
57+
dest="fabdem_path",
58+
)
59+
parser.add_argument(
60+
'--fabdem_patch_tiles',
61+
type=Path,
62+
help="Directory containing original FABDEM errata tiles",
63+
required=True,
64+
dest="fabdem_patch_path",
65+
)
66+
parser.add_argument(
67+
'--cglo_tiles',
68+
type=Path,
69+
help="Directory containing missing_cglo tiles",
70+
required=True,
71+
dest="cglo_path",
72+
)
73+
parser.add_argument(
74+
'--output',
75+
type=Path,
76+
help="Final output raster",
77+
required=True,
78+
dest='output_path',
79+
)
80+
args = parser.parse_args()
81+
82+
make_hybrid_elevation_map(
83+
args.fabdem_path,
84+
args.fabdem_patch_path,
85+
args.cglo_path,
86+
args.output_path,
87+
)
88+
89+
if __name__ == "__main__":
90+
main()

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pymer4
77
pyproj
88
scikit-image
99
redlistapi
10+
boto3
1011
yirgacheffe>=1.12,<2.0
1112
aoh[validation]>=2.0.1,<3.0
1213

0 commit comments

Comments
 (0)