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.
- yirgacheffe/__init__.py +17 -0
- yirgacheffe/backends/__init__.py +13 -0
- yirgacheffe/backends/enumeration.py +33 -0
- yirgacheffe/backends/mlx.py +156 -0
- yirgacheffe/backends/numpy.py +110 -0
- yirgacheffe/constants.py +1 -0
- yirgacheffe/h3layer.py +2 -0
- yirgacheffe/layers/__init__.py +44 -0
- yirgacheffe/layers/area.py +91 -0
- yirgacheffe/layers/base.py +265 -0
- yirgacheffe/layers/constant.py +41 -0
- yirgacheffe/layers/group.py +357 -0
- yirgacheffe/layers/h3layer.py +203 -0
- yirgacheffe/layers/rasters.py +333 -0
- yirgacheffe/layers/rescaled.py +94 -0
- yirgacheffe/layers/vectors.py +380 -0
- yirgacheffe/operators.py +738 -0
- yirgacheffe/rounding.py +57 -0
- yirgacheffe/window.py +141 -0
- yirgacheffe-1.2.0.dist-info/METADATA +473 -0
- yirgacheffe-1.2.0.dist-info/RECORD +25 -0
- yirgacheffe-1.2.0.dist-info/WHEEL +5 -0
- yirgacheffe-1.2.0.dist-info/entry_points.txt +2 -0
- yirgacheffe-1.2.0.dist-info/licenses/LICENSE +7 -0
- yirgacheffe-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -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)
|