ssb-sgis 1.0.3__py3-none-any.whl → 1.0.4__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.
Files changed (39) hide show
  1. sgis/__init__.py +10 -3
  2. sgis/debug_config.py +24 -0
  3. sgis/geopandas_tools/bounds.py +16 -21
  4. sgis/geopandas_tools/buffer_dissolve_explode.py +112 -30
  5. sgis/geopandas_tools/centerlines.py +4 -91
  6. sgis/geopandas_tools/cleaning.py +1576 -583
  7. sgis/geopandas_tools/conversion.py +24 -14
  8. sgis/geopandas_tools/duplicates.py +27 -6
  9. sgis/geopandas_tools/general.py +259 -100
  10. sgis/geopandas_tools/geometry_types.py +1 -1
  11. sgis/geopandas_tools/neighbors.py +16 -12
  12. sgis/geopandas_tools/overlay.py +2 -2
  13. sgis/geopandas_tools/point_operations.py +3 -3
  14. sgis/geopandas_tools/polygon_operations.py +505 -100
  15. sgis/geopandas_tools/polygons_as_rings.py +40 -8
  16. sgis/geopandas_tools/sfilter.py +26 -9
  17. sgis/io/dapla_functions.py +238 -19
  18. sgis/maps/examine.py +11 -10
  19. sgis/maps/explore.py +227 -155
  20. sgis/maps/legend.py +13 -4
  21. sgis/maps/map.py +22 -13
  22. sgis/maps/maps.py +100 -29
  23. sgis/maps/thematicmap.py +25 -18
  24. sgis/networkanalysis/_service_area.py +6 -1
  25. sgis/networkanalysis/cutting_lines.py +12 -5
  26. sgis/networkanalysis/finding_isolated_networks.py +13 -6
  27. sgis/networkanalysis/networkanalysis.py +10 -12
  28. sgis/parallel/parallel.py +27 -10
  29. sgis/raster/base.py +208 -0
  30. sgis/raster/cube.py +3 -3
  31. sgis/raster/image_collection.py +1419 -722
  32. sgis/raster/indices.py +10 -7
  33. sgis/raster/raster.py +7 -7
  34. sgis/raster/sentinel_config.py +33 -17
  35. {ssb_sgis-1.0.3.dist-info → ssb_sgis-1.0.4.dist-info}/METADATA +6 -7
  36. ssb_sgis-1.0.4.dist-info/RECORD +62 -0
  37. ssb_sgis-1.0.3.dist-info/RECORD +0 -61
  38. {ssb_sgis-1.0.3.dist-info → ssb_sgis-1.0.4.dist-info}/LICENSE +0 -0
  39. {ssb_sgis-1.0.3.dist-info → ssb_sgis-1.0.4.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.unary_union(
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.unary_union.intersects(other)]
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.unary_union([shapely.box(*r.bounds) for r in self])
974
+ return shapely.union_all([shapely.box(*r.bounds) for r in self])
975
975
 
976
976
  @property
977
977
  def centroid(self) -> GeoSeries: