fire2a.utils

👋🌎 Miscellaneous utility functions that simplify common tasks.

  1#!python3
  2"""👋🌎
  3Miscellaneous utility functions that simplify common tasks.
  4"""
  5__author__ = "Fernando Badilla"
  6__revision__ = "$Format:%H$"
  7import logging
  8import sys
  9from typing import Any, Union
 10
 11import numpy as np
 12from qgis.core import Qgis, QgsProcessingFeedback
 13
 14logger = logging.getLogger(__name__)
 15
 16
 17def read_toml(config_toml="config.toml"):
 18    if sys.version_info >= (3, 11):
 19        import tomllib
 20
 21        with open(config_toml, "rb") as f:
 22            config = tomllib.load(f)
 23    else:
 24        import toml
 25
 26        config = toml.load(config_toml)
 27    return config
 28
 29
 30def loadtxt_nodata(fname: str, no_data: int = -9999, dtype=np.float32, **kwargs) -> np.ndarray:
 31    """Load a text file into an array, casting safely to a specified data type, and replacing ValueError with a no_data value.
 32    Other arguments are passed to numpy.loadtxt. (delimiter=',' for example)
 33
 34    Args:
 35        fname : file, str, pathlib.Path, list of str, generator
 36            File, filename, list, or generator to read.  If the filename
 37            extension is ``.gz`` or ``.bz2``, the file is first decompressed. Note
 38            that generators must return bytes or strings. The strings
 39            in a list or produced by a generator are treated as lines.
 40        dtype : data-type, optional
 41            Data-type of the resulting array; default: float32.  If this is a
 42            structured data-type, the resulting array will be 1-dimensional, and
 43            each row will be interpreted as an element of the array.  In this
 44            case, the number of columns used must match the number of fields in
 45            the data-type.
 46        no_data (numeric, optional): No data value. Defaults to -9999.
 47        **kwargs: Other arguments are passed to numpy.loadtxt. (delimiter=',' for example)
 48
 49    Returns:
 50        out : numpy.ndarray: Data read from the text file.
 51
 52    See Also:
 53        numpy: loadtxt, load, fromstring, fromregex
 54    """
 55    from functools import partial
 56
 57    def conv(no_data, dtype, val):
 58        try:
 59            return dtype(val)
 60        except ValueError:
 61            return no_data
 62
 63    conv = partial(conv, no_data, dtype)
 64    return np.loadtxt(fname, converters=conv, dtype=dtype, **kwargs)
 65
 66
 67def qgis2numpy_dtype(qgis_dtype: Qgis.DataType) -> Union[np.dtype, None]:
 68    """Conver QGIS data type to corresponding numpy data type
 69    https://raw.githubusercontent.com/PUTvision/qgis-plugin-deepness/fbc99f02f7f065b2f6157da485bef589f611ea60/src/deepness/processing/processing_utils.py
 70    This is modified and extended copy of GDALDataType.
 71
 72    * ``UnknownDataType``: Unknown or unspecified type
 73    * ``Byte``: Eight bit unsigned integer (quint8)
 74    * ``Int8``: Eight bit signed integer (qint8) (added in QGIS 3.30)
 75    * ``UInt16``: Sixteen bit unsigned integer (quint16)
 76    * ``Int16``: Sixteen bit signed integer (qint16)
 77    * ``UInt32``: Thirty two bit unsigned integer (quint32)
 78    * ``Int32``: Thirty two bit signed integer (qint32)
 79    * ``Float32``: Thirty two bit floating point (float)
 80    * ``Float64``: Sixty four bit floating point (double)
 81    * ``CInt16``: Complex Int16
 82    * ``CInt32``: Complex Int32
 83    * ``CFloat32``: Complex Float32
 84    * ``CFloat64``: Complex Float64
 85    * ``ARGB32``: Color, alpha, red, green, blue, 4 bytes the same as QImage.Format_ARGB32
 86    * ``ARGB32_Premultiplied``: Color, alpha, red, green, blue, 4 bytes  the same as QImage.Format_ARGB32_Premultiplied
 87    """
 88    if qgis_dtype == Qgis.DataType.Byte or qgis_dtype == "Byte":
 89        return np.uint8
 90    if qgis_dtype == Qgis.DataType.UInt16 or qgis_dtype == "UInt16":
 91        return np.uint16
 92    if qgis_dtype == Qgis.DataType.Int16 or qgis_dtype == "Int16":
 93        return np.int16
 94    if qgis_dtype == Qgis.DataType.Float32 or qgis_dtype == "Float32":
 95        return np.float32
 96    if qgis_dtype == Qgis.DataType.Float64 or qgis_dtype == "Float64":
 97        return np.float64
 98    logger.error(f"QGIS data type {qgis_dtype} not matched to numpy data type.")
 99    return None
