rastr 0.4.0__py3-none-any.whl → 0.5.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 rastr might be problematic. Click here for more details.

rastr/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.4.0'
32
- __version_tuple__ = version_tuple = (0, 4, 0)
31
+ __version__ = version = '0.5.0'
32
+ __version_tuple__ = version_tuple = (0, 5, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
rastr/create.py CHANGED
@@ -13,7 +13,7 @@ from shapely.geometry import Point
13
13
 
14
14
  from rastr.gis.fishnet import create_point_grid, get_point_grid_shape
15
15
  from rastr.meta import RasterMeta
16
- from rastr.raster import RasterModel
16
+ from rastr.raster import Raster
17
17
 
18
18
  if TYPE_CHECKING:
19
19
  from collections.abc import Iterable
@@ -49,9 +49,9 @@ def raster_distance_from_polygon(
49
49
  *,
50
50
  raster_meta: RasterMeta,
51
51
  extent_polygon: Polygon | None = None,
52
- snap_raster: RasterModel | None = None,
52
+ snap_raster: Raster | None = None,
53
53
  show_pbar: bool = False,
54
- ) -> RasterModel:
54
+ ) -> Raster:
55
55
  """Make a raster where each cell's value is its centre's distance to a polygon.
56
56
 
57
57
  The raster should use a projected coordinate system.
@@ -116,7 +116,7 @@ def raster_distance_from_polygon(
116
116
  distances = np.where(mask, np.array([polygon.distance(pt) for pt in _pts]), np.nan)
117
117
  distance_raster = distances.reshape(x.shape)
118
118
 
119
- return RasterModel(arr=distance_raster, raster_meta=raster_meta)
119
+ return Raster(arr=distance_raster, raster_meta=raster_meta)
120
120
 
121
121
 
122
122
  def _pbar(iterable: Iterable[_T], *, desc: str | None = None) -> Iterable[_T]:
@@ -130,11 +130,11 @@ def full_raster(
130
130
  *,
131
131
  bounds: tuple[float, float, float, float],
132
132
  fill_value: float = np.nan,
133
- ) -> RasterModel:
133
+ ) -> Raster:
134
134
  """Create a raster with a specified fill value for all cells."""
135
135
  shape = get_point_grid_shape(bounds=bounds, cell_size=raster_meta.cell_size)
136
136
  arr = np.full(shape, fill_value, dtype=np.float32)
137
- return RasterModel(arr=arr, raster_meta=raster_meta)
137
+ return Raster(arr=arr, raster_meta=raster_meta)
138
138
 
139
139
 
140
140
  def rasterize_gdf(
@@ -142,7 +142,7 @@ def rasterize_gdf(
142
142
  *,
143
143
  raster_meta: RasterMeta,
144
144
  target_cols: list[str],
145
- ) -> list[RasterModel]:
145
+ ) -> list[Raster]:
146
146
  """Rasterize geometries from a GeoDataFrame.
147
147
 
148
148
  Supports polygons, points, linestrings, and other geometry types.
@@ -205,8 +205,8 @@ def rasterize_gdf(
205
205
  dtype=np.float32,
206
206
  )
207
207
 
208
- # Create RasterModel
209
- raster = RasterModel(arr=raster_array, raster_meta=raster_meta)
208
+ # Create Raster
209
+ raster = Raster(arr=raster_array, raster_meta=raster_meta)
210
210
  rasters.append(raster)
211
211
 
212
212
  return rasters
@@ -286,7 +286,7 @@ def raster_from_point_cloud(
286
286
  *,
287
287
  crs: CRS | str,
288
288
  cell_size: float | None = None,
289
- ) -> RasterModel:
289
+ ) -> Raster:
290
290
  """Create a raster from a point cloud via interpolation.
291
291
 
292
292
  Interpolation is only possible within the convex hull of the points. Outside of
@@ -320,8 +320,18 @@ def raster_from_point_cloud(
320
320
  if len(x) != len(y) or len(x) != len(z):
321
321
  msg = "Length of x, y, and z must be equal."
322
322
  raise ValueError(msg)
323
+ xy_finite_mask = np.isfinite(x) & np.isfinite(y)
324
+ if np.any(~xy_finite_mask):
325
+ msg = "Some (x,y) points are NaN-valued or non-finite. These will be ignored."
326
+ warnings.warn(msg, stacklevel=2)
327
+ x = x[xy_finite_mask]
328
+ y = y[xy_finite_mask]
329
+ z = z[xy_finite_mask]
323
330
  if len(x) < 3:
324
- msg = "At least three (x, y, z) points are required to triangulate a surface."
331
+ msg = (
332
+ "At least three valid (x, y, z) points are required to triangulate a "
333
+ "surface."
334
+ )
325
335
  raise ValueError(msg)
326
336
  # Check for duplicate (x, y) points
327
337
  xy_points = np.column_stack((x, y))
@@ -332,8 +342,8 @@ def raster_from_point_cloud(
332
342
  # Heuristic for cell size if not provided
333
343
  if cell_size is None:
334
344
  # Half the 5th percentile of nearest neighbor distances between the (x,y) points
335
- tree = KDTree(np.column_stack((x, y)))
336
- distances, _ = tree.query(np.column_stack((x, y)), k=2)
345
+ tree = KDTree(xy_points)
346
+ distances, _ = tree.query(xy_points, k=2)
337
347
  distances: np.ndarray
338
348
  cell_size = float(np.percentile(distances[distances > 0], 5)) / 2
339
349
 
@@ -378,4 +388,4 @@ def raster_from_point_cloud(
378
388
  crs=crs,
379
389
  transform=transform,
380
390
  )
381
- return RasterModel(arr=arr, raster_meta=raster_meta)
391
+ return Raster(arr=arr, raster_meta=raster_meta)
rastr/io.py CHANGED
@@ -9,7 +9,7 @@ import rasterio.merge
9
9
  from pyproj.crs.crs import CRS
10
10
 
11
11
  from rastr.meta import RasterMeta
12
- from rastr.raster import RasterModel
12
+ from rastr.raster import Raster
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from numpy.typing import NDArray
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
17
17
 
18
18
  def read_raster_inmem(
19
19
  raster_path: Path | str, *, crs: CRS | str | None = None
20
- ) -> RasterModel:
20
+ ) -> Raster:
21
21
  """Read raster data from a file and return an in-memory Raster object."""
22
22
  crs = CRS.from_user_input(crs) if crs is not None else None
23
23
 
@@ -35,13 +35,13 @@ def read_raster_inmem(
35
35
  arr[arr == nodata] = np.nan
36
36
 
37
37
  raster_meta = RasterMeta(cell_size=cell_size, crs=crs, transform=transform)
38
- raster_obj = RasterModel(arr=arr, raster_meta=raster_meta)
38
+ raster_obj = Raster(arr=arr, raster_meta=raster_meta)
39
39
  return raster_obj
40
40
 
41
41
 
42
42
  def read_raster_mosaic_inmem(
43
43
  mosaic_dir: Path | str, *, glob: str = "*.tif", crs: CRS | None = None
44
- ) -> RasterModel:
44
+ ) -> Raster:
45
45
  """Read a raster mosaic from a directory and return an in-memory Raster object.
