yirgacheffe 1.5.0__tar.gz → 1.6.0__tar.gz

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.

Files changed (55) hide show
  1. {yirgacheffe-1.5.0/yirgacheffe.egg-info → yirgacheffe-1.6.0}/PKG-INFO +6 -2
  2. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/README.md +5 -1
  3. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/pyproject.toml +1 -1
  4. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_datatypes.py +0 -1
  5. yirgacheffe-1.6.0/tests/test_nodata.py +64 -0
  6. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_raster.py +6 -6
  7. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/_core.py +6 -3
  8. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/area.py +10 -3
  9. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/base.py +40 -4
  10. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/constant.py +16 -2
  11. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/group.py +39 -10
  12. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/h3layer.py +8 -1
  13. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/rasters.py +37 -7
  14. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/rescaled.py +8 -1
  15. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/vectors.py +10 -3
  16. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/operators.py +2 -2
  17. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0/yirgacheffe.egg-info}/PKG-INFO +6 -2
  18. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe.egg-info/SOURCES.txt +1 -0
  19. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/LICENSE +0 -0
  20. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/MANIFEST.in +0 -0
  21. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/setup.cfg +0 -0
  22. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_area.py +0 -0
  23. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_auto_windowing.py +0 -0
  24. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_base.py +0 -0
  25. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_constants.py +0 -0
  26. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_group.py +0 -0
  27. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_h3layer.py +0 -0
  28. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_intersection.py +0 -0
  29. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_multiband.py +0 -0
  30. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_openers.py +0 -0
  31. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_operators.py +0 -0
  32. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_optimisation.py +0 -0
  33. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_parallel_operators.py +0 -0
  34. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_pickle.py +0 -0
  35. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_rescaling.py +0 -0
  36. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_rounding.py +0 -0
  37. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_save_with_window.py +0 -0
  38. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_sum_with_window.py +0 -0
  39. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_uniform_area_layer.py +0 -0
  40. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_union.py +0 -0
  41. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_vectors.py +0 -0
  42. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/tests/test_window.py +0 -0
  43. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/__init__.py +0 -0
  44. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/_backends/__init__.py +0 -0
  45. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/_backends/enumeration.py +0 -0
  46. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/_backends/mlx.py +0 -0
  47. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/_backends/numpy.py +0 -0
  48. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/constants.py +0 -0
  49. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/layers/__init__.py +0 -0
  50. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/rounding.py +0 -0
  51. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe/window.py +0 -0
  52. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe.egg-info/dependency_links.txt +0 -0
  53. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe.egg-info/entry_points.txt +0 -0
  54. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe.egg-info/requires.txt +0 -0
  55. {yirgacheffe-1.5.0 → yirgacheffe-1.6.0}/yirgacheffe.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yirgacheffe
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Abstraction of gdal datasets for doing basic math operations
5
5
  Author-email: Michael Dales <mwd24@cam.ac.uk>
6
6
  License-Expression: ISC
@@ -107,7 +107,7 @@ If you have set either the intersection window or union window on a layer and yo
107
107
 
108
108
  ### Direct access to data
109
109
 
110
- If doing per-layer operations isn't applicable for your application, you can read the pixel values for all layers (including VectorLayers) by calling `read_array` similarly to how you would for gdal. The data you access will be relative to the specified window - that is, if you've called either `set_window_for_intersection` or `set_window_for_union` then `read_array` will be relative to that and Yirgacheffe will clip or expand the data with zero values as necessary.
110
+ If doing per-layer operations isn't applicable for your application, you can read the pixel values for all layers (including VectorLayers) by calling `read_array` similarly to how you would for GDAL. The data you access will be relative to the specified window - that is, if you've called either `set_window_for_intersection` or `set_window_for_union` then `read_array` will be relative to that and Yirgacheffe will clip or expand the data with zero values as necessary.
111
111
 
112
112
 
113
113
  ## Layer types
