"""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[0, 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.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.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.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