ssb-sgis 1.0.3__py3-none-any.whl → 1.0.5__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.
- sgis/__init__.py +10 -3
- sgis/debug_config.py +24 -0
- sgis/geopandas_tools/bounds.py +16 -21
- sgis/geopandas_tools/buffer_dissolve_explode.py +112 -30
- sgis/geopandas_tools/centerlines.py +4 -91
- sgis/geopandas_tools/cleaning.py +1576 -583
- sgis/geopandas_tools/conversion.py +24 -14
- sgis/geopandas_tools/duplicates.py +27 -6
- sgis/geopandas_tools/general.py +259 -100
- sgis/geopandas_tools/geometry_types.py +1 -1
- sgis/geopandas_tools/neighbors.py +16 -12
- sgis/geopandas_tools/overlay.py +7 -3
- sgis/geopandas_tools/point_operations.py +3 -3
- sgis/geopandas_tools/polygon_operations.py +505 -100
- sgis/geopandas_tools/polygons_as_rings.py +40 -8
- sgis/geopandas_tools/sfilter.py +26 -9
- sgis/io/dapla_functions.py +238 -19
- sgis/maps/examine.py +11 -10
- sgis/maps/explore.py +227 -155
- sgis/maps/legend.py +13 -4
- sgis/maps/map.py +22 -13
- sgis/maps/maps.py +100 -29
- sgis/maps/thematicmap.py +25 -18
- sgis/networkanalysis/_service_area.py +6 -1
- sgis/networkanalysis/cutting_lines.py +12 -5
- sgis/networkanalysis/finding_isolated_networks.py +13 -6
- sgis/networkanalysis/networkanalysis.py +10 -12
- sgis/parallel/parallel.py +27 -10
- sgis/raster/base.py +208 -0
- sgis/raster/cube.py +3 -3
- sgis/raster/image_collection.py +1421 -724
- sgis/raster/indices.py +10 -7
- sgis/raster/raster.py +7 -7
- sgis/raster/sentinel_config.py +33 -17
- {ssb_sgis-1.0.3.dist-info → ssb_sgis-1.0.5.dist-info}/METADATA +6 -7
- ssb_sgis-1.0.5.dist-info/RECORD +62 -0
- ssb_sgis-1.0.3.dist-info/RECORD +0 -61
- {ssb_sgis-1.0.3.dist-info → ssb_sgis-1.0.5.dist-info}/LICENSE +0 -0
- {ssb_sgis-1.0.3.dist-info → ssb_sgis-1.0.5.dist-info}/WHEEL +0 -0
sgis/raster/base.py
CHANGED
|
@@ -1,8 +1,216 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import numbers
|
|
3
|
+
import warnings
|
|
4
|
+
from collections.abc import Callable
|
|
1
5
|
from contextlib import contextmanager
|
|
6
|
+
from typing import Any
|
|
2
7
|
|
|
8
|
+
import joblib
|
|
3
9
|
import numpy as np
|
|
4
10
|
import pandas as pd
|
|
5
11
|
import rasterio
|
|
12
|
+
from affine import Affine
|
|
13
|
+
from geopandas import GeoDataFrame
|
|
14
|
+
from geopandas import GeoSeries
|
|
15
|
+
from rasterio import features
|
|
16
|
+
from rasterio.enums import MergeAlg
|
|
17
|
+
from shapely import Geometry
|
|
18
|
+
from shapely.geometry import shape
|
|
19
|
+
|
|
20
|
+
from ..geopandas_tools.conversion import to_bbox
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_transform_from_bounds(
|
|
24
|
+
obj: GeoDataFrame | GeoSeries | Geometry | tuple, shape: tuple[float, ...]
|
|
25
|
+
) -> Affine:
|
|
26
|
+
minx, miny, maxx, maxy = to_bbox(obj)
|
|
27
|
+
if len(shape) == 2:
|
|
28
|
+
height, width = shape
|
|
29
|
+
elif len(shape) == 3:
|
|
30
|
+
_, height, width = shape
|
|
31
|
+
else:
|
|
32
|
+
return None
|
|
33
|
+
# raise ValueError(shape)
|
|
34
|
+
return rasterio.transform.from_bounds(minx, miny, maxx, maxy, width, height)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_shape_from_bounds(
|
|
38
|
+
obj: GeoDataFrame | GeoSeries | Geometry | tuple,
|
|
39
|
+
res: int,
|
|
40
|
+
indexes: int | tuple[int],
|
|
41
|
+
) -> tuple[int, int]:
|
|
42
|
+
resx, resy = (res, res) if isinstance(res, numbers.Number) else res
|
|
43
|
+
|
|
44
|
+
minx, miny, maxx, maxy = to_bbox(obj)
|
|
45
|
+
|
|
46
|
+
# minx = math.floor(minx / res) * res
|
|
47
|
+
# maxx = math.ceil(maxx / res) * res
|
|
48
|
+
# miny = math.floor(miny / res) * res
|
|
49
|
+
# maxy = math.ceil(maxy / res) * res
|
|
50
|
+
|
|
51
|
+
# # Compute output array shape. We guarantee it will cover the output
|
|
52
|
+
# # bounds completely
|
|
53
|
+
# width = round((maxx - minx) // res)
|
|
54
|
+
# height = round((maxy - miny) // res)
|
|
55
|
+
|
|
56
|
+
# if not isinstance(indexes, int):
|
|
57
|
+
# return len(indexes), height, width
|
|
58
|
+
# return height, width
|
|
59
|
+
|
|
60
|
+
diffx = maxx - minx
|
|
61
|
+
diffy = maxy - miny
|
|
62
|
+
width = int(diffx / resx)
|
|
63
|
+
height = int(diffy / resy)
|
|
64
|
+
if not isinstance(indexes, int):
|
|
65
|
+
return len(indexes), width, height
|
|
66
|
+
return height, width
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _array_to_geojson(
|
|
70
|
+
array: np.ndarray, transform: Affine, processes: int
|
|
71
|
+
) -> list[tuple]:
|
|
72
|
+
if hasattr(array, "mask"):
|
|
73
|
+
if isinstance(array.mask, np.ndarray):
|
|
74
|
+
mask = array.mask == False
|
|
75
|
+
else:
|
|
76
|
+
mask = None
|
|
77
|
+
array = array.data
|
|
78
|
+
else:
|
|
79
|
+
mask = None
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
return _array_to_geojson_loop(array, transform, mask, processes)
|
|
83
|
+
except ValueError:
|
|
84
|
+
try:
|
|
85
|
+
array = array.astype(np.float32)
|
|
86
|
+
return _array_to_geojson_loop(array, transform, mask, processes)
|
|
87
|
+
|
|
88
|
+
except Exception as err:
|
|
89
|
+
raise err.__class__(array.shape, err) from err
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _array_to_geojson_loop(array, transform, mask, processes):
|
|
93
|
+
if processes == 1:
|
|
94
|
+
return [
|
|
95
|
+
(value, shape(geom))
|
|
96
|
+
for geom, value in features.shapes(array, transform=transform, mask=mask)
|
|
97
|
+
]
|
|
98
|
+
else:
|
|
99
|
+
with joblib.Parallel(n_jobs=processes, backend="threading") as parallel:
|
|
100
|
+
return parallel(
|
|
101
|
+
joblib.delayed(_value_geom_pair)(value, geom)
|
|
102
|
+
for geom, value in features.shapes(
|
|
103
|
+
array, transform=transform, mask=mask
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _value_geom_pair(value, geom):
|
|
109
|
+
return (value, shape(geom))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _gdf_to_arr(
|
|
113
|
+
gdf: GeoDataFrame,
|
|
114
|
+
res: int | float,
|
|
115
|
+
fill: int = 0,
|
|
116
|
+
all_touched: bool = False,
|
|
117
|
+
merge_alg: Callable = MergeAlg.replace,
|
|
118
|
+
default_value: int = 1,
|
|
119
|
+
dtype: Any | None = None,
|
|
120
|
+
) -> np.ndarray:
|
|
121
|
+
"""Construct Raster from a GeoDataFrame or GeoSeries.
|
|
122
|
+
|
|
123
|
+
The GeoDataFrame should have
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
gdf: The GeoDataFrame to rasterize.
|
|
127
|
+
res: Resolution of the raster in units of the GeoDataFrame's coordinate reference system.
|
|
128
|
+
fill: Fill value for areas outside of input geometries (default is 0).
|
|
129
|
+
all_touched: Whether to consider all pixels touched by geometries,
|
|
130
|
+
not just those whose center is within the polygon (default is False).
|
|
131
|
+
merge_alg: Merge algorithm to use when combining geometries
|
|
132
|
+
(default is 'MergeAlg.replace').
|
|
133
|
+
default_value: Default value to use for the rasterized pixels
|
|
134
|
+
(default is 1).
|
|
135
|
+
dtype: Data type of the output array. If None, it will be
|
|
136
|
+
determined automatically.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
A Raster instance based on the specified GeoDataFrame and parameters.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
TypeError: If 'transform' is provided in kwargs, as this is
|
|
143
|
+
computed based on the GeoDataFrame bounds and resolution.
|
|
144
|
+
"""
|
|
145
|
+
if isinstance(gdf, GeoSeries):
|
|
146
|
+
values = gdf.index
|
|
147
|
+
gdf = gdf.to_frame("geometry")
|
|
148
|
+
elif isinstance(gdf, GeoDataFrame):
|
|
149
|
+
if len(gdf.columns) > 2:
|
|
150
|
+
raise ValueError(
|
|
151
|
+
"gdf should have only a geometry column and one numeric column to "
|
|
152
|
+
"use as array values. "
|
|
153
|
+
"Alternatively only a geometry column and a numeric index."
|
|
154
|
+
)
|
|
155
|
+
elif len(gdf.columns) == 1:
|
|
156
|
+
values = gdf.index
|
|
157
|
+
else:
|
|
158
|
+
col: str = next(
|
|
159
|
+
iter([col for col in gdf if col != gdf._geometry_column_name])
|
|
160
|
+
)
|
|
161
|
+
values = gdf[col]
|
|
162
|
+
|
|
163
|
+
if isinstance(values, pd.MultiIndex):
|
|
164
|
+
raise ValueError("Index cannot be MultiIndex.")
|
|
165
|
+
|
|
166
|
+
shape = _get_shape_from_bounds(gdf.total_bounds, res=res, indexes=1)
|
|
167
|
+
transform = _get_transform_from_bounds(gdf.total_bounds, shape)
|
|
168
|
+
|
|
169
|
+
return features.rasterize(
|
|
170
|
+
_gdf_to_geojson_with_col(gdf, values),
|
|
171
|
+
out_shape=shape,
|
|
172
|
+
transform=transform,
|
|
173
|
+
fill=fill,
|
|
174
|
+
all_touched=all_touched,
|
|
175
|
+
merge_alg=merge_alg,
|
|
176
|
+
default_value=default_value,
|
|
177
|
+
dtype=dtype,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _gdf_to_geojson_with_col(gdf: GeoDataFrame, values: np.ndarray) -> list[dict]:
|
|
182
|
+
with warnings.catch_warnings():
|
|
183
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
|
184
|
+
return [
|
|
185
|
+
(feature["geometry"], val)
|
|
186
|
+
for val, feature in zip(
|
|
187
|
+
values, json.loads(gdf.to_json())["features"], strict=False
|
|
188
|
+
)
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _shapely_to_raster(
|
|
193
|
+
geometry: Geometry,
|
|
194
|
+
res: int | float,
|
|
195
|
+
fill: int = 0,
|
|
196
|
+
all_touched: bool = False,
|
|
197
|
+
merge_alg: Callable = MergeAlg.replace,
|
|
198
|
+
default_value: int = 1,
|
|
199
|
+
dtype: Any | None = None,
|
|
200
|
+
) -> np.array:
|
|
201
|
+
shape = _get_shape_from_bounds(geometry.bounds, res=res, indexes=1)
|
|
202
|
+
transform = _get_transform_from_bounds(geometry.bounds, shape)
|
|
203
|
+
|
|
204
|
+
return features.rasterize(
|
|
205
|
+
[(geometry, default_value)],
|
|
206
|
+
out_shape=shape,
|
|
207
|
+
transform=transform,
|
|
208
|
+
fill=fill,
|
|
209
|
+
all_touched=all_touched,
|
|
210
|
+
merge_alg=merge_alg,
|
|
211
|
+
default_value=default_value,
|
|
212
|
+
dtype=dtype,
|
|
213
|
+
)
|
|
6
214
|
|
|
7
215
|
|
|
8
216
|
@contextmanager
|
sgis/raster/cube.py
CHANGED
|
@@ -383,7 +383,7 @@ class DataCube:
|
|
|
383
383
|
|
|
384
384
|
if grid is None:
|
|
385
385
|
crs = get_common_crs(gdf)
|
|
386
|
-
total_bounds = shapely.
|
|
386
|
+
total_bounds = shapely.union_all(
|
|
387
387
|
[shapely.box(*frame.total_bounds) for frame in gdf]
|
|
388
388
|
)
|
|
389
389
|
grid = make_grid(total_bounds, gridsize=tile_size, crs=crs)
|
|
@@ -595,7 +595,7 @@ class DataCube:
|
|
|
595
595
|
"""Spatially filter images by bounding box or geometry object."""
|
|
596
596
|
other = to_shapely(other)
|
|
597
597
|
cube = self.copy() if copy else self
|
|
598
|
-
cube.data = [raster for raster in self if raster.
|
|
598
|
+
cube.data = [raster for raster in self if raster.union_all().intersects(other)]
|
|
599
599
|
return cube
|
|
600
600
|
|
|
601
601
|
def clip(
|
|
@@ -971,7 +971,7 @@ class DataCube:
|
|
|
971
971
|
@property
|
|
972
972
|
def unary_union(self) -> Geometry:
|
|
973
973
|
"""Box polygon of the combined bounds of each image."""
|
|
974
|
-
return shapely.
|
|
974
|
+
return shapely.union_all([shapely.box(*r.bounds) for r in self])
|
|
975
975
|
|
|
976
976
|
@property
|
|
977
977
|
def centroid(self) -> GeoSeries:
|