ssb-sgis 1.0.1__py3-none-any.whl → 1.0.3__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.
Files changed (60) hide show
  1. sgis/__init__.py +107 -121
  2. sgis/exceptions.py +5 -3
  3. sgis/geopandas_tools/__init__.py +1 -0
  4. sgis/geopandas_tools/bounds.py +86 -47
  5. sgis/geopandas_tools/buffer_dissolve_explode.py +62 -39
  6. sgis/geopandas_tools/centerlines.py +53 -44
  7. sgis/geopandas_tools/cleaning.py +87 -104
  8. sgis/geopandas_tools/conversion.py +164 -107
  9. sgis/geopandas_tools/duplicates.py +33 -19
  10. sgis/geopandas_tools/general.py +84 -52
  11. sgis/geopandas_tools/geometry_types.py +24 -10
  12. sgis/geopandas_tools/neighbors.py +23 -11
  13. sgis/geopandas_tools/overlay.py +136 -53
  14. sgis/geopandas_tools/point_operations.py +11 -10
  15. sgis/geopandas_tools/polygon_operations.py +53 -61
  16. sgis/geopandas_tools/polygons_as_rings.py +121 -78
  17. sgis/geopandas_tools/sfilter.py +17 -17
  18. sgis/helpers.py +116 -58
  19. sgis/io/dapla_functions.py +32 -23
  20. sgis/io/opener.py +13 -6
  21. sgis/io/read_parquet.py +2 -2
  22. sgis/maps/examine.py +55 -28
  23. sgis/maps/explore.py +471 -112
  24. sgis/maps/httpserver.py +12 -12
  25. sgis/maps/legend.py +285 -134
  26. sgis/maps/map.py +248 -129
  27. sgis/maps/maps.py +123 -119
  28. sgis/maps/thematicmap.py +260 -94
  29. sgis/maps/tilesources.py +3 -8
  30. sgis/networkanalysis/_get_route.py +5 -4
  31. sgis/networkanalysis/_od_cost_matrix.py +44 -1
  32. sgis/networkanalysis/_points.py +10 -4
  33. sgis/networkanalysis/_service_area.py +5 -2
  34. sgis/networkanalysis/closing_network_holes.py +22 -64
  35. sgis/networkanalysis/cutting_lines.py +58 -46
  36. sgis/networkanalysis/directednetwork.py +16 -8
  37. sgis/networkanalysis/finding_isolated_networks.py +6 -5
  38. sgis/networkanalysis/network.py +15 -13
  39. sgis/networkanalysis/networkanalysis.py +79 -61
  40. sgis/networkanalysis/networkanalysisrules.py +21 -17
  41. sgis/networkanalysis/nodes.py +2 -3
  42. sgis/networkanalysis/traveling_salesman.py +6 -3
  43. sgis/parallel/parallel.py +372 -142
  44. sgis/raster/base.py +9 -3
  45. sgis/raster/cube.py +331 -213
  46. sgis/raster/cubebase.py +15 -29
  47. sgis/raster/image_collection.py +2560 -0
  48. sgis/raster/indices.py +17 -12
  49. sgis/raster/raster.py +356 -275
  50. sgis/raster/sentinel_config.py +104 -0
  51. sgis/raster/zonal.py +38 -14
  52. {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/LICENSE +1 -1
  53. {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/METADATA +87 -16
  54. ssb_sgis-1.0.3.dist-info/RECORD +61 -0
  55. {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/WHEEL +1 -1
  56. sgis/raster/bands.py +0 -48
  57. sgis/raster/gradient.py +0 -78
  58. sgis/raster/methods_as_functions.py +0 -124
  59. sgis/raster/torchgeo.py +0 -150
  60. 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, Iterable
6
- from copy import copy, deepcopy
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
- pass
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, GeoSeries
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, Polygon, shape
43
-
44
- from ..geopandas_tools.conversion import to_bbox, to_gdf, to_shapely
45
- from ..geopandas_tools.general import is_bbox_like, is_wkt
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, NESSECARY_META, get_index_mapper, memfile_from_array
49
- from .gradient import get_gradient
50
- from .zonal import (
51
- _aggregate,
52
- _no_overlap_df,
53
- make_geometry_iterrows,
54
- prepare_zonal,
55
- zonal_post,
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
72
- --------
73
-
90
+ Examples:
91
+ ---------
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
@@ -83,8 +102,8 @@ class Raster:
83
102
  The array is stored in the array attribute.
84
103
 
85
104
  >>> raster.load()
86
- >>> raster.array[raster.array < 0] = 0
87
- >>> raster.array
105
+ >>> raster.values[raster.values < 0] = 0
106
+ >>> raster.values
88
107
  [[[ 0. 0. 0. ... 158.4 155.6 152.6]
89
108
  [ 0. 0. 0. ... 158. 154.8 151.9]
90
109
  [ 0. 0. 0. ... 158.5 155.1 152.3]
@@ -147,14 +166,8 @@ class Raster:
147
166
 
148
167
  """
149
168
 
150
- # attributes conserning file path
151
- filename_regex: str | None = None
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,58 @@ 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
- raster=None,
182
+ data: Self | str | np.ndarray | None = None,
175
183
  *,
176
- path: str | None = None,
177
- # indexes: int | list[int] | None = None,
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
- if raster is not None:
183
- if not isinstance(raster, Raster):
184
- raise TypeError(
185
- "Raster should be constructed with the classmethods (from_...)."
186
- )
187
- for key, value in raster.__dict__.items():
188
- setattr(raster, key, value)
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
+ warnings.warn("This class is deprecated in favor of Band", stacklevel=1)
198
+ self.filename_regex = filename_regex
199
+ if filename_regex:
200
+ self.filename_pattern = re.compile(self.filename_regex, re.VERBOSE)
201
+ else:
202
+ self.filename_pattern = None
203
+
204
+ if isinstance(data, Raster):
205
+ for key, value in data.__dict__.items():
206
+ setattr(data, key, value)
189
207
  return
190
208
 
191
- if path is None and not any([kwargs.get("transform"), kwargs.get("bounds")]):
209
+ if isinstance(data, (str | Path | os.PathLike)):
210
+ self.path = data
211
+
212
+ else:
213
+ self.path = None
214
+
215
+ if isinstance(data, (np.ndarray)):
216
+ self.values = data
217
+ else:
218
+ self.values = None
219
+
220
+ if self.path is None and not any(
221
+ [kwargs.get("transform"), kwargs.get("bounds")]
222
+ ):
192
223
  raise TypeError(
193
224
  "Must specify either bounds or transform when constructing raster from array."
194
225
  )
195
226
 
196
- # add class profile first to override with args and kwargs
227
+ # add class profile first, then override with args and kwargs
197
228
  self.update(**self._profile)
198
229
 
199
230
  self._crs = kwargs.pop("crs", self._crs if hasattr(self, "_crs") else None)
200
231
  self._bounds = None
201
-
202
- self.path = path
203
- self.array = array
204
232
  self.file_system = file_system
205
233
  self._indexes = self._get_indexes(kwargs.pop("indexes", self.indexes))
206
234
 
@@ -220,21 +248,28 @@ class Raster:
220
248
  cls,
221
249
  path: str,
222
250
  res: int | None = None,
223
- file_system=None,
251
+ file_system: GCSFileSystem | None = None,
252
+ filename_regex: str | None = None,
224
253
  **kwargs,
225
- ):
254
+ ) -> Self:
226
255
  """Construct Raster from file path.
227
256
 
228
257
  Args:
229
258
  path: Path to a raster image file.
259
+ res: Spatial resolution when reading the image.
260
+ file_system: Optional file system.
261
+ filename_regex: Regular expression with optional match groups.
262
+ **kwargs: Arguments concerning file metadata or
263
+ spatial properties of the image.
230
264
 
231
265
  Returns:
232
266
  A Raster instance.
233
267
  """
234
268
  return cls(
235
- path=str(path),
269
+ str(path),
236
270
  file_system=file_system,
237
271
  res=res,
272
+ filename_regex=filename_regex,
238
273
  **kwargs,
239
274
  )
240
275
 
@@ -242,19 +277,18 @@ class Raster:
242
277
  def from_array(
243
278
  cls,
244
279
  array: np.ndarray,
245
- crs,
280
+ crs: Any,
246
281
  *,
247
282
  transform: Affine | None = None,
248
283
  bounds: tuple | Geometry | None = None,
249
284
  copy: bool = True,
250
285
  **kwargs,
251
- ):
286
+ ) -> Self:
252
287
  """Construct Raster from numpy array.
253
288
 
254
- Metadata must be specified, either individually or in a dictionary
255
- (hint: use the 'meta' attribute of an existing Raster object if applicable).
256
- The necessary metadata is 'crs' and either 'transform' (Affine object) or 'bounds',
257
- which transform will then be created from.
289
+ Must also specify nessecary spatial properties
290
+ The necessary metadata is 'crs' and either 'transform' (Affine object)
291
+ or 'bounds', which transform will then be created from.
258
292
 
259
293
  Args:
260
294
  array: 2d or 3d numpy ndarray.
@@ -263,7 +297,9 @@ class Raster:
263
297
  of bounds.
264
298
  bounds: Minimum and maximum x and y coordinates. Can be specified instead
265
299
  of transform.
266
- name: Optional name to give the raster.
300
+ copy: Whether to copy the array.
301
+ **kwargs: Arguments concerning file metadata or
302
+ spatial properties of the image.
267
303
 
268
304
  Returns:
269
305
  A Raster instance.
@@ -295,7 +331,7 @@ class Raster:
295
331
 
296
332
  crs = pyproj.CRS(crs) if crs else None
297
333
 
298
- return cls(array=array, crs=crs, transform=transform, bounds=bounds, **kwargs)
334
+ return cls(array, crs=crs, transform=transform, bounds=bounds, **kwargs)
299
335
 
300
336
  @classmethod
301
337
  def from_gdf(
@@ -303,21 +339,38 @@ class Raster:
303
339
  gdf: GeoDataFrame,
304
340
  columns: str | Iterable[str],
305
341
  res: int,
306
- fill=0,
307
- all_touched=False,
308
- merge_alg=MergeAlg.replace,
309
- default_value=1,
310
- dtype=None,
342
+ fill: int = 0,
343
+ all_touched: bool = False,
344
+ merge_alg: Callable = MergeAlg.replace,
345
+ default_value: int = 1,
346
+ dtype: Any | None = None,
311
347
  **kwargs,
312
- ):
348
+ ) -> Self:
313
349
  """Construct Raster from a GeoDataFrame.
314
350
 
315
351
  Args:
316
- gdf: The GeoDataFrame.
317
- column: The column to be used as values for the array.
318
- res: Resolution of the raster in units of gdf's coordinate
319
- reference system.
352
+ gdf: The GeoDataFrame to rasterize.
353
+ columns: Column(s) in the GeoDataFrame whose values are used to populate the raster.
354
+ This can be a single column name or a list of column names.
355
+ res: Resolution of the raster in units of the GeoDataFrame's coordinate reference system.
356
+ fill: Fill value for areas outside of input geometries (default is 0).
357
+ all_touched: Whether to consider all pixels touched by geometries,
358
+ not just those whose center is within the polygon (default is False).
359
+ merge_alg: Merge algorithm to use when combining geometries
360
+ (default is 'MergeAlg.replace').
361
+ default_value: Default value to use for the rasterized pixels
362
+ (default is 1).
363
+ dtype: Data type of the output array. If None, it will be
364
+ determined automatically.
365
+ **kwargs: Additional keyword arguments passed to the raster
366
+ creation process, e.g., custom CRS or transform settings.
320
367
 
368
+ Returns:
369
+ A Raster instance based on the specified GeoDataFrame and parameters.
370
+
371
+ Raises:
372
+ TypeError: If 'transform' is provided in kwargs, as this is
373
+ computed based on the GeoDataFrame bounds and resolution.
321
374
  """
322
375
  if not isinstance(gdf, GeoDataFrame):
323
376
  gdf = to_gdf(gdf)
@@ -362,10 +415,10 @@ class Raster:
362
415
  assert len(array.shape) == 3
363
416
  name = kwargs.get("name", None)
364
417
 
365
- return cls.from_array(array=array, name=name, **kwargs)
418
+ return cls.from_array(array, name=name, **kwargs)
366
419
 
367
420
  @classmethod
368
- def from_dict(cls, dictionary: dict):
421
+ def from_dict(cls, dictionary: dict) -> Self:
369
422
  """Construct Raster from metadata dict to fastpass the initializer.
370
423
 
371
424
  This is the fastest way to create a Raster since a metadata lookup is not
@@ -387,6 +440,7 @@ class Raster:
387
440
  return cls(**dictionary)
388
441
 
389
442
  def update(self, **kwargs) -> Self:
443
+ """Update attributes of the Raster."""
390
444
  for key, value in kwargs.items():
391
445
  self._validate_key(key)
392
446
  if is_property(self, key):
@@ -394,7 +448,9 @@ class Raster:
394
448
  setattr(self, key, value)
395
449
  return self
396
450
 
397
- def write(self, path: str, window=None, **kwargs) -> None:
451
+ def write(
452
+ self, path: str, window: rasterio.windows.Window | None = None, **kwargs
453
+ ) -> None:
398
454
  """Write the raster as a single file.
399
455
 
400
456
  Multiband arrays will result in a multiband image file.
@@ -402,26 +458,29 @@ class Raster:
402
458
  Args:
403
459
  path: File path to write to.
404
460
  window: Optional window to clip the image to.
461
+ **kwargs: Keyword arguments passed to rasterio.open.
462
+ Thise will override the items in the Raster's profile,
463
+ if overlapping.
405
464
  """
406
-
407
- if self.array is None:
465
+ if self.values is None:
408
466
  raise AttributeError("The image hasn't been loaded.")
409
467
 
410
468
  profile = self.profile | kwargs
411
469
 
412
- with opener(path, file_system=self.file_system) as file:
470
+ with opener(path, "wb", file_system=self.file_system) as file:
413
471
  with rasterio.open(file, "w", **profile) as dst:
414
472
  self._write(dst, window)
415
473
 
416
474
  self.path = str(path)
417
475
 
418
- def load(self, **kwargs) -> Self:
476
+ def load(self, reload: bool = False, **kwargs) -> Self:
419
477
  """Load the entire image as an np.array.
420
478
 
421
479
  The array is stored in the 'array' attribute
422
480
  of the Raster.
423
481
 
424
482
  Args:
483
+ reload: Whether to reload the array if already loaded.
425
484
  **kwargs: Keyword arguments passed to the rasterio read
426
485
  method.
427
486
  """
@@ -430,13 +489,14 @@ class Raster:
430
489
  if "window" in kwargs:
431
490
  raise ValueError("Got an unexpected keyword argument 'window'")
432
491
 
433
- self._read_tif(**kwargs)
492
+ if reload or self.values is None:
493
+ self._read_tif(**kwargs)
434
494
 
435
495
  return self
436
496
 
437
497
  def clip(
438
498
  self,
439
- mask,
499
+ mask: Any,
440
500
  masked: bool = False,
441
501
  boundless: bool = True,
442
502
  **kwargs,
@@ -448,6 +508,13 @@ class Raster:
448
508
 
449
509
  Args:
450
510
  mask: Geometry-like object or bounding box.
511
+ masked: If 'masked' is True the return value will be a masked
512
+ array. Otherwise (default) the return value will be a
513
+ regular array. Masks will be exactly the inverse of the
514
+ GDAL RFC 15 conforming arrays returned by read_masks().
515
+ boundless: If True, windows that extend beyond the dataset's extent
516
+ are permitted and partially or completely filled arrays will
517
+ be returned as appropriate.
451
518
  **kwargs: Keyword arguments passed to the mask function
452
519
  from the rasterio.mask module.
453
520
 
@@ -462,17 +529,18 @@ class Raster:
462
529
  except ValueError:
463
530
  mask = mask.set_crs(self.crs)
464
531
 
465
- # if not self.crs.equals(pyproj.CRS(mask.crs)):
466
- # raise ValueError("crs mismatch.")
467
-
468
532
  self._read_with_mask(mask=mask, masked=masked, boundless=boundless, **kwargs)
469
533
 
470
534
  return self
471
535
 
472
- def intersects(self, other) -> bool:
536
+ def intersects(self, other: Any) -> bool:
537
+ """Returns True if the image bounds intersect with 'other'."""
473
538
  return self.unary_union.intersects(to_shapely(other))
474
539
 
475
- def sample(self, n=1, size=20, mask=None, copy=True, **kwargs) -> Self:
540
+ def sample(
541
+ self, n: int = 1, size: int = 20, mask: Any = None, copy: bool = True, **kwargs
542
+ ) -> Self:
543
+ """Take a random spatial sample of the image."""
476
544
  if mask is not None:
477
545
  points = GeoSeries(self.unary_union).clip(mask).sample_points(n)
478
546
  else:
@@ -508,19 +576,21 @@ class Raster:
508
576
  A GeoDataFrame with aggregated values per polygon.
509
577
  """
510
578
  idx_mapper, idx_name = get_index_mapper(polygons)
511
- polygons, aggfunc, func_names = prepare_zonal(polygons, aggfunc)
512
- poly_iter = make_geometry_iterrows(polygons)
579
+ polygons, aggfunc, func_names = _prepare_zonal(polygons, aggfunc)
580
+ poly_iter = _make_geometry_iterrows(polygons)
513
581
 
514
582
  aggregated = []
515
583
  for i, poly in poly_iter:
516
584
  clipped = self.clip(poly)
517
- if not np.size(clipped.array):
585
+ if not np.size(clipped.values):
518
586
  aggregated.append(_no_overlap_df(func_names, i, date=self.date))
519
587
  aggregated.append(
520
- _aggregate(clipped.array, array_func, aggfunc, func_names, self.date, i)
588
+ _aggregate(
589
+ clipped.values, array_func, aggfunc, func_names, self.date, i
590
+ )
521
591
  )
522
592
 
523
- return zonal_post(
593
+ return _zonal_post(
524
594
  aggregated,
525
595
  polygons=polygons,
526
596
  idx_mapper=idx_mapper,
@@ -528,73 +598,23 @@ class Raster:
528
598
  dropna=dropna,
529
599
  )
530
600
 
531
- def gradient(self, degrees: bool = False, copy: bool = False) -> Self:
532
- """Get the slope of an elevation raster.
533
-
534
- Calculates the absolute slope between the grid cells
535
- based on the image resolution.
536
-
537
- For multiband images, the calculation is done for each band.
538
-
539
- Args:
540
- degrees: If False (default), the returned values will be in ratios,
541
- where a value of 1 means 1 meter up per 1 meter forward. If True,
542
- the values will be in degrees from 0 to 90.
543
- copy: Whether to copy or overwrite the original Raster.
544
- Defaults to False to save memory.
545
-
546
- Returns:
547
- The class instance with new array values, or a copy if copy is True.
548
-
549
- Examples
550
- --------
551
- Making an array where the gradient to the center is always 10.
552
-
553
- >>> import sgis as sg
554
- >>> import numpy as np
555
- >>> arr = np.array(
556
- ... [
557
- ... [100, 100, 100, 100, 100],
558
- ... [100, 110, 110, 110, 100],
559
- ... [100, 110, 120, 110, 100],
560
- ... [100, 110, 110, 110, 100],
561
- ... [100, 100, 100, 100, 100],
562
- ... ]
563
- ... )
564
-
565
- Now let's create a Raster from this array with a resolution of 10.
566
-
567
- >>> r = sg.Raster.from_array(arr, crs=None, bounds=(0, 0, 50, 50))
568
-
569
- The gradient will be 1 (1 meter up for every meter forward).
570
- The calculation is by default done in place to save memory.
571
-
572
- >>> r.gradient()
573
- >>> r.array
574
- array([[0., 1., 1., 1., 0.],
575
- [1., 1., 1., 1., 1.],
576
- [1., 1., 0., 1., 1.],
577
- [1., 1., 1., 1., 1.],
578
- [0., 1., 1., 1., 0.]])
579
- """
580
- return get_gradient(self, degrees=degrees, copy=copy)
581
-
582
601
  def to_xarray(self) -> DataArray:
602
+ """Convert the raster to an xarray.DataArray."""
583
603
  self._check_for_array()
584
604
  self.name = self.name or self.__class__.__name__.lower()
585
605
  coords = _generate_spatial_coords(self.transform, self.width, self.height)
586
- if len(self.array.shape) == 2:
606
+ if len(self.values.shape) == 2:
587
607
  dims = ["y", "x"]
588
608
  # dims = ["band", "y", "x"]
589
- # array = np.array([self.array])
609
+ # array = np.array([self.values])
590
610
  # assert len(array.shape) == 3
591
- elif len(self.array.shape) == 3:
611
+ elif len(self.values.shape) == 3:
592
612
  dims = ["band", "y", "x"]
593
- # array = self.array
613
+ # array = self.values
594
614
  else:
595
615
  raise ValueError("Array must be 2 or 3 dimensional.")
596
616
  return xr.DataArray(
597
- self.array,
617
+ self.values,
598
618
  coords=coords,
599
619
  dims=dims,
600
620
  name=self.name,
@@ -602,6 +622,7 @@ class Raster:
602
622
  ) # .transpose("y", "x")
603
623
 
604
624
  def to_dict(self) -> dict:
625
+ """Get a dictionary of Raster attributes."""
605
626
  out = {}
606
627
  for col in self.ALL_ATTRS:
607
628
  try:
@@ -642,11 +663,11 @@ class Raster:
642
663
  column = [column] * len(array_list)
643
664
 
644
665
  gdfs = []
645
- for i, (column, array) in enumerate(zip(column, array_list, strict=True)):
666
+ for i, (col, array) in enumerate(zip(column, array_list, strict=True)):
646
667
  gdf = gpd.GeoDataFrame(
647
668
  pd.DataFrame(
648
669
  self._array_to_geojson(array, self.transform),
649
- columns=[column, "geometry"],
670
+ columns=[col, "geometry"],
650
671
  ),
651
672
  geometry="geometry",
652
673
  crs=self.crs,
@@ -658,20 +679,20 @@ class Raster:
658
679
 
659
680
  def set_crs(
660
681
  self,
661
- crs,
682
+ crs: pyproj.CRS | Any,
662
683
  allow_override: bool = False,
663
684
  ) -> Self:
664
685
  """Set coordinate reference system."""
665
686
  if not allow_override and self.crs is not None:
666
687
  raise ValueError("Cannot overwrite crs when allow_override is False.")
667
688
 
668
- if self.array is None:
689
+ if self.values is None:
669
690
  raise ValueError("array must be loaded/clipped before set_crs")
670
691
 
671
692
  self._crs = pyproj.CRS(crs)
672
693
  return self
673
694
 
674
- def to_crs(self, crs, **kwargs) -> Self:
695
+ def to_crs(self, crs: pyproj.CRS | Any, **kwargs) -> Self:
675
696
  """Reproject the raster.
676
697
 
677
698
  Args:
@@ -687,7 +708,7 @@ class Raster:
687
708
  # ):
688
709
  # return self
689
710
 
690
- if self.array is None:
711
+ if self.values is None:
691
712
  project = pyproj.Transformer.from_crs(
692
713
  pyproj.CRS(self._prev_crs), pyproj.CRS(crs), always_xy=True
693
714
  ).transform
@@ -719,16 +740,16 @@ class Raster:
719
740
  # self._bounds = shapely.transform(old_box, project)
720
741
  else:
721
742
  was_2d = len(self.shape) == 2
722
- self.array, transform = reproject(
723
- source=self.array,
743
+ self.values, transform = reproject(
744
+ source=self.values,
724
745
  src_crs=self._prev_crs,
725
746
  src_transform=self.transform,
726
747
  dst_crs=pyproj.CRS(crs),
727
748
  **kwargs,
728
749
  )
729
- if was_2d and len(self.array.shape) == 3:
730
- assert self.array.shape[0] == 1
731
- self.array = self.array[0]
750
+ if was_2d and len(self.values.shape) == 3:
751
+ assert self.values.shape[0] == 1
752
+ self.values = self.values[0]
732
753
 
733
754
  self._bounds = rasterio.transform.array_bounds(
734
755
  self.height, self.width, transform
@@ -739,18 +760,18 @@ class Raster:
739
760
 
740
761
  return self
741
762
 
742
- def plot(self, mask=None) -> None:
743
- self._check_for_array()
763
+ def plot(self, mask: Any | None = None) -> None:
744
764
  """Plot the images. One image per band."""
765
+ self._check_for_array()
745
766
  if mask is not None:
746
767
  raster = self.copy().clip(mask)
747
768
  else:
748
769
  raster = self
749
770
 
750
771
  if len(raster.shape) == 2:
751
- array = np.array([raster.array])
772
+ array = np.array([raster.values])
752
773
  else:
753
- array = raster.array
774
+ array = raster.values
754
775
 
755
776
  for arr in array:
756
777
  ax = plt.axes()
@@ -760,28 +781,32 @@ class Raster:
760
781
  plt.close()
761
782
 
762
783
  def astype(self, dtype: type) -> Self:
763
- if self.array is None:
784
+ """Convert the datatype of the array."""
785
+ if self.values is None:
764
786
  raise ValueError("Array is not loaded.")
765
- if not rasterio.dtypes.can_cast_dtype(self.array, dtype):
766
- min_dtype = rasterio.dtypes.get_minimum_dtype(self.array)
787
+ if not rasterio.dtypes.can_cast_dtype(self.values, dtype):
788
+ min_dtype = rasterio.dtypes.get_minimum_dtype(self.values)
767
789
  raise ValueError(f"Cannot cast to dtype. Minimum dtype is {min_dtype}")
768
- self.array = self.array.astype(dtype)
790
+ self.values = self.values.astype(dtype)
769
791
  self._dtype = dtype
770
792
  return self
771
793
 
772
794
  def as_minimum_dtype(self) -> Self:
773
- min_dtype = rasterio.dtypes.get_minimum_dtype(self.array)
774
- self.array = self.array.astype(min_dtype)
795
+ """Convert the array to the minimum dtype without overflow."""
796
+ min_dtype = rasterio.dtypes.get_minimum_dtype(self.values)
797
+ self.values = self.values.astype(min_dtype)
775
798
  return self
776
799
 
777
800
  def min(self) -> int | None:
778
- if np.size(self.array):
779
- return np.min(self.array)
801
+ """Minimum value in the array."""
802
+ if np.size(self.values):
803
+ return np.min(self.values)
780
804
  return None
781
805
 
782
806
  def max(self) -> int | None:
783
- if np.size(self.array):
784
- return np.max(self.array)
807
+ """Maximum value in the array."""
808
+ if np.size(self.values):
809
+ return np.max(self.values)
785
810
  return None
786
811
 
787
812
  def _add_meta(self) -> Self:
@@ -798,20 +823,23 @@ class Raster:
798
823
  return self
799
824
 
800
825
  def array_list(self) -> list[np.ndarray]:
826
+ """Get a list of 2D arrays."""
801
827
  self._check_for_array()
802
- if len(self.array.shape) == 2:
803
- return [self.array]
804
- elif len(self.array.shape) == 3:
805
- return list(self.array)
828
+ if len(self.values.shape) == 2:
829
+ return [self.values]
830
+ elif len(self.values.shape) == 3:
831
+ return list(self.values)
806
832
  else:
807
833
  raise ValueError
808
834
 
809
835
  @property
810
836
  def indexes(self) -> int | tuple[int] | None:
837
+ """Band indexes of the image."""
811
838
  return self._indexes
812
839
 
813
840
  @property
814
841
  def name(self) -> str | None:
842
+ """Name of the file in the file path, if any."""
815
843
  try:
816
844
  return self._name
817
845
  except AttributeError:
@@ -821,35 +849,30 @@ class Raster:
821
849
  return None
822
850
 
823
851
  @name.setter
824
- def name(self, value):
852
+ def name(self, value) -> None:
825
853
  self._name = value
826
- return self._name
827
854
 
828
855
  @property
829
- def date(self):
856
+ def date(self) -> str | None:
857
+ """Date in the image file name, if filename_regex is present."""
830
858
  try:
831
- pattern = re.compile(self.filename_regex, re.VERBOSE)
832
- return re.match(pattern, Path(self.path).name).group("date")
859
+ return re.match(self.filename_pattern, Path(self.path).name).group("date")
833
860
  except (AttributeError, TypeError):
834
861
  return None
835
862
 
836
863
  @property
837
864
  def band(self) -> str | None:
865
+ """Band name of the image file name, if filename_regex is present."""
838
866
  try:
839
- pattern = re.compile(self.filename_regex, re.VERBOSE)
840
- return re.match(pattern, Path(self.path).name).group("band")
867
+ return re.match(self.filename_pattern, Path(self.path).name).group("band")
841
868
  except (AttributeError, TypeError):
842
869
  return None
843
870
 
844
- # @property
845
- # def band_color(self):
846
- # """To be implemented in subclasses."""
847
- # pass
848
-
849
871
  @property
850
- def dtype(self):
872
+ def dtype(self) -> Any:
873
+ """Data type of the array."""
851
874
  try:
852
- return self.array.dtype
875
+ return self.values.dtype
853
876
  except AttributeError:
854
877
  try:
855
878
  return self._dtype
@@ -857,12 +880,12 @@ class Raster:
857
880
  return None
858
881
 
859
882
  @dtype.setter
860
- def dtype(self, new_dtype):
861
- self.array = self.array.astype(new_dtype)
862
- return self.array.dtype
883
+ def dtype(self, new_dtype: Any) -> None:
884
+ self.values = self.values.astype(new_dtype)
863
885
 
864
886
  @property
865
887
  def nodata(self) -> int | None:
888
+ """No data value."""
866
889
  try:
867
890
  return self._nodata
868
891
  except AttributeError:
@@ -870,12 +893,15 @@ class Raster:
870
893
 
871
894
  @property
872
895
  def tile(self) -> str | None:
873
- if self.bounds is None:
896
+ """Tile name from regex."""
897
+ try:
898
+ return re.match(self.filename_pattern, Path(self.path).name).group("tile")
899
+ except (AttributeError, TypeError):
874
900
  return None
875
- return f"{int(self.bounds[0])}_{int(self.bounds[1])}"
876
901
 
877
902
  @property
878
903
  def meta(self) -> dict:
904
+ """Metadata dict."""
879
905
  return {
880
906
  "path": self.path,
881
907
  "type": self.__class__.__name__,
@@ -886,6 +912,7 @@ class Raster:
886
912
 
887
913
  @property
888
914
  def profile(self) -> dict:
915
+ """Profile of the image file."""
889
916
  # TODO: .crs blir feil hvis warpa. Eller?
890
917
  return {
891
918
  "driver": self.driver,
@@ -902,14 +929,16 @@ class Raster:
902
929
 
903
930
  @property
904
931
  def read_kwargs(self) -> dict:
932
+ """Keywords passed to the read method of rasterio.io.DatasetReader."""
905
933
  return {
906
934
  "indexes": self.indexes,
907
935
  "fill_value": self.nodata,
908
- "masked": True,
936
+ "masked": False,
909
937
  }
910
938
 
911
939
  @property
912
940
  def res(self) -> float | None:
941
+ """Get the spatial resolution of the image."""
913
942
  if hasattr(self, "_res") and self._res is not None:
914
943
  return self._res
915
944
  if self.width is None:
@@ -919,17 +948,19 @@ class Raster:
919
948
 
920
949
  @property
921
950
  def height(self) -> int | None:
922
- if self.array is None:
951
+ """Get the height of the image as number of pixels."""
952
+ if self.values is None:
923
953
  try:
924
954
  return self._height
925
955
  except AttributeError:
926
956
  return None
927
- i = 1 if len(self.array.shape) == 3 else 0
928
- return self.array.shape[i]
957
+ i = 1 if len(self.values.shape) == 3 else 0
958
+ return self.values.shape[i]
929
959
 
930
960
  @property
931
961
  def width(self) -> int | None:
932
- if self.array is None:
962
+ """Get the width of the image as number of pixels."""
963
+ if self.values is None:
933
964
  try:
934
965
  return self._width
935
966
  except AttributeError:
@@ -940,15 +971,16 @@ class Raster:
940
971
  return self._width
941
972
  except Exception:
942
973
  return None
943
- i = 2 if len(self.array.shape) == 3 else 1
944
- return self.array.shape[i]
974
+ i = 2 if len(self.values.shape) == 3 else 1
975
+ return self.values.shape[i]
945
976
 
946
977
  @property
947
978
  def count(self) -> int:
948
- if self.array is not None:
949
- if len(self.array.shape) == 3:
950
- return self.array.shape[0]
951
- if len(self.array.shape) == 2:
979
+ """Get the number of bands in the image."""
980
+ if self.values is not None:
981
+ if len(self.values.shape) == 3:
982
+ return self.values.shape[0]
983
+ if len(self.values.shape) == 2:
952
984
  return 1
953
985
  if not hasattr(self._indexes, "__iter__"):
954
986
  return 1
@@ -957,14 +989,15 @@ class Raster:
957
989
  @property
958
990
  def shape(self) -> tuple[int]:
959
991
  """Shape that is consistent with the array, whether it is loaded or not."""
960
- if self.array is not None:
961
- return self.array.shape
992
+ if self.values is not None:
993
+ return self.values.shape
962
994
  if hasattr(self._indexes, "__iter__"):
963
995
  return self.count, self.width, self.height
964
996
  return self.width, self.height
965
997
 
966
998
  @property
967
999
  def transform(self) -> Affine | None:
1000
+ """Get the Affine transform of the image."""
968
1001
  try:
969
1002
  return rasterio.transform.from_bounds(*self.bounds, self.width, self.height)
970
1003
  except (ZeroDivisionError, TypeError):
@@ -973,13 +1006,15 @@ class Raster:
973
1006
 
974
1007
  @property
975
1008
  def bounds(self) -> tuple[float, float, float, float] | None:
1009
+ """Get the bounds of the image."""
976
1010
  try:
977
1011
  return to_bbox(self._bounds)
978
1012
  except (AttributeError, TypeError):
979
1013
  return None
980
1014
 
981
1015
  @property
982
- def crs(self):
1016
+ def crs(self) -> pyproj.CRS | None:
1017
+ """Get the coordinate reference system of the image."""
983
1018
  try:
984
1019
  return self._warped_crs
985
1020
  except AttributeError:
@@ -990,24 +1025,29 @@ class Raster:
990
1025
 
991
1026
  @property
992
1027
  def area(self) -> float:
1028
+ """Get the area of the image."""
993
1029
  return shapely.area(self.unary_union)
994
1030
 
995
1031
  @property
996
1032
  def length(self) -> float:
1033
+ """Get the circumfence of the image."""
997
1034
  return shapely.length(self.unary_union)
998
1035
 
999
1036
  @property
1000
1037
  def unary_union(self) -> Polygon:
1038
+ """Get the image bounds as a Polygon."""
1001
1039
  return shapely.box(*self.bounds)
1002
1040
 
1003
1041
  @property
1004
1042
  def centroid(self) -> Point:
1043
+ """Get the centerpoint of the image."""
1005
1044
  x = (self.bounds[0] + self.bounds[2]) / 2
1006
1045
  y = (self.bounds[1] + self.bounds[3]) / 2
1007
1046
  return Point(x, y)
1008
1047
 
1009
1048
  @property
1010
1049
  def properties(self) -> list[str]:
1050
+ """List of all properties of the class."""
1011
1051
  out = []
1012
1052
  for attr in dir(self):
1013
1053
  try:
@@ -1018,11 +1058,12 @@ class Raster:
1018
1058
  return out
1019
1059
 
1020
1060
  def indexes_as_tuple(self) -> tuple[int, ...]:
1061
+ """Get the band index(es) as a tuple of integers."""
1021
1062
  if len(self.shape) == 2:
1022
1063
  return (1,)
1023
1064
  return tuple(i + 1 for i in range(self.shape[0]))
1024
1065
 
1025
- def copy(self, deep=True):
1066
+ def copy(self, deep: bool = True) -> "Raster":
1026
1067
  """Returns a (deep) copy of the class instance.
1027
1068
 
1028
1069
  Args:
@@ -1033,14 +1074,15 @@ class Raster:
1033
1074
  else:
1034
1075
  return copy(self)
1035
1076
 
1036
- def equals(self, other) -> bool:
1077
+ def equals(self, other: Any) -> bool:
1078
+ """Check if the Raster is equal to another Raster."""
1037
1079
  if not isinstance(other, Raster):
1038
1080
  raise NotImplementedError("other must be of type Raster")
1039
1081
  if type(other) != type(self):
1040
1082
  return False
1041
- if self.array is None and other.array is not None:
1083
+ if self.values is None and other.values is not None:
1042
1084
  return False
1043
- if self.array is not None and other.array is None:
1085
+ if self.values is not None and other.values is None:
1044
1086
  return False
1045
1087
 
1046
1088
  for method in dir(self):
@@ -1049,7 +1091,7 @@ class Raster:
1049
1091
  if getattr(self, method) != getattr(other, method):
1050
1092
  return False
1051
1093
 
1052
- return np.array_equal(self.array, other.array)
1094
+ return np.array_equal(self.values, other.values)
1053
1095
 
1054
1096
  def __repr__(self) -> str:
1055
1097
  """The print representation."""
@@ -1059,39 +1101,55 @@ class Raster:
1059
1101
  res = int(self.res)
1060
1102
  except TypeError:
1061
1103
  res = None
1062
- return f"{self.__class__.__name__}(shape=({shp}), res={res}, name={self.name}, path={self.path})"
1104
+ return f"{self.__class__.__name__}(shape=({shp}), res={res}, band={self.band})"
1105
+
1106
+ def __iter__(self) -> Iterator[np.ndarray]:
1107
+ """Iterate over the arrays."""
1108
+ if len(self.values.shape) == 2:
1109
+ return iter([self.values])
1110
+ if len(self.values.shape) == 3:
1111
+ return iter(self.values)
1112
+ raise ValueError(
1113
+ f"Array should have shape length 2 or 3. Got {len(self.values.shape)}"
1114
+ )
1063
1115
 
1064
- def __mul__(self, scalar):
1116
+ def __mul__(self, scalar: int | float) -> "Raster":
1117
+ """Multiply the array values with *."""
1065
1118
  self._check_for_array()
1066
- self.array = self.array * scalar
1119
+ self.values = self.values * scalar
1067
1120
  return self
1068
1121
 
1069
- def __add__(self, scalar):
1122
+ def __add__(self, scalar: int | float) -> "Raster":
1123
+ """Add to the array values with +."""
1070
1124
  self._check_for_array()
1071
- self.array = self.array + scalar
1125
+ self.values = self.values + scalar
1072
1126
  return self
1073
1127
 
1074
- def __sub__(self, scalar):
1128
+ def __sub__(self, scalar: int | float) -> "Raster":
1129
+ """Subtract the array values with -."""
1075
1130
  self._check_for_array()
1076
- self.array = self.array - scalar
1131
+ self.values = self.values - scalar
1077
1132
  return self
1078
1133
 
1079
- def __truediv__(self, scalar):
1134
+ def __truediv__(self, scalar: int | float) -> "Raster":
1135
+ """Divide the array values with /."""
1080
1136
  self._check_for_array()
1081
- self.array = self.array / scalar
1137
+ self.values = self.values / scalar
1082
1138
  return self
1083
1139
 
1084
- def __floordiv__(self, scalar):
1140
+ def __floordiv__(self, scalar: int | float) -> "Raster":
1141
+ """Floor divide the array values with //."""
1085
1142
  self._check_for_array()
1086
- self.array = self.array // scalar
1143
+ self.values = self.values // scalar
1087
1144
  return self
1088
1145
 
1089
- def __pow__(self, exponent):
1146
+ def __pow__(self, exponent: int | float) -> "Raster":
1147
+ """Exponentiate the array values with **."""
1090
1148
  self._check_for_array()
1091
- self.array = self.array**exponent
1149
+ self.values = self.values**exponent
1092
1150
  return self
1093
1151
 
1094
- def _has_nessecary_attrs(self, dict_like) -> bool:
1152
+ def _has_nessecary_attrs(self, dict_like: dict) -> bool:
1095
1153
  """Check if Raster init got enough kwargs to not need to read src."""
1096
1154
  try:
1097
1155
  self._validate_dict(dict_like)
@@ -1101,17 +1159,17 @@ class Raster:
1101
1159
  except AttributeError:
1102
1160
  return False
1103
1161
 
1104
- def _return_self_or_copy(self, array, copy: bool):
1162
+ def _return_self_or_copy(self, array: np.ndarray, copy: bool) -> "Raster":
1105
1163
  if not copy:
1106
- self.array = array
1164
+ self.values = array
1107
1165
  return self
1108
1166
  else:
1109
1167
  copy = self.copy()
1110
- copy.array = array
1168
+ copy.values = array
1111
1169
  return copy
1112
1170
 
1113
1171
  @classmethod
1114
- def _validate_dict(cls, dict_like) -> None:
1172
+ def _validate_dict(cls, dict_like: dict) -> None:
1115
1173
  missing = []
1116
1174
  for attr in NESSECARY_META:
1117
1175
  if any(
@@ -1127,14 +1185,14 @@ class Raster:
1127
1185
  raise AttributeError(f"Missing nessecary key(s) {', '.join(missing)}")
1128
1186
 
1129
1187
  @classmethod
1130
- def _validate_key(cls, key) -> None:
1188
+ def _validate_key(cls, key: str) -> None:
1131
1189
  if key not in ALLOWED_KEYS:
1132
1190
  raise ValueError(
1133
1191
  f"Got an unexpected key {key!r}. Allowed keys are ",
1134
1192
  ", ".join(ALLOWED_KEYS),
1135
1193
  )
1136
1194
 
1137
- def _get_shape_from_res(self, res) -> tuple[int] | None:
1195
+ def _get_shape_from_res(self, res: int) -> tuple[int] | None:
1138
1196
  if res is None:
1139
1197
  return None
1140
1198
  if hasattr(res, "__iter__") and len(res) == 2:
@@ -1147,36 +1205,38 @@ class Raster:
1147
1205
  return len(self.indexes), width, height
1148
1206
  return width, height
1149
1207
 
1150
- def _write(self, dst, window):
1151
- if np.ma.is_masked(self.array):
1152
- if len(self.array.shape) == 2:
1208
+ def _write(
1209
+ self, dst: rasterio.io.DatasetReader, window: rasterio.windows.Window
1210
+ ) -> None:
1211
+ if np.ma.is_masked(self.values):
1212
+ if len(self.values.shape) == 2:
1153
1213
  return dst.write(
1154
- self.array.filled(self.nodata), indexes=1, window=window
1214
+ self.values.filled(self.nodata), indexes=1, window=window
1155
1215
  )
1156
1216
 
1157
1217
  for i in range(len(self.indexes_as_tuple())):
1158
1218
  dst.write(
1159
- self.array[i].filled(self.nodata),
1219
+ self.values[i].filled(self.nodata),
1160
1220
  indexes=i + 1,
1161
1221
  window=window,
1162
1222
  )
1163
1223
 
1164
1224
  else:
1165
- if len(self.array.shape) == 2:
1166
- return dst.write(self.array, indexes=1, window=window)
1225
+ if len(self.values.shape) == 2:
1226
+ return dst.write(self.values, indexes=1, window=window)
1167
1227
 
1168
1228
  for i, idx in enumerate(self.indexes_as_tuple()):
1169
- dst.write(self.array[i], indexes=idx, window=window)
1229
+ dst.write(self.values[i], indexes=idx, window=window)
1170
1230
 
1171
- def _get_indexes(self, indexes):
1231
+ def _get_indexes(self, indexes: int | tuple[int] | None) -> int | tuple[int] | None:
1172
1232
  if isinstance(indexes, numbers.Number):
1173
1233
  return int(indexes)
1174
1234
  if indexes is None:
1175
- if self.array is not None and len(self.array.shape) == 3:
1176
- return tuple(i + 1 for i in range(self.array.shape[0]))
1177
- elif self.array is not None and len(self.array.shape) == 2:
1235
+ if self.values is not None and len(self.values.shape) == 3:
1236
+ return tuple(i + 1 for i in range(self.values.shape[0]))
1237
+ elif self.values is not None and len(self.values.shape) == 2:
1178
1238
  return 1
1179
- elif self.array is not None:
1239
+ elif self.values is not None:
1180
1240
  raise ValueError("Array must be 2 or 3 dimensional.")
1181
1241
  else:
1182
1242
  return None
@@ -1188,7 +1248,7 @@ class Raster:
1188
1248
  f"Got {type(indexes)}: {indexes}"
1189
1249
  ) from e
1190
1250
 
1191
- def _return_gdf(self, obj) -> GeoDataFrame:
1251
+ def _return_gdf(self, obj: Any) -> GeoDataFrame:
1192
1252
  if isinstance(obj, str) and not is_wkt(obj):
1193
1253
  return self._read_tif(obj)
1194
1254
  elif isinstance(obj, Raster):
@@ -1210,34 +1270,42 @@ class Raster:
1210
1270
  warnings.filterwarnings("ignore", category=UserWarning)
1211
1271
  return [
1212
1272
  (feature["geometry"], val)
1213
- for val, feature in zip(gdf[column], loads(gdf.to_json())["features"])
1273
+ for val, feature in zip(
1274
+ gdf[column], loads(gdf.to_json())["features"], strict=False
1275
+ )
1214
1276
  ]
1215
1277
 
1216
1278
  @staticmethod
1217
- def _array_to_geojson(array: np.ndarray, transform: Affine):
1279
+ def _array_to_geojson(array: np.ndarray, transform: Affine) -> list[tuple]:
1280
+ if np.ma.is_masked(array):
1281
+ array = array.data
1218
1282
  try:
1219
1283
  return [
1220
1284
  (value, shape(geom))
1221
- for geom, value in features.shapes(array, transform=transform)
1285
+ for geom, value in features.shapes(
1286
+ array, transform=transform, mask=None
1287
+ )
1222
1288
  ]
1223
1289
  except ValueError:
1224
1290
  array = array.astype(np.float32)
1225
1291
  return [
1226
1292
  (value, shape(geom))
1227
- for geom, value in features.shapes(array, transform=transform)
1293
+ for geom, value in features.shapes(
1294
+ array, transform=transform, mask=None
1295
+ )
1228
1296
  ]
1229
1297
 
1230
- def _add_indexes_from_array(self, indexes):
1298
+ def _add_indexes_from_array(self, indexes: int | tuple[int]) -> int | tuple[int]:
1231
1299
  if indexes is not None:
1232
1300
  return indexes
1233
- elif len(self.array.shape) == 3:
1234
- return tuple(x + 1 for x in range(len(self.array)))
1235
- elif len(self.array.shape) == 2:
1301
+ elif len(self.values.shape) == 3:
1302
+ return tuple(x + 1 for x in range(len(self.values)))
1303
+ elif len(self.values.shape) == 2:
1236
1304
  return 1
1237
1305
  else:
1238
1306
  raise ValueError
1239
1307
 
1240
- def _add_meta_from_src(self, src):
1308
+ def _add_meta_from_src(self, src: rasterio.io.DatasetReader) -> None:
1241
1309
  if not hasattr(self, "_bounds") or self._bounds is None:
1242
1310
  self._bounds = tuple(src.bounds)
1243
1311
 
@@ -1259,10 +1327,15 @@ class Raster:
1259
1327
  # except AttributeError:
1260
1328
  # pass
1261
1329
 
1262
- for attr in ["_indexes", "_nodata"]:
1263
- if not hasattr(self, attr) or getattr(self, attr) is None:
1264
- new_value = getattr(src, attr.replace("_", ""))
1265
- setattr(self, attr, new_value)
1330
+ if not hasattr(self, "_indexes") or self._indexes is None:
1331
+ new_value = src.indexes
1332
+ if new_value == 1 or new_value == (1,):
1333
+ new_value = 1
1334
+ self._indexes = new_value
1335
+
1336
+ if not hasattr(self, "_nodata") or self._nodata is None:
1337
+ new_value = src.nodata
1338
+ self._nodata = new_value
1266
1339
 
1267
1340
  # if not hasattr(self, "_indexes") or self._indexes is None:
1268
1341
  # self._indexes = src.indexes
@@ -1293,7 +1366,7 @@ class Raster:
1293
1366
  return self._read(self.path, **kwargs)
1294
1367
 
1295
1368
  @functools.lru_cache(maxsize=128)
1296
- def _read(self, path, **kwargs):
1369
+ def _read(self, path: str | Path, **kwargs) -> None:
1297
1370
  with opener(path, file_system=self.file_system) as file:
1298
1371
  with rasterio.open(file) as src:
1299
1372
  self._add_meta_from_src(src)
@@ -1302,7 +1375,7 @@ class Raster:
1302
1375
  if hasattr(self, "_warped_crs"):
1303
1376
  src = WarpedVRT(src, crs=self.crs)
1304
1377
 
1305
- self.array = src.read(
1378
+ self.values = src.read(
1306
1379
  out_shape=out_shape,
1307
1380
  **(self.read_kwargs | kwargs),
1308
1381
  )
@@ -1311,7 +1384,9 @@ class Raster:
1311
1384
  else:
1312
1385
  self = self.as_minimum_dtype()
1313
1386
 
1314
- def _read_with_mask(self, mask, masked, boundless, **kwargs):
1387
+ def _read_with_mask(
1388
+ self, mask: Any, masked: bool, boundless: bool, **kwargs
1389
+ ) -> None:
1315
1390
  kwargs["mask"] = mask
1316
1391
 
1317
1392
  def _read(self, src, mask, **kwargs):
@@ -1332,11 +1407,17 @@ class Raster:
1332
1407
  if hasattr(self, "_warped_crs"):
1333
1408
  src = WarpedVRT(src, crs=self.crs)
1334
1409
 
1335
- self.array = src.read(out_shape=out_shape, **kwargs)
1410
+ self.values = src.read(out_shape=out_shape, **kwargs)
1336
1411
 
1337
1412
  if not masked:
1338
- self.array[self.array.mask] = self.nodata
1339
- self.array = self.array.data
1413
+ try:
1414
+ self.values[self.values.mask] = self.nodata
1415
+ self.values = self.values.data
1416
+ except AttributeError:
1417
+ pass
1418
+ # self.values = np.ma.masked_array(self.values, mask=mask)
1419
+ # self.values[self.values.mask] = self.nodata
1420
+ # self.values = self.values.data
1340
1421
 
1341
1422
  if boundless:
1342
1423
  self._bounds = src.window_bounds(window=window)
@@ -1347,7 +1428,7 @@ class Raster:
1347
1428
  else:
1348
1429
  self._bounds = intersected.bounds
1349
1430
 
1350
- if not np.size(self.array):
1431
+ if not np.size(self.values):
1351
1432
  return
1352
1433
 
1353
1434
  if self._dtype:
@@ -1355,8 +1436,8 @@ class Raster:
1355
1436
  else:
1356
1437
  self = self.as_minimum_dtype()
1357
1438
 
1358
- if self.array is not None:
1359
- with memfile_from_array(self.array, **self.profile) as src:
1439
+ if self.values is not None:
1440
+ with memfile_from_array(self.values, **self.profile) as src:
1360
1441
  _read(self, src, **kwargs)
1361
1442
  else:
1362
1443
  with opener(self.path, file_system=self.file_system) as file:
@@ -1364,7 +1445,7 @@ class Raster:
1364
1445
  _read(self, src, **kwargs)
1365
1446
 
1366
1447
  def _check_for_array(self, text=""):
1367
- if self.array is None:
1448
+ if self.values is None:
1368
1449
  raise ValueError("Arrays are not loaded. " + text)
1369
1450
 
1370
1451