100
101
102def getGDALdrivers():
103    from osgeo import gdal  # isort: skip # fmt: skip
104    ret = []
105    for i in range(gdal.GetDriverCount()):
106        drv = {"ShortName": gdal.GetDriver(i).GetDescription()}
107        meta = gdal.GetDriver(i).GetMetadata()
108        assert "ShortName" not in meta
109        drv.update(meta)
110        ret += [drv]
111    return ret
112
113
114def getOGRdrivers():
115    from osgeo import ogr  # isort: skip # fmt: skip
116    ret = []
117    for i in range(ogr.GetDriverCount()):
118        drv = {"ShortName": ogr.GetDriver(i).GetDescription()}
119        meta = ogr.GetDriver(i).GetMetadata()
120        assert "ShortName" not in meta
121        drv.update(meta)
122        ret += [drv]
123    return ret
124
125
126def fprint(
127    *args, sep=" ", end="", level="warning", feedback: QgsProcessingFeedback = None, logger=None, **kwargs
128) -> None:
129    """replacement for print into logger and QgsProcessingFeedback
130    Args:
131        *args: positional arguments
132        sep (str, optional): separator between args. Defaults to " ".
133        end (str, optional): end of line. Defaults to "".
134        level (str, optional): logging level: debug, info, warning(default), error.
135        feedback (QgsProcessingFeedback, optional): QgsProcessingFeedback object. Defaults to None.
136        **kwargs: keyword arguments
137    """
138    if not logger:
139        logger = logging.getLogger(__name__)
140    msg = sep.join(map(str, args)) + sep
141    msg += sep.join([f"{k}={v}" for k, v in kwargs.items()]) + end
142    if level == "debug":
143        if feedback:
144            feedback.pushDebugInfo(msg)
145        else:
146            logger.debug(msg)
147    elif level == "info":
148        if feedback:
149            feedback.pushInfo(msg)
150        else:
151            logger.info(msg)
152    elif level == "warning":
153        if feedback:
154            feedback.pushWarning(msg)
155        else:
156            logger.warning(msg)
157    elif level == "error":
158        if feedback:
159            feedback.reportError(msg)
160        else:
161            logger.error(msg)
162
163
164def count_header_lines(file, sep=" ", feedback=None):
165    r"""Count header lines (e.g., in ASCII-Grid .asc files). The first line with a number is considered the end of the header section. Each line is split by the separator; empty lines are are skipped, staring with the separator is allowed (e.g., starting with a space).
166
167    When a number is found, the loop is broken and the number is returned. If no number is found, the loop continues until the end and returned
168
169    Common problem: Replace commas for periods in the file (if the file locale and python locale are different).
170    Unix:
171    ```bash
172    sed -i 's/,/./g' file.asc
173    ```
174    Windows-Powershell:
175    ```powershell
176    (Get-Content file.asc) -replace ',', '.' | Set-Content file.asc
177    ```
178
179    Args:
180    - file: str, path to the file
181    - sep: str, separator to split the line
182
183    Returns:
184    - header_count: int, number of header lines
185
186    Not Raises:
187    - ValueError: because the function expects to fail parsing to float
188    """
189    header_count = 0
190    found = None
191    with open(file, "r") as afile:
192        for line in afile:
193            split = line.split(sep, maxsplit=2)
194            if split == [""]:
195                continue
196            try:
197                if split[0] != "":
198                    found = float(split[0])
199                else:
200                    found = float(split[1])
201                break
202            except ValueError:
203                header_count += 1
204    if header_count == 0 or header_count > 6:
205        fprint(
206            f"Weird header count: {header_count} found! ({found}) Check {file} file. Maybe replace commas, for periods.?",
207            level="warning",
208            feedback=feedback,
209            logger=logger,
210        )
211    fprint(f"First number found: {found}", level="debug", feedback=feedback, logger=logger)
212    fprint(f"Number headers lines: {header_count}, in file {file}", level="info", feedback=feedback, logger=logger)
213    return header_count
logger = <Logger fire2a.utils (WARNING)>
def read_toml(config_toml='config.toml'):
18def read_toml(config_toml="config.toml"):
19    if sys.version_info >= (3, 11):
20        import tomllib
21
22        with open(config_toml, "rb") as f:
23            config = tomllib.load(f)
24    else:
25        import toml
26
27        config = toml.load(config_toml)
28    return config
def loadtxt_nodata( fname: str, no_data: int = -9999, dtype=<class 'numpy.float32'>, **kwargs) -> numpy.ndarray:
31def loadtxt_nodata(fname: str, no_data: int = -9999, dtype=np.float32, **kwargs) -> np.ndarray:
32    """Load a text file into an array, casting safely to a specified data type, and replacing ValueError with a no_data value.
33    Other arguments are passed to numpy.loadtxt. (delimiter=',' for example)
34
35    Args:
36        fname : file, str, pathlib.Path, list of str, generator
37            File, filename, list, or generator to read.  If the filename
38            extension is ``.gz`` or ``.bz2``, the file is first decompressed. Note
39            that generators must return bytes or strings. The strings
40            in a list or produced by a generator are treated as lines.
41        dtype : data-type, optional
42            Data-type of the resulting array; default: float32.  If this is a
43            structured data-type, the resulting array will be 1-dimensional, and
44            each row will be interpreted as an element of the array.  In this
45            case, the number of columns used must match the number of fields in
46            the data-type.
47        no_data (numeric, optional): No data value. Defaults to -9999.
48        **kwargs: Other arguments are passed to numpy.loadtxt. (delimiter=',' for example)
49
50    Returns:
51        out : numpy.ndarray: Data read from the text file.
52
53    See Also:
54        numpy: loadtxt, load, fromstring, fromregex
55    """
56    from functools import partial
57
58    def conv(no_data, dtype, val):
59        try:
60            return dtype(val)
61        except ValueError:
62            return no_data
63
64    conv = partial(conv, no_data, dtype)
65    return np.loadtxt(fname, converters=conv, dtype=dtype, **kwargs)

