Source code for topotoolbox.utils

"""Provide general utility functions for topotoolbox.
"""
import sys
import os
import random
from shutil import rmtree
from urllib.request import urlopen, urlretrieve

import rasterio
import numpy as np
import matplotlib.pyplot as plt

from .grid_object import GridObject

__all__ = ["load_dem", "get_dem_names", "read_tif", "gen_random", "write_tif",
           "gen_random_bool", "get_cache_contents", "clear_cache", "show"]

DEM_SOURCE = "https://raw.githubusercontent.com/TopoToolbox/DEMs/master"
DEM_NAMES = f"{DEM_SOURCE}/dem_names.txt"


[docs] def write_tif(dem: GridObject, path: str) -> None: """ Write a GridObject instance to a GeoTIFF file. Parameters ---------- dem : GridObject The GridObject instance to be written to a GeoTIFF file. path : str The file path where the GeoTIFF will be saved. Raises ------ TypeError If `dem` is not an instance of GridObject. Examples -------- >>> dem = topotoolbox.load_dem('taiwan') >>> topotoolbox.write_tif(dem, 'dem.tif') """ if not isinstance(dem, GridObject): err = "The provided dem is not an instance of GridObject." raise TypeError(err) from None with rasterio.open( fp=path, mode='w', count=1, driver='GTiff', height=dem.rows, width=dem.columns, dtype=np.float32, crs=dem.crs, transform=dem.transform ) as dataset: dataset.write(dem.z, 1)
[docs] def show(*grid: GridObject, dpi: int = 100, cmap: str = 'terrain'): """ Display one or more GridObject instances using Matplotlib. Parameters ---------- *grid : GridObject One or more GridObject instances to be displayed. Each GridObject should have an attribute `name` and be suitable for use with `imshow`. dpi : int, optional The resolution of the plots in dots per inch. Default is 100. cmap : str, optional Matplotlib colormap that will be used in the plot. Notes ----- The function creates a subplot for each GridObject instance passed as an argument. Each subplot displays the grid using the 'terrain' colormap. A colorbar is added to each subplot. The title of each subplot is set to the `name` attribute of the respective GridObject. Examples -------- >>> dem1 = topotoolbox.load_dem('taiwan') >>> dem2 = topotoolbox.load_dem('perfectworld') >>> topotoolbox.show(dem1, dem2) """ num_grids = len(grid) fig, axes = plt.subplots(1, num_grids, figsize=(5*num_grids, 5), dpi=dpi, squeeze=False) for i, dem in enumerate(grid): ax = axes[i] im = ax.imshow(dem, cmap=cmap) ax.set_title(dem.name) fig.colorbar(im, ax=ax, orientation='vertical') plt.tight_layout() plt.show()
[docs] def read_tif(path: str) -> GridObject: """Generate a new GridObject from a .tif file. Parameters ---------- path : str path to .tif file. Returns ------- GridObject A new GridObject of the .tif file. """ grid = GridObject() if path is not None: try: dataset = rasterio.open(path) except TypeError as err: raise TypeError(err) from None except Exception as err: raise ValueError(err) from None grid.path = path grid.name = os.path.splitext(os.path.basename(grid.path))[0] grid.z = dataset.read(1).astype(np.float32, order='F') grid.rows = dataset.height grid.columns = dataset.width grid.shape = grid.z.shape grid.cellsize = dataset.res[0] grid.bounds = dataset.bounds grid.transform = dataset.transform grid.crs = dataset.crs return grid
[docs] def gen_random(hillsize: int = 24, rows: int = 128, columns: int = 128, cellsize: float = 10.0, seed: int = 3, name: str = 'random grid') -> 'GridObject': """Generate a GridObject instance that is generated with OpenSimplex noise. Parameters ---------- hillsize : int, optional Controls the "smoothness" of the generated terrain. Defaults to 24. rows : int, optional Number of rows. Defaults to 128. columns : int, optional Number of columns. Defaults to 128. cellsize : float, optional Size of each cell in the grid. Defaults to 10.0. seed : int, optional Seed for the terrain generation. Defaults to 3 name : str, optional Name for the generated GridObject. Defaults to 'random grid' Raises ------ ImportError If OpenSimplex has not been installed. Returns ------- GridObject An instance of GridObject with randomly generated values. """ try: import opensimplex as simplex # pylint: disable=C0415 except ImportError: err = ("For gen_random to work, use \"pip install topotool" + "box[opensimplex]\" or \"pip install .[opensimplex]\"") raise ImportError(err) from None noise_array = np.empty((rows, columns), dtype=np.float32, order='F') simplex.seed(seed) for y in range(0, rows): for x in range(0, columns): value = simplex.noise4(x / hillsize, y / hillsize, 0.0, 0.0) color = int((value + 1) * 128) noise_array[y, x] = color grid = GridObject() grid.z = noise_array grid.rows = rows grid.columns = columns grid.shape = grid.z.shape grid.cellsize = cellsize grid.name = name return grid
[docs] def gen_random_bool( rows: int = 32, columns: int = 32, cellsize: float = 10.0, name: str = 'random grid') -> 'GridObject': """Generate a GridObject instance that contains only randomly generated Boolean values. Parameters ---------- rows : int, optional Number of rows. Defaults to 32. columns : int, optional Number of columns. Defaults to 32. cellsize : float, optional Size of each cell in the grid. Defaults to 10.0. Returns ------- GridObject An instance of GridObject with randomly generated Boolean values. """ bool_array = np.empty((rows, columns), dtype=np.float32) for y in range(0, rows): for x in range(0, columns): bool_array[x][y] = random.choice([0, 1]) grid = GridObject() grid.path = '' grid.z = bool_array grid.rows = rows grid.columns = columns grid.shape = grid.z.shape grid.cellsize = cellsize grid.name = name return grid
[docs] def get_dem_names() -> list[str]: """Returns a list of provided example Digital Elevation Models (DEMs). Requires internet connection to download available names. Returns ------- list[str] A list of strings, where each string is the name of a DEM. """ with urlopen(DEM_NAMES) as dem_names: dem_names = dem_names.read().decode() return dem_names.splitlines()
[docs] def load_dem(dem: str, cache: bool = True) -> GridObject: """Downloads a DEM from the TopoToolbox/DEMs repository. Find possible names by using 'get_dem_names()'. Parameters ---------- dem : str Name of the DEM to be downloaded. cache : bool, optional If true, the DEM will be cached. Defaults to True. Returns ------- GridObject A GridObject generated from the downloaded DEM. """ if dem not in get_dem_names(): err = ("Selected DEM has to be selected from the provided examples." + " See which DEMs are available by using 'get_dem_names()'.") raise ValueError(err) url = f"{DEM_SOURCE}/{dem}.tif" if cache: cache_path = os.path.join(get_save_location(), f"{dem}.tif") if not os.path.exists(cache_path): urlretrieve(url, cache_path) full_path = cache_path else: full_path = url grid_object = read_tif(full_path) return grid_object
def get_save_location() -> str: """Generates filepath to file saved in cache. Returns ------- str Filepath to file saved in cache. """ system = sys.platform if system == "win32": path = os.getenv('LOCALAPPDATA') if path is None: raise EnvironmentError( "LOCALAPPDATA environment variable is not set." + " Unable to generate path to cache.") from None path = os.path.join(path, "topotoolbox") elif system == 'darwin': path = os.path.expanduser('~/Library/Caches') path = os.path.join(path, "topotoolbox") else: path = os.path.expanduser('~/.cache') path = os.path.join(path, "topotoolbox") if not os.path.exists(path): os.makedirs(path) return path
[docs] def clear_cache(filename: str | None = None) -> None: """Deletes the cache directory and its contents. Can also delete a single file when using the argument filename. To get the contents of your cache, use 'get_cache_contents()'. Parameters ---------- filename : str, optional Add a filename if only one specific file is to be deleted. Defaults to None. """ path = get_save_location() if filename: path = os.path.join(path, filename) if os.path.exists(path): if os.path.isdir(path): # using shutil.rmtree since os.rmdir requires dir to be empty. rmtree(path) else: os.remove(path) else: print("Cache directory or file does not exist.")
[docs] def get_cache_contents() -> (list[str] | None): """Returns the contents of the cache directory. Returns ------- list[str] List of all files in the TopoToolbox cache. If cache does not exist, None is returned. """ path = get_save_location() if os.path.exists(path): return os.listdir(path) print("Cache directory does not exist.") return None