rastr 0.2.0__py3-none-any.whl → 0.3.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 +16 -3
- rastr/arr/fill.py +9 -2
- rastr/create.py +155 -35
- rastr/gis/fishnet.py +13 -4
- rastr/gis/smooth.py +7 -4
- rastr/io.py +68 -3
- rastr/meta.py +47 -10
- rastr/raster.py +238 -56
- rastr-0.3.0.dist-info/METADATA +138 -0
- rastr-0.3.0.dist-info/RECORD +15 -0
- rastr-0.2.0.dist-info/METADATA +0 -44
- rastr-0.2.0.dist-info/RECORD +0 -15
- {rastr-0.2.0.dist-info → rastr-0.3.0.dist-info}/WHEEL +0 -0
- {rastr-0.2.0.dist-info → rastr-0.3.0.dist-info}/licenses/LICENSE +0 -0
rastr/_version.py
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
3
|
|
|
4
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
5
12
|
|
|
6
13
|
TYPE_CHECKING = False
|
|
7
14
|
if TYPE_CHECKING:
|
|
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
|
|
|
9
16
|
from typing import Union
|
|
10
17
|
|
|
11
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
12
20
|
else:
|
|
13
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
14
23
|
|
|
15
24
|
version: str
|
|
16
25
|
__version__: str
|
|
17
26
|
__version_tuple__: VERSION_TUPLE
|
|
18
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
19
30
|
|
|
20
|
-
__version__ = version = '0.
|
|
21
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.3.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
rastr/arr/fill.py
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
1
5
|
import numpy as np
|
|
2
|
-
|
|
3
|
-
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from numpy.typing import NDArray
|
|
4
9
|
|
|
5
10
|
|
|
6
11
|
def fillna_nearest_neighbours(arr: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
@@ -11,6 +16,8 @@ def fillna_nearest_neighbours(arr: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
|
11
16
|
In the case of tiebreaks, the value from the neighbour with the lowest index is
|
|
12
17
|
imputed.
|
|
13
18
|
"""
|
|
19
|
+
from scipy.interpolate import NearestNDInterpolator
|
|
20
|
+
|
|
14
21
|
nonnan_mask = np.nonzero(~np.isnan(arr))
|
|
15
22
|
nonnan_idxs = np.array(nonnan_mask).transpose()
|
|
16
23
|
|
rastr/create.py
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import warnings
|
|
5
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
3
6
|
|
|
4
|
-
import geopandas as gpd
|
|
5
7
|
import numpy as np
|
|
6
|
-
import pandas as pd
|
|
7
8
|
import rasterio.features
|
|
9
|
+
import rasterio.transform
|
|
8
10
|
from affine import Affine
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
+
from pyproj import CRS
|
|
12
|
+
from shapely.geometry import Point
|
|
11
13
|
|
|
12
14
|
from rastr.gis.fishnet import create_point_grid, get_point_grid_shape
|
|
13
15
|
from rastr.meta import RasterMeta
|
|
14
16
|
from rastr.raster import RasterModel
|
|
15
17
|
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Iterable
|
|
20
|
+
|
|
21
|
+
import geopandas as gpd
|
|
22
|
+
from numpy.typing import ArrayLike
|
|
23
|
+
from shapely.geometry import Polygon
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
TQDM_INSTALLED = importlib.util.find_spec("tqdm") is not None
|
|
27
|
+
|
|
28
|
+
_T = TypeVar("_T")
|
|
29
|
+
|
|
16
30
|
|
|
17
31
|
class MissingColumnsError(ValueError):
|
|
18
32
|
"""Raised when target columns are missing from the GeoDataFrame."""
|
|
@@ -61,17 +75,10 @@ def raster_distance_from_polygon(
|
|
|
61
75
|
Raises:
|
|
62
76
|
ValueError: If the provided CRS is geographic (lat/lon).
|
|
63
77
|
"""
|
|
64
|
-
if
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
err_msg = "Only one of 'extent_polygon' or 'snap_raster' can be provided. "
|
|
69
|
-
raise ValueError(err_msg)
|
|
70
|
-
|
|
71
|
-
if not show_pbar:
|
|
72
|
-
|
|
73
|
-
def _pbar(x: Iterable) -> None:
|
|
74
|
-
return x # No-op if no progress bar is needed
|
|
78
|
+
if show_pbar and not TQDM_INSTALLED:
|
|
79
|
+
msg = "The 'tqdm' package is not installed. Progress bars will not be shown."
|
|
80
|
+
warnings.warn(msg, UserWarning, stacklevel=2)
|
|
81
|
+
show_pbar = False
|
|
75
82
|
|
|
76
83
|
# Check if the provided CRS is projected (cartesian)
|
|
77
84
|
if raster_meta.crs.is_geographic:
|
|
@@ -80,36 +87,44 @@ def raster_distance_from_polygon(
|
|
|
80
87
|
)
|
|
81
88
|
raise ValueError(err_msg)
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
if extent_polygon is None and snap_raster is None:
|
|
91
|
+
err_msg = "Either 'extent_polygon' or 'snap_raster' must be provided. "
|
|
92
|
+
raise ValueError(err_msg)
|
|
93
|
+
elif extent_polygon is not None and snap_raster is not None:
|
|
94
|
+
err_msg = "Only one of 'extent_polygon' or 'snap_raster' can be provided. "
|
|
95
|
+
raise ValueError(err_msg)
|
|
96
|
+
elif extent_polygon is None and snap_raster is not None:
|
|
97
|
+
# Calculate the coordinates
|
|
85
98
|
x, y = snap_raster.get_xy()
|
|
86
|
-
|
|
99
|
+
|
|
100
|
+
# Create a mask to identify points for which distance should be calculated
|
|
101
|
+
distance_extent = snap_raster.bbox.difference(polygon)
|
|
102
|
+
elif extent_polygon is not None and snap_raster is None:
|
|
87
103
|
x, y = create_point_grid(
|
|
88
104
|
bounds=extent_polygon.bounds, cell_size=raster_meta.cell_size
|
|
89
105
|
)
|
|
90
|
-
|
|
91
|
-
points = [Point(x, y) for x, y in zip(x.flatten(), y.flatten(), strict=True)]
|
|
92
|
-
|
|
93
|
-
# Create a mask to identify points for which distance should be calculated
|
|
94
|
-
if extent_polygon is not None:
|
|
95
106
|
distance_extent = extent_polygon.difference(polygon)
|
|
96
107
|
else:
|
|
97
|
-
|
|
108
|
+
raise AssertionError
|
|
98
109
|
|
|
99
|
-
|
|
100
|
-
_pbar = partial(tqdm, desc="Finding points within extent")
|
|
101
|
-
mask = [distance_extent.intersects(point) for point in _pbar(points)]
|
|
110
|
+
pts = [Point(x, y) for x, y in zip(x.flatten(), y.flatten(), strict=True)]
|
|
102
111
|
|
|
103
|
-
if show_pbar
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
112
|
+
_pts = _pbar(pts, desc="Finding points within extent") if show_pbar else pts
|
|
113
|
+
mask = [distance_extent.intersects(pt) for pt in _pts]
|
|
114
|
+
|
|
115
|
+
_pts = _pbar(pts, desc="Calculating distances") if show_pbar else pts
|
|
116
|
+
distances = np.where(mask, np.array([polygon.distance(pt) for pt in _pts]), np.nan)
|
|
108
117
|
distance_raster = distances.reshape(x.shape)
|
|
109
118
|
|
|
110
119
|
return RasterModel(arr=distance_raster, raster_meta=raster_meta)
|
|
111
120
|
|
|
112
121
|
|
|
122
|
+
def _pbar(iterable: Iterable[_T], *, desc: str | None = None) -> Iterable[_T]:
|
|
123
|
+
from tqdm import tqdm
|
|
124
|
+
|
|
125
|
+
return tqdm(iterable, desc=desc)
|
|
126
|
+
|
|
127
|
+
|
|
113
128
|
def full_raster(
|
|
114
129
|
raster_meta: RasterMeta,
|
|
115
130
|
*,
|
|
@@ -185,7 +200,8 @@ def rasterize_gdf(
|
|
|
185
200
|
shapes,
|
|
186
201
|
out_shape=shape,
|
|
187
202
|
transform=transform,
|
|
188
|
-
|
|
203
|
+
# Fill gaps with NaN
|
|
204
|
+
fill=np.nan, # type: ignore[reportArgumentType] docstring contradicts inferred annotation
|
|
189
205
|
dtype=np.float32,
|
|
190
206
|
)
|
|
191
207
|
|
|
@@ -222,6 +238,8 @@ def _validate_columns_numeric(gdf: gpd.GeoDataFrame, target_cols: list[str]) ->
|
|
|
222
238
|
Raises:
|
|
223
239
|
NonNumericColumnsError: If any columns contain non-numeric data.
|
|
224
240
|
"""
|
|
241
|
+
import pandas as pd
|
|
242
|
+
|
|
225
243
|
non_numeric_cols = []
|
|
226
244
|
for col in target_cols:
|
|
227
245
|
if not pd.api.types.is_numeric_dtype(gdf[col]):
|
|
@@ -259,3 +277,105 @@ def _validate_no_overlapping_geometries(gdf: gpd.GeoDataFrame) -> None:
|
|
|
259
277
|
"Overlapping geometries can lead to data loss during rasterization."
|
|
260
278
|
)
|
|
261
279
|
raise OverlappingGeometriesError(msg)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def raster_from_point_cloud(
|
|
283
|
+
x: ArrayLike,
|
|
284
|
+
y: ArrayLike,
|
|
285
|
+
z: ArrayLike,
|
|
286
|
+
*,
|
|
287
|
+
crs: CRS | str,
|
|
288
|
+
cell_size: float | None = None,
|
|
289
|
+
) -> RasterModel:
|
|
290
|
+
"""Create a raster from a point cloud via interpolation.
|
|
291
|
+
|
|
292
|
+
Interpolation is only possible within the convex hull of the points. Outside of
|
|
293
|
+
this, cells will be NaN-valued.
|
|
294
|
+
|
|
295
|
+
All (x,y) points must be unique.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
x: X coordinates of points.
|
|
299
|
+
y: Y coordinates of points.
|
|
300
|
+
z: Values at each (x, y) point to assign the raster.
|
|
301
|
+
crs: Coordinate reference system for the (x, y) coordinates.
|
|
302
|
+
cell_size: Desired cell size for the raster. If None, a heuristic is used based
|
|
303
|
+
on the spacing between (x, y) points.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Raster containing the interpolated values.
|
|
307
|
+
|
|
308
|
+
Raises:
|
|
309
|
+
ValueError: If any (x, y) points are duplicated, or if they are all collinear.
|
|
310
|
+
"""
|
|
311
|
+
from scipy.interpolate import LinearNDInterpolator
|
|
312
|
+
from scipy.spatial import KDTree, QhullError
|
|
313
|
+
|
|
314
|
+
x = np.asarray(x).ravel()
|
|
315
|
+
y = np.asarray(y).ravel()
|
|
316
|
+
z = np.asarray(z).ravel()
|
|
317
|
+
crs = CRS.from_user_input(crs)
|
|
318
|
+
|
|
319
|
+
# Validate input arrays
|
|
320
|
+
if len(x) != len(y) or len(x) != len(z):
|
|
321
|
+
msg = "Length of x, y, and z must be equal."
|
|
322
|
+
raise ValueError(msg)
|
|
323
|
+
if len(x) < 3:
|
|
324
|
+
msg = "At least three (x, y, z) points are required to triangulate a surface."
|
|
325
|
+
raise ValueError(msg)
|
|
326
|
+
# Check for duplicate (x, y) points
|
|
327
|
+
xy_points = np.column_stack((x, y))
|
|
328
|
+
if len(xy_points) != len(np.unique(xy_points, axis=0)):
|
|
329
|
+
msg = "Duplicate (x, y) points found. Each (x, y) point must be unique."
|
|
330
|
+
raise ValueError(msg)
|
|
331
|
+
|
|
332
|
+
# Heuristic for cell size if not provided
|
|
333
|
+
if cell_size is None:
|
|
334
|
+
# 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)
|
|
337
|
+
distances: np.ndarray
|
|
338
|
+
cell_size = float(np.percentile(distances[distances > 0], 5)) / 2
|
|
339
|
+
|
|
340
|
+
# Compute bounds from data
|
|
341
|
+
minx, miny, maxx, maxy = np.min(x), np.min(y), np.max(x), np.max(y)
|
|
342
|
+
|
|
343
|
+
# Compute grid shape
|
|
344
|
+
width = int(np.ceil((maxx - minx) / cell_size))
|
|
345
|
+
height = int(np.ceil((maxy - miny) / cell_size))
|
|
346
|
+
shape = (height, width)
|
|
347
|
+
|
|
348
|
+
# Compute transform: upper left corner is (minx, maxy)
|
|
349
|
+
transform = Affine.translation(minx, maxy) * Affine.scale(cell_size, -cell_size)
|
|
350
|
+
|
|
351
|
+
# Create grid coordinates for raster cells
|
|
352
|
+
rows, cols = np.indices(shape)
|
|
353
|
+
xs, ys = rasterio.transform.xy(
|
|
354
|
+
transform=transform, rows=rows, cols=cols, offset="center"
|
|
355
|
+
)
|
|
356
|
+
grid_x = np.array(xs).ravel()
|
|
357
|
+
grid_y = np.array(ys).ravel()
|
|
358
|
+
|
|
359
|
+
# Perform interpolation
|
|
360
|
+
try:
|
|
361
|
+
interpolator = LinearNDInterpolator(
|
|
362
|
+
points=xy_points, values=z, fill_value=np.nan
|
|
363
|
+
)
|
|
364
|
+
except QhullError as err:
|
|
365
|
+
msg = (
|
|
366
|
+
"Failed to interpolate. This may be due to insufficient or "
|
|
367
|
+
"degenerate input points. Ensure that the (x, y) points are not all "
|
|
368
|
+
"collinear (i.e. that the convex hull is non-degenerate)."
|
|
369
|
+
)
|
|
370
|
+
raise ValueError(msg) from err
|
|
371
|
+
|
|
372
|
+
grid_values = np.array(interpolator(np.column_stack((grid_x, grid_y))))
|
|
373
|
+
|
|
374
|
+
arr = grid_values.reshape(shape).astype(np.float32)
|
|
375
|
+
|
|
376
|
+
raster_meta = RasterMeta(
|
|
377
|
+
cell_size=cell_size,
|
|
378
|
+
crs=crs,
|
|
379
|
+
transform=transform,
|
|
380
|
+
)
|
|
381
|
+
return RasterModel(arr=arr, raster_meta=raster_meta)
|
rastr/gis/fishnet.py
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
2
5
|
import numpy as np
|
|
3
|
-
from geopandas.array import GeometryArray
|
|
4
6
|
from shapely import BufferCapStyle, BufferJoinStyle
|
|
5
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from geopandas.array import GeometryArray
|
|
10
|
+
from numpy.typing import NDArray
|
|
11
|
+
|
|
6
12
|
|
|
7
13
|
def create_point_grid(
|
|
8
14
|
*, bounds: tuple[float, float, float, float], cell_size: float
|
|
9
|
-
) -> tuple[
|
|
15
|
+
) -> tuple[NDArray, NDArray]:
|
|
10
16
|
"""Create a regular grid of point coordinates for raster centers.
|
|
11
17
|
|
|
12
18
|
This function replicates the original grid generation logic that uses
|
|
@@ -25,7 +31,8 @@ def create_point_grid(
|
|
|
25
31
|
x_coords = np.arange(xmin + cell_size / 2, xmax + cell_size / 2, cell_size)
|
|
26
32
|
y_coords = np.arange(ymax - cell_size / 2, ymin - cell_size / 2, -cell_size)
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
x_points, y_points = np.meshgrid(x_coords, y_coords) # type: ignore[reportAssignmentType]
|
|
35
|
+
return x_points, y_points
|
|
29
36
|
|
|
30
37
|
|
|
31
38
|
def get_point_grid_shape(
|
|
@@ -58,6 +65,8 @@ def create_fishnet(
|
|
|
58
65
|
Returns:
|
|
59
66
|
Shapely Polygons.
|
|
60
67
|
"""
|
|
68
|
+
import geopandas as gpd
|
|
69
|
+
|
|
61
70
|
# Use the shared helper function to create the point grid
|
|
62
71
|
xx, yy = create_point_grid(bounds=bounds, cell_size=res)
|
|
63
72
|
|
rastr/gis/smooth.py
CHANGED
|
@@ -5,12 +5,15 @@ Fork + Port of <https://github.com/philipschall/shapelysmooth> (Public domain)
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from typing import TypeAlias
|
|
8
|
+
from typing import TYPE_CHECKING, TypeAlias
|
|
9
9
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
from shapely.geometry import LineString, Polygon
|
|
12
12
|
from typing_extensions import assert_never
|
|
13
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from numpy.typing import NDArray
|
|
16
|
+
|
|
14
17
|
T: TypeAlias = LineString | Polygon
|
|
15
18
|
|
|
16
19
|
|
|
@@ -46,7 +49,7 @@ def catmull_rom_smooth(geometry: T, alpha: float = 0.5, subdivs: int = 10) -> T:
|
|
|
46
49
|
|
|
47
50
|
|
|
48
51
|
def _catmull_rom(
|
|
49
|
-
coords:
|
|
52
|
+
coords: NDArray,
|
|
50
53
|
*,
|
|
51
54
|
alpha: float = 0.5,
|
|
52
55
|
subdivs: int = 8,
|
|
@@ -89,7 +92,7 @@ def _catmull_rom(
|
|
|
89
92
|
|
|
90
93
|
|
|
91
94
|
def _recursive_eval(
|
|
92
|
-
slice4:
|
|
95
|
+
slice4: NDArray, tangents: list[float], ts: NDArray
|
|
93
96
|
) -> list[tuple[float, float]]:
|
|
94
97
|
"""De Boor/De Casteljau-style recursive linear interpolation over 4 control points.
|
|
95
98
|
|
|
@@ -128,7 +131,7 @@ def _recursive_eval(
|
|
|
128
131
|
|
|
129
132
|
def _get_coords(
|
|
130
133
|
geometry: LineString | Polygon,
|
|
131
|
-
) -> tuple[
|
|
134
|
+
) -> tuple[NDArray, list[NDArray]]:
|
|
132
135
|
if isinstance(geometry, LineString):
|
|
133
136
|
return np.array(geometry.coords), []
|
|
134
137
|
elif isinstance(geometry, Polygon):
|
rastr/io.py
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
2
5
|
|
|
3
6
|
import numpy as np
|
|
4
7
|
import rasterio
|
|
5
|
-
|
|
6
|
-
from pyproj.crs import CRS
|
|
8
|
+
import rasterio.merge
|
|
9
|
+
from pyproj.crs.crs import CRS
|
|
7
10
|
|
|
8
11
|
from rastr.meta import RasterMeta
|
|
9
12
|
from rastr.raster import RasterModel
|
|
10
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from numpy.typing import NDArray
|
|
16
|
+
|
|
11
17
|
|
|
12
|
-
def read_raster_inmem(
|
|
18
|
+
def read_raster_inmem(
|
|
19
|
+
raster_path: Path | str, *, crs: CRS | str | None = None
|
|
20
|
+
) -> RasterModel:
|
|
13
21
|
"""Read raster data from a file and return an in-memory Raster object."""
|
|
22
|
+
crs = CRS.from_user_input(crs) if crs is not None else None
|
|
23
|
+
|
|
14
24
|
with rasterio.open(raster_path, mode="r") as dst:
|
|
15
25
|
# Read the entire array
|
|
16
26
|
arr: NDArray[np.float64] = dst.read()
|
|
@@ -27,3 +37,58 @@ def read_raster_inmem(raster_path: Path | str, crs: CRS | None = None) -> Raster
|
|
|
27
37
|
raster_meta = RasterMeta(cell_size=cell_size, crs=crs, transform=transform)
|
|
28
38
|
raster_obj = RasterModel(arr=arr, raster_meta=raster_meta)
|
|
29
39
|
return raster_obj
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def read_raster_mosaic_inmem(
|
|
43
|
+
mosaic_dir: Path | str, *, glob: str = "*.tif", crs: CRS | None = None
|
|
44
|
+
) -> RasterModel:
|
|
45
|
+
"""Read a raster mosaic from a directory and return an in-memory Raster object.
|
|
46
|
+
|
|
47
|
+
This assumes that all rasters have the same metadata, e.g. coordinate system,
|
|
48
|
+
cell size, etc.
|
|
49
|
+
"""
|
|
50
|
+
mosaic_dir = Path(mosaic_dir)
|
|
51
|
+
raster_paths = list(mosaic_dir.glob(glob))
|
|
52
|
+
if not raster_paths:
|
|
53
|
+
msg = f"No raster files found in {mosaic_dir} matching {glob}"
|
|
54
|
+
raise FileNotFoundError(msg)
|
|
55
|
+
|
|
56
|
+
# Sort raster_paths in alphabetical order by stem
|
|
57
|
+
raster_paths.sort(key=lambda p: p.stem)
|
|
58
|
+
|
|
59
|
+
# Open all TIFF datasets using context managers to ensure proper closure
|
|
60
|
+
sources = []
|
|
61
|
+
try:
|
|
62
|
+
for raster_path in raster_paths:
|
|
63
|
+
src = rasterio.open(raster_path)
|
|
64
|
+
sources.append(src)
|
|
65
|
+
|
|
66
|
+
# Merge into a single mosaic array & transform
|
|
67
|
+
arr, transform = rasterio.merge.merge(sources)
|
|
68
|
+
|
|
69
|
+
# Copy metadata from the first dataset
|
|
70
|
+
out_meta = sources[0].meta.copy()
|
|
71
|
+
out_meta.update(
|
|
72
|
+
{
|
|
73
|
+
"driver": "GTiff",
|
|
74
|
+
"height": arr.shape[1],
|
|
75
|
+
"width": arr.shape[2],
|
|
76
|
+
"transform": transform,
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
cell_size = sources[0].res[0]
|
|
80
|
+
if crs is None:
|
|
81
|
+
crs = CRS.from_user_input(sources[0].crs)
|
|
82
|
+
|
|
83
|
+
nodata = sources[0].nodata
|
|
84
|
+
if nodata is not None:
|
|
85
|
+
arr[arr == nodata] = np.nan
|
|
86
|
+
|
|
87
|
+
arr = arr.squeeze().astype(np.float64)
|
|
88
|
+
|
|
89
|
+
raster_meta = RasterMeta(cell_size=cell_size, crs=crs, transform=transform)
|
|
90
|
+
raster_obj = RasterModel(arr=arr, raster_meta=raster_meta)
|
|
91
|
+
return raster_obj
|
|
92
|
+
finally:
|
|
93
|
+
for src in sources:
|
|
94
|
+
src.close()
|
rastr/meta.py
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
1
5
|
import numpy as np
|
|
2
6
|
from affine import Affine
|
|
3
7
|
from pydantic import BaseModel, InstanceOf
|
|
4
8
|
from pyproj import CRS
|
|
5
|
-
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
from typing_extensions import Self
|
|
6
13
|
|
|
7
14
|
|
|
8
15
|
class RasterMeta(BaseModel, extra="forbid"):
|
|
@@ -28,7 +35,7 @@ class RasterMeta(BaseModel, extra="forbid"):
|
|
|
28
35
|
transform=Affine.scale(2.0, 2.0),
|
|
29
36
|
)
|
|
30
37
|
|
|
31
|
-
def get_cell_centre_coords(self, shape: tuple[int, int]) ->
|
|
38
|
+
def get_cell_centre_coords(self, shape: tuple[int, int]) -> NDArray:
|
|
32
39
|
"""Return an array of (x, y) coordinates for the center of each cell.
|
|
33
40
|
|
|
34
41
|
The coordinates will be in the coordinate system defined by the
|
|
@@ -38,13 +45,43 @@ class RasterMeta(BaseModel, extra="forbid"):
|
|
|
38
45
|
shape: (rows, cols) of the raster array.
|
|
39
46
|
|
|
40
47
|
Returns:
|
|
41
|
-
|
|
42
|
-
cell center.
|
|
48
|
+
(x, y) coordinates for each cell center, with shape (rows, cols, 2)
|
|
43
49
|
"""
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
xv, yv = np.meshgrid(x_idx, y_idx)
|
|
48
|
-
x_coords, y_coords = self.transform * (xv + 0.5, yv + 0.5)
|
|
49
|
-
coords = np.stack([x_coords, y_coords], axis=-1)
|
|
50
|
+
x_coords = self.get_cell_x_coords(shape[1]) # cols for x-coordinates
|
|
51
|
+
y_coords = self.get_cell_y_coords(shape[0]) # rows for y-coordinates
|
|
52
|
+
coords = np.stack(np.meshgrid(x_coords, y_coords), axis=-1)
|
|
50
53
|
return coords
|
|
54
|
+
|
|
55
|
+
def get_cell_x_coords(self, n_columns: int) -> NDArray:
|
|
56
|
+
"""Return an array of x coordinates for the center of each cell.
|
|
57
|
+
|
|
58
|
+
The coordinates will be in the coordinate system defined by the
|
|
59
|
+
raster's transform.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
n_columns: Number of columns in the raster array.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
x_coordinates at cell centers, with shape (n_columns,)
|
|
66
|
+
"""
|
|
67
|
+
x_idx = np.arange(n_columns) + 0.5
|
|
68
|
+
y_idx = np.zeros_like(x_idx) # Use y=0 for a single row
|
|
69
|
+
x_coords, _ = self.transform * (x_idx, y_idx) # type: ignore[reportAssignmentType] overloaded tuple size in affine
|
|
70
|
+
return x_coords
|
|
71
|
+
|
|
72
|
+
def get_cell_y_coords(self, n_rows: int) -> NDArray:
|
|
73
|
+
"""Return an array of y coordinates for the center of each cell.
|
|
74
|
+
|
|
75
|
+
The coordinates will be in the coordinate system defined by the
|
|
76
|
+
raster's transform.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
n_rows: Number of rows in the raster array.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
y_coordinates at cell centers, with shape (n_rows,)
|
|
83
|
+
"""
|
|
84
|
+
x_idx = np.zeros(n_rows) # Use x=0 for a single column
|
|
85
|
+
y_idx = np.arange(n_rows) + 0.5
|
|
86
|
+
_, y_coords = self.transform * (x_idx, y_idx) # type: ignore[reportAssignmentType] overloaded tuple size in affine
|
|
87
|
+
return y_coords
|
rastr/raster.py
CHANGED
|
@@ -1,30 +1,24 @@
|
|
|
1
1
|
"""Raster data structure."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import warnings
|
|
4
7
|
from contextlib import contextmanager
|
|
5
8
|
from pathlib import Path
|
|
6
|
-
from typing import TYPE_CHECKING, Literal
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
7
10
|
|
|
8
|
-
import geopandas as gpd
|
|
9
|
-
import matplotlib as mpl
|
|
10
11
|
import numpy as np
|
|
11
|
-
import
|
|
12
|
+
import numpy.ma
|
|
12
13
|
import rasterio.plot
|
|
13
14
|
import rasterio.sample
|
|
14
15
|
import rasterio.transform
|
|
15
16
|
import skimage.measure
|
|
16
|
-
import xyzservices.providers as xyz
|
|
17
|
-
from matplotlib import pyplot as plt
|
|
18
|
-
from matplotlib.axes import Axes
|
|
19
|
-
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
20
|
-
from numpy.typing import NDArray
|
|
21
17
|
from pydantic import BaseModel, InstanceOf, field_validator
|
|
22
18
|
from pyproj.crs.crs import CRS
|
|
23
19
|
from rasterio.enums import Resampling
|
|
24
|
-
from rasterio.io import
|
|
25
|
-
from
|
|
26
|
-
from shapely.geometry import LineString, Polygon
|
|
27
|
-
from typing_extensions import Self
|
|
20
|
+
from rasterio.io import MemoryFile
|
|
21
|
+
from shapely.geometry import LineString, Point, Polygon
|
|
28
22
|
|
|
29
23
|
from rastr.arr.fill import fillna_nearest_neighbours
|
|
30
24
|
from rastr.gis.fishnet import create_fishnet
|
|
@@ -32,16 +26,14 @@ from rastr.gis.smooth import catmull_rom_smooth
|
|
|
32
26
|
from rastr.meta import RasterMeta
|
|
33
27
|
|
|
34
28
|
if TYPE_CHECKING:
|
|
35
|
-
from
|
|
29
|
+
from collections.abc import Callable, Generator
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
import folium
|
|
39
|
-
import folium.raster_layers
|
|
31
|
+
import geopandas as gpd
|
|
40
32
|
from folium import Map
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
from matplotlib.axes import Axes
|
|
34
|
+
from numpy.typing import ArrayLike, NDArray
|
|
35
|
+
from rasterio.io import BufferedDatasetWriter, DatasetReader, DatasetWriter
|
|
36
|
+
from typing_extensions import Self
|
|
45
37
|
|
|
46
38
|
try:
|
|
47
39
|
from rasterio._err import CPLE_BaseError
|
|
@@ -49,7 +41,9 @@ except ImportError:
|
|
|
49
41
|
CPLE_BaseError = Exception # Fallback if private module import fails
|
|
50
42
|
|
|
51
43
|
|
|
52
|
-
|
|
44
|
+
FOLIUM_INSTALLED = importlib.util.find_spec("folium") is not None
|
|
45
|
+
BRANCA_INSTALLED = importlib.util.find_spec("branca") is not None
|
|
46
|
+
MATPLOTLIB_INSTALLED = importlib.util.find_spec("matplotlib") is not None
|
|
53
47
|
|
|
54
48
|
|
|
55
49
|
class RasterCellArrayShapeError(ValueError):
|
|
@@ -62,6 +56,42 @@ class RasterModel(BaseModel):
|
|
|
62
56
|
arr: InstanceOf[np.ndarray]
|
|
63
57
|
raster_meta: RasterMeta
|
|
64
58
|
|
|
59
|
+
@property
|
|
60
|
+
def meta(self) -> RasterMeta:
|
|
61
|
+
"""Alias for raster_meta."""
|
|
62
|
+
return self.raster_meta
|
|
63
|
+
|
|
64
|
+
@meta.setter
|
|
65
|
+
def meta(self, value: RasterMeta) -> None:
|
|
66
|
+
self.raster_meta = value
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
arr: ArrayLike,
|
|
72
|
+
meta: RasterMeta | None = None,
|
|
73
|
+
raster_meta: RasterMeta | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
arr = np.asarray(arr)
|
|
76
|
+
|
|
77
|
+
# Set the meta
|
|
78
|
+
if meta is not None and raster_meta is not None:
|
|
79
|
+
msg = (
|
|
80
|
+
"Only one of 'meta' or 'raster_meta' should be provided, they are "
|
|
81
|
+
"aliases."
|
|
82
|
+
)
|
|
83
|
+
raise ValueError(msg)
|
|
84
|
+
elif meta is not None and raster_meta is None:
|
|
85
|
+
raster_meta = meta
|
|
86
|
+
elif meta is None and raster_meta is not None:
|
|
87
|
+
pass
|
|
88
|
+
else:
|
|
89
|
+
# Don't need to mention `'meta'` to simplify the messaging.
|
|
90
|
+
msg = "The attribute 'raster_meta' is required."
|
|
91
|
+
raise ValueError(msg)
|
|
92
|
+
|
|
93
|
+
super().__init__(arr=arr, raster_meta=raster_meta)
|
|
94
|
+
|
|
65
95
|
def __eq__(self, other: object) -> bool:
|
|
66
96
|
"""Check equality of two RasterModel objects."""
|
|
67
97
|
if not isinstance(other, RasterModel):
|
|
@@ -160,6 +190,16 @@ class RasterModel(BaseModel):
|
|
|
160
190
|
"""Get the coordinates of the cell centres in the raster."""
|
|
161
191
|
return self.raster_meta.get_cell_centre_coords(self.arr.shape)
|
|
162
192
|
|
|
193
|
+
@property
|
|
194
|
+
def cell_x_coords(self) -> NDArray[np.float64]:
|
|
195
|
+
"""Get the x coordinates of the cell centres in the raster."""
|
|
196
|
+
return self.raster_meta.get_cell_x_coords(self.arr.shape[0])
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def cell_y_coords(self) -> NDArray[np.float64]:
|
|
200
|
+
"""Get the y coordinates of the cell centres in the raster."""
|
|
201
|
+
return self.raster_meta.get_cell_y_coords(self.arr.shape[1])
|
|
202
|
+
|
|
163
203
|
@contextmanager
|
|
164
204
|
def to_rasterio_dataset(
|
|
165
205
|
self,
|
|
@@ -195,14 +235,15 @@ class RasterModel(BaseModel):
|
|
|
195
235
|
|
|
196
236
|
def sample(
|
|
197
237
|
self,
|
|
198
|
-
xy: list[tuple[float, float]],
|
|
238
|
+
xy: list[tuple[float, float]] | list[Point] | ArrayLike,
|
|
199
239
|
*,
|
|
200
240
|
na_action: Literal["raise", "ignore"] = "raise",
|
|
201
241
|
) -> NDArray[np.float64]:
|
|
202
242
|
"""Sample raster values at GeoSeries locations and return sampled values.
|
|
203
243
|
|
|
204
244
|
Args:
|
|
205
|
-
xy: A list of (x, y) coordinates to sample the
|
|
245
|
+
xy: A list of (x, y) coordinates or shapely Point objects to sample the
|
|
246
|
+
raster at.
|
|
206
247
|
na_action: Action to take when a NaN value is encountered in the input xy.
|
|
207
248
|
Options are "raise" (raise an error) or "ignore" (replace with
|
|
208
249
|
NaN).
|
|
@@ -213,6 +254,12 @@ class RasterModel(BaseModel):
|
|
|
213
254
|
# If this function is too slow, consider the optimizations detailed here:
|
|
214
255
|
# https://rdrn.me/optimising-sampling/
|
|
215
256
|
|
|
257
|
+
# Convert shapely Points to coordinate tuples if needed
|
|
258
|
+
if isinstance(xy, (list, tuple)):
|
|
259
|
+
xy = [_get_xy_tuple(point) for point in xy]
|
|
260
|
+
|
|
261
|
+
xy = np.asarray(xy, dtype=float)
|
|
262
|
+
|
|
216
263
|
# Short-circuit
|
|
217
264
|
if len(xy) == 0:
|
|
218
265
|
return np.array([], dtype=float)
|
|
@@ -249,7 +296,7 @@ class RasterModel(BaseModel):
|
|
|
249
296
|
|
|
250
297
|
# Convert the sampled values to a NumPy array and set masked values to NaN
|
|
251
298
|
raster_values = np.array(
|
|
252
|
-
[s.data[0] if not s
|
|
299
|
+
[s.data[0] if not numpy.ma.getmask(s) else np.nan for s in samples]
|
|
253
300
|
).astype(float)
|
|
254
301
|
|
|
255
302
|
if len(xy_nan_idxs) > 0:
|
|
@@ -298,19 +345,25 @@ class RasterModel(BaseModel):
|
|
|
298
345
|
m: Map | None = None,
|
|
299
346
|
opacity: float = 1.0,
|
|
300
347
|
colormap: str = "viridis",
|
|
348
|
+
cbar_label: str | None = None,
|
|
301
349
|
) -> Map:
|
|
302
350
|
"""Display the raster on a folium map."""
|
|
303
|
-
if not FOLIUM_INSTALLED:
|
|
304
|
-
msg = "The 'folium'
|
|
351
|
+
if not FOLIUM_INSTALLED or not MATPLOTLIB_INSTALLED:
|
|
352
|
+
msg = "The 'folium' and 'matplotlib' packages are required for 'explore()'."
|
|
305
353
|
raise ImportError(msg)
|
|
306
354
|
|
|
355
|
+
import folium.raster_layers
|
|
356
|
+
import geopandas as gpd
|
|
357
|
+
import matplotlib as mpl
|
|
358
|
+
|
|
307
359
|
if m is None:
|
|
308
360
|
m = folium.Map()
|
|
309
361
|
|
|
310
|
-
|
|
362
|
+
rgba_map: Callable[[float], tuple[float, float, float, float]] = mpl.colormaps[
|
|
311
363
|
colormap
|
|
312
364
|
]
|
|
313
365
|
|
|
366
|
+
# Cast to GDF to facilitate converting bounds to WGS84
|
|
314
367
|
wgs84_crs = CRS.from_epsg(4326)
|
|
315
368
|
gdf = gpd.GeoDataFrame(geometry=[self.bbox], crs=self.raster_meta.crs).to_crs(
|
|
316
369
|
wgs84_crs
|
|
@@ -320,8 +373,15 @@ class RasterModel(BaseModel):
|
|
|
320
373
|
arr = np.array(self.arr)
|
|
321
374
|
|
|
322
375
|
# Normalize the data to the range [0, 1] as this is the cmap range
|
|
323
|
-
|
|
324
|
-
|
|
376
|
+
with warnings.catch_warnings():
|
|
377
|
+
warnings.filterwarnings(
|
|
378
|
+
"ignore",
|
|
379
|
+
message="All-NaN slice encountered",
|
|
380
|
+
category=RuntimeWarning,
|
|
381
|
+
)
|
|
382
|
+
min_val = np.nanmin(arr)
|
|
383
|
+
max_val = np.nanmax(arr)
|
|
384
|
+
|
|
325
385
|
if max_val > min_val: # Prevent division by zero
|
|
326
386
|
arr = (arr - min_val) / (max_val - min_val)
|
|
327
387
|
else:
|
|
@@ -332,26 +392,46 @@ class RasterModel(BaseModel):
|
|
|
332
392
|
flip_x = self.raster_meta.transform.a < 0
|
|
333
393
|
flip_y = self.raster_meta.transform.e > 0
|
|
334
394
|
if flip_x:
|
|
335
|
-
arr = np.flip(
|
|
395
|
+
arr = np.flip(arr, axis=1)
|
|
336
396
|
if flip_y:
|
|
337
|
-
arr = np.flip(
|
|
397
|
+
arr = np.flip(arr, axis=0)
|
|
338
398
|
|
|
339
399
|
img = folium.raster_layers.ImageOverlay(
|
|
340
400
|
image=arr,
|
|
341
401
|
bounds=[[ymin, xmin], [ymax, xmax]],
|
|
342
402
|
opacity=opacity,
|
|
343
|
-
colormap=
|
|
403
|
+
colormap=rgba_map,
|
|
344
404
|
mercator_project=True,
|
|
345
405
|
)
|
|
346
406
|
|
|
347
407
|
img.add_to(m)
|
|
348
408
|
|
|
409
|
+
# Add a colorbar legend
|
|
410
|
+
if BRANCA_INSTALLED:
|
|
411
|
+
from branca.colormap import LinearColormap as BrancaLinearColormap
|
|
412
|
+
from matplotlib.colors import to_hex
|
|
413
|
+
|
|
414
|
+
# Determine legend data range in original units
|
|
415
|
+
vmin = float(min_val) if np.isfinite(min_val) else 0.0
|
|
416
|
+
vmax = float(max_val) if np.isfinite(max_val) else 1.0
|
|
417
|
+
if vmax <= vmin:
|
|
418
|
+
vmax = vmin + 1.0
|
|
419
|
+
|
|
420
|
+
sample_points = np.linspace(0, 1, rgba_map.N)
|
|
421
|
+
colors = [to_hex(rgba_map(x)) for x in sample_points]
|
|
422
|
+
legend = BrancaLinearColormap(colors=colors, vmin=vmin, vmax=vmax)
|
|
423
|
+
if cbar_label:
|
|
424
|
+
legend.caption = cbar_label
|
|
425
|
+
legend.add_to(m)
|
|
426
|
+
|
|
349
427
|
m.fit_bounds([[ymin, xmin], [ymax, xmax]])
|
|
350
428
|
|
|
351
429
|
return m
|
|
352
430
|
|
|
353
431
|
def to_clipboard(self) -> None:
|
|
354
432
|
"""Copy the raster cell array to the clipboard."""
|
|
433
|
+
import pandas as pd
|
|
434
|
+
|
|
355
435
|
pd.DataFrame(self.arr).to_clipboard(index=False, header=False)
|
|
356
436
|
|
|
357
437
|
def plot(
|
|
@@ -363,9 +443,17 @@ class RasterModel(BaseModel):
|
|
|
363
443
|
cmap: str = "viridis",
|
|
364
444
|
) -> Axes:
|
|
365
445
|
"""Plot the raster on a matplotlib axis."""
|
|
446
|
+
if not MATPLOTLIB_INSTALLED:
|
|
447
|
+
msg = "The 'matplotlib' package is required for 'plot()'."
|
|
448
|
+
raise ImportError(msg)
|
|
449
|
+
|
|
450
|
+
from matplotlib import pyplot as plt
|
|
451
|
+
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
452
|
+
|
|
366
453
|
if ax is None:
|
|
367
|
-
_,
|
|
368
|
-
|
|
454
|
+
_, _ax = plt.subplots()
|
|
455
|
+
_ax: Axes
|
|
456
|
+
ax = _ax
|
|
369
457
|
|
|
370
458
|
if basemap:
|
|
371
459
|
msg = "Basemap plotting is not yet implemented."
|
|
@@ -387,8 +475,8 @@ class RasterModel(BaseModel):
|
|
|
387
475
|
max_y_nonzero = np.max(y_nonzero)
|
|
388
476
|
|
|
389
477
|
# Transform to raster CRS
|
|
390
|
-
x1, y1 = self.raster_meta.transform * (min_x_nonzero, min_y_nonzero)
|
|
391
|
-
x2, y2 = self.raster_meta.transform * (max_x_nonzero, max_y_nonzero)
|
|
478
|
+
x1, y1 = self.raster_meta.transform * (min_x_nonzero, min_y_nonzero) # type: ignore[reportAssignmentType] overloaded tuple size in affine
|
|
479
|
+
x2, y2 = self.raster_meta.transform * (max_x_nonzero, max_y_nonzero) # type: ignore[reportAssignmentType]
|
|
392
480
|
xmin, xmax = sorted([x1, x2])
|
|
393
481
|
ymin, ymax = sorted([y1, y2])
|
|
394
482
|
|
|
@@ -409,11 +497,14 @@ class RasterModel(BaseModel):
|
|
|
409
497
|
divider = make_axes_locatable(ax)
|
|
410
498
|
cax = divider.append_axes("right", size="5%", pad=0.05)
|
|
411
499
|
fig = ax.get_figure()
|
|
412
|
-
fig
|
|
500
|
+
if fig is not None:
|
|
501
|
+
fig.colorbar(img, label=cbar_label, cax=cax)
|
|
413
502
|
return ax
|
|
414
503
|
|
|
415
504
|
def as_geodataframe(self, name: str = "value") -> gpd.GeoDataFrame:
|
|
416
505
|
"""Create a GeoDataFrame representation of the raster."""
|
|
506
|
+
import geopandas as gpd
|
|
507
|
+
|
|
417
508
|
polygons = create_fishnet(bounds=self.bounds, res=self.raster_meta.cell_size)
|
|
418
509
|
point_tuples = [polygon.centroid.coords[0] for polygon in polygons]
|
|
419
510
|
raster_gdf = gpd.GeoDataFrame(
|
|
@@ -493,24 +584,22 @@ class RasterModel(BaseModel):
|
|
|
493
584
|
return new_raster
|
|
494
585
|
|
|
495
586
|
def get_xy(self) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
496
|
-
"""Get the x and y coordinates of the raster in meshgrid format.
|
|
497
|
-
col_idx, row_idx = np.meshgrid(
|
|
498
|
-
np.arange(self.arr.shape[1]),
|
|
499
|
-
np.arange(self.arr.shape[0]),
|
|
500
|
-
)
|
|
587
|
+
"""Get the x and y coordinates of the raster cell centres in meshgrid format.
|
|
501
588
|
|
|
502
|
-
|
|
503
|
-
|
|
589
|
+
Returns the coordinates of the cell centres as two separate 2D arrays in
|
|
590
|
+
meshgrid format, where each array has the same shape as the raster data array.
|
|
504
591
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
592
|
+
Returns:
|
|
593
|
+
A tuple of (x, y) coordinate arrays where:
|
|
594
|
+
- x: 2D array of x-coordinates of cell centres
|
|
595
|
+
- y: 2D array of y-coordinates of cell centres
|
|
596
|
+
Both arrays have the same shape as the raster data array.
|
|
597
|
+
"""
|
|
598
|
+
coords = self.raster_meta.get_cell_centre_coords(self.arr.shape)
|
|
599
|
+
return coords[:, :, 0], coords[:, :, 1]
|
|
511
600
|
|
|
512
601
|
def contour(
|
|
513
|
-
self, *, levels: list[float], smoothing: bool = True
|
|
602
|
+
self, *, levels: list[float] | NDArray, smoothing: bool = True
|
|
514
603
|
) -> gpd.GeoDataFrame:
|
|
515
604
|
"""Create contour lines from the raster data, optionally with smoothing.
|
|
516
605
|
|
|
@@ -521,13 +610,14 @@ class RasterModel(BaseModel):
|
|
|
521
610
|
contouring, to denoise the contours.
|
|
522
611
|
|
|
523
612
|
Args:
|
|
524
|
-
levels: A list of contour levels to generate. The contour lines
|
|
525
|
-
generated for each level in this
|
|
613
|
+
levels: A list or array of contour levels to generate. The contour lines
|
|
614
|
+
will be generated for each level in this sequence.
|
|
526
615
|
smoothing: Defaults to true, which corresponds to applying a smoothing
|
|
527
616
|
algorithm to the contour lines. At the moment, this is the
|
|
528
617
|
Catmull-Rom spline algorithm. If set to False, the raw
|
|
529
618
|
contours will be returned without any smoothing.
|
|
530
619
|
"""
|
|
620
|
+
import geopandas as gpd
|
|
531
621
|
|
|
532
622
|
all_levels = []
|
|
533
623
|
all_geoms = []
|
|
@@ -576,6 +666,7 @@ class RasterModel(BaseModel):
|
|
|
576
666
|
coordinate distance (e.g. meters). A larger sigma results in a more
|
|
577
667
|
blurred image.
|
|
578
668
|
"""
|
|
669
|
+
from scipy.ndimage import gaussian_filter
|
|
579
670
|
|
|
580
671
|
cell_sigma = sigma / self.raster_meta.cell_size
|
|
581
672
|
|
|
@@ -605,6 +696,82 @@ class RasterModel(BaseModel):
|
|
|
605
696
|
|
|
606
697
|
return raster
|
|
607
698
|
|
|
699
|
+
def crop(
|
|
700
|
+
self,
|
|
701
|
+
bounds: tuple[float, float, float, float],
|
|
702
|
+
strategy: Literal["underflow", "overflow"] = "underflow",
|
|
703
|
+
) -> Self:
|
|
704
|
+
"""Crop the raster to the specified bounds.
|
|
705
|
+
|
|
706
|
+
Args:
|
|
707
|
+
bounds: A tuple of (minx, miny, maxx, maxy) defining the bounds to crop to.
|
|
708
|
+
strategy: The cropping strategy to use. 'underflow' will crop the raster
|
|
709
|
+
to be fully within the bounds, ignoring any cells that are
|
|
710
|
+
partially outside the bounds. 'overflow' will instead include
|
|
711
|
+
cells that intersect the bounds, ensuring the bounds area
|
|
712
|
+
remains covered with cells.
|
|
713
|
+
|
|
714
|
+
Returns:
|
|
715
|
+
A new RasterModel instance cropped to the specified bounds.
|
|
716
|
+
"""
|
|
717
|
+
|
|
718
|
+
minx, miny, maxx, maxy = bounds
|
|
719
|
+
arr = self.arr
|
|
720
|
+
|
|
721
|
+
# Get the half cell size for cropping
|
|
722
|
+
cell_size = self.raster_meta.cell_size
|
|
723
|
+
half_cell_size = cell_size / 2
|
|
724
|
+
|
|
725
|
+
# Get the cell centre coordinates as 1D arrays
|
|
726
|
+
x_coords = self.cell_x_coords
|
|
727
|
+
y_coords = self.cell_y_coords
|
|
728
|
+
|
|
729
|
+
# Get the indices to crop the array
|
|
730
|
+
if strategy == "underflow":
|
|
731
|
+
x_idx = (x_coords >= minx + half_cell_size) & (
|
|
732
|
+
x_coords <= maxx - half_cell_size
|
|
733
|
+
)
|
|
734
|
+
y_idx = (y_coords >= miny + half_cell_size) & (
|
|
735
|
+
y_coords <= maxy - half_cell_size
|
|
736
|
+
)
|
|
737
|
+
elif strategy == "overflow":
|
|
738
|
+
x_idx = (x_coords > minx - half_cell_size) & (
|
|
739
|
+
x_coords < maxx + half_cell_size
|
|
740
|
+
)
|
|
741
|
+
y_idx = (y_coords > miny - half_cell_size) & (
|
|
742
|
+
y_coords < maxy + half_cell_size
|
|
743
|
+
)
|
|
744
|
+
else:
|
|
745
|
+
msg = f"Unsupported cropping strategy: {strategy}"
|
|
746
|
+
raise NotImplementedError(msg)
|
|
747
|
+
|
|
748
|
+
# Crop the array
|
|
749
|
+
cropped_arr = arr[np.ix_(x_idx, y_idx)]
|
|
750
|
+
|
|
751
|
+
# Check the shape of the cropped array
|
|
752
|
+
if cropped_arr.size == 0:
|
|
753
|
+
msg = "Cropped array is empty; no cells within the specified bounds."
|
|
754
|
+
raise ValueError(msg)
|
|
755
|
+
|
|
756
|
+
# Recalculate the transform for the cropped raster
|
|
757
|
+
x_coords = x_coords[x_idx]
|
|
758
|
+
y_coords = y_coords[y_idx]
|
|
759
|
+
transform = rasterio.transform.from_bounds(
|
|
760
|
+
west=x_coords.min() - half_cell_size,
|
|
761
|
+
south=y_coords.min() - half_cell_size,
|
|
762
|
+
east=x_coords.max() + half_cell_size,
|
|
763
|
+
north=y_coords.max() + half_cell_size,
|
|
764
|
+
width=cropped_arr.shape[1],
|
|
765
|
+
height=cropped_arr.shape[0],
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
# Update the raster
|
|
769
|
+
cls = self.__class__
|
|
770
|
+
new_meta = RasterMeta(
|
|
771
|
+
cell_size=cell_size, crs=self.raster_meta.crs, transform=transform
|
|
772
|
+
)
|
|
773
|
+
return cls(arr=cropped_arr, raster_meta=new_meta)
|
|
774
|
+
|
|
608
775
|
def resample(
|
|
609
776
|
self, new_cell_size: float, *, method: Literal["bilinear"] = "bilinear"
|
|
610
777
|
) -> Self:
|
|
@@ -654,9 +821,24 @@ class RasterModel(BaseModel):
|
|
|
654
821
|
|
|
655
822
|
@field_validator("arr")
|
|
656
823
|
@classmethod
|
|
657
|
-
def check_2d_array(cls, v:
|
|
824
|
+
def check_2d_array(cls, v: NDArray) -> NDArray:
|
|
658
825
|
"""Validator to ensure the cell array is 2D."""
|
|
659
826
|
if v.ndim != 2:
|
|
660
827
|
msg = "Cell array must be 2D"
|
|
661
828
|
raise RasterCellArrayShapeError(msg)
|
|
662
829
|
return v
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def _get_xy_tuple(xy: Any) -> tuple[float, float]:
|
|
833
|
+
"""Convert Point or coordinate tuple to coordinate tuple.
|
|
834
|
+
|
|
835
|
+
Args:
|
|
836
|
+
xy: Either a coordinate tuple or a shapely Point object.
|
|
837
|
+
|
|
838
|
+
Returns:
|
|
839
|
+
A coordinate tuple (x, y).
|
|
840
|
+
"""
|
|
841
|
+
if isinstance(xy, Point):
|
|
842
|
+
return (xy.x, xy.y)
|
|
843
|
+
x, y = xy
|
|
844
|
+
return (float(x), float(y))
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rastr
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Geospatial Raster datatype library for Python.
|
|
5
|
+
Project-URL: Source Code, https://github.com/tonkintaylor/rastr
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/tonkintaylor/rastr/issues
|
|
7
|
+
Project-URL: Releases, https://github.com/tonkintaylor/rastr/releases
|
|
8
|
+
Project-URL: Source Archive, https://github.com/tonkintaylor/rastr/archive/46f802e7abd275eff61c73c5edc147d92966c886.zip
|
|
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
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: affine>=2.4.0
|
|
19
|
+
Requires-Dist: branca>=0.8.1
|
|
20
|
+
Requires-Dist: folium>=0.20.0
|
|
21
|
+
Requires-Dist: geopandas>=1.1.1
|
|
22
|
+
Requires-Dist: matplotlib>=3.10.5
|
|
23
|
+
Requires-Dist: numpy>=2.2.6
|
|
24
|
+
Requires-Dist: pandas>=2.3.1
|
|
25
|
+
Requires-Dist: pydantic>=2.11.7
|
|
26
|
+
Requires-Dist: pyproj>=3.7.1
|
|
27
|
+
Requires-Dist: rasterio>=1.4.3
|
|
28
|
+
Requires-Dist: scikit-image>=0.25.2
|
|
29
|
+
Requires-Dist: scipy>=1.15.3
|
|
30
|
+
Requires-Dist: shapely>=2.1.1
|
|
31
|
+
Requires-Dist: tqdm>=4.67.1
|
|
32
|
+
Requires-Dist: typing-extensions>=4.14.1
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
<h1 align="center">
|
|
36
|
+
<img src="https://raw.githubusercontent.com/tonkintaylor/rastr/refs/heads/develop/docs/logo.svg"><br>
|
|
37
|
+
</h1>
|
|
38
|
+
|
|
39
|
+
# rastr
|
|
40
|
+
|
|
41
|
+
[](<https://pypi.python.org/pypi/rastr>)
|
|
42
|
+
[](https://github.com/astral-sh/uv)
|
|
43
|
+
[](https://github.com/astral-sh/ruff)
|
|
44
|
+
[](https://github.com/usethis-python/usethis-python)
|
|
45
|
+
|
|
46
|
+
A lightweight geospatial raster datatype library for Python focused on simplicity.
|
|
47
|
+
|
|
48
|
+
## Overview
|
|
49
|
+
|
|
50
|
+
`rastr` provides an intuitive interface for creating, reading, manipulating, and exporting geospatial raster data in Python.
|
|
51
|
+
|
|
52
|
+
### Features
|
|
53
|
+
|
|
54
|
+
- 🧮 **Complete raster arithmetic**: Full support for mathematical operations (`+`, `-`, `*`, `/`) between rasters and scalars.
|
|
55
|
+
- 📊 **Flexible visualization**: Built-in plotting with matplotlib and interactive mapping with folium.
|
|
56
|
+
- 🗺️ **Geospatial analysis tools**: Contour generation, Gaussian blurring, and spatial sampling.
|
|
57
|
+
- 🛠️ **Data manipulation**: Fill NaN values, extrapolate missing data, and resample to different resolutions.
|
|
58
|
+
- 🔗 **Seamless integration**: Works with GeoPandas, rasterio, and the broader Python geospatial ecosystem.
|
|
59
|
+
- ↔️ **Vector-to-raster workflows**: Convert GeoDataFrame polygons, points, and lines to raster format.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# With uv
|
|
65
|
+
uv add rastr
|
|
66
|
+
|
|
67
|
+
# With pip
|
|
68
|
+
pip install rastr
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from pyproj.crs.crs import CRS
|
|
75
|
+
from rasterio.transform import from_origin
|
|
76
|
+
from rastr.create import full_raster
|
|
77
|
+
from rastr.meta import RasterMeta
|
|
78
|
+
from rastr.raster import RasterModel
|
|
79
|
+
|
|
80
|
+
# Create an example raster
|
|
81
|
+
raster = RasterModel.example()
|
|
82
|
+
|
|
83
|
+
# Basic arithmetic operations
|
|
84
|
+
doubled = raster * 2
|
|
85
|
+
summed = raster + 10
|
|
86
|
+
combined = raster + doubled
|
|
87
|
+
|
|
88
|
+
# Create full rasters with specified values
|
|
89
|
+
cell_size = 1.0
|
|
90
|
+
empty_raster = full_raster(
|
|
91
|
+
RasterMeta(
|
|
92
|
+
cell_size=cell_size,
|
|
93
|
+
crs=CRS.from_epsg(2193),
|
|
94
|
+
transform=from_origin(0, 100, cell_size, cell_size),
|
|
95
|
+
),
|
|
96
|
+
bounds=(0, 0, 100, 100),
|
|
97
|
+
fill_value=0.0,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Visualize the data
|
|
101
|
+
ax = raster.plot(cbar_label="Values")
|
|
102
|
+
|
|
103
|
+
# Interactive web mapping (requires folium)
|
|
104
|
+
m = raster.explore(opacity=0.8, colormap="plasma")
|
|
105
|
+
|
|
106
|
+
# Sample values at specific coordinates
|
|
107
|
+
xy_points = [(100.0, 200.0), (150.0, 250.0)]
|
|
108
|
+
values = raster.sample(xy_points)
|
|
109
|
+
|
|
110
|
+
# Generate contour lines
|
|
111
|
+
contours = raster.contour(levels=[0.1, 0.5, 0.9], smoothing=True)
|
|
112
|
+
|
|
113
|
+
# Apply spatial operations
|
|
114
|
+
blurred = raster.blur(sigma=2.0) # Gaussian blur
|
|
115
|
+
filled = raster.extrapolate(method="nearest") # Fill NaN values via nearest-neighbours
|
|
116
|
+
resampled = raster.resample(new_cell_size=0.5) # Change resolution
|
|
117
|
+
|
|
118
|
+
# Export to file
|
|
119
|
+
raster.to_file("output.tif")
|
|
120
|
+
|
|
121
|
+
# Convert to GeoDataFrame for vector analysis
|
|
122
|
+
gdf = raster.as_geodataframe(name="elevation")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Limitations
|
|
126
|
+
|
|
127
|
+
Current version limitations:
|
|
128
|
+
|
|
129
|
+
- Only Single-band rasters are supported.
|
|
130
|
+
- In-memory processing only (streaming support planned).
|
|
131
|
+
- Square cells only (rectangular cell support planned).
|
|
132
|
+
- Only float dtypes (integer support planned).
|
|
133
|
+
|
|
134
|
+
### Contributing
|
|
135
|
+
|
|
136
|
+
See the
|
|
137
|
+
[CONTRIBUTING.md](https://github.com/usethis-python/usethis-python/blob/main/CONTRIBUTING.md)
|
|
138
|
+
file.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
rastr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
rastr/_version.py,sha256=5zTqm8rgXsWYBpB2M3Zw_K1D-aV8wP7NsBLrmMKkrAQ,704
|
|
3
|
+
rastr/create.py,sha256=6aHpRFRXmpXzuzTt-SxY_BfVc7dXKCBLCArb7DjUrsM,13494
|
|
4
|
+
rastr/io.py,sha256=RgkiV_emOPjTFeI2a1aCBtfWwrSLH0XmP8rx9tu6PAI,2952
|
|
5
|
+
rastr/meta.py,sha256=5iDvGkYe8iMMkPV6gSL04jNcLRhuRNFqe9AppUpp55E,2928
|
|
6
|
+
rastr/raster.py,sha256=iHN8L91HW-cpDexZYgDnLaOw-N6KktQpXIusvUEiL0o,29931
|
|
7
|
+
rastr/arr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
rastr/arr/fill.py,sha256=80ucb36el9s042fDSwK1SZJhp_GNJNMM0fpQTWmJvgE,1001
|
|
9
|
+
rastr/gis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
rastr/gis/fishnet.py,sha256=Ic-0HV61ST8OxwhyoMyV_ybihs2xuhgAY-3n4CknAt8,2670
|
|
11
|
+
rastr/gis/smooth.py,sha256=3HQDQHQM5_LeNk21R8Eb8VpF727JcXq21HO9JMvcpW4,4810
|
|
12
|
+
rastr-0.3.0.dist-info/METADATA,sha256=pr4zdjI5FPlVynm2W09ErYddJ0Nmvx9wICWzBH2IpNI,4953
|
|
13
|
+
rastr-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
+
rastr-0.3.0.dist-info/licenses/LICENSE,sha256=7qUsx93G2ATTRLZiSYuQofwAX_uWvrqnAiMK8PzxvNc,1080
|
|
15
|
+
rastr-0.3.0.dist-info/RECORD,,
|
rastr-0.2.0.dist-info/METADATA
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: rastr
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: Geospatial Raster datatype library for Python.
|
|
5
|
-
Project-URL: Source Code, https://github.com/tonkintaylor/rastr
|
|
6
|
-
Project-URL: Bug Tracker, https://github.com/tonkintaylor/rastr/issues
|
|
7
|
-
Project-URL: Releases, https://github.com/tonkintaylor/rastr/releases
|
|
8
|
-
Project-URL: Source Archive, https://github.com/tonkintaylor/rastr/archive/551d36604b84f947fffbe328e9e877732c9e35fb.zip
|
|
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
|
-
License-Expression: MIT
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
-
Requires-Python: >=3.10
|
|
18
|
-
Requires-Dist: affine>=2.4.0
|
|
19
|
-
Requires-Dist: folium>=0.20.0
|
|
20
|
-
Requires-Dist: geopandas>=1.1.1
|
|
21
|
-
Requires-Dist: matplotlib>=3.10.5
|
|
22
|
-
Requires-Dist: numpy>=2.2.6
|
|
23
|
-
Requires-Dist: pandas>=2.3.1
|
|
24
|
-
Requires-Dist: pydantic>=2.11.7
|
|
25
|
-
Requires-Dist: pyproj>=3.7.1
|
|
26
|
-
Requires-Dist: rasterio>=1.4.3
|
|
27
|
-
Requires-Dist: scikit-image>=0.25.2
|
|
28
|
-
Requires-Dist: scipy>=1.15.3
|
|
29
|
-
Requires-Dist: shapely>=2.1.1
|
|
30
|
-
Requires-Dist: tqdm>=4.67.1
|
|
31
|
-
Requires-Dist: typing-extensions>=4.14.1
|
|
32
|
-
Requires-Dist: xyzservices>=2025.4.0
|
|
33
|
-
Description-Content-Type: text/markdown
|
|
34
|
-
|
|
35
|
-
# Rastr
|
|
36
|
-
|
|
37
|
-
[](<https://pypi.python.org/pypi/rastr>)
|
|
38
|
-
[](https://github.com/astral-sh/uv)
|
|
39
|
-
[](https://github.com/astral-sh/ruff)
|
|
40
|
-
[](https://github.com/usethis-python/usethis-python)
|
|
41
|
-
|
|
42
|
-
Geospatial Raster datatype library for Python.
|
|
43
|
-
|
|
44
|
-
Currently, only single-banded, in-memory rasters with square cells are supported.
|
rastr-0.2.0.dist-info/RECORD
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
rastr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
rastr/_version.py,sha256=iB5DfB5V6YB5Wo4JmvS-txT42QtmGaWcWp3udRT7zCI,511
|
|
3
|
-
rastr/create.py,sha256=tHLVnGarMt04p1z8CVknMWMjQLKrb0WrcP_Wgdw8xr4,9346
|
|
4
|
-
rastr/io.py,sha256=dKOY5JYQDULg3Si24cysVTZuWoPjftAE8pbxgSoN1tw,967
|
|
5
|
-
rastr/meta.py,sha256=jmDDx3w61ZZ3dQSrMFgaBx0LJxYp42xTm_7uS_s96lg,1547
|
|
6
|
-
rastr/raster.py,sha256=jGk9buG1uKQ3tb3ciI4yhw9JFu7oyXWXMnUBnPjOD54,23142
|
|
7
|
-
rastr/arr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
rastr/arr/fill.py,sha256=7N3ECMli7ssNJJk5qgDoj_3xExgu03nohGPgUKWxcCk,903
|
|
9
|
-
rastr/gis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
rastr/gis/fishnet.py,sha256=LZqtI9cgYPacuWNfIfdbTkRMRSnJCQdYlaT2eVPmorM,2459
|
|
11
|
-
rastr/gis/smooth.py,sha256=LbWvAG1O-O5H6P5LrbwD03mbnXVYjkH1re-_iZ4arIU,4754
|
|
12
|
-
rastr-0.2.0.dist-info/METADATA,sha256=7uUi8aLMaEbxoPfk6go_szv3H_TnDGYGE4RSC_sbUqo,2191
|
|
13
|
-
rastr-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
-
rastr-0.2.0.dist-info/licenses/LICENSE,sha256=7qUsx93G2ATTRLZiSYuQofwAX_uWvrqnAiMK8PzxvNc,1080
|
|
15
|
-
rastr-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|