ssb-sgis 1.0.1__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.1.dist-info → ssb_sgis-1.0.2.dist-info}/LICENSE +1 -1
- {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.2.dist-info}/METADATA +87 -16
- ssb_sgis-1.0.2.dist-info/RECORD +61 -0
- {ssb_sgis-1.0.1.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.1.dist-info/RECORD +0 -63
sgis/geopandas_tools/cleaning.py
CHANGED
|
@@ -1,65 +1,43 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import warnings
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
|
-
import networkx as nx
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pandas as pd
|
|
8
8
|
import shapely
|
|
9
|
-
from geopandas import GeoDataFrame
|
|
9
|
+
from geopandas import GeoDataFrame
|
|
10
|
+
from geopandas import GeoSeries
|
|
10
11
|
from geopandas.array import GeometryArray
|
|
11
12
|
from numpy.typing import NDArray
|
|
12
|
-
from shapely import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
force_2d,
|
|
17
|
-
get_coordinates,
|
|
18
|
-
get_exterior_ring,
|
|
19
|
-
get_parts,
|
|
20
|
-
linearrings,
|
|
21
|
-
linestrings,
|
|
22
|
-
make_valid,
|
|
23
|
-
multipoints,
|
|
24
|
-
polygons,
|
|
25
|
-
reverse,
|
|
26
|
-
segmentize,
|
|
27
|
-
simplify,
|
|
28
|
-
unary_union,
|
|
29
|
-
)
|
|
13
|
+
from shapely import extract_unique_points
|
|
14
|
+
from shapely import get_coordinates
|
|
15
|
+
from shapely import get_parts
|
|
16
|
+
from shapely import linestrings
|
|
30
17
|
from shapely.errors import GEOSException
|
|
31
|
-
from shapely.geometry import
|
|
32
|
-
from shapely.
|
|
18
|
+
from shapely.geometry import LineString
|
|
19
|
+
from shapely.geometry import Point
|
|
33
20
|
|
|
34
|
-
from
|
|
35
|
-
from
|
|
36
|
-
from .
|
|
37
|
-
from .conversion import
|
|
38
|
-
from .duplicates import get_intersections
|
|
21
|
+
from .buffer_dissolve_explode import buff
|
|
22
|
+
from .buffer_dissolve_explode import dissexp
|
|
23
|
+
from .conversion import coordinate_array
|
|
24
|
+
from .conversion import to_gdf
|
|
25
|
+
from .duplicates import get_intersections
|
|
26
|
+
from .duplicates import update_geometries
|
|
39
27
|
|
|
40
28
|
# from .general import sort_large_first as _sort_large_first
|
|
41
|
-
from .general import
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
to_lines,
|
|
48
|
-
)
|
|
49
|
-
from .geometry_types import get_geom_type, make_all_singlepart, to_single_geom_type
|
|
50
|
-
from .neighbors import get_k_nearest_neighbors, get_neighbor_indices
|
|
29
|
+
from .general import clean_geoms
|
|
30
|
+
from .general import sort_large_first
|
|
31
|
+
from .general import sort_small_first
|
|
32
|
+
from .general import to_lines
|
|
33
|
+
from .geometry_types import make_all_singlepart
|
|
34
|
+
from .geometry_types import to_single_geom_type
|
|
51
35
|
from .overlay import clean_overlay
|
|
52
|
-
from .polygon_operations import
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
get_cluster_mapper,
|
|
58
|
-
get_gaps,
|
|
59
|
-
)
|
|
60
|
-
from .polygons_as_rings import PolygonsAsRings
|
|
61
|
-
from .sfilter import sfilter, sfilter_inverse, sfilter_split
|
|
62
|
-
|
|
36
|
+
from .polygon_operations import eliminate_by_longest
|
|
37
|
+
from .polygon_operations import get_cluster_mapper
|
|
38
|
+
from .polygon_operations import get_gaps
|
|
39
|
+
from .sfilter import sfilter_inverse
|
|
40
|
+
from .sfilter import sfilter_split
|
|
63
41
|
|
|
64
42
|
warnings.simplefilter(action="ignore", category=UserWarning)
|
|
65
43
|
warnings.simplefilter(action="ignore", category=RuntimeWarning)
|
|
@@ -74,8 +52,6 @@ def coverage_clean(
|
|
|
74
52
|
tolerance: int | float,
|
|
75
53
|
duplicate_action: str = "fix",
|
|
76
54
|
grid_sizes: tuple[None | int] = (None,),
|
|
77
|
-
logger=None,
|
|
78
|
-
mask=None,
|
|
79
55
|
n_jobs: int = 1,
|
|
80
56
|
) -> GeoDataFrame:
|
|
81
57
|
"""Fix thin gaps, holes, slivers and double surfaces.
|
|
@@ -102,7 +78,7 @@ def coverage_clean(
|
|
|
102
78
|
for polygons to be eliminated. Any gap, hole, sliver or double
|
|
103
79
|
surface that are empty after a negative buffer of tolerance / 2
|
|
104
80
|
are eliminated into the neighbor with the longest shared border.
|
|
105
|
-
|
|
81
|
+
duplicate_action: Either "fix", "error" or "ignore".
|
|
106
82
|
If "fix" (default), double surfaces thicker than the
|
|
107
83
|
tolerance will be updated from top to bottom (function update_geometries)
|
|
108
84
|
and then dissolved into the neighbor polygon with the longest shared border.
|
|
@@ -110,36 +86,11 @@ def coverage_clean(
|
|
|
110
86
|
than the tolerance. If "ignore", double surfaces are kept as is.
|
|
111
87
|
grid_sizes: One or more grid_sizes used in overlay and dissolve operations that
|
|
112
88
|
might raise a GEOSException. Defaults to (None,), meaning no grid_sizes.
|
|
89
|
+
n_jobs: Number of threads.
|
|
113
90
|
|
|
114
91
|
Returns:
|
|
115
92
|
A GeoDataFrame with cleaned polygons.
|
|
116
|
-
|
|
117
|
-
Examples
|
|
118
|
-
--------
|
|
119
|
-
|
|
120
|
-
>>> cleaned = coverage_clean(
|
|
121
|
-
... gdf,
|
|
122
|
-
... 0.1,
|
|
123
|
-
... grid_sizes=[None, 1e-6, 1e-5, 1e-4, 1e-3],
|
|
124
|
-
... )
|
|
125
|
-
|
|
126
|
-
If you have a known mask for your coverage, e.g. municipality polygons,
|
|
127
|
-
it might be a good idea to buffer the gaps, slivers and double surfaces
|
|
128
|
-
before elimination to make sure the polygons are properly dissolved.
|
|
129
|
-
|
|
130
|
-
>>> def _small_buffer(df):
|
|
131
|
-
... df.geometry = df.buffer(0.001)
|
|
132
|
-
... return df
|
|
133
|
-
...
|
|
134
|
-
>>> cleaned = coverage_clean(
|
|
135
|
-
... gdf,
|
|
136
|
-
... 0.1,
|
|
137
|
-
... grid_sizes=[None, 1e-6, 1e-5, 1e-4, 1e-3],
|
|
138
|
-
... pre_dissolve_func=_small_buffer,
|
|
139
|
-
... ).pipe(sg.clean_clip, your_mask, geom_type="polygon")
|
|
140
|
-
|
|
141
93
|
"""
|
|
142
|
-
|
|
143
94
|
if not len(gdf):
|
|
144
95
|
return gdf
|
|
145
96
|
|
|
@@ -153,7 +104,7 @@ def coverage_clean(
|
|
|
153
104
|
]
|
|
154
105
|
|
|
155
106
|
try:
|
|
156
|
-
gdf =
|
|
107
|
+
gdf = _safe_simplify(gdf, PRECISION)
|
|
157
108
|
except GEOSException:
|
|
158
109
|
pass
|
|
159
110
|
|
|
@@ -175,7 +126,7 @@ def coverage_clean(
|
|
|
175
126
|
break
|
|
176
127
|
except GEOSException as e:
|
|
177
128
|
if i == len(grid_sizes) - 1:
|
|
178
|
-
explore_geosexception(e, gdf
|
|
129
|
+
explore_geosexception(e, gdf)
|
|
179
130
|
raise e
|
|
180
131
|
|
|
181
132
|
gaps["_was_gap"] = 1
|
|
@@ -262,7 +213,7 @@ def coverage_clean(
|
|
|
262
213
|
break
|
|
263
214
|
except GEOSException as e:
|
|
264
215
|
if i == len(grid_sizes) - 1:
|
|
265
|
-
explore_geosexception(e, gdf, intersecting, isolated
|
|
216
|
+
explore_geosexception(e, gdf, intersecting, isolated)
|
|
266
217
|
raise e
|
|
267
218
|
|
|
268
219
|
not_really_isolated = isolated[["geometry", "_eliminate_idx", "_cluster"]].merge(
|
|
@@ -333,9 +284,7 @@ def coverage_clean(
|
|
|
333
284
|
break
|
|
334
285
|
except GEOSException as e:
|
|
335
286
|
if i == len(grid_sizes) - 1:
|
|
336
|
-
explore_geosexception(
|
|
337
|
-
e, gdf, without_double, isolated, really_isolated, logger=logger
|
|
338
|
-
)
|
|
287
|
+
explore_geosexception(e, gdf, without_double, isolated, really_isolated)
|
|
339
288
|
raise e
|
|
340
289
|
|
|
341
290
|
cleaned = pd.concat([many_hits, one_hit], ignore_index=True)
|
|
@@ -349,6 +298,7 @@ def coverage_clean(
|
|
|
349
298
|
cleaned,
|
|
350
299
|
how="update",
|
|
351
300
|
geom_type="polygon",
|
|
301
|
+
grid_size=grid_size,
|
|
352
302
|
n_jobs=n_jobs,
|
|
353
303
|
)
|
|
354
304
|
break
|
|
@@ -361,7 +311,6 @@ def coverage_clean(
|
|
|
361
311
|
without_double,
|
|
362
312
|
isolated,
|
|
363
313
|
really_isolated,
|
|
364
|
-
logger=logger,
|
|
365
314
|
)
|
|
366
315
|
raise e
|
|
367
316
|
|
|
@@ -388,11 +337,10 @@ def coverage_clean(
|
|
|
388
337
|
without_double,
|
|
389
338
|
isolated,
|
|
390
339
|
really_isolated,
|
|
391
|
-
logger=logger,
|
|
392
340
|
)
|
|
393
341
|
raise e
|
|
394
342
|
|
|
395
|
-
# cleaned =
|
|
343
|
+
# cleaned = _safe_simplify(cleaned, PRECISION)
|
|
396
344
|
# cleaned.geometry = shapely.make_valid(cleaned.geometry)
|
|
397
345
|
|
|
398
346
|
# TODO check why polygons dissappear in rare cases. For now, just add back the missing
|
|
@@ -402,7 +350,7 @@ def coverage_clean(
|
|
|
402
350
|
return to_single_geom_type(cleaned, "polygon")
|
|
403
351
|
|
|
404
352
|
|
|
405
|
-
def
|
|
353
|
+
def _safe_simplify(gdf: GeoDataFrame, tolerance: float | int, **kwargs) -> GeoDataFrame:
|
|
406
354
|
"""Simplify only if the resulting area is no more than 1 percent larger.
|
|
407
355
|
|
|
408
356
|
Because simplifying can result in holes being filled.
|
|
@@ -420,7 +368,7 @@ def safe_simplify(gdf, tolerance: float | int, **kwargs):
|
|
|
420
368
|
return copied
|
|
421
369
|
|
|
422
370
|
|
|
423
|
-
def
|
|
371
|
+
def _remove_interior_slivers(gdf: GeoDataFrame, tolerance: int | float) -> GeoDataFrame:
|
|
424
372
|
gdf, slivers = split_out_slivers(gdf, tolerance)
|
|
425
373
|
slivers["_idx"] = range(len(slivers))
|
|
426
374
|
without_thick = clean_overlay(
|
|
@@ -439,14 +387,29 @@ def remove_interior_slivers(gdf, tolerance):
|
|
|
439
387
|
def remove_spikes(
|
|
440
388
|
gdf: GeoDataFrame, tolerance: int | float, n_jobs: int = 1
|
|
441
389
|
) -> GeoDataFrame:
|
|
390
|
+
"""Remove thin spikes from polygons.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
gdf: A GeoDataFrame.
|
|
394
|
+
tolerance: Spike tolerance.
|
|
395
|
+
n_jobs: Number of threads.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
A GeoDataFrame.
|
|
399
|
+
"""
|
|
442
400
|
return clean_overlay(
|
|
443
401
|
gdf, gdf[["geometry"]], how="intersection", grid_size=tolerance, n_jobs=n_jobs
|
|
444
402
|
)
|
|
445
403
|
|
|
446
404
|
|
|
447
405
|
def _properly_fix_duplicates(
|
|
448
|
-
gdf
|
|
449
|
-
|
|
406
|
+
gdf: GeoDataFrame,
|
|
407
|
+
double: GeoDataFrame,
|
|
408
|
+
slivers: GeoDataFrame,
|
|
409
|
+
thin_gaps_and_double: GeoDataFrame,
|
|
410
|
+
tolerance: int | float,
|
|
411
|
+
n_jobs: int,
|
|
412
|
+
) -> GeoDataFrame:
|
|
450
413
|
gdf = _dissolve_thick_double_and_update(gdf, double, thin_gaps_and_double, n_jobs)
|
|
451
414
|
gdf, more_slivers = split_out_slivers(gdf, tolerance)
|
|
452
415
|
slivers = pd.concat([slivers, more_slivers], ignore_index=True)
|
|
@@ -462,7 +425,9 @@ def _properly_fix_duplicates(
|
|
|
462
425
|
return gdf, thin_gaps_and_double, slivers
|
|
463
426
|
|
|
464
427
|
|
|
465
|
-
def _dissolve_thick_double_and_update(
|
|
428
|
+
def _dissolve_thick_double_and_update(
|
|
429
|
+
gdf: GeoDataFrame, double: GeoDataFrame, thin_double: GeoDataFrame, n_jobs: int
|
|
430
|
+
) -> GeoDataFrame:
|
|
466
431
|
large = (
|
|
467
432
|
double.loc[~double["_double_idx"].isin(thin_double["_double_idx"])].drop(
|
|
468
433
|
columns="_double_idx"
|
|
@@ -479,7 +444,9 @@ def _dissolve_thick_double_and_update(gdf, double, thin_double, n_jobs):
|
|
|
479
444
|
)
|
|
480
445
|
|
|
481
446
|
|
|
482
|
-
def _cleaning_checks(
|
|
447
|
+
def _cleaning_checks(
|
|
448
|
+
gdf: GeoDataFrame, tolerance: int | float, duplicate_action: bool
|
|
449
|
+
) -> GeoDataFrame: # , spike_action):
|
|
483
450
|
if not len(gdf) or not tolerance:
|
|
484
451
|
return gdf
|
|
485
452
|
if tolerance < PRECISION:
|
|
@@ -503,11 +470,11 @@ def split_out_slivers(
|
|
|
503
470
|
|
|
504
471
|
|
|
505
472
|
def try_for_grid_size(
|
|
506
|
-
func,
|
|
473
|
+
func: Callable,
|
|
507
474
|
grid_sizes: tuple[None, float | int],
|
|
508
475
|
args: tuple | None = None,
|
|
509
476
|
kwargs: dict | None = None,
|
|
510
|
-
):
|
|
477
|
+
) -> Any:
|
|
511
478
|
args = args or ()
|
|
512
479
|
kwargs = kwargs or {}
|
|
513
480
|
for i, grid_size in enumerate(grid_sizes):
|
|
@@ -523,7 +490,6 @@ def split_and_eliminate_by_longest(
|
|
|
523
490
|
to_eliminate: GeoDataFrame,
|
|
524
491
|
tolerance: int | float,
|
|
525
492
|
grid_sizes: tuple[None | float | int] = (None,),
|
|
526
|
-
logger=None,
|
|
527
493
|
n_jobs: int = 1,
|
|
528
494
|
**kwargs,
|
|
529
495
|
) -> GeoDataFrame | tuple[GeoDataFrame]:
|
|
@@ -585,7 +551,12 @@ def split_and_eliminate_by_longest(
|
|
|
585
551
|
)
|
|
586
552
|
|
|
587
553
|
|
|
588
|
-
def split_by_neighbors(
|
|
554
|
+
def split_by_neighbors(
|
|
555
|
+
df: GeoDataFrame,
|
|
556
|
+
split_by: GeoDataFrame,
|
|
557
|
+
tolerance: int | float,
|
|
558
|
+
grid_size: float | int | None = None,
|
|
559
|
+
) -> GeoDataFrame:
|
|
589
560
|
if not len(df):
|
|
590
561
|
return df
|
|
591
562
|
|
|
@@ -621,7 +592,7 @@ def split_by_neighbors(df, split_by, tolerance, grid_size=None):
|
|
|
621
592
|
return clean_overlay(df, buffered, how="identity", grid_size=grid_size)
|
|
622
593
|
|
|
623
594
|
|
|
624
|
-
def extend_lines(arr1, arr2, distance):
|
|
595
|
+
def extend_lines(arr1, arr2, distance) -> NDArray[LineString]:
|
|
625
596
|
if len(arr1) != len(arr2):
|
|
626
597
|
raise ValueError
|
|
627
598
|
if not len(arr1):
|
|
@@ -664,7 +635,7 @@ def make_lines_between_points(
|
|
|
664
635
|
return linestrings(coords.values, indices=coords.index)
|
|
665
636
|
|
|
666
637
|
|
|
667
|
-
def get_line_segments(lines) -> GeoDataFrame:
|
|
638
|
+
def get_line_segments(lines: GeoDataFrame | GeoSeries) -> GeoDataFrame:
|
|
668
639
|
assert lines.index.is_unique
|
|
669
640
|
if isinstance(lines, GeoDataFrame):
|
|
670
641
|
geom_col = lines._geometry_column_name
|
|
@@ -711,7 +682,8 @@ def multipoints_to_line_segments(multipoints: GeoSeries) -> GeoDataFrame:
|
|
|
711
682
|
assert point_df["next"].notna().all()
|
|
712
683
|
|
|
713
684
|
point_df["geometry"] = [
|
|
714
|
-
LineString([x1, x2])
|
|
685
|
+
LineString([x1, x2])
|
|
686
|
+
for x1, x2 in zip(point_df["geometry"], point_df["next"], strict=False)
|
|
715
687
|
]
|
|
716
688
|
return GeoDataFrame(point_df.drop(columns=["next"]), geometry="geometry", crs=crs)
|
|
717
689
|
|
|
@@ -727,16 +699,27 @@ def points_to_line_segments(points: GeoDataFrame) -> GeoDataFrame:
|
|
|
727
699
|
assert points["next"].notna().all()
|
|
728
700
|
|
|
729
701
|
points["geometry"] = [
|
|
730
|
-
LineString([x1, x2])
|
|
702
|
+
LineString([x1, x2])
|
|
703
|
+
for x1, x2 in zip(points["geometry"], points["next"], strict=False)
|
|
731
704
|
]
|
|
732
705
|
return GeoDataFrame(
|
|
733
706
|
points.drop(columns=["next"]), geometry="geometry", crs=points.crs
|
|
734
707
|
)
|
|
735
708
|
|
|
736
709
|
|
|
737
|
-
def explore_geosexception(
|
|
738
|
-
|
|
739
|
-
|
|
710
|
+
def explore_geosexception(
|
|
711
|
+
e: GEOSException, *gdfs: GeoDataFrame, logger: Any | None = None
|
|
712
|
+
) -> None:
|
|
713
|
+
"""Extract the coordinates of a GEOSException and show in map.
|
|
714
|
+
|
|
715
|
+
Args:
|
|
716
|
+
e: The exception thrown by a GEOS operation, which potentially contains coordinates information.
|
|
717
|
+
*gdfs: One or more GeoDataFrames to display for context in the map.
|
|
718
|
+
logger: An optional logger to log the error with visualization. If None, uses standard output.
|
|
719
|
+
|
|
720
|
+
"""
|
|
721
|
+
from ..maps.maps import Explore
|
|
722
|
+
from ..maps.maps import explore
|
|
740
723
|
|
|
741
724
|
pattern = r"(\d+\.\d+)\s+(\d+\.\d+)"
|
|
742
725
|
|