ssb-sgis 1.0.1__py3-none-any.whl → 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sgis/__init__.py +97 -115
- sgis/exceptions.py +3 -1
- sgis/geopandas_tools/__init__.py +1 -0
- sgis/geopandas_tools/bounds.py +75 -38
- sgis/geopandas_tools/buffer_dissolve_explode.py +38 -34
- sgis/geopandas_tools/centerlines.py +53 -44
- sgis/geopandas_tools/cleaning.py +87 -104
- sgis/geopandas_tools/conversion.py +149 -101
- sgis/geopandas_tools/duplicates.py +31 -17
- sgis/geopandas_tools/general.py +76 -48
- sgis/geopandas_tools/geometry_types.py +21 -7
- sgis/geopandas_tools/neighbors.py +20 -8
- sgis/geopandas_tools/overlay.py +136 -53
- sgis/geopandas_tools/point_operations.py +9 -8
- sgis/geopandas_tools/polygon_operations.py +48 -56
- sgis/geopandas_tools/polygons_as_rings.py +121 -78
- sgis/geopandas_tools/sfilter.py +14 -14
- sgis/helpers.py +114 -56
- sgis/io/dapla_functions.py +32 -23
- sgis/io/opener.py +13 -6
- sgis/io/read_parquet.py +1 -1
- sgis/maps/examine.py +39 -26
- sgis/maps/explore.py +112 -66
- sgis/maps/httpserver.py +12 -12
- sgis/maps/legend.py +124 -65
- sgis/maps/map.py +66 -41
- sgis/maps/maps.py +31 -29
- sgis/maps/thematicmap.py +46 -33
- sgis/maps/tilesources.py +3 -8
- sgis/networkanalysis/_get_route.py +5 -4
- sgis/networkanalysis/_od_cost_matrix.py +44 -1
- sgis/networkanalysis/_points.py +10 -4
- sgis/networkanalysis/_service_area.py +5 -2
- sgis/networkanalysis/closing_network_holes.py +20 -62
- sgis/networkanalysis/cutting_lines.py +55 -43
- sgis/networkanalysis/directednetwork.py +15 -7
- sgis/networkanalysis/finding_isolated_networks.py +4 -3
- sgis/networkanalysis/network.py +15 -13
- sgis/networkanalysis/networkanalysis.py +72 -54
- sgis/networkanalysis/networkanalysisrules.py +20 -16
- sgis/networkanalysis/nodes.py +2 -3
- sgis/networkanalysis/traveling_salesman.py +5 -2
- sgis/parallel/parallel.py +337 -127
- sgis/raster/__init__.py +6 -0
- sgis/raster/base.py +9 -3
- sgis/raster/cube.py +280 -208
- sgis/raster/cubebase.py +15 -29
- sgis/raster/indices.py +3 -7
- sgis/raster/methods_as_functions.py +0 -124
- sgis/raster/raster.py +313 -127
- sgis/raster/torchgeo.py +58 -37
- sgis/raster/zonal.py +38 -13
- {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.2.dist-info}/LICENSE +1 -1
- {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.2.dist-info}/METADATA +87 -16
- ssb_sgis-1.0.2.dist-info/RECORD +61 -0
- {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.2.dist-info}/WHEEL +1 -1
- sgis/raster/bands.py +0 -48
- sgis/raster/gradient.py +0 -78
- ssb_sgis-1.0.1.dist-info/RECORD +0 -63
sgis/raster/raster.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import numbers
|
|
3
|
+
import os
|
|
3
4
|
import re
|
|
4
5
|
import warnings
|
|
5
|
-
from collections.abc import Callable
|
|
6
|
-
from
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from copy import copy
|
|
10
|
+
from copy import deepcopy
|
|
7
11
|
from json import loads
|
|
8
12
|
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
from typing import ClassVar
|
|
9
15
|
|
|
10
16
|
import geopandas as gpd
|
|
11
17
|
import matplotlib.pyplot as plt
|
|
@@ -13,17 +19,25 @@ import numpy as np
|
|
|
13
19
|
import pandas as pd
|
|
14
20
|
import pyproj
|
|
15
21
|
import rasterio
|
|
22
|
+
import rasterio.windows
|
|
16
23
|
import shapely
|
|
17
24
|
from typing_extensions import Self # TODO: imperter fra typing når python 3.11
|
|
18
25
|
|
|
19
|
-
|
|
20
26
|
try:
|
|
21
27
|
import xarray as xr
|
|
22
28
|
from xarray import DataArray
|
|
23
29
|
except ImportError:
|
|
24
30
|
|
|
25
31
|
class DataArray:
|
|
26
|
-
|
|
32
|
+
"""Placeholder."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
from dapla.gcs import GCSFileSystem
|
|
37
|
+
except ImportError:
|
|
38
|
+
|
|
39
|
+
class GCSFileSystem:
|
|
40
|
+
"""Placeholder."""
|
|
27
41
|
|
|
28
42
|
|
|
29
43
|
try:
|
|
@@ -31,7 +45,8 @@ try:
|
|
|
31
45
|
except ImportError:
|
|
32
46
|
pass
|
|
33
47
|
from affine import Affine
|
|
34
|
-
from geopandas import GeoDataFrame
|
|
48
|
+
from geopandas import GeoDataFrame
|
|
49
|
+
from geopandas import GeoSeries
|
|
35
50
|
from pandas.api.types import is_list_like
|
|
36
51
|
from rasterio import features
|
|
37
52
|
from rasterio.enums import MergeAlg
|
|
@@ -39,22 +54,26 @@ from rasterio.io import DatasetReader
|
|
|
39
54
|
from rasterio.vrt import WarpedVRT
|
|
40
55
|
from rasterio.warp import reproject
|
|
41
56
|
from shapely import Geometry
|
|
42
|
-
from shapely.geometry import Point
|
|
43
|
-
|
|
44
|
-
from
|
|
45
|
-
|
|
57
|
+
from shapely.geometry import Point
|
|
58
|
+
from shapely.geometry import Polygon
|
|
59
|
+
from shapely.geometry import shape
|
|
60
|
+
|
|
61
|
+
from ..geopandas_tools.conversion import to_bbox
|
|
62
|
+
from ..geopandas_tools.conversion import to_gdf
|
|
63
|
+
from ..geopandas_tools.conversion import to_shapely
|
|
64
|
+
from ..geopandas_tools.general import is_bbox_like
|
|
65
|
+
from ..geopandas_tools.general import is_wkt
|
|
46
66
|
from ..helpers import is_property
|
|
47
67
|
from ..io.opener import opener
|
|
48
|
-
from .base import ALLOWED_KEYS
|
|
49
|
-
from .
|
|
50
|
-
from .
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
from .base import ALLOWED_KEYS
|
|
69
|
+
from .base import NESSECARY_META
|
|
70
|
+
from .base import get_index_mapper
|
|
71
|
+
from .base import memfile_from_array
|
|
72
|
+
from .zonal import _aggregate
|
|
73
|
+
from .zonal import _make_geometry_iterrows
|
|
74
|
+
from .zonal import _no_overlap_df
|
|
75
|
+
from .zonal import _prepare_zonal
|
|
76
|
+
from .zonal import _zonal_post
|
|
58
77
|
|
|
59
78
|
numpy_func_message = (
|
|
60
79
|
"aggfunc must be functions or strings of numpy functions or methods."
|
|
@@ -68,11 +87,11 @@ class Raster:
|
|
|
68
87
|
'from_gdf'.
|
|
69
88
|
|
|
70
89
|
|
|
71
|
-
Examples
|
|
90
|
+
Examples:
|
|
72
91
|
--------
|
|
73
|
-
|
|
74
92
|
Read tif file.
|
|
75
93
|
|
|
94
|
+
>>> import sgis as sg
|
|
76
95
|
>>> path = 'https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/raster/dtm_10.tif'
|
|
77
96
|
>>> raster = sg.Raster.from_path(path)
|
|
78
97
|
>>> raster
|
|
@@ -147,14 +166,8 @@ class Raster:
|
|
|
147
166
|
|
|
148
167
|
"""
|
|
149
168
|
|
|
150
|
-
# attributes
|
|
151
|
-
|
|
152
|
-
date_format: str | None = None
|
|
153
|
-
contains: str | None = None
|
|
154
|
-
endswith: str = ".tif"
|
|
155
|
-
|
|
156
|
-
# attributes conserning rasterio metadata
|
|
157
|
-
_profile = {
|
|
169
|
+
# attributes concerning rasterio metadata
|
|
170
|
+
_profile: ClassVar[dict[str, str | None]] = {
|
|
158
171
|
"driver": "GTiff",
|
|
159
172
|
"compress": "LZW",
|
|
160
173
|
"nodata": None,
|
|
@@ -164,43 +177,53 @@ class Raster:
|
|
|
164
177
|
"indexes": None,
|
|
165
178
|
}
|
|
166
179
|
|
|
167
|
-
# driver: str = "GTiff"
|
|
168
|
-
# compress: str = "LZW"
|
|
169
|
-
# _nodata: int | float | None = None
|
|
170
|
-
# _dtype: type | None = None
|
|
171
|
-
|
|
172
180
|
def __init__(
|
|
173
181
|
self,
|
|
174
|
-
|
|
182
|
+
data: Self | str | np.ndarray | None = None,
|
|
175
183
|
*,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
array: np.ndarray | None = None,
|
|
179
|
-
file_system=None,
|
|
184
|
+
file_system: GCSFileSystem | None = None,
|
|
185
|
+
filename_regex: str | None = None,
|
|
180
186
|
**kwargs,
|
|
181
|
-
):
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Note: use the classmethods from_path, from_array, from_gdf etc. instead of the initialiser.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
data: A file path, an array or a Raster object.
|
|
192
|
+
file_system: Optional GCSFileSystem.
|
|
193
|
+
filename_regex: Regular expression to match file name attributes (date, band, tile, resolution).
|
|
194
|
+
**kwargs: Arguments concerning file metadata or
|
|
195
|
+
spatial properties of the image.
|
|
196
|
+
"""
|
|
197
|
+
self.filename_regex = filename_regex
|
|
198
|
+
|
|
199
|
+
if isinstance(data, Raster):
|
|
200
|
+
for key, value in data.__dict__.items():
|
|
201
|
+
setattr(data, key, value)
|
|
189
202
|
return
|
|
190
203
|
|
|
191
|
-
if
|
|
204
|
+
if isinstance(data, (str | Path | os.PathLike)):
|
|
205
|
+
self.path = data
|
|
206
|
+
|
|
207
|
+
else:
|
|
208
|
+
self.path = None
|
|
209
|
+
|
|
210
|
+
if isinstance(data, (np.ndarray)):
|
|
211
|
+
self.array = data
|
|
212
|
+
else:
|
|
213
|
+
self.array = None
|
|
214
|
+
|
|
215
|
+
if self.path is None and not any(
|
|
216
|
+
[kwargs.get("transform"), kwargs.get("bounds")]
|
|
217
|
+
):
|
|
192
218
|
raise TypeError(
|
|
193
219
|
"Must specify either bounds or transform when constructing raster from array."
|
|
194
220
|
)
|
|
195
221
|
|
|
196
|
-
# add class profile first
|
|
222
|
+
# add class profile first, then override with args and kwargs
|
|
197
223
|
self.update(**self._profile)
|
|
198
224
|
|
|
199
225
|
self._crs = kwargs.pop("crs", self._crs if hasattr(self, "_crs") else None)
|
|
200
226
|
self._bounds = None
|
|
201
|
-
|
|
202
|
-
self.path = path
|
|
203
|
-
self.array = array
|
|
204
227
|
self.file_system = file_system
|
|
205
228
|
self._indexes = self._get_indexes(kwargs.pop("indexes", self.indexes))
|
|
206
229
|
|
|
@@ -220,21 +243,28 @@ class Raster:
|
|
|
220
243
|
cls,
|
|
221
244
|
path: str,
|
|
222
245
|
res: int | None = None,
|
|
223
|
-
file_system=None,
|
|
246
|
+
file_system: GCSFileSystem | None = None,
|
|
247
|
+
filename_regex: str | None = None,
|
|
224
248
|
**kwargs,
|
|
225
|
-
):
|
|
249
|
+
) -> Self:
|
|
226
250
|
"""Construct Raster from file path.
|
|
227
251
|
|
|
228
252
|
Args:
|
|
229
253
|
path: Path to a raster image file.
|
|
254
|
+
res: Spatial resolution when reading the image.
|
|
255
|
+
file_system: Optional file system.
|
|
256
|
+
filename_regex: Regular expression with optional match groups.
|
|
257
|
+
**kwargs: Arguments concerning file metadata or
|
|
258
|
+
spatial properties of the image.
|
|
230
259
|
|
|
231
260
|
Returns:
|
|
232
261
|
A Raster instance.
|
|
233
262
|
"""
|
|
234
263
|
return cls(
|
|
235
|
-
|
|
264
|
+
str(path),
|
|
236
265
|
file_system=file_system,
|
|
237
266
|
res=res,
|
|
267
|
+
filename_regex=filename_regex,
|
|
238
268
|
**kwargs,
|
|
239
269
|
)
|
|
240
270
|
|
|
@@ -242,19 +272,18 @@ class Raster:
|
|
|
242
272
|
def from_array(
|
|
243
273
|
cls,
|
|
244
274
|
array: np.ndarray,
|
|
245
|
-
crs,
|
|
275
|
+
crs: Any,
|
|
246
276
|
*,
|
|
247
277
|
transform: Affine | None = None,
|
|
248
278
|
bounds: tuple | Geometry | None = None,
|
|
249
279
|
copy: bool = True,
|
|
250
280
|
**kwargs,
|
|
251
|
-
):
|
|
281
|
+
) -> Self:
|
|
252
282
|
"""Construct Raster from numpy array.
|
|
253
283
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
which transform will then be created from.
|
|
284
|
+
Must also specify nessecary spatial properties
|
|
285
|
+
The necessary metadata is 'crs' and either 'transform' (Affine object)
|
|
286
|
+
or 'bounds', which transform will then be created from.
|
|
258
287
|
|
|
259
288
|
Args:
|
|
260
289
|
array: 2d or 3d numpy ndarray.
|
|
@@ -263,7 +292,9 @@ class Raster:
|
|
|
263
292
|
of bounds.
|
|
264
293
|
bounds: Minimum and maximum x and y coordinates. Can be specified instead
|
|
265
294
|
of transform.
|
|
266
|
-
|
|
295
|
+
copy: Whether to copy the array.
|
|
296
|
+
**kwargs: Arguments concerning file metadata or
|
|
297
|
+
spatial properties of the image.
|
|
267
298
|
|
|
268
299
|
Returns:
|
|
269
300
|
A Raster instance.
|
|
@@ -295,7 +326,7 @@ class Raster:
|
|
|
295
326
|
|
|
296
327
|
crs = pyproj.CRS(crs) if crs else None
|
|
297
328
|
|
|
298
|
-
return cls(array
|
|
329
|
+
return cls(array, crs=crs, transform=transform, bounds=bounds, **kwargs)
|
|
299
330
|
|
|
300
331
|
@classmethod
|
|
301
332
|
def from_gdf(
|
|
@@ -303,21 +334,38 @@ class Raster:
|
|
|
303
334
|
gdf: GeoDataFrame,
|
|
304
335
|
columns: str | Iterable[str],
|
|
305
336
|
res: int,
|
|
306
|
-
fill=0,
|
|
307
|
-
all_touched=False,
|
|
308
|
-
merge_alg=MergeAlg.replace,
|
|
309
|
-
default_value=1,
|
|
310
|
-
dtype=None,
|
|
337
|
+
fill: int = 0,
|
|
338
|
+
all_touched: bool = False,
|
|
339
|
+
merge_alg: Callable = MergeAlg.replace,
|
|
340
|
+
default_value: int = 1,
|
|
341
|
+
dtype: Any | None = None,
|
|
311
342
|
**kwargs,
|
|
312
|
-
):
|
|
343
|
+
) -> Self:
|
|
313
344
|
"""Construct Raster from a GeoDataFrame.
|
|
314
345
|
|
|
315
346
|
Args:
|
|
316
|
-
gdf: The GeoDataFrame.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
347
|
+
gdf: The GeoDataFrame to rasterize.
|
|
348
|
+
columns: Column(s) in the GeoDataFrame whose values are used to populate the raster.
|
|
349
|
+
This can be a single column name or a list of column names.
|
|
350
|
+
res: Resolution of the raster in units of the GeoDataFrame's coordinate reference system.
|
|
351
|
+
fill: Fill value for areas outside of input geometries (default is 0).
|
|
352
|
+
all_touched: Whether to consider all pixels touched by geometries,
|
|
353
|
+
not just those whose center is within the polygon (default is False).
|
|
354
|
+
merge_alg: Merge algorithm to use when combining geometries
|
|
355
|
+
(default is 'MergeAlg.replace').
|
|
356
|
+
default_value: Default value to use for the rasterized pixels
|
|
357
|
+
(default is 1).
|
|
358
|
+
dtype: Data type of the output array. If None, it will be
|
|
359
|
+
determined automatically.
|
|
360
|
+
**kwargs: Additional keyword arguments passed to the raster
|
|
361
|
+
creation process, e.g., custom CRS or transform settings.
|
|
320
362
|
|
|
363
|
+
Returns:
|
|
364
|
+
A Raster instance based on the specified GeoDataFrame and parameters.
|
|
365
|
+
|
|
366
|
+
Raises:
|
|
367
|
+
TypeError: If 'transform' is provided in kwargs, as this is
|
|
368
|
+
computed based on the GeoDataFrame bounds and resolution.
|
|
321
369
|
"""
|
|
322
370
|
if not isinstance(gdf, GeoDataFrame):
|
|
323
371
|
gdf = to_gdf(gdf)
|
|
@@ -362,10 +410,10 @@ class Raster:
|
|
|
362
410
|
assert len(array.shape) == 3
|
|
363
411
|
name = kwargs.get("name", None)
|
|
364
412
|
|
|
365
|
-
return cls.from_array(array
|
|
413
|
+
return cls.from_array(array, name=name, **kwargs)
|
|
366
414
|
|
|
367
415
|
@classmethod
|
|
368
|
-
def from_dict(cls, dictionary: dict):
|
|
416
|
+
def from_dict(cls, dictionary: dict) -> Self:
|
|
369
417
|
"""Construct Raster from metadata dict to fastpass the initializer.
|
|
370
418
|
|
|
371
419
|
This is the fastest way to create a Raster since a metadata lookup is not
|
|
@@ -387,6 +435,7 @@ class Raster:
|
|
|
387
435
|
return cls(**dictionary)
|
|
388
436
|
|
|
389
437
|
def update(self, **kwargs) -> Self:
|
|
438
|
+
"""Update attributes of the Raster."""
|
|
390
439
|
for key, value in kwargs.items():
|
|
391
440
|
self._validate_key(key)
|
|
392
441
|
if is_property(self, key):
|
|
@@ -394,7 +443,9 @@ class Raster:
|
|
|
394
443
|
setattr(self, key, value)
|
|
395
444
|
return self
|
|
396
445
|
|
|
397
|
-
def write(
|
|
446
|
+
def write(
|
|
447
|
+
self, path: str, window: rasterio.windows.Window | None = None, **kwargs
|
|
448
|
+
) -> None:
|
|
398
449
|
"""Write the raster as a single file.
|
|
399
450
|
|
|
400
451
|
Multiband arrays will result in a multiband image file.
|
|
@@ -402,8 +453,10 @@ class Raster:
|
|
|
402
453
|
Args:
|
|
403
454
|
path: File path to write to.
|
|
404
455
|
window: Optional window to clip the image to.
|
|
456
|
+
**kwargs: Keyword arguments passed to rasterio.open.
|
|
457
|
+
Thise will override the items in the Raster's profile,
|
|
458
|
+
if overlapping.
|
|
405
459
|
"""
|
|
406
|
-
|
|
407
460
|
if self.array is None:
|
|
408
461
|
raise AttributeError("The image hasn't been loaded.")
|
|
409
462
|
|
|
@@ -436,7 +489,7 @@ class Raster:
|
|
|
436
489
|
|
|
437
490
|
def clip(
|
|
438
491
|
self,
|
|
439
|
-
mask,
|
|
492
|
+
mask: Any,
|
|
440
493
|
masked: bool = False,
|
|
441
494
|
boundless: bool = True,
|
|
442
495
|
**kwargs,
|
|
@@ -448,6 +501,13 @@ class Raster:
|
|
|
448
501
|
|
|
449
502
|
Args:
|
|
450
503
|
mask: Geometry-like object or bounding box.
|
|
504
|
+
masked: If 'masked' is True the return value will be a masked
|
|
505
|
+
array. Otherwise (default) the return value will be a
|
|
506
|
+
regular array. Masks will be exactly the inverse of the
|
|
507
|
+
GDAL RFC 15 conforming arrays returned by read_masks().
|
|
508
|
+
boundless: If True, windows that extend beyond the dataset's extent
|
|
509
|
+
are permitted and partially or completely filled arrays will
|
|
510
|
+
be returned as appropriate.
|
|
451
511
|
**kwargs: Keyword arguments passed to the mask function
|
|
452
512
|
from the rasterio.mask module.
|
|
453
513
|
|
|
@@ -462,17 +522,18 @@ class Raster:
|
|
|
462
522
|
except ValueError:
|
|
463
523
|
mask = mask.set_crs(self.crs)
|
|
464
524
|
|
|
465
|
-
# if not self.crs.equals(pyproj.CRS(mask.crs)):
|
|
466
|
-
# raise ValueError("crs mismatch.")
|
|
467
|
-
|
|
468
525
|
self._read_with_mask(mask=mask, masked=masked, boundless=boundless, **kwargs)
|
|
469
526
|
|
|
470
527
|
return self
|
|
471
528
|
|
|
472
|
-
def intersects(self, other) -> bool:
|
|
529
|
+
def intersects(self, other: Any) -> bool:
|
|
530
|
+
"""Returns True if the image bounds intersect with 'other'."""
|
|
473
531
|
return self.unary_union.intersects(to_shapely(other))
|
|
474
532
|
|
|
475
|
-
def sample(
|
|
533
|
+
def sample(
|
|
534
|
+
self, n: int = 1, size: int = 20, mask: Any = None, copy: bool = True, **kwargs
|
|
535
|
+
) -> Self:
|
|
536
|
+
"""Take a random spatial sample of the image."""
|
|
476
537
|
if mask is not None:
|
|
477
538
|
points = GeoSeries(self.unary_union).clip(mask).sample_points(n)
|
|
478
539
|
else:
|
|
@@ -508,8 +569,8 @@ class Raster:
|
|
|
508
569
|
A GeoDataFrame with aggregated values per polygon.
|
|
509
570
|
"""
|
|
510
571
|
idx_mapper, idx_name = get_index_mapper(polygons)
|
|
511
|
-
polygons, aggfunc, func_names =
|
|
512
|
-
poly_iter =
|
|
572
|
+
polygons, aggfunc, func_names = _prepare_zonal(polygons, aggfunc)
|
|
573
|
+
poly_iter = _make_geometry_iterrows(polygons)
|
|
513
574
|
|
|
514
575
|
aggregated = []
|
|
515
576
|
for i, poly in poly_iter:
|
|
@@ -520,7 +581,7 @@ class Raster:
|
|
|
520
581
|
_aggregate(clipped.array, array_func, aggfunc, func_names, self.date, i)
|
|
521
582
|
)
|
|
522
583
|
|
|
523
|
-
return
|
|
584
|
+
return _zonal_post(
|
|
524
585
|
aggregated,
|
|
525
586
|
polygons=polygons,
|
|
526
587
|
idx_mapper=idx_mapper,
|
|
@@ -546,7 +607,7 @@ class Raster:
|
|
|
546
607
|
Returns:
|
|
547
608
|
The class instance with new array values, or a copy if copy is True.
|
|
548
609
|
|
|
549
|
-
Examples
|
|
610
|
+
Examples:
|
|
550
611
|
--------
|
|
551
612
|
Making an array where the gradient to the center is always 10.
|
|
552
613
|
|
|
@@ -580,6 +641,7 @@ class Raster:
|
|
|
580
641
|
return get_gradient(self, degrees=degrees, copy=copy)
|
|
581
642
|
|
|
582
643
|
def to_xarray(self) -> DataArray:
|
|
644
|
+
"""Convert the raster to an xarray.DataArray."""
|
|
583
645
|
self._check_for_array()
|
|
584
646
|
self.name = self.name or self.__class__.__name__.lower()
|
|
585
647
|
coords = _generate_spatial_coords(self.transform, self.width, self.height)
|
|
@@ -602,6 +664,7 @@ class Raster:
|
|
|
602
664
|
) # .transpose("y", "x")
|
|
603
665
|
|
|
604
666
|
def to_dict(self) -> dict:
|
|
667
|
+
"""Get a dictionary of Raster attributes."""
|
|
605
668
|
out = {}
|
|
606
669
|
for col in self.ALL_ATTRS:
|
|
607
670
|
try:
|
|
@@ -642,11 +705,11 @@ class Raster:
|
|
|
642
705
|
column = [column] * len(array_list)
|
|
643
706
|
|
|
644
707
|
gdfs = []
|
|
645
|
-
for i, (
|
|
708
|
+
for i, (col, array) in enumerate(zip(column, array_list, strict=True)):
|
|
646
709
|
gdf = gpd.GeoDataFrame(
|
|
647
710
|
pd.DataFrame(
|
|
648
711
|
self._array_to_geojson(array, self.transform),
|
|
649
|
-
columns=[
|
|
712
|
+
columns=[col, "geometry"],
|
|
650
713
|
),
|
|
651
714
|
geometry="geometry",
|
|
652
715
|
crs=self.crs,
|
|
@@ -658,7 +721,7 @@ class Raster:
|
|
|
658
721
|
|
|
659
722
|
def set_crs(
|
|
660
723
|
self,
|
|
661
|
-
crs,
|
|
724
|
+
crs: pyproj.CRS | Any,
|
|
662
725
|
allow_override: bool = False,
|
|
663
726
|
) -> Self:
|
|
664
727
|
"""Set coordinate reference system."""
|
|
@@ -671,7 +734,7 @@ class Raster:
|
|
|
671
734
|
self._crs = pyproj.CRS(crs)
|
|
672
735
|
return self
|
|
673
736
|
|
|
674
|
-
def to_crs(self, crs, **kwargs) -> Self:
|
|
737
|
+
def to_crs(self, crs: pyproj.CRS | Any, **kwargs) -> Self:
|
|
675
738
|
"""Reproject the raster.
|
|
676
739
|
|
|
677
740
|
Args:
|
|
@@ -739,9 +802,9 @@ class Raster:
|
|
|
739
802
|
|
|
740
803
|
return self
|
|
741
804
|
|
|
742
|
-
def plot(self, mask=None) -> None:
|
|
743
|
-
self._check_for_array()
|
|
805
|
+
def plot(self, mask: Any | None = None) -> None:
|
|
744
806
|
"""Plot the images. One image per band."""
|
|
807
|
+
self._check_for_array()
|
|
745
808
|
if mask is not None:
|
|
746
809
|
raster = self.copy().clip(mask)
|
|
747
810
|
else:
|
|
@@ -760,6 +823,7 @@ class Raster:
|
|
|
760
823
|
plt.close()
|
|
761
824
|
|
|
762
825
|
def astype(self, dtype: type) -> Self:
|
|
826
|
+
"""Convert the datatype of the array."""
|
|
763
827
|
if self.array is None:
|
|
764
828
|
raise ValueError("Array is not loaded.")
|
|
765
829
|
if not rasterio.dtypes.can_cast_dtype(self.array, dtype):
|
|
@@ -770,16 +834,19 @@ class Raster:
|
|
|
770
834
|
return self
|
|
771
835
|
|
|
772
836
|
def as_minimum_dtype(self) -> Self:
|
|
837
|
+
"""Convert the array to the minimum dtype without overflow."""
|
|
773
838
|
min_dtype = rasterio.dtypes.get_minimum_dtype(self.array)
|
|
774
839
|
self.array = self.array.astype(min_dtype)
|
|
775
840
|
return self
|
|
776
841
|
|
|
777
842
|
def min(self) -> int | None:
|
|
843
|
+
"""Minimum value in the array."""
|
|
778
844
|
if np.size(self.array):
|
|
779
845
|
return np.min(self.array)
|
|
780
846
|
return None
|
|
781
847
|
|
|
782
848
|
def max(self) -> int | None:
|
|
849
|
+
"""Maximum value in the array."""
|
|
783
850
|
if np.size(self.array):
|
|
784
851
|
return np.max(self.array)
|
|
785
852
|
return None
|
|
@@ -798,6 +865,7 @@ class Raster:
|
|
|
798
865
|
return self
|
|
799
866
|
|
|
800
867
|
def array_list(self) -> list[np.ndarray]:
|
|
868
|
+
"""Get a list of 2D arrays."""
|
|
801
869
|
self._check_for_array()
|
|
802
870
|
if len(self.array.shape) == 2:
|
|
803
871
|
return [self.array]
|
|
@@ -808,10 +876,12 @@ class Raster:
|
|
|
808
876
|
|
|
809
877
|
@property
|
|
810
878
|
def indexes(self) -> int | tuple[int] | None:
|
|
879
|
+
"""Band indexes of the image."""
|
|
811
880
|
return self._indexes
|
|
812
881
|
|
|
813
882
|
@property
|
|
814
883
|
def name(self) -> str | None:
|
|
884
|
+
"""Name of the file in the file path, if any."""
|
|
815
885
|
try:
|
|
816
886
|
return self._name
|
|
817
887
|
except AttributeError:
|
|
@@ -821,12 +891,12 @@ class Raster:
|
|
|
821
891
|
return None
|
|
822
892
|
|
|
823
893
|
@name.setter
|
|
824
|
-
def name(self, value):
|
|
894
|
+
def name(self, value) -> None:
|
|
825
895
|
self._name = value
|
|
826
|
-
return self._name
|
|
827
896
|
|
|
828
897
|
@property
|
|
829
|
-
def date(self):
|
|
898
|
+
def date(self) -> str | None:
|
|
899
|
+
"""Date in the image file name, if filename_regex is present."""
|
|
830
900
|
try:
|
|
831
901
|
pattern = re.compile(self.filename_regex, re.VERBOSE)
|
|
832
902
|
return re.match(pattern, Path(self.path).name).group("date")
|
|
@@ -835,19 +905,16 @@ class Raster:
|
|
|
835
905
|
|
|
836
906
|
@property
|
|
837
907
|
def band(self) -> str | None:
|
|
908
|
+
"""Band name of the image file name, if filename_regex is present."""
|
|
838
909
|
try:
|
|
839
910
|
pattern = re.compile(self.filename_regex, re.VERBOSE)
|
|
840
911
|
return re.match(pattern, Path(self.path).name).group("band")
|
|
841
912
|
except (AttributeError, TypeError):
|
|
842
913
|
return None
|
|
843
914
|
|
|
844
|
-
# @property
|
|
845
|
-
# def band_color(self):
|
|
846
|
-
# """To be implemented in subclasses."""
|
|
847
|
-
# pass
|
|
848
|
-
|
|
849
915
|
@property
|
|
850
|
-
def dtype(self):
|
|
916
|
+
def dtype(self) -> Any:
|
|
917
|
+
"""Data type of the array."""
|
|
851
918
|
try:
|
|
852
919
|
return self.array.dtype
|
|
853
920
|
except AttributeError:
|
|
@@ -857,12 +924,12 @@ class Raster:
|
|
|
857
924
|
return None
|
|
858
925
|
|
|
859
926
|
@dtype.setter
|
|
860
|
-
def dtype(self, new_dtype):
|
|
927
|
+
def dtype(self, new_dtype: Any) -> None:
|
|
861
928
|
self.array = self.array.astype(new_dtype)
|
|
862
|
-
return self.array.dtype
|
|
863
929
|
|
|
864
930
|
@property
|
|
865
931
|
def nodata(self) -> int | None:
|
|
932
|
+
"""No data value."""
|
|
866
933
|
try:
|
|
867
934
|
return self._nodata
|
|
868
935
|
except AttributeError:
|
|
@@ -870,12 +937,14 @@ class Raster:
|
|
|
870
937
|
|
|
871
938
|
@property
|
|
872
939
|
def tile(self) -> str | None:
|
|
940
|
+
"""The lower left corner (minx, miny) of the image as a string."""
|
|
873
941
|
if self.bounds is None:
|
|
874
942
|
return None
|
|
875
943
|
return f"{int(self.bounds[0])}_{int(self.bounds[1])}"
|
|
876
944
|
|
|
877
945
|
@property
|
|
878
946
|
def meta(self) -> dict:
|
|
947
|
+
"""Metadata dict."""
|
|
879
948
|
return {
|
|
880
949
|
"path": self.path,
|
|
881
950
|
"type": self.__class__.__name__,
|
|
@@ -886,6 +955,7 @@ class Raster:
|
|
|
886
955
|
|
|
887
956
|
@property
|
|
888
957
|
def profile(self) -> dict:
|
|
958
|
+
"""Profile of the image file."""
|
|
889
959
|
# TODO: .crs blir feil hvis warpa. Eller?
|
|
890
960
|
return {
|
|
891
961
|
"driver": self.driver,
|
|
@@ -902,6 +972,7 @@ class Raster:
|
|
|
902
972
|
|
|
903
973
|
@property
|
|
904
974
|
def read_kwargs(self) -> dict:
|
|
975
|
+
"""Keywords passed to the read method of rasterio.io.DatasetReader."""
|
|
905
976
|
return {
|
|
906
977
|
"indexes": self.indexes,
|
|
907
978
|
"fill_value": self.nodata,
|
|
@@ -910,6 +981,7 @@ class Raster:
|
|
|
910
981
|
|
|
911
982
|
@property
|
|
912
983
|
def res(self) -> float | None:
|
|
984
|
+
"""Get the spatial resolution of the image."""
|
|
913
985
|
if hasattr(self, "_res") and self._res is not None:
|
|
914
986
|
return self._res
|
|
915
987
|
if self.width is None:
|
|
@@ -919,6 +991,7 @@ class Raster:
|
|
|
919
991
|
|
|
920
992
|
@property
|
|
921
993
|
def height(self) -> int | None:
|
|
994
|
+
"""Get the height of the image as number of pixels."""
|
|
922
995
|
if self.array is None:
|
|
923
996
|
try:
|
|
924
997
|
return self._height
|
|
@@ -929,6 +1002,7 @@ class Raster:
|
|
|
929
1002
|
|
|
930
1003
|
@property
|
|
931
1004
|
def width(self) -> int | None:
|
|
1005
|
+
"""Get the width of the image as number of pixels."""
|
|
932
1006
|
if self.array is None:
|
|
933
1007
|
try:
|
|
934
1008
|
return self._width
|
|
@@ -945,6 +1019,7 @@ class Raster:
|
|
|
945
1019
|
|
|
946
1020
|
@property
|
|
947
1021
|
def count(self) -> int:
|
|
1022
|
+
"""Get the number of bands in the image."""
|
|
948
1023
|
if self.array is not None:
|
|
949
1024
|
if len(self.array.shape) == 3:
|
|
950
1025
|
return self.array.shape[0]
|
|
@@ -965,6 +1040,7 @@ class Raster:
|
|
|
965
1040
|
|
|
966
1041
|
@property
|
|
967
1042
|
def transform(self) -> Affine | None:
|
|
1043
|
+
"""Get the Affine transform of the image."""
|
|
968
1044
|
try:
|
|
969
1045
|
return rasterio.transform.from_bounds(*self.bounds, self.width, self.height)
|
|
970
1046
|
except (ZeroDivisionError, TypeError):
|
|
@@ -973,13 +1049,15 @@ class Raster:
|
|
|
973
1049
|
|
|
974
1050
|
@property
|
|
975
1051
|
def bounds(self) -> tuple[float, float, float, float] | None:
|
|
1052
|
+
"""Get the bounds of the image."""
|
|
976
1053
|
try:
|
|
977
1054
|
return to_bbox(self._bounds)
|
|
978
1055
|
except (AttributeError, TypeError):
|
|
979
1056
|
return None
|
|
980
1057
|
|
|
981
1058
|
@property
|
|
982
|
-
def crs(self):
|
|
1059
|
+
def crs(self) -> pyproj.CRS | None:
|
|
1060
|
+
"""Get the coordinate reference system of the image."""
|
|
983
1061
|
try:
|
|
984
1062
|
return self._warped_crs
|
|
985
1063
|
except AttributeError:
|
|
@@ -990,24 +1068,29 @@ class Raster:
|
|
|
990
1068
|
|
|
991
1069
|
@property
|
|
992
1070
|
def area(self) -> float:
|
|
1071
|
+
"""Get the area of the image."""
|
|
993
1072
|
return shapely.area(self.unary_union)
|
|
994
1073
|
|
|
995
1074
|
@property
|
|
996
1075
|
def length(self) -> float:
|
|
1076
|
+
"""Get the circumfence of the image."""
|
|
997
1077
|
return shapely.length(self.unary_union)
|
|
998
1078
|
|
|
999
1079
|
@property
|
|
1000
1080
|
def unary_union(self) -> Polygon:
|
|
1081
|
+
"""Get the image bounds as a Polygon."""
|
|
1001
1082
|
return shapely.box(*self.bounds)
|
|
1002
1083
|
|
|
1003
1084
|
@property
|
|
1004
1085
|
def centroid(self) -> Point:
|
|
1086
|
+
"""Get the centerpoint of the image."""
|
|
1005
1087
|
x = (self.bounds[0] + self.bounds[2]) / 2
|
|
1006
1088
|
y = (self.bounds[1] + self.bounds[3]) / 2
|
|
1007
1089
|
return Point(x, y)
|
|
1008
1090
|
|
|
1009
1091
|
@property
|
|
1010
1092
|
def properties(self) -> list[str]:
|
|
1093
|
+
"""List of all properties of the class."""
|
|
1011
1094
|
out = []
|
|
1012
1095
|
for attr in dir(self):
|
|
1013
1096
|
try:
|
|
@@ -1018,11 +1101,12 @@ class Raster:
|
|
|
1018
1101
|
return out
|
|
1019
1102
|
|
|
1020
1103
|
def indexes_as_tuple(self) -> tuple[int, ...]:
|
|
1104
|
+
"""Get the band index(es) as a tuple of integers."""
|
|
1021
1105
|
if len(self.shape) == 2:
|
|
1022
1106
|
return (1,)
|
|
1023
1107
|
return tuple(i + 1 for i in range(self.shape[0]))
|
|
1024
1108
|
|
|
1025
|
-
def copy(self, deep=True):
|
|
1109
|
+
def copy(self, deep: bool = True) -> "Raster":
|
|
1026
1110
|
"""Returns a (deep) copy of the class instance.
|
|
1027
1111
|
|
|
1028
1112
|
Args:
|
|
@@ -1033,7 +1117,8 @@ class Raster:
|
|
|
1033
1117
|
else:
|
|
1034
1118
|
return copy(self)
|
|
1035
1119
|
|
|
1036
|
-
def equals(self, other) -> bool:
|
|
1120
|
+
def equals(self, other: Any) -> bool:
|
|
1121
|
+
"""Check if the Raster is equal to another Raster."""
|
|
1037
1122
|
if not isinstance(other, Raster):
|
|
1038
1123
|
raise NotImplementedError("other must be of type Raster")
|
|
1039
1124
|
if type(other) != type(self):
|
|
@@ -1061,37 +1146,53 @@ class Raster:
|
|
|
1061
1146
|
res = None
|
|
1062
1147
|
return f"{self.__class__.__name__}(shape=({shp}), res={res}, name={self.name}, path={self.path})"
|
|
1063
1148
|
|
|
1064
|
-
def
|
|
1149
|
+
def __iter__(self) -> Iterator[np.ndarray]:
|
|
1150
|
+
"""Iterate over the arrays."""
|
|
1151
|
+
if len(self.array.shape) == 2:
|
|
1152
|
+
return iter([self.array])
|
|
1153
|
+
if len(self.array.shape) == 3:
|
|
1154
|
+
return iter(self.array)
|
|
1155
|
+
raise ValueError(
|
|
1156
|
+
f"Array should have shape length 2 or 3. Got {len(self.array.shape)}"
|
|
1157
|
+
)
|
|
1158
|
+
|
|
1159
|
+
def __mul__(self, scalar: int | float) -> "Raster":
|
|
1160
|
+
"""Multiply the array values with *."""
|
|
1065
1161
|
self._check_for_array()
|
|
1066
1162
|
self.array = self.array * scalar
|
|
1067
1163
|
return self
|
|
1068
1164
|
|
|
1069
|
-
def __add__(self, scalar):
|
|
1165
|
+
def __add__(self, scalar: int | float) -> "Raster":
|
|
1166
|
+
"""Add to the array values with +."""
|
|
1070
1167
|
self._check_for_array()
|
|
1071
1168
|
self.array = self.array + scalar
|
|
1072
1169
|
return self
|
|
1073
1170
|
|
|
1074
|
-
def __sub__(self, scalar):
|
|
1171
|
+
def __sub__(self, scalar: int | float) -> "Raster":
|
|
1172
|
+
"""Subtract the array values with -."""
|
|
1075
1173
|
self._check_for_array()
|
|
1076
1174
|
self.array = self.array - scalar
|
|
1077
1175
|
return self
|
|
1078
1176
|
|
|
1079
|
-
def __truediv__(self, scalar):
|
|
1177
|
+
def __truediv__(self, scalar: int | float) -> "Raster":
|
|
1178
|
+
"""Divide the array values with /."""
|
|
1080
1179
|
self._check_for_array()
|
|
1081
1180
|
self.array = self.array / scalar
|
|
1082
1181
|
return self
|
|
1083
1182
|
|
|
1084
|
-
def __floordiv__(self, scalar):
|
|
1183
|
+
def __floordiv__(self, scalar: int | float) -> "Raster":
|
|
1184
|
+
"""Floor divide the array values with //."""
|
|
1085
1185
|
self._check_for_array()
|
|
1086
1186
|
self.array = self.array // scalar
|
|
1087
1187
|
return self
|
|
1088
1188
|
|
|
1089
|
-
def __pow__(self, exponent):
|
|
1189
|
+
def __pow__(self, exponent: int | float) -> "Raster":
|
|
1190
|
+
"""Exponentiate the array values with **."""
|
|
1090
1191
|
self._check_for_array()
|
|
1091
1192
|
self.array = self.array**exponent
|
|
1092
1193
|
return self
|
|
1093
1194
|
|
|
1094
|
-
def _has_nessecary_attrs(self, dict_like) -> bool:
|
|
1195
|
+
def _has_nessecary_attrs(self, dict_like: dict) -> bool:
|
|
1095
1196
|
"""Check if Raster init got enough kwargs to not need to read src."""
|
|
1096
1197
|
try:
|
|
1097
1198
|
self._validate_dict(dict_like)
|
|
@@ -1101,7 +1202,7 @@ class Raster:
|
|
|
1101
1202
|
except AttributeError:
|
|
1102
1203
|
return False
|
|
1103
1204
|
|
|
1104
|
-
def _return_self_or_copy(self, array, copy: bool):
|
|
1205
|
+
def _return_self_or_copy(self, array: np.ndarray, copy: bool) -> "Raster":
|
|
1105
1206
|
if not copy:
|
|
1106
1207
|
self.array = array
|
|
1107
1208
|
return self
|
|
@@ -1111,7 +1212,7 @@ class Raster:
|
|
|
1111
1212
|
return copy
|
|
1112
1213
|
|
|
1113
1214
|
@classmethod
|
|
1114
|
-
def _validate_dict(cls, dict_like) -> None:
|
|
1215
|
+
def _validate_dict(cls, dict_like: dict) -> None:
|
|
1115
1216
|
missing = []
|
|
1116
1217
|
for attr in NESSECARY_META:
|
|
1117
1218
|
if any(
|
|
@@ -1127,14 +1228,14 @@ class Raster:
|
|
|
1127
1228
|
raise AttributeError(f"Missing nessecary key(s) {', '.join(missing)}")
|
|
1128
1229
|
|
|
1129
1230
|
@classmethod
|
|
1130
|
-
def _validate_key(cls, key) -> None:
|
|
1231
|
+
def _validate_key(cls, key: str) -> None:
|
|
1131
1232
|
if key not in ALLOWED_KEYS:
|
|
1132
1233
|
raise ValueError(
|
|
1133
1234
|
f"Got an unexpected key {key!r}. Allowed keys are ",
|
|
1134
1235
|
", ".join(ALLOWED_KEYS),
|
|
1135
1236
|
)
|
|
1136
1237
|
|
|
1137
|
-
def _get_shape_from_res(self, res) -> tuple[int] | None:
|
|
1238
|
+
def _get_shape_from_res(self, res: int) -> tuple[int] | None:
|
|
1138
1239
|
if res is None:
|
|
1139
1240
|
return None
|
|
1140
1241
|
if hasattr(res, "__iter__") and len(res) == 2:
|
|
@@ -1147,7 +1248,9 @@ class Raster:
|
|
|
1147
1248
|
return len(self.indexes), width, height
|
|
1148
1249
|
return width, height
|
|
1149
1250
|
|
|
1150
|
-
def _write(
|
|
1251
|
+
def _write(
|
|
1252
|
+
self, dst: rasterio.io.DatasetReader, window: rasterio.windows.Window
|
|
1253
|
+
) -> None:
|
|
1151
1254
|
if np.ma.is_masked(self.array):
|
|
1152
1255
|
if len(self.array.shape) == 2:
|
|
1153
1256
|
return dst.write(
|
|
@@ -1168,7 +1271,7 @@ class Raster:
|
|
|
1168
1271
|
for i, idx in enumerate(self.indexes_as_tuple()):
|
|
1169
1272
|
dst.write(self.array[i], indexes=idx, window=window)
|
|
1170
1273
|
|
|
1171
|
-
def _get_indexes(self, indexes):
|
|
1274
|
+
def _get_indexes(self, indexes: int | tuple[int] | None) -> int | tuple[int] | None:
|
|
1172
1275
|
if isinstance(indexes, numbers.Number):
|
|
1173
1276
|
return int(indexes)
|
|
1174
1277
|
if indexes is None:
|
|
@@ -1188,7 +1291,7 @@ class Raster:
|
|
|
1188
1291
|
f"Got {type(indexes)}: {indexes}"
|
|
1189
1292
|
) from e
|
|
1190
1293
|
|
|
1191
|
-
def _return_gdf(self, obj) -> GeoDataFrame:
|
|
1294
|
+
def _return_gdf(self, obj: Any) -> GeoDataFrame:
|
|
1192
1295
|
if isinstance(obj, str) and not is_wkt(obj):
|
|
1193
1296
|
return self._read_tif(obj)
|
|
1194
1297
|
elif isinstance(obj, Raster):
|
|
@@ -1210,11 +1313,13 @@ class Raster:
|
|
|
1210
1313
|
warnings.filterwarnings("ignore", category=UserWarning)
|
|
1211
1314
|
return [
|
|
1212
1315
|
(feature["geometry"], val)
|
|
1213
|
-
for val, feature in zip(
|
|
1316
|
+
for val, feature in zip(
|
|
1317
|
+
gdf[column], loads(gdf.to_json())["features"], strict=False
|
|
1318
|
+
)
|
|
1214
1319
|
]
|
|
1215
1320
|
|
|
1216
1321
|
@staticmethod
|
|
1217
|
-
def _array_to_geojson(array: np.ndarray, transform: Affine):
|
|
1322
|
+
def _array_to_geojson(array: np.ndarray, transform: Affine) -> list[tuple]:
|
|
1218
1323
|
try:
|
|
1219
1324
|
return [
|
|
1220
1325
|
(value, shape(geom))
|
|
@@ -1227,7 +1332,7 @@ class Raster:
|
|
|
1227
1332
|
for geom, value in features.shapes(array, transform=transform)
|
|
1228
1333
|
]
|
|
1229
1334
|
|
|
1230
|
-
def _add_indexes_from_array(self, indexes):
|
|
1335
|
+
def _add_indexes_from_array(self, indexes: int | tuple[int]) -> int | tuple[int]:
|
|
1231
1336
|
if indexes is not None:
|
|
1232
1337
|
return indexes
|
|
1233
1338
|
elif len(self.array.shape) == 3:
|
|
@@ -1237,7 +1342,7 @@ class Raster:
|
|
|
1237
1342
|
else:
|
|
1238
1343
|
raise ValueError
|
|
1239
1344
|
|
|
1240
|
-
def _add_meta_from_src(self, src):
|
|
1345
|
+
def _add_meta_from_src(self, src: rasterio.io.DatasetReader) -> None:
|
|
1241
1346
|
if not hasattr(self, "_bounds") or self._bounds is None:
|
|
1242
1347
|
self._bounds = tuple(src.bounds)
|
|
1243
1348
|
|
|
@@ -1293,7 +1398,7 @@ class Raster:
|
|
|
1293
1398
|
return self._read(self.path, **kwargs)
|
|
1294
1399
|
|
|
1295
1400
|
@functools.lru_cache(maxsize=128)
|
|
1296
|
-
def _read(self, path, **kwargs):
|
|
1401
|
+
def _read(self, path: str | Path, **kwargs) -> None:
|
|
1297
1402
|
with opener(path, file_system=self.file_system) as file:
|
|
1298
1403
|
with rasterio.open(file) as src:
|
|
1299
1404
|
self._add_meta_from_src(src)
|
|
@@ -1311,7 +1416,9 @@ class Raster:
|
|
|
1311
1416
|
else:
|
|
1312
1417
|
self = self.as_minimum_dtype()
|
|
1313
1418
|
|
|
1314
|
-
def _read_with_mask(
|
|
1419
|
+
def _read_with_mask(
|
|
1420
|
+
self, mask: Any, masked: bool, boundless: bool, **kwargs
|
|
1421
|
+
) -> None:
|
|
1315
1422
|
kwargs["mask"] = mask
|
|
1316
1423
|
|
|
1317
1424
|
def _read(self, src, mask, **kwargs):
|
|
@@ -1392,3 +1499,82 @@ def get_shape_from_bounds(
|
|
|
1392
1499
|
width = int(diffx / resx)
|
|
1393
1500
|
heigth = int(diffy / resy)
|
|
1394
1501
|
return heigth, width
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
def get_gradient(raster: Raster, degrees: bool = False, copy: bool = False) -> Raster:
|
|
1505
|
+
"""Get the slope of an elevation raster.
|
|
1506
|
+
|
|
1507
|
+
Calculates the absolute slope between the grid cells
|
|
1508
|
+
based on the image resolution.
|
|
1509
|
+
|
|
1510
|
+
For multiband images, the calculation is done for each band.
|
|
1511
|
+
|
|
1512
|
+
Args:
|
|
1513
|
+
raster: Raster instance.
|
|
1514
|
+
degrees: If False (default), the returned values will be in ratios,
|
|
1515
|
+
where a value of 1 means 1 meter up per 1 meter forward. If True,
|
|
1516
|
+
the values will be in degrees from 0 to 90.
|
|
1517
|
+
copy: Whether to copy or overwrite the original Raster.
|
|
1518
|
+
Defaults to False to save memory.
|
|
1519
|
+
|
|
1520
|
+
Returns:
|
|
1521
|
+
The class instance with new array values, or a copy if copy is True.
|
|
1522
|
+
|
|
1523
|
+
Examples:
|
|
1524
|
+
--------
|
|
1525
|
+
Making an array where the gradient to the center is always 10.
|
|
1526
|
+
|
|
1527
|
+
>>> import sgis as sg
|
|
1528
|
+
>>> import numpy as np
|
|
1529
|
+
>>> arr = np.array(
|
|
1530
|
+
... [
|
|
1531
|
+
... [100, 100, 100, 100, 100],
|
|
1532
|
+
... [100, 110, 110, 110, 100],
|
|
1533
|
+
... [100, 110, 120, 110, 100],
|
|
1534
|
+
... [100, 110, 110, 110, 100],
|
|
1535
|
+
... [100, 100, 100, 100, 100],
|
|
1536
|
+
... ]
|
|
1537
|
+
... )
|
|
1538
|
+
|
|
1539
|
+
Now let's create a Raster from this array with a resolution of 10.
|
|
1540
|
+
|
|
1541
|
+
>>> r = sg.Raster.from_array(arr, crs=None, bounds=(0, 0, 50, 50), res=10)
|
|
1542
|
+
|
|
1543
|
+
The gradient will be 1 (1 meter up for every meter forward).
|
|
1544
|
+
The calculation is by default done in place to save memory.
|
|
1545
|
+
|
|
1546
|
+
>>> r.gradient()
|
|
1547
|
+
>>> r.array
|
|
1548
|
+
array([[0., 1., 1., 1., 0.],
|
|
1549
|
+
[1., 1., 1., 1., 1.],
|
|
1550
|
+
[1., 1., 0., 1., 1.],
|
|
1551
|
+
[1., 1., 1., 1., 1.],
|
|
1552
|
+
[0., 1., 1., 1., 0.]])
|
|
1553
|
+
"""
|
|
1554
|
+
out_array = []
|
|
1555
|
+
for array in raster:
|
|
1556
|
+
results = _slope_2d(array, raster.res, degrees=degrees)
|
|
1557
|
+
out_array.append(results)
|
|
1558
|
+
|
|
1559
|
+
if len(raster.shape) == 2:
|
|
1560
|
+
out_array = out_array[0]
|
|
1561
|
+
else:
|
|
1562
|
+
out_array = np.array(out_array)
|
|
1563
|
+
|
|
1564
|
+
return raster._return_self_or_copy(out_array, copy)
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
def _slope_2d(array: np.ndarray, res: int, degrees: int) -> np.ndarray:
|
|
1568
|
+
gradient_x, gradient_y = np.gradient(array, res, res)
|
|
1569
|
+
|
|
1570
|
+
gradient = abs(gradient_x) + abs(gradient_y)
|
|
1571
|
+
|
|
1572
|
+
if not degrees:
|
|
1573
|
+
return gradient
|
|
1574
|
+
|
|
1575
|
+
radians = np.arctan(gradient)
|
|
1576
|
+
degrees = np.degrees(radians)
|
|
1577
|
+
|
|
1578
|
+
assert np.max(degrees) <= 90
|
|
1579
|
+
|
|
1580
|
+
return degrees
|