yirgacheffe 1.4.1__py3-none-any.whl → 1.6.0__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.

Potentially problematic release.


This version of yirgacheffe might be problematic. Click here for more details.

yirgacheffe/__init__.py CHANGED
@@ -5,10 +5,7 @@ try:
5
5
  except ModuleNotFoundError:
6
6
  __version__ = "unknown"
7
7
 
8
- gdal.UseExceptions()
8
+ from ._core import read_raster, read_rasters, read_shape, read_shape_like
9
+ from .constants import WGS_84_PROJECTION
9
10
 
10
- # I don't really want this here, but it's just too useful having it exposed
11
- WGS_84_PROJECTION = 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,'\
12
- 'AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0],'\
13
- 'UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],'\
14
- 'AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]]'
11
+ gdal.UseExceptions()
yirgacheffe/_core.py ADDED
@@ -0,0 +1,136 @@
1
+ from pathlib import Path
2
+ from typing import List, Optional, Tuple, Union
3
+
4
+ from .layers.base import YirgacheffeLayer
5
+ from .layers.group import GroupLayer, TiledGroupLayer
6
+ from .layers.rasters import RasterLayer
7
+ from .layers.vectors import VectorLayer
8
+ from .window import PixelScale
9
+ from .operators import DataType
10
+
11
+ def read_raster(
12
+ filename: Union[Path,str],
13
+ band: int = 1,
14
+ ignore_nodata: bool = False,
15
+ ) -> RasterLayer:
16
+ """Open a raster file (e.g., GeoTIFF).
17
+
18
+ Parameters
19
+ ----------
20
+ filename : Path
21
+ Path of raster file to open.
22
+ band : int, default=1
23
+ For multi-band rasters, which band to use (defaults to first if not specified)
24
+ ignore_nodata : bool, default=False
25
+ If the GeoTIFF has a NODATA value, don't subsitute that value for NaN
26
+
27
+ Returns
28
+ -------
29
+ RasterLayer
30
+ Returns an layer representing the raster data.
31
+ """
32
+ return RasterLayer.layer_from_file(filename, band, ignore_nodata)
33
+
34
+ def read_rasters(
35
+ filenames : Union[List[Path],List[str]],
36
+ tiled: bool=False
37
+ ) -> GroupLayer:
38
+ """Open a set of raster files (e.g., GeoTIFFs) as a single layer.
39
+
40
+ Parameters
41
+ ----------
42
+ filenames : List[Path]
43
+ List of paths of raster files to open.
44
+ tiled : bool, default=False
45
+ If you know that the rasters for a regular tileset, then setting this flag allows
46
+ Yirgacheffe to perform certain optimisations that significantly improve performance for
47
+ this use case.
48
+
49
+ Returns
50
+ -------
51
+ GroupLayer
52
+ Returns an layer representing the raster data.
53
+ """
54
+ if not tiled:
55
+ return GroupLayer.layer_from_files(filenames)
56
+ else:
57
+ return TiledGroupLayer.layer_from_files(filenames)
58
+
59
+ def read_shape(
60
+ filename: Union[Path,str],
61
+ scale: Union[PixelScale, Tuple[float,float]],
62
+ projection: str,
63
+ where_filter: Optional[str] = None,
64
+ datatype: Optional[DataType] = None,
65
+ burn_value: Union[int,float,str] = 1,
66
+ ) -> VectorLayer:
67
+ """Open a polygon file (e.g., GeoJSON, GPKG, or ESRI Shape File).
68
+
69
+ Parameters
70
+ ----------
71
+ filename : Path
72
+ Path of raster file to open.
73
+ scale: PixelScale or tuple of float
74
+ The dimensions of each pixel.
75
+ projection: str
76
+ The map projection to use
77
+ where_filter : str, optional
78
+ For use with files with many entries (e.g., GPKG), applies this filter to the data.
79
+ datatype: DataType, default=DataType.Byte
80
+ Specify the data type of the raster data generated.
81
+ burn_value: int or float or str, default=1
82
+ The value of each pixel in the polygon.
83
+
84
+ Returns
85
+ -------
86
+ VectorLayer
87
+ Returns an layer representing the vector data.
88
+ """
89
+
90
+ if not isinstance(scale, PixelScale):
91
+ scale = PixelScale(scale[0], scale[1])
92
+
93
+ return VectorLayer.layer_from_file(
94
+ filename,
95
+ where_filter,
96
+ scale,
97
+ projection,
98
+ datatype,
99
+ burn_value
100
+ )
101
+
102
+ def read_shape_like(
103
+ filename: Union[Path,str],
104
+ like: YirgacheffeLayer,
105
+ where_filter: Optional[str] = None,
106
+ datatype: Optional[DataType] = None,
107
+ burn_value: Union[int,float,str] = 1,
108
+ ) -> VectorLayer:
109
+ """Open a polygon file (e.g., GeoJSON, GPKG, or ESRI Shape File).
110
+
111
+ Parameters
112
+ ----------
113
+ filename : Path
114
+ Path of raster file to open.
115
+ like: YirgacheffeLayer
116
+ Another layer that has a projection and pixel scale set. This layer will
117
+ use the same projection and pixel scale as that one.
118
+ where_filter : str, optional
119
+ For use with files with many entries (e.g., GPKG), applies this filter to the data.
120
+ datatype: DataType, default=DataType.Byte
121
+ Specify the data type of the raster data generated.
122
+ burn_value: int or float or str, default=1
123
+ The value of each pixel in the polygon.
124
+
125
+ Returns
126
+ -------
127
+ VectorLayer
128
+ Returns an layer representing the vector data.
129
+ """
130
+ return VectorLayer.layer_from_file_like(
131
+ filename,
132
+ like,
133
+ where_filter,
134
+ datatype,
135
+ burn_value,
136
+ )
yirgacheffe/constants.py CHANGED
@@ -1,2 +1,8 @@
1
1
  YSTEP = 512