46
46
 
47
47
  This assumes that all rasters have the same metadata, e.g. coordinate system,
@@ -87,7 +87,7 @@ def read_raster_mosaic_inmem(
87
87
  arr = arr.squeeze().astype(np.float64)
88
88
 
89
89
  raster_meta = RasterMeta(cell_size=cell_size, crs=crs, transform=transform)
90
- raster_obj = RasterModel(arr=arr, raster_meta=raster_meta)
90
+ raster_obj = Raster(arr=arr, raster_meta=raster_meta)
91
91
  return raster_obj
92
92
  finally:
93
93
  for src in sources:
rastr/raster.py CHANGED
@@ -4,17 +4,20 @@ from __future__ import annotations
4
4
 
5
5
  import importlib.util
6
6
  import warnings
7
+ from collections.abc import Collection
7
8
  from contextlib import contextmanager
8
9
  from pathlib import Path
9
10
  from typing import TYPE_CHECKING, Any, Literal, overload
10
11
 
11
12
  import numpy as np
12
13
  import numpy.ma
14
+ import rasterio.features
13
15
  import rasterio.plot
14
16
  import rasterio.sample
15
17
  import rasterio.transform
16
18
  import skimage.measure
17
19
  from pydantic import BaseModel, InstanceOf, field_validator
20
+ from pyproj import Transformer
18
21
  from pyproj.crs.crs import CRS
19
22
  from rasterio.enums import Resampling
20
23
  from rasterio.io import MemoryFile
@@ -29,10 +32,14 @@ if TYPE_CHECKING:
29
32
  from collections.abc import Callable, Generator
30
33
 
31
34
  import geopandas as gpd
35
+ from affine import Affine
36
+ from branca.colormap import LinearColormap as BrancaLinearColormap
32
37
  from folium import Map
33
38
  from matplotlib.axes import Axes
39
+ from matplotlib.image import AxesImage
34
40
  from numpy.typing import ArrayLike, NDArray
35
41
  from rasterio.io import BufferedDatasetWriter, DatasetReader, DatasetWriter
42
+ from shapely import MultiPolygon
36
43
  from typing_extensions import Self
37
44
 
38
45
  try:
@@ -45,17 +52,28 @@ FOLIUM_INSTALLED = importlib.util.find_spec("folium") is not None
45
52
  BRANCA_INSTALLED = importlib.util.find_spec("branca") is not None
46
53
  MATPLOTLIB_INSTALLED = importlib.util.find_spec("matplotlib") is not None
47
54
 
55
+ CONTOUR_PERTURB_EPS = 1e-10
56
+
48
57
 
49
58
  class RasterCellArrayShapeError(ValueError):
50
59
  """Custom error for invalid raster cell array shapes."""
51
60
 
52
61
 
53
- class RasterModel(BaseModel):
62
+ class Raster(BaseModel):
54
63
  """2-dimensional raster and metadata."""
55
64
 
56
65
  arr: InstanceOf[np.ndarray]
57
66
  raster_meta: RasterMeta
58
67
 
68
+ @field_validator("arr")
69
+ @classmethod
70
+ def check_2d_array(cls, v: NDArray) -> NDArray:
71
+ """Validator to ensure the cell array is 2D."""
72
+ if v.ndim != 2:
73
+ msg = "Cell array must be 2D"
74
+ raise RasterCellArrayShapeError(msg)
75
+ return v
76
+
59
77
  @property
60
78
  def meta(self) -> RasterMeta:
61
79
  """Alias for raster_meta."""
@@ -80,6 +98,16 @@ class RasterModel(BaseModel):
80
98
  """Set the CRS via meta."""
81
99
  self.meta.crs = value
82
100
 
101
+ @property
102
+ def transform(self) -> Affine:
103
+ """Convenience property to access the transform via meta."""
104
+ return self.meta.transform
105
+
106
+ @transform.setter
107
+ def transform(self, value: Affine) -> None:
108
+ """Set the transform via meta."""
109
+ self.meta.transform = value
110
+
83
111
  def __init__(
84
112
  self,
85
113
  *,
@@ -108,14 +136,25 @@ class RasterModel(BaseModel):
108
136
  super().__init__(arr=arr, raster_meta=raster_meta)
109
137
 
110
138
  def __eq__(self, other: object) -> bool:
111
- """Check equality of two RasterModel objects."""
112
- if not isinstance(other, RasterModel):
139
+ """Check equality of two Raster objects."""
140
+ if not isinstance(other, Raster):
113
141
  return NotImplemented
114
142
  return (
115
143
  np.array_equal(self.arr, other.arr)
116
144
  and self.raster_meta == other.raster_meta
117
145
  )
118
146
 
147
+ def is_like(self, other: Raster) -> bool:
148
+ """Check if two Raster objects have the same metadata and shape.
149
+
150
+ Args:
151
+ other: Another Raster to compare with.
152
+
153
+ Returns:
154
+ True if both rasters have the same meta and shape attributes.
155
+ """
156
+ return self.meta == other.meta and self.shape == other.shape
157
+
119
158
  __hash__ = BaseModel.__hash__
120
159
 
121
160
  def __add__(self, other: float | Self) -> Self:
@@ -123,7 +162,7 @@ class RasterModel(BaseModel):
123
162
  if isinstance(other, float | int):
124
163
  new_arr = self.arr + other
125
164
  return cls(arr=new_arr, raster_meta=self.raster_meta)
126
- elif isinstance(other, RasterModel):
165
+ elif isinstance(other, Raster):
127
166
  if self.raster_meta != other.raster_meta:
128
167
  msg = (
129
168
  "Rasters must have the same metadata (e.g. CRS, cell size, etc.) "
@@ -149,7 +188,7 @@ class RasterModel(BaseModel):
149
188
  if isinstance(other, float | int):
150
189
  new_arr = self.arr * other
151
190
  return cls(arr=new_arr, raster_meta=self.raster_meta)
152
- elif isinstance(other, RasterModel):
191
+ elif isinstance(other, Raster):
153
192
  if self.raster_meta != other.raster_meta:
154
193
  msg = (
155
194
  "Rasters must have the same metadata (e.g. CRS, cell size, etc.) "
@@ -172,7 +211,7 @@ class RasterModel(BaseModel):
172
211
  if isinstance(other, float | int):
173
212
  new_arr = self.arr / other
174
213
  return cls(arr=new_arr, raster_meta=self.raster_meta)
175
- elif isinstance(other, RasterModel):
214
+ elif isinstance(other, Raster):
176
215
  if self.raster_meta != other.raster_meta:
177
216
  msg = (
178
217
  "Rasters must have the same metadata (e.g. CRS, cell size, etc.) "
@@ -222,7 +261,7 @@ class RasterModel(BaseModel):
222
261
  """Create a rasterio in-memory dataset from the Raster object.
223
262
 
224
263
  Example:
225
- >>> raster = RasterModel.example()
264
+ >>> raster = Raster.example()
226
265
  >>> with raster.to_rasterio_dataset() as dataset:
227
266
  >>> ...
228
267
  """
@@ -248,12 +287,30 @@ class RasterModel(BaseModel):
248
287
  finally:
249
288
  memfile.close()
250
289
 
290
+ @overload
291
+ def sample(
292
+ self,
293
+ xy: Collection[tuple[float, float]] | Collection[Point] | ArrayLike,
294
+ *,
295
+ na_action: Literal["raise", "ignore"] = "raise",
296
+ ) -> NDArray: ...
297
+ @overload
251
298
  def sample(
252
299
  self,
253
- xy: list[tuple[float, float]] | list[Point] | ArrayLike,
300
+ xy: tuple[float, float] | Point,
254
301
  *,
255
302
  na_action: Literal["raise", "ignore"] = "raise",
256
- ) -> NDArray[np.float64]:
303
+ ) -> float: ...
304
+ def sample(
305
+ self,
306
+ xy: Collection[tuple[float, float]]
307
+ | Collection[Point]
308
+ | ArrayLike
309
+ | tuple[float, float]
310
+ | Point,
311
+ *,
312
+ na_action: Literal["raise", "ignore"] = "raise",
313
+ ) -> NDArray | float:
257
314
  """Sample raster values at GeoSeries locations and return sampled values.
258
315
 
259
316
  Args:
@@ -270,13 +327,30 @@ class RasterModel(BaseModel):
270
327
  # https://rdrn.me/optimising-sampling/
271
328
 
272
329
  # Convert shapely Points to coordinate tuples if needed
273
- if isinstance(xy, (list, tuple)):
274
- xy = [_get_xy_tuple(point) for point in xy]
330
+ if isinstance(xy, Point):
331
+ xy = [(xy.x, xy.y)]
332
+ singleton = True
333
+ elif (
334
+ isinstance(xy, Collection)
335
+ and len(xy) > 0
336
+ and isinstance(next(iter(xy)), Point)
337
+ ):
338
+ xy = [(point.x, point.y) for point in xy] # pyright: ignore[reportAttributeAccessIssue]
339
+ singleton = False
340
+ elif (
341
+ isinstance(xy, tuple)
342
+ and len(xy) == 2
343
+ and isinstance(next(iter(xy)), (float, int))
344
+ ):
345
+ xy = [xy] # pyright: ignore[reportAssignmentType]
346
+ singleton = True
347
+ else:
348
+ singleton = False
275
349
 
276
350
  xy = np.asarray(xy, dtype=float)
277
351
 
278
- # Short-circuit
279
352
  if len(xy) == 0:
353
+ # Short-circuit
280
354
  return np.array([], dtype=float)
281
355
 
282
356
  # Create in-memory rasterio dataset from the incumbent Raster object
@@ -326,6 +400,10 @@ class RasterModel(BaseModel):
326
400
  axis=0,
327
401
  )
328
402
 
403
+ if singleton:
404
+ (raster_value,) = raster_values
405
+ return raster_value
406
+
329
407
  return raster_values
330
408
 
331
409
  @property
@@ -354,13 +432,16 @@ class RasterModel(BaseModel):
354
432
  ]
355
433
  )
356
434
 
357
- def explore(
435
+ def explore( # noqa: PLR0913 c.f. geopandas.explore which also has many input args
358
436
  self,
359
437
  *,
360
438
  m: Map | None = None,
361
439
  opacity: float = 1.0,
362
- colormap: str = "viridis",
440
+ colormap: str
441
+ | Callable[[float], tuple[float, float, float, float]] = "viridis",
363
442
  cbar_label: str | None = None,
443
+ vmin: float | None = None,
444
+ vmax: float | None = None,
364
445
  ) -> Map:
365
446
  """Display the raster on a folium map."""
366
447
  if not FOLIUM_INSTALLED or not MATPLOTLIB_INSTALLED:
@@ -368,39 +449,44 @@ class RasterModel(BaseModel):
368
449
  raise ImportError(msg)
369
450
 
370
451
  import folium.raster_layers
371
- import geopandas as gpd
372
452
  import matplotlib as mpl
373
453
 
374
454
  if m is None:
375
455
  m = folium.Map()
376
456
 
377
- rgba_map: Callable[[float], tuple[float, float, float, float]] = mpl.colormaps[
378
- colormap
379
- ]
457
+ if vmin is not None and vmax is not None and vmax <= vmin:
458
+ msg = "'vmin' must be less than 'vmax'."
459
+ raise ValueError(msg)
460
+
461
+ if isinstance(colormap, str):
462
+ colormap = mpl.colormaps[colormap]
380
463
 
381
- # Cast to GDF to facilitate converting bounds to WGS84
464
+ # Transform bounds to WGS84 using pyproj directly
382
465
  wgs84_crs = CRS.from_epsg(4326)
383
- gdf = gpd.GeoDataFrame(geometry=[self.bbox], crs=self.raster_meta.crs).to_crs(
384
- wgs84_crs
466
+ transformer = Transformer.from_crs(
467
+ self.raster_meta.crs, wgs84_crs, always_xy=True
385
468
  )
386
- xmin, ymin, xmax, ymax = gdf.total_bounds
387
469
 
388
- arr = np.array(self.arr)
470
+ # Get the corner points of the bounding box
471
+ raster_xmin, raster_ymin, raster_xmax, raster_ymax = self.bounds
472
+ corner_points = [
473
+ (raster_xmin, raster_ymin),
474
+ (raster_xmin, raster_ymax),
475
+ (raster_xmax, raster_ymax),
476
+ (raster_xmax, raster_ymin),
477
+ ]
478
+
479
+ # Transform all corner points to WGS84
480
+ transformed_points = [transformer.transform(x, y) for x, y in corner_points]
389
481
 
390
- # Normalize the data to the range [0, 1] as this is the cmap range
391
- with warnings.catch_warnings():
392
- warnings.filterwarnings(
393
- "ignore",
394
- message="All-NaN slice encountered",
395
- category=RuntimeWarning,
396
- )
397
- min_val = np.nanmin(arr)
398
- max_val = np.nanmax(arr)
482
+ # Find the bounding box of the transformed points
483
+ transformed_xs, transformed_ys = zip(*transformed_points, strict=True)
484
+ xmin, xmax = min(transformed_xs), max(transformed_xs)
485
+ ymin, ymax = min(transformed_ys), max(transformed_ys)
399
486
 
400
- if max_val > min_val: # Prevent division by zero
401
- arr = (arr - min_val) / (max_val - min_val)
402
- else:
403
- arr = np.zeros_like(arr) # In case all values are the same
487
+ # Normalize the array to [0, 1] for colormap mapping
488
+ _vmin, _vmax = _get_vmin_vmax(self, vmin=vmin, vmax=vmax)
489
+ arr = self.normalize(vmin=_vmin, vmax=_vmax).arr
404
490
 
405
491
  # Finally, need to determine whether to flip the image based on negative Affine
406
492
  # coefficients
@@ -411,11 +497,12 @@ class RasterModel(BaseModel):
411
497
  if flip_y:
412
498
  arr = np.flip(arr, axis=0)
413
499
 
500
+ bounds = [[ymin, xmin], [ymax, xmax]]
414
501
  img = folium.raster_layers.ImageOverlay(
415
502
  image=arr,
416
- bounds=[[ymin, xmin], [ymax, xmax]],
503
+ bounds=bounds,
417
504
  opacity=opacity,
418
- colormap=rgba_map,
505
+ colormap=colormap,
419
506
  mercator_project=True,
420
507
  )
421
508
 
@@ -423,26 +510,39 @@ class RasterModel(BaseModel):
423
510
 
424
511
  # Add a colorbar legend
425
512
  if BRANCA_INSTALLED:
426
- from branca.colormap import LinearColormap as BrancaLinearColormap
427
- from matplotlib.colors import to_hex
428
-
429
- # Determine legend data range in original units
430
- vmin = float(min_val) if np.isfinite(min_val) else 0.0
431
- vmax = float(max_val) if np.isfinite(max_val) else 1.0
432
- if vmax <= vmin:
433
- vmax = vmin + 1.0
434
-
435
- sample_points = np.linspace(0, 1, rgba_map.N)
436
- colors = [to_hex(rgba_map(x)) for x in sample_points]
437
- legend = BrancaLinearColormap(colors=colors, vmin=vmin, vmax=vmax)
513
+ cbar = _map_colorbar(colormap=colormap, vmin=_vmin, vmax=_vmax)
438
514
  if cbar_label:
439
- legend.caption = cbar_label
440
- legend.add_to(m)
515
+ cbar.caption = cbar_label
516
+ cbar.add_to(m)
441
517
 
442
- m.fit_bounds([[ymin, xmin], [ymax, xmax]])
518
+ m.fit_bounds(bounds)
443
519
 
444
520
  return m
445
521
 
522
+ def normalize(
523
+ self, *, vmin: float | None = None, vmax: float | None = None
524
+ ) -> Self:
525
+ """Normalize the raster values to the range [0, 1].
526
+
527
+ If custom vmin and vmax values are provided, values below vmin will be set to 0,
528
+ and values above vmax will be set to 1.
529
+
530
+ Args:
531
+ vmin: Minimum value for normalization. Values below this will be set to 0.
532
+ If None, the minimum value in the array is used.
533
+ vmax: Maximum value for normalization. Values above this will be set to 1.
534
+ If None, the maximum value in the array is used.
535
+ """
536
+ _vmin, _vmax = _get_vmin_vmax(self, vmin=vmin, vmax=vmax)
537
+
538
+ arr = self.arr.copy()
539
+ if _vmax > _vmin:
540
+ arr = (arr - _vmin) / (_vmax - _vmin)
541
+ arr = np.clip(arr, 0, 1)
542
+ else:
543
+ arr = np.zeros_like(arr)
544
+ return self.__class__(arr=arr, raster_meta=self.raster_meta)
545
+
446
546
  def to_clipboard(self) -> None:
447
547
  """Copy the raster cell array to the clipboard."""
448
548
  import pandas as pd
@@ -456,8 +556,22 @@ class RasterModel(BaseModel):
456
556
  cbar_label: str | None = None,
457
557
  basemap: bool = False,
458
558
  cmap: str = "viridis",
559
+ suppressed: Collection[float] | float = tuple(),
560
+ **kwargs: Any,
459
561
  ) -> Axes:
460
- """Plot the raster on a matplotlib axis."""
562
+ """Plot the raster on a matplotlib axis.
563
+
564
+ Args:
565
+ ax: A matplotlib axes object to plot on. If None, a new figure will be
566
+ created.
567
+ cbar_label: Label for the colorbar. If None, no label is added.
568
+ basemap: Whether to add a basemap. Currently not implemented.
569
+ cmap: Colormap to use for the plot.
570
+ suppressed: Values to suppress from the plot (i.e. not display). This can be
571
+ useful for zeroes especially.
572
+ **kwargs: Additional keyword arguments to pass to `rasterio.plot.show()`.
573
+ This includes parameters like `alpha` for transparency.
574
+ """
461
575
  if not MATPLOTLIB_INSTALLED:
462
576
  msg = "The 'matplotlib' package is required for 'plot()'."
463
577
  raise ImportError(msg)
@@ -465,6 +579,8 @@ class RasterModel(BaseModel):
465
579
  from matplotlib import pyplot as plt
466
580
  from mpl_toolkits.axes_grid1 import make_axes_locatable
467
581
 
582
+ suppressed = np.array(suppressed)
583
+
468
584
  if ax is None:
469
585
  _, _ax = plt.subplots()
470
586
  _ax: Axes
@@ -474,33 +590,34 @@ class RasterModel(BaseModel):
474
590
  msg = "Basemap plotting is not yet implemented."
475
591
  raise NotImplementedError(msg)
476
592
 
477
- arr = self.arr.copy()
593
+ model = self.model_copy()
594
+ model.arr = model.arr.copy()
478
595
 
479
- # Get extent of the non-zero values in array index coordinates
480
- (x_nonzero,) = np.nonzero(arr.any(axis=0))
481
- (y_nonzero,) = np.nonzero(arr.any(axis=1))
596
+ # Get extent of the unsuppressed values in array index coordinates
597
+ suppressed_mask = np.isin(model.arr, suppressed)
598
+ (x_unsuppressed,) = np.nonzero((~suppressed_mask).any(axis=0))
599
+ (y_unsuppressed,) = np.nonzero((~suppressed_mask).any(axis=1))
482
600
 
483
- if len(x_nonzero) == 0 or len(y_nonzero) == 0:
484
- msg = "Raster contains no non-zero values; cannot plot."
601
+ if len(x_unsuppressed) == 0 or len(y_unsuppressed) == 0:
602
+ msg = "Raster contains no unsuppressed values; cannot plot."
485
603
  raise ValueError(msg)
486
604
 
487
- min_x_nonzero = np.min(x_nonzero)
488
- max_x_nonzero = np.max(x_nonzero)
489
- min_y_nonzero = np.min(y_nonzero)
490
- max_y_nonzero = np.max(y_nonzero)
605
+ # N.B. these are array index coordinates, so np.min and np.max are safe since
606
+ # they cannot encounter NaN values.
607
+ min_x_unsuppressed = np.min(x_unsuppressed)
608
+ max_x_unsuppressed = np.max(x_unsuppressed)
609
+ min_y_unsuppressed = np.min(y_unsuppressed)
610
+ max_y_unsuppressed = np.max(y_unsuppressed)
491
611
 
492
612
  # Transform to raster CRS
493
- x1, y1 = self.raster_meta.transform * (min_x_nonzero, min_y_nonzero) # type: ignore[reportAssignmentType] overloaded tuple size in affine
494
- x2, y2 = self.raster_meta.transform * (max_x_nonzero, max_y_nonzero) # type: ignore[reportAssignmentType]
613
+ x1, y1 = self.raster_meta.transform * (min_x_unsuppressed, min_y_unsuppressed) # type: ignore[reportAssignmentType] overloaded tuple size in affine
614
+ x2, y2 = self.raster_meta.transform * (max_x_unsuppressed, max_y_unsuppressed) # type: ignore[reportAssignmentType]
495
615
  xmin, xmax = sorted([x1, x2])
496
616
  ymin, ymax = sorted([y1, y2])
497
617
 
498
- arr[arr == 0] = np.nan
618
+ model.arr[suppressed_mask] = np.nan
499
619
 
500
- with self.to_rasterio_dataset() as dataset:
501
- img, *_ = rasterio.plot.show(
502
- dataset, with_bounds=True, ax=ax, cmap=cmap
503
- ).get_images()
620
+ img, *_ = model.rio_show(ax=ax, cmap=cmap, with_bounds=True, **kwargs)
504
621
 
505
622
  ax.set_xlim(xmin, xmax)
506
623
  ax.set_ylim(ymin, ymax)
@@ -516,6 +633,20 @@ class RasterModel(BaseModel):
516
633
  fig.colorbar(img, label=cbar_label, cax=cax)
517
634
  return ax
518
635
 
636
+ def rio_show(self, **kwargs: Any) -> list[AxesImage]:
637
+ """Plot the raster using rasterio's built-in plotting function.
638
+
639
+ This is useful for lower-level access to rasterio's plotting capabilities.
640
+ Generally, the `plot()` method is preferred for most use cases.
641
+
642
+ Args:
643
+ **kwargs: Keyword arguments to pass to `rasterio.plot.show()`. This includes
644
+ parameters like `alpha` for transparency, and `with_bounds` to control
645
+ whether to plot in spatial coordinates or array index coordinates.
646
+ """
647
+ with self.to_rasterio_dataset() as dataset:
648
+ return rasterio.plot.show(dataset, **kwargs).get_images()
649
+
519
650
  def as_geodataframe(self, name: str = "value") -> gpd.GeoDataFrame:
520
651
  """Create a GeoDataFrame representation of the raster."""
521
652
  import geopandas as gpd
@@ -576,7 +707,7 @@ class RasterModel(BaseModel):
576
707
 
577
708
  @classmethod
578
709
  def example(cls) -> Self:
579
- """Create an example RasterModel."""
710
+ """Create an example Raster."""
580
711
  # Peaks dataset style example
581
712
  n = 256
582
713
  x = np.linspace(-3, 3, n)
@@ -625,6 +756,59 @@ class RasterModel(BaseModel):
625
756
  new_raster.arr = np.asarray(new_arr)
626
757
  return new_raster
627
758
 
759
+ def max(self) -> float:
760
+ """Get the maximum value in the raster, ignoring NaN values.
761
+
762
+ Returns:
763
+ The maximum value in the raster. Returns NaN if all values are NaN.
764
+ """
765
+ return float(np.nanmax(self.arr))
766
+
767
+ def min(self) -> float:
768
+ """Get the minimum value in the raster, ignoring NaN values.
769
+
770
+ Returns:
771
+ The minimum value in the raster. Returns NaN if all values are NaN.
772
+ """
773
+ return float(np.nanmin(self.arr))
774
+
775
+ def mean(self) -> float:
776
+ """Get the mean value in the raster, ignoring NaN values.
777
+
778
+ Returns:
779
+ The mean value in the raster. Returns NaN if all values are NaN.
780
+ """
781
+ return float(np.nanmean(self.arr))
782
+
783
+ def std(self) -> float:
784
+ """Get the standard deviation of values in the raster, ignoring NaN values.
785
+
786
+ Returns:
787
+ The standard deviation of the raster. Returns NaN if all values are NaN.
788
+ """
789
+ return float(np.nanstd(self.arr))
790
+
791
+ def quantile(self, q: float) -> float:
792
+ """Get the specified quantile value in the raster, ignoring NaN values.
793
+
794
+ Args:
795
+ q: Quantile to compute, must be between 0 and 1 inclusive.
796
+
797
+ Returns:
798
+ The quantile value. Returns NaN if all values are NaN.
799
+ """
800
+ return float(np.nanquantile(self.arr, q))
801
+
802
+ def median(self) -> float:
803
+ """Get the median value in the raster, ignoring NaN values.
804
+
805
+ This is equivalent to quantile(0.5).
806
+
807
+ Returns:
808
+ The median value in the raster. Returns NaN if all values are NaN.
809
+ """
810
+ return float(np.nanmedian(self.arr))
811
+
628
812
  def fillna(self, value: float) -> Self:
629
813
  """Fill NaN values in the raster with a specified value.
630
814
 
@@ -676,9 +860,17 @@ class RasterModel(BaseModel):
676
860
  all_levels = []
677
861
  all_geoms = []
678
862
  for level in levels:
863
+ # If this is the maximum or minimum level, perturb it ever-so-slightly to
864
+ # ensure we get contours at the edges of the raster
865
+ perturbed_level = level
866
+ if level == self.max():
867
+ perturbed_level -= CONTOUR_PERTURB_EPS
868
+ elif level == self.min():
869
+ perturbed_level += CONTOUR_PERTURB_EPS
870
+
679
871
  contours = skimage.measure.find_contours(
680
872
  self.arr,
681
- level=level,
873
+ level=perturbed_level,
682
874
  )
683
875
 
684
876
  # Construct shapely LineString objects
@@ -751,9 +943,75 @@ class RasterModel(BaseModel):
751
943
 
752
944
  return raster
753
945
 
946
+ def pad(self, width: float, *, value: float = np.nan) -> Self:
947
+ """Extend the raster by adding a constant fill value around the edges.
948
+
949
+ By default, the padding value is NaN, but this can be changed via the
950
+ `value` parameter.
951
+
952
+ This grows the raster by adding padding around all edges. New cells are
953
+ filled with the constant `value`.
954
+
955
+ If the width is not an exact multiple of the cell size, the padding may be
956
+ slightly larger than the specified width, i.e. the value is rounded up to
957
+ the nearest whole number of cells.
958
+
959
+ Args:
960
+ width: The width of the padding, in the same units as the raster CRS
961
+ (e.g. meters). This defines how far from the edge the padding
962
+ extends.
963
+ value: The constant value to use for padding. Default is NaN.
964
+ """
965
+ cell_size = self.raster_meta.cell_size
966
+
967
+ # Calculate number of cells to pad in each direction
968
+ pad_cells = int(np.ceil(width / cell_size))
969
+
970
+ # Get current bounds
971
+ xmin, ymin, xmax, ymax = self.bounds
972
+
973
+ # Calculate new bounds with padding
974
+ new_xmin = xmin - (pad_cells * cell_size)
975
+ new_ymin = ymin - (pad_cells * cell_size)
976
+ new_xmax = xmax + (pad_cells * cell_size)
977
+ new_ymax = ymax + (pad_cells * cell_size)
978
+
979
+ # Create padded array
980
+ new_height = self.arr.shape[0] + 2 * pad_cells
981
+ new_width = self.arr.shape[1] + 2 * pad_cells
982
+
983
+ # Create new array filled with the padding value
984
+ padded_arr = np.full((new_height, new_width), value, dtype=self.arr.dtype)
985
+
986
+ # Copy original array into the center of the padded array
987
+ padded_arr[
988
+ pad_cells : pad_cells + self.arr.shape[0],
989
+ pad_cells : pad_cells + self.arr.shape[1],
990
+ ] = self.arr
991
+
992
+ # Create new transform for the padded raster
993
+ new_transform = rasterio.transform.from_bounds(
994
+ west=new_xmin,
995
+ south=new_ymin,
996
+ east=new_xmax,
997
+ north=new_ymax,
998
+ width=new_width,
999
+ height=new_height,
1000
+ )
1001
+
1002
+ # Create new raster metadata
1003
+ new_meta = RasterMeta(
1004
+ cell_size=cell_size,
1005
+ crs=self.raster_meta.crs,
1006
+ transform=new_transform,
1007
+ )
1008
+
1009
+ return self.__class__(arr=padded_arr, raster_meta=new_meta)
1010
+
754
1011
  def crop(
755
1012
  self,
756
1013
  bounds: tuple[float, float, float, float],
1014
+ *,
757
1015
  strategy: Literal["underflow", "overflow"] = "underflow",
758
1016
  ) -> Self:
759
1017
  """Crop the raster to the specified bounds as (minx, miny, maxx, maxy).
@@ -767,7 +1025,7 @@ class RasterModel(BaseModel):
767
1025
  remains covered with cells.
768
1026
 
769
1027
  Returns:
770
- A new RasterModel instance cropped to the specified bounds.
1028
+ A new Raster instance cropped to the specified bounds.
771
1029
  """
772
1030
 
773
1031
  minx, miny, maxx, maxy = bounds
@@ -827,6 +1085,141 @@ class RasterModel(BaseModel):
827
1085
  )
828
1086
  return cls(arr=cropped_arr, raster_meta=new_meta)
829
1087
 
1088
+ def taper_border(self, width: float, *, limit: float = 0.0) -> Self:
1089
+ """Taper values to a limiting value around the border of the raster.
1090
+
1091
+ By default, the borders are tapered to zero, but this can be changed via the
1092
+ `limit` parameter.
1093
+
1094
+ This keeps the raster size the same, overwriting values in the border area.
1095
+ To instead grow the raster, consider using `pad()` followed by `taper_border()`.
1096
+
1097
+ The tapering is linear from the cell centres around the border of the raster,
1098
+ so the value at the edge of the raster will be equal to `limit`.
1099
+
1100
+ Args:
1101
+ width: The width of the taper, in the same units as the raster CRS
1102
+ (e.g. meters). This defines how far from the edge the tapering
1103
+ starts.
1104
+ limit: The limiting value to taper to at the edges. Default is zero.
1105
+ """
1106
+
1107
+ # Determine the width in cell units (possibly fractional)
1108
+ cell_size = self.raster_meta.cell_size
1109
+ width_in_cells = width / cell_size
1110
+
1111
+ # Calculate the distance from the edge in cell units
1112
+ arr_height, arr_width = self.arr.shape
1113
+ y_indices, x_indices = np.indices((int(arr_height), int(arr_width)))
1114
+ dist_from_left = x_indices
1115
+ dist_from_right = arr_width - 1 - x_indices
1116
+ dist_from_top = y_indices
1117
+ dist_from_bottom = arr_height - 1 - y_indices
1118
+ dist_from_edge = np.minimum.reduce(
1119
+ [dist_from_left, dist_from_right, dist_from_top, dist_from_bottom]
1120
+ )
1121
+
1122
+ # Mask the arrays to only the area within the width from the edge, rounding up
1123
+ mask = dist_from_edge < np.ceil(width_in_cells)
1124
+ masked_dist_arr = np.where(mask, dist_from_edge, np.nan)
1125
+ masked_arr = np.where(mask, self.arr, np.nan)
1126
+
1127
+ # Calculate the tapering factor based on the distance from the edge
1128
+ taper_factor = np.clip(masked_dist_arr / width_in_cells, 0.0, 1.0)
1129
+ tapered_values = limit + (masked_arr - limit) * taper_factor
1130
+
1131
+ # Create the new raster array
1132
+ new_arr = self.arr.copy()
1133
+ new_arr[mask] = tapered_values[mask]
1134
+ new_raster = self.model_copy()
1135
+ new_raster.arr = new_arr
1136
+
1137
+ return new_raster
1138
+
1139
+ def clip(
1140
+ self,
1141
+ polygon: Polygon | MultiPolygon,
1142
+ *,
1143
+ strategy: Literal["centres"] = "centres",
1144
+ ) -> Self:
1145
+ """Clip the raster to the specified polygon, replacing cells outside with NaN.
1146
+
1147
+ The clipping strategy determines how to handle cells that are partially
1148
+ within the polygon. Currently, only the 'centres' strategy is supported, which
1149
+ retains cells whose centres fall within the polygon.
1150
+
1151
+ Args:
1152
+ polygon: A shapely Polygon or MultiPolygon defining the area to clip to.
1153
+ strategy: The clipping strategy to use. Currently only 'centres' is
1154
+ supported, which retains cells whose centres fall within the
1155
+ polygon.
1156
+
1157
+ Returns:
1158
+ A new Raster with cells outside the polygon set to NaN.
1159
+ """
1160
+ if strategy != "centres":
1161
+ msg = f"Unsupported clipping strategy: {strategy}"
1162
+ raise NotImplementedError(msg)
1163
+
1164
+ raster = self.model_copy()
1165
+
1166
+ mask = rasterio.features.rasterize(
1167
+ [(polygon, 1)],
1168
+ fill=0,
1169
+ out_shape=self.shape,
1170
+ transform=self.meta.transform,
1171
+ dtype=np.uint8,
1172
+ )
1173
+
1174
+ raster.arr = np.where(mask, raster.arr, np.nan)
1175
+
1176
+ return raster
1177
+
1178
+ def trim_nan(self) -> Self:
1179
+ """Crop the raster by trimming away all-NaN slices at the edges.
1180
+
1181
+ This effectively trims the raster to the smallest bounding box that contains all
1182
+ of the non-NaN values. Note that this does not guarantee no NaN values at all
1183
+ around the edges, only that there won't be entire edges which are all-NaN.
1184
+
1185
+ Consider using `.extrapolate()` for further cleanup of NaN values.
1186
+ """
1187
+ arr = self.arr
1188
+
1189
+ # Check if the entire array is NaN
1190
+ if np.all(np.isnan(arr)):
1191
+ msg = "Cannot crop raster: all values are NaN"
1192
+ raise ValueError(msg)
1193
+
1194
+ # Find rows and columns that are not all NaN
1195
+ nan_row_mask = np.all(np.isnan(arr), axis=1)
1196
+ nan_col_mask = np.all(np.isnan(arr), axis=0)
1197
+
1198
+ # Find the bounding indices
1199
+ (row_indices,) = np.where(~nan_row_mask)
1200
+ (col_indices,) = np.where(~nan_col_mask)
1201
+
1202
+ min_row, max_row = row_indices[0], row_indices[-1]
1203
+ min_col, max_col = col_indices[0], col_indices[-1]
1204
+
1205
+ # Crop the array
1206
+ cropped_arr = arr[min_row : max_row + 1, min_col : max_col + 1]
1207
+
1208
+ # Shift the transform by the number of pixels cropped (min_col, min_row)
1209
+ new_transform = (
1210
+ self.raster_meta.transform
1211
+ * rasterio.transform.Affine.translation(min_col, min_row)
1212
+ )
1213
+
1214
+ # Create new metadata
1215
+ new_meta = RasterMeta(
1216
+ cell_size=self.raster_meta.cell_size,
1217
+ crs=self.raster_meta.crs,
1218
+ transform=new_transform,
1219
+ )
1220
+
1221
+ return self.__class__(arr=cropped_arr, raster_meta=new_meta)
1222
+
830
1223
  def resample(
831
1224
  self, new_cell_size: float, *, method: Literal["bilinear"] = "bilinear"
832
1225
  ) -> Self:
@@ -874,26 +1267,55 @@ class RasterModel(BaseModel):
874
1267
 
875
1268
  return cls(arr=new_arr, raster_meta=new_raster_meta)
876
1269
 
877
- @field_validator("arr")
878
- @classmethod
879
- def check_2d_array(cls, v: NDArray) -> NDArray:
880
- """Validator to ensure the cell array is 2D."""
881
- if v.ndim != 2:
882
- msg = "Cell array must be 2D"
883
- raise RasterCellArrayShapeError(msg)
884
- return v
885
1270
 
1271
+ def _map_colorbar(
1272
+ *,
1273
+ colormap: Callable[[float], tuple[float, float, float, float]],
1274
+ vmin: float,
1275
+ vmax: float,
1276
+ ) -> BrancaLinearColormap:
1277
+ from branca.colormap import LinearColormap as BrancaLinearColormap
1278
+ from matplotlib.colors import ListedColormap, to_hex
1279
+
1280
+ # Determine legend data range in original units
1281
+ vmin = float(vmin) if np.isfinite(vmin) else 0.0
1282
+ vmax = float(vmax) if np.isfinite(vmax) else 1.0
1283
+ if vmax <= vmin:
1284
+ vmax = vmin + 1.0
1285
+
1286
+ if isinstance(colormap, ListedColormap):
1287
+ n = colormap.N
1288
+ else:
1289
+ n = 256
1290
+
1291
+ sample_points = np.linspace(0, 1, n)
1292
+ colors = [to_hex(colormap(x)) for x in sample_points]
1293
+ return BrancaLinearColormap(colors=colors, vmin=vmin, vmax=vmax)
886
1294
 
887
- def _get_xy_tuple(xy: Any) -> tuple[float, float]:
888
- """Convert Point or coordinate tuple to coordinate tuple.
889
1295
 
890
- Args:
891
- xy: Either a coordinate tuple or a shapely Point object.
1296
+ def _get_vmin_vmax(
1297
+ raster: Raster, *, vmin: float | None = None, vmax: float | None = None
1298
+ ) -> tuple[float, float]:
1299
+ """Get maximum and minimum values from a raster array, ignoring NaNs.
892
1300
 
893
- Returns:
894
- A coordinate tuple (x, y).
1301
+ Allows for custom over-ride vmin and vmax values to be provided.
895
1302
  """
896
- if isinstance(xy, Point):
897
- return (xy.x, xy.y)
898
- x, y = xy
899
- return (float(x), float(y))
1303
+ with warnings.catch_warnings():
1304
+ warnings.filterwarnings(
1305
+ "ignore",
1306
+ message="All-NaN slice encountered",
1307
+ category=RuntimeWarning,
1308
+ )
1309
+ if vmin is None:
1310
+ _vmin = raster.min()
1311
+ else:
1312
+ _vmin = vmin
1313
+ if vmax is None:
1314
+ _vmax = raster.max()
1315
+ else:
1316
+ _vmax = vmax
1317
+
1318
+ return _vmin, _vmax
1319
+
1320
+
1321
+ RasterModel = Raster
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rastr
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Geospatial Raster datatype library for Python.
5
5
  Project-URL: Source Code, https://github.com/tonkintaylor/rastr
6
6
  Project-URL: Bug Tracker, https://github.com/tonkintaylor/rastr/issues
7
7
  Project-URL: Releases, https://github.com/tonkintaylor/rastr/releases
8
- Project-URL: Source Archive, https://github.com/tonkintaylor/rastr/archive/336916af169603534dd0728c10401667f263d98a.zip
8
+ Project-URL: Source Archive, https://github.com/tonkintaylor/rastr/archive/fde05e3c098da7ff9e77a37332d55fb7dbacd873.zip
9
9
  Author-email: Tonkin & Taylor Limited <Sub-DisciplineData+AnalyticsStaff@tonkintaylor.co.nz>, Nathan McDougall <nmcdougall@tonkintaylor.co.nz>, Ben Karl <bkarl@tonkintaylor.co.nz>
10
10
  License-Expression: MIT
11
11
  License-File: LICENSE
@@ -75,10 +75,10 @@ from pyproj.crs.crs import CRS
75
75
  from rasterio.transform import from_origin
76
76
  from rastr.create import full_raster
77
77
  from rastr.meta import RasterMeta
78
- from rastr.raster import RasterModel
78
+ from rastr.raster import Raster
79
79
 
80
80
  # Create an example raster
81
- raster = RasterModel.example()
81
+ raster = Raster.example()
82
82
 
83
83
  # Basic arithmetic operations
84
84
  doubled = raster * 2
@@ -131,6 +131,12 @@ Current version limitations:
131
131
  - Square cells only (rectangular cell support planned).
132
132
  - Only float dtypes (integer support planned).
133
133
 
134
+ ## Similar Projects
135
+
136
+ - [rasters](https://github.com/python-rasters/rasters) is a project with similar goals of providing a dedicated raster datatype in Python with higher-level interfaces for GIS operations. Unlike `rastr`, it has support for multi-band rasters, and has some more advanced functionality for Earth Science applications. Both projects are relatively new and under active development.
137
+ - [rasterio](https://rasterio.readthedocs.io/) is a core dependency of `rastr` and provides low-level raster I/O and processing capabilities.
138
+ - [rioxarray](https://corteva.github.io/rioxarray/stable/getting_started/getting_started.html) extends [`xarray`](https://docs.xarray.dev/en/stable/index.html) for raster data with geospatial support via `rasterio`.
139
+
134
140
  ### Contributing
135
141
 
136
142
  See the
@@ -1,15 +1,15 @@
1
1
  rastr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- rastr/_version.py,sha256=2_0GUP7yBCXRus-qiJKxQD62z172WSs1sQ6DVpPsbmM,704
3
- rastr/create.py,sha256=6aHpRFRXmpXzuzTt-SxY_BfVc7dXKCBLCArb7DjUrsM,13494
4
- rastr/io.py,sha256=RgkiV_emOPjTFeI2a1aCBtfWwrSLH0XmP8rx9tu6PAI,2952
2
+ rastr/_version.py,sha256=fvHpBU3KZKRinkriKdtAt3crenOyysELF-M9y3ozg3U,704
3
+ rastr/create.py,sha256=7fmg4GKeTXdM5w8oqrCD9eDrc7PU87oHYGPNA-IZ8Cc,13759
4
+ rastr/io.py,sha256=llR2wFyrJVjEG6HN82UAJLVPs_H8nvDxmbEZLjJYjno,2927
5
5
  rastr/meta.py,sha256=5iDvGkYe8iMMkPV6gSL04jNcLRhuRNFqe9AppUpp55E,2928
6
- rastr/raster.py,sha256=I9CQB2NYUOFvvpdzNsQcDBdgLXDCjB7ONFxDAq_6_SU,31995
6
+ rastr/raster.py,sha256=_1Wr78B0_V9r4oh7X-h5lT1QXlnBsSBW6ZkICGSjUFw,47514
7
7
  rastr/arr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  rastr/arr/fill.py,sha256=80ucb36el9s042fDSwK1SZJhp_GNJNMM0fpQTWmJvgE,1001
9
9
  rastr/gis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  rastr/gis/fishnet.py,sha256=Ic-0HV61ST8OxwhyoMyV_ybihs2xuhgAY-3n4CknAt8,2670
11
11
  rastr/gis/smooth.py,sha256=3HQDQHQM5_LeNk21R8Eb8VpF727JcXq21HO9JMvcpW4,4810
12
- rastr-0.4.0.dist-info/METADATA,sha256=GjZF16Tmxjz3WUbEFhAB1jW19AecpNkJpk__YNSamQQ,4953
13
- rastr-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
- rastr-0.4.0.dist-info/licenses/LICENSE,sha256=7qUsx93G2ATTRLZiSYuQofwAX_uWvrqnAiMK8PzxvNc,1080
15
- rastr-0.4.0.dist-info/RECORD,,
12
+ rastr-0.5.0.dist-info/METADATA,sha256=1-sja-_hUE6ukT7KqsDH-nzPZNWVHVeXCABmM9rzTg8,5701
13
+ rastr-0.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
+ rastr-0.5.0.dist-info/licenses/LICENSE,sha256=7qUsx93G2ATTRLZiSYuQofwAX_uWvrqnAiMK8PzxvNc,1080
15
+ rastr-0.5.0.dist-info/RECORD,,
File without changes