ssb-sgis 1.0.0__py3-none-any.whl → 1.0.2__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 +97 -115
- sgis/exceptions.py +3 -1
- sgis/geopandas_tools/__init__.py +1 -0
- sgis/geopandas_tools/bounds.py +75 -38
- sgis/geopandas_tools/buffer_dissolve_explode.py +38 -34
- sgis/geopandas_tools/centerlines.py +53 -44
- sgis/geopandas_tools/cleaning.py +87 -104
- sgis/geopandas_tools/conversion.py +149 -101
- sgis/geopandas_tools/duplicates.py +31 -17
- sgis/geopandas_tools/general.py +76 -48
- sgis/geopandas_tools/geometry_types.py +21 -7
- sgis/geopandas_tools/neighbors.py +20 -8
- sgis/geopandas_tools/overlay.py +136 -53
- sgis/geopandas_tools/point_operations.py +9 -8
- sgis/geopandas_tools/polygon_operations.py +48 -56
- sgis/geopandas_tools/polygons_as_rings.py +121 -78
- sgis/geopandas_tools/sfilter.py +14 -14
- sgis/helpers.py +114 -56
- sgis/io/dapla_functions.py +32 -23
- sgis/io/opener.py +13 -6
- sgis/io/read_parquet.py +1 -1
- sgis/maps/examine.py +39 -26
- sgis/maps/explore.py +112 -66
- sgis/maps/httpserver.py +12 -12
- sgis/maps/legend.py +124 -65
- sgis/maps/map.py +66 -41
- sgis/maps/maps.py +31 -29
- sgis/maps/thematicmap.py +46 -33
- sgis/maps/tilesources.py +3 -8
- sgis/networkanalysis/_get_route.py +5 -4
- sgis/networkanalysis/_od_cost_matrix.py +44 -1
- sgis/networkanalysis/_points.py +10 -4
- sgis/networkanalysis/_service_area.py +5 -2
- sgis/networkanalysis/closing_network_holes.py +20 -62
- sgis/networkanalysis/cutting_lines.py +55 -43
- sgis/networkanalysis/directednetwork.py +15 -7
- sgis/networkanalysis/finding_isolated_networks.py +4 -3
- sgis/networkanalysis/network.py +15 -13
- sgis/networkanalysis/networkanalysis.py +72 -54
- sgis/networkanalysis/networkanalysisrules.py +20 -16
- sgis/networkanalysis/nodes.py +2 -3
- sgis/networkanalysis/traveling_salesman.py +5 -2
- sgis/parallel/parallel.py +337 -127
- sgis/raster/__init__.py +6 -0
- sgis/raster/base.py +9 -3
- sgis/raster/cube.py +280 -208
- sgis/raster/cubebase.py +15 -29
- sgis/raster/indices.py +3 -7
- sgis/raster/methods_as_functions.py +0 -124
- sgis/raster/raster.py +313 -127
- sgis/raster/torchgeo.py +58 -37
- sgis/raster/zonal.py +38 -13
- {ssb_sgis-1.0.0.dist-info → ssb_sgis-1.0.2.dist-info}/LICENSE +1 -1
- {ssb_sgis-1.0.0.dist-info → ssb_sgis-1.0.2.dist-info}/METADATA +89 -18
- ssb_sgis-1.0.2.dist-info/RECORD +61 -0
- {ssb_sgis-1.0.0.dist-info → ssb_sgis-1.0.2.dist-info}/WHEEL +1 -1
- sgis/raster/bands.py +0 -48
- sgis/raster/gradient.py +0 -78
- ssb_sgis-1.0.0.dist-info/RECORD +0 -63
|
@@ -14,15 +14,19 @@ for the following:
|
|
|
14
14
|
- The buff function returns a GeoDataFrame, the geopandas method returns a GeoSeries.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
from
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from collections.abc import Sequence
|
|
18
19
|
|
|
19
20
|
import numpy as np
|
|
20
21
|
import pandas as pd
|
|
21
|
-
from geopandas import GeoDataFrame
|
|
22
|
+
from geopandas import GeoDataFrame
|
|
23
|
+
from geopandas import GeoSeries
|
|
22
24
|
|
|
23
|
-
from .general import
|
|
25
|
+
from .general import _merge_geometries
|
|
26
|
+
from .general import _parallel_unary_union
|
|
24
27
|
from .geometry_types import make_all_singlepart
|
|
25
|
-
from .polygon_operations import get_cluster_mapper
|
|
28
|
+
from .polygon_operations import get_cluster_mapper
|
|
29
|
+
from .polygon_operations import get_grouped_centroids
|
|
26
30
|
|
|
27
31
|
|
|
28
32
|
def _decide_ignore_index(kwargs: dict) -> tuple[dict, bool]:
|
|
@@ -65,6 +69,8 @@ def buffdissexp(
|
|
|
65
69
|
index_parts: If False (default), the index after dissolve is respected. If
|
|
66
70
|
True, an integer index level is added during explode.
|
|
67
71
|
copy: Whether to copy the GeoDataFrame before buffering. Defaults to True.
|
|
72
|
+
grid_size: Rounding of the coordinates. Defaults to None.
|
|
73
|
+
n_jobs: Number of threads to use. Defaults to 1.
|
|
68
74
|
**dissolve_kwargs: additional keyword arguments passed to geopandas' dissolve.
|
|
69
75
|
|
|
70
76
|
Returns:
|
|
@@ -109,12 +115,13 @@ def buffdiss(
|
|
|
109
115
|
resolution: The number of segments used to approximate a quarter circle.
|
|
110
116
|
Here defaults to 50, as opposed to the default 16 in geopandas.
|
|
111
117
|
copy: Whether to copy the GeoDataFrame before buffering. Defaults to True.
|
|
118
|
+
n_jobs: Number of threads to use. Defaults to 1.
|
|
112
119
|
**dissolve_kwargs: additional keyword arguments passed to geopandas' dissolve.
|
|
113
120
|
|
|
114
121
|
Returns:
|
|
115
122
|
A buffered GeoDataFrame where geometries are dissolved.
|
|
116
123
|
|
|
117
|
-
Examples
|
|
124
|
+
Examples:
|
|
118
125
|
--------
|
|
119
126
|
Create some random points.
|
|
120
127
|
|
|
@@ -167,7 +174,13 @@ def buffdiss(
|
|
|
167
174
|
return _dissolve(buffered, n_jobs=n_jobs, **dissolve_kwargs)
|
|
168
175
|
|
|
169
176
|
|
|
170
|
-
def _dissolve(
|
|
177
|
+
def _dissolve(
|
|
178
|
+
gdf: GeoDataFrame,
|
|
179
|
+
aggfunc: str = "first",
|
|
180
|
+
grid_size: None | float = None,
|
|
181
|
+
n_jobs: int = 1,
|
|
182
|
+
**dissolve_kwargs,
|
|
183
|
+
) -> GeoDataFrame:
|
|
171
184
|
|
|
172
185
|
if not len(gdf):
|
|
173
186
|
return gdf
|
|
@@ -220,7 +233,7 @@ def _dissolve(gdf, aggfunc="first", grid_size=None, n_jobs=1, **dissolve_kwargs)
|
|
|
220
233
|
|
|
221
234
|
if n_jobs > 1:
|
|
222
235
|
try:
|
|
223
|
-
agged =
|
|
236
|
+
agged = _parallel_unary_union(
|
|
224
237
|
many_hits, n_jobs=n_jobs, by=by, grid_size=grid_size, **dissolve_kwargs
|
|
225
238
|
)
|
|
226
239
|
dissolved[geom_col] = agged
|
|
@@ -230,7 +243,7 @@ def _dissolve(gdf, aggfunc="first", grid_size=None, n_jobs=1, **dissolve_kwargs)
|
|
|
230
243
|
raise e
|
|
231
244
|
|
|
232
245
|
geoms_agged = many_hits.groupby(by, **dissolve_kwargs)[geom_col].agg(
|
|
233
|
-
lambda x:
|
|
246
|
+
lambda x: _merge_geometries(x, grid_size=grid_size)
|
|
234
247
|
)
|
|
235
248
|
|
|
236
249
|
if not dissolve_kwargs.get("as_index"):
|
|
@@ -248,13 +261,13 @@ def _dissolve(gdf, aggfunc="first", grid_size=None, n_jobs=1, **dissolve_kwargs)
|
|
|
248
261
|
|
|
249
262
|
def diss(
|
|
250
263
|
gdf: GeoDataFrame,
|
|
251
|
-
by=None,
|
|
252
|
-
aggfunc="first",
|
|
264
|
+
by: str | Sequence[str] | None = None,
|
|
265
|
+
aggfunc: str | Callable | dict[str, str | Callable] = "first",
|
|
253
266
|
as_index: bool = True,
|
|
254
267
|
grid_size: float | int | None = None,
|
|
255
268
|
n_jobs: int = 1,
|
|
256
269
|
**dissolve_kwargs,
|
|
257
|
-
):
|
|
270
|
+
) -> GeoDataFrame:
|
|
258
271
|
"""Dissolves geometries.
|
|
259
272
|
|
|
260
273
|
It takes a GeoDataFrame and dissolves and fixes geometries.
|
|
@@ -265,6 +278,8 @@ def diss(
|
|
|
265
278
|
aggfunc: How to aggregate the non-geometry colums not in "by".
|
|
266
279
|
as_index: Whether the 'by' columns should be returned as index. Defaults to
|
|
267
280
|
True to be consistent with geopandas.
|
|
281
|
+
grid_size: Rounding of the coordinates. Defaults to None.
|
|
282
|
+
n_jobs: Number of threads to use. Defaults to 1.
|
|
268
283
|
**dissolve_kwargs: additional keyword arguments passed to geopandas' dissolve.
|
|
269
284
|
|
|
270
285
|
Returns:
|
|
@@ -292,14 +307,14 @@ def diss(
|
|
|
292
307
|
|
|
293
308
|
def dissexp(
|
|
294
309
|
gdf: GeoDataFrame,
|
|
295
|
-
by=None,
|
|
296
|
-
aggfunc="first",
|
|
310
|
+
by: str | Sequence[str] | None = None,
|
|
311
|
+
aggfunc: str | Callable | dict[str, str | Callable] = "first",
|
|
297
312
|
as_index: bool = True,
|
|
298
313
|
index_parts: bool = False,
|
|
299
314
|
grid_size: float | int | None = None,
|
|
300
315
|
n_jobs: int = 1,
|
|
301
316
|
**dissolve_kwargs,
|
|
302
|
-
):
|
|
317
|
+
) -> GeoDataFrame:
|
|
303
318
|
"""Dissolves overlapping geometries.
|
|
304
319
|
|
|
305
320
|
It takes a GeoDataFrame and dissolves, fixes and explodes geometries.
|
|
@@ -312,6 +327,8 @@ def dissexp(
|
|
|
312
327
|
True to be consistent with geopandas.
|
|
313
328
|
index_parts: If False (default), the index after dissolve is respected. If
|
|
314
329
|
True, an integer index level is added during explode.
|
|
330
|
+
grid_size: Rounding of the coordinates. Defaults to None.
|
|
331
|
+
n_jobs: Number of threads to use. Defaults to 1.
|
|
315
332
|
**dissolve_kwargs: additional keyword arguments passed to geopandas' dissolve.
|
|
316
333
|
|
|
317
334
|
Returns:
|
|
@@ -334,7 +351,7 @@ def dissexp(
|
|
|
334
351
|
|
|
335
352
|
|
|
336
353
|
def dissexp_by_cluster(
|
|
337
|
-
gdf: GeoDataFrame, predicate=None, n_jobs: int = 1, **dissolve_kwargs
|
|
354
|
+
gdf: GeoDataFrame, predicate: str | None = None, n_jobs: int = 1, **dissolve_kwargs
|
|
338
355
|
) -> GeoDataFrame:
|
|
339
356
|
"""Dissolves overlapping geometries through clustering with sjoin and networkx.
|
|
340
357
|
|
|
@@ -348,6 +365,8 @@ def dissexp_by_cluster(
|
|
|
348
365
|
|
|
349
366
|
Args:
|
|
350
367
|
gdf: the GeoDataFrame that will be dissolved and exploded.
|
|
368
|
+
predicate: Spatial predicate to use.
|
|
369
|
+
n_jobs: Number of threads to use. Defaults to 1.
|
|
351
370
|
**dissolve_kwargs: Keyword arguments passed to geopandas' dissolve.
|
|
352
371
|
|
|
353
372
|
Returns:
|
|
@@ -373,6 +392,8 @@ def diss_by_cluster(
|
|
|
373
392
|
|
|
374
393
|
Args:
|
|
375
394
|
gdf: the GeoDataFrame that will be dissolved and exploded.
|
|
395
|
+
predicate: Spatial predicate to use.
|
|
396
|
+
n_jobs: Number of threads to use. Defaults to 1.
|
|
376
397
|
**dissolve_kwargs: Keyword arguments passed to geopandas' dissolve.
|
|
377
398
|
|
|
378
399
|
Returns:
|
|
@@ -386,27 +407,10 @@ def diss_by_cluster(
|
|
|
386
407
|
def _run_func_by_cluster(
|
|
387
408
|
func: Callable,
|
|
388
409
|
gdf: GeoDataFrame,
|
|
389
|
-
predicate=None,
|
|
410
|
+
predicate: str | None = None,
|
|
390
411
|
n_jobs: int = 1,
|
|
391
412
|
**dissolve_kwargs,
|
|
392
413
|
) -> GeoDataFrame:
|
|
393
|
-
"""Dissolves overlapping geometries through clustering with sjoin and networkx.
|
|
394
|
-
|
|
395
|
-
Works exactly like dissexp, but, before dissolving, the geometries are divided
|
|
396
|
-
into clusters based on overlap (uses the function sgis.get_polygon_clusters).
|
|
397
|
-
The geometries are then dissolved based on this column (and optionally other
|
|
398
|
-
columns).
|
|
399
|
-
|
|
400
|
-
This might be many times faster than a regular dissexp, if there are many
|
|
401
|
-
non-overlapping geometries.
|
|
402
|
-
|
|
403
|
-
Args:
|
|
404
|
-
gdf: the GeoDataFrame that will be dissolved and exploded.
|
|
405
|
-
**dissolve_kwargs: Keyword arguments passed to geopandas' dissolve.
|
|
406
|
-
|
|
407
|
-
Returns:
|
|
408
|
-
A GeoDataFrame where overlapping geometries are dissolved.
|
|
409
|
-
"""
|
|
410
414
|
is_geoseries = isinstance(gdf, GeoSeries)
|
|
411
415
|
|
|
412
416
|
by = dissolve_kwargs.pop("by", [])
|
|
@@ -477,6 +481,7 @@ def buffdissexp_by_cluster(
|
|
|
477
481
|
resolution: The number of segments used to approximate a quarter circle.
|
|
478
482
|
Here defaults to 50, as opposed to the default 16 in geopandas.
|
|
479
483
|
copy: Whether to copy the GeoDataFrame before buffering. Defaults to True.
|
|
484
|
+
n_jobs: int = 1,
|
|
480
485
|
**dissolve_kwargs: additional keyword arguments passed to geopandas' dissolve.
|
|
481
486
|
|
|
482
487
|
Returns:
|
|
@@ -507,7 +512,6 @@ def buff(
|
|
|
507
512
|
Returns:
|
|
508
513
|
A buffered GeoDataFrame.
|
|
509
514
|
"""
|
|
510
|
-
|
|
511
515
|
if isinstance(gdf, GeoSeries):
|
|
512
516
|
return gdf.buffer(distance, resolution=resolution, **buffer_kwargs).make_valid()
|
|
513
517
|
|
|
@@ -1,51 +1,56 @@
|
|
|
1
1
|
import functools
|
|
2
|
+
import itertools
|
|
2
3
|
import warnings
|
|
3
4
|
|
|
4
5
|
import numpy as np
|
|
5
6
|
import pandas as pd
|
|
6
7
|
import shapely
|
|
7
|
-
from geopandas import GeoDataFrame
|
|
8
|
-
from geopandas
|
|
8
|
+
from geopandas import GeoDataFrame
|
|
9
|
+
from geopandas import GeoSeries
|
|
9
10
|
from numpy.typing import NDArray
|
|
10
|
-
from shapely import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
unary_union,
|
|
20
|
-
voronoi_polygons,
|
|
21
|
-
)
|
|
11
|
+
from shapely import STRtree
|
|
12
|
+
from shapely import distance
|
|
13
|
+
from shapely import extract_unique_points
|
|
14
|
+
from shapely import get_rings
|
|
15
|
+
from shapely import line_merge
|
|
16
|
+
from shapely import make_valid
|
|
17
|
+
from shapely import segmentize
|
|
18
|
+
from shapely import unary_union
|
|
19
|
+
from shapely import voronoi_polygons
|
|
22
20
|
from shapely.errors import GEOSException
|
|
23
21
|
from shapely.geometry import LineString
|
|
24
22
|
from shapely.ops import nearest_points
|
|
25
23
|
|
|
26
|
-
from ..maps.maps import explore
|
|
24
|
+
from ..maps.maps import explore
|
|
27
25
|
from ..networkanalysis.traveling_salesman import traveling_salesman_problem
|
|
28
|
-
from .conversion import to_gdf
|
|
29
|
-
from .
|
|
26
|
+
from .conversion import to_gdf
|
|
27
|
+
from .conversion import to_geoseries
|
|
28
|
+
from .general import clean_geoms
|
|
29
|
+
from .general import make_lines_between_points
|
|
30
|
+
from .general import sort_long_first
|
|
30
31
|
from .geometry_types import make_all_singlepart
|
|
31
|
-
from .sfilter import sfilter_inverse
|
|
32
|
-
|
|
32
|
+
from .sfilter import sfilter_inverse
|
|
33
|
+
from .sfilter import sfilter_split
|
|
33
34
|
|
|
34
35
|
warnings.simplefilter(action="ignore", category=FutureWarning)
|
|
35
36
|
|
|
36
37
|
|
|
37
|
-
def get_traveling_salesman_lines(
|
|
38
|
+
def get_traveling_salesman_lines(
|
|
39
|
+
df: GeoDataFrame, return_to_start: bool = False
|
|
40
|
+
) -> list[LineString]:
|
|
38
41
|
path = traveling_salesman_problem(df, return_to_start=return_to_start)
|
|
39
42
|
|
|
40
43
|
try:
|
|
41
|
-
return [LineString([p1, p2]) for p1, p2 in
|
|
44
|
+
return [LineString([p1, p2]) for p1, p2 in itertools.pairwise(path)]
|
|
42
45
|
except IndexError as e:
|
|
43
46
|
if len(path) == 1:
|
|
44
47
|
return path
|
|
45
48
|
raise e
|
|
46
49
|
|
|
47
50
|
|
|
48
|
-
def
|
|
51
|
+
def _remove_longest_if_not_intersecting(
|
|
52
|
+
centerlines: GeoDataFrame, geoms: GeoDataFrame
|
|
53
|
+
) -> GeoDataFrame:
|
|
49
54
|
centerlines = sort_long_first(make_all_singlepart(centerlines))
|
|
50
55
|
|
|
51
56
|
has_only_one_line = centerlines.groupby(level=0).size() == 1
|
|
@@ -83,8 +88,7 @@ def get_rough_centerlines(
|
|
|
83
88
|
complext polygons like (buffered) road networks.
|
|
84
89
|
|
|
85
90
|
"""
|
|
86
|
-
|
|
87
|
-
PRECISION = 0.01
|
|
91
|
+
precision = 0.01
|
|
88
92
|
|
|
89
93
|
if not len(gdf):
|
|
90
94
|
return gdf
|
|
@@ -96,12 +100,12 @@ def get_rough_centerlines(
|
|
|
96
100
|
|
|
97
101
|
segmentized: GeoSeries = segmentize(geoms, max_segment_length=max_segment_length)
|
|
98
102
|
|
|
99
|
-
points: GeoSeries =
|
|
103
|
+
points: GeoSeries = _get_points_in_polygons(segmentized, precision)
|
|
100
104
|
|
|
101
105
|
has_no_points = geoms.loc[(~geoms.index.isin(points.index))]
|
|
102
106
|
|
|
103
|
-
more_points: GeoSeries =
|
|
104
|
-
has_no_points.buffer(
|
|
107
|
+
more_points: GeoSeries = _get_points_in_polygons(
|
|
108
|
+
has_no_points.buffer(precision), precision
|
|
105
109
|
)
|
|
106
110
|
|
|
107
111
|
# Geometries that have no lines inside, might be perfect circles.
|
|
@@ -131,7 +135,7 @@ def get_rough_centerlines(
|
|
|
131
135
|
]
|
|
132
136
|
|
|
133
137
|
# make sure to include the endpoints
|
|
134
|
-
endpoints =
|
|
138
|
+
endpoints = _get_approximate_polygon_endpoints(segmentized)
|
|
135
139
|
|
|
136
140
|
geoms = geoms.loc[~geoms.index.isin(still_has_no_points.index)]
|
|
137
141
|
|
|
@@ -148,7 +152,7 @@ def get_rough_centerlines(
|
|
|
148
152
|
# keep lines 90 percent intersecting the polygon
|
|
149
153
|
length_now = end_to_end.length
|
|
150
154
|
end_to_end = (
|
|
151
|
-
end_to_end.intersection(geoms.buffer(
|
|
155
|
+
end_to_end.intersection(geoms.buffer(precision))
|
|
152
156
|
.dropna()
|
|
153
157
|
.loc[lambda x: x.length > length_now * 0.9]
|
|
154
158
|
)
|
|
@@ -157,7 +161,7 @@ def get_rough_centerlines(
|
|
|
157
161
|
to_be_erased = points.index.isin(end_to_end.index)
|
|
158
162
|
|
|
159
163
|
dont_intersect = sfilter_inverse(
|
|
160
|
-
points.iloc[to_be_erased], end_to_end.buffer(
|
|
164
|
+
points.iloc[to_be_erased], end_to_end.buffer(precision, cap_style=2)
|
|
161
165
|
)
|
|
162
166
|
|
|
163
167
|
points = (
|
|
@@ -184,7 +188,7 @@ def get_rough_centerlines(
|
|
|
184
188
|
|
|
185
189
|
explore(points=to_gdf(points, 25833), gdf=gdf)
|
|
186
190
|
|
|
187
|
-
remove_longest = functools.partial(
|
|
191
|
+
remove_longest = functools.partial(_remove_longest_if_not_intersecting, geoms=geoms)
|
|
188
192
|
|
|
189
193
|
centerlines = GeoSeries(
|
|
190
194
|
points.groupby(level=0).apply(get_traveling_salesman_lines).explode()
|
|
@@ -235,7 +239,7 @@ def get_rough_centerlines(
|
|
|
235
239
|
return centerlines
|
|
236
240
|
|
|
237
241
|
|
|
238
|
-
def
|
|
242
|
+
def _get_points_in_polygons(geometries: GeoSeries, precision: float) -> GeoSeries:
|
|
239
243
|
# voronoi can cause problems if coordinates are nearly identical
|
|
240
244
|
# buffering solves it
|
|
241
245
|
try:
|
|
@@ -267,7 +271,7 @@ def get_points_in_polygons(geometries: GeoSeries, precision: float) -> GeoSeries
|
|
|
267
271
|
return pd.concat([within_polygons, not_within_but_relevant]).centroid
|
|
268
272
|
|
|
269
273
|
|
|
270
|
-
def
|
|
274
|
+
def _get_approximate_polygon_endpoints(geoms: GeoSeries) -> GeoSeries:
|
|
271
275
|
out_geoms = []
|
|
272
276
|
|
|
273
277
|
are_thin = geoms.buffer(-1e-2).is_empty
|
|
@@ -332,7 +336,7 @@ def get_approximate_polygon_endpoints(geoms: GeoSeries) -> GeoSeries:
|
|
|
332
336
|
|
|
333
337
|
out_geoms.append(nearest_geom_points)
|
|
334
338
|
|
|
335
|
-
lines_around_geometries =
|
|
339
|
+
lines_around_geometries = _multipoints_to_line_segments(
|
|
336
340
|
extract_unique_points(rectangles)
|
|
337
341
|
)
|
|
338
342
|
|
|
@@ -370,7 +374,7 @@ def get_approximate_polygon_endpoints(geoms: GeoSeries) -> GeoSeries:
|
|
|
370
374
|
return pd.concat(out_geoms)
|
|
371
375
|
|
|
372
376
|
|
|
373
|
-
def
|
|
377
|
+
def _multipoints_to_line_segments(
|
|
374
378
|
multipoints: GeoSeries | GeoDataFrame, to_next: bool = True, cycle: bool = True
|
|
375
379
|
) -> GeoSeries | GeoDataFrame:
|
|
376
380
|
if not len(multipoints):
|
|
@@ -384,13 +388,13 @@ def multipoints_to_line_segments(
|
|
|
384
388
|
for i in range(multipoints.index.nlevels)
|
|
385
389
|
]
|
|
386
390
|
multipoints.index = pd.MultiIndex.from_arrays(
|
|
387
|
-
[list(range(len(multipoints)))
|
|
388
|
-
names=["range_idx"
|
|
391
|
+
[list(range(len(multipoints))), *index],
|
|
392
|
+
names=["range_idx", *multipoints.index.names],
|
|
389
393
|
)
|
|
390
394
|
else:
|
|
391
395
|
multipoints.index = pd.MultiIndex.from_arrays(
|
|
392
396
|
[np.arange(0, len(multipoints)), multipoints.index],
|
|
393
|
-
names=["range_idx"
|
|
397
|
+
names=["range_idx", multipoints.index.name],
|
|
394
398
|
)
|
|
395
399
|
|
|
396
400
|
try:
|
|
@@ -402,15 +406,17 @@ def multipoints_to_line_segments(
|
|
|
402
406
|
|
|
403
407
|
if to_next:
|
|
404
408
|
shift = -1
|
|
405
|
-
|
|
409
|
+
keep = "first"
|
|
406
410
|
else:
|
|
407
411
|
shift = 1
|
|
408
|
-
|
|
412
|
+
keep = "last"
|
|
409
413
|
|
|
410
414
|
point_df["next"] = point_df.groupby(level=0)["geometry"].shift(shift)
|
|
411
415
|
|
|
412
416
|
if cycle:
|
|
413
|
-
first_points = point_df.loc[
|
|
417
|
+
first_points: GeoSeries = point_df.loc[
|
|
418
|
+
lambda x: ~x.index.get_level_values(0).duplicated(keep=keep), "geometry"
|
|
419
|
+
]
|
|
414
420
|
is_last_point = point_df["next"].isna()
|
|
415
421
|
|
|
416
422
|
point_df.loc[is_last_point, "next"] = first_points
|
|
@@ -419,7 +425,8 @@ def multipoints_to_line_segments(
|
|
|
419
425
|
point_df = point_df[point_df["next"].notna()]
|
|
420
426
|
|
|
421
427
|
point_df["geometry"] = [
|
|
422
|
-
LineString([x1, x2])
|
|
428
|
+
LineString([x1, x2])
|
|
429
|
+
for x1, x2 in zip(point_df["geometry"], point_df["next"], strict=False)
|
|
423
430
|
]
|
|
424
431
|
if isinstance(multipoints.index, pd.MultiIndex):
|
|
425
432
|
point_df.index = point_df.index.droplevel(0)
|
|
@@ -431,7 +438,9 @@ def multipoints_to_line_segments(
|
|
|
431
438
|
return GeoSeries(point_df["geometry"], crs=crs)
|
|
432
439
|
|
|
433
440
|
|
|
434
|
-
def get_line_segments(
|
|
441
|
+
def get_line_segments(
|
|
442
|
+
lines: GeoDataFrame | GeoSeries, extract_unique: bool = False, cycle=False
|
|
443
|
+
) -> GeoDataFrame:
|
|
435
444
|
try:
|
|
436
445
|
assert lines.index.is_unique
|
|
437
446
|
except AttributeError:
|
|
@@ -445,4 +454,4 @@ def get_line_segments(lines, extract_unique: bool = False, cycle=False) -> GeoDa
|
|
|
445
454
|
coords, indices = shapely.get_coordinates(lines, return_index=True)
|
|
446
455
|
points = GeoSeries(shapely.points(coords), index=indices)
|
|
447
456
|
|
|
448
|
-
return
|
|
457
|
+
return _multipoints_to_line_segments(points, cycle=cycle)
|