ssb-sgis 0.3.9__tar.gz → 0.3.11__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.
Files changed (60) hide show
  1. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/PKG-INFO +1 -4
  2. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/pyproject.toml +4 -4
  3. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/__init__.py +13 -4
  4. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/geopandas_tools/bounds.py +236 -37
  5. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/geopandas_tools/buffer_dissolve_explode.py +41 -9
  6. ssb_sgis-0.3.11/src/sgis/geopandas_tools/cleaning.py +683 -0
  7. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/geopandas_tools/conversion.py +2 -2
  8. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/geopandas_tools/duplicates.py +22 -18
  9. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/geopandas_tools/general.py +87 -9
  10. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/geopandas_tools/overlay.py +12 -4
  11. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/geopandas_tools/polygon_operations.py +83 -8
  12. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/helpers.py +8 -0
  13. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/io/dapla_functions.py +9 -6
  14. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/maps/explore.py +76 -1
  15. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/maps/maps.py +11 -8
  16. ssb_sgis-0.3.9/src/sgis/geopandas_tools/cleaning.py +0 -331
  17. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/LICENSE +0 -0
  18. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/README.md +0 -0
  19. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/exceptions.py +0 -0
  20. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/geopandas_tools/__init__.py +0 -0
  21. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/geopandas_tools/centerlines.py +0 -0
  22. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/geopandas_tools/geocoding.py +0 -0
  23. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/geopandas_tools/geometry_types.py +0 -0
  24. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/geopandas_tools/neighbors.py +0 -0
  25. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/geopandas_tools/point_operations.py +0 -0
  26. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/geopandas_tools/polygons_as_rings.py +0 -0
  27. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/geopandas_tools/sfilter.py +53 -53
  28. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/io/_is_dapla.py +0 -0
  29. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/io/opener.py +0 -0
  30. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/io/read_parquet.py +0 -0
  31. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/io/write_municipality_data.py +0 -0
  32. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/maps/__init__.py +0 -0
  33. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/maps/examine.py +0 -0
  34. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/maps/httpserver.py +0 -0
  35. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/maps/legend.py +0 -0
  36. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/maps/map.py +0 -0
  37. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/maps/thematicmap.py +0 -0
  38. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/maps/tilesources.py +0 -0
  39. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/networkanalysis/__init__.py +0 -0
  40. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/networkanalysis/_get_route.py +0 -0
  41. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/networkanalysis/_od_cost_matrix.py +0 -0
  42. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/networkanalysis/_points.py +0 -0
  43. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/networkanalysis/_service_area.py +0 -0
  44. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/networkanalysis/closing_network_holes.py +0 -0
  45. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/networkanalysis/cutting_lines.py +0 -0
  46. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/networkanalysis/directednetwork.py +0 -0
  47. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/networkanalysis/finding_isolated_networks.py +0 -0
  48. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/networkanalysis/network.py +0 -0
  49. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/networkanalysis/networkanalysis.py +0 -0
  50. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/networkanalysis/networkanalysisrules.py +0 -0
  51. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/networkanalysis/nodes.py +0 -0
  52. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/networkanalysis/traveling_salesman.py +0 -0
  53. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/parallel/parallel.py +0 -0
  54. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/py.typed +0 -0
  55. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/raster/__init__.py +0 -0
  56. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/raster/base.py +0 -0
  57. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/raster/elevationraster.py +0 -0
  58. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/raster/raster.py +0 -0
  59. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/raster/sentinel.py +0 -0
  60. {ssb_sgis-0.3.9 → ssb_sgis-0.3.11}/src/sgis/raster/zonal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ssb-sgis
3
- Version: 0.3.9
3
+ Version: 0.3.11
4
4
  Summary: GIS functions used at Statistics Norway.
5
5
  Home-page: https://github.com/statisticsnorway/ssb-sgis
6
6
  License: MIT
@@ -24,18 +24,15 @@ Requires-Dist: jenkspy (>=0.3.2)
24
24
  Requires-Dist: mapclassify (>=2.5.0)
25
25
  Requires-Dist: matplotlib (>=3.7.0)
26
26
  Requires-Dist: networkx (>=3.0)
27
- Requires-Dist: numba (>=0.57.1,<0.58.0)
28
27
  Requires-Dist: numpy (>=1.24.2)
29
28
  Requires-Dist: pandas (>=1.5.3)
30
29
  Requires-Dist: pip (==23.2.1)
31
30
  Requires-Dist: pyarrow (>=11.0.0)
32
31
  Requires-Dist: rasterio (>=1.3.8,<2.0.0)
33
32
  Requires-Dist: requests (>=2.28.2)
34
- Requires-Dist: rioxarray (>=0.14.1,<0.15.0)
35
33
  Requires-Dist: rtree (>=1.0.1,<2.0.0)
36
34
  Requires-Dist: scikit-learn (>=1.2.1)
37
35
  Requires-Dist: shapely (>=2.0.1)
38
- Requires-Dist: xarray (==2023.8.0)
39
36
  Requires-Dist: xyzservices (>=2023.2.0)
40
37
  Project-URL: Changelog, https://github.com/statisticsnorway/ssb-sgis/releases
41
38
  Project-URL: Repository, https://github.com/statisticsnorway/ssb-sgis
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ssb-sgis"
3
- version = "0.3.9"
3
+ version = "0.3.11"
4
4
  description = "GIS functions used at Statistics Norway."
5
5
  authors = ["Statistics Norway <ort@ssb.no>"]
6
6
  license = "MIT"
@@ -37,9 +37,6 @@ ipython = ">=8.13.2"
37
37
  rtree = "^1.0.1"
38
38
  geocoder = "^1.38.1"
39
39
  rasterio = "^1.3.8"
40
- xarray = "2023.8.0"
41
- rioxarray = "^0.14.1"
42
- numba = "^0.57.1"
43
40
  pip = "23.2.1"
44
41
 
45
42
  [tool.poetry.group.dev.dependencies]
@@ -47,6 +44,9 @@ black = {extras = ["d", "jupyter"], version = ">=23.1.0"}
47
44
  coverage = {extras = ["toml"], version = ">=7.2.1"}
48
45
  darglint = ">=1.8.1"
49
46
  deptry = ">=0.8.0"
47
+ numba = "^0.57.1"
48
+ xarray = "2023.8.0"
49
+ rioxarray = "^0.14.1"
50
50
  flake8 = ">=6.0.0"
51
51
  flake8-bandit = ">=4.1.1"
52
52
  flake8-bugbear = ">=23.2.13"