@@ -160,6 +160,7 @@ with RasterLayer.layer_from_file('test1.tif') as source:
160
160
  scaled = RasterLayer.scaled_raster_from_raster(source, PixelScale(0.0001, -0.0001), 'scaled.tif')
161
161
  ```
162
162
 
163
+ If the data is from a GeoTIFF that has a nodata value specified, then pixel values with that specified nodata value in them will be converted to NaN. You can override that by providing `ignore_nodata=True` as an optional argument to `layer_from_file` (or with the new 2.0 API, `read_raster`). You can find out if a layer has a nodata value by accessing the `nodata` property - it is None if there is no such value.
163
164
 
164
165
  ### VectorLayer
165
166
 
@@ -229,6 +230,9 @@ with yg.read_rasters(['tile_N10_E10.tif', 'tile_N20_E10.tif']) as all_tiles:
229
230
  ...
230
231
  ```
231
232
 
233
+ If any of the layers have a `nodata` value specified, then any pixel with that value will be masked out to allow data from other layers to be visible.
234
+
235
+
232
236
  ### TiledGroupLayer
233
237
 
234
238
  This is a specialisation of GroupLayer, which you can use if your layers are all the same size and form a grid, as is often the case with map tiles. In this case the rendering code can be optimised and this class is significantly faster that GroupLayer.
@@ -82,7 +82,7 @@ If you have set either the intersection window or union window on a layer and yo
82
82
 
83
83
  ### Direct access to data
84
84
 
85
- If doing per-layer operations isn't applicable for your application, you can read the pixel values for all layers (including VectorLayers) by calling `read_array` similarly to how you would for gdal. The data you access will be relative to the specified window - that is, if you've called either `set_window_for_intersection` or `set_window_for_union` then `read_array` will be relative to that and Yirgacheffe will clip or expand the data with zero values as necessary.
85
+ If doing per-layer operations isn't applicable for your application, you can read the pixel values for all layers (including VectorLayers) by calling `read_array` similarly to how you would for GDAL. The data you access will be relative to the specified window - that is, if you've called either `set_window_for_intersection` or `set_window_for_union` then `read_array` will be relative to that and Yirgacheffe will clip or expand the data with zero values as necessary.
86
86
 
87
87
 
88
88
  ## Layer types
@@ -135,6 +135,7 @@ with RasterLayer.layer_from_file('test1.tif') as source:
135
135
  scaled = RasterLayer.scaled_raster_from_raster(source, PixelScale(0.0001, -0.0001), 'scaled.tif')
136
136
  ```
137
137
 
138
+ If the data is from a GeoTIFF that has a nodata value specified, then pixel values with that specified nodata value in them will be converted to NaN. You can override that by providing `ignore_nodata=True` as an optional argument to `layer_from_file` (or with the new 2.0 API, `read_raster`). You can find out if a layer has a nodata value by accessing the `nodata` property - it is None if there is no such value.
138
139
 
139
140
  ### VectorLayer
140
141
 
@@ -204,6 +205,9 @@ with yg.read_rasters(['tile_N10_E10.tif', 'tile_N20_E10.tif']) as all_tiles:
204
205
  ...
205
206
  ```
206
207
 
208
+ If any of the layers have a `nodata` value specified, then any pixel with that value will be masked out to allow data from other layers to be visible.
209
+
210
+
207
211
  ### TiledGroupLayer
208
212
 
209
213
  This is a specialisation of GroupLayer, which you can use if your layers are all the same size and form a grid, as is often the case with map tiles. In this case the rendering code can be optimised and this class is significantly faster that GroupLayer.
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "yirgacheffe"
9
- version = "1.5.0"
9
+ version = "1.6.0"
10
10
  description = "Abstraction of gdal datasets for doing basic math operations"
11
11
  readme = "README.md"
12
12
  authors = [{ name = "Michael Dales", email = "mwd24@cam.ac.uk" }]