Load a text file into an array, casting safely to a specified data type, and replacing ValueError with a no_data value. Other arguments are passed to numpy.loadtxt. (delimiter=',' for example)

Args: fname : file, str, pathlib.Path, list of str, generator File, filename, list, or generator to read. If the filename extension is .gz or .bz2, the file is first decompressed. Note that generators must return bytes or strings. The strings in a list or produced by a generator are treated as lines. dtype : data-type, optional Data-type of the resulting array; default: float32. If this is a structured data-type, the resulting array will be 1-dimensional, and each row will be interpreted as an element of the array. In this case, the number of columns used must match the number of fields in the data-type. no_data (numeric, optional): No data value. Defaults to -9999. **kwargs: Other arguments are passed to numpy.loadtxt. (delimiter=',' for example)

Returns: out : numpy.ndarray: Data read from the text file.

See Also: numpy: loadtxt, load, fromstring, fromregex

def qgis2numpy_dtype(qgis_dtype: qgis._core.Qgis.DataType) -> Optional[numpy.dtype]:
 68def qgis2numpy_dtype(qgis_dtype: Qgis.DataType) -> Union[np.dtype, None]:
 69    """Conver QGIS data type to corresponding numpy data type
 70    https://raw.githubusercontent.com/PUTvision/qgis-plugin-deepness/fbc99f02f7f065b2f6157da485bef589f611ea60/src/deepness/processing/processing_utils.py
 71    This is modified and extended copy of GDALDataType.
 72
 73    * ``UnknownDataType``: Unknown or unspecified type
 74    * ``Byte``: Eight bit unsigned integer (quint8)
 75    * ``Int8``: Eight bit signed integer (qint8) (added in QGIS 3.30)
 76    * ``UInt16``: Sixteen bit unsigned integer (quint16)
 77    * ``Int16``: Sixteen bit signed integer (qint16)
 78    * ``UInt32``: Thirty two bit unsigned integer (quint32)
 79    * ``Int32``: Thirty two bit signed integer (qint32)
 80    * ``Float32``: Thirty two bit floating point (float)
 81    * ``Float64``: Sixty four bit floating point (double)
 82    * ``CInt16``: Complex Int16
 83    * ``CInt32``: Complex Int32
 84    * ``CFloat32``: Complex Float32
 85    * ``CFloat64``: Complex Float64
 86    * ``ARGB32``: Color, alpha, red, green, blue, 4 bytes the same as QImage.Format_ARGB32
 87    * ``ARGB32_Premultiplied``: Color, alpha, red, green, blue, 4 bytes  the same as QImage.Format_ARGB32_Premultiplied
 88    """
 89    if qgis_dtype == Qgis.DataType.Byte or qgis_dtype == "Byte":
 90        return np.uint8
 91    if qgis_dtype == Qgis.DataType.UInt16 or qgis_dtype == "UInt16":
 92        return np.uint16
 93    if qgis_dtype == Qgis.DataType.Int16 or qgis_dtype == "Int16":
 94        return np.int16
 95    if qgis_dtype == Qgis.DataType.Float32 or qgis_dtype == "Float32":
 96        return np.float32
 97    if qgis_dtype == Qgis.DataType.Float64 or qgis_dtype == "Float64":
 98        return np.float64
 99    logger.error(f"QGIS data type {qgis_dtype} not matched to numpy data type.")
