diff --git a/examples/test_load_dem.ipynb b/examples/test_load_dem.ipynb new file mode 100644 index 0000000..a77807c --- /dev/null +++ b/examples/test_load_dem.ipynb @@ -0,0 +1,72 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import topotoolbox as topo" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(topo.get_dem_names())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dem = topo.load_dem(\"taiwan\")\n", + "dem.info()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(topo.get_cache_contents())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "topo.clear_cache()\n", + "print(topo.get_cache_contents())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/topotoolbox/__init__.py b/src/topotoolbox/__init__.py index 67fab60..f6cf69b 100644 --- a/src/topotoolbox/__init__.py +++ b/src/topotoolbox/__init__.py @@ -1 +1,2 @@ from .grid_object import GridObject +from .utils import * diff --git a/src/topotoolbox/grid_object.py b/src/topotoolbox/grid_object.py index 6cb1cab..e2b328b 100644 --- a/src/topotoolbox/grid_object.py +++ b/src/topotoolbox/grid_object.py @@ -1,15 +1,15 @@ """This module contains the GridObject class. """ + import random from typing import Union import numpy as np import rasterio -from .gridmixins.fillsinks import FillsinksMixin -from .gridmixins.info import InfoMixin -from .gridmixins.magic import MagicMixin -from .gridmixins.identifyflats import IdentifyflatsMixin +from .gridmixins import * # pylint: disable=W0401 + +__all__ = ['GridObject'] class GridObject( @@ -19,23 +19,21 @@ class GridObject( MagicMixin ): """A class containing all information of a Digital Elevation Model (DEM). - This class combines mixins to provide various functionalities for working with DEMs. - - Args: - InfoMixin: A mixin class providing methods to retrieve information about the DEM. - FillsinksMixin: A mixin class providing a method to fill sinks in the DEM. - MagicMixin: A mixin class providing magical methods for the DEM. + This class combines mixins to provide various functionalities + for working with DEMs. """ def __init__(self, path: Union[str, None] = None) -> None: """Initialize a GridObject instance. Args: - path (str, optional): The path to the raster file. Defaults to None. + path (str, optional): The path to the raster file. + Defaults to None. Raises: TypeError: If an invalid type is passed as the `path`. - ValueError: If an error occurs while processing the `path` argument. + ValueError: If an error occurs while processing + the `path` argument. """ if path is not None: @@ -66,7 +64,8 @@ def __init__(self, path: Union[str, None] = None) -> None: def gen_random( cls, hillsize: int = 24, rows: int = 128, columns: int = 128, cellsize: float = 10.0) -> 'GridObject': - """Generate a GridObject instance that is generated with OpenSimplex noise. + """Generate a GridObject instance that is generated with + OpenSimplex noise. Args: hillsize (int, optional): Controls the "smoothness" of the @@ -80,16 +79,17 @@ def gen_random( ImportError: If OpenSimplex has not been installed. Returns: - GridObject: An instance of GridObject with randomly generated values. + GridObject: An instance of GridObject with randomly + generated values. """ try: - import opensimplex as simplex + import opensimplex as simplex # pylint: disable=C0415 except ImportError: - raise ImportError( - """For gen_random to work, use \"pip install topotoolbox[opensimplex]\" - or \"pip install .[opensimplex]\"""") from None + 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) for y in range(0, rows): @@ -116,14 +116,16 @@ def gen_empty(cls) -> None: @classmethod def gen_random_bool( - cls, rows: int = 32, columns: int = 32, cellsize: float = 10.0) -> 'GridObject': - """Generate a GridObject instance that caontains only randomly + cls, rows: int = 32, columns: int = 32, cellsize: float = 10.0 + ) -> 'GridObject': + """Generate a GridObject instance that contains only randomly generated Boolean values. Args: 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. + cellsize (float, optional): size of each cell in the grid. + Defaults to 10. Returns: GridObject: _description_ diff --git a/src/topotoolbox/gridmixins/__init__.py b/src/topotoolbox/gridmixins/__init__.py index e69de29..32ddd41 100644 --- a/src/topotoolbox/gridmixins/__init__.py +++ b/src/topotoolbox/gridmixins/__init__.py @@ -0,0 +1,6 @@ +from .fillsinks import FillsinksMixin +from .info import InfoMixin +from .magic import MagicMixin +from .identifyflats import IdentifyflatsMixin + +__all__ = ['FillsinksMixin', 'InfoMixin', 'MagicMixin', 'IdentifyflatsMixin'] diff --git a/src/topotoolbox/gridmixins/identifyflats.py b/src/topotoolbox/gridmixins/identifyflats.py index 190d4c1..98a88fe 100644 --- a/src/topotoolbox/gridmixins/identifyflats.py +++ b/src/topotoolbox/gridmixins/identifyflats.py @@ -1,3 +1,5 @@ +"""This module contains the Mixin class IdentifyflatsMixin for the GridObject. +""" import copy import numpy as np @@ -6,24 +8,32 @@ class IdentifyflatsMixin(): + """Mixin class containing Identifyflats. + """ - def identifyflats(self, raw=False, output=['sills', 'flats']) -> tuple: + def identifyflats( + self, raw: bool = False, output: list[str] = None) -> tuple: """ Identifies flats and sills in a digital elevation model (DEM). Args: raw (bool): If True, returns the raw output grid as np.ndarray. - Defaults to False. + Defaults to False. output (list): List of strings indicating desired output types. - Possible values are 'sills', 'flats'. Defaults to ['sills', 'flats']. + Possible values are 'sills', 'flats'. + Defaults to ['sills', 'flats']. Returns: - tuple: A tuple containing copies of the DEM with identified flats and/or sills. + tuple: A tuple containing copies of the DEM with identified + flats and/or sills. Note: - Flats are identified as 1s, sills as 2s and presills as 5s (since they are also flats) - in the output grid. Only relevant when using raw=True. + Flats are identified as 1s, sills as 2s and presills as 5s + (since they are also flats) in the output grid. + Only relevant when using raw=True. """ + if output is None: + output = ['sills', 'flats'] dem = self.z.astype(np.float32) output_grid = np.zeros_like(dem).astype(np.int32) diff --git a/src/topotoolbox/gridmixins/magic.py b/src/topotoolbox/gridmixins/magic.py index 8b532e6..e931d33 100644 --- a/src/topotoolbox/gridmixins/magic.py +++ b/src/topotoolbox/gridmixins/magic.py @@ -161,7 +161,8 @@ def __and__(self, other): or other.z[x][y] not in [0, 1]): raise ValueError( - "Invalid cell value. 'and' can only compare True (1) and False (0) values.") + "Invalid cell value. 'and' can only compare " + + "True (1) and False (0) values.") dem.z[x][y] = (int(self.z[x][y]) & int(other.z[x][y])) @@ -183,7 +184,8 @@ def __or__(self, other): or other.z[x][y] not in [0, 1]): raise ValueError( - "Invalid cell value. 'or' can only compare True (1) and False (0) values.") + "Invalid cell value. 'or' can only compare True (1)" + + " and False (0) values.") dem.z[x][y] = (int(self.z[x][y]) | int(other.z[x][y])) @@ -205,7 +207,8 @@ def __xor__(self, other): or other.z[x][y] not in [0, 1]): raise ValueError( - "Invalid cell value. 'xor' can only compare True (1) and False (0) values.") + "Invalid cell value. 'xor' can only compare True (1)" + + " and False (0) values.") dem.z[x][y] = (int(self.z[x][y]) ^ int(other.z[x][y])) diff --git a/src/topotoolbox/utils.py b/src/topotoolbox/utils.py new file mode 100644 index 0000000..bc2c75a --- /dev/null +++ b/src/topotoolbox/utils.py @@ -0,0 +1,139 @@ +"""Provide general utility functions for topotoolbox. +""" +import sys +import os +from shutil import rmtree +from urllib.request import urlopen, urlretrieve + +from .grid_object import GridObject + +__all__ = ["load_dem", "get_dem_names", "read_tif", + "get_cache_contents", "clear_cache"] + +DEM_SOURCE = "https://raw.githubusercontent.com/TopoToolbox/DEMs/master" +DEM_NAMES = f"{DEM_SOURCE}/dem_names.txt" + + +def read_tif(path: str) -> GridObject: + """Generate a new GridObject from a .tif file. + + Args: + path (str): path to .tif file. + + Returns: + GridObject: A new GridObject of the .tif file. + """ + return GridObject(path) + + +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() + + +def load_dem(dem: str, cache: bool = True) -> GridObject: + """Downloads DEM from TopoToolbox/DEMs repository. + Find possible names by using 'get_dem_names()' + + Args: + dem (str): Name of dem about 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 + + dem = GridObject(full_path) + + return dem + + +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') + 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 + + +def clear_cache(filename: str = None) -> None: + """Deletes the cache directory and it's contents. Can also delete a single + file when using the argument filename. To get the contents of your cache, + use 'get_cache_contents()' + + Args: + 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.") + + +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