2
2
  MINIMUM_CHUNKS_PER_THREAD = 1
3
+
4
+ # I don't really want this here, but it's just too useful having it exposed
5
+ WGS_84_PROJECTION = 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,'\
6
+ 'AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0],'\
7
+ 'UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],'\
8
+ 'AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]]'
@@ -56,12 +56,12 @@ class UniformAreaLayer(RasterLayer):
56
56
  return False
57
57
  return True
58
58
 
59
- def __init__(self, dataset, name: Optional[str] = None, band: int = 1):
59
+ def __init__(self, dataset, name: Optional[str] = None, band: int = 1, ignore_nodata: bool = False):
60
60
  if dataset.RasterXSize > 1:
61
61
  raise ValueError("Expected a shrunk dataset")
62
62
  self.databand = dataset.GetRasterBand(1).ReadAsArray(0, 0, 1, dataset.RasterYSize)
63
63
 
64
- super().__init__(dataset, name, band)
64
+ super().__init__(dataset, name, band, ignore_nodata)
65
65
 
66
66
  transform = dataset.GetGeoTransform()
67
67
 
@@ -84,7 +84,14 @@ class UniformAreaLayer(RasterLayer):
84
84
  )
85
85
  self._raster_xsize = self.window.xsize
86
86
 
87
- def read_array_with_window(self, xoffset: int, yoffset: int, xsize: int, ysize: int, window: Window) -> Any:
87
+ def _read_array_with_window(
88
+ self,
89
+ xoffset: int,
90
+ yoffset: int,
91
+ xsize: int,
92
+ ysize: int,
93
+ window: Window,
94
+ ) -> Any:
88
95
  if ysize <= 0:
89
96
  raise ValueError("Request dimensions must be positive and non-zero")
90
97
  offset = window.yoff + yoffset
@@ -1,5 +1,5 @@
1
-
2
- from typing import Any, List, Optional, Tuple
1
+ from __future__ import annotations
2
+ from typing import Any, Optional, Sequence, Tuple
3
3
 
4
4
  from ..operators import DataType, LayerMathMixin
