ssb-sgis 1.0.2__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 (50) hide show
  1. sgis/__init__.py +20 -9
  2. sgis/debug_config.py +24 -0
  3. sgis/exceptions.py +2 -2
  4. sgis/geopandas_tools/bounds.py +33 -36
  5. sgis/geopandas_tools/buffer_dissolve_explode.py +136 -35
  6. sgis/geopandas_tools/centerlines.py +4 -91
  7. sgis/geopandas_tools/cleaning.py +1576 -583
  8. sgis/geopandas_tools/conversion.py +38 -19
  9. sgis/geopandas_tools/duplicates.py +29 -8
  10. sgis/geopandas_tools/general.py +263 -100
  11. sgis/geopandas_tools/geometry_types.py +4 -4
  12. sgis/geopandas_tools/neighbors.py +19 -15
  13. sgis/geopandas_tools/overlay.py +2 -2
  14. sgis/geopandas_tools/point_operations.py +5 -5
  15. sgis/geopandas_tools/polygon_operations.py +510 -105
  16. sgis/geopandas_tools/polygons_as_rings.py +40 -8
  17. sgis/geopandas_tools/sfilter.py +29 -12
  18. sgis/helpers.py +3 -3
  19. sgis/io/dapla_functions.py +238 -19
  20. sgis/io/read_parquet.py +1 -1
  21. sgis/maps/examine.py +27 -12
  22. sgis/maps/explore.py +450 -65
  23. sgis/maps/legend.py +177 -76
  24. sgis/maps/map.py +206 -103
  25. sgis/maps/maps.py +178 -105
  26. sgis/maps/thematicmap.py +243 -83
  27. sgis/networkanalysis/_service_area.py +6 -1
  28. sgis/networkanalysis/closing_network_holes.py +2 -2
  29. sgis/networkanalysis/cutting_lines.py +15 -8
  30. sgis/networkanalysis/directednetwork.py +1 -1
  31. sgis/networkanalysis/finding_isolated_networks.py +15 -8
  32. sgis/networkanalysis/networkanalysis.py +17 -19
  33. sgis/networkanalysis/networkanalysisrules.py +1 -1
  34. sgis/networkanalysis/traveling_salesman.py +1 -1
  35. sgis/parallel/parallel.py +64 -27
  36. sgis/raster/__init__.py +0 -6
  37. sgis/raster/base.py +208 -0
  38. sgis/raster/cube.py +54 -8
  39. sgis/raster/image_collection.py +3257 -0
  40. sgis/raster/indices.py +17 -5
  41. sgis/raster/raster.py +138 -243
  42. sgis/raster/sentinel_config.py +120 -0
  43. sgis/raster/zonal.py +0 -1
  44. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/METADATA +6 -7
  45. ssb_sgis-1.0.4.dist-info/RECORD +62 -0
  46. sgis/raster/methods_as_functions.py +0 -0
  47. sgis/raster/torchgeo.py +0 -171
  48. ssb_sgis-1.0.2.dist-info/RECORD +0 -61
  49. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/LICENSE +0 -0
  50. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/WHEEL +0 -0
sgis/__init__.py CHANGED
@@ -2,6 +2,7 @@ config = {
2
2
  "n_jobs": 1,
3
3
  }
4
4
 
5
+
5
6
  import sgis.raster.indices as indices
6
7
  from sgis.raster.raster import Raster
7
8
  from sgis.raster.raster import get_shape_from_bounds
@@ -15,7 +16,6 @@ from .geopandas_tools.bounds import gridloop
15
16
  from .geopandas_tools.bounds import make_grid
16
17
  from .geopandas_tools.bounds import make_grid_from_bbox
17
18
  from .geopandas_tools.bounds import make_ssb_grid
18
- from .geopandas_tools.bounds import points_in_bounds
19
19
  from .geopandas_tools.buffer_dissolve_explode import buff
20
20
  from .geopandas_tools.buffer_dissolve_explode import buffdiss
21
21
  from .geopandas_tools.buffer_dissolve_explode import buffdissexp
