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.
- sgis/__init__.py +20 -9
- sgis/debug_config.py +24 -0
- sgis/exceptions.py +2 -2
- sgis/geopandas_tools/bounds.py +33 -36
- sgis/geopandas_tools/buffer_dissolve_explode.py +136 -35
- sgis/geopandas_tools/centerlines.py +4 -91
- sgis/geopandas_tools/cleaning.py +1576 -583
- sgis/geopandas_tools/conversion.py +38 -19
- sgis/geopandas_tools/duplicates.py +29 -8
- sgis/geopandas_tools/general.py +263 -100
- sgis/geopandas_tools/geometry_types.py +4 -4
- sgis/geopandas_tools/neighbors.py +19 -15
- sgis/geopandas_tools/overlay.py +2 -2
- sgis/geopandas_tools/point_operations.py +5 -5
- sgis/geopandas_tools/polygon_operations.py +510 -105
- sgis/geopandas_tools/polygons_as_rings.py +40 -8
- sgis/geopandas_tools/sfilter.py +29 -12
- sgis/helpers.py +3 -3
- sgis/io/dapla_functions.py +238 -19
- sgis/io/read_parquet.py +1 -1
- sgis/maps/examine.py +27 -12
- sgis/maps/explore.py +450 -65
- sgis/maps/legend.py +177 -76
- sgis/maps/map.py +206 -103
- sgis/maps/maps.py +178 -105
- sgis/maps/thematicmap.py +243 -83
- sgis/networkanalysis/_service_area.py +6 -1
- sgis/networkanalysis/closing_network_holes.py +2 -2
- sgis/networkanalysis/cutting_lines.py +15 -8
- sgis/networkanalysis/directednetwork.py +1 -1
- sgis/networkanalysis/finding_isolated_networks.py +15 -8
- sgis/networkanalysis/networkanalysis.py +17 -19
- sgis/networkanalysis/networkanalysisrules.py +1 -1
- sgis/networkanalysis/traveling_salesman.py +1 -1
- sgis/parallel/parallel.py +64 -27
- sgis/raster/__init__.py +0 -6
- sgis/raster/base.py +208 -0
- sgis/raster/cube.py +54 -8
- sgis/raster/image_collection.py +3257 -0
- sgis/raster/indices.py +17 -5
- sgis/raster/raster.py +138 -243
- sgis/raster/sentinel_config.py +120 -0
- sgis/raster/zonal.py +0 -1
- {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/METADATA +6 -7
- ssb_sgis-1.0.4.dist-info/RECORD +62 -0
- sgis/raster/methods_as_functions.py +0 -0
- sgis/raster/torchgeo.py +0 -171
- ssb_sgis-1.0.2.dist-info/RECORD +0 -61
- {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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(
|
|
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(
|
|
22
|
+
class ZeroLinesError(ValueError):
|
|
23
23
|
"""DataFrame has 0 rows."""
|
sgis/geopandas_tools/bounds.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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 .
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
443
|
-
|
|
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(
|
|
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(
|
|
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
|