5
5
  from ..rounding import almost_equal, are_pixel_scales_equal_enough, round_up_pixels, round_down_pixels
@@ -66,8 +66,12 @@ class YirgacheffeLayer(LayerMathMixin):
66
66
  raise AttributeError("Layer has no window")
67
67
  return self._window
68
68
 
69
+ @property
70
+ def nodata(self) -> None:
71
+ return None
72
+
69
73
  @staticmethod
70
- def find_intersection(layers: List) -> Area:
74
+ def find_intersection(layers: Sequence[YirgacheffeLayer]) -> Area:
71
75
  if not layers:
72
76
  raise ValueError("Expected list of layers")
73
77
 
@@ -87,7 +91,7 @@ class YirgacheffeLayer(LayerMathMixin):
87
91
  return intersection
88
92
 
89
93
  @staticmethod
90
- def find_union(layers: List) -> Area:
94
+ def find_union(layers: Sequence[YirgacheffeLayer]) -> Area:
91
95
  if not layers:
92
96
  raise ValueError("Expected list of layers")
93
97
 
@@ -183,7 +187,7 @@ class YirgacheffeLayer(LayerMathMixin):
183
187
  self._window = new_window
184
188
  self._active_area = new_area
185
189
 
186
- def reset_window(self):
190
+ def reset_window(self) -> None:
187
191
  self._active_area = self._underlying_area
188
192
  if self._pixel_scale:
189
193
  abs_xstep, abs_ystep = abs(self._pixel_scale.xstep), abs(self._pixel_scale.ystep)
@@ -194,7 +198,7 @@ class YirgacheffeLayer(LayerMathMixin):
194
198
  ysize=round_up_pixels((self.area.bottom - self.area.top) / self._pixel_scale.ystep, abs_ystep),
195
199
  )
196
200
 
197
- def offset_window_by_pixels(self, offset: int):
201
+ def offset_window_by_pixels(self, offset: int) -> None:
198
202
  """Grows (if pixels is positive) or shrinks (if pixels is negative) the window for the layer."""
199
203
  if offset == 0:
200
204
  return
@@ -227,10 +231,24 @@ class YirgacheffeLayer(LayerMathMixin):
227
231
  self._window = new_window
228
232
  self._active_area = new_area
229
233
 
230
- def read_array_with_window(self, _x: int, _y: int, _xsize: int, _ysize: int, window: Window) -> Any:
234
+ def _read_array_with_window(
235
+ self,
236
+ _x: int,
237
+ _y: int,
238
+ _xsize: int,
239
+ _ysize: int,
240
+ _window: Window,
241
+ ) -> Any:
231
242
  raise NotImplementedError("Must be overridden by subclass")
232
243
 
233
- def read_array_for_area(self, target_area: Area, x: int, y: int, width: int, height: int) -> Any:
244
+ def _read_array_for_area(
245
+ self,
246
+ target_area: Area,
247
+ x: int,
248
+ y: int,
249
+ width: int,
250
+ height: int,
251
+ ) -> Any:
234
252
  assert self._pixel_scale is not None
235
253
 
236
254
  target_window = Window(
@@ -247,10 +265,28 @@ class YirgacheffeLayer(LayerMathMixin):
247
265
  (self._pixel_scale.ystep * -1.0)
248
266
  ),
249
267
  )
250
- return self.read_array_with_window(x, y, width, height, target_window)
268
+ return self._read_array_with_window(x, y, width, height, target_window)
251
269
 
252
270
  def read_array(self, x: int, y: int, width: int, height: int) -> Any:
253
- return self.read_array_with_window(x, y, width, height, self.window)
271
+ """Reads data from the layer based on the current reference window.
272
+
273
+ Arguments
274
+ ---------
275
+ x : int
276
+ X axis offset for reading
277
+ y : int
278
+ Y axis offset for reading
279
+ width : int
280
+ Width of data to read
281
+ height : int
282
+ Height of data to read
283
+
284
+ Results
285
+ -------
286
+ Any
287
+ An array of values from the layer.
288
+ """
289
+ return self._read_array_with_window(x, y, width, height, self.window)
254
290
 
