Source code for bmi_topography.topography

"""Base class to access elevation data"""

import os
import urllib
import warnings
from pathlib import Path

import requests
import rioxarray
from rasterio.crs import CRS
from rasterio.errors import CRSError

from .api_key import ApiKey
from .bbox import BoundingBox


[docs] class Topography: """Fetch and cache land elevation data from OpenTopography.""" SCHEME = "https" NETLOC = "portal.opentopography.org" PATH = "/API/globaldem" DEFAULT = { "dem_type": "SRTMGL3", "south": 36.738884, "north": 38.091337, "west": -120.168457, "east": -118.465576, "output_format": "GTiff", "cache_dir": "~/.bmi_topography", } VALID_DEM_TYPES = ( "SRTMGL3", "SRTMGL1", "SRTMGL1_E", "AW3D30", "AW3D30_E", "SRTM15Plus", "NASADEM", "COP30", "COP90", ) VALID_OUTPUT_FORMATS = {"GTiff": "tif", "AAIGrid": "asc", "HFA": "img"} def __init__( self, dem_type=None, south=None, north=None, west=None, east=None, output_format=None, cache_dir=None, api_key=None, ): self._api_key = ApiKey.from_sources(api_key) # if api_key is None: # self._api_key = find_user_api_key() or use_demo_key() # else: # self._api_key = api_key if dem_type in Topography.VALID_DEM_TYPES: self._dem_type = dem_type else: raise ValueError( "dem_type must be one of %s." % (Topography.VALID_DEM_TYPES,) ) if output_format in Topography.VALID_OUTPUT_FORMATS.keys(): self._output_format = output_format self._file_extension = Topography.VALID_OUTPUT_FORMATS[output_format] else: raise ValueError( "output_format must be one of %s." % [k for k in Topography.VALID_OUTPUT_FORMATS.keys()] ) self._bbox = BoundingBox((south, west), (north, east)) self._da = None if cache_dir is None: cache_dir = Path(Topography.DEFAULT["cache_dir"]) self._cache_dir = Path(cache_dir).expanduser().resolve().absolute() @property def dem_type(self): return str(self._dem_type) @property def output_format(self): return str(self._output_format) @property def file_extension(self): return str(self._file_extension) @property def bbox(self): return self._bbox @property def cache_dir(self): return self._cache_dir
[docs] @staticmethod def data_url(): return urllib.parse.urlunparse( (Topography.SCHEME, Topography.NETLOC, Topography.PATH, "", "", "") )
[docs] def fetch(self): """Download and locally store topography data. Returns: pathlib.Path: The path to the downloaded file """ fname = Path( self.cache_dir ) / "{dem_type}_{south}_{west}_{north}_{east}.{ext}".format( dem_type=self.dem_type, south=self.bbox.south, north=self.bbox.north, west=self.bbox.west, east=self.bbox.east, ext=self.file_extension, ) if not fname.is_file(): self.cache_dir.mkdir(exist_ok=True) params = { "demtype": self.dem_type, "south": self.bbox.south, "north": self.bbox.north, "west": self.bbox.west, "east": self.bbox.east, "outputFormat": self.output_format, } if self._api_key: params["API_Key"] = str(self._api_key) response = requests.get(Topography.data_url(), params=params, stream=True) if response.status_code == 401: if self._api_key.source == "demo": msg = os.linesep.join( "It looks like you are using a demo key. This error may be the result of " "you reaching your maximum number of downloads." ) else: msg = ( "It looks like you are using a user-supplied key. This error may mean " "that your key is out of date or there is a typo in the supplied key. " f"(source={self._api_key.source})" ) response.reason = os.linesep.join([response.reason, "", msg, ""]) response.raise_for_status() with fname.open("wb") as fp: for chunk in response.iter_content(chunk_size=None): fp.write(chunk) return fname.absolute()
@property def da(self): return self._da
[docs] def load(self): """Load a cached topography data file into an xarray DataArray. Returns: xarray.DataArray: A container for the data """ if self._da is None: self._da = rioxarray.open_rasterio(self.fetch()) self._da.name = self.dem_type self._da.attrs["units"] = "unknown" try: crs = CRS.from_wkt(self._da.spatial_ref.crs_wkt) except (AttributeError, CRSError): warnings.warn( "A CRS cannot be identified for these data. " "Grid units will be set to 'unknown'." ) else: if crs.is_geographic: self._da.attrs["units"] = "degrees" else: self._da.attrs["units"] = crs.linear_units return self._da