100    return None

Conver QGIS data type to corresponding numpy data type https://raw.githubusercontent.com/PUTvision/qgis-plugin-deepness/fbc99f02f7f065b2f6157da485bef589f611ea60/src/deepness/processing/processing_utils.py This is modified and extended copy of GDALDataType.

  • UnknownDataType: Unknown or unspecified type
  • Byte: Eight bit unsigned integer (quint8)
  • Int8: Eight bit signed integer (qint8) (added in QGIS 3.30)
  • UInt16: Sixteen bit unsigned integer (quint16)
  • Int16: Sixteen bit signed integer (qint16)
  • UInt32: Thirty two bit unsigned integer (quint32)
  • Int32: Thirty two bit signed integer (qint32)
  • Float32: Thirty two bit floating point (float)
  • Float64: Sixty four bit floating point (double)
  • CInt16: Complex Int16
  • CInt32: Complex Int32
  • CFloat32: Complex Float32
  • CFloat64: Complex Float64
  • ARGB32: Color, alpha, red, green, blue, 4 bytes the same as QImage.Format_ARGB32
  • ARGB32_Premultiplied: Color, alpha, red, green, blue, 4 bytes the same as QImage.Format_ARGB32_Premultiplied
def getGDALdrivers():
103def getGDALdrivers():
104    from osgeo import gdal  # isort: skip # fmt: skip
105    ret = []
106    for i in range(gdal.GetDriverCount()):
107        drv = {"ShortName": gdal.GetDriver(i).GetDescription()}
108        meta = gdal.GetDriver(i).GetMetadata()
109        assert "ShortName" not in meta
110        drv.update(meta)
111        ret += [drv]
112    return ret
def getOGRdrivers():
115def getOGRdrivers():
116    from osgeo import ogr  # isort: skip # fmt: skip
117    ret = []
118    for i in range(ogr.GetDriverCount()):
119        drv = {"ShortName": ogr.GetDriver(i).GetDescription()}
120        meta = ogr.GetDriver(i).GetMetadata()
121        assert "ShortName" not in meta
122        drv.update(meta)
123        ret += [drv]
124    return ret
def fprint( *args, sep=' ', end='', level='warning', feedback: qgis._core.QgsProcessingFeedback = None, logger=None, **kwargs) -> None:
127def fprint(
128    *args, sep=" ", end="", level="warning", feedback: QgsProcessingFeedback = None, logger=None, **kwargs
129) -> None:
130    """replacement for print into logger and QgsProcessingFeedback
131    Args:
132        *args: positional arguments
133        sep (str, optional): separator between args. Defaults to " ".
134        end (str, optional): end of line. Defaults to "".
135        level (str, optional): logging level: debug, info, warning(default), error.
136        feedback (QgsProcessingFeedback, optional): QgsProcessingFeedback object. Defaults to None.
137        **kwargs: keyword arguments
138    """
139    if not logger:
140        logger = logging.getLogger(__name__)
141    msg = sep.join(map(str, args)) + sep
142    msg += sep.join([f"{k}={v}" for k, v in kwargs.items()]) + end
143    if level == "debug":
144        if feedback:
145            feedback.pushDebugInfo(msg)
146        else:
147            logger.debug(msg)
148    elif level == "info":
149        if feedback:
150            feedback.pushInfo(msg)
151        else:
152            logger.info(msg)
153    elif level == "warning":
154        if feedback:
155            feedback.pushWarning(msg)
156        else:
157            logger.warning(msg)
158    elif level == "error":
159        if feedback:
160            feedback.reportError(msg)
161        else:
162            logger.error(msg)