255
291
  def latlng_for_pixel(self, x_coord: int, y_coord: int) -> Tuple[float,float]:
256
292
  """Get geo coords for pixel. This is relative to the set view window."""
@@ -4,19 +4,14 @@ from ..operators import DataType
4
4
  from ..window import Area, PixelScale, Window
5
5
  from .base import YirgacheffeLayer
6
6
  from .._backends import backend
7
- from .. import WGS_84_PROJECTION
7
+ from ..constants import WGS_84_PROJECTION
8
8
 
9
9
 
10
10
  class ConstantLayer(YirgacheffeLayer):
11
11
  """This is a layer that will return the identity value - can be used when an input layer is
12
12
  missing (e.g., area) without having the calculation full of branches."""
13
13
  def __init__(self, value: Union[int,float]): # pylint: disable=W0231
14
- area = Area(
15
- left = -180.0,
16
- top = 90.0,
17
- right = 180.0,
18
- bottom = -90.0
19
- )
14
+ area = Area.world()
20
15
  super().__init__(area, None, WGS_84_PROJECTION)
21
16
  self.value = float(value)
22
17
 
@@ -33,8 +28,22 @@ class ConstantLayer(YirgacheffeLayer):
33
28
  def read_array(self, _x: int, _y: int, width: int, height: int) -> Any:
34
29
  return backend.full((height, width), self.value)
35
30
 
36
- def read_array_with_window(self, _x: int, _y: int, width: int, height: int, _window: Window) -> Any:
31
+ def _read_array_with_window(
32
+ self,
33
+ _x: int,
34
+ _y: int,
35
+ width: int,
36
+ height: int,
37
+ _window: Window,
38
+ ) -> Any:
37
39
  return backend.full((height, width), self.value)
38
40
 
39
- def read_array_for_area(self, _target_area: Area, x: int, y: int, width: int, height: int) -> Any:
41
+ def _read_array_for_area(
42
+ self,
43
+ _target_area: Area,
44
+ x: int,
45
+ y: int,
46
+ width: int,
47
+ height: int,
48
+ ) -> Any:
40
49
  return self.read_array(x, y, width, height)
@@ -1,12 +1,12 @@
1
1
  from __future__ import annotations
2
2
  import copy
3
- import glob
4
- import os
5
- from typing import Any, List, Optional
3
+ from pathlib import Path
4
+ from typing import Any, List, Optional, Union
6
5
 
7
6
  import numpy as np
8
- from yirgacheffe.operators import DataType
7
+ from numpy import ma
9
8
 
9
+ from ..operators import DataType
10
10
  from ..rounding import are_pixel_scales_equal_enough, round_down_pixels
11
11
  from ..window import Area, Window
12
12
  from .base import YirgacheffeLayer
@@ -23,19 +23,25 @@ class GroupLayer(YirgacheffeLayer):
23
23
  @classmethod
24
24
  def layer_from_directory(
25
25
  cls,
26
- directory_path: str,
26
+ directory_path: Union[Path,str],
27
27
  name: Optional[str] = None,
28
28
  matching: str = "*.tif"
29
29
  ) -> GroupLayer:
30
30
  if directory_path is None:
31
31
  raise ValueError("Directory path is None")
32
- files = [os.path.join(directory_path, x) for x in glob.glob(matching, root_dir=directory_path)]
32
+ if isinstance(directory_path, str):
33
+ directory_path = Path(directory_path)
34
+ files = list(directory_path.glob(matching))
33
35
  if len(files) < 1:
34
36
  raise GroupLayerEmpty(directory_path)
35
37
  return cls.layer_from_files(files, name)
36
38
 
37
39
  @classmethod
38
- def layer_from_files(cls, filenames: List[str], name: Optional[str] = None) -> GroupLayer:
40
+ def layer_from_files(
41
+ cls,
42
+ filenames: Union[List[Path],List[str]],
43
+ name: Optional[str] = None
44
+ ) -> GroupLayer:
39
45
  if filenames is None:
40
46
  raise ValueError("filenames argument is None")
