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,203 @@
1
+ from math import ceil, floor
2
+ from typing import Any
3
+
4
+ import h3
5
+ import numpy as np
6
+ from osgeo import gdal
7
+
8
+ from ..rounding import round_up_pixels
9
+ from ..window import Area, PixelScale, Window
10
+ from .base import YirgacheffeLayer
11
+ from ..backends import backend
12
+
13
+ class H3CellLayer(YirgacheffeLayer):
14
+
15
+ def __init__(self, cell_id: str, scale: PixelScale, projection: str):
16
+ if not h3.is_valid_cell(cell_id):
17
+ raise ValueError(f"{cell_id} is not a valid H3 cell identifier")
18
+ self.cell_id = cell_id
19
+ self.zoom = h3.get_resolution(cell_id)
20
+
21
+ self.cell_boundary = h3.cell_to_boundary(cell_id)
22
+
23
+ abs_xstep, abs_ystep = abs(scale.xstep), abs(scale.ystep)
24
+ area = Area(
25
+ left=(floor(min(x[1] for x in self.cell_boundary) / abs_xstep) * abs_xstep),
26
+ top=(ceil(max(x[0] for x in self.cell_boundary) / abs_ystep) * abs_ystep),
27
+ right=(ceil(max(x[1] for x in self.cell_boundary) / abs_xstep) * abs_xstep),
28
+ bottom=(floor(min(x[0] for x in self.cell_boundary) / abs_ystep) * abs_ystep),
29
+ )
30
+
31
+ # Statistically, most hex tiles will be within a projection, but some will wrap around
32
+ # the edge, so check if we're hit one of those cases.
33
+ self.centre = h3.cell_to_latlng(cell_id)
34
+
35
+ # Due to time constraints, and that most geospatial data we are working with stops before the poles
36
+ # I'm going to explicitly not handle the poles right now, where you get different distortions from
37
+ # at the 180 line:
38
+ if not area.bottom < self.centre[0] < area.top:
39
+ raise NotImplementedError("Distortion at poles not currently handled")
40
+
41
+ # For wrap around due to hitting the longitunal wrap, we currently just do the naive thing and
42
+ # project a band for the full width of the projection. I have a plan to fix this, as I'd like layers
43
+ # to have sublayers, allowing us to handle vector layers more efficiently, but curently we have a
44
+ # deadline, and given how infrequently we hit this case, doing the naive thing for now is sufficient
45
+ if (abs(area.left - area.right)) > 180.0:
46
+ left = (-180.0 / abs_xstep) * abs_xstep
47
+ if left < -180.0:
48
+ left += abs_xstep
49
+ right = (180.0 / abs_xstep) * abs_xstep
50
+ if right > 180.0:
51
+ right -= abs_xstep
52
+ area = Area(
53
+ left=left,
54
+ right=right,
55
+ top=area.top,
56
+ bottom=area.bottom,
57
+ )
58
+
59
+ super().__init__(area, scale, projection)
60
+ self._window = Window(
61
+ xoff=0,
62
+ yoff=0,
63
+ xsize=round_up_pixels((area.right - area.left) / scale.xstep, abs_xstep),
64
+ ysize=round_up_pixels((area.bottom - area.top) / scale.ystep, abs_ystep),
65
+ )
66
+ self._raster_xsize = self.window.xsize
67
+ self._raster_ysize = self.window.ysize
68
+
69
+ # see comment in read_array for why we're doing this
70
+ sorted_lats = [x[0] for x in self.cell_boundary]
71
+ sorted_lats.sort()
72
+ self._raster_safe_bounds = (
73
+ (sorted_lats[-2] / abs_ystep) * abs_ystep,
74
+ (sorted_lats[1] / abs_ystep) * abs_ystep,
75
+ )
76
+
77
+
78
+ @property
79
+ def datatype(self) -> int:
80
+ return gdal.GDT_Float64
81
+
82
+ def read_array_with_window(self, xoffset: int, yoffset: int, xsize: int, ysize: int, window: Window) -> Any:
83
+ if (xsize <= 0) or (ysize <= 0):
84
+ raise ValueError("Request dimensions must be positive and non-zero")
85
+
86
+ # We have two paths: one for the common case where the hex cell doesn't cross 180˚ longitude,
87
+ # and another case for where it does
88
+ max_width_projection = self.area.right - self.area.left
89
+ if max_width_projection < 180:
90
+
91
+ target_window = Window(
92
+ window.xoff + xoffset,
93
+ window.yoff + yoffset,
94
+ xsize,
95
+ ysize
96
+ )
97
+ source_window = Window(
98
+ xoff=0,
99
+ yoff=0,
100
+ xsize=self._raster_xsize,
101
+ ysize=self._raster_ysize,
102
+ )
103
+ try:
104
+ intersection = Window.find_intersection([source_window, target_window])
105
+ except ValueError:
106
+ return 0.0
107
+
108
+ subset = np.zeros((intersection.ysize, intersection.xsize))
109
+
110
+ start_x = self._active_area.left + ((intersection.xoff - self.window.xoff) * self._pixel_scale.xstep)
111
+ start_y = self._active_area.top + ((intersection.yoff - self.window.yoff) * self._pixel_scale.ystep)
112
+
113
+ for ypixel in range(intersection.ysize):
114
+ # The latlng_to_cell is quite expensive, so ideally we want to avoid
115
+ # calling latlng_to_cell for every pixel, though I do want to do that
116
+ # rather than infer from the cell_boundary, as this way I'm ensured
117
+ # I get an authoritative result and no pixel falls between the cracks.
118
+
119
+ # Originally I just tried the old-school way of finding the left-most
120
+ # and the right-most pixel per row, and then filling between, which in
121
+ # theory should work fine for rasterising hexagons, but in certain places
122
+ # the map projection distorts the edges sufficiently they become concave,
123
+ # and that approach lead to over filling, which caused issues with cells
124
+ # overlapping.
125
+
126
+ # The current implementation tries to hedge its bets by looking where the
127
+ # lowest and highest edges are (stored in _raster_safe_bounds) and then
128
+ # only doing the fill between left and right within those bounds. I suspect
129
+ # longer term this will need some padding, but it works for the current set
130
+ # of known failures in test_optimisation. As we hit more issues we should
131
+ # expand that test case before tweaking here. This is quite wasteful, as it's
132
+ # only a tiny number of cells that have convex edges, so in future it'd be
133
+ # interesting to see if we can infer when that is.
134
+
135
+ lat = start_y + (ypixel * self._pixel_scale.ystep)
136
+
137
+ if self._raster_safe_bounds[0] < lat < self._raster_safe_bounds[1]:
138
+ # In "safe" zone, try to be clever
139
+ left_most = intersection.xsize + 1
140
+ right_most = -1
141
+
142
+ for xpixel in range(intersection.xsize):
143
+ lng = start_x + (xpixel * self._pixel_scale.xstep)
144
+ this_cell = h3.latlng_to_cell(lat, lng, self.zoom)
145
+ if this_cell == self.cell_id:
146
+ left_most = xpixel
147
+ break
148
+
149
+ for xpixel in range(intersection.xsize - 1, left_most - 1, -1):
150
+ lng = start_x + (xpixel * self._pixel_scale.xstep)
151
+ this_cell = h3.latlng_to_cell(lat, lng, self.zoom)
152
+ if this_cell == self.cell_id:
153
+ right_most = xpixel
154
+ break
155
+
156
+ for xpixel in range(left_most, right_most + 1, 1):
157
+ subset[ypixel][xpixel] = 1.0
158
+ else:
159
+ # Not in safe zone, be diligent.
160
+ for xpixel in range(intersection.xsize):
161
+ lng = start_x + (xpixel * self._pixel_scale.xstep)
162
+ this_cell = h3.latlng_to_cell(lat, lng, self.zoom)
163
+ if this_cell == self.cell_id:
164
+ subset[ypixel][xpixel] = 1.0
165
+
166
+ return backend.pad(
167
+ backend.promote(subset),
168
+ (
169
+ (
170
+ (intersection.yoff - self.window.yoff) - yoffset,
171
+ (ysize - ((intersection.yoff - self.window.yoff) + intersection.ysize)) + yoffset,
172
+ ),
173
+ (
174
+ (intersection.xoff - self.window.xoff) - xoffset,
175
+ xsize - ((intersection.xoff - self.window.xoff) + intersection.xsize) + xoffset,
176
+ )
177
+ ),
178
+ 'constant'
179
+ )
180
+ else:
181
+ # This handles the case where the cell wraps over 180˚ longitude
182
+ res = np.zeros((ysize, xsize))
183
+
184
+ left = min(x[1] for x in self.cell_boundary if x[1] > 0.0)
185
+ right = max(x[1] for x in self.cell_boundary if x[1] < 0.0) + 360.0
186
+ max_width_projection = right - left
187
+ max_width = ceil(max_width_projection / self._pixel_scale.xstep)
188
+
189
+ for ypixel in range(yoffset, yoffset + ysize):
190
+ lat = self._active_area.top + (ypixel * self._pixel_scale.ystep)
191
+
192
+ for xpixel in range(xoffset, min(xoffset + xsize, max_width)):
193
+ lng = self._active_area.left + (xpixel * self._pixel_scale.xstep)
194
+ this_cell = h3.latlng_to_cell(lat, lng, self.zoom)
195
+ if this_cell == self.cell_id:
196
+ res[ypixel - yoffset][xpixel - xoffset] = 1.0
197
+
198
+ for xpixel in range(xoffset + xsize - 1, xoffset + xsize - max_width, -1):
199
+ lng = self._active_area.left + (xpixel * self._pixel_scale.xstep)
200
+ this_cell = h3.latlng_to_cell(lat, lng, self.zoom)
201
+ if this_cell == self.cell_id:
202
+ res[ypixel - yoffset][xpixel - xoffset] = 1.0
203
+ return backend.promote(res)
@@ -0,0 +1,333 @@
1
+ import math
2
+ import os
3
+ from typing import Any, Optional, TypeVar, Union
4
+
5
+ import numpy as np
6
+ from osgeo import gdal
7
+
8
+ from .. import WGS_84_PROJECTION
9
+ from ..window import Area, PixelScale, Window
10
+ from ..rounding import round_up_pixels
11
+ from .base import YirgacheffeLayer
12
+ from ..backends import backend
13
+
14
+ # Still to early to require Python 3.11 :/
15
+ RasterLayerT = TypeVar("RasterLayerT", bound="RasterLayer")
16
+
17
+ class InvalidRasterBand(Exception):
18
+ def __init__ (self, band):
19
+ self.band = band
20
+
21
+ class RasterLayer(YirgacheffeLayer):
22
+ """Layer provides a wrapper around a gdal dataset/band that also records offset state so that
23
+ we can work with maps over different geographic regions but work withing a particular frame
24
+ of reference."""
25
+
26
+ @staticmethod
27
+ def empty_raster_layer(
28
+ area: Area,
29
+ scale: PixelScale,
30
+ datatype: int,
31
+ filename: Optional[str]=None,
32
+ projection: str=WGS_84_PROJECTION,
33
+ name: Optional[str]=None,
34
+ compress: bool=True,
35
+ nodata: Optional[Union[float,int]]=None,
36
+ nbits: Optional[int]=None,
37
+ threads: Optional[int]=None,
38
+ bands: int=1
39
+ ) -> RasterLayerT:
40
+ abs_xstep, abs_ystep = abs(scale.xstep), abs(scale.ystep)
41
+
42
+ # We treat the provided area as aspirational, and we need to align it to pixel boundaries
43
+ pixel_friendly_area = Area(
44
+ left=math.floor(area.left / abs_xstep) * abs_xstep,
45
+ right=math.ceil(area.right / abs_xstep) * abs_xstep,
46
+ top=math.ceil(area.top / abs_ystep) * abs_ystep,
47
+ bottom=math.floor(area.bottom / abs_ystep) * abs_ystep,
48
+ )
49
+
50
+ options = []
51
+ if threads is not None:
52
+ options.append(f"NUM_THREADS={threads}")
53
+ if nbits is not None:
54
+ options.append(f"NBITS={nbits}")
55
+
56
+ if filename:
57
+ driver = gdal.GetDriverByName('GTiff')
58
+ options.append('BIGTIFF=YES')
59
+ if compress:
60
+ options.append('COMPRESS=LZW')
61
+ else:
62
+ options.append('COMPRESS=NONE')
63
+ else:
64
+ driver = gdal.GetDriverByName('mem')
65
+ filename = 'mem'
66
+ compress = False
67
+ dataset = driver.Create(
68
+ filename,
69
+ round_up_pixels((pixel_friendly_area.right - pixel_friendly_area.left) / abs_xstep, abs_xstep),
70
+ round_up_pixels((pixel_friendly_area.top - pixel_friendly_area.bottom) / abs_ystep, abs_ystep),
71
+ bands,
72
+ datatype,
73
+ options
74
+ )
75
+ dataset.SetGeoTransform([
76
+ pixel_friendly_area.left, scale.xstep, 0.0, pixel_friendly_area.top, 0.0, scale.ystep
77
+ ])
78
+ dataset.SetProjection(projection)
79
+ if nodata is not None:
80
+ dataset.GetRasterBand(1).SetNoDataValue(nodata)
81
+ return RasterLayer(dataset, name=name)
82
+
83
+ @staticmethod
84
+ def empty_raster_layer_like(
85
+ layer: Any,
86
+ filename: Optional[str]=None,
87
+ area: Optional[Area]=None,
88
+ datatype: Optional[int]=None,
89
+ compress: bool=True,
90
+ nodata: Optional[Union[float,int]]=None,
91
+ nbits: Optional[int]=None,
92
+ threads: Optional[int]=None,
93
+ bands: int=1
94
+ ) -> RasterLayerT:
95
+ width = layer.window.xsize
96
+ height = layer.window.ysize
97
+ if area is None:
98
+ area = layer.area
99
+ assert area is not None
100
+
101
+ scale = layer.pixel_scale
102
+ if scale is None:
103
+ raise ValueError("Can not work out area without explicit pixel scale")
104
+ abs_xstep, abs_ystep = abs(scale.xstep), abs(scale.ystep)
105
+ width = round_up_pixels((area.right - area.left) / abs_xstep, abs_xstep)
106
+ height = round_up_pixels((area.top - area.bottom) / abs_ystep, abs_ystep)
107
+ geo_transform = (
108
+ area.left, scale.xstep, 0.0, area.top, 0.0, scale.ystep
109
+ )
110
+
111
+ options = []
112
+ if threads is not None:
113
+ options.append(f"NUM_THREADS={threads}")
114
+ if nbits is not None:
115
+ options.append(f"NBITS={nbits}")
116
+
117
+ if filename:
118
+ driver = gdal.GetDriverByName('GTiff')
119
+ options.append('BIGTIFF=YES')
120
+ if compress:
121
+ options.append('COMPRESS=LZW')
122
+ else:
123
+ options.append('COMPRESS=NONE')
124
+ else:
125
+ driver = gdal.GetDriverByName('mem')
126
+ filename = 'mem'
127
+ compress = False
128
+ dataset = driver.Create(
129
+ filename,
130
+ width,
131
+ height,
132
+ bands,
133
+ datatype if datatype is not None else layer.datatype,
134
+ options,
135
+ )
136
+ dataset.SetGeoTransform(geo_transform)
137
+ dataset.SetProjection(layer.projection)
138
+ if nodata is not None:
139
+ dataset.GetRasterBand(1).SetNoDataValue(nodata)
140
+
141
+ return RasterLayer(dataset)
142
+
143
+ @classmethod
144
+ def scaled_raster_from_raster(
145
+ cls,
146
+ source: RasterLayerT,
147
+ new_pixel_scale: PixelScale,
148
+ filename: Optional[str]=None,
149
+ compress: bool=True,
150
+ algorithm: int=gdal.GRA_NearestNeighbour,
151
+ ) -> RasterLayerT:
152
+ source_dataset = source._dataset
153
+ old_pixel_scale = source.pixel_scale
154
+ assert old_pixel_scale
155
+
156
+ x_scale = old_pixel_scale.xstep / new_pixel_scale.xstep
157
+ y_scale = old_pixel_scale.ystep / new_pixel_scale.ystep
158
+ new_width = round_up_pixels(source_dataset.RasterXSize * x_scale,
159
+ abs(new_pixel_scale.xstep))
160
+ new_height = round_up_pixels(source_dataset.RasterYSize * y_scale,
161
+ abs(new_pixel_scale.ystep))
162
+
163
+ # in yirgacheffe we like to have things aligned to the pixel_scale, so work
164
+ # out new top left corner
165
+ new_left = math.floor((source.area.left / new_pixel_scale.xstep)) * new_pixel_scale.xstep
166
+ new_top = math.ceil((source.area.top / new_pixel_scale.ystep)) * new_pixel_scale.ystep
167
+
168
+ # now build a target dataset
169
+ options = []
170
+ if filename:
171
+ driver = gdal.GetDriverByName('GTiff')
172
+ options.append('BIGTIFF=YES')
173
+ if compress:
174
+ options.append('COMPRESS=LZW')
175
+ else:
176
+ driver = gdal.GetDriverByName('mem')
177
+ filename = 'mem'
178
+ compress = False
179
+ dataset = driver.Create(
180
+ filename,
181
+ new_width,
182
+ new_height,
183
+ 1,
184
+ source.datatype,
185
+ options
186
+ )
187
+ dataset.SetGeoTransform((
188
+ new_left, new_pixel_scale.xstep, 0.0,
189
+ new_top, 0.0, new_pixel_scale.ystep
190
+ ))
191
+ dataset.SetProjection(source_dataset.GetProjection())
192
+
193
+ # now use gdal to do the reprojection
194
+ gdal.ReprojectImage(source_dataset, dataset, eResampleAlg=algorithm)
195
+
196
+ return RasterLayer(dataset)
197
+
198
+ @classmethod
199
+ def layer_from_file(cls, filename: str, band: int = 1) -> RasterLayerT:
200
+ try:
201
+ dataset = gdal.Open(filename, gdal.GA_ReadOnly)
202
+ except RuntimeError as exc:
203
+ # With exceptions on GDAL now returns the wrong (IMHO) exception
204
+ raise FileNotFoundError(filename) from exc
205
+ try:
206
+ _ = dataset.GetRasterBand(band)
207
+ except RuntimeError as exc:
208
+ raise InvalidRasterBand(band) from exc
209
+ return cls(dataset, filename, band)
210
+
211
+ def __init__(self, dataset: gdal.Dataset, name: Optional[str] = None, band: int = 1):
212
+ if not dataset:
213
+ raise ValueError("None is not a valid dataset")
214
+
215
+ transform = dataset.GetGeoTransform()
216
+ scale = PixelScale(transform[1], transform[5])
217
+ area = Area(
218
+ left=transform[0],
219
+ top=transform[3],
220
+ right=transform[0] + (dataset.RasterXSize * scale.xstep),
221
+ bottom=transform[3] + (dataset.RasterYSize * scale.ystep),
222
+ )
223
+
224
+ super().__init__(
225
+ area,
226
+ scale,
227
+ dataset.GetProjection(),
228
+ name=name
229
+ )
230
+
231
+ # The constructor works out the window from the area
232
+ # so sanity check that the calculated window matches the
233
+ # dataset's dimensions
234
+ assert self.window == Window(0, 0, dataset.RasterXSize, dataset.RasterYSize)
235
+
236
+ self._dataset = dataset
237
+ self._dataset_path = dataset.GetDescription()
238
+ self._band = band
239
+ self._raster_xsize = dataset.RasterXSize
240
+ self._raster_ysize = dataset.RasterYSize
241
+
242
+ def close(self):
243
+ try:
244
+ if self._dataset:
245
+ try:
246
+ self._dataset.Close()
247
+ except AttributeError:
248
+ pass
249
+ del self._dataset
250
+ except AttributeError:
251
+ # Don't error if close was already called
252
+ pass
253
+
254
+ def __getstate__(self) -> object:
255
+ # Only support pickling on file backed layers (ideally read only ones...)
256
+ if not os.path.isfile(self._dataset_path):
257
+ raise ValueError("Can not pickle layer that is not file backed.")
258
+ odict = self.__dict__.copy()
259
+ self._park()
260
+ del odict['_dataset']
261
+ return odict
262
+
263
+ def __setstate__(self, state):
264
+ self.__dict__.update(state)
265
+ self._unpark()
266
+
267
+ def _park(self):
268
+ try:
269
+ self._dataset.Close()
270
+ except AttributeError:
271
+ pass
272
+ self._dataset = None
273
+
274
+ def _unpark(self):
275
+ if getattr(self, "_dataset", None) is None:
276
+ try:
277
+ self._dataset = gdal.Open(self._dataset_path)
278
+ except RuntimeError as exc:
279
+ raise FileNotFoundError(f"Failed to open pickled raster {self._dataset_path}") from exc
280
+
281
+ @property
282
+ def datatype(self) -> int:
283
+ if self._dataset is None:
284
+ self._unpark()
285
+ assert self._dataset
286
+ return self._dataset.GetRasterBand(1).DataType
287
+
288
+ def read_array_with_window(self, xoffset: int, yoffset: int, xsize: int, ysize: int, window: Window) -> Any:
289
+ if self._dataset is None:
290
+ self._unpark()
291
+ assert self._dataset
292
+
293
+ if (xsize <= 0) or (ysize <= 0):
294
+ raise ValueError("Request dimensions must be positive and non-zero")
295
+
296
+ # if we're dealing with an intersection, we can just read the data directly,
297
+ # otherwise we need to read the data into another array with suitable padding
298
+ target_window = Window(
299
+ window.xoff + xoffset,
300
+ window.yoff + yoffset,
301
+ xsize,
302
+ ysize
303
+ )
304
+ source_window = Window(
305
+ xoff=0,
306
+ yoff=0,
307
+ xsize=self._raster_xsize,
308
+ ysize=self._raster_ysize,
309
+ )
310
+ try:
311
+ intersection = Window.find_intersection([source_window, target_window])
312
+ except ValueError:
313
+ return backend.zeros((ysize, xsize))
314
+
315
+ if target_window == intersection:
316
+ # The target window is a subset of or equal to the source, so we can just ask for the data
317
+ data = backend.promote(self._dataset.GetRasterBand(self._band).ReadAsArray(*intersection.as_array_args))
318
+ return data
319
+ else:
320
+ # We should read the intersection from the array, and the rest should be zeros
321
+ subset = backend.promote(self._dataset.GetRasterBand(self._band).ReadAsArray(*intersection.as_array_args))
322
+ region = np.array((
323
+ (
324
+ (intersection.yoff - window.yoff) - yoffset,
325
+ (ysize - ((intersection.yoff - window.yoff) + intersection.ysize)) + yoffset,
326
+ ),
327
+ (
328
+ (intersection.xoff - window.xoff) - xoffset,
329
+ xsize - ((intersection.xoff - window.xoff) + intersection.xsize) + xoffset,
330
+ )
331
+ )).astype(int)
332
+ data = backend.pad(subset, region, mode='constant')
333
+ return data
@@ -0,0 +1,94 @@
1
+ from math import floor, ceil
2
+ from typing import Any, Optional
3
+
4
+ from skimage import transform
5
+
6
+ from ..window import PixelScale, Window
7
+ from .rasters import RasterLayer, YirgacheffeLayer
8
+ from ..backends import backend
9
+
10
+
11
+ class RescaledRasterLayer(YirgacheffeLayer):
12
+ """RescaledRaster dynamically rescales a raster, so to you to work with multiple layers at
13
+ different scales without having to store unnecessary data. """
14
+
15
+ @classmethod
16
+ def layer_from_file(
17
+ cls,
18
+ filename: str,
19
+ pixel_scale: PixelScale,
20
+ band: int = 1,
21
+ nearest_neighbour: bool = True,
22
+ ):
23
+ src = RasterLayer.layer_from_file(filename, band=band)
24
+ return RescaledRasterLayer(src, pixel_scale, nearest_neighbour, src.name)
25
+
26
+ def __init__(
27
+ self,
28
+ src: RasterLayer,
29
+ pixel_scale: PixelScale,
30
+ nearest_neighbour: bool = True,
31
+ name: Optional[str] = None,
32
+ ):
33
+ super().__init__(
34
+ src.area,
35
+ pixel_scale=pixel_scale,
36
+ projection=src.projection,
37
+ name=name
38
+ )
39
+
40
+ self._src = src
41
+ self._nearest_neighbour = nearest_neighbour
42
+
43
+ src_pixel_scale = src.pixel_scale
44
+ assert src_pixel_scale # from raster we should always have one
45
+
46
+ self._x_scale = src_pixel_scale.xstep / pixel_scale.xstep
47
+ self._y_scale = src_pixel_scale.ystep / pixel_scale.ystep
48
+
49
+ def close(self):
50
+ self._src.close()
51
+
52
+ def _park(self):
53
+ self._src._park()
54
+
55
+ def _unpark(self):
56
+ self._src._unpark()
57
+
58
+ @property
59
+ def datatype(self) -> int:
60
+ return self._src.datatype
61
+
62
+ def read_array_with_window(self, xoffset: int, yoffset: int, xsize: int, ysize: int, window: Window) -> Any:
63
+
64
+ # to avoid aliasing issues, we try to scale to the nearest pixel
65
+ # and recrop when scaling bigger
66
+
67
+ xoffset = xoffset + window.xoff
68
+ yoffset = yoffset + window.yoff
69
+
70
+ src_x_offset = floor(xoffset / self._x_scale)
71
+ src_y_offset = floor(yoffset / self._y_scale)
72
+
73
+ diff_x = floor(((xoffset / self._x_scale) - src_x_offset) * self._x_scale)
74
+ diff_y = floor(((yoffset / self._x_scale) - src_y_offset) * self._x_scale)
75
+
76
+ src_x_width = ceil((xsize + diff_x) / self._x_scale)
77
+ src_y_width = ceil((ysize + diff_y) / self._y_scale)
78
+
79
+ # Get the matching src data
80
+ src_data = backend.demote_array(self._src.read_array(
81
+ src_x_offset,
82
+ src_y_offset,
83
+ src_x_width,
84
+ src_y_width
85
+ ))
86
+
87
+ scaled = transform.resize(
88
+ src_data,
89
+ (src_y_width * self._y_scale, src_x_width * self._x_scale),
90
+ order=(0 if self._nearest_neighbour else 1),
91
+ anti_aliasing=(not self._nearest_neighbour)
92
+ )
93
+
94
+ return backend.promote(scaled[diff_y:(diff_y + ysize),diff_x:(diff_x + xsize)])