@@ -44,7 +44,6 @@ def test_round_trip_from_gdal(ytype) -> None:
44
44
  def test_round_trip_float64() -> None:
45
45
  backend_type = backend.dtype_to_backed(DataType.Float64)
46
46
  ytype = backend.backend_to_dtype(backend_type)
47
- print(BACKEND, "sad")
48
47
  match BACKEND:
49
48
  case "NUMPY":
50
49
  assert ytype == DataType.Float64
@@ -0,0 +1,64 @@
1
+ import numpy as np
2
+
3
+ from yirgacheffe.layers.rasters import RasterLayer
4
+ from yirgacheffe.layers.group import GroupLayer
5
+
6
+ from tests.helpers import gdal_dataset_with_data
7
+
8
+ def test_raster_without_nodata_value() -> None:
9
+ data1 = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 5.0, 8.0]])
10
+ dataset = gdal_dataset_with_data((0.0, 0.0), 0.02, data1)
11
+ with RasterLayer(dataset) as layer:
12
+ assert layer.nodata is None
13
+ actual = layer.read_array(0, 0, 4, 2)
14
+ assert np.array_equal(data1, actual, equal_nan=True)
15
+
16
+ def test_raster_with_nodata_value() -> None:
17
+ data1 = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 5.0, 8.0]])
18
+ dataset = gdal_dataset_with_data((0.0, 0.0), 0.02, data1)
19
+ dataset.GetRasterBand(1).SetNoDataValue(5.0)
20
+ with RasterLayer(dataset) as layer:
21
+ assert layer.nodata == 5.0
22
+ data1[data1 == 5.0] = np.nan
23
+ actual = layer.read_array(0, 0, 4, 2)
24
+ assert np.array_equal(data1, actual, equal_nan=True)
25
+
26
+ def test_raster_with_nodata_value_ignored() -> None:
27
+ data1 = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 5.0, 8.0]])
28
+ dataset = gdal_dataset_with_data((0.0, 0.0), 0.02, data1)
29
+ dataset.GetRasterBand(1).SetNoDataValue(5.0)
30
+ with RasterLayer(dataset, ignore_nodata=True) as layer:
31
+ assert layer.nodata == 5.0
32
+ actual = layer.read_array(0, 0, 4, 2)
33
+ assert np.array_equal(data1, actual, equal_nan=True)
34
+
35
+ def test_group_layer_with_nodata_values() -> None:
36
+ data1 = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 5.0, 5.0, 5.0]])
37
+ dataset1 = gdal_dataset_with_data((0.0, 0.0), 0.02, data1)
38
+ dataset1.GetRasterBand(1).SetNoDataValue(5.0)
39
+
40
+ data2 = np.array([[1.0, 1.0, 1.0, 1.0], [5.0, 6.0, 7.0, 8.0]])
41
+ dataset2 = gdal_dataset_with_data((0.0, 0.0), 0.02, data2)
42
+ dataset2.GetRasterBand(1).SetNoDataValue(1.0)
43
+
44
+ with RasterLayer(dataset1) as layer1:
45
+ with RasterLayer(dataset2) as layer2:
46
+ with GroupLayer([layer1, layer2]) as group:
47
+ actual = group.read_array(0, 0, 4, 2)
48
+ expected = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]])
49
+ assert np.array_equal(expected, actual, equal_nan=True)
50
+
51
+ def test_group_layer_with_nodata_values_ignore_nodata() -> None:
52
+ data1 = np.array([[1.0, 2.0, 3.0, 4.0], [5.0, 5.0, 5.0, 5.0]])
53
+ dataset1 = gdal_dataset_with_data((0.0, 0.0), 0.02, data1)
54
+ dataset1.GetRasterBand(1).SetNoDataValue(5.0)
55
+
56
+ data2 = np.array([[1.0, 1.0, 1.0, 1.0], [5.0, 6.0, 7.0, 8.0]])
57
+ dataset2 = gdal_dataset_with_data((0.0, 0.0), 0.02, data2)
58
+ dataset2.GetRasterBand(1).SetNoDataValue(1.0)
59
+
60
+ with RasterLayer(dataset1, ignore_nodata=True) as layer1:
61
+ with RasterLayer(dataset2, ignore_nodata=True) as layer2:
62
+ with GroupLayer([layer1, layer2]) as group:
63
+ actual = group.read_array(0, 0, 4, 2)
64
+ assert np.array_equal(data1, actual, equal_nan=True)
@@ -56,12 +56,12 @@ def test_open_file() -> None:
56
56
  dataset = gdal_dataset_of_region(area, 0.02, filename=path)