41
47
  if len(filenames) < 1:
@@ -43,7 +49,11 @@ class GroupLayer(YirgacheffeLayer):
43
49
  rasters: List[YirgacheffeLayer] = [RasterLayer.layer_from_file(x) for x in filenames]
44
50
  return cls(rasters, name)
45
51
 
46
- def __init__(self, layers: List[YirgacheffeLayer], name: Optional[str] = None) -> None:
52
+ def __init__(
53
+ self,
54
+ layers: List[YirgacheffeLayer],
55
+ name: Optional[str] = None
56
+ ) -> None:
47
57
  if not layers:
48
58
  raise GroupLayerEmpty("Expected one or more layers")
49
59
  if not are_pixel_scales_equal_enough([x.pixel_scale for x in layers]):
@@ -95,8 +105,14 @@ class GroupLayer(YirgacheffeLayer):
95
105
  except AttributeError:
96
106
  pass # called from Base constructor before we've added the extra field
97
107
 
98
- def read_array_with_window(self, xoffset: int, yoffset: int, xsize: int, ysize: int, window: Window) -> Any:
99
-
108
+ def _read_array_with_window(
109
+ self,
110
+ xoffset: int,
111
+ yoffset: int,
112
+ xsize: int,
113
+ ysize: int,
114
+ window: Window,
115
+ ) -> Any:
100
116
  if (xsize <= 0) or (ysize <= 0):
101
117
  raise ValueError("Request dimensions must be positive and non-zero")
102
118
 
@@ -130,12 +146,15 @@ class GroupLayer(YirgacheffeLayer):
130
146
  if len(contributing_layers) == 1:
131
147
  layer, adjusted_layer_window, intersection = contributing_layers[0]
132
148
  if target_window == intersection:
133
- return layer.read_array(
149
+ data = layer.read_array(
134
150
  intersection.xoff - adjusted_layer_window.xoff,
135
151
  intersection.yoff - adjusted_layer_window.yoff,
136
152
  intersection.xsize,
137
153
  intersection.ysize
138
154
  )
155
+ if layer.nodata is not None:
156
+ data[data.isnan()] = 0.0
157
+ return data
139
158
 
140
159
  result = np.zeros((ysize, xsize), dtype=float)
141
160
  for layer, adjusted_layer_window, intersection in contributing_layers:
@@ -143,14 +162,26 @@ class GroupLayer(YirgacheffeLayer):
143
162
  intersection.xoff - adjusted_layer_window.xoff,
144
163
  intersection.yoff - adjusted_layer_window.yoff,
145
164
  intersection.xsize,
146
- intersection.ysize
165
+ intersection.ysize,
147
166
  )
148
167
  result_x_offset = (intersection.xoff - xoffset) - window.xoff
149
168
  result_y_offset = (intersection.yoff - yoffset) - window.yoff
150
- result[
151
- result_y_offset:result_y_offset + intersection.ysize,
152
- result_x_offset:result_x_offset + intersection.xsize
153
- ] = data
169
+ if layer.nodata is None:
170
+ result[
171
+ result_y_offset:result_y_offset + intersection.ysize,
172
+ result_x_offset:result_x_offset + intersection.xsize
173
+ ] = data
174
+ else:
175
+ masked = ma.masked_invalid(data)
176
+ before = result[
177
+ result_y_offset:result_y_offset + intersection.ysize,
178
+ result_x_offset:result_x_offset + intersection.xsize
179
+ ]
180
+ merged = ma.where(masked.mask, before, masked)
181
+ result[
182
+ result_y_offset:result_y_offset + intersection.ysize,
183
+ result_x_offset:result_x_offset + intersection.xsize
184
+ ] = merged
154
185
 
155
186
  return backend.promote(result)
156
187
 
@@ -205,7 +236,14 @@ class TiledGroupLayer(GroupLayer):
205
236
  * You can have missing tiles, and it'll fill in zeros.
206
237
  * The tiles can overlap - e.g., JRC Annual Change tiles all overlap by a few pixels on all edges.