@@ -1,4 +1,5 @@
1
1
  from .geopandas_tools.bounds import (
2
+ Gridlooper,
2
3
  bounds_to_points,
3
4
  bounds_to_polygon,
4
5
  get_total_bounds,
@@ -18,11 +19,15 @@ from .geopandas_tools.buffer_dissolve_explode import (
18
19
  dissexp_by_cluster,
19
20
  )
20
21
  from .geopandas_tools.centerlines import get_rough_centerlines
21
- from .geopandas_tools.cleaning import coverage_clean, remove_spikes
22
+ from .geopandas_tools.cleaning import (
23
+ coverage_clean,
24
+ remove_spikes,
25
+ split_spiky_polygons,
26
+ )
22
27
  from .geopandas_tools.conversion import (
23
28
  coordinate_array,
24
- get_lonlat,
25
- get_utm33,
29
+ from_4326,
30
+ to_4326,
26
31
  to_gdf,
27
32
  to_geoseries,
28
33
  to_shapely,
@@ -38,9 +43,12 @@ from .geopandas_tools.general import (
38
43
  get_common_crs,
39
44
  get_grouped_centroids,
40
45
  random_points,
46
+ random_points_in_polygons,
41
47
  rename_geometry_if,
42
48
  sort_large_first,
43
49
  sort_long_first,
50
+ sort_short_first,
51
+ sort_small_first,
44
52
  to_lines,
45
53
  )
46
54
  from .geopandas_tools.geocoding import address_to_coords, address_to_gdf
@@ -63,6 +71,7 @@ from .geopandas_tools.polygon_operations import (
63
71
  PolygonsAsRings,
64
72
  close_all_holes,
65
73
  close_small_holes,
74
+ close_thin_holes,
66
75
  eliminate_by_largest,
67
76
  eliminate_by_longest,
68
77
  eliminate_by_smallest,
@@ -116,7 +125,7 @@ from .raster.sentinel import Sentinel2
116
125
 
117
126
 
118
127
  try:
119
- from .io.dapla_functions import check_files, exists, read_geopandas, write_geopandas
128
+ from .io.dapla_functions import check_files, read_geopandas, write_geopandas
120
129
  from .io.write_municipality_data import write_municipality_data
121
130
  except ImportError:
122
131
  pass
@@ -1,28 +1,178 @@
1
+ import functools
1
2
  import numbers
2
- from typing import Any
3
3
  from collections.abc import Callable, Collection, Mapping
4
+ from dataclasses import dataclass
5
+ from typing import Any
4
6
 
5
7
  import geopandas as gpd
6
8
  import numpy as np
9
+ import pandas as pd
7
10
  from geopandas import GeoDataFrame, GeoSeries
8
11
  from pandas.api.types import is_dict_like
9
12
  from shapely import Geometry, box, extract_unique_points
10
13
  from shapely.geometry import Polygon
11
14
 
15
+ from ..parallel.parallel import Parallel
12
16
  from .conversion import to_gdf
13
17
  from .general import clean_clip, is_bbox_like
14
18
 
15
19
 
20
+ @dataclass
21
+ class Gridlooper:
22
+ """Run functions in a loop cellwise based on a grid.
23
+
24
+ Args:
25
+ gridsize: Size of the grid cells in units of the crs (meters, degrees).
26
+ mask: Geometry object to create a grid around.
27
+ gridbuffer: Units to buffer each gridcell by. For edge cases.
28
+ Defaults to 0.
29
+ clip: If True (default) geometries are clipped by the grid cells.
30
+ If False, all geometries that intersect will be selected in each iteration.
31
+ verbose: Whether to print progress. Defaults to False.
32
+ keep_geom_type: Whether to keep only the input geometry types after clipping.
33
+ Defaults to True.
34
+
35
+ Examples
36
+ --------
37
+
38
+ Get some points and some polygons.
39
+
40
+ >>> import sgis as sg
41
+ >>> points = sg.read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/points_oslo.parquet")
42
+ >>> points["idx"] = points.index
43
+ >>> buffered = sg.buff(points, 100)
44
+ >>> buffered
45
+ idx geometry
46
+ 0 0 POLYGON ((263222.700 6651184.900, 263222.651 6...
47
+ 1 1 POLYGON ((272556.100 6653369.500, 272556.051 6...
48
+ 2 2 POLYGON ((270182.300 6653032.700, 270182.251 6...
49
+ 3 3 POLYGON ((259904.800 6650339.700, 259904.751 6...
50
+ 4 4 POLYGON ((272976.200 6652889.100, 272976.151 6...
51
+ .. ... ...
52
+ 995 995 POLYGON ((266901.700 6647844.500, 266901.651 6...
53
+ 996 996 POLYGON ((261374.000 6653593.400, 261373.951 6...
54
+ 997 997 POLYGON ((263642.900 6645427.000, 263642.851 6...
55
+ 998 998 POLYGON ((269326.700 6650628.000, 269326.651 6...
56
+ 999 999 POLYGON ((264670.300 6644239.500, 264670.251 6...
57
+
58
+ [1000 rows x 2 columns]
59
+
60
+ Instantiate a gridlooper.
61
+
62
+ >>> looper = sg.Gridlooper(gridsize=200, mask=buffered, parallelizer=sg.Parallel(1, backend="multiprocessing"))
63
+
64
+ Run the function clean_overlay in a gridloop.
65
+
66
+ >>> resultslist = looper.run(
67
+ ... sg.clean_overlay,
68
+ ... points,
69
+ ... buffered,
70
+ ... )
71
+ >>> type(resultslist)
72
+ list
73
+
74
+ >>> results = pd.concat(resultslist, ignore_index=True)
75
+ >>> results
76
+ idx_1 idx_2 geometry
77
+ 0 220 220 POINT (254575.200 6661631.500)
78
+ 1 735 735 POINT (256337.400 6649931.700)
79
+ 2 575 575 POINT (256369.200 6650413.300)
80
+ 3 39 39 POINT (256142.300 6650526.300)
81
+ 4 235 235 POINT (256231.300 6650720.200)
82
+ ... ... ... ...
83
+ 1481 711 795 POINT (272845.500 6655048.800)
84
+ 1482 711 711 POINT (272845.500 6655048.800)
85
+ 1483 757 757 POINT (273507.600 6652806.600)
86
+ 1484 457 457 POINT (273524.400 6652979.900)
87
+ 1485 284 284 POINT (273650.800 6653000.500)
88
+
89
+ [1486 rows x 3 columns]
90
+
91
+ """
92
+
93
+ gridsize: int
94
+ mask: GeoDataFrame | GeoSeries | Geometry
95
+ gridbuffer: int = 0
96
+ clip: bool = True
97
+ keep_geom_type: bool = True
98
+ verbose: bool = False
99
+ parallelizer: Parallel | None = None
100
+
101
+ def __post_init__(self):
102
+ if not isinstance(self.mask, GeoDataFrame):
103
+ self.mask = to_gdf(self.mask)
104
+
105
+ def run(self, func: Callable, *args, **kwargs):
106
+ intersects_mask = lambda df: df.index.isin(df.sjoin(self.mask).index)
107
+ grid: GeoSeries = (
108
+ make_grid(self.mask, gridsize=self.gridsize).loc[intersects_mask].geometry
109
+ )
110
+
111
+ n = len(grid)
112
+
113
+ buffered_grid = grid.buffer(self.gridbuffer, resolution=1, join_style=2)
114
+
115
+ if self.parallelizer is not None:
116
+ func_with_clip = functools.partial(
117
+ _clip_and_run_func,
118
+ func=func,
119
+ args=args,
120
+ kwargs=kwargs,
121
+ keep_geom_type=self.keep_geom_type,
122
+ clip=self.clip,
123
+ )
124
+ results = self.parallelizer.map(func_with_clip, buffered_grid)
125
+ if not self.gridbuffer or not self.clip:
126
+ return results
127
+ out = []
128
+ for cell_res, unbuffered in zip(results, grid, strict=True):
129
+ out.append(
130
+ _clip_back_to_unbuffered_grid(
131
+ cell_res, unbuffered, self.keep_geom_type
132
+ )
133
+ )
134
+ return out
135
+
136
+ results = []
137
+ for i, (unbuffered, buffered) in enumerate(zip(grid, buffered_grid)):
138
+ cell_kwargs = {
139
+ key: _clip_if_isinstance(
140
+ value, buffered, self.keep_geom_type, self.clip
141
+ )
142
+ for key, value in kwargs.items()
143
+ }
144
+ cell_args = tuple(
145
+ _clip_if_isinstance(value, buffered, self.keep_geom_type, self.clip)
146
+ for value in args
147
+ )
148
+
149
+ cell_res = func(*cell_args, **cell_kwargs)
150
+
151
+ # clip back to original
152
+ if self.gridbuffer and self.clip:
153
+ cell_res = _clip_back_to_unbuffered_grid(
154
+ cell_res, unbuffered, self.keep_geom_type
155
+ )
156
+
157
+ results.append(cell_res)
158
+
159
+ if self.verbose:
160
+ print(f"Done with {i+1} of {n} grid cells", end="\r")
161
+
162
+ return results
163
+
164
+
16
165
  def gridloop(
17
166
  func: Callable,
18
- mask: GeoDataFrame | GeoSeries | Geometry,
19
167
  gridsize: int,
168
+ mask: GeoDataFrame | GeoSeries | Geometry,
20
169
  gridbuffer: int = 0,
21
170
  clip: bool = True,
22
171
  keep_geom_type: bool = True,
23
172
  verbose: bool = False,
24
173
  args: tuple | None = None,
25
174
  kwargs: dict | None = None,
175
+ parallelizer: Parallel | None = None,
26
176
  ) -> list[Any]:
27
177
  """Runs a function in a loop cellwise based on a grid.
28
178
 
@@ -108,9 +258,6 @@ def gridloop(
108
258
  [1486 rows x 3 columns]
109
259
 
110
260
  """
111
- if not isinstance(mask, GeoDataFrame):
112
- mask = to_gdf(mask)
113
-
114
261
  if kwargs is None:
115
262
  kwargs = {}
116
263
  elif not isinstance(kwargs, dict):
@@ -121,49 +268,52 @@ def gridloop(
121
268
  elif not isinstance(args, tuple):
122
269
  raise TypeError("args should be a tuple")
123
270
 
271
+ if not isinstance(mask, GeoDataFrame):
272
+ mask = to_gdf(mask)
273
+
124
274
  intersects_mask = lambda df: df.index.isin(df.sjoin(mask).index)
125
275
  grid: GeoSeries = make_grid(mask, gridsize=gridsize).loc[intersects_mask].geometry
126
276
 
127
- if verbose:
128
- n = len(grid)
129
-
130
- def clip_if_isinstance(value, cell, keep_geom_type):
131
- if not isinstance(value, (gpd.GeoDataFrame, gpd.GeoSeries, Geometry)):
132
- return value
133
-
134
- if isinstance(value, (gpd.GeoDataFrame, gpd.GeoSeries)):
135
- if clip:
136
- return clean_clip(value, cell, keep_geom_type=keep_geom_type)
137
- return value.loc[value.intersects(cell)]
277
+ n = len(grid)
138
278
 
139
- return value.intersection(cell).make_valid()
279
+ buffered_grid = grid.buffer(gridbuffer, resolution=1, join_style=2)
140
280
 
141
- buffered = grid.buffer(gridbuffer, resolution=1, join_style=2)
281
+ if parallelizer is not None:
282
+ func_with_clip = functools.partial(
283
+ _clip_and_run_func,
284
+ func=func,
285
+ args=args,
286
+ kwargs=kwargs,
287
+ keep_geom_type=keep_geom_type,
288
+ clip=clip,
289
+ )
290
+ results = parallelizer.map(func_with_clip, buffered_grid)
291
+ if not gridbuffer or not clip:
292
+ return results
293
+ out = []
294
+ for cell_res, unbuffered in zip(results, grid, strict=True):
295
+ out.append(
296
+ _clip_back_to_unbuffered_grid(cell_res, unbuffered, keep_geom_type)
297
+ )
298
+ return out
142
299
 
143
300
  results = []
144
- for i, (cell, buffered) in enumerate(zip(grid, buffered)):
145
- cell_kwargs = {}
146
- for key, value in kwargs.items():
147
- value = clip_if_isinstance(value, buffered, keep_geom_type)
148
- cell_kwargs[key] = value
149
-
150
- cell_args = ()
151
- for arg in args:
152
- arg = clip_if_isinstance(arg, buffered, keep_geom_type)
153
- cell_args = cell_args + (arg,)
301
+ for i, (unbuffered, buffered) in enumerate(zip(grid, buffered_grid)):
302
+ cell_kwargs = {
303
+ key: _clip_if_isinstance(value, buffered, keep_geom_type, clip)
304
+ for key, value in kwargs.items()
305
+ }
306
+ cell_args = tuple(
307
+ _clip_if_isinstance(value, buffered, keep_geom_type, clip) for value in args
308
+ )
154
309
 
155
310
  cell_res = func(*cell_args, **cell_kwargs)
156
311
 
157
312
  # clip back to original
158
313
  if gridbuffer and clip:
159
- if isinstance(cell_res, (gpd.GeoDataFrame, gpd.GeoSeries, Geometry)):
160
- cell_res = clip_if_isinstance(cell_res, cell, keep_geom_type)
161
- else:
162
- try:
163
- for res in cell_res:
164
- res = clip_if_isinstance(res, cell, keep_geom_type)
165
- except TypeError:
166
- pass
314
+ cell_res = _clip_back_to_unbuffered_grid(
315
+ cell_res, unbuffered, keep_geom_type
316
+ )
167
317
 
168
318
  results.append(cell_res)
169
319
 
@@ -173,6 +323,56 @@ def gridloop(
173
323
  return results
174
324
 
175
325
 
326
+ def _clip_and_run_func(
327
+ grid_cell: Polygon,
328
+ func: Callable,
329
+ args: tuple,
330
+ kwargs: dict,
331
+ keep_geom_type: bool,
332
+ clip: bool,
333
+ ):
334
+ cell_args = tuple(
335
+ _clip_if_isinstance(value, grid_cell, keep_geom_type, clip) for value in args
336
+ )
337
+ cell_kwargs = {
338
+ key: _clip_if_isinstance(value, grid_cell, keep_geom_type, clip)
339
+ for key, value in kwargs.items()
340
+ }
341
+
342
+ return func(*cell_args, **cell_kwargs)
343
+
344
+
345
+ def _clip_if_isinstance(value, cell, keep_geom_type, clip: bool):
346
+ if not isinstance(value, (gpd.GeoDataFrame, gpd.GeoSeries, Geometry)):
347
+ return value
348
+
349
+ if isinstance(value, (gpd.GeoDataFrame, gpd.GeoSeries)):
350
+ if clip:
351
+ return clean_clip(value, cell, keep_geom_type=keep_geom_type)
352
+ return value.loc[value.intersects(cell)]
353
+
354
+ return value.intersection(cell).make_valid()
355
+
356
+
357
+ def _clip_back_to_unbuffered_grid(results, mask, keep_geom_type):
358
+ if isinstance(results, (gpd.GeoDataFrame, gpd.GeoSeries, Geometry)):
359
+ return _clip_if_isinstance(results, mask, keep_geom_type, clip=True)
360
+ elif isinstance(results, (pd.DataFrame, pd.Series, np.ndarray)):
361
+ return results
362
+ try:
363
+ for key, value in results.items():
364
+ results[key] = _clip_if_isinstance(value, mask, keep_geom_type, clip=True)
365
+ except AttributeError:
366
+ try:
367
+ return [
368
+ _clip_if_isinstance(res, mask, keep_geom_type, clip=True)
369
+ for res in results
370
+ ]
371
+ except TypeError:
372
+ pass
373
+ return results
374
+
375
+
176
376
  def make_grid_from_bbox(
177
377
  minx: int | float,
178
378
  miny: int | float,
@@ -506,7 +706,6 @@ def get_total_bounds(
506
706
  minx, miny, maxx, maxy = to_bbox(obj)
507
707
  xs += [minx, maxx]
508
708
  ys += [miny, maxy]
509
-
510
709
  return min(xs), min(ys), max(xs), max(ys)
511
710
 
512
711
 
@@ -14,7 +14,9 @@ for the following:
14
14
  - The buff function returns a GeoDataFrame, the geopandas method returns a GeoSeries.
15
15
  """
16
16
 
17
+ import numpy as np
17
18
  from geopandas import GeoDataFrame, GeoSeries
19
+ from shapely import make_valid, unary_union
18
20
 
19
21
  from .general import _push_geom_col
20
22
  from .geometry_types import make_all_singlepart
@@ -46,6 +48,7 @@ def buffdissexp(
46
48
  resolution: int = 50,
47
49
  index_parts: bool = False,
48
50
  copy: bool = True,
51
+ grid_size: float | int | None = None,
49
52
  **dissolve_kwargs,
50
53
  ) -> GeoDataFrame:
51
54
  """Buffers and dissolves overlapping geometries.
@@ -75,6 +78,7 @@ def buffdissexp(
75
78
  distance,
76
79
  resolution=resolution,
77
80
  copy=copy,
81
+ grid_size=grid_size,
78
82
  **dissolve_kwargs,
79
83
  )
80
84
 
@@ -159,11 +163,43 @@ def buffdiss(
159
163
  """
160
164
  buffered = buff(gdf, distance, resolution=resolution, copy=copy)
161
165
 
162
- dissolved = buffered.dissolve(**dissolve_kwargs)
166
+ return _dissolve(buffered, **dissolve_kwargs)
163
167
 
164
- dissolved[gdf._geometry_column_name] = dissolved.make_valid()
165
168
 
166
- return dissolved
169
+ def _dissolve(gdf, aggfunc="first", grid_size=None, **dissolve_kwargs):
170
+ geom_col = gdf._geometry_column_name
171
+ if grid_size is None:
172
+ dissolved = gdf.dissolve(aggfunc=aggfunc, **dissolve_kwargs)
173
+
174
+ dissolved[geom_col] = dissolved.make_valid()
175
+ return dissolved
176
+
177
+ def merge_geometries(x):
178
+ return make_valid(unary_union(x, grid_size=grid_size))
179
+
180
+ geom_col = gdf._geometry_column_name
181
+
182
+ by = dissolve_kwargs.pop("by", None)
183
+
184
+ if by is None and dissolve_kwargs.get("level") is None:
185
+ by = np.zeros(len(gdf), dtype="int64")
186
+ other_cols = list(gdf.columns.difference({geom_col}))
187
+ else:
188
+ if isinstance(by, str):
189
+ by = [by]
190
+ other_cols = list(gdf.columns.difference({geom_col} | set(by)))
191
+
192
+ dissolved = gdf.groupby(by, **dissolve_kwargs)[other_cols].agg(aggfunc)
193
+ geoms_agged = gdf.groupby(by, **dissolve_kwargs)[geom_col].agg(merge_geometries)
194
+
195
+ if not dissolve_kwargs.get("as_index"):
196
+ try:
197
+ geoms_agged = geoms_agged[geom_col]
198
+ except KeyError:
199
+ pass
200
+ dissolved[geom_col] = geoms_agged
201
+
202
+ return GeoDataFrame(dissolved, geometry=geom_col, crs=gdf.crs)
167
203
 
168
204
 
169
205
  def dissexp(
@@ -172,6 +208,7 @@ def dissexp(
172
208
  aggfunc="first",
173
209
  as_index: bool = True,
174
210
  index_parts: bool = False,
211
+ grid_size: float | int | None = None,
175
212
  **dissolve_kwargs,
176
213
  ):
177
214
  """Dissolves overlapping geometries.
@@ -191,19 +228,14 @@ def dissexp(
191
228
  Returns:
192
229
  A GeoDataFrame where overlapping geometries are dissolved.
193
230
  """
194
- geom_col = gdf._geometry_column_name
195
-
196
231
  dissolve_kwargs = dissolve_kwargs | {
197
232
  "by": by,
198
- "aggfunc": aggfunc,
199
233
  "as_index": as_index,
200
234
  }
201
235
 
202
236
  dissolve_kwargs, ignore_index = _decide_ignore_index(dissolve_kwargs)
203
237
 
204
- dissolved = gdf.dissolve(**dissolve_kwargs)
205
-
206
- dissolved[geom_col] = dissolved.make_valid()
238
+ dissolved = _dissolve(gdf, aggfunc=aggfunc, grid_size=grid_size, **dissolve_kwargs)
207
239
 
208
240
  return make_all_singlepart(
209
241
  dissolved, ignore_index=ignore_index, index_parts=index_parts