@@ -26,9 +26,9 @@ from .geopandas_tools.buffer_dissolve_explode import dissexp
26
26
  from .geopandas_tools.buffer_dissolve_explode import dissexp_by_cluster
27
27
  from .geopandas_tools.centerlines import get_rough_centerlines
28
28
  from .geopandas_tools.cleaning import coverage_clean
29
- from .geopandas_tools.cleaning import remove_spikes
30
29
  from .geopandas_tools.cleaning import split_and_eliminate_by_longest
31
- from .geopandas_tools.cleaning import split_by_neighbors
30
+
31
+ # from .geopandas_tools.cleaning import split_by_neighbors
32
32
  from .geopandas_tools.conversion import coordinate_array
33
33
  from .geopandas_tools.conversion import from_4326
34
34
  from .geopandas_tools.conversion import to_4326
@@ -44,6 +44,8 @@ from .geopandas_tools.general import clean_geoms
44
44
  from .geopandas_tools.general import drop_inactive_geometry_columns
45
45
  from .geopandas_tools.general import get_common_crs
46
46
  from .geopandas_tools.general import get_grouped_centroids
47
+ from .geopandas_tools.general import get_line_segments
48
+ from .geopandas_tools.general import points_in_bounds
47
49
  from .geopandas_tools.general import random_points
48
50
  from .geopandas_tools.general import random_points_in_polygons
49
51
  from .geopandas_tools.general import sort_large_first
@@ -66,6 +68,7 @@ from .geopandas_tools.neighbors import sjoin_within_distance
66
68
  from .geopandas_tools.overlay import clean_overlay
67
69
  from .geopandas_tools.point_operations import snap_all
68
70
  from .geopandas_tools.point_operations import snap_within_distance
71
+ from .geopandas_tools.polygon_operations import clean_dissexp
69
72
  from .geopandas_tools.polygon_operations import close_all_holes
70
73
  from .geopandas_tools.polygon_operations import close_small_holes
71
74
  from .geopandas_tools.polygon_operations import close_thin_holes
@@ -76,6 +79,7 @@ from .geopandas_tools.polygon_operations import get_cluster_mapper
76
79
  from .geopandas_tools.polygon_operations import get_gaps
77
80
  from .geopandas_tools.polygon_operations import get_holes
78
81
  from .geopandas_tools.polygon_operations import get_polygon_clusters
82
+ from .geopandas_tools.polygon_operations import split_polygons_by_lines
79
83
  from .geopandas_tools.polygons_as_rings import PolygonsAsRings
80
84
  from .geopandas_tools.sfilter import sfilter
81
85
  from .geopandas_tools.sfilter import sfilter_inverse
@@ -115,15 +119,22 @@ from .networkanalysis.traveling_salesman import traveling_salesman_problem
115
119
  from .parallel.parallel import Parallel
116
120
  from .parallel.parallel import parallel_overlay
117
121
  from .raster.cube import DataCube
118
-
119
- try:
120
- import sgis.raster.torchgeo as torchgeo
121
- except ImportError:
122
- pass
123
-
122
+ from .raster.cube import concat_cubes
123
+ from .raster.image_collection import Band
124
+ from .raster.image_collection import Image
125
+ from .raster.image_collection import ImageCollection
126
+ from .raster.image_collection import NDVIBand
127
+ from .raster.image_collection import Sentinel2Band
128
+ from .raster.image_collection import Sentinel2CloudlessBand
129
+ from .raster.image_collection import Sentinel2CloudlessCollection
130
+ from .raster.image_collection import Sentinel2CloudlessImage
131
+ from .raster.image_collection import Sentinel2Collection
132
+ from .raster.image_collection import Sentinel2Image
133
+ from .raster.image_collection import concat_image_collections
124
134
 
125
135
  try:
126
136
  from .io.dapla_functions import check_files
137
+ from .io.dapla_functions import get_bounds_series
127
138
  from .io.dapla_functions import read_geopandas
128
139
  from .io.dapla_functions import write_geopandas
129
140
  except ImportError:
sgis/debug_config.py ADDED
@@ -0,0 +1,24 @@
1
+ from typing import Any
2
+
3
+
4
+ class _NoExplore:
5
+ """Simply so signal that explore functions should be immediately exited."""
6
+
7
+
8
+ _DEBUG_CONFIG = {
9
+ # "center": (5.3719398, 59.00999914, 0.01),
10
+ # "center": (5.27306727, 59.44232754, 200),
11
+ # "center": (5.85575588, 62.33991158, 200),
12
+ # "center": (26.02870514, 70.68108478, 200),
13
+ "center": _NoExplore(),
14
+ "print": False,
15
+ }
16
+
17
+
18
+ def _try_debug_print(*args: Any) -> None:
19
+ if not _DEBUG_CONFIG["print"]:
20
+ return
21
+ try:
22
+ print(*args)
23
+ except Exception:
24
+ pass
sgis/exceptions.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Some small exception classes."""
2
2
 
3
3
 
4
- class NoPointsWithinSearchToleranceError(Exception):
4
+ class NoPointsWithinSearchToleranceError(ValueError):
5
5
  """Exception for when the points are too far away from the network."""
6
6
 
7
7
  def __init__(
@@ -19,5 +19,5 @@ class NoPointsWithinSearchToleranceError(Exception):
19
19
  )
20
20
 
21
21
 
22
- class ZeroLinesError(Exception):
22
+ class ZeroLinesError(ValueError):
23
23
  """DataFrame has 0 rows."""
@@ -18,7 +18,7 @@ from ..parallel.parallel import Parallel
18
18
  from .conversion import to_bbox
19
19
  from .conversion import to_gdf
20
20
  from .general import clean_clip
21
- from .general import is_bbox_like
21
+ from .general import get_common_crs
22
22
 
23
23
 
24
24
  @dataclass
@@ -37,7 +37,7 @@ class Gridlooper:
37
37
  Defaults to True.
38
38
 
39
39
  Examples:
40
- --------
40
+ ---------
41
41
  Get some points and some polygons.
42
42
 
43
43
  >>> import sgis as sg
@@ -133,11 +133,7 @@ class Gridlooper:
133
133
  )
134
134
  results = self.parallelizer.map(func_with_clip, buffered_grid)
135
135
  if not self.gridbuffer or not self.clip:
136
- return (
137
- results
138
- if not self.concat
139
- else pd.concat(results, ignore_index=True)
140
- )
136
+ return self._return(results, args, kwargs)
141
137
  out = []
142
138
  for cell_res, unbuffered in zip(results, grid, strict=True):
143
139
  out.append(
@@ -145,7 +141,7 @@ class Gridlooper:
145
141
  cell_res, unbuffered, self.keep_geom_type
146
142
  )
147
143
  )
148
- return out if not self.concat else pd.concat(out, ignore_index=True)
144
+ return self._return(out, args, kwargs)
149
145
 
150
146
  results = []
151
147
  for i, (unbuffered, buffered) in enumerate(
@@ -175,7 +171,18 @@ class Gridlooper:
175
171
  if self.verbose:
176
172
  print(f"Done with {i+1} of {n} grid cells", end="\r")
177
173
 
178
- return results if not self.concat else pd.concat(results, ignore_index=True)
174
+ return self._return(results, args, kwargs)
175
+
176
+ def _return(
177
+ self, results: list[Any], args: tuple[Any], kwargs: dict[str, Any]
178
+ ) -> list[Any] | GeoDataFrame:
179
+ if self.concat and len(results):
180
+ return pd.concat(results, ignore_index=True)
181
+ elif self.concat:
182
+ crs = get_common_crs(list(args) + list(kwargs.values()))
183
+ return GeoDataFrame({"geometry": []}, crs=crs)
184
+ else:
185
+ return results
179
186
 
180
187
 
181
188
  def gridloop(
@@ -222,7 +229,7 @@ def gridloop(
222
229
  TypeError: If args or kwargs has a wrong type
223
230
 
224
231
  Examples:
225
- --------
232
+ ---------
226
233
  Get some points and some polygons.
227
234
 
228
235
  >>> import sgis as sg
@@ -442,7 +449,7 @@ def make_grid(
442
449
  obj: GeoDataFrame | GeoSeries | Geometry | tuple,
443
450
  gridsize: int | float,
444
451
  *,
445
- crs: CRS = None,
452
+ crs: CRS | None = None,
446
453
  clip_to_bounds: bool = False,
447
454
  ) -> GeoDataFrame:
448
455
  """Create a polygon grid around geometries.
