ssb-sgis 0.2.6__py3-none-any.whl → 0.2.8__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 +4 -0
- sgis/geopandas_tools/buffer_dissolve_explode.py +26 -0
- sgis/geopandas_tools/general.py +9 -2
- sgis/geopandas_tools/polygon_operations.py +265 -195
- sgis/geopandas_tools/to_geodataframe.py +81 -5
- sgis/helpers.py +3 -3
- sgis/maps/examine.py +192 -0
- sgis/maps/explore.py +35 -2
- sgis/maps/map.py +2 -0
- {ssb_sgis-0.2.6.dist-info → ssb_sgis-0.2.8.dist-info}/METADATA +1 -1
- {ssb_sgis-0.2.6.dist-info → ssb_sgis-0.2.8.dist-info}/RECORD +13 -12
- {ssb_sgis-0.2.6.dist-info → ssb_sgis-0.2.8.dist-info}/LICENSE +0 -0
- {ssb_sgis-0.2.6.dist-info → ssb_sgis-0.2.8.dist-info}/WHEEL +0 -0
sgis/__init__.py
CHANGED
|
@@ -3,7 +3,9 @@ from .geopandas_tools.buffer_dissolve_explode import (
|
|
|
3
3
|
buff,
|
|
4
4
|
buffdiss,
|
|
5
5
|
buffdissexp,
|
|
6
|
+
buffdissexp_by_cluster,
|
|
6
7
|
dissexp,
|
|
8
|
+
dissexp_by_cluster,
|
|
7
9
|
)
|
|
8
10
|
from .geopandas_tools.general import (
|
|
9
11
|
bounds_to_points,
|
|
@@ -23,6 +25,7 @@ from .geopandas_tools.general import (
|
|
|
23
25
|
from .geopandas_tools.geometry_types import (
|
|
24
26
|
get_geom_type,
|
|
25
27
|
is_single_geom_type,
|
|
28
|
+
make_all_singlepart,
|
|
26
29
|
to_single_geom_type,
|
|
27
30
|
)
|
|
28
31
|
from .geopandas_tools.neighbors import (
|
|
@@ -46,6 +49,7 @@ from .geopandas_tools.polygon_operations import (
|
|
|
46
49
|
)
|
|
47
50
|
from .geopandas_tools.to_geodataframe import to_gdf
|
|
48
51
|
from .helpers import get_name
|
|
52
|
+
from .maps.examine import Examine
|
|
49
53
|
from .maps.explore import Explore
|
|
50
54
|
from .maps.httpserver import run_html_server
|
|
51
55
|
from .maps.legend import Legend
|
|
@@ -17,6 +17,7 @@ for the following:
|
|
|
17
17
|
from geopandas import GeoDataFrame, GeoSeries
|
|
18
18
|
|
|
19
19
|
from .geometry_types import make_all_singlepart
|
|
20
|
+
from .polygon_operations import get_polygon_clusters
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _decide_ignore_index(kwargs: dict) -> tuple[dict, bool]:
|
|
@@ -190,6 +191,31 @@ def dissexp(
|
|
|
190
191
|
)
|
|
191
192
|
|
|
192
193
|
|
|
194
|
+
def dissexp_by_cluster(gdf: GeoDataFrame) -> GeoDataFrame:
|
|
195
|
+
return (
|
|
196
|
+
gdf.explode(ignore_index=True)
|
|
197
|
+
.pipe(get_polygon_clusters, cluster_col="cluster")
|
|
198
|
+
.pipe(dissexp, by="cluster")
|
|
199
|
+
.reset_index(drop=True)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def buffdissexp_by_cluster(
|
|
204
|
+
gdf: GeoDataFrame,
|
|
205
|
+
distance: int | float,
|
|
206
|
+
*,
|
|
207
|
+
resolution: int = 50,
|
|
208
|
+
copy: bool = True,
|
|
209
|
+
) -> GeoDataFrame:
|
|
210
|
+
return (
|
|
211
|
+
buff(gdf, distance, resolution=resolution, copy=copy)
|
|
212
|
+
.explode(ignore_index=True)
|
|
213
|
+
.pipe(get_polygon_clusters, cluster_col="cluster")
|
|
214
|
+
.pipe(dissexp, by="cluster")
|
|
215
|
+
.reset_index(drop=True)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
193
219
|
def buff(
|
|
194
220
|
gdf: GeoDataFrame | GeoSeries,
|
|
195
221
|
distance: int | float,
|
sgis/geopandas_tools/general.py
CHANGED
|
@@ -2,6 +2,7 @@ import warnings
|
|
|
2
2
|
|
|
3
3
|
import geopandas as gpd
|
|
4
4
|
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
5
6
|
from geopandas import GeoDataFrame, GeoSeries
|
|
6
7
|
from geopandas.array import GeometryDtype
|
|
7
8
|
from shapely import (
|
|
@@ -519,11 +520,17 @@ def to_lines(*gdfs: GeoDataFrame, copy: bool = True) -> GeoDataFrame:
|
|
|
519
520
|
if len(lines) == 1:
|
|
520
521
|
return lines[0]
|
|
521
522
|
|
|
522
|
-
|
|
523
|
+
if len(lines[0]) and len(lines[1]):
|
|
524
|
+
unioned = lines[0].overlay(lines[1], how="union", keep_geom_type=True)
|
|
525
|
+
else:
|
|
526
|
+
unioned = pd.concat([lines[0], lines[1]], ignore_index=True)
|
|
523
527
|
|
|
524
528
|
if len(lines) > 2:
|
|
525
529
|
for line_gdf in lines[2:]:
|
|
526
|
-
|
|
530
|
+
if len(line_gdf):
|
|
531
|
+
unioned = unioned.overlay(line_gdf, how="union", keep_geom_type=True)
|
|
532
|
+
else:
|
|
533
|
+
unioned = pd.concat([unioned, line_gdf], ignore_index=True)
|
|
527
534
|
|
|
528
535
|
return make_all_singlepart(unioned, ignore_index=True)
|
|
529
536
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Functions for polygon geometries."""
|
|
2
|
+
import functools
|
|
2
3
|
import warnings
|
|
3
4
|
|
|
4
5
|
import geopandas as gpd
|
|
@@ -8,6 +9,7 @@ import pandas as pd
|
|
|
8
9
|
from geopandas import GeoDataFrame, GeoSeries
|
|
9
10
|
from shapely import (
|
|
10
11
|
area,
|
|
12
|
+
difference,
|
|
11
13
|
get_exterior_ring,
|
|
12
14
|
get_interior_ring,
|
|
13
15
|
get_num_interior_rings,
|
|
@@ -21,6 +23,147 @@ from .neighbors import get_neighbor_indices
|
|
|
21
23
|
from .overlay import clean_overlay
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
def get_polygon_clusters(
|
|
27
|
+
*gdfs: GeoDataFrame | GeoSeries,
|
|
28
|
+
cluster_col: str = "cluster",
|
|
29
|
+
allow_multipart: bool = False,
|
|
30
|
+
) -> GeoDataFrame | tuple[GeoDataFrame]:
|
|
31
|
+
"""Find which polygons overlap without dissolving.
|
|
32
|
+
|
|
33
|
+
Devides polygons into clusters in a fast and precice manner by using spatial join
|
|
34
|
+
and networkx to find the connected components, i.e. overlapping geometries.
|
|
35
|
+
If multiple GeoDataFrames are given, the clusters will be based on all
|
|
36
|
+
combined.
|
|
37
|
+
|
|
38
|
+
This can be used instead of dissolve+explode, or before dissolving by the cluster
|
|
39
|
+
column. This has been tested to be a lot faster if there are many
|
|
40
|
+
non-overlapping polygons, but somewhat slower than dissolve+explode if most
|
|
41
|
+
polygons overlap.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
gdfs: One or more GeoDataFrames of polygons.
|
|
45
|
+
cluster_col: Name of the resulting cluster column.
|
|
46
|
+
allow_multipart: Whether to allow mutipart geometries in the gdfs.
|
|
47
|
+
Defaults to False to avoid confusing results.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
One or more GeoDataFrames (same amount as was given) with a new cluster column.
|
|
51
|
+
|
|
52
|
+
Examples
|
|
53
|
+
--------
|
|
54
|
+
|
|
55
|
+
Create geometries with three clusters of overlapping polygons.
|
|
56
|
+
|
|
57
|
+
>>> import sgis as sg
|
|
58
|
+
>>> gdf = sg.to_gdf([(0, 0), (1, 1), (0, 1), (4, 4), (4, 3), (7, 7)])
|
|
59
|
+
>>> buffered = sg.buff(gdf, 1)
|
|
60
|
+
>>> gdf
|
|
61
|
+
geometry
|
|
62
|
+
0 POLYGON ((1.00000 0.00000, 0.99951 -0.03141, 0...
|
|
63
|
+
1 POLYGON ((2.00000 1.00000, 1.99951 0.96859, 1....
|
|
64
|
+
2 POLYGON ((1.00000 1.00000, 0.99951 0.96859, 0....
|
|
65
|
+
3 POLYGON ((5.00000 4.00000, 4.99951 3.96859, 4....
|
|
66
|
+
4 POLYGON ((5.00000 3.00000, 4.99951 2.96859, 4....
|
|
67
|
+
5 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
|
|
68
|
+
|
|
69
|
+
Add a cluster column to the GeoDataFrame:
|
|
70
|
+
|
|
71
|
+
>>> gdf = sg.get_polygon_clusters(gdf, cluster_col="cluster")
|
|
72
|
+
>>> gdf
|
|
73
|
+
cluster geometry
|
|
74
|
+
0 0 POLYGON ((1.00000 0.00000, 0.99951 -0.03141, 0...
|
|
75
|
+
1 0 POLYGON ((2.00000 1.00000, 1.99951 0.96859, 1....
|
|
76
|
+
2 0 POLYGON ((1.00000 1.00000, 0.99951 0.96859, 0....
|
|
77
|
+
3 1 POLYGON ((5.00000 4.00000, 4.99951 3.96859, 4....
|
|
78
|
+
4 1 POLYGON ((5.00000 3.00000, 4.99951 2.96859, 4....
|
|
79
|
+
5 2 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
|
|
80
|
+
|
|
81
|
+
If multiple GeoDataFrames are given, all are returned with common
|
|
82
|
+
cluster values.
|
|
83
|
+
|
|
84
|
+
>>> gdf2 = sg.to_gdf([(0, 0), (7, 7)])
|
|
85
|
+
>>> gdf, gdf2 = sg.get_polygon_clusters(gdf, gdf2, cluster_col="cluster")
|
|
86
|
+
>>> gdf2
|
|
87
|
+
cluster geometry
|
|
88
|
+
0 0 POINT (0.00000 0.00000)
|
|
89
|
+
1 2 POINT (7.00000 7.00000)
|
|
90
|
+
>>> gdf
|
|
91
|
+
cluster geometry
|
|
92
|
+
0 0 POLYGON ((1.00000 0.00000, 0.99951 -0.03141, 0...
|
|
93
|
+
1 0 POLYGON ((2.00000 1.00000, 1.99951 0.96859, 1....
|
|
94
|
+
2 0 POLYGON ((1.00000 1.00000, 0.99951 0.96859, 0....
|
|
95
|
+
3 1 POLYGON ((5.00000 4.00000, 4.99951 3.96859, 4....
|
|
96
|
+
4 1 POLYGON ((5.00000 3.00000, 4.99951 2.96859, 4....
|
|
97
|
+
5 2 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
|
|
98
|
+
|
|
99
|
+
Dissolving 'by' the cluster column will make the dissolve much
|
|
100
|
+
faster if there are a lot of non-overlapping polygons.
|
|
101
|
+
|
|
102
|
+
>>> dissolved = gdf.dissolve(by="cluster", as_index=False)
|
|
103
|
+
>>> dissolved
|
|
104
|
+
cluster geometry
|
|
105
|
+
0 0 POLYGON ((0.99951 -0.03141, 0.99803 -0.06279, ...
|
|
106
|
+
1 1 POLYGON ((4.99951 2.96859, 4.99803 2.93721, 4....
|
|
107
|
+
2 2 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
|
|
108
|
+
"""
|
|
109
|
+
if isinstance(gdfs[-1], str):
|
|
110
|
+
*gdfs, cluster_col = gdfs
|
|
111
|
+
|
|
112
|
+
concated = pd.DataFrame()
|
|
113
|
+
orig_indices = ()
|
|
114
|
+
for i, gdf in enumerate(gdfs):
|
|
115
|
+
if isinstance(gdf, GeoSeries):
|
|
116
|
+
gdf = gdf.to_frame()
|
|
117
|
+
|
|
118
|
+
if not isinstance(gdf, GeoDataFrame):
|
|
119
|
+
raise TypeError("'gdfs' should be GeoDataFrames or GeoSeries.")
|
|
120
|
+
|
|
121
|
+
if not allow_multipart and len(gdf) != len(gdf.explode(index_parts=False)):
|
|
122
|
+
raise ValueError(
|
|
123
|
+
"All geometries should be exploded to singlepart "
|
|
124
|
+
"in order to get correct polygon clusters. "
|
|
125
|
+
"To allow multipart geometries, set allow_multipart=True"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
orig_indices = orig_indices + (gdf.index,)
|
|
129
|
+
|
|
130
|
+
gdf["i__"] = i
|
|
131
|
+
|
|
132
|
+
concated = pd.concat([concated, gdf], ignore_index=True)
|
|
133
|
+
|
|
134
|
+
neighbors = get_neighbor_indices(concated, concated)
|
|
135
|
+
|
|
136
|
+
edges = [(source, target) for source, target in neighbors.items()]
|
|
137
|
+
|
|
138
|
+
graph = nx.Graph()
|
|
139
|
+
graph.add_edges_from(edges)
|
|
140
|
+
|
|
141
|
+
component_mapper = {
|
|
142
|
+
j: i
|
|
143
|
+
for i, component in enumerate(nx.connected_components(graph))
|
|
144
|
+
for j in component
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
concated[cluster_col] = component_mapper
|
|
148
|
+
|
|
149
|
+
concated = _push_geom_col(concated)
|
|
150
|
+
|
|
151
|
+
n_gdfs = concated["i__"].unique()
|
|
152
|
+
|
|
153
|
+
if len(n_gdfs) == 1:
|
|
154
|
+
concated.index = orig_indices[0]
|
|
155
|
+
return concated.drop(["i__"], axis=1)
|
|
156
|
+
|
|
157
|
+
unconcated = ()
|
|
158
|
+
for i in n_gdfs:
|
|
159
|
+
gdf = concated[concated["i__"] == i]
|
|
160
|
+
gdf.index = orig_indices[i]
|
|
161
|
+
gdf = gdf.drop(["i__"], axis=1)
|
|
162
|
+
unconcated = unconcated + (gdf,)
|
|
163
|
+
|
|
164
|
+
return unconcated
|
|
165
|
+
|
|
166
|
+
|
|
24
167
|
def eliminate_by_longest(
|
|
25
168
|
gdf: GeoDataFrame,
|
|
26
169
|
to_eliminate: GeoDataFrame,
|
|
@@ -213,146 +356,6 @@ def _eliminate_by_area(
|
|
|
213
356
|
return eliminated
|
|
214
357
|
|
|
215
358
|
|
|
216
|
-
def get_polygon_clusters(
|
|
217
|
-
*gdfs: GeoDataFrame | GeoSeries,
|
|
218
|
-
cluster_col: str = "cluster",
|
|
219
|
-
allow_multipart: bool = False,
|
|
220
|
-
) -> GeoDataFrame | tuple[GeoDataFrame]:
|
|
221
|
-
"""Find which polygons overlap without dissolving.
|
|
222
|
-
|
|
223
|
-
Devides polygons into clusters in a fast and precice manner by using spatial join
|
|
224
|
-
and networkx to find the connected components, i.e. overlapping geometries.
|
|
225
|
-
If multiple GeoDataFrames are given, the clusters will be based on all
|
|
226
|
-
combined.
|
|
227
|
-
|
|
228
|
-
This can be used instead of dissolve+explode, or before dissolving by the cluster
|
|
229
|
-
column. This has been tested to be a lot faster if there are many
|
|
230
|
-
non-overlapping polygons, but somewhat slower than dissolve+explode if most
|
|
231
|
-
polygons overlap.
|
|
232
|
-
|
|
233
|
-
Args:
|
|
234
|
-
gdfs: One or more GeoDataFrames of polygons.
|
|
235
|
-
cluster_col: Name of the resulting cluster column.
|
|
236
|
-
allow_multipart: Whether to allow mutipart geometries in the gdfs.
|
|
237
|
-
Defaults to False to avoid confusing results.
|
|
238
|
-
|
|
239
|
-
Returns:
|
|
240
|
-
One or more GeoDataFrames (same amount as was given) with a new cluster column.
|
|
241
|
-
|
|
242
|
-
Examples
|
|
243
|
-
--------
|
|
244
|
-
|
|
245
|
-
Create geometries with three clusters of overlapping polygons.
|
|
246
|
-
|
|
247
|
-
>>> import sgis as sg
|
|
248
|
-
>>> gdf = sg.to_gdf([(0, 0), (1, 1), (0, 1), (4, 4), (4, 3), (7, 7)])
|
|
249
|
-
>>> buffered = sg.buff(gdf, 1)
|
|
250
|
-
>>> gdf
|
|
251
|
-
geometry
|
|
252
|
-
0 POLYGON ((1.00000 0.00000, 0.99951 -0.03141, 0...
|
|
253
|
-
1 POLYGON ((2.00000 1.00000, 1.99951 0.96859, 1....
|
|
254
|
-
2 POLYGON ((1.00000 1.00000, 0.99951 0.96859, 0....
|
|
255
|
-
3 POLYGON ((5.00000 4.00000, 4.99951 3.96859, 4....
|
|
256
|
-
4 POLYGON ((5.00000 3.00000, 4.99951 2.96859, 4....
|
|
257
|
-
5 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
|
|
258
|
-
|
|
259
|
-
Add a cluster column to the GeoDataFrame:
|
|
260
|
-
|
|
261
|
-
>>> gdf = sg.get_polygon_clusters(gdf, cluster_col="cluster")
|
|
262
|
-
>>> gdf
|
|
263
|
-
cluster geometry
|
|
264
|
-
0 0 POLYGON ((1.00000 0.00000, 0.99951 -0.03141, 0...
|
|
265
|
-
1 0 POLYGON ((2.00000 1.00000, 1.99951 0.96859, 1....
|
|
266
|
-
2 0 POLYGON ((1.00000 1.00000, 0.99951 0.96859, 0....
|
|
267
|
-
3 1 POLYGON ((5.00000 4.00000, 4.99951 3.96859, 4....
|
|
268
|
-
4 1 POLYGON ((5.00000 3.00000, 4.99951 2.96859, 4....
|
|
269
|
-
5 2 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
|
|
270
|
-
|
|
271
|
-
If multiple GeoDataFrames are given, all are returned with common
|
|
272
|
-
cluster values.
|
|
273
|
-
|
|
274
|
-
>>> gdf2 = sg.to_gdf([(0, 0), (7, 7)])
|
|
275
|
-
>>> gdf, gdf2 = sg.get_polygon_clusters(gdf, gdf2, cluster_col="cluster")
|
|
276
|
-
>>> gdf2
|
|
277
|
-
cluster geometry
|
|
278
|
-
0 0 POINT (0.00000 0.00000)
|
|
279
|
-
1 2 POINT (7.00000 7.00000)
|
|
280
|
-
>>> gdf
|
|
281
|
-
cluster geometry
|
|
282
|
-
0 0 POLYGON ((1.00000 0.00000, 0.99951 -0.03141, 0...
|
|
283
|
-
1 0 POLYGON ((2.00000 1.00000, 1.99951 0.96859, 1....
|
|
284
|
-
2 0 POLYGON ((1.00000 1.00000, 0.99951 0.96859, 0....
|
|
285
|
-
3 1 POLYGON ((5.00000 4.00000, 4.99951 3.96859, 4....
|
|
286
|
-
4 1 POLYGON ((5.00000 3.00000, 4.99951 2.96859, 4....
|
|
287
|
-
5 2 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
|
|
288
|
-
|
|
289
|
-
Dissolving 'by' the cluster column will make the dissolve much
|
|
290
|
-
faster if there are a lot of non-overlapping polygons.
|
|
291
|
-
|
|
292
|
-
>>> dissolved = gdf.dissolve(by="cluster", as_index=False)
|
|
293
|
-
>>> dissolved
|
|
294
|
-
cluster geometry
|
|
295
|
-
0 0 POLYGON ((0.99951 -0.03141, 0.99803 -0.06279, ...
|
|
296
|
-
1 1 POLYGON ((4.99951 2.96859, 4.99803 2.93721, 4....
|
|
297
|
-
2 2 POLYGON ((8.00000 7.00000, 7.99951 6.96859, 7....
|
|
298
|
-
"""
|
|
299
|
-
if isinstance(gdfs[-1], str):
|
|
300
|
-
*gdfs, cluster_col = gdfs
|
|
301
|
-
|
|
302
|
-
concated = pd.DataFrame()
|
|
303
|
-
orig_indices = ()
|
|
304
|
-
for i, gdf in enumerate(gdfs):
|
|
305
|
-
if isinstance(gdf, GeoSeries):
|
|
306
|
-
gdf = gdf.to_frame()
|
|
307
|
-
|
|
308
|
-
if not isinstance(gdf, GeoDataFrame):
|
|
309
|
-
raise TypeError("'gdfs' should be one or more GeoDataFrames or GeoSeries.")
|
|
310
|
-
|
|
311
|
-
if not allow_multipart and len(gdf) != len(gdf.explode(index_parts=False)):
|
|
312
|
-
raise ValueError(
|
|
313
|
-
"All geometries should be exploded to singlepart "
|
|
314
|
-
"in order to get correct polygon clusters. "
|
|
315
|
-
"To allow multipart geometries, set allow_multipart=True"
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
orig_indices = orig_indices + (gdf.index,)
|
|
319
|
-
gdf["i__"] = i
|
|
320
|
-
|
|
321
|
-
concated = pd.concat([concated, gdf], ignore_index=True)
|
|
322
|
-
|
|
323
|
-
neighbors = get_neighbor_indices(concated, concated)
|
|
324
|
-
|
|
325
|
-
edges = [(source, target) for source, target in neighbors.items()]
|
|
326
|
-
|
|
327
|
-
graph = nx.Graph()
|
|
328
|
-
graph.add_edges_from(edges)
|
|
329
|
-
|
|
330
|
-
component_mapper = {
|
|
331
|
-
j: i
|
|
332
|
-
for i, component in enumerate(nx.connected_components(graph))
|
|
333
|
-
for j in component
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
concated[cluster_col] = component_mapper
|
|
337
|
-
|
|
338
|
-
concated = _push_geom_col(concated)
|
|
339
|
-
|
|
340
|
-
n_gdfs = concated["i__"].unique()
|
|
341
|
-
|
|
342
|
-
if len(n_gdfs) == 1:
|
|
343
|
-
concated.index = orig_indices[0]
|
|
344
|
-
return concated.drop(["i__"], axis=1)
|
|
345
|
-
|
|
346
|
-
unconcated = ()
|
|
347
|
-
for i in n_gdfs:
|
|
348
|
-
gdf = concated[concated["i__"] == i]
|
|
349
|
-
gdf.index = orig_indices[i]
|
|
350
|
-
gdf = gdf.drop(["i__"], axis=1)
|
|
351
|
-
unconcated = unconcated + (gdf,)
|
|
352
|
-
|
|
353
|
-
return unconcated
|
|
354
|
-
|
|
355
|
-
|
|
356
359
|
def get_overlapping_polygons(
|
|
357
360
|
gdf: GeoDataFrame | GeoSeries, ignore_index: bool = False
|
|
358
361
|
) -> GeoDataFrame | GeoSeries:
|
|
@@ -457,21 +460,19 @@ def get_overlapping_polygon_product(gdf: GeoDataFrame | GeoSeries) -> pd.Index:
|
|
|
457
460
|
return series
|
|
458
461
|
|
|
459
462
|
|
|
460
|
-
def
|
|
463
|
+
def close_all_holes(
|
|
461
464
|
gdf: GeoDataFrame | GeoSeries,
|
|
462
|
-
max_area: int | float,
|
|
463
465
|
*,
|
|
466
|
+
without_islands: bool = True,
|
|
464
467
|
copy: bool = True,
|
|
465
468
|
) -> GeoDataFrame | GeoSeries:
|
|
466
|
-
"""Closes holes in polygons
|
|
469
|
+
"""Closes all holes in polygons.
|
|
467
470
|
|
|
468
471
|
It takes a GeoDataFrame or GeoSeries of polygons and
|
|
469
|
-
|
|
470
|
-
either square meters ('max_m2') or square kilometers ('max_km2').
|
|
472
|
+
returns the outer circle.
|
|
471
473
|
|
|
472
474
|
Args:
|
|
473
475
|
gdf: GeoDataFrame or GeoSeries of polygons.
|
|
474
|
-
max_area: The maximum area in the unit of the GeoDataFrame's crs.
|
|
475
476
|
copy: if True (default), the input GeoDataFrame or GeoSeries is copied.
|
|
476
477
|
Defaults to True.
|
|
477
478
|
|
|
@@ -479,17 +480,11 @@ def close_small_holes(
|
|
|
479
480
|
A GeoDataFrame or GeoSeries of polygons with closed holes in the geometry
|
|
480
481
|
column.
|
|
481
482
|
|
|
482
|
-
Raises:
|
|
483
|
-
ValueError: If the coordinate reference system of the GeoDataFrame is not in
|
|
484
|
-
meter units.
|
|
485
|
-
ValueError: If both 'max_m2' and 'max_km2' is given.
|
|
486
|
-
|
|
487
483
|
Examples
|
|
488
484
|
--------
|
|
489
|
-
|
|
490
485
|
Let's create a circle with a hole in it.
|
|
491
486
|
|
|
492
|
-
>>> from sgis import
|
|
487
|
+
>>> from sgis import close_all_holes, buff, to_gdf
|
|
493
488
|
>>> point = to_gdf([260000, 6650000], crs=25833)
|
|
494
489
|
>>> point
|
|
495
490
|
geometry
|
|
@@ -501,50 +496,54 @@ def close_small_holes(
|
|
|
501
496
|
0 2.355807e+06
|
|
502
497
|
dtype: float64
|
|
503
498
|
|
|
504
|
-
Close
|
|
499
|
+
Close the hole.
|
|
505
500
|
|
|
506
|
-
>>> holes_closed =
|
|
501
|
+
>>> holes_closed = close_all_holes(circle_with_hole)
|
|
507
502
|
>>> holes_closed.area
|
|
508
503
|
0 3.141076e+06
|
|
509
504
|
dtype: float64
|
|
510
|
-
|
|
511
|
-
The hole will not be closed if it is larger.
|
|
512
|
-
|
|
513
|
-
>>> holes_closed = close_small_holes(circle_with_hole, max_area=1_000)
|
|
514
|
-
>>> holes_closed.area
|
|
515
|
-
0 2.355807e+06
|
|
516
|
-
dtype: float64
|
|
517
505
|
"""
|
|
518
|
-
if
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if isinstance(gdf, GeoDataFrame):
|
|
522
|
-
gdf["geometry"] = gdf.geometry.map(
|
|
523
|
-
lambda x: _close_small_holes_poly(x, max_area)
|
|
506
|
+
if not isinstance(gdf, (GeoDataFrame, GeoSeries)):
|
|
507
|
+
raise ValueError(
|
|
508
|
+
f"'gdf' should be of type GeoDataFrame or GeoSeries. Got {type(gdf)}"
|
|
524
509
|
)
|
|
525
|
-
return gdf
|
|
526
510
|
|
|
527
|
-
|
|
528
|
-
|
|
511
|
+
if copy:
|
|
512
|
+
gdf = gdf.copy()
|
|
529
513
|
|
|
514
|
+
if without_islands:
|
|
515
|
+
all_geoms = gdf.unary_union
|
|
516
|
+
if isinstance(gdf, GeoDataFrame):
|
|
517
|
+
gdf["geometry"] = gdf.geometry.map(
|
|
518
|
+
lambda x: _close_all_holes_no_islands(x, all_geoms)
|
|
519
|
+
)
|
|
520
|
+
return gdf
|
|
521
|
+
else:
|
|
522
|
+
return gdf.map(lambda x: _close_all_holes_no_islands(x, all_geoms))
|
|
530
523
|
else:
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
524
|
+
if isinstance(gdf, GeoDataFrame):
|
|
525
|
+
gdf["geometry"] = gdf.geometry.map(_close_all_holes)
|
|
526
|
+
return gdf
|
|
527
|
+
else:
|
|
528
|
+
return gdf.map(_close_all_holes)
|
|
534
529
|
|
|
535
530
|
|
|
536
|
-
def
|
|
531
|
+
def close_small_holes(
|
|
537
532
|
gdf: GeoDataFrame | GeoSeries,
|
|
533
|
+
max_area: int | float,
|
|
538
534
|
*,
|
|
535
|
+
without_islands: bool = True,
|
|
539
536
|
copy: bool = True,
|
|
540
537
|
) -> GeoDataFrame | GeoSeries:
|
|
541
|
-
"""Closes
|
|
538
|
+
"""Closes holes in polygons if the area is less than the given maximum.
|
|
542
539
|
|
|
543
540
|
It takes a GeoDataFrame or GeoSeries of polygons and
|
|
544
|
-
|
|
541
|
+
fills the holes that are smaller than the specified area given in units of
|
|
542
|
+
either square meters ('max_m2') or square kilometers ('max_km2').
|
|
545
543
|
|
|
546
544
|
Args:
|
|
547
545
|
gdf: GeoDataFrame or GeoSeries of polygons.
|
|
546
|
+
max_area: The maximum area in the unit of the GeoDataFrame's crs.
|
|
548
547
|
copy: if True (default), the input GeoDataFrame or GeoSeries is copied.
|
|
549
548
|
Defaults to True.
|
|
550
549
|
|
|
@@ -552,11 +551,17 @@ def close_all_holes(
|
|
|
552
551
|
A GeoDataFrame or GeoSeries of polygons with closed holes in the geometry
|
|
553
552
|
column.
|
|
554
553
|
|
|
554
|
+
Raises:
|
|
555
|
+
ValueError: If the coordinate reference system of the GeoDataFrame is not in
|
|
556
|
+
meter units.
|
|
557
|
+
ValueError: If both 'max_m2' and 'max_km2' is given.
|
|
558
|
+
|
|
555
559
|
Examples
|
|
556
560
|
--------
|
|
561
|
+
|
|
557
562
|
Let's create a circle with a hole in it.
|
|
558
563
|
|
|
559
|
-
>>> from sgis import
|
|
564
|
+
>>> from sgis import close_small_holes, buff, to_gdf
|
|
560
565
|
>>> point = to_gdf([260000, 6650000], crs=25833)
|
|
561
566
|
>>> point
|
|
562
567
|
geometry
|
|
@@ -568,35 +573,51 @@ def close_all_holes(
|
|
|
568
573
|
0 2.355807e+06
|
|
569
574
|
dtype: float64
|
|
570
575
|
|
|
571
|
-
Close
|
|
576
|
+
Close holes smaller than 1 square kilometer (1 million square meters).
|
|
572
577
|
|
|
573
|
-
>>> holes_closed =
|
|
578
|
+
>>> holes_closed = close_small_holes(circle_with_hole, max_area=1_000_000)
|
|
574
579
|
>>> holes_closed.area
|
|
575
580
|
0 3.141076e+06
|
|
576
581
|
dtype: float64
|
|
577
|
-
"""
|
|
578
|
-
if copy:
|
|
579
|
-
gdf = gdf.copy()
|
|
580
582
|
|
|
581
|
-
|
|
582
|
-
return unary_union(polygons(get_exterior_ring(get_parts(poly))))
|
|
583
|
+
The hole will not be closed if it is larger.
|
|
583
584
|
|
|
584
|
-
|
|
585
|
+
>>> holes_closed = close_small_holes(circle_with_hole, max_area=1_000)
|
|
586
|
+
>>> holes_closed.area
|
|
587
|
+
0 2.355807e+06
|
|
588
|
+
dtype: float64
|
|
589
|
+
"""
|
|
590
|
+
if not isinstance(gdf, (GeoSeries, GeoDataFrame)):
|
|
591
|
+
raise ValueError(
|
|
592
|
+
f"'gdf' should be of type GeoDataFrame or GeoSeries. Got {type(gdf)}"
|
|
593
|
+
)
|
|
585
594
|
|
|
586
|
-
if
|
|
587
|
-
gdf
|
|
588
|
-
return gdf
|
|
595
|
+
if copy:
|
|
596
|
+
gdf = gdf.copy()
|
|
589
597
|
|
|
590
|
-
|
|
591
|
-
|
|
598
|
+
if without_islands:
|
|
599
|
+
all_geoms = gdf.unary_union
|
|
592
600
|
|
|
601
|
+
if isinstance(gdf, GeoDataFrame):
|
|
602
|
+
gdf["geometry"] = gdf.geometry.map(
|
|
603
|
+
lambda x: _close_small_holes_no_islands(x, max_area, all_geoms)
|
|
604
|
+
)
|
|
605
|
+
return gdf
|
|
606
|
+
else:
|
|
607
|
+
return gdf.map(
|
|
608
|
+
lambda x: _close_small_holes_no_islands(x, max_area, all_geoms)
|
|
609
|
+
)
|
|
593
610
|
else:
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
611
|
+
if isinstance(gdf, GeoDataFrame):
|
|
612
|
+
gdf["geometry"] = gdf.geometry.map(
|
|
613
|
+
lambda x: _close_small_holes(x, max_area)
|
|
614
|
+
)
|
|
615
|
+
return gdf
|
|
616
|
+
else:
|
|
617
|
+
return gdf.map(lambda x: _close_small_holes(x, max_area))
|
|
597
618
|
|
|
598
619
|
|
|
599
|
-
def
|
|
620
|
+
def _close_small_holes(poly, max_area):
|
|
600
621
|
"""Closes cmall holes within one shapely geometry of polygons."""
|
|
601
622
|
|
|
602
623
|
# start with a list containing the polygon,
|
|
@@ -612,7 +633,56 @@ def _close_small_holes_poly(poly, max_area):
|
|
|
612
633
|
for n in range(n_interior_rings):
|
|
613
634
|
hole = polygons(get_interior_ring(part, n))
|
|
614
635
|
|
|
636
|
+
print(area(hole))
|
|
637
|
+
|
|
615
638
|
if area(hole) < max_area:
|
|
616
639
|
holes_closed.append(hole)
|
|
617
640
|
|
|
618
641
|
return unary_union(holes_closed)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _close_small_holes_no_islands(poly, max_area, all_geoms):
|
|
645
|
+
"""Closes small holes within one shapely geometry of polygons."""
|
|
646
|
+
|
|
647
|
+
# start with a list containing the polygon,
|
|
648
|
+
# then append all holes smaller than 'max_km2' to the list.
|
|
649
|
+
holes_closed = [poly]
|
|
650
|
+
singlepart = get_parts(poly)
|
|
651
|
+
for part in singlepart:
|
|
652
|
+
n_interior_rings = get_num_interior_rings(part)
|
|
653
|
+
|
|
654
|
+
if not (n_interior_rings):
|
|
655
|
+
continue
|
|
656
|
+
|
|
657
|
+
for n in range(n_interior_rings):
|
|
658
|
+
hole = polygons(get_interior_ring(part, n))
|
|
659
|
+
no_islands = unary_union(hole.difference(all_geoms))
|
|
660
|
+
if area(no_islands) < max_area:
|
|
661
|
+
holes_closed.append(no_islands)
|
|
662
|
+
|
|
663
|
+
return unary_union(holes_closed)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _close_all_holes(poly):
|
|
667
|
+
return unary_union(polygons(get_exterior_ring(get_parts(poly))))
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _close_all_holes_no_islands(poly, all_geoms):
|
|
671
|
+
"""Closes all holes within one shapely geometry of polygons."""
|
|
672
|
+
|
|
673
|
+
# start with a list containing the polygon,
|
|
674
|
+
# then append all holes smaller than 'max_km2' to the list.
|
|
675
|
+
holes_closed = [poly]
|
|
676
|
+
singlepart = get_parts(poly)
|
|
677
|
+
for part in singlepart:
|
|
678
|
+
n_interior_rings = get_num_interior_rings(part)
|
|
679
|
+
|
|
680
|
+
if not (n_interior_rings):
|
|
681
|
+
continue
|
|
682
|
+
|
|
683
|
+
for n in range(n_interior_rings):
|
|
684
|
+
hole = polygons(get_interior_ring(part, n))
|
|
685
|
+
no_islands = unary_union(hole.difference(all_geoms))
|
|
686
|
+
holes_closed.append(no_islands)
|
|
687
|
+
|
|
688
|
+
return unary_union(holes_closed)
|
|
@@ -3,9 +3,10 @@ from collections.abc import Iterator, Sized
|
|
|
3
3
|
|
|
4
4
|
import geopandas as gpd
|
|
5
5
|
import pandas as pd
|
|
6
|
+
import shapely
|
|
6
7
|
from geopandas import GeoDataFrame, GeoSeries
|
|
7
8
|
from pandas.api.types import is_array_like, is_dict_like, is_list_like
|
|
8
|
-
from shapely import Geometry, wkb, wkt
|
|
9
|
+
from shapely import Geometry, box, wkb, wkt
|
|
9
10
|
from shapely.geometry import Point
|
|
10
11
|
from shapely.ops import unary_union
|
|
11
12
|
|
|
@@ -128,7 +129,7 @@ def to_gdf(
|
|
|
128
129
|
raise TypeError("'to_gdf' doesn't accept GeoDataFrames as input type.")
|
|
129
130
|
|
|
130
131
|
if isinstance(geom, GeoSeries):
|
|
131
|
-
geom_col =
|
|
132
|
+
geom_col = geometry if geometry else "geometry"
|
|
132
133
|
return _geoseries_to_gdf(geom, geom_col, crs, **kwargs)
|
|
133
134
|
|
|
134
135
|
geom_col = _find_geometry_column(geom, geometry)
|
|
@@ -144,8 +145,26 @@ def to_gdf(
|
|
|
144
145
|
return GeoDataFrame({geom_col: geom}, geometry=geom_col, crs=crs, **kwargs)
|
|
145
146
|
|
|
146
147
|
if not is_dict_like(geom):
|
|
147
|
-
geom
|
|
148
|
-
|
|
148
|
+
if not hasattr(geom, "__iter__") and hasattr(geom, "__dict__"):
|
|
149
|
+
if all(attr in geom.__dict__ for attr in ["minx", "miny", "maxx", "maxy"]):
|
|
150
|
+
geom = GeoSeries(
|
|
151
|
+
shapely.box(*(geom.minx, geom.miny, geom.maxx, geom.maxy)),
|
|
152
|
+
index=index,
|
|
153
|
+
)
|
|
154
|
+
return GeoDataFrame(
|
|
155
|
+
{geom_col: geom}, geometry=geom_col, crs=crs, **kwargs
|
|
156
|
+
)
|
|
157
|
+
if hasattr(geom, "__iter__") and all(isinstance(g, dict) for g in geom):
|
|
158
|
+
crs = crs if crs else _get_crs(geom)
|
|
159
|
+
geom = pd.concat(GeoSeries(_from_json(g)) for g in geom)
|
|
160
|
+
if index is not None:
|
|
161
|
+
geom.index = index
|
|
162
|
+
else:
|
|
163
|
+
geom = geom.reset_index(drop=True)
|
|
164
|
+
return GeoDataFrame({geom_col: geom}, geometry=geom_col, crs=crs, **kwargs)
|
|
165
|
+
else:
|
|
166
|
+
geom = GeoSeries(_make_shapely_geoms(geom), index=index)
|
|
167
|
+
return GeoDataFrame({geom_col: geom}, geometry=geom_col, crs=crs, **kwargs)
|
|
149
168
|
|
|
150
169
|
# now we have dict, Series or DataFrame
|
|
151
170
|
|
|
@@ -168,6 +187,8 @@ def to_gdf(
|
|
|
168
187
|
geoseries = GeoSeries(
|
|
169
188
|
_make_shapely_geoms(list(geom.values())[0]), index=index
|
|
170
189
|
)
|
|
190
|
+
elif isinstance(geom, pd.Series):
|
|
191
|
+
geoseries = GeoSeries(_make_shapely_geoms(geom), index=index)
|
|
171
192
|
else:
|
|
172
193
|
geoseries = GeoSeries(_make_shapely_geoms(geom.iloc[:, 0]), index=index)
|
|
173
194
|
return GeoDataFrame({key: geoseries}, geometry=key, crs=crs, **kwargs)
|
|
@@ -175,10 +196,61 @@ def to_gdf(
|
|
|
175
196
|
if geometry and geom_col not in geom or isinstance(geom, pd.DataFrame):
|
|
176
197
|
raise ValueError("Cannot find geometry column(s)", geometry)
|
|
177
198
|
|
|
199
|
+
# geojson, __geo_interface__
|
|
200
|
+
if (
|
|
201
|
+
isinstance(geom, dict)
|
|
202
|
+
and sum(key in geom for key in ["type", "coordinates", "features"]) >= 2
|
|
203
|
+
):
|
|
204
|
+
if "geometry" in geom:
|
|
205
|
+
geometry = "geometry"
|
|
206
|
+
|
|
207
|
+
crs = crs if crs else _get_crs(geom)
|
|
208
|
+
print(crs)
|
|
209
|
+
geom = GeoSeries(_from_json(geom), index=index)
|
|
210
|
+
return GeoDataFrame({geom_col: geom}, geometry=geom_col, crs=crs, **kwargs)
|
|
211
|
+
|
|
178
212
|
geoseries = _series_like_to_geoseries(geom, index=index)
|
|
179
213
|
return GeoDataFrame(geometry=geoseries, crs=crs, **kwargs)
|
|
180
214
|
|
|
181
215
|
|
|
216
|
+
def _get_crs(geom):
|
|
217
|
+
if not is_dict_like(geom) and is_dict_like(geom[0]):
|
|
218
|
+
crss = list({_get_crs(g) for g in geom})
|
|
219
|
+
if len(crss) == 1:
|
|
220
|
+
return crss[0]
|
|
221
|
+
return None
|
|
222
|
+
if "properties" in geom:
|
|
223
|
+
return _get_crs(geom["properties"])
|
|
224
|
+
if "crs" in geom:
|
|
225
|
+
geom = geom["crs"]
|
|
226
|
+
while is_dict_like(geom):
|
|
227
|
+
if "properties" in geom:
|
|
228
|
+
geom = geom["properties"]
|
|
229
|
+
elif "name" in geom:
|
|
230
|
+
geom = geom["name"]
|
|
231
|
+
else:
|
|
232
|
+
return None
|
|
233
|
+
return geom
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _from_json(geom: dict):
|
|
238
|
+
if not isinstance(geom, dict) and isinstance(geom[0], dict):
|
|
239
|
+
return [_from_json(g) for g in geom]
|
|
240
|
+
if "geometry" in geom:
|
|
241
|
+
return _from_json(geom["geometry"])
|
|
242
|
+
if "features" in geom:
|
|
243
|
+
return _from_json(geom["features"])
|
|
244
|
+
coords = geom["coordinates"]
|
|
245
|
+
constructor = eval("shapely.geometry." + geom.get("type", Point))
|
|
246
|
+
try:
|
|
247
|
+
return constructor(coords)
|
|
248
|
+
except TypeError:
|
|
249
|
+
while len(coords) == 1:
|
|
250
|
+
coords = coords[0]
|
|
251
|
+
return constructor(coords)
|
|
252
|
+
|
|
253
|
+
|
|
182
254
|
def _series_like_to_geoseries(geom, index):
|
|
183
255
|
if index is None:
|
|
184
256
|
index = geom.keys()
|
|
@@ -250,6 +322,8 @@ def _is_one_geometry(geom) -> bool:
|
|
|
250
322
|
def _make_shapely_geoms(geom):
|
|
251
323
|
if _is_one_geometry(geom):
|
|
252
324
|
return _make_one_shapely_geom(geom)
|
|
325
|
+
if isinstance(geom, dict) and "coordinates" in geom:
|
|
326
|
+
return _from_json(geom)
|
|
253
327
|
return (_make_one_shapely_geom(g) for g in geom)
|
|
254
328
|
|
|
255
329
|
|
|
@@ -292,9 +366,11 @@ def _make_one_shapely_geom(geom):
|
|
|
292
366
|
elif len(geom) == 2 or len(geom) == 3:
|
|
293
367
|
return Point(geom)
|
|
294
368
|
|
|
369
|
+
elif len(geom) == 4:
|
|
370
|
+
return box(*geom)
|
|
295
371
|
else:
|
|
296
372
|
raise ValueError(
|
|
297
373
|
"If 'geom' is an iterable, each item should consist of "
|
|
298
|
-
"wkt, wkb or
|
|
374
|
+
"wkt, wkb or (x, y (z) or bbox). Got ",
|
|
299
375
|
geom,
|
|
300
376
|
)
|
sgis/helpers.py
CHANGED
|
@@ -58,9 +58,9 @@ def get_name(var: object, n: int = 5) -> str | None:
|
|
|
58
58
|
frame = inspect.currentframe().f_back.f_back
|
|
59
59
|
|
|
60
60
|
for _ in range(n):
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
names = [
|
|
62
|
+
var_name for var_name, var_val in frame.f_locals.items() if var_val is var
|
|
63
|
+
]
|
|
64
64
|
if names and len(names) == 1:
|
|
65
65
|
return names[0]
|
|
66
66
|
|
sgis/maps/examine.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import geopandas as gpd
|
|
2
|
+
|
|
3
|
+
from ..helpers import unit_is_degrees
|
|
4
|
+
from .maps import clipmap
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Examine:
|
|
8
|
+
"""Explore geometries one row at a time.
|
|
9
|
+
|
|
10
|
+
It takes one or more GeoDataFrames and shows an interactive map
|
|
11
|
+
of one area at the time with the 'next', 'prev' and 'current' methods.
|
|
12
|
+
|
|
13
|
+
After creating the examiner object, the 'next' method will create a map
|
|
14
|
+
showing all geometries within a given radius (the size parameter) of the
|
|
15
|
+
first geometry in 'mask_gdf' (or the first speficied gdf). The 'next' method
|
|
16
|
+
can then be repeated.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
*gdfs: One or more GeoDataFrames. The rows of the first GeoDataFrame
|
|
20
|
+
will be used as masks, unless 'mask_gdf' is specified.
|
|
21
|
+
column: Column to use as colors.
|
|
22
|
+
mask_gdf: Optional GeoDataFrame to use as mask iterator. The geometries
|
|
23
|
+
of mask_gdf will not be shown.
|
|
24
|
+
size: Number of meters (or other crs unit) to buffer the mask geometry
|
|
25
|
+
before clipping.
|
|
26
|
+
sort_values: Optional sorting column(s) of the mask GeoDataFrame. Rows
|
|
27
|
+
will be iterated through from the top.
|
|
28
|
+
**kwargs: Additional keyword arguments passed to sgis.clipmap.
|
|
29
|
+
|
|
30
|
+
Examples
|
|
31
|
+
--------
|
|
32
|
+
Create the examiner.
|
|
33
|
+
|
|
34
|
+
>>> import sgis as sg
|
|
35
|
+
>>> roads = sg.read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
|
|
36
|
+
>>> points = sg.read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/points_oslo.parquet")
|
|
37
|
+
>>> e = sg.Examine(points, roads)
|
|
38
|
+
>>> e
|
|
39
|
+
|
|
40
|
+
Then the line below can be repeated for all rows if 'points'. This has to be
|
|
41
|
+
in a separate notebook cell to the previous.
|
|
42
|
+
|
|
43
|
+
>>> e.next()
|
|
44
|
+
|
|
45
|
+
Previous geometry:
|
|
46
|
+
|
|
47
|
+
>>> e.prev()
|
|
48
|
+
|
|
49
|
+
Repeating the current area with another layer and new column:
|
|
50
|
+
|
|
51
|
+
>>> some_points = points.sample(100)
|
|
52
|
+
>>> e.current(some_points, column="idx")
|
|
53
|
+
|
|
54
|
+
The row number can also be specified manually.
|
|
55
|
+
Can be done in 'next', 'prev' and 'current'.
|
|
56
|
+
|
|
57
|
+
>>> e.next(i=101)
|
|
58
|
+
|
|
59
|
+
This will create an examiner where 'points' is not shown, only used as mask.
|
|
60
|
+
|
|
61
|
+
>>> e = sg.Examine(roads, mask_gdf=points, column="oneway")
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
*gdfs: gpd.GeoDataFrame,
|
|
67
|
+
column: str | None = None,
|
|
68
|
+
mask_gdf: gpd.GeoDataFrame | None = None,
|
|
69
|
+
sort_values: str | None = None,
|
|
70
|
+
size: int | float = 1000,
|
|
71
|
+
**kwargs,
|
|
72
|
+
):
|
|
73
|
+
if not all(isinstance(gdf, gpd.GeoDataFrame) for gdf in gdfs):
|
|
74
|
+
raise ValueError("gdfs must be of type GeoDataFrame.")
|
|
75
|
+
|
|
76
|
+
self.gdfs = gdfs
|
|
77
|
+
if mask_gdf is None:
|
|
78
|
+
self.mask_gdf = gdfs[0]
|
|
79
|
+
else:
|
|
80
|
+
self.mask_gdf = mask_gdf
|
|
81
|
+
|
|
82
|
+
if unit_is_degrees(self.mask_gdf) and size > 360:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"CRS unit is degrees. Use geopandas' "
|
|
85
|
+
"to_crs method to change crs to e.g. UTM. "
|
|
86
|
+
"Or set 'size' to a smaller number."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if sort_values is not None:
|
|
90
|
+
self.mask_gdf = self.mask_gdf.sort_values(sort_values)
|
|
91
|
+
|
|
92
|
+
self.indices = list(range(len(gdfs[0])))
|
|
93
|
+
self.i = 0
|
|
94
|
+
self.column = column
|
|
95
|
+
self.size = size
|
|
96
|
+
self.kwargs = kwargs
|
|
97
|
+
|
|
98
|
+
def next(self, *gdfs, i: int | None = None, **kwargs):
|
|
99
|
+
"""Displays a map of geometries within the next row of the mask gdf.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
*gdfs: Optional GeoDataFrames to be added on top of the current.
|
|
103
|
+
i: Optionally set the integer index of which row to use as mask.
|
|
104
|
+
**kwargs: Additional keyword arguments passed to sgis.clipmap.
|
|
105
|
+
"""
|
|
106
|
+
gdfs = () if not gdfs else gdfs
|
|
107
|
+
self.gdfs = self.gdfs + gdfs
|
|
108
|
+
if kwargs:
|
|
109
|
+
kwargs = self._fix_kwargs(kwargs)
|
|
110
|
+
self.kwargs = self.kwargs | kwargs
|
|
111
|
+
|
|
112
|
+
if i:
|
|
113
|
+
self.i = i
|
|
114
|
+
|
|
115
|
+
if self.i >= len(self.mask_gdf):
|
|
116
|
+
print("All rows are shown.")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
print(f"{self.i + 1} of {len(self.mask_gdf)}")
|
|
120
|
+
clipmap(
|
|
121
|
+
*self.gdfs,
|
|
122
|
+
self.column,
|
|
123
|
+
mask=self.mask_gdf.iloc[[self.i]].buffer(self.size),
|
|
124
|
+
**self.kwargs,
|
|
125
|
+
)
|
|
126
|
+
self.i += 1
|
|
127
|
+
|
|
128
|
+
def prev(self, *gdfs, i: int | None = None, **kwargs):
|
|
129
|
+
"""Displays a map of geometries within the previus row of the mask gdf.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
*gdfs: Optional GeoDataFrames to be added on top of the current.
|
|
133
|
+
i: Optionally set the integer index of which row to use as mask.
|
|
134
|
+
**kwargs: Additional keyword arguments passed to sgis.clipmap.
|
|
135
|
+
"""
|
|
136
|
+
gdfs = () if not gdfs else gdfs
|
|
137
|
+
self.gdfs = self.gdfs + gdfs
|
|
138
|
+
if kwargs:
|
|
139
|
+
kwargs = self._fix_kwargs(kwargs)
|
|
140
|
+
self.kwargs = self.kwargs | kwargs
|
|
141
|
+
|
|
142
|
+
self.i -= 2
|
|
143
|
+
|
|
144
|
+
if i:
|
|
145
|
+
self.i = i
|
|
146
|
+
|
|
147
|
+
print(f"{self.i + 1} of {len(self.mask_gdf)}")
|
|
148
|
+
clipmap(
|
|
149
|
+
*self.gdfs,
|
|
150
|
+
self.column,
|
|
151
|
+
mask=self.mask_gdf.iloc[[self.i]].buffer(self.size),
|
|
152
|
+
**self.kwargs,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def current(self, *gdfs, i: int | None = None, **kwargs):
|
|
156
|
+
"""Repeat the last shown map."""
|
|
157
|
+
gdfs = () if not gdfs else gdfs
|
|
158
|
+
self.gdfs = self.gdfs + gdfs
|
|
159
|
+
if kwargs:
|
|
160
|
+
kwargs = self._fix_kwargs(kwargs)
|
|
161
|
+
self.kwargs = self.kwargs | kwargs
|
|
162
|
+
|
|
163
|
+
if i:
|
|
164
|
+
self.i = i
|
|
165
|
+
|
|
166
|
+
print(f"{self.i + 1} of {len(self.mask_gdf)}")
|
|
167
|
+
clipmap(
|
|
168
|
+
*self.gdfs,
|
|
169
|
+
self.column,
|
|
170
|
+
mask=self.mask_gdf.iloc[[self.i]].buffer(self.size),
|
|
171
|
+
**self.kwargs,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def get_current_mask(self) -> gpd.GeoDataFrame:
|
|
175
|
+
"""Returns a GeoDataFrame of the last shown mask geometry."""
|
|
176
|
+
return self.mask_gdf.iloc[[self.i]]
|
|
177
|
+
|
|
178
|
+
def get_current_geoms(self) -> tuple[gpd.GeoDataFrame]:
|
|
179
|
+
"""Returns all GeoDataFrames in the area of the last shown mask geometry."""
|
|
180
|
+
mask = self.mask_gdf.iloc[[self.i]]
|
|
181
|
+
gdfs = ()
|
|
182
|
+
for gdf in self.gdfs:
|
|
183
|
+
gdfs = gdfs + (gdf.clip(mask.buffer(self.size)),)
|
|
184
|
+
return gdfs
|
|
185
|
+
|
|
186
|
+
def _fix_kwargs(self, kwargs) -> dict:
|
|
187
|
+
self.size = kwargs.pop("size", self.size)
|
|
188
|
+
self.column = kwargs.pop("column", self.column)
|
|
189
|
+
return kwargs
|
|
190
|
+
|
|
191
|
+
def __repr__(self) -> str:
|
|
192
|
+
return f"{self.__class__}(indices={len(self.indices)}, current={self.i}, n_gdfs={len(self.gdfs)})"
|
sgis/maps/explore.py
CHANGED
|
@@ -4,6 +4,7 @@ This module holds the Explore class, which is the basis for the explore, samplem
|
|
|
4
4
|
clipmap functions from the 'maps' module.
|
|
5
5
|
"""
|
|
6
6
|
import warnings
|
|
7
|
+
from numbers import Number
|
|
7
8
|
from statistics import mean
|
|
8
9
|
|
|
9
10
|
import branca as bc
|
|
@@ -15,10 +16,11 @@ from folium import plugins
|
|
|
15
16
|
from geopandas import GeoDataFrame
|
|
16
17
|
from IPython.display import display
|
|
17
18
|
from jinja2 import Template
|
|
19
|
+
from shapely import Geometry
|
|
18
20
|
from shapely.geometry import LineString
|
|
19
21
|
|
|
20
22
|
from ..geopandas_tools.general import clean_geoms, make_all_singlepart
|
|
21
|
-
from ..geopandas_tools.geometry_types import get_geom_type
|
|
23
|
+
from ..geopandas_tools.geometry_types import get_geom_type, to_single_geom_type
|
|
22
24
|
from ..geopandas_tools.to_geodataframe import to_gdf
|
|
23
25
|
from ..helpers import unit_is_degrees
|
|
24
26
|
from .httpserver import run_html_server
|
|
@@ -110,6 +112,17 @@ class Explore(Map):
|
|
|
110
112
|
|
|
111
113
|
super().__init__(*gdfs, column=column, **kwargs)
|
|
112
114
|
|
|
115
|
+
# remove columns not renerable by leaflet (list columns etc.)
|
|
116
|
+
new_gdfs = []
|
|
117
|
+
for gdf in self.gdfs:
|
|
118
|
+
cols_to_keep = [
|
|
119
|
+
col
|
|
120
|
+
for col in gdf.columns
|
|
121
|
+
if isinstance(gdf[col].iloc[0], (Number, str, Geometry))
|
|
122
|
+
]
|
|
123
|
+
new_gdfs.append(gdf[cols_to_keep])
|
|
124
|
+
self._gdfs = new_gdfs
|
|
125
|
+
|
|
113
126
|
self.popup = popup
|
|
114
127
|
self.max_zoom = max_zoom
|
|
115
128
|
self.smooth_factor = smooth_factor
|
|
@@ -266,6 +279,7 @@ class Explore(Map):
|
|
|
266
279
|
|
|
267
280
|
new_gdfs = []
|
|
268
281
|
for gdf in self._gdfs:
|
|
282
|
+
print(gdf)
|
|
269
283
|
if get_geom_type(gdf) == "mixed" and not unit_is_degrees(gdf):
|
|
270
284
|
gdf[gdf._geometry_column_name] = gdf.buffer(0.01)
|
|
271
285
|
gdf = make_all_singlepart(gdf)
|
|
@@ -615,7 +629,26 @@ class Explore(Map):
|
|
|
615
629
|
"supported as marker values"
|
|
616
630
|
)
|
|
617
631
|
|
|
618
|
-
gdf = clean_geoms(gdf)
|
|
632
|
+
gdf = clean_geoms(gdf).pipe(make_all_singlepart)
|
|
633
|
+
if get_geom_type(gdf) == "mixed":
|
|
634
|
+
if gdf.geom_type.str.lower().str.contains("polygon").any():
|
|
635
|
+
warnings.warn(
|
|
636
|
+
"GeoJsonTooltip is not configured to render for GeoJson "
|
|
637
|
+
"GeometryCollection geometries. Keeping only polygons."
|
|
638
|
+
)
|
|
639
|
+
gdf = to_single_geom_type(gdf, geom_type="polygon")
|
|
640
|
+
elif gdf.geom_type.str.lower().str.contains("line").any():
|
|
641
|
+
warnings.warn(
|
|
642
|
+
"GeoJsonTooltip is not configured to render for GeoJson "
|
|
643
|
+
"GeometryCollection geometries. Keeping only lines."
|
|
644
|
+
)
|
|
645
|
+
gdf = to_single_geom_type(gdf, geom_type="line")
|
|
646
|
+
else:
|
|
647
|
+
warnings.warn(
|
|
648
|
+
"GeoJsonTooltip is not configured to render for GeoJson "
|
|
649
|
+
"GeometryCollection geometries. Keeping only points."
|
|
650
|
+
)
|
|
651
|
+
gdf = to_single_geom_type(gdf, geom_type="point")
|
|
619
652
|
|
|
620
653
|
# prepare tooltip and popup
|
|
621
654
|
if isinstance(gdf, GeoDataFrame):
|
sgis/maps/map.py
CHANGED
|
@@ -467,6 +467,8 @@ class Map:
|
|
|
467
467
|
if gdf[self._column].isna().all():
|
|
468
468
|
return np.repeat(len(bins), len(gdf))
|
|
469
469
|
|
|
470
|
+
# need numpy.nan instead of pd.NA as of now
|
|
471
|
+
gdf[self._column] = gdf[self._column].fillna(np.nan)
|
|
470
472
|
classified = np.searchsorted(bins, gdf[self._column])
|
|
471
473
|
|
|
472
474
|
return classified
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
sgis/__init__.py,sha256=
|
|
1
|
+
sgis/__init__.py,sha256=84lCHfBsH9nHwopwoPTMNHs1FvTWemuRPLALAhUEFIA,2445
|
|
2
2
|
sgis/dapla.py,sha256=BlJ62kLwpTTQtmbj0Yutbh-bwokVPXHVb3QsRlMugF8,3542
|
|
3
3
|
sgis/exceptions.py,sha256=ztMp4sB9xxPvwj2IEsO5kOaB4FmHuU_7-M2pZ7qaxTs,576
|
|
4
4
|
sgis/geopandas_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
sgis/geopandas_tools/buffer_dissolve_explode.py,sha256=
|
|
6
|
-
sgis/geopandas_tools/general.py,sha256=
|
|
5
|
+
sgis/geopandas_tools/buffer_dissolve_explode.py,sha256=JHFdhFVURHKAW9NraQZ7YEtvp_lBveGdL0PkBZn1fuE,8780
|
|
6
|
+
sgis/geopandas_tools/general.py,sha256=Hbk1A_aFH_xgD_TkvaJQDW1U2lJYh-LHI9CmsPTkHaI,17930
|
|
7
7
|
sgis/geopandas_tools/geometry_types.py,sha256=o3MbBP-aI7hVWWKVr_5p91TDhjiqZ_2IGxJq7SxlCT4,5870
|
|
8
8
|
sgis/geopandas_tools/neighbors.py,sha256=tv8bmYgq4VNFbXmT2wcmJsFH8946NwbIBMQXAi3n8L4,14520
|
|
9
9
|
sgis/geopandas_tools/overlay.py,sha256=RSxrDF0sXs6ZMxbeBJC9HFBVM4yaz10-cdbq3SCosFQ,11862
|
|
10
10
|
sgis/geopandas_tools/point_operations.py,sha256=3JynroucouAbpON4DWG32S3MQQGmfIJuY7D6gkqtk70,6888
|
|
11
|
-
sgis/geopandas_tools/polygon_operations.py,sha256=
|
|
12
|
-
sgis/geopandas_tools/to_geodataframe.py,sha256=
|
|
13
|
-
sgis/helpers.py,sha256=
|
|
11
|
+
sgis/geopandas_tools/polygon_operations.py,sha256=84AGRHKZR-3zKKVDNbKYCziC9YNUVm2qUEpfRvmIdn4,22635
|
|
12
|
+
sgis/geopandas_tools/to_geodataframe.py,sha256=qtbeiQ9rPcM9afopNPzlN2fiMH5o6SoIYDotX6e_E0Q,12458
|
|
13
|
+
sgis/helpers.py,sha256=6tElQx9Rr3nbQPp_vAgugw-pNHNuYQO-Ta2311_EcWs,2669
|
|
14
14
|
sgis/maps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
sgis/maps/
|
|
15
|
+
sgis/maps/examine.py,sha256=Wf48WwyNyPC1vYdw-n7GpvJYhwd-YMd_VZLOUI5vWZ4,6479
|
|
16
|
+
sgis/maps/explore.py,sha256=d16h8hHyUeSfsrAWdeFUPK6XT6S7-xr1w4_cHu2XA_I,25138
|
|
16
17
|
sgis/maps/httpserver.py,sha256=7ksCSs-WlchcREgjdCZd6II-riJpox34HpVXsCzN_AU,1923
|
|
17
18
|
sgis/maps/legend.py,sha256=GXAqGOb_zAWcDavd5aHzRyRB7nTRhPCQfSupYA693lk,20499
|
|
18
|
-
sgis/maps/map.py,sha256=
|
|
19
|
+
sgis/maps/map.py,sha256=niK6N0eFJjAalxjHTNA7kh-2KuazLVsnWlu8i9Ava7o,18611
|
|
19
20
|
sgis/maps/maps.py,sha256=NaK_wu4RGf6kKRUnnY7gLtxAY9x0d6gKxgQLubDbgHY,15961
|
|
20
21
|
sgis/maps/thematicmap.py,sha256=6aVPciftW1YjxjRVQDipxayl3aI3tHpYiZ3HfpnSavc,14132
|
|
21
22
|
sgis/networkanalysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -33,7 +34,7 @@ sgis/networkanalysis/networkanalysisrules.py,sha256=BhhaSXIyBRNzxSOUP2kVBIR--TRq
|
|
|
33
34
|
sgis/networkanalysis/nodes.py,sha256=Ys3FjB39Pir3U0jOoLKIPxCC4psC9mdlqdC7G6dSJg0,6767
|
|
34
35
|
sgis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
36
|
sgis/read_parquet.py,sha256=GSW2NDy4-XosbamPEzB1xhWxFAPHuGEJZglfQ-V6DzY,3774
|
|
36
|
-
ssb_sgis-0.2.
|
|
37
|
-
ssb_sgis-0.2.
|
|
38
|
-
ssb_sgis-0.2.
|
|
39
|
-
ssb_sgis-0.2.
|
|
37
|
+
ssb_sgis-0.2.8.dist-info/LICENSE,sha256=lL2h0dNKGTKAE0CjTy62SDbRennVD1xPgM5LzGqhKeo,1074
|
|
38
|
+
ssb_sgis-0.2.8.dist-info/METADATA,sha256=hR3yvnhqhsaE8jEvutRwNHAFJq78Hhua5JodgYX99mE,8831
|
|
39
|
+
ssb_sgis-0.2.8.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
|
|
40
|
+
ssb_sgis-0.2.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|