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
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
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 typeByte
: 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 Int16CInt32
: Complex Int32CFloat32
: Complex Float32CFloat64
: Complex Float64ARGB32
: Color, alpha, red, green, blue, 4 bytes the same as QImage.Format_ARGB32ARGB32_Premultiplied
: Color, alpha, red, green, blue, 4 bytes the same as QImage.Format_ARGB32_Premultiplied
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
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
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
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