rastr 0.7.1__py3-none-any.whl → 0.8.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 +2 -2
- rastr/create.py +156 -7
- rastr/gis/fishnet.py +3 -3
- rastr/gis/interpolate.py +5 -1
- rastr/io.py +120 -1
- rastr/meta.py +1 -1
- rastr/raster.py +217 -86
- {rastr-0.7.1.dist-info → rastr-0.8.0.dist-info}/METADATA +5 -7
- rastr-0.8.0.dist-info/RECORD +17 -0
- rastr-0.7.1.dist-info/RECORD +0 -17
- {rastr-0.7.1.dist-info → rastr-0.8.0.dist-info}/WHEEL +0 -0
- {rastr-0.7.1.dist-info → rastr-0.8.0.dist-info}/licenses/LICENSE +0 -0
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.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.8.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 8, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
rastr/create.py
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import importlib.util
|
|
4
4
|
import warnings
|
|
5
|
-
from typing import TYPE_CHECKING, TypeVar
|
|
5
|
+
from typing import TYPE_CHECKING, Literal, TypeVar
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
import rasterio.features
|
|
@@ -10,19 +10,21 @@ import rasterio.transform
|
|
|
10
10
|
from affine import Affine
|
|
11
11
|
from pyproj import CRS
|
|
12
12
|
from shapely.geometry import Point
|
|
13
|
+
from typing_extensions import assert_never
|
|
13
14
|
|
|
14
15
|
from rastr.gis.crs import get_affine_sign
|
|
15
16
|
from rastr.gis.fishnet import create_point_grid, get_point_grid_shape
|
|
16
|
-
from rastr.gis.interpolate import interpn_kernel
|
|
17
|
+
from rastr.gis.interpolate import InterpolationError, interpn_kernel
|
|
17
18
|
from rastr.meta import RasterMeta
|
|
18
|
-
from rastr.raster import Raster
|
|
19
|
+
from rastr.raster import Raster, RasterModel
|
|
19
20
|
|
|
20
21
|
if TYPE_CHECKING:
|
|
21
22
|
from collections.abc import Collection, Iterable
|
|
22
23
|
|
|
23
24
|
import geopandas as gpd
|
|
24
|
-
from numpy.typing import ArrayLike
|
|
25
|
+
from numpy.typing import ArrayLike, NDArray
|
|
25
26
|
from shapely.geometry import Polygon
|
|
27
|
+
from shapely.geometry.base import BaseGeometry
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
TQDM_INSTALLED = importlib.util.find_spec("tqdm") is not None
|
|
@@ -215,6 +217,137 @@ def rasterize_gdf(
|
|
|
215
217
|
return rasters
|
|
216
218
|
|
|
217
219
|
|
|
220
|
+
def rasterize_z_gdf(
|
|
221
|
+
gdf: gpd.GeoDataFrame,
|
|
222
|
+
*,
|
|
223
|
+
cell_size: float,
|
|
224
|
+
crs: CRS | str,
|
|
225
|
+
agg: Literal["mean", "min", "max"] = "mean",
|
|
226
|
+
) -> RasterModel:
|
|
227
|
+
"""Rasterize interpolated Z-values from geometries in a GeoDataFrame.
|
|
228
|
+
|
|
229
|
+
Handles overlapping geometries by aggregating values using a specified method.
|
|
230
|
+
All geometries must be 3D (have Z coordinates) for interpolation to work.
|
|
231
|
+
|
|
232
|
+
The Z-value for each cell is interpolated at the cell center.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
gdf: GeoDataFrame containing 3D geometries with Z coordinates.
|
|
236
|
+
cell_size: Desired cell size for the output raster.
|
|
237
|
+
crs: Coordinate reference system for the output raster.
|
|
238
|
+
agg: Aggregation function to use for overlapping values ("mean", "min", "max").
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
A raster of interpolated Z values.
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
ValueError: If any geometries are not 3D.
|
|
245
|
+
"""
|
|
246
|
+
crs = CRS.from_user_input(crs)
|
|
247
|
+
|
|
248
|
+
if len(gdf) == 0:
|
|
249
|
+
msg = "Cannot rasterize an empty GeoDataFrame."
|
|
250
|
+
raise ValueError(msg)
|
|
251
|
+
|
|
252
|
+
_validate_geometries_are_3d(gdf)
|
|
253
|
+
|
|
254
|
+
# Determine the bounds that would encompass the geometry while respecting the grid
|
|
255
|
+
gdf_bounds = gdf.total_bounds
|
|
256
|
+
meta, shape = RasterMeta.infer(
|
|
257
|
+
x=np.array([gdf_bounds[0], gdf_bounds[2]]),
|
|
258
|
+
y=np.array([gdf_bounds[1], gdf_bounds[3]]),
|
|
259
|
+
cell_size=cell_size,
|
|
260
|
+
crs=crs,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Generate grid coordinates for interpolation
|
|
264
|
+
x_coords, y_coords = _get_grid(meta, shape=shape)
|
|
265
|
+
|
|
266
|
+
# Create 2D accumulation arrays
|
|
267
|
+
z_stack = []
|
|
268
|
+
for geom in gdf.geometry:
|
|
269
|
+
z_vals = _interpolate_z_in_geometry(geom, x_coords, y_coords)
|
|
270
|
+
z_stack.append(z_vals)
|
|
271
|
+
|
|
272
|
+
if not z_stack:
|
|
273
|
+
msg = (
|
|
274
|
+
"No valid Z values could be interpolated from the geometries. Raster "
|
|
275
|
+
"will be entirely NaN-valued."
|
|
276
|
+
)
|
|
277
|
+
warnings.warn(msg, stacklevel=2)
|
|
278
|
+
arr = np.full(shape, np.nan, dtype=np.float64)
|
|
279
|
+
return RasterModel(arr=arr, raster_meta=meta)
|
|
280
|
+
|
|
281
|
+
z_stack = np.array(z_stack) # Shape: (N, height * width)
|
|
282
|
+
|
|
283
|
+
with warnings.catch_warnings():
|
|
284
|
+
warnings.filterwarnings(
|
|
285
|
+
"ignore", category=RuntimeWarning, message="All-NaN slice encountered"
|
|
286
|
+
)
|
|
287
|
+
warnings.filterwarnings(
|
|
288
|
+
"ignore", category=RuntimeWarning, message="Mean of empty slice"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if agg == "mean":
|
|
292
|
+
z_agg = np.nanmean(z_stack, axis=0)
|
|
293
|
+
elif agg == "min":
|
|
294
|
+
z_agg = np.nanmin(z_stack, axis=0)
|
|
295
|
+
elif agg == "max":
|
|
296
|
+
z_agg = np.nanmax(z_stack, axis=0)
|
|
297
|
+
else:
|
|
298
|
+
assert_never(agg)
|
|
299
|
+
|
|
300
|
+
arr = np.asarray(z_agg, dtype=np.float64).reshape(shape)
|
|
301
|
+
|
|
302
|
+
return RasterModel(arr=arr, raster_meta=meta)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _validate_geometries_are_3d(gdf: gpd.GeoDataFrame) -> None:
|
|
306
|
+
"""Validate that all geometries have 3D coordinates (Z values).
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
gdf: The GeoDataFrame to check for 3D geometries.
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
ValueError: If any geometries are not 3D.
|
|
313
|
+
"""
|
|
314
|
+
for idx, geom in enumerate(gdf.geometry):
|
|
315
|
+
if geom is None or geom.is_empty:
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
# Check if geometry has Z coordinates
|
|
319
|
+
if not geom.has_z:
|
|
320
|
+
msg = (
|
|
321
|
+
f"Geometry at index {idx} is not 3D. Z-coordinates are required since "
|
|
322
|
+
"they give the cell values during rasterization."
|
|
323
|
+
)
|
|
324
|
+
raise ValueError(msg)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _interpolate_z_in_geometry(
|
|
328
|
+
geometry: BaseGeometry, x: NDArray, y: NDArray
|
|
329
|
+
) -> NDArray[np.float64]:
|
|
330
|
+
"""Vectorized interpolation of Z values in a geometry at multiple (x, y) points.
|
|
331
|
+
|
|
332
|
+
Only the boundary is considered (e.g. holes in polygons are ignored).
|
|
333
|
+
|
|
334
|
+
Parameters:
|
|
335
|
+
geometry: Shapely geometry with Z coordinates (Polygon, LineString, etc.).
|
|
336
|
+
x: Array of X coordinates, shape (N,).
|
|
337
|
+
y: Array of Y coordinates, shape (N,).
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Array of interpolated Z values (NaN if outside convex hull or no boundary).
|
|
341
|
+
"""
|
|
342
|
+
# Extract coordinates from geometry boundary only
|
|
343
|
+
coords = np.array(geometry.boundary.coords)
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
return interpn_kernel(coords[:, :2], coords[:, 2], xi=np.column_stack((x, y)))
|
|
347
|
+
except InterpolationError:
|
|
348
|
+
return np.full_like(x, np.nan, dtype=np.float64)
|
|
349
|
+
|
|
350
|
+
|
|
218
351
|
def _validate_columns_exist(
|
|
219
352
|
gdf: gpd.GeoDataFrame, target_cols: Collection[str]
|
|
220
353
|
) -> None:
|
|
@@ -299,7 +432,8 @@ def raster_from_point_cloud(
|
|
|
299
432
|
Interpolation is only possible within the convex hull of the points. Outside of
|
|
300
433
|
this, cells will be NaN-valued.
|
|
301
434
|
|
|
302
|
-
|
|
435
|
+
Duplicate (x, y, z) triples are silently deduplicated. However, duplicate (x, y)
|
|
436
|
+
points with different z values will raise an error.
|
|
303
437
|
|
|
304
438
|
Args:
|
|
305
439
|
x: X coordinates of points.
|
|
@@ -313,7 +447,8 @@ def raster_from_point_cloud(
|
|
|
313
447
|
Raster containing the interpolated values.
|
|
314
448
|
|
|
315
449
|
Raises:
|
|
316
|
-
ValueError: If any (x, y) points
|
|
450
|
+
ValueError: If any (x, y) points have different z values, or if they are all
|
|
451
|
+
collinear.
|
|
317
452
|
"""
|
|
318
453
|
crs = CRS.from_user_input(crs)
|
|
319
454
|
x, y, z = _validate_xyz(
|
|
@@ -357,7 +492,21 @@ def _validate_xyz(
|
|
|
357
492
|
"surface."
|
|
358
493
|
)
|
|
359
494
|
raise ValueError(msg)
|
|
360
|
-
# Check for duplicate (x, y)
|
|
495
|
+
# Check for duplicate (x, y, z) triples and deduplicate them
|
|
496
|
+
xyz_points = np.column_stack((x, y, z))
|
|
497
|
+
unique_xyz, first_occurrence_indices = np.unique(
|
|
498
|
+
xyz_points, axis=0, return_index=True
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# If we have duplicate (x, y, z) triples, deduplicate them
|
|
502
|
+
if len(unique_xyz) < len(xyz_points):
|
|
503
|
+
x = x[first_occurrence_indices]
|
|
504
|
+
y = y[first_occurrence_indices]
|
|
505
|
+
z = z[first_occurrence_indices]
|
|
506
|
+
|
|
507
|
+
# Check for duplicate (x, y) points with different z values
|
|
508
|
+
# After deduplication, if there are still duplicate (x,y) points, they must have
|
|
509
|
+
# different z values
|
|
361
510
|
xy_points = np.column_stack((x, y))
|
|
362
511
|
if len(xy_points) != len(np.unique(xy_points, axis=0)):
|
|
363
512
|
msg = "Duplicate (x, y) points found. Each (x, y) point must be unique."
|
rastr/gis/fishnet.py
CHANGED
|
@@ -7,7 +7,7 @@ from shapely import BufferCapStyle, BufferJoinStyle
|
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from geopandas.array import GeometryArray
|
|
10
|
-
from numpy.typing import NDArray
|
|
10
|
+
from numpy.typing import ArrayLike, NDArray
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def create_point_grid(
|
|
@@ -36,11 +36,11 @@ def create_point_grid(
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def get_point_grid_shape(
|
|
39
|
-
*, bounds: tuple[float, float, float, float], cell_size: float
|
|
39
|
+
*, bounds: tuple[float, float, float, float] | ArrayLike, cell_size: float
|
|
40
40
|
) -> tuple[int, int]:
|
|
41
41
|
"""Calculate the shape of the point grid based on bounds and cell size."""
|
|
42
42
|
|
|
43
|
-
xmin, ymin, xmax, ymax = bounds
|
|
43
|
+
xmin, ymin, xmax, ymax = np.asarray(bounds)
|
|
44
44
|
ncols_exact = (xmax - xmin) / cell_size
|
|
45
45
|
nrows_exact = (ymax - ymin) / cell_size
|
|
46
46
|
|
rastr/gis/interpolate.py
CHANGED
|
@@ -8,6 +8,10 @@ if TYPE_CHECKING:
|
|
|
8
8
|
from collections.abc import Callable
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
class InterpolationError(ValueError):
|
|
12
|
+
"""Exception for interpolation-related errors."""
|
|
13
|
+
|
|
14
|
+
|
|
11
15
|
def interpn_kernel(
|
|
12
16
|
points: np.ndarray,
|
|
13
17
|
values: np.ndarray,
|
|
@@ -44,7 +48,7 @@ def interpn_kernel(
|
|
|
44
48
|
"degenerate input points. Ensure that the (x, y) points are not all "
|
|
45
49
|
"collinear (i.e. that the convex hull is non-degenerate)."
|
|
46
50
|
)
|
|
47
|
-
raise
|
|
51
|
+
raise InterpolationError(msg) from err
|
|
48
52
|
|
|
49
53
|
grid_values = np.array(interpolator(xi))
|
|
50
54
|
return grid_values
|
rastr/io.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import warnings
|
|
3
4
|
from pathlib import Path
|
|
4
|
-
from typing import TYPE_CHECKING, TypeVar
|
|
5
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
7
8
|
import rasterio
|
|
@@ -12,8 +13,14 @@ from rastr.meta import RasterMeta
|
|
|
12
13
|
from rastr.raster import Raster
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
16
|
+
import geopandas as gpd
|
|
15
17
|
from numpy.typing import NDArray
|
|
16
18
|
|
|
19
|
+
try:
|
|
20
|
+
from rasterio._err import CPLE_BaseError
|
|
21
|
+
except ImportError:
|
|
22
|
+
CPLE_BaseError = Exception # Fallback if private module import fails
|
|
23
|
+
|
|
17
24
|
R = TypeVar("R", bound=Raster)
|
|
18
25
|
|
|
19
26
|
|
|
@@ -118,3 +125,115 @@ def read_raster_mosaic_inmem(
|
|
|
118
125
|
finally:
|
|
119
126
|
for src in sources:
|
|
120
127
|
src.close()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def write_raster(raster: Raster, *, path: Path | str, **kwargs: Any) -> None:
|
|
131
|
+
"""Write the raster to a file.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
raster: The Raster object to write.
|
|
135
|
+
path: Path to output file.
|
|
136
|
+
**kwargs: Additional keyword arguments to pass to `rasterio.open()`. If
|
|
137
|
+
`nodata` is provided, NaN values in the raster will be replaced
|
|
138
|
+
with the nodata value.
|
|
139
|
+
"""
|
|
140
|
+
path = Path(path)
|
|
141
|
+
|
|
142
|
+
suffix = path.suffix.lower()
|
|
143
|
+
if suffix in (".tif", ".tiff"):
|
|
144
|
+
driver = "GTiff"
|
|
145
|
+
elif suffix in (".grd"):
|
|
146
|
+
# https://grapherhelp.goldensoftware.com/subsys/ascii_grid_file_format.htm
|
|
147
|
+
# e.g. Used by AnAqSim
|
|
148
|
+
driver = "GSAG"
|
|
149
|
+
else:
|
|
150
|
+
msg = f"Unsupported file extension: {suffix}"
|
|
151
|
+
raise ValueError(msg)
|
|
152
|
+
|
|
153
|
+
# Handle nodata: use provided value or default to np.nan
|
|
154
|
+
if "nodata" in kwargs:
|
|
155
|
+
# Replace NaN values with the nodata value
|
|
156
|
+
nodata_value = kwargs.pop("nodata")
|
|
157
|
+
arr_to_write = np.where(np.isnan(raster.arr), nodata_value, raster.arr)
|
|
158
|
+
else:
|
|
159
|
+
nodata_value = np.nan
|
|
160
|
+
arr_to_write = raster.arr
|
|
161
|
+
|
|
162
|
+
with rasterio.open(
|
|
163
|
+
path,
|
|
164
|
+
"w",
|
|
165
|
+
driver=driver,
|
|
166
|
+
height=raster.arr.shape[0],
|
|
167
|
+
width=raster.arr.shape[1],
|
|
168
|
+
count=1,
|
|
169
|
+
dtype=raster.arr.dtype,
|
|
170
|
+
crs=raster.raster_meta.crs,
|
|
171
|
+
transform=raster.raster_meta.transform,
|
|
172
|
+
nodata=nodata_value,
|
|
173
|
+
**kwargs,
|
|
174
|
+
) as dst:
|
|
175
|
+
try:
|
|
176
|
+
dst.write(arr_to_write, 1)
|
|
177
|
+
except CPLE_BaseError as err:
|
|
178
|
+
msg = f"Failed to write raster to file: {err}"
|
|
179
|
+
raise OSError(msg) from err
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def read_cad_gdf(path: Path | str, crs: CRS | str | None = None) -> gpd.GeoDataFrame:
|
|
183
|
+
"""Read a GeoDataFrame from a DXF/CAD file with warning suppression.
|
|
184
|
+
|
|
185
|
+
This is useful in tandem with `Raster.clip` and `rastr.create.rasterize_z_gdf` to
|
|
186
|
+
create rasters from CAD files. Often CAD files represent surfaces which for GIS
|
|
187
|
+
contexts are better represented as rasters. In other cases, CAD files represent
|
|
188
|
+
geometries but their difficult representation means that it is often easier to first
|
|
189
|
+
rasterize them, and then spatially join with a clean vector representation
|
|
190
|
+
developed separately.
|
|
191
|
+
|
|
192
|
+
DXF files often have large geometries that can trigger warnings during reading.
|
|
193
|
+
This function suppresses those warnings and ensures proper CRS handling.
|
|
194
|
+
|
|
195
|
+
Supports any format supported by `geopandas.read_file` which provides 3D geometries.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
path: Path to the CAD file.
|
|
199
|
+
crs: Optional CRS for the output GeoDataFrame. If None, uses the CRS from the
|
|
200
|
+
CAD file.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
GeoDataFrame: The CAD data as a GeoDataFrame.
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
ValueError: If CRS is missing from CAD file and not provided, or if CRS is
|
|
207
|
+
inconsistent between CAD file and provided CRS.
|
|
208
|
+
"""
|
|
209
|
+
import geopandas as gpd
|
|
210
|
+
|
|
211
|
+
path = Path(path)
|
|
212
|
+
|
|
213
|
+
with warnings.catch_warnings():
|
|
214
|
+
# The CAD-derived geometries are really messy, so we can get warnings
|
|
215
|
+
# about large polygons, but we're going to handle those.
|
|
216
|
+
warnings.filterwarnings(
|
|
217
|
+
"ignore",
|
|
218
|
+
message=".*Non closed ring detected.*",
|
|
219
|
+
category=RuntimeWarning,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Read the CAD file using geopandas
|
|
223
|
+
gdf = gpd.read_file(path)
|
|
224
|
+
|
|
225
|
+
# Handle CRS logic
|
|
226
|
+
crs = CRS.from_user_input(crs) if crs is not None else None
|
|
227
|
+
if crs is None:
|
|
228
|
+
if gdf.crs is None:
|
|
229
|
+
msg = (
|
|
230
|
+
f"No CRS found in CAD file {path} and no CRS provided. "
|
|
231
|
+
"Please provide a CRS parameter."
|
|
232
|
+
)
|
|
233
|
+
raise ValueError(msg)
|
|
234
|
+
# Check if CRS are consistent
|
|
235
|
+
elif gdf.crs is not None and not gdf.crs.equals(crs):
|
|
236
|
+
# Reproject if inconsistent
|
|
237
|
+
gdf = gdf.to_crs(crs)
|
|
238
|
+
|
|
239
|
+
return gdf
|
rastr/meta.py
CHANGED
|
@@ -97,7 +97,7 @@ class RasterMeta(BaseModel, extra="forbid"):
|
|
|
97
97
|
cell_size: float | None = None,
|
|
98
98
|
crs: CRS,
|
|
99
99
|
) -> tuple[Self, tuple[int, int]]:
|
|
100
|
-
"""Automatically get recommended raster metadata (and shape) using data
|
|
100
|
+
"""Automatically get recommended raster metadata (and shape) using data points.
|
|
101
101
|
|
|
102
102
|
The cell size can be provided, or a heuristic will be used based on the spacing
|
|
103
103
|
of the (x, y) points.
|
rastr/raster.py
CHANGED
|
@@ -6,8 +6,7 @@ import importlib.util
|
|
|
6
6
|
import warnings
|
|
7
7
|
from collections.abc import Collection
|
|
8
8
|
from contextlib import contextmanager
|
|
9
|
-
from
|
|
10
|
-
from typing import TYPE_CHECKING, Any, Literal, overload
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Literal, NamedTuple, overload
|
|
11
10
|
|
|
12
11
|
import numpy as np
|
|
13
12
|
import numpy.ma
|
|
@@ -15,13 +14,12 @@ import rasterio.features
|
|
|
15
14
|
import rasterio.plot
|
|
16
15
|
import rasterio.sample
|
|
17
16
|
import rasterio.transform
|
|
18
|
-
import skimage.measure
|
|
19
17
|
from pydantic import BaseModel, InstanceOf, field_validator
|
|
20
18
|
from pyproj import Transformer
|
|
21
19
|
from pyproj.crs.crs import CRS
|
|
22
20
|
from rasterio.enums import Resampling
|
|
23
21
|
from rasterio.io import MemoryFile
|
|
24
|
-
from shapely.geometry import LineString, Point, Polygon
|
|
22
|
+
from shapely.geometry import LineString, MultiPolygon, Point, Polygon
|
|
25
23
|
|
|
26
24
|
from rastr.arr.fill import fillna_nearest_neighbours
|
|
27
25
|
from rastr.gis.fishnet import create_fishnet
|
|
@@ -30,6 +28,7 @@ from rastr.meta import RasterMeta
|
|
|
30
28
|
|
|
31
29
|
if TYPE_CHECKING:
|
|
32
30
|
from collections.abc import Callable, Generator
|
|
31
|
+
from pathlib import Path
|
|
33
32
|
|
|
34
33
|
import geopandas as gpd
|
|
35
34
|
from affine import Affine
|
|
@@ -39,14 +38,9 @@ if TYPE_CHECKING:
|
|
|
39
38
|
from matplotlib.image import AxesImage
|
|
40
39
|
from numpy.typing import ArrayLike, NDArray
|
|
41
40
|
from rasterio.io import BufferedDatasetWriter, DatasetReader, DatasetWriter
|
|
42
|
-
from shapely import
|
|
41
|
+
from shapely.geometry.base import BaseGeometry
|
|
43
42
|
from typing_extensions import Self
|
|
44
43
|
|
|
45
|
-
try:
|
|
46
|
-
from rasterio._err import CPLE_BaseError
|
|
47
|
-
except ImportError:
|
|
48
|
-
CPLE_BaseError = Exception # Fallback if private module import fails
|
|
49
|
-
|
|
50
44
|
|
|
51
45
|
FOLIUM_INSTALLED = importlib.util.find_spec("folium") is not None
|
|
52
46
|
BRANCA_INSTALLED = importlib.util.find_spec("branca") is not None
|
|
@@ -55,6 +49,27 @@ MATPLOTLIB_INSTALLED = importlib.util.find_spec("matplotlib") is not None
|
|
|
55
49
|
CONTOUR_PERTURB_EPS = 1e-10
|
|
56
50
|
|
|
57
51
|
|
|
52
|
+
@contextmanager
|
|
53
|
+
def suppress_slice_warning() -> Generator[None, None, None]:
|
|
54
|
+
"""Context manager to suppress all-NaN slice warnings from NumPy operations.
|
|
55
|
+
|
|
56
|
+
An all-NaN slice is a row or a column of an array where every value is NaN.
|
|
57
|
+
This is common for rasters around the edges, and is almost always a false
|
|
58
|
+
positive.
|
|
59
|
+
|
|
60
|
+
Note that even .trim_nan() won't even guarantee there aren't NaN slices,
|
|
61
|
+
since that method only applies around the edges of a raster, whereas there
|
|
62
|
+
might be NaN slices in between non-NaN data in the middle.
|
|
63
|
+
"""
|
|
64
|
+
with warnings.catch_warnings():
|
|
65
|
+
warnings.filterwarnings(
|
|
66
|
+
"ignore",
|
|
67
|
+
message="All-NaN slice encountered",
|
|
68
|
+
category=RuntimeWarning,
|
|
69
|
+
)
|
|
70
|
+
yield
|
|
71
|
+
|
|
72
|
+
|
|
58
73
|
class RasterCellArrayShapeError(ValueError):
|
|
59
74
|
"""Custom error for invalid raster cell array shapes."""
|
|
60
75
|
|
|
@@ -249,6 +264,18 @@ class Raster(BaseModel):
|
|
|
249
264
|
cls = self.__class__
|
|
250
265
|
return cls(arr=-self.arr, raster_meta=self.raster_meta)
|
|
251
266
|
|
|
267
|
+
def abs(self) -> Self:
|
|
268
|
+
"""Compute the absolute value of the raster.
|
|
269
|
+
|
|
270
|
+
Returns a new raster with the absolute value of each cell. The original raster
|
|
271
|
+
is not modified.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
A new Raster instance with the absolute values.
|
|
275
|
+
"""
|
|
276
|
+
cls = self.__class__
|
|
277
|
+
return cls(arr=np.abs(self.arr), raster_meta=self.raster_meta)
|
|
278
|
+
|
|
252
279
|
@property
|
|
253
280
|
def cell_centre_coords(self) -> NDArray[np.float64]:
|
|
254
281
|
"""Get the coordinates of the cell centres in the raster."""
|
|
@@ -417,8 +444,8 @@ class Raster(BaseModel):
|
|
|
417
444
|
return raster_values
|
|
418
445
|
|
|
419
446
|
@property
|
|
420
|
-
def bounds(self) ->
|
|
421
|
-
"""Bounding box of the raster as
|
|
447
|
+
def bounds(self) -> Bounds:
|
|
448
|
+
"""Bounding box of the raster as a named tuple with xmin, ymin, xmax, ymax."""
|
|
422
449
|
x1, y1, x2, y2 = rasterio.transform.array_bounds(
|
|
423
450
|
height=self.arr.shape[0],
|
|
424
451
|
width=self.arr.shape[1],
|
|
@@ -426,7 +453,7 @@ class Raster(BaseModel):
|
|
|
426
453
|
)
|
|
427
454
|
xmin, xmax = sorted([x1, x2])
|
|
428
455
|
ymin, ymax = sorted([y1, y2])
|
|
429
|
-
return (xmin, ymin, xmax, ymax)
|
|
456
|
+
return Bounds(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax)
|
|
430
457
|
|
|
431
458
|
@property
|
|
432
459
|
def bbox(self) -> Polygon:
|
|
@@ -580,7 +607,8 @@ class Raster(BaseModel):
|
|
|
580
607
|
suppressed: Values to suppress from the plot (i.e. not display). This can be
|
|
581
608
|
useful for zeroes especially.
|
|
582
609
|
**kwargs: Additional keyword arguments to pass to `rasterio.plot.show()`.
|
|
583
|
-
This includes parameters like `alpha` for transparency
|
|
610
|
+
This includes parameters like `alpha` for transparency, and `vmin`
|
|
611
|
+
and `vmax` for controlling the color scale limits.
|
|
584
612
|
"""
|
|
585
613
|
if not MATPLOTLIB_INSTALLED:
|
|
586
614
|
msg = "The 'matplotlib' package is required for 'plot()'."
|
|
@@ -673,8 +701,15 @@ class Raster(BaseModel):
|
|
|
673
701
|
|
|
674
702
|
return raster_gdf
|
|
675
703
|
|
|
704
|
+
def gdf(self, name: str = "value") -> gpd.GeoDataFrame:
|
|
705
|
+
"""Create a GeoDataFrame representation of the raster.
|
|
706
|
+
|
|
707
|
+
Alias for `as_geodataframe()`.
|
|
708
|
+
"""
|
|
709
|
+
return self.as_geodataframe(name=name)
|
|
710
|
+
|
|
676
711
|
def to_file(self, path: Path | str, **kwargs: Any) -> None:
|
|
677
|
-
"""Write the raster to a
|
|
712
|
+
"""Write the raster to a file.
|
|
678
713
|
|
|
679
714
|
Args:
|
|
680
715
|
path: Path to output file.
|
|
@@ -682,47 +717,9 @@ class Raster(BaseModel):
|
|
|
682
717
|
`nodata` is provided, NaN values in the raster will be replaced
|
|
683
718
|
with the nodata value.
|
|
684
719
|
"""
|
|
720
|
+
from rastr.io import write_raster # noqa: PLC0415
|
|
685
721
|
|
|
686
|
-
path
|
|
687
|
-
|
|
688
|
-
suffix = path.suffix.lower()
|
|
689
|
-
if suffix in (".tif", ".tiff"):
|
|
690
|
-
driver = "GTiff"
|
|
691
|
-
elif suffix in (".grd"):
|
|
692
|
-
# https://grapherhelp.goldensoftware.com/subsys/ascii_grid_file_format.htm
|
|
693
|
-
# e.g. Used by AnAqSim
|
|
694
|
-
driver = "GSAG"
|
|
695
|
-
else:
|
|
696
|
-
msg = f"Unsupported file extension: {suffix}"
|
|
697
|
-
raise ValueError(msg)
|
|
698
|
-
|
|
699
|
-
# Handle nodata: use provided value or default to np.nan
|
|
700
|
-
if "nodata" in kwargs:
|
|
701
|
-
# Replace NaN values with the nodata value
|
|
702
|
-
nodata_value = kwargs.pop("nodata")
|
|
703
|
-
arr_to_write = np.where(np.isnan(self.arr), nodata_value, self.arr)
|
|
704
|
-
else:
|
|
705
|
-
nodata_value = np.nan
|
|
706
|
-
arr_to_write = self.arr
|
|
707
|
-
|
|
708
|
-
with rasterio.open(
|
|
709
|
-
path,
|
|
710
|
-
"w",
|
|
711
|
-
driver=driver,
|
|
712
|
-
height=self.arr.shape[0],
|
|
713
|
-
width=self.arr.shape[1],
|
|
714
|
-
count=1,
|
|
715
|
-
dtype=self.arr.dtype,
|
|
716
|
-
crs=self.raster_meta.crs,
|
|
717
|
-
transform=self.raster_meta.transform,
|
|
718
|
-
nodata=nodata_value,
|
|
719
|
-
**kwargs,
|
|
720
|
-
) as dst:
|
|
721
|
-
try:
|
|
722
|
-
dst.write(arr_to_write, 1)
|
|
723
|
-
except CPLE_BaseError as err:
|
|
724
|
-
msg = f"Failed to write raster to file: {err}"
|
|
725
|
-
raise OSError(msg) from err
|
|
722
|
+
return write_raster(self, path=path, **kwargs)
|
|
726
723
|
|
|
727
724
|
def __str__(self) -> str:
|
|
728
725
|
cls = self.__class__
|
|
@@ -817,7 +814,8 @@ class Raster(BaseModel):
|
|
|
817
814
|
Returns:
|
|
818
815
|
The maximum value in the raster. Returns NaN if all values are NaN.
|
|
819
816
|
"""
|
|
820
|
-
|
|
817
|
+
with suppress_slice_warning():
|
|
818
|
+
return float(np.nanmax(self.arr))
|
|
821
819
|
|
|
822
820
|
def min(self) -> float:
|
|
823
821
|
"""Get the minimum value in the raster, ignoring NaN values.
|
|
@@ -825,7 +823,8 @@ class Raster(BaseModel):
|
|
|
825
823
|
Returns:
|
|
826
824
|
The minimum value in the raster. Returns NaN if all values are NaN.
|
|
827
825
|
"""
|
|
828
|
-
|
|
826
|
+
with suppress_slice_warning():
|
|
827
|
+
return float(np.nanmin(self.arr))
|
|
829
828
|
|
|
830
829
|
def mean(self) -> float:
|
|
831
830
|
"""Get the mean value in the raster, ignoring NaN values.
|
|
@@ -833,7 +832,8 @@ class Raster(BaseModel):
|
|
|
833
832
|
Returns:
|
|
834
833
|
The mean value in the raster. Returns NaN if all values are NaN.
|
|
835
834
|
"""
|
|
836
|
-
|
|
835
|
+
with suppress_slice_warning():
|
|
836
|
+
return float(np.nanmean(self.arr))
|
|
837
837
|
|
|
838
838
|
def std(self) -> float:
|
|
839
839
|
"""Get the standard deviation of values in the raster, ignoring NaN values.
|
|
@@ -841,7 +841,8 @@ class Raster(BaseModel):
|
|
|
841
841
|
Returns:
|
|
842
842
|
The standard deviation of the raster. Returns NaN if all values are NaN.
|
|
843
843
|
"""
|
|
844
|
-
|
|
844
|
+
with suppress_slice_warning():
|
|
845
|
+
return float(np.nanstd(self.arr))
|
|
845
846
|
|
|
846
847
|
def quantile(self, q: float) -> float:
|
|
847
848
|
"""Get the specified quantile value in the raster, ignoring NaN values.
|
|
@@ -852,7 +853,8 @@ class Raster(BaseModel):
|
|
|
852
853
|
Returns:
|
|
853
854
|
The quantile value. Returns NaN if all values are NaN.
|
|
854
855
|
"""
|
|
855
|
-
|
|
856
|
+
with suppress_slice_warning():
|
|
857
|
+
return float(np.nanquantile(self.arr, q))
|
|
856
858
|
|
|
857
859
|
def median(self) -> float:
|
|
858
860
|
"""Get the median value in the raster, ignoring NaN values.
|
|
@@ -862,7 +864,8 @@ class Raster(BaseModel):
|
|
|
862
864
|
Returns:
|
|
863
865
|
The median value in the raster. Returns NaN if all values are NaN.
|
|
864
866
|
"""
|
|
865
|
-
|
|
867
|
+
with suppress_slice_warning():
|
|
868
|
+
return float(np.nanmedian(self.arr))
|
|
866
869
|
|
|
867
870
|
def fillna(self, value: float) -> Self:
|
|
868
871
|
"""Fill NaN values in the raster with a specified value.
|
|
@@ -983,6 +986,7 @@ class Raster(BaseModel):
|
|
|
983
986
|
contours will be returned without any smoothing.
|
|
984
987
|
"""
|
|
985
988
|
import geopandas as gpd
|
|
989
|
+
import skimage.measure
|
|
986
990
|
|
|
987
991
|
all_levels = []
|
|
988
992
|
all_geoms = []
|
|
@@ -1032,6 +1036,27 @@ class Raster(BaseModel):
|
|
|
1032
1036
|
# Dissolve contours by level to merge all contour lines of the same level
|
|
1033
1037
|
return contour_gdf.dissolve(by="level", as_index=False)
|
|
1034
1038
|
|
|
1039
|
+
def sobel(self) -> Self:
|
|
1040
|
+
"""Compute the Sobel gradient magnitude of the raster.
|
|
1041
|
+
|
|
1042
|
+
This is effectively a discrete differentiation operator, computing an
|
|
1043
|
+
approximation of the magnitude of the gradient of the image intensity function.
|
|
1044
|
+
|
|
1045
|
+
Borders are treated using half-sample symmetric sampling, i.e. repeating the
|
|
1046
|
+
border values. Be aware that this can lead to edge artifacts and under-estimate
|
|
1047
|
+
the gradient along the border pixels.
|
|
1048
|
+
|
|
1049
|
+
Returns:
|
|
1050
|
+
New raster containing the gradient magnitude in units of raster cell units
|
|
1051
|
+
per unit distance (e.g. per meter).
|
|
1052
|
+
"""
|
|
1053
|
+
from skimage import filters
|
|
1054
|
+
|
|
1055
|
+
new_raster = self.model_copy()
|
|
1056
|
+
# Scale by cell size to convert to per unit distance
|
|
1057
|
+
new_raster.arr = filters.sobel(self.arr) / self.cell_size
|
|
1058
|
+
return new_raster
|
|
1059
|
+
|
|
1035
1060
|
def blur(self, sigma: float, *, preserve_nan: bool = True) -> Self:
|
|
1036
1061
|
"""Apply a Gaussian blur to the raster data.
|
|
1037
1062
|
|
|
@@ -1286,7 +1311,7 @@ class Raster(BaseModel):
|
|
|
1286
1311
|
|
|
1287
1312
|
def clip(
|
|
1288
1313
|
self,
|
|
1289
|
-
polygon:
|
|
1314
|
+
polygon: BaseGeometry,
|
|
1290
1315
|
*,
|
|
1291
1316
|
strategy: Literal["centres"] = "centres",
|
|
1292
1317
|
) -> Self:
|
|
@@ -1304,22 +1329,25 @@ class Raster(BaseModel):
|
|
|
1304
1329
|
|
|
1305
1330
|
Returns:
|
|
1306
1331
|
A new Raster with cells outside the polygon set to NaN.
|
|
1332
|
+
|
|
1333
|
+
Raises:
|
|
1334
|
+
TypeError: If the provided geometry is not a Polygon or MultiPolygon.
|
|
1307
1335
|
"""
|
|
1336
|
+
if not isinstance(polygon, Polygon | MultiPolygon):
|
|
1337
|
+
msg = (
|
|
1338
|
+
f"Only Polygon and MultiPolygon geometries are supported for clipping, "
|
|
1339
|
+
f"got {type(polygon).__name__}"
|
|
1340
|
+
)
|
|
1341
|
+
raise TypeError(msg)
|
|
1342
|
+
|
|
1308
1343
|
if strategy != "centres":
|
|
1309
1344
|
msg = f"Unsupported clipping strategy: {strategy}"
|
|
1310
1345
|
raise NotImplementedError(msg)
|
|
1311
1346
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
mask = rasterio.features.rasterize(
|
|
1315
|
-
[(polygon, 1)],
|
|
1316
|
-
fill=0,
|
|
1317
|
-
out_shape=self.shape,
|
|
1318
|
-
transform=self.meta.transform,
|
|
1319
|
-
dtype=np.uint8,
|
|
1320
|
-
)
|
|
1347
|
+
mask_raster = self._polygon_indicator(polygon)
|
|
1321
1348
|
|
|
1322
|
-
raster
|
|
1349
|
+
raster = self.model_copy()
|
|
1350
|
+
raster.arr = np.where(mask_raster.arr, raster.arr, np.nan)
|
|
1323
1351
|
|
|
1324
1352
|
return raster
|
|
1325
1353
|
|
|
@@ -1433,6 +1461,99 @@ class Raster(BaseModel):
|
|
|
1433
1461
|
|
|
1434
1462
|
return cls(arr=new_arr, raster_meta=new_raster_meta)
|
|
1435
1463
|
|
|
1464
|
+
def replace_polygon(
|
|
1465
|
+
self,
|
|
1466
|
+
polygon: BaseGeometry | dict[BaseGeometry, float],
|
|
1467
|
+
value: float | None = None,
|
|
1468
|
+
) -> Self:
|
|
1469
|
+
"""Replace values within the specified polygon(s) with other values.
|
|
1470
|
+
|
|
1471
|
+
Creates a new raster with the specified values replaced. This is useful for
|
|
1472
|
+
operations like masking or setting regions to NaN.
|
|
1473
|
+
|
|
1474
|
+
The method supports two interfaces:
|
|
1475
|
+
1. Single replacement: `raster.replace_polygon(polygon1, value=np.nan)`
|
|
1476
|
+
2. Multiple replacements using a dictionary:
|
|
1477
|
+
`raster.replace_polygon({polygon1: 0, polygon2: 1})`
|
|
1478
|
+
|
|
1479
|
+
Args:
|
|
1480
|
+
polygon: Geometry to replace, or dict mapping geometries to values.
|
|
1481
|
+
value: Replacement value. Required if polygon is a geometry, None if polygon
|
|
1482
|
+
is a dict.
|
|
1483
|
+
|
|
1484
|
+
Examples:
|
|
1485
|
+
>>> # Replace a single polygon
|
|
1486
|
+
>>> raster.replace_polygon(polygon1, value=np.nan)
|
|
1487
|
+
>>> # Replace multiple polygons
|
|
1488
|
+
>>> raster.replace_polygon({polygon1: 0, polygon2: 1})
|
|
1489
|
+
"""
|
|
1490
|
+
# Normalize input to dict format
|
|
1491
|
+
if isinstance(polygon, dict):
|
|
1492
|
+
if value is not None:
|
|
1493
|
+
msg = "value must be None when polygon is a dict"
|
|
1494
|
+
raise ValueError(msg)
|
|
1495
|
+
replacements = polygon
|
|
1496
|
+
else:
|
|
1497
|
+
if value is None:
|
|
1498
|
+
msg = "value must be specified when polygon is a geometry"
|
|
1499
|
+
raise ValueError(msg)
|
|
1500
|
+
replacements = {polygon: value}
|
|
1501
|
+
|
|
1502
|
+
# Validate all geometries upfront
|
|
1503
|
+
for geom in replacements.keys():
|
|
1504
|
+
if not isinstance(geom, Polygon | MultiPolygon):
|
|
1505
|
+
msg = (
|
|
1506
|
+
f"Only Polygon and MultiPolygon geometries are supported, "
|
|
1507
|
+
f"got {type(geom).__name__}"
|
|
1508
|
+
)
|
|
1509
|
+
raise TypeError(msg)
|
|
1510
|
+
|
|
1511
|
+
# Create copy and convert to float if needed
|
|
1512
|
+
raster = self.model_copy()
|
|
1513
|
+
needs_float = any(
|
|
1514
|
+
isinstance(v, float) or (v is not None and np.isnan(v))
|
|
1515
|
+
for v in replacements.values()
|
|
1516
|
+
)
|
|
1517
|
+
if needs_float and not np.issubdtype(raster.arr.dtype, np.floating):
|
|
1518
|
+
raster.arr = raster.arr.astype(float)
|
|
1519
|
+
|
|
1520
|
+
# Apply all replacements
|
|
1521
|
+
for geom, val in replacements.items():
|
|
1522
|
+
mask_raster = self._polygon_indicator(geom)
|
|
1523
|
+
raster.arr = np.where(mask_raster.arr, val, raster.arr)
|
|
1524
|
+
|
|
1525
|
+
return raster
|
|
1526
|
+
|
|
1527
|
+
def _polygon_indicator(self, geom: BaseGeometry) -> Self:
|
|
1528
|
+
"""Create a binary mask with 1 inside the polygon, 0 outside.
|
|
1529
|
+
|
|
1530
|
+
The mask has the same shape and transform as the raster.
|
|
1531
|
+
|
|
1532
|
+
Args:
|
|
1533
|
+
geom:
|
|
1534
|
+
A shapely geometry (typically Polygon or MultiPolygon)
|
|
1535
|
+
to rasterize.
|
|
1536
|
+
|
|
1537
|
+
Returns:
|
|
1538
|
+
Raster:
|
|
1539
|
+
A new Raster where cells inside the geometry are 1,
|
|
1540
|
+
and outside are 0.
|
|
1541
|
+
"""
|
|
1542
|
+
|
|
1543
|
+
arr = rasterio.features.rasterize(
|
|
1544
|
+
[(geom, 1)],
|
|
1545
|
+
fill=0,
|
|
1546
|
+
out_shape=self.shape,
|
|
1547
|
+
transform=self.meta.transform,
|
|
1548
|
+
dtype=np.uint8,
|
|
1549
|
+
)
|
|
1550
|
+
|
|
1551
|
+
# Create a new raster with the binary mask array
|
|
1552
|
+
raster = self.model_copy()
|
|
1553
|
+
raster.arr = arr
|
|
1554
|
+
|
|
1555
|
+
return raster
|
|
1556
|
+
|
|
1436
1557
|
|
|
1437
1558
|
def _map_colorbar(
|
|
1438
1559
|
*,
|
|
@@ -1466,22 +1587,32 @@ def _get_vmin_vmax(
|
|
|
1466
1587
|
|
|
1467
1588
|
Allows for custom over-ride vmin and vmax values to be provided.
|
|
1468
1589
|
"""
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
)
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
else:
|
|
1478
|
-
_vmin = vmin
|
|
1479
|
-
if vmax is None:
|
|
1480
|
-
_vmax = float(raster.max())
|
|
1481
|
-
else:
|
|
1482
|
-
_vmax = vmax
|
|
1590
|
+
if vmin is None:
|
|
1591
|
+
_vmin = float(raster.min())
|
|
1592
|
+
else:
|
|
1593
|
+
_vmin = vmin
|
|
1594
|
+
if vmax is None:
|
|
1595
|
+
_vmax = float(raster.max())
|
|
1596
|
+
else:
|
|
1597
|
+
_vmax = vmax
|
|
1483
1598
|
|
|
1484
1599
|
return _vmin, _vmax
|
|
1485
1600
|
|
|
1486
1601
|
|
|
1487
1602
|
RasterModel = Raster
|
|
1603
|
+
|
|
1604
|
+
|
|
1605
|
+
class Bounds(NamedTuple):
|
|
1606
|
+
"""Bounding box coordinates for a raster.
|
|
1607
|
+
|
|
1608
|
+
Attributes:
|
|
1609
|
+
xmin: The minimum x-coordinate.
|
|
1610
|
+
ymin: The minimum y-coordinate.
|
|
1611
|
+
xmax: The maximum x-coordinate.
|
|
1612
|
+
ymax: The maximum y-coordinate.
|
|
1613
|
+
"""
|
|
1614
|
+
|
|
1615
|
+
xmin: float
|
|
1616
|
+
ymin: float
|
|
1617
|
+
xmax: float
|
|
1618
|
+
ymax: float
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rastr
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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/
|
|
8
|
+
Project-URL: Source Archive, https://github.com/tonkintaylor/rastr/archive/68715eeb3fa4dca80a0999df65ebf2f9c7daca6d.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
|
|
@@ -39,9 +39,8 @@ Description-Content-Type: text/markdown
|
|
|
39
39
|
# rastr
|
|
40
40
|
|
|
41
41
|
[](<https://pypi.python.org/pypi/rastr>)
|
|
42
|
-
[](https://github.com/usethis-python/usethis-python)
|
|
42
|
+
[](https://pypi.python.org/pypi/rastr)
|
|
43
|
+

|
|
45
44
|
|
|
46
45
|
A lightweight geospatial raster datatype library for Python focused on simplicity.
|
|
47
46
|
|
|
@@ -73,9 +72,8 @@ pip install rastr
|
|
|
73
72
|
```python
|
|
74
73
|
from pyproj.crs.crs import CRS
|
|
75
74
|
from rasterio.transform import from_origin
|
|
75
|
+
from rastr import Raster, RasterMeta
|
|
76
76
|
from rastr.create import full_raster
|
|
77
|
-
from rastr.meta import RasterMeta
|
|
78
|
-
from rastr.raster import Raster
|
|
79
77
|
|
|
80
78
|
# Read a raster from a file
|
|
81
79
|
raster = Raster.read_file("path/to/raster.tif")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
rastr/__init__.py,sha256=z26KywZdRKwO-N5Qc34SuuGGwH8Y812csKORc3S4SYU,113
|
|
2
|
+
rastr/_version.py,sha256=Rttl-BDadtcW1QzGnNffCWA_Wc9mUKDMOBPZp--Mnsc,704
|
|
3
|
+
rastr/create.py,sha256=a2T-kv6Zi0Pnx65kCq9oHBPOeIhS1mtioqTm_XoRRU0,18312
|
|
4
|
+
rastr/io.py,sha256=Tui_1AzmGEkvyDrNEHVK6S-I5OEjh36lZqNRwA9effQ,7722
|
|
5
|
+
rastr/meta.py,sha256=ex4GVIwkJza8qZGJiEvMMstx4HOum0kHPCnl9PzrFkw,5980
|
|
6
|
+
rastr/raster.py,sha256=zTwyUPGZAgEBjawg0i6dTR6knrFYrzvH7sUaF_DuMuU,59078
|
|
7
|
+
rastr/arr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
rastr/arr/fill.py,sha256=ZSd9mcfzYafkAes2G2q8hJGlxhW47kI2brPf--jds3o,1029
|
|
9
|
+
rastr/gis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
rastr/gis/crs.py,sha256=9K57Ys6P32v0uzap-l7L_HbjolJMX-ETuRB_rN30Qz0,1953
|
|
11
|
+
rastr/gis/fishnet.py,sha256=J6hQEBFSWaSTg6dC9f6ppDxTk-cV2UVmX-O35T9Yj7w,3143
|
|
12
|
+
rastr/gis/interpolate.py,sha256=osRobeMx1QntggbNv-K3HDrWLUS7WO_XPp8nrMiPqVE,1799
|
|
13
|
+
rastr/gis/smooth.py,sha256=bGNBFs7PYW_J_vzcX0h2VL2R0o4dX3Da_nEm7Pjivlw,5295
|
|
14
|
+
rastr-0.8.0.dist-info/METADATA,sha256=EiigwvI6q_QFgJHMWmJ1iMHXE9Zm25SH9GTCbbyjsz8,5369
|
|
15
|
+
rastr-0.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
+
rastr-0.8.0.dist-info/licenses/LICENSE,sha256=7qUsx93G2ATTRLZiSYuQofwAX_uWvrqnAiMK8PzxvNc,1080
|
|
17
|
+
rastr-0.8.0.dist-info/RECORD,,
|
rastr-0.7.1.dist-info/RECORD
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
rastr/__init__.py,sha256=z26KywZdRKwO-N5Qc34SuuGGwH8Y812csKORc3S4SYU,113
|
|
2
|
-
rastr/_version.py,sha256=az9tfX88VBVbBBILFx2zOlpMh1RBTXIwP6baQTMKtHc,704
|
|
3
|
-
rastr/create.py,sha256=jT2X7mgJoMapnRz-M11dJoKFidaf0k_qleR5zxnRAnw,13195
|
|
4
|
-
rastr/io.py,sha256=RPhypnSNhLaWYdGRzctM9aTXbw9_TuMjvhMvDyUZavk,3640
|
|
5
|
-
rastr/meta.py,sha256=lUZVodFzhnzLI1sr7SgiM9XN9D-n7nXvs0voWTJYlMg,5980
|
|
6
|
-
rastr/raster.py,sha256=9yib1g0HOzPepaLCu_ApEbfUynb0IjQzCD__FEOco1c,54219
|
|
7
|
-
rastr/arr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
rastr/arr/fill.py,sha256=ZSd9mcfzYafkAes2G2q8hJGlxhW47kI2brPf--jds3o,1029
|
|
9
|
-
rastr/gis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
rastr/gis/crs.py,sha256=9K57Ys6P32v0uzap-l7L_HbjolJMX-ETuRB_rN30Qz0,1953
|
|
11
|
-
rastr/gis/fishnet.py,sha256=nAiJ_DuSQP326pLM9JmI8A4QwWWgVu7Mae1K1dWjDc4,3108
|
|
12
|
-
rastr/gis/interpolate.py,sha256=DzjtD5ynnwKP7TrwPiK3P0dOy5ZRzME9bV8-7tn5TFk,1697
|
|
13
|
-
rastr/gis/smooth.py,sha256=bGNBFs7PYW_J_vzcX0h2VL2R0o4dX3Da_nEm7Pjivlw,5295
|
|
14
|
-
rastr-0.7.1.dist-info/METADATA,sha256=-MvdAkql2szGVK4jAI2Kw3evRhIYq3daQBmvPAfpJ0o,5724
|
|
15
|
-
rastr-0.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
-
rastr-0.7.1.dist-info/licenses/LICENSE,sha256=7qUsx93G2ATTRLZiSYuQofwAX_uWvrqnAiMK8PzxvNc,1080
|
|
17
|
-
rastr-0.7.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|