rastr 0.3.0__tar.gz → 0.4.0__tar.gz
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-0.3.0 → rastr-0.4.0}/PKG-INFO +2 -2
- {rastr-0.3.0 → rastr-0.4.0}/src/rastr/_version.py +2 -2
- {rastr-0.3.0 → rastr-0.4.0}/src/rastr/raster.py +65 -10
- {rastr-0.3.0 → rastr-0.4.0}/tests/rastr/test_raster.py +145 -5
- {rastr-0.3.0 → rastr-0.4.0}/.copier-answers.yml +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/.github/ISSUE_TEMPLATE/enhancement.md +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/.github/copilot-instructions.md +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/.github/workflows/ci.yml +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/.github/workflows/release.yml +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/.gitignore +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/.pre-commit-config.yaml +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/.python-version +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/CONTRIBUTING.md +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/LICENSE +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/README.md +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/docs/index.md +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/docs/logo.svg +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/mkdocs.yml +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/pyproject.toml +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/pyrightconfig.json +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/requirements.txt +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/src/archive/.gitkeep +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/src/notebooks/.gitkeep +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/src/rastr/__init__.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/src/rastr/arr/__init__.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/src/rastr/arr/fill.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/src/rastr/create.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/src/rastr/gis/__init__.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/src/rastr/gis/fishnet.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/src/rastr/gis/smooth.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/src/rastr/io.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/src/rastr/meta.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/src/scripts/.gitkeep +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/src/scripts/demo_point_cloud.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/ABOUT_TASKS.md +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/dev_sync.ps1 +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/scripts/activate_venv.sh +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/scripts/configure_project.sh +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/scripts/dev_sync.sh +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/scripts/install_backend.sh +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/scripts/install_venv.sh +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/scripts/recover_corrupt_venv.sh +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/scripts/sh_runner.ps1 +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/scripts/sync_requirements.sh +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/scripts/sync_template.sh +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/activate_venv +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/activate_venv.cmd +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/activate_venv.ps1 +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/configure_project +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/configure_project.cmd +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/configure_project.ps1 +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/dev_sync +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/dev_sync.cmd +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/dev_sync.ps1 +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/install_backend +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/install_backend.cmd +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/install_backend.ps1 +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/install_venv +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/install_venv.cmd +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/install_venv.ps1 +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/recover_corrupt_venv +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/recover_corrupt_venv.cmd +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/recover_corrupt_venv.ps1 +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/sync_requirements +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/sync_requirements.cmd +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/sync_requirements.ps1 +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/sync_template +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/sync_template.cmd +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/shims/sync_template.ps1 +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tasks/sync_template.ps1 +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tests/assets/.gitkeep +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tests/assets/pga_g_clipped.grd +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tests/assets/pga_g_clipped.tif +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tests/conftest.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tests/rastr/.gitkeep +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tests/rastr/gis/test_fishnet.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tests/rastr/gis/test_smooth.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tests/rastr/regression_test_data/test_plot_raster.png +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tests/rastr/regression_test_data/test_write_raster_to_file.tif +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tests/rastr/test_create.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tests/rastr/test_io.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/tests/rastr/test_meta.py +0 -0
- {rastr-0.3.0 → rastr-0.4.0}/uv.lock +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rastr
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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/336916af169603534dd0728c10401667f263d98a.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
|
|
@@ -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.4.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 4, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -6,7 +6,7 @@ import importlib.util
|
|
|
6
6
|
import warnings
|
|
7
7
|
from contextlib import contextmanager
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import TYPE_CHECKING, Any, Literal
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Literal, overload
|
|
10
10
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
import numpy.ma
|
|
@@ -65,6 +65,21 @@ class RasterModel(BaseModel):
|
|
|
65
65
|
def meta(self, value: RasterMeta) -> None:
|
|
66
66
|
self.raster_meta = value
|
|
67
67
|
|
|
68
|
+
@property
|
|
69
|
+
def shape(self) -> tuple[int, ...]:
|
|
70
|
+
"""Shape of the raster array."""
|
|
71
|
+
return self.arr.shape
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def crs(self) -> CRS:
|
|
75
|
+
"""Convenience property to access the CRS via meta."""
|
|
76
|
+
return self.meta.crs
|
|
77
|
+
|
|
78
|
+
@crs.setter
|
|
79
|
+
def crs(self, value: CRS) -> None:
|
|
80
|
+
"""Set the CRS via meta."""
|
|
81
|
+
self.meta.crs = value
|
|
82
|
+
|
|
68
83
|
def __init__(
|
|
69
84
|
self,
|
|
70
85
|
*,
|
|
@@ -193,12 +208,12 @@ class RasterModel(BaseModel):
|
|
|
193
208
|
@property
|
|
194
209
|
def cell_x_coords(self) -> NDArray[np.float64]:
|
|
195
210
|
"""Get the x coordinates of the cell centres in the raster."""
|
|
196
|
-
return self.raster_meta.get_cell_x_coords(self.arr.shape[
|
|
211
|
+
return self.raster_meta.get_cell_x_coords(self.arr.shape[1])
|
|
197
212
|
|
|
198
213
|
@property
|
|
199
214
|
def cell_y_coords(self) -> NDArray[np.float64]:
|
|
200
215
|
"""Get the y coordinates of the cell centres in the raster."""
|
|
201
|
-
return self.raster_meta.get_cell_y_coords(self.arr.shape[
|
|
216
|
+
return self.raster_meta.get_cell_y_coords(self.arr.shape[0])
|
|
202
217
|
|
|
203
218
|
@contextmanager
|
|
204
219
|
def to_rasterio_dataset(
|
|
@@ -573,6 +588,43 @@ class RasterModel(BaseModel):
|
|
|
573
588
|
raster_meta = RasterMeta.example()
|
|
574
589
|
return cls(arr=arr, raster_meta=raster_meta)
|
|
575
590
|
|
|
591
|
+
@overload
|
|
592
|
+
def apply(
|
|
593
|
+
self,
|
|
594
|
+
func: Callable[[np.ndarray], np.ndarray],
|
|
595
|
+
*,
|
|
596
|
+
raw: Literal[True],
|
|
597
|
+
) -> Self: ...
|
|
598
|
+
@overload
|
|
599
|
+
def apply(
|
|
600
|
+
self,
|
|
601
|
+
func: Callable[[float], float] | Callable[[np.ndarray], np.ndarray],
|
|
602
|
+
*,
|
|
603
|
+
raw: Literal[False] = False,
|
|
604
|
+
) -> Self: ...
|
|
605
|
+
def apply(self, func, *, raw=False) -> Self:
|
|
606
|
+
"""Apply a function element-wise to the raster array.
|
|
607
|
+
|
|
608
|
+
Creates a new raster instance with the same metadata (CRS, transform, etc.)
|
|
609
|
+
but with the data array transformed by the provided function. The original
|
|
610
|
+
raster is not modified.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
func: The function to apply to the raster array. If `raw` is True, this
|
|
614
|
+
function should accept and return a NumPy array. If `raw` is False,
|
|
615
|
+
this function should accept and return a single float value.
|
|
616
|
+
raw: If True, the function is applied directly to the entire array at
|
|
617
|
+
once. If False, the function is applied element-wise to each cell
|
|
618
|
+
in the array using `np.vectorize()`. Default is False.
|
|
619
|
+
"""
|
|
620
|
+
new_raster = self.model_copy()
|
|
621
|
+
if raw:
|
|
622
|
+
new_arr = func(self.arr)
|
|
623
|
+
else:
|
|
624
|
+
new_arr = np.vectorize(func)(self.arr)
|
|
625
|
+
new_raster.arr = np.asarray(new_arr)
|
|
626
|
+
return new_raster
|
|
627
|
+
|
|
576
628
|
def fillna(self, value: float) -> Self:
|
|
577
629
|
"""Fill NaN values in the raster with a specified value.
|
|
578
630
|
|
|
@@ -599,12 +651,14 @@ class RasterModel(BaseModel):
|
|
|
599
651
|
return coords[:, :, 0], coords[:, :, 1]
|
|
600
652
|
|
|
601
653
|
def contour(
|
|
602
|
-
self,
|
|
654
|
+
self, levels: list[float] | NDArray, *, smoothing: bool = True
|
|
603
655
|
) -> gpd.GeoDataFrame:
|
|
604
656
|
"""Create contour lines from the raster data, optionally with smoothing.
|
|
605
657
|
|
|
606
|
-
The contour lines are returned as a GeoDataFrame with the contours
|
|
607
|
-
|
|
658
|
+
The contour lines are returned as a GeoDataFrame with the contours dissolved
|
|
659
|
+
by level, resulting in one row per contour level. Each row contains a
|
|
660
|
+
(Multi)LineString geometry representing all contour lines for that level,
|
|
661
|
+
and the contour level value in a column named 'level'.
|
|
608
662
|
|
|
609
663
|
Consider calling `blur()` before this method to smooth the raster data before
|
|
610
664
|
contouring, to denoise the contours.
|
|
@@ -627,7 +681,7 @@ class RasterModel(BaseModel):
|
|
|
627
681
|
level=level,
|
|
628
682
|
)
|
|
629
683
|
|
|
630
|
-
#
|
|
684
|
+
# Construct shapely LineString objects
|
|
631
685
|
# Convert to CRS from array index coordinates to raster CRS
|
|
632
686
|
geoms = [
|
|
633
687
|
LineString(
|
|
@@ -656,7 +710,8 @@ class RasterModel(BaseModel):
|
|
|
656
710
|
crs=self.raster_meta.crs,
|
|
657
711
|
)
|
|
658
712
|
|
|
659
|
-
|
|
713
|
+
# Dissolve contours by level to merge all contour lines of the same level
|
|
714
|
+
return contour_gdf.dissolve(by="level", as_index=False)
|
|
660
715
|
|
|
661
716
|
def blur(self, sigma: float) -> Self:
|
|
662
717
|
"""Apply a Gaussian blur to the raster data.
|
|
@@ -701,7 +756,7 @@ class RasterModel(BaseModel):
|
|
|
701
756
|
bounds: tuple[float, float, float, float],
|
|
702
757
|
strategy: Literal["underflow", "overflow"] = "underflow",
|
|
703
758
|
) -> Self:
|
|
704
|
-
"""Crop the raster to the specified bounds.
|
|
759
|
+
"""Crop the raster to the specified bounds as (minx, miny, maxx, maxy).
|
|
705
760
|
|
|
706
761
|
Args:
|
|
707
762
|
bounds: A tuple of (minx, miny, maxx, maxy) defining the bounds to crop to.
|
|
@@ -746,7 +801,7 @@ class RasterModel(BaseModel):
|
|
|
746
801
|
raise NotImplementedError(msg)
|
|
747
802
|
|
|
748
803
|
# Crop the array
|
|
749
|
-
cropped_arr = arr[np.ix_(
|
|
804
|
+
cropped_arr = arr[np.ix_(y_idx, x_idx)]
|
|
750
805
|
|
|
751
806
|
# Check the shape of the cropped array
|
|
752
807
|
if cropped_arr.size == 0:
|
|
@@ -12,7 +12,7 @@ from affine import Affine
|
|
|
12
12
|
from branca.colormap import LinearColormap
|
|
13
13
|
from pydantic import ValidationError
|
|
14
14
|
from pyproj.crs.crs import CRS
|
|
15
|
-
from shapely.geometry import Point, Polygon
|
|
15
|
+
from shapely.geometry import LineString, MultiLineString, Point, Polygon
|
|
16
16
|
|
|
17
17
|
from rastr.meta import RasterMeta
|
|
18
18
|
from rastr.raster import RasterModel
|
|
@@ -116,6 +116,43 @@ class TestRasterModel:
|
|
|
116
116
|
assert example_raster.meta is new_meta
|
|
117
117
|
assert example_raster.raster_meta != original_meta
|
|
118
118
|
|
|
119
|
+
class TestShape:
|
|
120
|
+
def test_shape_property(self, example_raster: RasterModel):
|
|
121
|
+
# Act
|
|
122
|
+
shape = example_raster.shape
|
|
123
|
+
|
|
124
|
+
# Assert
|
|
125
|
+
assert shape == (2, 2)
|
|
126
|
+
assert shape == example_raster.arr.shape
|
|
127
|
+
|
|
128
|
+
class TestCRS:
|
|
129
|
+
def test_crs_getter(self, example_raster: RasterModel):
|
|
130
|
+
# Act
|
|
131
|
+
crs_via_property = example_raster.crs
|
|
132
|
+
crs_via_meta = example_raster.meta.crs
|
|
133
|
+
crs_via_raster_meta = example_raster.raster_meta.crs
|
|
134
|
+
|
|
135
|
+
# Assert
|
|
136
|
+
assert crs_via_property is crs_via_meta
|
|
137
|
+
assert crs_via_property is crs_via_raster_meta
|
|
138
|
+
assert crs_via_property == crs_via_meta
|
|
139
|
+
assert crs_via_property == crs_via_raster_meta
|
|
140
|
+
assert isinstance(crs_via_property, CRS)
|
|
141
|
+
|
|
142
|
+
def test_crs_setter(self, example_raster: RasterModel):
|
|
143
|
+
# Arrange
|
|
144
|
+
new_crs = CRS.from_epsg(4326)
|
|
145
|
+
original_crs = example_raster.crs
|
|
146
|
+
|
|
147
|
+
# Act
|
|
148
|
+
example_raster.crs = new_crs
|
|
149
|
+
|
|
150
|
+
# Assert
|
|
151
|
+
assert example_raster.crs is new_crs
|
|
152
|
+
assert example_raster.meta.crs is new_crs
|
|
153
|
+
assert example_raster.raster_meta.crs is new_crs
|
|
154
|
+
assert example_raster.crs != original_crs
|
|
155
|
+
|
|
119
156
|
class TestSample:
|
|
120
157
|
def test_sample_nan_raise(self, example_raster: RasterModel):
|
|
121
158
|
with pytest.raises(
|
|
@@ -600,6 +637,14 @@ class TestRasterModel:
|
|
|
600
637
|
# Assert
|
|
601
638
|
np.testing.assert_array_equal(result.arr, np.array([[0, -1], [-2, -3]]))
|
|
602
639
|
|
|
640
|
+
class TestApply:
|
|
641
|
+
def test_sine(self, example_raster: RasterModel):
|
|
642
|
+
# Act
|
|
643
|
+
result = example_raster.apply(np.sin)
|
|
644
|
+
|
|
645
|
+
# Assert
|
|
646
|
+
np.testing.assert_array_equal(result.arr, np.sin(example_raster.arr))
|
|
647
|
+
|
|
603
648
|
class TestToFile:
|
|
604
649
|
def test_saving_gtiff(self, tmp_path: Path, example_raster: RasterModel):
|
|
605
650
|
# Arrange
|
|
@@ -818,6 +863,61 @@ class TestRasterModel:
|
|
|
818
863
|
assert len(contour_gdf_list) == len(contour_gdf_array)
|
|
819
864
|
assert list(contour_gdf_list["level"]) == list(contour_gdf_array["level"])
|
|
820
865
|
|
|
866
|
+
def test_contour_positional_levels(self):
|
|
867
|
+
# Arrange
|
|
868
|
+
raster = RasterModel.example()
|
|
869
|
+
levels = [0.0, 0.5]
|
|
870
|
+
|
|
871
|
+
# Act - should pass without error when using positional levels arg
|
|
872
|
+
contour_gdf = raster.contour(levels) # noqa: F841
|
|
873
|
+
|
|
874
|
+
def test_contour_returns_gdf_with_correct_columns(self):
|
|
875
|
+
raster = RasterModel.example()
|
|
876
|
+
gdf = raster.contour(levels=[0.0, 0.5])
|
|
877
|
+
|
|
878
|
+
assert isinstance(gdf, gpd.GeoDataFrame)
|
|
879
|
+
assert list(gdf.columns) == ["level", "geometry"]
|
|
880
|
+
assert "level" in gdf.columns
|
|
881
|
+
assert "geometry" in gdf.columns
|
|
882
|
+
|
|
883
|
+
def test_contour_levels_in_result(self):
|
|
884
|
+
raster = RasterModel.example()
|
|
885
|
+
levels = [0.0, 0.5]
|
|
886
|
+
gdf = raster.contour(levels=levels)
|
|
887
|
+
|
|
888
|
+
result_levels = set(gdf["level"].unique())
|
|
889
|
+
expected_levels = set(levels)
|
|
890
|
+
assert result_levels == expected_levels
|
|
891
|
+
|
|
892
|
+
def test_contour_dissolve_behavior_one_row_per_level(self):
|
|
893
|
+
raster = RasterModel.example()
|
|
894
|
+
levels = [0.0, 0.5]
|
|
895
|
+
gdf = raster.contour(levels=levels)
|
|
896
|
+
|
|
897
|
+
# After dissolving, should have exactly one row per level
|
|
898
|
+
assert len(gdf) == len(levels)
|
|
899
|
+
assert set(gdf["level"]) == set(levels)
|
|
900
|
+
|
|
901
|
+
# Geometries should be MultiLineString (dissolved from multiple LineStrings)
|
|
902
|
+
for geom in gdf.geometry:
|
|
903
|
+
assert isinstance(
|
|
904
|
+
geom, (MultiLineString, LineString)
|
|
905
|
+
) # Can be either depending on dissolve result
|
|
906
|
+
|
|
907
|
+
def test_contour_with_smoothing(self):
|
|
908
|
+
raster = RasterModel.example()
|
|
909
|
+
gdf = raster.contour(levels=[0.0], smoothing=True)
|
|
910
|
+
|
|
911
|
+
assert len(gdf) > 0
|
|
912
|
+
assert all(gdf["level"] == 0.0)
|
|
913
|
+
|
|
914
|
+
def test_contour_without_smoothing(self):
|
|
915
|
+
raster = RasterModel.example()
|
|
916
|
+
gdf = raster.contour(levels=[0.0], smoothing=False)
|
|
917
|
+
|
|
918
|
+
assert len(gdf) > 0
|
|
919
|
+
assert all(gdf["level"] == 0.0)
|
|
920
|
+
|
|
821
921
|
|
|
822
922
|
@pytest.fixture
|
|
823
923
|
def base_raster():
|
|
@@ -868,13 +968,13 @@ class TestCrop:
|
|
|
868
968
|
minx, miny, maxx, maxy = base_raster.bounds
|
|
869
969
|
cell_size = base_raster.raster_meta.cell_size
|
|
870
970
|
bounds = (minx, miny + cell_size, maxx, maxy - cell_size)
|
|
871
|
-
expected_transform = Affine(
|
|
971
|
+
expected_transform = Affine(10.0, 0.0, 0.0, 0.0, -10.0, 100.0 - cell_size)
|
|
872
972
|
|
|
873
973
|
# Act
|
|
874
974
|
cropped = base_raster.crop(bounds)
|
|
875
975
|
|
|
876
976
|
# Assert
|
|
877
|
-
assert cropped.arr.shape == (4,
|
|
977
|
+
assert cropped.arr.shape == (2, 4) # Y-crop reduces rows, keeps columns
|
|
878
978
|
assert cropped.bounds == bounds
|
|
879
979
|
assert cropped.raster_meta.cell_size == base_raster.raster_meta.cell_size
|
|
880
980
|
assert cropped.raster_meta.crs == base_raster.raster_meta.crs
|
|
@@ -885,13 +985,13 @@ class TestCrop:
|
|
|
885
985
|
minx, miny, maxx, maxy = base_raster.bounds
|
|
886
986
|
cell_size = base_raster.raster_meta.cell_size
|
|
887
987
|
bounds = (minx + cell_size, miny, maxx - cell_size, maxy)
|
|
888
|
-
expected_transform = Affine(
|
|
988
|
+
expected_transform = Affine(10.0, 0.0, minx + cell_size, 0.0, -10.0, 100.0)
|
|
889
989
|
|
|
890
990
|
# Act
|
|
891
991
|
cropped = base_raster.crop(bounds)
|
|
892
992
|
|
|
893
993
|
# Assert
|
|
894
|
-
assert cropped.arr.shape == (2,
|
|
994
|
+
assert cropped.arr.shape == (4, 2) # X-crop reduces columns, keeps rows
|
|
895
995
|
assert cropped.bounds == bounds
|
|
896
996
|
assert cropped.raster_meta.cell_size == base_raster.raster_meta.cell_size
|
|
897
997
|
assert cropped.raster_meta.crs == base_raster.raster_meta.crs
|
|
@@ -980,6 +1080,46 @@ class TestCrop:
|
|
|
980
1080
|
):
|
|
981
1081
|
base_raster.crop(bounds)
|
|
982
1082
|
|
|
1083
|
+
def test_crop_non_square_raster_indexing(self):
|
|
1084
|
+
"""Test that crop method correctly indexes non-square rasters.
|
|
1085
|
+
|
|
1086
|
+
This tests the fix for issue #140 where array indexing was backwards,
|
|
1087
|
+
causing spatial misalignment in cropped rasters.
|
|
1088
|
+
"""
|
|
1089
|
+
# Arrange: Create a non-square raster with distinctive values
|
|
1090
|
+
meta = RasterMeta(
|
|
1091
|
+
cell_size=1.0,
|
|
1092
|
+
crs=CRS.from_epsg(2193),
|
|
1093
|
+
transform=Affine(1.0, 0.0, 0.0, 0.0, -1.0, 3.0),
|
|
1094
|
+
)
|
|
1095
|
+
arr = np.array(
|
|
1096
|
+
[
|
|
1097
|
+
[1, 2, 3, 4, 5], # row 0
|
|
1098
|
+
[6, 7, 8, 9, 10], # row 1
|
|
1099
|
+
[11, 12, 13, 14, 15], # row 2
|
|
1100
|
+
],
|
|
1101
|
+
dtype=float,
|
|
1102
|
+
)
|
|
1103
|
+
raster = RasterModel(arr=arr, raster_meta=meta)
|
|
1104
|
+
|
|
1105
|
+
# Act: Crop to select middle 3 columns (keeping all rows)
|
|
1106
|
+
bounds = (1.0, 0.0, 4.0, 3.0) # Should select columns at x=1.5, 2.5, 3.5
|
|
1107
|
+
cropped = raster.crop(bounds)
|
|
1108
|
+
|
|
1109
|
+
# Assert: Result should have all 3 rows but only 3 columns
|
|
1110
|
+
expected_shape = (3, 3)
|
|
1111
|
+
expected_array = np.array(
|
|
1112
|
+
[
|
|
1113
|
+
[2, 3, 4], # row 0, columns 1,2,3 (0-indexed)
|
|
1114
|
+
[7, 8, 9], # row 1, columns 1,2,3
|
|
1115
|
+
[12, 13, 14], # row 2, columns 1,2,3
|
|
1116
|
+
],
|
|
1117
|
+
dtype=float,
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
assert cropped.arr.shape == expected_shape
|
|
1121
|
+
np.testing.assert_array_equal(cropped.arr, expected_array)
|
|
1122
|
+
|
|
983
1123
|
def test_unsupported_crop_strategy(self, base_raster: RasterModel):
|
|
984
1124
|
# Arrange
|
|
985
1125
|
bounds = base_raster.bounds
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|