57
57
  dataset.Close()
58
58
  assert os.path.exists(path)
59
- layer = RasterLayer.layer_from_file(path)
60
- assert layer.area == area
61
- assert layer.pixel_scale == (0.02, -0.02)
62
- assert layer.geo_transform == (-10, 0.02, 0.0, 10, 0.0, -0.02)
63
- assert layer.window == Window(0, 0, 1000, 1000)
64
- del layer
59
+ with RasterLayer.layer_from_file(path) as layer:
60
+ assert layer.area == area
61
+ assert layer.pixel_scale == (0.02, -0.02)
62
+ assert layer.geo_transform == (-10, 0.02, 0.0, 10, 0.0, -0.02)
63
+ assert layer.window == Window(0, 0, 1000, 1000)
64
+ del layer
65
65
 
66
66
  @pytest.mark.parametrize("initial_area",
67
67
  [
@@ -10,7 +10,8 @@ from .operators import DataType
10
10
 
11
11
  def read_raster(
12
12
  filename: Union[Path,str],
13
- band: int = 1
13
+ band: int = 1,
14
+ ignore_nodata: bool = False,
14
15
  ) -> RasterLayer:
15
16
  """Open a raster file (e.g., GeoTIFF).
16
17
 
@@ -19,14 +20,16 @@ def read_raster(
19
20
  filename : Path
20
21
  Path of raster file to open.
21
22
  band : int, default=1
22
- For multi-band rasters, which band to use (defaults to first if not specified).
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
23
26
 
24
27
  Returns
25
28
  -------
26
29
  RasterLayer
27
30
  Returns an layer representing the raster data.
28
31
  """
29
- return RasterLayer.layer_from_file(filename, band)
32
+ return RasterLayer.layer_from_file(filename, band, ignore_nodata)
30
33
 
31
34
  def read_rasters(
32
35
  filenames : Union[List[Path],List[str]],
@@ -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
@@ -66,6 +66,10 @@ 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
74
  def find_intersection(layers: Sequence[YirgacheffeLayer]) -> Area:
71
75
  if not layers:
@@ -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."""
@@ -28,8 +28,22 @@ class ConstantLayer(YirgacheffeLayer):
28
28
  def read_array(self, _x: int, _y: int, width: int, height: int) -> Any:
29
29
  return backend.full((height, width), self.value)
30
30
 
31
- 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:
32
39
  return backend.full((height, width), self.value)
33
40
 
34
- 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:
35
49
  return self.read_array(x, y, width, height)
@@ -4,8 +4,9 @@ from pathlib import Path
4
4
  from typing import Any, List, Optional, Union
5
5
 
6
6
  import numpy as np
7
- from yirgacheffe.operators import DataType
7
+ from numpy import ma
8
8
 
9
+ from ..operators import DataType
9
10
  from ..rounding import are_pixel_scales_equal_enough, round_down_pixels
10
11
  from ..window import Area, Window
11
12
  from .base import YirgacheffeLayer
@@ -104,8 +105,14 @@ class GroupLayer(YirgacheffeLayer):
104
105
  except AttributeError:
105
106
  pass # called from Base constructor before we've added the extra field
106
107
 
107
- def read_array_with_window(self, xoffset: int, yoffset: int, xsize: int, ysize: int, window: Window) -> Any:
108
-
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:
109
116
  if (xsize <= 0) or (ysize <= 0):
110
117
  raise ValueError("Request dimensions must be positive and non-zero")
111
118
 
@@ -139,12 +146,15 @@ class GroupLayer(YirgacheffeLayer):
139
146
  if len(contributing_layers) == 1:
140
147
  layer, adjusted_layer_window, intersection = contributing_layers[0]
141
148
  if target_window == intersection:
142
- return layer.read_array(
149
+ data = layer.read_array(
143
150
  intersection.xoff - adjusted_layer_window.xoff,
144
151
  intersection.yoff - adjusted_layer_window.yoff,
145
152
  intersection.xsize,
146
153
  intersection.ysize
147
154
  )
155
+ if layer.nodata is not None:
156
+ data[data.isnan()] = 0.0
157
+ return data
148
158
 
149
159
  result = np.zeros((ysize, xsize), dtype=float)
150
160
  for layer, adjusted_layer_window, intersection in contributing_layers:
@@ -152,14 +162,26 @@ class GroupLayer(YirgacheffeLayer):
152
162
  intersection.xoff - adjusted_layer_window.xoff,
153
163
  intersection.yoff - adjusted_layer_window.yoff,
154
164
  intersection.xsize,
155
- intersection.ysize
165
+ intersection.ysize,
156
166
  )
157
167
  result_x_offset = (intersection.xoff - xoffset) - window.xoff
158
168
  result_y_offset = (intersection.yoff - yoffset) - window.yoff
159
- result[
160
- result_y_offset:result_y_offset + intersection.ysize,
161
- result_x_offset:result_x_offset + intersection.xsize
162
- ] = 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
163
185
 
164
186
  return backend.promote(result)
165
187
 
@@ -214,7 +236,14 @@ class TiledGroupLayer(GroupLayer):
214
236
  * You can have missing tiles, and it'll fill in zeros.
215
237
  * The tiles can overlap - e.g., JRC Annual Change tiles all overlap by a few pixels on all edges.
216
238
  """
217
- 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:
218
247
  if (xsize <= 0) or (ysize <= 0):
219
248
  raise ValueError("Request dimensions must be positive and non-zero")
220
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):
@@ -214,7 +214,12 @@ class RasterLayer(YirgacheffeLayer):
214
214
  return RasterLayer(dataset)
215
215
 
216
216
  @classmethod
217
- def layer_from_file(cls, filename: Union[Path,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, str(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
 
@@ -256,6 +267,7 @@ class RasterLayer(YirgacheffeLayer):
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]:
@@ -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
@@ -62,7 +62,14 @@ class RescaledRasterLayer(YirgacheffeLayer):
62
62
  def datatype(self) -> DataType:
63
63
  return self._src.datatype
64
64
 
65
- 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:
66
73
 
67
74
  # to avoid aliasing issues, we try to scale to the nearest pixel
68
75
  # and recrop when scaling bigger
@@ -359,7 +359,14 @@ class VectorLayer(YirgacheffeLayer):
359
359
  def datatype(self) -> DataType:
360
360
  return self._datatype
361
361
 
362
- def read_array_for_area(self, target_area: Area, x: int, y: int, width: int, height: int) -> Any:
362
+ def _read_array_for_area(
363
+ self,
364
+ target_area: Area,
365
+ x: int,
366
+ y: int,
367
+ width: int,
368
+ height: int,
369
+ ) -> Any:
363
370
  assert self._pixel_scale is not None
364
371
 
365
372
  if self._original is None:
@@ -399,8 +406,8 @@ class VectorLayer(YirgacheffeLayer):
399
406
  res = backend.promote(dataset.ReadAsArray(0, 0, width, height))
400
407
  return res
401
408
 
402
- def read_array_with_window(self, _x, _y, _width, _height, _window) -> Any:
409
+ def _read_array_with_window(self, _x, _y, _width, _height, _window) -> Any:
403
410
  assert NotRequired
404
411
 
405
412
  def read_array(self, x: int, y: int, width: int, height: int) -> Any:
406
- return self.read_array_for_area(self._active_area, x, y, width, height)
413
+ return self._read_array_for_area(self._active_area, x, y, width, height)
@@ -98,9 +98,9 @@ class LayerMathMixin:
98
98
  def _eval(self, area, index, step, target_window=None):
99
99
  try:
100
100
  window = self.window if target_window is None else target_window
101
- return self.read_array_for_area(area, 0, index, window.xsize, step)
101
+ return self._read_array_for_area(area, 0, index, window.xsize, step)
102
102
  except AttributeError:
103
- return self.read_array_for_area(area, 0, index, target_window.xsize if target_window else 1, step)
103
+ return self._read_array_for_area(area, 0, index, target_window.xsize if target_window else 1, step)
104
104
 
105
105
  def nan_to_num(self, nan=0, posinf=None, neginf=None):
106
106
  return LayerOperation(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yirgacheffe
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Abstraction of gdal datasets for doing basic math operations
5
5
  Author-email: Michael Dales <mwd24@cam.ac.uk>
6
6
  License-Expression: ISC
@@ -107,7 +107,7 @@ If you have set either the intersection window or union window on a layer and yo
107
107
 
108
108
  ### Direct access to data
109
109
 
110
- If doing per-layer operations isn't applicable for your application, you can read the pixel values for all layers (including VectorLayers) by calling `read_array` similarly to how you would for gdal. The data you access will be relative to the specified window - that is, if you've called either `set_window_for_intersection` or `set_window_for_union` then `read_array` will be relative to that and Yirgacheffe will clip or expand the data with zero values as necessary.
110
+ If doing per-layer operations isn't applicable for your application, you can read the pixel values for all layers (including VectorLayers) by calling `read_array` similarly to how you would for GDAL. The data you access will be relative to the specified window - that is, if you've called either `set_window_for_intersection` or `set_window_for_union` then `read_array` will be relative to that and Yirgacheffe will clip or expand the data with zero values as necessary.
111
111
 
112
112
 
113
113
  ## Layer types
@@ -160,6 +160,7 @@ with RasterLayer.layer_from_file('test1.tif') as source:
160
160
  scaled = RasterLayer.scaled_raster_from_raster(source, PixelScale(0.0001, -0.0001), 'scaled.tif')
161
161
  ```
162
162
 
163
+ If the data is from a GeoTIFF that has a nodata value specified, then pixel values with that specified nodata value in them will be converted to NaN. You can override that by providing `ignore_nodata=True` as an optional argument to `layer_from_file` (or with the new 2.0 API, `read_raster`). You can find out if a layer has a nodata value by accessing the `nodata` property - it is None if there is no such value.
163
164
 
164
165
  ### VectorLayer
165
166
 
@@ -229,6 +230,9 @@ with yg.read_rasters(['tile_N10_E10.tif', 'tile_N20_E10.tif']) as all_tiles:
229
230
  ...
230
231
  ```
231
232
 
233
+ If any of the layers have a `nodata` value specified, then any pixel with that value will be masked out to allow data from other layers to be visible.
234
+
235
+
232
236
  ### TiledGroupLayer
233
237
 
234
238
  This is a specialisation of GroupLayer, which you can use if your layers are all the same size and form a grid, as is often the case with map tiles. In this case the rendering code can be optimised and this class is significantly faster that GroupLayer.
@@ -11,6 +11,7 @@ tests/test_group.py
11
11
  tests/test_h3layer.py
12
12
  tests/test_intersection.py
13
13
  tests/test_multiband.py
14
+ tests/test_nodata.py
14
15
  tests/test_openers.py
15
16
  tests/test_operators.py
16
17
  tests/test_optimisation.py
File without changes
File without changes
File without changes