@@ -467,10 +474,6 @@ def make_grid(
467
474
  """
468
475
  if isinstance(obj, (GeoDataFrame | GeoSeries)):
469
476
  crs = obj.crs or crs
470
- elif not crs:
471
- raise ValueError(
472
- "'crs' cannot be None when 'obj' is not GeoDataFrame/GeoSeries."
473
- )
474
477
  if hasattr(obj, "__len__") and not len(obj):
475
478
  return GeoDataFrame({"geometry": []}, crs=crs)
476
479
 
@@ -617,7 +620,7 @@ def bounds_to_polygon(
617
620
  GeoDataFrame of box polygons with length and index of 'gdf'.
618
621
 
619
622
  Examples:
620
- --------
623
+ ---------
621
624
  >>> import sgis as sg
622
625
  >>> gdf = sg.to_gdf([MultiPoint([(0, 0), (1, 1)]), Point(0, 0)])
623
626
  >>> gdf
@@ -652,8 +655,9 @@ def bounds_to_points(
652
655
  GeoDataFrame of multipoints with same length and index as 'gdf'.
653
656
 
654
657
  Examples:
655
- --------
658
+ ---------
656
659
  >>> import sgis as sg
660
+ >>> from shapely.geometry import MultiPoint, Point
657
661
  >>> gdf = sg.to_gdf([MultiPoint([(0, 0), (1, 1)]), Point(0, 0)])
658
662
  >>> gdf
659
663
  geometry
@@ -680,24 +684,17 @@ def get_total_bounds(
680
684
  for obj in geometries:
681
685
  try:
682
686
  minx, miny, maxx, maxy = to_bbox(obj)
687
+ xs += [minx, maxx]
688
+ ys += [miny, maxy]
683
689
  except Exception as e:
684
- if strict:
685
- raise e
686
- else:
687
- continue
688
- xs += [minx, maxx]
689
- ys += [miny, maxy]
690
+ try:
691
+ for x in obj:
692
+ minx, miny, maxx, maxy = to_bbox(x)
693
+ xs += [minx, maxx]
694
+ ys += [miny, maxy]
695
+ except Exception as e2:
696
+ if strict:
697
+ raise e2 from e
698
+ else:
699
+ continue
690
700
  return min(xs), min(ys), max(xs), max(ys)
691
-
692
-
693
- def points_in_bounds(gdf: GeoDataFrame | GeoSeries, n2: int) -> GeoDataFrame:
694
- """Get a GeoDataFrame of points within the bounds of the GeoDataFrame."""
695
- if not isinstance(gdf, (GeoDataFrame | GeoSeries)) and is_bbox_like(gdf):
696
- minx, miny, maxx, maxy = gdf
697
- else:
698
- minx, miny, maxx, maxy = gdf.total_bounds
699
- xs = np.linspace(minx, maxx, num=n2)
700
- ys = np.linspace(miny, maxy, num=n2)
701
- x_coords, y_coords = np.meshgrid(xs, ys, indexing="ij")
702
- coords = np.concatenate((x_coords.reshape(-1, 1), y_coords.reshape(-1, 1)), axis=1)
703
- return to_gdf(coords, crs=gdf.crs)
@@ -21,9 +21,12 @@ import numpy as np
21
21
  import pandas as pd
22
22
  from geopandas import GeoDataFrame
23
23
  from geopandas import GeoSeries
24
+ from shapely import get_num_geometries
24
25
 
25
- from .general import _merge_geometries
26
+ from ..parallel.parallel import Parallel
27
+ from .general import _grouped_unary_union
26
28
  from .general import _parallel_unary_union
29
+ from .general import _unary_union_for_notna
27
30
  from .geometry_types import make_all_singlepart
28
31
  from .polygon_operations import get_cluster_mapper
29
32
  from .polygon_operations import get_grouped_centroids
@@ -52,6 +55,7 @@ def buffdissexp(
52
55
  copy: bool = True,
53
56
  grid_size: float | int | None = None,
54
57
  n_jobs: int = 1,
58
+ join_style: int | str = "round",
55
59
  **dissolve_kwargs,
56
60
  ) -> GeoDataFrame:
57
61
  """Buffers and dissolves overlapping geometries.
@@ -71,6 +75,7 @@ def buffdissexp(
71
75
  copy: Whether to copy the GeoDataFrame before buffering. Defaults to True.
72
76
  grid_size: Rounding of the coordinates. Defaults to None.
73
77
  n_jobs: Number of threads to use. Defaults to 1.
78
+ join_style: Buffer join style.
74
79
  **dissolve_kwargs: additional keyword arguments passed to geopandas' dissolve.
75
80
 
76
81
  Returns:
@@ -85,6 +90,7 @@ def buffdissexp(
85
90
  copy=copy,
86
91
  grid_size=grid_size,
87
92
  n_jobs=n_jobs,
93
+ join_style=join_style,
88
94
  **dissolve_kwargs,
89
95
  )
90
96
 
@@ -99,6 +105,7 @@ def buffdiss(
99
105
  resolution: int = 50,
100
106
  copy: bool = True,
101
107
  n_jobs: int = 1,
108
+ join_style: int | str = "round",
102
109
  **dissolve_kwargs,
103
110
  ) -> GeoDataFrame:
104
111
  """Buffers and dissolves geometries.
@@ -114,6 +121,7 @@ def buffdiss(
114
121
  the geometry by
115
122
  resolution: The number of segments used to approximate a quarter circle.
116
123
  Here defaults to 50, as opposed to the default 16 in geopandas.
124
+ join_style: Buffer join style.
117
125
  copy: Whether to copy the GeoDataFrame before buffering. Defaults to True.
118
126
  n_jobs: Number of threads to use. Defaults to 1.
119
127
  **dissolve_kwargs: additional keyword arguments passed to geopandas' dissolve.
@@ -122,7 +130,7 @@ def buffdiss(
122
130
  A buffered GeoDataFrame where geometries are dissolved.
123
131
 
124
132
  Examples:
125
- --------
133
+ ---------
126
134
  Create some random points.
127
135
 
128
136
  >>> import sgis as sg
@@ -169,7 +177,9 @@ def buffdiss(
169
177
  1 b MULTIPOLYGON (((258404.858 6647830.931, 258404... 0.687635
170
178
  2 d MULTIPOLYGON (((258180.258 6647935.731, 258179... 0.580157
171
179
  """
172
- buffered = buff(gdf, distance, resolution=resolution, copy=copy)
180
+ buffered = buff(
181
+ gdf, distance, resolution=resolution, copy=copy, join_style=join_style
182
+ )
173
183
 
174
184
  return _dissolve(buffered, n_jobs=n_jobs, **dissolve_kwargs)
175
185
 
@@ -179,6 +189,7 @@ def _dissolve(
179
189
  aggfunc: str = "first",
180
190
  grid_size: None | float = None,
181
191
  n_jobs: int = 1,
192
+ as_index: bool = True,
182
193
  **dissolve_kwargs,
183
194
  ) -> GeoDataFrame:
184
195
 
@@ -187,6 +198,13 @@ def _dissolve(
187
198
 
188
199
  geom_col = gdf._geometry_column_name
189
200
 
201
+ gdf[geom_col] = gdf[geom_col].make_valid()
202
+
203
+ more_than_one = get_num_geometries(gdf.geometry.values) > 1
204
+ gdf.loc[more_than_one, geom_col] = gdf.loc[more_than_one, geom_col].apply(
205
+ _unary_union_for_notna
206
+ )
207
+
190
208
  by = dissolve_kwargs.pop("by", None)
191
209
 
192
210
  by_was_none = not bool(by)
@@ -200,7 +218,9 @@ def _dissolve(
200
218
  other_cols = list(gdf.columns.difference({geom_col} | set(by or {})))
201
219
 
202
220
  try:
203
- is_one_hit = gdf.groupby(by, **dissolve_kwargs).transform("size") == 1
221
+ is_one_hit = (
222
+ gdf.groupby(by, as_index=True, **dissolve_kwargs).transform("size") == 1
223
+ )
204
224
  except IndexError:
205
225
  # if no rows when dropna=True
206
226
  original_by = [x for x in by]
@@ -209,16 +229,17 @@ def _dissolve(
209
229
  query &= gdf[col].notna()
210
230
  gdf = gdf.loc[query]
211
231
  assert not len(gdf), gdf
212
- if not by_was_none and dissolve_kwargs.get("as_index", True):
232
+ if not by_was_none and as_index:
213
233
  try:
214
234
  gdf = gdf.set_index(original_by)
215
235
  except Exception as e:
216
236
  print(gdf)
217
237
  print(original_by)
218
238
  raise e
239
+
219
240
  return gdf
220
241
 
221
- if not by_was_none and dissolve_kwargs.get("as_index", True):
242
+ if not by_was_none and as_index:
222
243
  one_hit = gdf[is_one_hit].set_index(by)
223
244
  else:
224
245
  one_hit = gdf[is_one_hit]
@@ -227,14 +248,21 @@ def _dissolve(
227
248
  if not len(many_hits):
228
249
  return GeoDataFrame(one_hit, geometry=geom_col, crs=gdf.crs)
229
250
 
230
- dissolved = many_hits.groupby(by, **dissolve_kwargs)[other_cols].agg(aggfunc)
251
+ dissolved = many_hits.groupby(by, as_index=True, **dissolve_kwargs)[other_cols].agg(
252
+ aggfunc
253
+ )
231
254
 
232
255
  # dissolved = gdf.groupby(by, **dissolve_kwargs)[other_cols].agg(aggfunc)
233
256
 
234
257
  if n_jobs > 1:
235
258
  try:
236
259
  agged = _parallel_unary_union(
237
- many_hits, n_jobs=n_jobs, by=by, grid_size=grid_size, **dissolve_kwargs
260
+ many_hits,
261
+ n_jobs=n_jobs,
262
+ by=by,
263
+ grid_size=grid_size,
264
+ as_index=True,
265
+ **dissolve_kwargs,
238
266
  )
239
267
  dissolved[geom_col] = agged
240
268
  return GeoDataFrame(dissolved, geometry=geom_col, crs=gdf.crs)
@@ -242,21 +270,55 @@ def _dissolve(
242
270
  print(e, dissolved, agged, many_hits)
243
271
  raise e
244
272
 
245
- geoms_agged = many_hits.groupby(by, **dissolve_kwargs)[geom_col].agg(
246
- lambda x: _merge_geometries(x, grid_size=grid_size)
247
- )
273
+ # geoms_agged = many_hits.groupby(by, **dissolve_kwargs)[geom_col].agg(
274
+ # lambda x: _unary_union_for_notna(x, grid_size=grid_size)
275
+ # )
276
+ # print("\n\n\ngeomsagged\n", geoms_agged, geoms_agged.shape)
277
+ geoms_agged = _grouped_unary_union(many_hits, by, as_index=True, **dissolve_kwargs)
278
+ # print(geoms_agged, geoms_agged.shape)
248
279
 
249
- if not dissolve_kwargs.get("as_index"):
250
- try:
251
- geoms_agged = geoms_agged[geom_col]
252
- except KeyError:
253
- pass
280
+ # if not as_index:
281
+ # try:
282
+ # geoms_agged = geoms_agged[geom_col]
283
+ # except KeyError:
284
+ # pass
254
285
 
255
286
  dissolved[geom_col] = geoms_agged
256
287
 
257
- return GeoDataFrame(
258
- pd.concat([dissolved, one_hit]).sort_index(), geometry=geom_col, crs=gdf.crs
259
- )
288
+ if not as_index:
289
+ dissolved = dissolved.reset_index()
290
+ # else:
291
+ # one_hit = one_hit.set
292
+ # dissolved = dissolved.reset_index()
293
+
294
+ # from ..maps.maps import explore, explore_locals
295
+ # from .conversion import to_gdf
296
+
297
+ # try:
298
+ # explore(
299
+ # dissolved=to_gdf(dissolved, 25833),
300
+ # geoms_agged=to_gdf(geoms_agged, 25833),
301
+ # gdf=gdf,
302
+ # column="ARTYPE",
303
+ # )
304
+ # except Exception:
305
+ # explore(
306
+ # dissolved=to_gdf(dissolved, 25833),
307
+ # geoms_agged=to_gdf(geoms_agged, 25833),
308
+ # gdf=gdf,
309
+ # )
310
+
311
+ # from ..maps.maps import explore_locals
312
+ # from .conversion import to_gdf
313
+
314
+ # explore_locals()
315
+
316
+ try:
317
+ return GeoDataFrame(
318
+ pd.concat([dissolved, one_hit]).sort_index(), geometry=geom_col, crs=gdf.crs
319
+ )
320
+ except TypeError as e:
321
+ raise e.__class__(e, dissolved.index, one_hit.index) from e
260
322
 
261
323
 
262
324
  def diss(
@@ -351,7 +413,10 @@ def dissexp(
351
413
 
352
414
 
353
415
  def dissexp_by_cluster(
354
- gdf: GeoDataFrame, predicate: str | None = None, n_jobs: int = 1, **dissolve_kwargs
416
+ gdf: GeoDataFrame,
417
+ predicate: str | None = "intersects",
418
+ n_jobs: int = 1,
419
+ **dissolve_kwargs,
355
420
  ) -> GeoDataFrame:
356
421
  """Dissolves overlapping geometries through clustering with sjoin and networkx.
357
422
 
@@ -407,12 +472,14 @@ def diss_by_cluster(
407
472
  def _run_func_by_cluster(
408
473
  func: Callable,
409
474
  gdf: GeoDataFrame,
410
- predicate: str | None = None,
475
+ predicate: str | None = "intersects",
411
476
  n_jobs: int = 1,
412
477
  **dissolve_kwargs,
413
478
  ) -> GeoDataFrame:
414
479
  is_geoseries = isinstance(gdf, GeoSeries)
415
480
 
481
+ processes = dissolve_kwargs.pop("processes", 1)
482
+
416
483
  by = dissolve_kwargs.pop("by", [])
417
484
  if isinstance(by, str):
418
485
  by = [by]
@@ -425,22 +492,44 @@ def _run_func_by_cluster(
425
492
  def get_group_clusters(group: GeoDataFrame):
426
493
  """Adds cluster column. Applied to each group because much faster."""
427
494
  group = group.reset_index(drop=True)
428
- group["_cluster"] = get_cluster_mapper(
429
- group, predicate=predicate
430
- ) # component_mapper
495
+ group["_cluster"] = get_cluster_mapper(group, predicate=predicate)
431
496
  group["_cluster"] = get_grouped_centroids(group, groupby="_cluster")
432
497
  return group
433
498
 
499
+ gdf = make_all_singlepart(gdf)
500
+
434
501
  if by:
435
- dissolved = (
436
- make_all_singlepart(gdf)
437
- .groupby(by, group_keys=True, dropna=False, as_index=False)
438
- .apply(get_group_clusters)
439
- .pipe(func, by=["_cluster"] + by, n_jobs=n_jobs, **dissolve_kwargs)
440
- )
502
+ if processes == 1:
503
+ gdf = gdf.groupby(by, group_keys=False, dropna=False, as_index=False).apply(
504
+ get_group_clusters
505
+ )
506
+ else:
507
+ gdf = pd.concat(
508
+ Parallel(processes, backend="loky").map(
509
+ get_group_clusters,
510
+ [
511
+ gdf[lambda x: x[by].values == values]
512
+ for values in np.unique(gdf[by].values)
513
+ ],
514
+ ),
515
+ )
516
+ _by = ["_cluster"] + by
441
517
  else:
442
- dissolved = get_group_clusters(make_all_singlepart(gdf)).pipe(
443
- func, by="_cluster", n_jobs=n_jobs, **dissolve_kwargs
518
+ gdf = get_group_clusters(gdf)
519
+ _by = ["_cluster"]
520
+
521
+ if processes == 1:
522
+ dissolved = func(gdf, by=_by, n_jobs=n_jobs, **dissolve_kwargs)
523
+ else:
524
+ dissolved = pd.concat(
525
+ Parallel(processes, backend="loky").map(
526
+ func,
527
+ [
528
+ gdf[gdf["_cluster"] == cluster]
529
+ for cluster in gdf["_cluster"].unique()
530
+ ],
531
+ kwargs=dissolve_kwargs | {"n_jobs": n_jobs, "by": _by},
532
+ ),
444
533
  )
445
534
 
446
535
  if not by:
@@ -462,6 +551,7 @@ def buffdissexp_by_cluster(
462
551
  resolution: int = 50,
463
552
  copy: bool = True,
464
553
  n_jobs: int = 1,
554
+ join_style: int | str = "round",
465
555
  **dissolve_kwargs,
466
556
  ) -> GeoDataFrame:
467
557
  """Buffers and dissolves overlapping geometries.
@@ -480,6 +570,7 @@ def buffdissexp_by_cluster(
480
570
  the geometry by
481
571
  resolution: The number of segments used to approximate a quarter circle.
482
572
  Here defaults to 50, as opposed to the default 16 in geopandas.
573
+ join_style: Buffer join style.
483
574
  copy: Whether to copy the GeoDataFrame before buffering. Defaults to True.
484
575
  n_jobs: int = 1,
485
576
  **dissolve_kwargs: additional keyword arguments passed to geopandas' dissolve.
@@ -487,7 +578,13 @@ def buffdissexp_by_cluster(
487
578
  Returns:
488
579
  A buffered GeoDataFrame where overlapping geometries are dissolved.
489
580
  """
490
- buffered = buff(gdf, distance, resolution=resolution, copy=copy)
581
+ buffered = buff(
582
+ gdf,
583
+ distance,
584
+ resolution=resolution,
585
+ copy=copy,
586
+ join_style=join_style,
587
+ )
491
588
  return dissexp_by_cluster(buffered, n_jobs=n_jobs, **dissolve_kwargs)
492
589
 
493
590
 
@@ -496,6 +593,7 @@ def buff(
496
593
  distance: int | float,
497
594
  resolution: int = 50,
498
595
  copy: bool = True,
596
+ join_style: int | str = "round",
499
597
  **buffer_kwargs,
500
598
  ) -> GeoDataFrame:
501
599
  """Buffers a GeoDataFrame with high resolution and returns a new GeoDataFrame.
@@ -506,6 +604,7 @@ def buff(
506
604
  the geometry by
507
605
  resolution: The number of segments used to approximate a quarter circle.
508
606
  Here defaults to 50, as opposed to the default 16 in geopandas.
607
+ join_style: Buffer join style.
509
608
  copy: Whether to copy the GeoDataFrame before buffering. Defaults to True.
510
609
  **buffer_kwargs: additional keyword arguments passed to geopandas' buffer.
511
610
 
@@ -513,13 +612,15 @@ def buff(
513
612
  A buffered GeoDataFrame.
514
613
  """
515
614
  if isinstance(gdf, GeoSeries):
516
- return gdf.buffer(distance, resolution=resolution, **buffer_kwargs).make_valid()
615
+ return gdf.buffer(
616
+ distance, resolution=resolution, join_style=join_style, **buffer_kwargs
617
+ ).make_valid()
517
618
 
518
619
  if copy:
519
620
  gdf = gdf.copy()
520
621
 
521
622
  gdf[gdf._geometry_column_name] = gdf.buffer(
522
- distance, resolution=resolution, **buffer_kwargs
623
+ distance, resolution=resolution, join_style=join_style, **buffer_kwargs
523
624
  ).make_valid()
524
625
 
525
626
  return gdf