yirgacheffe 1.2.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.
@@ -0,0 +1,380 @@
1
+ import os
2
+ from math import ceil, floor
3
+ from typing import Any, Optional, Tuple, Union
4
+ from typing_extensions import NotRequired
5
+
6
+ from osgeo import gdal, ogr
7
+
8
+ from ..window import Area, PixelScale
9
+ from .base import YirgacheffeLayer
10
+ from .rasters import RasterLayer
11
+ from ..backends import backend
12
+
13
+ def _validate_burn_value(burn_value: Any, layer: ogr.Layer) -> int: # pylint: disable=R0911
14
+ if isinstance(burn_value, str):
15
+ # burn value is field name, so validate it
16
+ index = layer.FindFieldIndex(burn_value, True)
17
+ if index < 0:
18
+ raise ValueError("Burn value not found as field")
19
+ # if the user hasn't specified, pick datatype from
20
+ # fiend definition.
21
+ definition = layer.GetLayerDefn()
22
+ field = definition.GetFieldDefn(index)
23
+ typename = field.GetTypeName()
24
+ if typename == "Integer":
25
+ return gdal.GDT_Int64
26
+ elif typename == "Real":
27
+ return gdal.GDT_Float64
28
+ else:
29
+ raise ValueError(f"Can't set datatype {typename} for burn value {burn_value}")
30
+ elif isinstance(burn_value, int):
31
+ if 0 <= burn_value <= 255:
32
+ return gdal.GDT_Byte
33
+ else:
34
+ unsigned = burn_value > 0
35
+ if unsigned:
36
+ if burn_value < (pow(2, 16)):
37
+ return gdal.GDT_UInt16
38
+ elif burn_value < (pow(2, 32)):
39
+ return gdal.GDT_UInt32
40
+ else:
41
+ return gdal.GDT_UInt64
42
+ else:
43
+ if abs(burn_value) < (pow(2, 15)):
44
+ return gdal.GDT_Int16
45
+ elif abs(burn_value) < (pow(2, 31)):
46
+ return gdal.GDT_Int32
47
+ else:
48
+ return gdal.GDT_Int64
49
+ elif isinstance(burn_value, float):
50
+ return gdal.GDT_Float64
51
+ else:
52
+ raise ValueError(f"data type of burn value {burn_value} not supported")
53
+
54
+
55
+ class RasteredVectorLayer(RasterLayer):
56
+ """This layer takes a vector file and rasterises it for the given filter. Rasterization
57
+ up front like this is very expensive, so not recommended. Instead you should use
58
+ VectorLayer."""
59
+
60
+ @classmethod
61
+ def layer_from_file(
62
+ cls,
63
+ filename: str,
64
+ where_filter: Optional[str],
65
+ scale: PixelScale,
66
+ projection: str,
67
+ datatype: Optional[int] = None,
68
+ burn_value: Union[int,float,str] = 1,
69
+ ): # pylint: disable=W0221
70
+ vectors = ogr.Open(filename)
71
+ if vectors is None:
72
+ raise FileNotFoundError(filename)
73
+ layer = vectors.GetLayer()
74
+ if where_filter is not None:
75
+ layer.SetAttributeFilter(where_filter)
76
+
77
+ estimated_datatype = _validate_burn_value(burn_value, layer)
78
+ if datatype is None:
79
+ datatype = estimated_datatype
80
+
81
+ vector_layer = RasteredVectorLayer(
82
+ layer,
83
+ scale,
84
+ projection,
85
+ datatype=datatype,
86
+ burn_value=burn_value
87
+ )
88
+
89
+ # this is a gross hack, but unless you hold open the original file, you'll get
90
+ # a SIGSEGV when using the layers from it later, as some SWIG pointers outlive
91
+ # the original object being around
92
+ vector_layer._original = vectors
93
+ return vector_layer
94
+
95
+ def __init__(
96
+ self,
97
+ layer: ogr.Layer,
98
+ scale: PixelScale,
99
+ projection: str,
100
+ datatype: int = gdal.GDT_Byte,
101
+ burn_value: Union[int,float,str] = 1,
102
+ ):
103
+ if layer is None:
104
+ raise ValueError('No layer provided')
105
+ self.layer = layer
106
+
107
+ self._original = None
108
+
109
+ # work out region for mask
110
+ envelopes = []
111
+ layer.ResetReading()
112
+ feature = layer.GetNextFeature()
113
+ while feature:
114
+ geometry = feature.GetGeometryRef()
115
+ if geometry:
116
+ envelopes.append(geometry.GetEnvelope())
117
+ feature = layer.GetNextFeature()
118
+ if len(envelopes) == 0:
119
+ raise ValueError('No geometry found for')
120
+
121
+ # Get the area, but scale it to the pixel resolution that we're using. Note that
122
+ # the pixel scale GDAL uses can have -ve values, but those will mess up the
123
+ # ceil/floor math, so we use absolute versions when trying to round.
124
+ abs_xstep, abs_ystep = abs(scale.xstep), abs(scale.ystep)
125
+ area = Area(
126
+ left=floor(min(x[0] for x in envelopes) / abs_xstep) * abs_xstep,
127
+ top=ceil(max(x[3] for x in envelopes) / abs_ystep) * abs_ystep,
128
+ right=ceil(max(x[1] for x in envelopes) / abs_xstep) * abs_xstep,
129
+ bottom=floor(min(x[2] for x in envelopes) / abs_ystep) * abs_ystep,
130
+ )
131
+
132
+ # create new dataset for just that area
133
+ dataset = gdal.GetDriverByName('mem').Create(
134
+ 'mem',
135
+ round((area.right - area.left) / abs_xstep),
136
+ round((area.top - area.bottom) / abs_ystep),
137
+ 1,
138
+ datatype,
139
+ []
140
+ )
141
+ if not dataset:
142
+ raise MemoryError('Failed to create memory mask')
143
+
144
+ dataset.SetProjection(projection)
145
+ dataset.SetGeoTransform([area.left, scale.xstep, 0.0, area.top, 0.0, scale.ystep])
146
+ if isinstance(burn_value, (int, float)):
147
+ gdal.RasterizeLayer(dataset, [1], self.layer, burn_values=[burn_value], options=["ALL_TOUCHED=TRUE"])
148
+ elif isinstance(burn_value, str):
149
+ gdal.RasterizeLayer(dataset, [1], self.layer, options=[f"ATTRIBUTE={burn_value}", "ALL_TOUCHED=TRUE"])
150
+ else:
151
+ raise ValueError("Burn value for layer should be number or field name")
152
+ super().__init__(dataset)
153
+
154
+
155
+ class VectorLayer(YirgacheffeLayer):
156
+ """This layer takes a vector file and rasterises it for the given filter. Rasterization occurs only
157
+ when the data is fetched, so there is no explosive memeory cost, but fetching small units (e.g., one
158
+ line at a time) can be quite slow, so recommended that you fetch reasonable chunks each time (or
159
+ modify this class so that it chunks things internally)."""
160
+
161
+ @classmethod
162
+ def layer_from_file_like(
163
+ cls,
164
+ filename: str,
165
+ other_layer: YirgacheffeLayer,
166
+ where_filter: Optional[str]=None,
167
+ datatype: Optional[int] = None,
168
+ burn_value: Union[int,float,str] = 1,
169
+ ):
170
+ if other_layer is None:
171
+ raise ValueError("like layer can not be None")
172
+ if other_layer.pixel_scale is None:
173
+ raise ValueError("Reference layer must have pixel scale")
174
+
175
+ vectors = ogr.Open(filename)
176
+ if vectors is None:
177
+ raise FileNotFoundError(filename)
178
+ layer = vectors.GetLayer()
179
+ if where_filter is not None:
180
+ layer.SetAttributeFilter(where_filter)
181
+
182
+ vector_layer = VectorLayer(
183
+ layer,
184
+ other_layer.pixel_scale,
185
+ other_layer.projection,
186
+ name=filename,
187
+ datatype=datatype if datatype is not None else other_layer.datatype,
188
+ burn_value=burn_value,
189
+ anchor=(other_layer.area.left, other_layer.area.top),
190
+ )
191
+
192
+ # this is a gross hack, but unless you hold open the original file, you'll get
193
+ # a SIGSEGV when using the layers from it later, as some SWIG pointers outlive
194
+ # the original object being around
195
+ vector_layer._original = vectors
196
+ vector_layer._dataset_path = filename
197
+ vector_layer._filter = where_filter
198
+ return vector_layer
199
+
200
+
201
+ @classmethod
202
+ def layer_from_file(
203
+ cls,
204
+ filename: str,
205
+ where_filter: Optional[str],
206
+ scale: PixelScale,
207
+ projection: str,
208
+ datatype: Optional[int] = None,
209
+ burn_value: Union[int,float,str] = 1,
210
+ anchor: Tuple[float,float] = (0.0, 0.0)
211
+ ):
212
+ vectors = ogr.Open(filename)
213
+ if vectors is None:
214
+ raise FileNotFoundError(filename)
215
+ layer = vectors.GetLayer()
216
+ if where_filter is not None:
217
+ layer.SetAttributeFilter(where_filter)
218
+
219
+ estimated_datatype = _validate_burn_value(burn_value, layer)
220
+ if datatype is None:
221
+ datatype = estimated_datatype
222
+
223
+ vector_layer = VectorLayer(
224
+ layer,
225
+ scale,
226
+ projection,
227
+ name=filename,
228
+ datatype=datatype,
229
+ burn_value=burn_value,
230
+ anchor=anchor
231
+ )
232
+
233
+ # this is a gross hack, but unless you hold open the original file, you'll get
234
+ # a SIGSEGV when using the layers from it later, as some SWIG pointers outlive
235
+ # the original object being around
236
+ vector_layer._original = vectors
237
+ vector_layer._dataset_path = filename
238
+ vector_layer._filter = where_filter
239
+ return vector_layer
240
+
241
+ def __init__(
242
+ self,
243
+ layer: ogr.Layer,
244
+ scale: PixelScale,
245
+ projection: str,
246
+ name: Optional[str] = None,
247
+ datatype: int = gdal.GDT_Byte,
248
+ burn_value: Union[int,float,str] = 1,
249
+ anchor: Tuple[float,float] = (0.0, 0.0)
250
+ ):
251
+ if layer is None:
252
+ raise ValueError('No layer provided')
253
+ self.layer = layer
254
+ self.name = name
255
+
256
+ self._datatype = datatype
257
+
258
+ # If the burn value is a number, use it directly, if it's a string
259
+ # then assume it is a column name in the dataset
260
+ self.burn_value = burn_value
261
+
262
+ self._original = None
263
+ self._dataset_path = None
264
+ self._filter = None
265
+
266
+ # work out region for mask
267
+ envelopes = []
268
+ layer.ResetReading()
269
+ feature = layer.GetNextFeature()
270
+ while feature:
271
+ geometry = feature.GetGeometryRef()
272
+ if geometry:
273
+ envelopes.append(geometry.GetEnvelope())
274
+ feature = layer.GetNextFeature()
275
+ if len(envelopes) == 0:
276
+ raise ValueError('No geometry found')
277
+
278
+ # Get the area, but scale it to the pixel resolution that we're using. Note that
279
+ # the pixel scale GDAL uses can have -ve values, but those will mess up the
280
+ # ceil/floor math, so we use absolute versions when trying to round.
281
+ abs_xstep, abs_ystep = abs(scale.xstep), abs(scale.ystep)
282
+
283
+ # Lacking any other reference, we will make the raster align with
284
+ # (0.0, 0.0), if sometimes we want to align with an existing raster, so if
285
+ # an anchor is specified, ensure we use that as our pixel space alignment
286
+ x_anchor = anchor[0]
287
+ y_anchor = anchor[1]
288
+ left_shift = x_anchor - abs_xstep
289
+ right_shift = x_anchor
290
+ top_shift = y_anchor
291
+ bottom_shift = y_anchor - abs_ystep
292
+
293
+ area = Area(
294
+ left=(floor((min(x[0] for x in envelopes) - left_shift) / abs_xstep) * abs_xstep) + left_shift,
295
+ top=(ceil((max(x[3] for x in envelopes) - top_shift) / abs_ystep) * abs_ystep) + top_shift,
296
+ right=(ceil((max(x[1] for x in envelopes) - right_shift) / abs_xstep) * abs_xstep) + right_shift,
297
+ bottom=(floor((min(x[2] for x in envelopes) - bottom_shift) / abs_ystep) * abs_ystep) + bottom_shift,
298
+ )
299
+
300
+ super().__init__(area, scale, projection)
301
+
302
+ def __getstate__(self) -> object:
303
+ # Only support pickling on file backed layers (ideally read only ones...)
304
+ if not os.path.isfile(self._dataset_path):
305
+ raise ValueError("Can not pickle layer that is not file backed.")
306
+ odict = self.__dict__.copy()
307
+ del odict['_original']
308
+ del odict['layer']
309
+ return odict
310
+
311
+ def __setstate__(self, state):
312
+ vectors = ogr.Open(state['_dataset_path'])
313
+ if vectors is None:
314
+ raise FileNotFoundError(f"Failed to open pickled vectors {state['_dataset_path']}")
315
+ self.__dict__.update(state)
316
+ self._original = vectors
317
+ self.layer = vectors.GetLayer()
318
+ if self._filter is not None:
319
+ self.layer.SetAttributeFilter(self._filter)
320
+
321
+ def _park(self):
322
+ self._original = None
323
+
324
+ def _unpark(self):
325
+ if getattr(self, "_original", None) is None:
326
+ try:
327
+ self._original = ogr.Open(self._dataset_path)
328
+ except RuntimeError as exc:
329
+ raise FileNotFoundError(f"Failed to open pickled layer {self._dataset_path}") from exc
330
+ self.layer = self._original.GetLayer()
331
+ if self._filter is not None:
332
+ self.layer.SetAttributeFilter(self._filter)
333
+
334
+ @property
335
+ def datatype(self) -> int:
336
+ return self._datatype
337
+
338
+ def read_array_for_area(self, target_area: Area, x: int, y: int, width: int, height: int) -> Any:
339
+ if self._original is None:
340
+ self._unpark()
341
+ if (width <= 0) or (height <= 0):
342
+ raise ValueError("Request dimensions must be positive and non-zero")
343
+
344
+ # I did try recycling this object to save allocation/dealloction, but in practice it
345
+ # seemed to only make things slower (particularly as you need to zero the memory each time yourself)
346
+ dataset = gdal.GetDriverByName('mem').Create(
347
+ 'mem',
348
+ width,
349
+ height,
350
+ 1,
351
+ self.datatype,
352
+ []
353
+ )
354
+ if not dataset:
355
+ raise MemoryError('Failed to create memory mask')
356
+
357
+ dataset.SetProjection(self._projection)
358
+ dataset.SetGeoTransform([
359
+ target_area.left + (x * self._pixel_scale.xstep),
360
+ self._pixel_scale.xstep,
361
+ 0.0,
362
+ target_area.top + (y * self._pixel_scale.ystep),
363
+ 0.0,
364
+ self._pixel_scale.ystep
365
+ ])
366
+ if isinstance(self.burn_value, (int, float)):
367
+ gdal.RasterizeLayer(dataset, [1], self.layer, burn_values=[self.burn_value], options=["ALL_TOUCHED=TRUE"])
368
+ elif isinstance(self.burn_value, str):
369
+ gdal.RasterizeLayer(dataset, [1], self.layer, options=[f"ATTRIBUTE={self.burn_value}", "ALL_TOUCHED=TRUE"])
370
+ else:
371
+ raise ValueError("Burn value for layer should be number or field name")
372
+
373
+ res = backend.promote(dataset.ReadAsArray(0, 0, width, height))
374
+ return res
375
+
376
+ def read_array_with_window(self, _x, _y, _width, _height, _window) -> Any:
377
+ assert NotRequired
378
+
379
+ def read_array(self, x: int, y: int, width: int, height: int) -> Any:
380
+ return self.read_array_for_area(self._active_area, x, y, width, height)