replacement for print into logger and QgsProcessingFeedback Args: args: positional arguments sep (str, optional): separator between args. Defaults to " ". end (str, optional): end of line. Defaults to "". level (str, optional): logging level: debug, info, warning(default), error. feedback (QgsProcessingFeedback, optional): QgsProcessingFeedback object. Defaults to None. *kwargs: keyword arguments

def count_header_lines(file, sep=' ', feedback=None):
165def count_header_lines(file, sep=" ", feedback=None):
166    r"""Count header lines (e.g., in ASCII-Grid .asc files). The first line with a number is considered the end of the header section. Each line is split by the separator; empty lines are are skipped, staring with the separator is allowed (e.g., starting with a space).
167
168    When a number is found, the loop is broken and the number is returned. If no number is found, the loop continues until the end and returned
169
170    Common problem: Replace commas for periods in the file (if the file locale and python locale are different).
171    Unix:
172    ```bash
173    sed -i 's/,/./g' file.asc
174    ```
175    Windows-Powershell:
176    ```powershell
177    (Get-Content file.asc) -replace ',', '.' | Set-Content file.asc
178    ```
179
180    Args:
181    - file: str, path to the file
182    - sep: str, separator to split the line
183
184    Returns:
185    - header_count: int, number of header lines
186
187    Not Raises:
188    - ValueError: because the function expects to fail parsing to float
189    """
190    header_count = 0
191    found = None
192    with open(file, "r") as afile:
193        for line in afile:
194            split = line.split(sep, maxsplit=2)
195            if split == [""]:
196                continue
197            try:
198                if split[0] != "":
199                    found = float(split[0])
200                else:
201                    found = float(split[1])
202                break
203            except ValueError:
204                header_count += 1
205    if header_count == 0 or header_count > 6:
206        fprint(
207            f"Weird header count: {header_count} found! ({found}) Check {file} file. Maybe replace commas, for periods.?",
208            level="warning",
209            feedback=feedback,
210            logger=logger,
211        )
212    fprint(f"First number found: {found}", level="debug", feedback=feedback, logger=logger)
213    fprint(f"Number headers lines: {header_count}, in file {file}", level="info", feedback=feedback, logger=logger)
214    return header_count

Count header lines (e.g., in ASCII-Grid .asc files). The first line with a number is considered the end of the header section. Each line is split by the separator; empty lines are are skipped, staring with the separator is allowed (e.g., starting with a space).

When a number is found, the loop is broken and the number is returned. If no number is found, the loop continues until the end and returned

Common problem: Replace commas for periods in the file (if the file locale and python locale are different). Unix:

sed -i 's/,/./g' file.asc

Windows-Powershell:

(Get-Content file.asc) -replace ',', '.' | Set-Content file.asc

Args:

  • file: str, path to the file
  • sep: str, separator to split the line

Returns:

  • header_count: int, number of header lines

Not Raises:

  • ValueError: because the function expects to fail parsing to float