ssb-sgis 1.0.1__py3-none-any.whl → 1.0.3__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 +107 -121
- sgis/exceptions.py +5 -3
- sgis/geopandas_tools/__init__.py +1 -0
- sgis/geopandas_tools/bounds.py +86 -47
- sgis/geopandas_tools/buffer_dissolve_explode.py +62 -39
- sgis/geopandas_tools/centerlines.py +53 -44
- sgis/geopandas_tools/cleaning.py +87 -104
- sgis/geopandas_tools/conversion.py +164 -107
- sgis/geopandas_tools/duplicates.py +33 -19
- sgis/geopandas_tools/general.py +84 -52
- sgis/geopandas_tools/geometry_types.py +24 -10
- sgis/geopandas_tools/neighbors.py +23 -11
- sgis/geopandas_tools/overlay.py +136 -53
- sgis/geopandas_tools/point_operations.py +11 -10
- sgis/geopandas_tools/polygon_operations.py +53 -61
- sgis/geopandas_tools/polygons_as_rings.py +121 -78
- sgis/geopandas_tools/sfilter.py +17 -17
- sgis/helpers.py +116 -58
- sgis/io/dapla_functions.py +32 -23
- sgis/io/opener.py +13 -6
- sgis/io/read_parquet.py +2 -2
- sgis/maps/examine.py +55 -28
- sgis/maps/explore.py +471 -112
- sgis/maps/httpserver.py +12 -12
- sgis/maps/legend.py +285 -134
- sgis/maps/map.py +248 -129
- sgis/maps/maps.py +123 -119
- sgis/maps/thematicmap.py +260 -94
- 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 +22 -64
- sgis/networkanalysis/cutting_lines.py +58 -46
- sgis/networkanalysis/directednetwork.py +16 -8
- sgis/networkanalysis/finding_isolated_networks.py +6 -5
- sgis/networkanalysis/network.py +15 -13
- sgis/networkanalysis/networkanalysis.py +79 -61
- sgis/networkanalysis/networkanalysisrules.py +21 -17
- sgis/networkanalysis/nodes.py +2 -3
- sgis/networkanalysis/traveling_salesman.py +6 -3
- sgis/parallel/parallel.py +372 -142
- sgis/raster/base.py +9 -3
- sgis/raster/cube.py +331 -213
- sgis/raster/cubebase.py +15 -29
- sgis/raster/image_collection.py +2560 -0
- sgis/raster/indices.py +17 -12
- sgis/raster/raster.py +356 -275
- sgis/raster/sentinel_config.py +104 -0
- sgis/raster/zonal.py +38 -14
- {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/LICENSE +1 -1
- {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/METADATA +87 -16
- ssb_sgis-1.0.3.dist-info/RECORD +61 -0
- {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/WHEEL +1 -1
- sgis/raster/bands.py +0 -48
- sgis/raster/gradient.py +0 -78
- sgis/raster/methods_as_functions.py +0 -124
- sgis/raster/torchgeo.py +0 -150
- ssb_sgis-1.0.1.dist-info/RECORD +0 -63
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,17 +122,26 @@ 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
|
-
return [path for path in glob.glob(str(Path(root)) + "
|
|
144
|
+
return [path for path in glob.glob(str(Path(root)) + "/**")]
|
|
95
145
|
paths = []
|
|
96
146
|
for root_dir, _, files in os.walk(root):
|
|
97
147
|
for file in files:
|
|
@@ -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:
|
|
@@ -155,45 +205,32 @@ def unit_is_degrees(gdf: GeoDataFrame) -> bool:
|
|
|
155
205
|
|
|
156
206
|
def get_object_name(
|
|
157
207
|
var: object, start: int = 2, stop: int = 7, ignore_self: bool = True
|
|
158
|
-
) -> str
|
|
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
|
|
208
|
+
) -> str:
|
|
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
|
+
raise ValueError(f"Couldn't find name for {var}")
|
|
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"
|
sgis/io/dapla_functions.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
"""Functions for reading and writing GeoDataFrames in Statistics Norway's GCS Dapla.
|
|
2
|
-
"""
|
|
1
|
+
"""Functions for reading and writing GeoDataFrames in Statistics Norway's GCS Dapla."""
|
|
3
2
|
|
|
4
3
|
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
6
4
|
|
|
7
5
|
import dapla as dp
|
|
8
6
|
import geopandas as gpd
|
|
7
|
+
import joblib
|
|
9
8
|
import pandas as pd
|
|
10
9
|
from geopandas import GeoDataFrame
|
|
11
10
|
from geopandas.io.arrow import _geopandas_to_arrow
|
|
@@ -14,9 +13,9 @@ from pyarrow import parquet
|
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def read_geopandas(
|
|
17
|
-
gcs_path: str | Path,
|
|
16
|
+
gcs_path: str | Path | list[str | Path],
|
|
18
17
|
pandas_fallback: bool = False,
|
|
19
|
-
file_system:
|
|
18
|
+
file_system: dp.gcs.GCSFileSystem | None = None,
|
|
20
19
|
**kwargs,
|
|
21
20
|
) -> GeoDataFrame | DataFrame:
|
|
22
21
|
"""Reads geoparquet or other geodata from a file on GCS.
|
|
@@ -28,25 +27,35 @@ def read_geopandas(
|
|
|
28
27
|
Does not currently read shapefiles or filegeodatabases.
|
|
29
28
|
|
|
30
29
|
Args:
|
|
31
|
-
gcs_path: path to
|
|
30
|
+
gcs_path: path to one or more files on Google Cloud Storage.
|
|
31
|
+
Multiple paths are read with threading.
|
|
32
32
|
pandas_fallback: If False (default), an exception is raised if the file can
|
|
33
33
|
not be read with geopandas and the number of rows is more than 0. If True,
|
|
34
|
-
the file will be read
|
|
34
|
+
the file will be read with pandas if geopandas fails.
|
|
35
|
+
file_system: Optional file system.
|
|
35
36
|
**kwargs: Additional keyword arguments passed to geopandas' read_parquet
|
|
36
37
|
or read_file, depending on the file type.
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
Returns:
|
|
39
40
|
A GeoDataFrame if it has rows. If zero rows, a pandas DataFrame is returned.
|
|
40
41
|
"""
|
|
42
|
+
if file_system is None:
|
|
43
|
+
file_system = dp.FileClient.get_gcs_file_system()
|
|
44
|
+
|
|
45
|
+
if isinstance(gcs_path, (list, tuple)):
|
|
46
|
+
kwargs |= {"file_system": file_system, "pandas_fallback": pandas_fallback}
|
|
47
|
+
# recursive read with threads
|
|
48
|
+
with joblib.Parallel(n_jobs=len(gcs_path), backend="threading") as parallel:
|
|
49
|
+
dfs: list[GeoDataFrame] = parallel(
|
|
50
|
+
joblib.delayed(read_geopandas)(x, **kwargs) for x in gcs_path
|
|
51
|
+
)
|
|
52
|
+
return pd.concat(dfs)
|
|
41
53
|
|
|
42
54
|
if not isinstance(gcs_path, str):
|
|
43
55
|
try:
|
|
44
56
|
gcs_path = str(gcs_path)
|
|
45
|
-
except TypeError:
|
|
46
|
-
raise TypeError(f"Unexpected type {type(gcs_path)}.")
|
|
47
|
-
|
|
48
|
-
if file_system is None:
|
|
49
|
-
file_system = dp.FileClient.get_gcs_file_system()
|
|
57
|
+
except TypeError as e:
|
|
58
|
+
raise TypeError(f"Unexpected type {type(gcs_path)}.") from e
|
|
50
59
|
|
|
51
60
|
if "parquet" in gcs_path or "prqt" in gcs_path:
|
|
52
61
|
with file_system.open(gcs_path, mode="rb") as file:
|
|
@@ -77,11 +86,11 @@ def read_geopandas(
|
|
|
77
86
|
|
|
78
87
|
|
|
79
88
|
def write_geopandas(
|
|
80
|
-
df:
|
|
89
|
+
df: GeoDataFrame,
|
|
81
90
|
gcs_path: str | Path,
|
|
82
91
|
overwrite: bool = True,
|
|
83
92
|
pandas_fallback: bool = False,
|
|
84
|
-
file_system:
|
|
93
|
+
file_system: dp.gcs.GCSFileSystem | None = None,
|
|
85
94
|
**kwargs,
|
|
86
95
|
) -> None:
|
|
87
96
|
"""Writes a GeoDataFrame to the speficied format.
|
|
@@ -93,10 +102,13 @@ def write_geopandas(
|
|
|
93
102
|
df: The GeoDataFrame to write.
|
|
94
103
|
gcs_path: The path to the file you want to write to.
|
|
95
104
|
overwrite: Whether to overwrite the file if it exists. Defaults to True.
|
|
105
|
+
pandas_fallback: If False (default), an exception is raised if the file can
|
|
106
|
+
not be written with geopandas and the number of rows is more than 0. If True,
|
|
107
|
+
the file will be written without geo-metadata if >0 rows.
|
|
108
|
+
file_system: Optional file sustem.
|
|
96
109
|
**kwargs: Additional keyword arguments passed to parquet.write_table
|
|
97
110
|
(for parquet) or geopandas' to_file method (if not parquet).
|
|
98
111
|
"""
|
|
99
|
-
|
|
100
112
|
if not isinstance(gcs_path, str):
|
|
101
113
|
try:
|
|
102
114
|
gcs_path = str(gcs_path)
|
|
@@ -109,7 +121,8 @@ def write_geopandas(
|
|
|
109
121
|
if file_system is None:
|
|
110
122
|
file_system = dp.FileClient.get_gcs_file_system()
|
|
111
123
|
|
|
112
|
-
|
|
124
|
+
if not isinstance(df, GeoDataFrame):
|
|
125
|
+
raise ValueError("DataFrame must be GeoDataFrame.")
|
|
113
126
|
|
|
114
127
|
if not len(df):
|
|
115
128
|
if pandas_fallback:
|
|
@@ -152,7 +165,6 @@ def exists(path: str | Path) -> bool:
|
|
|
152
165
|
Returns:
|
|
153
166
|
True if the path exists, False if not.
|
|
154
167
|
"""
|
|
155
|
-
|
|
156
168
|
file_system = dp.FileClient.get_gcs_file_system()
|
|
157
169
|
return file_system.exists(path)
|
|
158
170
|
|
|
@@ -185,7 +197,7 @@ def check_files(
|
|
|
185
197
|
]
|
|
186
198
|
folderinfo = [x["name"] for x in info if x["storageClass"] == "DIRECTORY"]
|
|
187
199
|
|
|
188
|
-
fileinfo +=
|
|
200
|
+
fileinfo += _get_files_in_subfolders(folderinfo)
|
|
189
201
|
|
|
190
202
|
df = pd.DataFrame(fileinfo, columns=["path", "kb", "updated"])
|
|
191
203
|
|
|
@@ -224,12 +236,9 @@ def check_files(
|
|
|
224
236
|
return df.loc[lambda x: x.index > the_time, ["kb", "mb", "name", "child", "path"]]
|
|
225
237
|
|
|
226
238
|
|
|
227
|
-
def
|
|
239
|
+
def _get_files_in_subfolders(folderinfo: list[dict]) -> list[tuple]:
|
|
228
240
|
file_system = dp.FileClient.get_gcs_file_system()
|
|
229
241
|
|
|
230
|
-
if isinstance(folderinfo, (str, Path)):
|
|
231
|
-
folderinfo = [folderinfo]
|
|
232
|
-
|
|
233
242
|
fileinfo = []
|
|
234
243
|
|
|
235
244
|
while folderinfo:
|
sgis/io/opener.py
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
1
2
|
from contextlib import contextmanager
|
|
2
|
-
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
try:
|
|
5
|
-
|
|
6
|
+
from dapla import FileClient
|
|
7
|
+
from dapla.gcs import GCSFileSystem
|
|
6
8
|
except ImportError:
|
|
7
|
-
|
|
9
|
+
|
|
10
|
+
class GCSFileSystem: # type: ignore[no-redef]
|
|
11
|
+
"""Placeholder."""
|
|
12
|
+
|
|
8
13
|
|
|
9
14
|
from ._is_dapla import is_dapla
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
@contextmanager
|
|
13
|
-
def opener(
|
|
18
|
+
def opener(
|
|
19
|
+
path, mode: str = "rb", file_system: GCSFileSystem | None = None
|
|
20
|
+
) -> Generator[str | Any, None, None]:
|
|
14
21
|
"""Yields a gcs buffer if in Dapla, otherwise yields the path.
|
|
15
22
|
|
|
16
|
-
Example
|
|
23
|
+
Example:
|
|
17
24
|
-------
|
|
18
25
|
>>> with opener(path) as file:
|
|
19
26
|
>>> with rasterio.open(file) as src:
|
|
@@ -21,7 +28,7 @@ def opener(path, mode="rb", file_system=None):
|
|
|
21
28
|
"""
|
|
22
29
|
if is_dapla():
|
|
23
30
|
if file_system is None:
|
|
24
|
-
file_system =
|
|
31
|
+
file_system = FileClient.get_gcs_file_system()
|
|
25
32
|
yield file_system.open(str(path), mode=mode)
|
|
26
33
|
else:
|
|
27
34
|
yield str(path)
|
sgis/io/read_parquet.py
CHANGED
|
@@ -14,8 +14,8 @@ def read_parquet_url(url: str) -> GeoDataFrame:
|
|
|
14
14
|
Returns:
|
|
15
15
|
A GeoDataFrame.
|
|
16
16
|
|
|
17
|
-
Examples
|
|
18
|
-
|
|
17
|
+
Examples:
|
|
18
|
+
---------
|
|
19
19
|
>>> from sgis import read_parquet_url
|
|
20
20
|
>>> url = "https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/points_oslo.parquet"
|
|
21
21
|
>>> points = read_parquet_url(url)
|
sgis/maps/examine.py
CHANGED
|
@@ -3,8 +3,14 @@ import numpy as np
|
|
|
3
3
|
|
|
4
4
|
from ..geopandas_tools.bounds import get_total_bounds
|
|
5
5
|
from ..helpers import unit_is_degrees
|
|
6
|
+
from ..raster.image_collection import Band
|
|
7
|
+
from ..raster.image_collection import Image
|
|
8
|
+
from ..raster.image_collection import ImageCollection
|
|
9
|
+
from .explore import Explore
|
|
6
10
|
from .map import Map
|
|
7
|
-
from .maps import clipmap
|
|
11
|
+
from .maps import clipmap
|
|
12
|
+
from .maps import explore
|
|
13
|
+
from .maps import samplemap
|
|
8
14
|
|
|
9
15
|
|
|
10
16
|
class Examine:
|
|
@@ -19,20 +25,8 @@ class Examine:
|
|
|
19
25
|
first geometry in 'mask_gdf' (or the first speficied gdf). The 'next' method
|
|
20
26
|
can then be repeated.
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
will be used as masks, unless 'mask_gdf' is specified.
|
|
25
|
-
column: Column to use as colors.
|
|
26
|
-
mask_gdf: Optional GeoDataFrame to use as mask iterator. The geometries
|
|
27
|
-
of mask_gdf will not be shown.
|
|
28
|
-
size: Number of meters (or other crs unit) to buffer the mask geometry
|
|
29
|
-
before clipping.
|
|
30
|
-
sort_values: Optional sorting column(s) of the mask GeoDataFrame. Rows
|
|
31
|
-
will be iterated through from the top.
|
|
32
|
-
**kwargs: Additional keyword arguments passed to sgis.clipmap.
|
|
33
|
-
|
|
34
|
-
Examples
|
|
35
|
-
--------
|
|
28
|
+
Examples:
|
|
29
|
+
---------
|
|
36
30
|
Create the examiner.
|
|
37
31
|
|
|
38
32
|
>>> import sgis as sg
|
|
@@ -78,7 +72,24 @@ class Examine:
|
|
|
78
72
|
size: int | float = 1000,
|
|
79
73
|
only_show_mask: bool = True,
|
|
80
74
|
**kwargs,
|
|
81
|
-
):
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Initialiser.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
*gdfs: One or more GeoDataFrames. The rows of the first GeoDataFrame
|
|
80
|
+
will be used as masks, unless 'mask_gdf' is specified.
|
|
81
|
+
column: Column to use as colors.
|
|
82
|
+
mask_gdf: Optional GeoDataFrame to use as mask iterator. The geometries
|
|
83
|
+
of mask_gdf will not be shown.
|
|
84
|
+
size: Number of meters (or other crs unit) to buffer the mask geometry
|
|
85
|
+
before clipping.
|
|
86
|
+
sort_values: Optional sorting column(s) of the mask GeoDataFrame. Rows
|
|
87
|
+
will be iterated through from the top.
|
|
88
|
+
only_show_mask: If True (default), show only the mask GeoDataFrame by default.
|
|
89
|
+
The other layers can be toggled on.
|
|
90
|
+
**kwargs: Additional keyword arguments passed to sgis.clipmap.
|
|
91
|
+
|
|
92
|
+
"""
|
|
82
93
|
gdfs, column, kwargs = Map._separate_args(gdfs, column, kwargs)
|
|
83
94
|
|
|
84
95
|
if mask_gdf is None:
|
|
@@ -86,8 +97,14 @@ class Examine:
|
|
|
86
97
|
else:
|
|
87
98
|
self.mask_gdf = mask_gdf
|
|
88
99
|
|
|
89
|
-
m =
|
|
90
|
-
|
|
100
|
+
m = Explore(*gdfs, column=column, **kwargs)
|
|
101
|
+
|
|
102
|
+
# m = Map(*gdfs, column=column, **kwargs)
|
|
103
|
+
self._gdfs: dict[str, gpd.GeoDataFrame] = dict(
|
|
104
|
+
zip(m.labels, m.gdfs, strict=False)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
self.rasters: dict[str, ImageCollection | Image | Band] = m.rasters
|
|
91
108
|
|
|
92
109
|
self.indices = list(range(len(self.mask_gdf)))
|
|
93
110
|
self.i = 0
|
|
@@ -121,7 +138,7 @@ class Examine:
|
|
|
121
138
|
elif not kwargs.get("show", True):
|
|
122
139
|
self.kwargs["show"] = [False] * len(self._gdfs)
|
|
123
140
|
|
|
124
|
-
def next(self, i: int | None = None, **kwargs):
|
|
141
|
+
def next(self, i: int | None = None, **kwargs) -> None:
|
|
125
142
|
"""Displays a map of geometries within the next row of the mask gdf.
|
|
126
143
|
|
|
127
144
|
Args:
|
|
@@ -145,13 +162,14 @@ class Examine:
|
|
|
145
162
|
print(f"i == {self.i} (of {len(self.mask_gdf)})")
|
|
146
163
|
clipmap(
|
|
147
164
|
self.column,
|
|
165
|
+
*list(self.rasters.values()),
|
|
148
166
|
**self._gdfs,
|
|
149
167
|
mask=self.mask_gdf.iloc[[self.i]].buffer(self.size),
|
|
150
168
|
**self.kwargs,
|
|
151
169
|
)
|
|
152
170
|
self.i += 1
|
|
153
171
|
|
|
154
|
-
def sample(self, **kwargs):
|
|
172
|
+
def sample(self, **kwargs) -> None:
|
|
155
173
|
"""Takes a sample index of the mask and displays a map of this area.
|
|
156
174
|
|
|
157
175
|
Args:
|
|
@@ -166,12 +184,13 @@ class Examine:
|
|
|
166
184
|
print(f"Showing index {i}")
|
|
167
185
|
clipmap(
|
|
168
186
|
self.column,
|
|
187
|
+
*list(self.rasters.values()),
|
|
169
188
|
**self._gdfs,
|
|
170
189
|
mask=self.mask_gdf.iloc[[i]].buffer(self.size),
|
|
171
190
|
**self.kwargs,
|
|
172
191
|
)
|
|
173
192
|
|
|
174
|
-
def current(self, i: int | None = None, **kwargs):
|
|
193
|
+
def current(self, i: int | None = None, **kwargs) -> None:
|
|
175
194
|
"""Repeat the last shown map."""
|
|
176
195
|
if kwargs:
|
|
177
196
|
kwargs = self._fix_kwargs(kwargs)
|
|
@@ -185,42 +204,46 @@ class Examine:
|
|
|
185
204
|
print(f"{self.i + 1} of {len(self.mask_gdf)}")
|
|
186
205
|
clipmap(
|
|
187
206
|
self.column,
|
|
207
|
+
*list(self.rasters.values()),
|
|
188
208
|
**self._gdfs,
|
|
189
209
|
mask=self.mask_gdf.iloc[[self.i]].buffer(self.size),
|
|
190
210
|
**self.kwargs,
|
|
191
211
|
)
|
|
192
212
|
|
|
193
|
-
def explore(self, **kwargs):
|
|
213
|
+
def explore(self, **kwargs) -> None:
|
|
194
214
|
"""Show all rows like the function explore."""
|
|
195
215
|
if kwargs:
|
|
196
216
|
kwargs = self._fix_kwargs(kwargs)
|
|
197
217
|
self.kwargs = self.kwargs | kwargs
|
|
198
218
|
|
|
199
219
|
explore(
|
|
220
|
+
*list(self.rasters.values()),
|
|
200
221
|
**self._gdfs,
|
|
201
222
|
column=self.column,
|
|
202
223
|
**self.kwargs,
|
|
203
224
|
)
|
|
204
225
|
|
|
205
|
-
def clipmap(self, **kwargs):
|
|
226
|
+
def clipmap(self, **kwargs) -> None:
|
|
206
227
|
"""Show all rows like the function clipmap."""
|
|
207
228
|
if kwargs:
|
|
208
229
|
kwargs = self._fix_kwargs(kwargs)
|
|
209
230
|
self.kwargs = self.kwargs | kwargs
|
|
210
231
|
|
|
211
232
|
clipmap(
|
|
233
|
+
*list(self.rasters.values()),
|
|
212
234
|
**self._gdfs,
|
|
213
235
|
column=self.column,
|
|
214
236
|
**self.kwargs,
|
|
215
237
|
)
|
|
216
238
|
|
|
217
|
-
def samplemap(self, **kwargs):
|
|
239
|
+
def samplemap(self, **kwargs) -> None:
|
|
218
240
|
"""Show all rows like the function samplemap."""
|
|
219
241
|
if kwargs:
|
|
220
242
|
kwargs = self._fix_kwargs(kwargs)
|
|
221
243
|
self.kwargs = self.kwargs | kwargs
|
|
222
244
|
|
|
223
245
|
samplemap(
|
|
246
|
+
*list(self.rasters.values()),
|
|
224
247
|
**self._gdfs,
|
|
225
248
|
column=self.column,
|
|
226
249
|
**self.kwargs,
|
|
@@ -241,21 +264,25 @@ class Examine:
|
|
|
241
264
|
return gdfs
|
|
242
265
|
|
|
243
266
|
@property
|
|
244
|
-
def bounds(self):
|
|
267
|
+
def bounds(self) -> tuple[float, float, float, float]:
|
|
268
|
+
"""Total bounds of all GeoDataFrames."""
|
|
245
269
|
return get_total_bounds(*list(self.gdfs.values()))
|
|
246
270
|
|
|
247
|
-
def _fix_kwargs(self, kwargs) -> dict:
|
|
271
|
+
def _fix_kwargs(self, kwargs: dict) -> dict:
|
|
248
272
|
self.size = kwargs.pop("size", self.size)
|
|
249
273
|
self.column = kwargs.pop("column", self.column)
|
|
250
274
|
return kwargs
|
|
251
275
|
|
|
252
276
|
def __repr__(self) -> str:
|
|
277
|
+
"""Representation."""
|
|
253
278
|
return f"{self.__class__.__name__}(indices={len(self.indices)}, current={self.i}, n_gdfs={len(self._gdfs)})"
|
|
254
279
|
|
|
255
|
-
def __add__(self, scalar):
|
|
280
|
+
def __add__(self, scalar: int) -> "Examine":
|
|
281
|
+
"""Add a number to the index."""
|
|
256
282
|
self.i += scalar
|
|
257
283
|
return self
|
|
258
284
|
|
|
259
|
-
def __sub__(self, scalar):
|
|
285
|
+
def __sub__(self, scalar: int) -> "Examine":
|
|
286
|
+
"""Subtract a number from the index."""
|
|
260
287
|
self.i -= scalar
|
|
261
288
|
return self
|