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
|
@@ -1,59 +1,26 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any
|
|
2
3
|
|
|
3
|
-
import geopandas as gpd
|
|
4
|
-
import igraph
|
|
5
|
-
import networkx as nx
|
|
6
4
|
import numpy as np
|
|
7
5
|
import pandas as pd
|
|
8
|
-
from geopandas import GeoDataFrame
|
|
6
|
+
from geopandas import GeoDataFrame
|
|
7
|
+
from geopandas import GeoSeries
|
|
9
8
|
from geopandas.array import GeometryArray
|
|
10
|
-
from IPython.display import display
|
|
11
|
-
from networkx.algorithms import approximation as approx
|
|
12
|
-
from numpy import ndarray
|
|
13
9
|
from numpy.typing import NDArray
|
|
14
|
-
from
|
|
15
|
-
from shapely import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
get_num_interior_rings,
|
|
29
|
-
get_parts,
|
|
30
|
-
intersection,
|
|
31
|
-
intersects,
|
|
32
|
-
is_empty,
|
|
33
|
-
is_ring,
|
|
34
|
-
length,
|
|
35
|
-
line_merge,
|
|
36
|
-
linearrings,
|
|
37
|
-
linestrings,
|
|
38
|
-
make_valid,
|
|
39
|
-
points,
|
|
40
|
-
polygons,
|
|
41
|
-
segmentize,
|
|
42
|
-
simplify,
|
|
43
|
-
unary_union,
|
|
44
|
-
voronoi_polygons,
|
|
45
|
-
)
|
|
46
|
-
from shapely.errors import GEOSException
|
|
47
|
-
from shapely.geometry import (
|
|
48
|
-
LinearRing,
|
|
49
|
-
LineString,
|
|
50
|
-
MultiLineString,
|
|
51
|
-
MultiPoint,
|
|
52
|
-
Point,
|
|
53
|
-
Polygon,
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
from .conversion import to_gdf, to_geoseries
|
|
10
|
+
from pyproj import CRS
|
|
11
|
+
from shapely import get_coordinates
|
|
12
|
+
from shapely import get_exterior_ring
|
|
13
|
+
from shapely import get_interior_ring
|
|
14
|
+
from shapely import get_num_interior_rings
|
|
15
|
+
from shapely import linearrings
|
|
16
|
+
from shapely import make_valid
|
|
17
|
+
from shapely import polygons
|
|
18
|
+
from shapely import unary_union
|
|
19
|
+
from shapely.geometry import LinearRing
|
|
20
|
+
from shapely.geometry import Polygon
|
|
21
|
+
|
|
22
|
+
from .conversion import to_gdf
|
|
23
|
+
from .conversion import to_geoseries
|
|
57
24
|
|
|
58
25
|
|
|
59
26
|
class PolygonsAsRings:
|
|
@@ -62,10 +29,18 @@ class PolygonsAsRings:
|
|
|
62
29
|
def __init__(
|
|
63
30
|
self,
|
|
64
31
|
polys: GeoDataFrame | GeoSeries | GeometryArray,
|
|
65
|
-
crs=None,
|
|
32
|
+
crs: CRS | Any | None = None,
|
|
66
33
|
allow_multipart: bool = False,
|
|
67
34
|
gridsize: int | None = None,
|
|
68
|
-
):
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Initialize the PolygonsAsRings object with polygons and optional CRS information.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
polys: GeoDataFrame, GeoSeries, or GeometryArray containing polygon geometries.
|
|
40
|
+
crs: Coordinate Reference System to be used, defaults to None.
|
|
41
|
+
allow_multipart: Allow multipart polygons if True, defaults to False.
|
|
42
|
+
gridsize: Size of the grid for any grid operations, defaults to None.
|
|
43
|
+
"""
|
|
69
44
|
if not isinstance(polys, (pd.DataFrame, pd.Series, GeometryArray)):
|
|
70
45
|
raise TypeError(type(polys))
|
|
71
46
|
|
|
@@ -125,7 +100,16 @@ class PolygonsAsRings:
|
|
|
125
100
|
|
|
126
101
|
self.rings = pd.concat([exterior, interiors])
|
|
127
102
|
|
|
128
|
-
def get_rings(self, agg: bool = False):
|
|
103
|
+
def get_rings(self, agg: bool = False) -> GeoDataFrame | GeoSeries | np.ndarray:
|
|
104
|
+
"""Retrieve rings from the polygons, optionally aggregating them.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
agg: If True, aggregate the rings into single geometries.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The rings either aggregated or separated, in the type of
|
|
111
|
+
the input polygons.
|
|
112
|
+
"""
|
|
129
113
|
gdf = self.gdf.copy()
|
|
130
114
|
rings = self.rings.copy()
|
|
131
115
|
if not len(rings):
|
|
@@ -144,9 +128,21 @@ class PolygonsAsRings:
|
|
|
144
128
|
return self.polyclass(gdf.geometry.values)
|
|
145
129
|
|
|
146
130
|
def apply_numpy_func_to_interiors(
|
|
147
|
-
self,
|
|
148
|
-
|
|
149
|
-
|
|
131
|
+
self,
|
|
132
|
+
func: Callable,
|
|
133
|
+
args: tuple | None = None,
|
|
134
|
+
kwargs: dict | None = None,
|
|
135
|
+
) -> "PolygonsAsRings":
|
|
136
|
+
"""Apply a numpy function specifically to the interior rings of the polygons.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
func: Numpy function to apply.
|
|
140
|
+
args: Tuple of positional arguments for the function.
|
|
141
|
+
kwargs: Dictionary of keyword arguments for the function.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
PolygonsAsRings: The instance itself after applying the function.
|
|
145
|
+
"""
|
|
150
146
|
kwargs = kwargs or {}
|
|
151
147
|
args = args or ()
|
|
152
148
|
arr: NDArray[LinearRing] = self.rings.loc[self.is_interior].values
|
|
@@ -159,9 +155,21 @@ class PolygonsAsRings:
|
|
|
159
155
|
return self
|
|
160
156
|
|
|
161
157
|
def apply_numpy_func(
|
|
162
|
-
self,
|
|
163
|
-
|
|
164
|
-
|
|
158
|
+
self,
|
|
159
|
+
func: Callable,
|
|
160
|
+
args: tuple | None = None,
|
|
161
|
+
kwargs: dict | None = None,
|
|
162
|
+
) -> "PolygonsAsRings":
|
|
163
|
+
"""Apply a numpy function to all rings of the polygons.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
func: Numpy function to apply.
|
|
167
|
+
args: Tuple of positional arguments for the function.
|
|
168
|
+
kwargs: Dictionary of keyword arguments for the function.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
PolygonsAsRings: The instance itself after applying the function.
|
|
172
|
+
"""
|
|
165
173
|
kwargs = kwargs or {}
|
|
166
174
|
args = args or ()
|
|
167
175
|
|
|
@@ -172,18 +180,30 @@ class PolygonsAsRings:
|
|
|
172
180
|
f"Different length of results. Got {len(results)} and {len(self.rings)} original rings"
|
|
173
181
|
)
|
|
174
182
|
|
|
175
|
-
self.rings.loc[:] = results
|
|
183
|
+
self.rings.loc[:] = results # type: ignore [call-overload]
|
|
176
184
|
|
|
177
185
|
return self
|
|
178
186
|
|
|
179
187
|
def apply_geoseries_func(
|
|
180
|
-
self,
|
|
181
|
-
|
|
182
|
-
|
|
188
|
+
self,
|
|
189
|
+
func: Callable,
|
|
190
|
+
args: tuple | None = None,
|
|
191
|
+
kwargs: dict | None = None,
|
|
192
|
+
) -> "PolygonsAsRings":
|
|
193
|
+
"""Apply a function that operates on a GeoSeries to the rings.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
func: Function to apply that expects a GeoSeries.
|
|
197
|
+
args: Tuple of positional arguments for the function.
|
|
198
|
+
kwargs: Dictionary of keyword arguments for the function.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
PolygonsAsRings: The instance itself after applying the function.
|
|
202
|
+
"""
|
|
183
203
|
kwargs = kwargs or {}
|
|
184
204
|
args = args or ()
|
|
185
205
|
|
|
186
|
-
self.rings.loc[:] = np.array(
|
|
206
|
+
self.rings.loc[:] = np.array( # type: ignore [call-overload]
|
|
187
207
|
func(
|
|
188
208
|
GeoSeries(
|
|
189
209
|
self.rings.values,
|
|
@@ -198,9 +218,21 @@ class PolygonsAsRings:
|
|
|
198
218
|
return self
|
|
199
219
|
|
|
200
220
|
def apply_gdf_func(
|
|
201
|
-
self,
|
|
202
|
-
|
|
203
|
-
|
|
221
|
+
self,
|
|
222
|
+
func: Callable,
|
|
223
|
+
args: tuple | None = None,
|
|
224
|
+
kwargs: dict | None = None,
|
|
225
|
+
) -> "PolygonsAsRings":
|
|
226
|
+
"""Apply a function that operates on a GeoDataFrame to the rings.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
func: Function to apply that expects a GeoDataFrame.
|
|
230
|
+
args: Tuple of positional arguments for the function.
|
|
231
|
+
kwargs: Dictionary of keyword arguments for the function.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
PolygonsAsRings: The instance itself after applying the function.
|
|
235
|
+
"""
|
|
204
236
|
kwargs = kwargs or {}
|
|
205
237
|
args = args or ()
|
|
206
238
|
|
|
@@ -214,7 +246,7 @@ class PolygonsAsRings:
|
|
|
214
246
|
|
|
215
247
|
gdf.index = self.rings.index
|
|
216
248
|
|
|
217
|
-
self.rings.loc[:] = func(
|
|
249
|
+
self.rings.loc[:] = func( # type: ignore [call-overload]
|
|
218
250
|
gdf,
|
|
219
251
|
*args,
|
|
220
252
|
**kwargs,
|
|
@@ -223,15 +255,17 @@ class PolygonsAsRings:
|
|
|
223
255
|
return self
|
|
224
256
|
|
|
225
257
|
@property
|
|
226
|
-
def is_interior(self):
|
|
258
|
+
def is_interior(self) -> bool:
|
|
259
|
+
"""Returns a boolean Series of whether the row is an interior ring."""
|
|
227
260
|
return self.rings.index.get_level_values(0) == 1
|
|
228
261
|
|
|
229
262
|
@property
|
|
230
|
-
def is_exterior(self):
|
|
263
|
+
def is_exterior(self) -> bool:
|
|
264
|
+
"""Returns a boolean Series of whether the row is an exterior ring."""
|
|
231
265
|
return self.rings.index.get_level_values(0) == 0
|
|
232
266
|
|
|
233
267
|
@property
|
|
234
|
-
def _interiors_index(self):
|
|
268
|
+
def _interiors_index(self) -> pd.MultiIndex:
|
|
235
269
|
"""A three-leveled MultiIndex.
|
|
236
270
|
|
|
237
271
|
Used to separate interior and exterior and sort the interior in
|
|
@@ -254,7 +288,7 @@ class PolygonsAsRings:
|
|
|
254
288
|
)
|
|
255
289
|
|
|
256
290
|
@property
|
|
257
|
-
def _exterior_index(self):
|
|
291
|
+
def _exterior_index(self) -> pd.MultiIndex:
|
|
258
292
|
"""A three-leveled MultiIndex.
|
|
259
293
|
|
|
260
294
|
Used to separate interior and exterior in the 'to_numpy' method.
|
|
@@ -274,8 +308,8 @@ class PolygonsAsRings:
|
|
|
274
308
|
self.gdf.geometry = self.to_numpy()
|
|
275
309
|
return self.gdf
|
|
276
310
|
|
|
277
|
-
def to_geoseries(self) ->
|
|
278
|
-
"""Return the
|
|
311
|
+
def to_geoseries(self) -> GeoSeries:
|
|
312
|
+
"""Return the GeoSeries with polygons."""
|
|
279
313
|
self.gdf.geometry = self.to_numpy()
|
|
280
314
|
return self.gdf.geometry
|
|
281
315
|
|
|
@@ -293,7 +327,7 @@ class PolygonsAsRings:
|
|
|
293
327
|
try:
|
|
294
328
|
return make_valid(polygons(exterior.values))
|
|
295
329
|
except Exception:
|
|
296
|
-
return _geoms_to_linearrings_fallback(exterior)
|
|
330
|
+
return _geoms_to_linearrings_fallback(exterior).values
|
|
297
331
|
|
|
298
332
|
empty_interiors = pd.Series(
|
|
299
333
|
[None for _ in range(len(self.gdf) * self.max_rings)],
|
|
@@ -312,10 +346,18 @@ class PolygonsAsRings:
|
|
|
312
346
|
try:
|
|
313
347
|
return make_valid(polygons(exterior.values, interiors.values))
|
|
314
348
|
except Exception:
|
|
315
|
-
return _geoms_to_linearrings_fallback(exterior, interiors)
|
|
349
|
+
return _geoms_to_linearrings_fallback(exterior, interiors).values
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def get_linearring_series(geoms: GeoDataFrame | GeoSeries) -> pd.Series:
|
|
353
|
+
"""Convert geometries into a series of LinearRings.
|
|
316
354
|
|
|
355
|
+
Args:
|
|
356
|
+
geoms: GeoDataFrame or GeoSeries from which to extract LinearRings.
|
|
317
357
|
|
|
318
|
-
|
|
358
|
+
Returns:
|
|
359
|
+
pd.Series: A series containing LinearRings.
|
|
360
|
+
"""
|
|
319
361
|
geoms = to_geoseries(geoms).explode(index_parts=False)
|
|
320
362
|
coords, indices = get_coordinates(geoms, return_index=True)
|
|
321
363
|
return pd.Series(linearrings(coords, indices=indices), index=geoms.index)
|
|
@@ -325,6 +367,7 @@ def _geoms_to_linearrings_fallback(
|
|
|
325
367
|
exterior: pd.Series, interiors: pd.Series | None = None
|
|
326
368
|
) -> pd.Series:
|
|
327
369
|
exterior.index = exterior.index.get_level_values(1)
|
|
370
|
+
assert exterior.index.is_monotonic_increasing
|
|
328
371
|
|
|
329
372
|
exterior = get_linearring_series(exterior)
|
|
330
373
|
|
sgis/geopandas_tools/sfilter.py
CHANGED
|
@@ -2,12 +2,12 @@ import warnings
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
import pandas as pd
|
|
5
|
-
from geopandas import GeoDataFrame
|
|
5
|
+
from geopandas import GeoDataFrame
|
|
6
|
+
from geopandas import GeoSeries
|
|
6
7
|
from shapely import Geometry
|
|
7
8
|
|
|
8
9
|
from .conversion import to_gdf
|
|
9
10
|
|
|
10
|
-
|
|
11
11
|
gdf_type_error_message = "'gdf' should be of type GeoDataFrame or GeoSeries."
|
|
12
12
|
|
|
13
13
|
|
|
@@ -34,9 +34,9 @@ def sfilter(
|
|
|
34
34
|
A copy of 'gdf' with only the rows matching the
|
|
35
35
|
spatial predicate with 'other'.
|
|
36
36
|
|
|
37
|
-
Examples
|
|
37
|
+
Examples:
|
|
38
38
|
--------
|
|
39
|
-
|
|
39
|
+
>>> import sgis as sg
|
|
40
40
|
>>> df1 = sg.to_gdf([(0, 0), (0, 1)])
|
|
41
41
|
>>> df1
|
|
42
42
|
geometry
|
|
@@ -71,7 +71,7 @@ def sfilter(
|
|
|
71
71
|
0 POINT (0.00000 0.00000)
|
|
72
72
|
|
|
73
73
|
"""
|
|
74
|
-
if not isinstance(gdf, (GeoDataFrame
|
|
74
|
+
if not isinstance(gdf, (GeoDataFrame | GeoSeries)):
|
|
75
75
|
raise TypeError(gdf_type_error_message)
|
|
76
76
|
|
|
77
77
|
other = _sfilter_checks(other, crs=gdf.crs)
|
|
@@ -100,9 +100,9 @@ def sfilter_split(
|
|
|
100
100
|
A tuple of GeoDataFrames, one with the rows that match the spatial predicate
|
|
101
101
|
and one with the rows that do not.
|
|
102
102
|
|
|
103
|
-
Examples
|
|
103
|
+
Examples:
|
|
104
104
|
--------
|
|
105
|
-
|
|
105
|
+
>>> import sgis as sg
|
|
106
106
|
>>> df1 = sg.to_gdf([(0, 0), (0, 1)])
|
|
107
107
|
>>> df1
|
|
108
108
|
geometry
|
|
@@ -140,7 +140,7 @@ def sfilter_split(
|
|
|
140
140
|
>>> not_intersecting = df1.loc[~filt]
|
|
141
141
|
|
|
142
142
|
"""
|
|
143
|
-
if not isinstance(gdf, (GeoDataFrame
|
|
143
|
+
if not isinstance(gdf, (GeoDataFrame | GeoSeries)):
|
|
144
144
|
raise TypeError(gdf_type_error_message)
|
|
145
145
|
|
|
146
146
|
other = _sfilter_checks(other, crs=gdf.crs)
|
|
@@ -149,7 +149,7 @@ def sfilter_split(
|
|
|
149
149
|
|
|
150
150
|
return (
|
|
151
151
|
gdf.iloc[indices],
|
|
152
|
-
gdf.iloc[pd.Index(range(len(gdf))).difference(indices)],
|
|
152
|
+
gdf.iloc[pd.Index(range(len(gdf))).difference(pd.Index(indices))],
|
|
153
153
|
)
|
|
154
154
|
|
|
155
155
|
|
|
@@ -171,9 +171,9 @@ def sfilter_inverse(
|
|
|
171
171
|
A copy of 'gdf' with only the rows that do not match the
|
|
172
172
|
spatial predicate with 'other'.
|
|
173
173
|
|
|
174
|
-
Examples
|
|
174
|
+
Examples:
|
|
175
175
|
--------
|
|
176
|
-
|
|
176
|
+
>>> import sgis as sg
|
|
177
177
|
>>> df1 = sg.to_gdf([(0, 0), (0, 1)])
|
|
178
178
|
>>> df1
|
|
179
179
|
geometry
|
|
@@ -205,14 +205,14 @@ def sfilter_inverse(
|
|
|
205
205
|
>>> not_intersecting = df1.loc[~df1.intersects(df2.unary_union)]
|
|
206
206
|
|
|
207
207
|
"""
|
|
208
|
-
if not isinstance(gdf, (GeoDataFrame
|
|
208
|
+
if not isinstance(gdf, (GeoDataFrame | GeoSeries)):
|
|
209
209
|
raise TypeError(gdf_type_error_message)
|
|
210
210
|
|
|
211
211
|
other = _sfilter_checks(other, crs=gdf.crs)
|
|
212
212
|
|
|
213
213
|
indices = _get_sfilter_indices(gdf, other, predicate)
|
|
214
214
|
|
|
215
|
-
return gdf.iloc[pd.Index(range(len(gdf))).difference(indices)]
|
|
215
|
+
return gdf.iloc[pd.Index(range(len(gdf))).difference(pd.Index(indices))]
|
|
216
216
|
|
|
217
217
|
|
|
218
218
|
def _sfilter_checks(other, crs):
|
|
@@ -256,7 +256,7 @@ def _get_sfilter_indices(
|
|
|
256
256
|
predicate : string
|
|
257
257
|
Binary predicate to query.
|
|
258
258
|
|
|
259
|
-
Returns
|
|
259
|
+
Returns:
|
|
260
260
|
-------
|
|
261
261
|
DataFrame
|
|
262
262
|
DataFrame with matching indices in
|
sgis/helpers.py
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
"""Small helper functions."""
|
|
2
|
+
|
|
2
3
|
import glob
|
|
3
4
|
import inspect
|
|
4
5
|
import os
|
|
5
6
|
import warnings
|
|
6
7
|
from collections.abc import Callable
|
|
8
|
+
from collections.abc import Generator
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
7
11
|
|
|
8
12
|
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
9
14
|
from geopandas import GeoDataFrame
|
|
10
15
|
|
|
11
16
|
|
|
12
|
-
def get_numpy_func(text, error_message: str | None = None) -> Callable:
|
|
17
|
+
def get_numpy_func(text: str, error_message: str | None = None) -> Callable:
|
|
18
|
+
"""Fetch a numpy function based on its name.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
text: The name of the numpy function to retrieve.
|
|
22
|
+
error_message: Custom error message if the function is not found.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The numpy function corresponding to the provided text.
|
|
26
|
+
"""
|
|
13
27
|
f = getattr(np, text, None)
|
|
14
28
|
if f is not None:
|
|
15
29
|
return f
|
|
@@ -19,20 +33,36 @@ def get_numpy_func(text, error_message: str | None = None) -> Callable:
|
|
|
19
33
|
raise ValueError(error_message)
|
|
20
34
|
|
|
21
35
|
|
|
22
|
-
def get_func_name(func):
|
|
36
|
+
def get_func_name(func: Callable) -> str:
|
|
37
|
+
"""Return the name of a function.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
func: The function object whose name is to be retrieved.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The name of the function.
|
|
44
|
+
"""
|
|
23
45
|
try:
|
|
24
46
|
return func.__name__
|
|
25
47
|
except AttributeError:
|
|
26
48
|
return str(func)
|
|
27
49
|
|
|
28
50
|
|
|
29
|
-
def get_non_numpy_func_name(f):
|
|
51
|
+
def get_non_numpy_func_name(f: Callable | str) -> str:
|
|
30
52
|
if callable(f):
|
|
31
53
|
return f.__name__
|
|
32
54
|
return str(f).replace("np.", "").replace("numpy.", "")
|
|
33
55
|
|
|
34
56
|
|
|
35
|
-
def to_numpy_func(text):
|
|
57
|
+
def to_numpy_func(text: str) -> Callable:
|
|
58
|
+
"""Convert a text identifier into a numpy function.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
text: Name of the numpy function.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The numpy function.
|
|
65
|
+
"""
|
|
36
66
|
f = getattr(np, text, None)
|
|
37
67
|
if f is not None:
|
|
38
68
|
return f
|
|
@@ -42,13 +72,22 @@ def to_numpy_func(text):
|
|
|
42
72
|
raise ValueError
|
|
43
73
|
|
|
44
74
|
|
|
45
|
-
def is_property(obj, attribute) -> bool:
|
|
75
|
+
def is_property(obj: object, attribute: str) -> bool:
|
|
76
|
+
"""Determine if a class attribute is a property.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
obj: The object to check.
|
|
80
|
+
attribute: The attribute name to check on the object.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if the attribute is a property, False otherwise.
|
|
84
|
+
"""
|
|
46
85
|
return hasattr(obj.__class__, attribute) and isinstance(
|
|
47
86
|
getattr(obj.__class__, attribute), property
|
|
48
87
|
)
|
|
49
88
|
|
|
50
89
|
|
|
51
|
-
def dict_zip_intersection(*dicts):
|
|
90
|
+
def dict_zip_intersection(*dicts: dict) -> Generator[tuple[Any, ...], None, None]:
|
|
52
91
|
"""From mCoding (YouTube)."""
|
|
53
92
|
if not dicts:
|
|
54
93
|
return
|
|
@@ -58,7 +97,9 @@ def dict_zip_intersection(*dicts):
|
|
|
58
97
|
yield key, *(d[key] for d in dicts)
|
|
59
98
|
|
|
60
99
|
|
|
61
|
-
def dict_zip_union(
|
|
100
|
+
def dict_zip_union(
|
|
101
|
+
*dicts: dict, fillvalue: Any | None = None
|
|
102
|
+
) -> Generator[tuple[Any, ...], None, None]:
|
|
62
103
|
"""From mCoding (YouTube)."""
|
|
63
104
|
if not dicts:
|
|
64
105
|
return
|
|
@@ -68,7 +109,7 @@ def dict_zip_union(*dicts, fillvalue=None):
|
|
|
68
109
|
yield key, *(d.get(key, fillvalue) for d in dicts)
|
|
69
110
|
|
|
70
111
|
|
|
71
|
-
def dict_zip(*dicts):
|
|
112
|
+
def dict_zip(*dicts: dict) -> Generator[tuple[Any, ...], None, None]:
|
|
72
113
|
"""From mCoding (YouTube)."""
|
|
73
114
|
if not dicts:
|
|
74
115
|
return
|
|
@@ -81,15 +122,24 @@ def dict_zip(*dicts):
|
|
|
81
122
|
yield key, first_val, *(other[key] for other in dicts[1:])
|
|
82
123
|
|
|
83
124
|
|
|
84
|
-
def in_jupyter():
|
|
125
|
+
def in_jupyter() -> bool:
|
|
85
126
|
try:
|
|
86
|
-
get_ipython
|
|
127
|
+
get_ipython # type: ignore[name-defined]
|
|
87
128
|
return True
|
|
88
129
|
except NameError:
|
|
89
130
|
return False
|
|
90
131
|
|
|
91
132
|
|
|
92
|
-
def get_all_files(root, recursive=True):
|
|
133
|
+
def get_all_files(root: str, recursive: bool = True) -> list[str]:
|
|
134
|
+
"""Fetch all files in a directory.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
root: The root directory path.
|
|
138
|
+
recursive: Whether to include subdirectories.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
A list of file paths.
|
|
142
|
+
"""
|
|
93
143
|
if not recursive:
|
|
94
144
|
return [path for path in glob.glob(str(Path(root)) + "/*")]
|
|
95
145
|
paths = []
|
|
@@ -101,8 +151,8 @@ def get_all_files(root, recursive=True):
|
|
|
101
151
|
|
|
102
152
|
|
|
103
153
|
def return_two_vals(
|
|
104
|
-
vals: tuple[str
|
|
105
|
-
) -> tuple[str | int | float, str | int | float
|
|
154
|
+
vals: tuple[str, str] | list[str] | str | int | float
|
|
155
|
+
) -> tuple[str | int | float, str | int | float]:
|
|
106
156
|
"""Return a two-length tuple from a str/int/float or list/tuple of length 1 or 2.
|
|
107
157
|
|
|
108
158
|
Returns 'vals' as a 2-length tuple. If the input is a string, return
|
|
@@ -117,7 +167,7 @@ def return_two_vals(
|
|
|
117
167
|
"""
|
|
118
168
|
if isinstance(vals, str):
|
|
119
169
|
return vals, vals
|
|
120
|
-
if
|
|
170
|
+
if isinstance(vals, (tuple, list)):
|
|
121
171
|
if len(vals) == 2:
|
|
122
172
|
return vals[0], vals[1]
|
|
123
173
|
if len(vals) == 1:
|
|
@@ -156,44 +206,31 @@ def unit_is_degrees(gdf: GeoDataFrame) -> bool:
|
|
|
156
206
|
def get_object_name(
|
|
157
207
|
var: object, start: int = 2, stop: int = 7, ignore_self: bool = True
|
|
158
208
|
) -> str | None:
|
|
159
|
-
|
|
160
|
-
frame
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
frame = frame.f_back
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if names and len(names) > 1:
|
|
185
|
-
if ignore_self and names[0] == "self":
|
|
186
|
-
frame = frame.f_back
|
|
187
|
-
continue
|
|
188
|
-
warnings.warn(
|
|
189
|
-
"More than one local variable matches the object. Name might be wrong."
|
|
190
|
-
)
|
|
191
|
-
return names[0]
|
|
192
|
-
|
|
193
|
-
frame = frame.f_back
|
|
194
|
-
|
|
195
|
-
if not frame:
|
|
196
|
-
return
|
|
209
|
+
frame = inspect.currentframe() # frame can be FrameType or None
|
|
210
|
+
if frame:
|
|
211
|
+
try:
|
|
212
|
+
for _ in range(start):
|
|
213
|
+
frame = frame.f_back if frame else None
|
|
214
|
+
for _ in range(start, stop):
|
|
215
|
+
if frame:
|
|
216
|
+
names = [
|
|
217
|
+
var_name
|
|
218
|
+
for var_name, var_val in frame.f_locals.items()
|
|
219
|
+
if var_val is var and not (ignore_self and var_name == "self")
|
|
220
|
+
]
|
|
221
|
+
names = [name for name in names if not name.startswith("_")]
|
|
222
|
+
if names:
|
|
223
|
+
if len(names) != 1:
|
|
224
|
+
warnings.warn(
|
|
225
|
+
"More than one local variable matches the object. Name might be wrong.",
|
|
226
|
+
stacklevel=2,
|
|
227
|
+
)
|
|
228
|
+
return names[0]
|
|
229
|
+
frame = frame.f_back if frame else None
|
|
230
|
+
finally:
|
|
231
|
+
if frame:
|
|
232
|
+
del frame # Explicitly delete frame reference to assist with garbage collection
|
|
233
|
+
return None
|
|
197
234
|
|
|
198
235
|
|
|
199
236
|
def make_namedict(gdfs: tuple[GeoDataFrame]) -> dict[int, str]:
|
|
@@ -207,7 +244,16 @@ def make_namedict(gdfs: tuple[GeoDataFrame]) -> dict[int, str]:
|
|
|
207
244
|
return namedict
|
|
208
245
|
|
|
209
246
|
|
|
210
|
-
def sort_nans_last(df, ignore_index: bool = False):
|
|
247
|
+
def sort_nans_last(df: pd.DataFrame, ignore_index: bool = False) -> pd.DataFrame:
|
|
248
|
+
"""Sort a DataFrame placing rows with the most NaNs last.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
df: DataFrame to sort.
|
|
252
|
+
ignore_index: If True, the index will be reset.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Sorted DataFrame with NaNs last.
|
|
256
|
+
"""
|
|
211
257
|
if not len(df):
|
|
212
258
|
return df
|
|
213
259
|
df["n_nan"] = df.isna().sum(axis=1).values
|
|
@@ -219,7 +265,15 @@ def sort_nans_last(df, ignore_index: bool = False):
|
|
|
219
265
|
return df.reset_index(drop=True) if ignore_index else df
|
|
220
266
|
|
|
221
267
|
|
|
222
|
-
def is_number(text) -> bool:
|
|
268
|
+
def is_number(text: str) -> bool:
|
|
269
|
+
"""Check if a string can be converted to a number.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
text: The string to check.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
True if the string can be converted to a number, False otherwise.
|
|
276
|
+
"""
|
|
223
277
|
try:
|
|
224
278
|
float(text)
|
|
225
279
|
return True
|
|
@@ -228,10 +282,14 @@ def is_number(text) -> bool:
|
|
|
228
282
|
|
|
229
283
|
|
|
230
284
|
class LocalFunctionError(ValueError):
|
|
231
|
-
|
|
285
|
+
"""Exception for when a locally defined function is used in Jupyter, which is incompatible with multiprocessing."""
|
|
286
|
+
|
|
287
|
+
def __init__(self, func: Callable) -> None:
|
|
288
|
+
"""Initialiser."""
|
|
232
289
|
self.func = func.__name__
|
|
233
290
|
|
|
234
|
-
def __str__(self):
|
|
291
|
+
def __str__(self) -> str:
|
|
292
|
+
"""Error message representation."""
|
|
235
293
|
return (
|
|
236
294
|
f"{self.func}. "
|
|
237
295
|
"In Jupyter, functions to be parallelized must \n"
|