207
238
  """
208
- def read_array_with_window(self, xoffset: int, yoffset: int, xsize: int, ysize: int, window: Window) -> Any:
239
+ def _read_array_with_window(
240
+ self,
241
+ xoffset: int,
242
+ yoffset: int,
243
+ xsize: int,
244
+ ysize: int,
245
+ window: Window,
246
+ ) -> Any:
209
247
  if (xsize <= 0) or (ysize <= 0):
210
248
  raise ValueError("Request dimensions must be positive and non-zero")
211
249
 
@@ -82,7 +82,14 @@ class H3CellLayer(YirgacheffeLayer):
82
82
  def datatype(self) -> DataType:
83
83
  return DataType.Float64
84
84
 
85
- def read_array_with_window(self, xoffset: int, yoffset: int, xsize: int, ysize: int, window: Window) -> Any:
85
+ def _read_array_with_window(
86
+ self,
87
+ xoffset: int,
88
+ yoffset: int,
89
+ xsize: int,
90
+ ysize: int,
91
+ window: Window,
92
+ ) -> Any:
86
93
  assert self._pixel_scale is not None
87
94
 
88
95
  if (xsize <= 0) or (ysize <= 0):
@@ -1,12 +1,12 @@
1
1
  from __future__ import annotations
2
2
  import math
3
- import os
3
+ from pathlib import Path
4
4
  from typing import Any, Optional, Tuple, Union
5
5
 
6
6
  import numpy as np
7
7
  from osgeo import gdal
8
8
 
9
- from .. import WGS_84_PROJECTION
9
+ from ..constants import WGS_84_PROJECTION
10
10
  from ..window import Area, PixelScale, Window
11
11
  from ..rounding import round_up_pixels
12
12
  from .base import YirgacheffeLayer
@@ -88,7 +88,7 @@ class RasterLayer(YirgacheffeLayer):
88
88
  @staticmethod
89
89
  def empty_raster_layer_like(
90
90
  layer: Any,
91
- filename: Optional[str]=None,
91
+ filename: Optional[Union[Path,str]]=None,
92
92
  area: Optional[Area]=None,
93
93
  datatype: Optional[Union[int, DataType]]=None,
94
94
  compress: bool=True,
@@ -214,7 +214,12 @@ class RasterLayer(YirgacheffeLayer):
214
214
  return RasterLayer(dataset)
215
215
 
216
216
  @classmethod
217
- def layer_from_file(cls, filename: str, band: int = 1) -> RasterLayer:
217
+ def layer_from_file(
218
+ cls,
219
+ filename: Union[Path,str],
220
+ band: int = 1,
221
+ ignore_nodata: bool = False,
222
+ ) -> RasterLayer:
218
223
  try:
219
224
  dataset = gdal.Open(filename, gdal.GA_ReadOnly)
220
225
  except RuntimeError as exc:
@@ -224,9 +229,15 @@ class RasterLayer(YirgacheffeLayer):
224
229
  _ = dataset.GetRasterBand(band)
225
230
  except RuntimeError as exc:
226
231
  raise InvalidRasterBand(band) from exc
227
- return cls(dataset, filename, band)
228
-
229
- def __init__(self, dataset: gdal.Dataset, name: Optional[str] = None, band: int = 1):
232
+ return cls(dataset, str(filename), band, ignore_nodata)
233
+
234
+ def __init__(
235
+ self,
236
+ dataset: gdal.Dataset,
237
+ name: Optional[str] = None,
238
+ band: int = 1,
239
+ ignore_nodata: bool = False,
240
+ ) -> None:
230
241
  if not dataset:
231
242
  raise ValueError("None is not a valid dataset")
232
243
 
@@ -252,10 +263,11 @@ class RasterLayer(YirgacheffeLayer):
252
263
  assert self.window == Window(0, 0, dataset.RasterXSize, dataset.RasterYSize)
253
264
 
254
265
  self._dataset = dataset
255
- self._dataset_path = dataset.GetDescription()
266
+ self._dataset_path = Path(dataset.GetDescription())
256
267
  self._band = band
257
268
  self._raster_xsize = dataset.RasterXSize
258
269
  self._raster_ysize = dataset.RasterYSize
270
+ self._ignore_nodata = ignore_nodata
259
271
 
260
272
  @property
261
273
  def _raster_dimensions(self) -> Tuple[int,int]:
@@ -275,7 +287,7 @@ class RasterLayer(YirgacheffeLayer):
275
287
 
276
288
  def __getstate__(self) -> object:
277
289
  # Only support pickling on file backed layers (ideally read only ones...)
278
- if not os.path.isfile(self._dataset_path):
290
+ if not self._dataset_path.exists():
279
291
  raise ValueError("Can not pickle layer that is not file backed.")
280
292
  odict = self.__dict__.copy()
281
293
  self._park()
@@ -305,7 +317,21 @@ class RasterLayer(YirgacheffeLayer):
305
317
  assert self._dataset
306
318
  return DataType.of_gdal(self._dataset.GetRasterBand(1).DataType)
307
319
 
308
- def read_array_with_window(self, xoffset: int, yoffset: int, xsize: int, ysize: int, window: Window) -> Any:
320
+ @property
321
+ def nodata(self) -> Optional[Any]:
322
+ if self._dataset is None:
323
+ self._unpark()
324
+ assert self._dataset
325
+ return self._dataset.GetRasterBand(self._band).GetNoDataValue()
326
+
327
+ def _read_array_with_window(
328
+ self,
329
+ xoffset: int,
330
+ yoffset: int,
331
+ xsize: int,
332
+ ysize: int,
333
+ window: Window,
334
+ ) -> Any:
309
335
  if self._dataset is None:
310
336
  self._unpark()
311
337
  assert self._dataset
@@ -335,7 +361,6 @@ class RasterLayer(YirgacheffeLayer):
335
361
  if target_window == intersection:
336
362
  # The target window is a subset of or equal to the source, so we can just ask for the data
337
363
  data = backend.promote(self._dataset.GetRasterBand(self._band).ReadAsArray(*intersection.as_array_args))
338
- return data
339
364
  else:
340
365
  # We should read the intersection from the array, and the rest should be zeros
341
366
  subset = backend.promote(self._dataset.GetRasterBand(self._band).ReadAsArray(*intersection.as_array_args))
@@ -350,4 +375,9 @@ class RasterLayer(YirgacheffeLayer):
350
375
  )
351
376
  )).astype(int)
352
377
  data = backend.pad(subset, region, mode='constant')
353
- return data
378
+
379
+ nodata = self.nodata
380
+ if not self._ignore_nodata and nodata is not None:
381
+ data = backend.where(data == nodata, float("nan"), data)
382
+
383
+ return data
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
  from math import floor, ceil
3
- from typing import Any, Optional
3
+ from pathlib import Path
4
+ from typing import Any, Optional, Union
4
5
 
5
6
  from skimage import transform
6
7
  from yirgacheffe.operators import DataType
@@ -17,7 +18,7 @@ class RescaledRasterLayer(YirgacheffeLayer):
17
18
  @classmethod
18
19
  def layer_from_file(
19
20
  cls,
20
- filename: str,
21
+ filename: Union[Path,str],
21
22
  pixel_scale: PixelScale,
22
23
  band: int = 1,
23
24
  nearest_neighbour: bool = True,
@@ -61,7 +62,14 @@ class RescaledRasterLayer(YirgacheffeLayer):
61
62
  def datatype(self) -> DataType:
62
63
  return self._src.datatype
63
64
 
64
- def read_array_with_window(self, xoffset: int, yoffset: int, xsize: int, ysize: int, window: Window) -> Any:
65
+ def _read_array_with_window(
66
+ self,
67
+ xoffset: int,
68
+ yoffset: int,
69
+ xsize: int,
70
+ ysize: int,
71
+ window: Window,
72
+ ) -> Any:
65
73
 
66
74
  # to avoid aliasing issues, we try to scale to the nearest pixel
67
75
  # and recrop when scaling bigger