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.
Files changed (60) hide show
  1. sgis/__init__.py +107 -121
  2. sgis/exceptions.py +5 -3
  3. sgis/geopandas_tools/__init__.py +1 -0
  4. sgis/geopandas_tools/bounds.py +86 -47
  5. sgis/geopandas_tools/buffer_dissolve_explode.py +62 -39
  6. sgis/geopandas_tools/centerlines.py +53 -44
  7. sgis/geopandas_tools/cleaning.py +87 -104
  8. sgis/geopandas_tools/conversion.py +164 -107
  9. sgis/geopandas_tools/duplicates.py +33 -19
  10. sgis/geopandas_tools/general.py +84 -52
  11. sgis/geopandas_tools/geometry_types.py +24 -10
  12. sgis/geopandas_tools/neighbors.py +23 -11
  13. sgis/geopandas_tools/overlay.py +136 -53
  14. sgis/geopandas_tools/point_operations.py +11 -10
  15. sgis/geopandas_tools/polygon_operations.py +53 -61
  16. sgis/geopandas_tools/polygons_as_rings.py +121 -78
  17. sgis/geopandas_tools/sfilter.py +17 -17
  18. sgis/helpers.py +116 -58
  19. sgis/io/dapla_functions.py +32 -23
  20. sgis/io/opener.py +13 -6
  21. sgis/io/read_parquet.py +2 -2
  22. sgis/maps/examine.py +55 -28
  23. sgis/maps/explore.py +471 -112
  24. sgis/maps/httpserver.py +12 -12
  25. sgis/maps/legend.py +285 -134
  26. sgis/maps/map.py +248 -129
  27. sgis/maps/maps.py +123 -119
  28. sgis/maps/thematicmap.py +260 -94
  29. sgis/maps/tilesources.py +3 -8
  30. sgis/networkanalysis/_get_route.py +5 -4
  31. sgis/networkanalysis/_od_cost_matrix.py +44 -1
  32. sgis/networkanalysis/_points.py +10 -4
  33. sgis/networkanalysis/_service_area.py +5 -2
  34. sgis/networkanalysis/closing_network_holes.py +22 -64
  35. sgis/networkanalysis/cutting_lines.py +58 -46
  36. sgis/networkanalysis/directednetwork.py +16 -8
  37. sgis/networkanalysis/finding_isolated_networks.py +6 -5
  38. sgis/networkanalysis/network.py +15 -13
  39. sgis/networkanalysis/networkanalysis.py +79 -61
  40. sgis/networkanalysis/networkanalysisrules.py +21 -17
  41. sgis/networkanalysis/nodes.py +2 -3
  42. sgis/networkanalysis/traveling_salesman.py +6 -3
  43. sgis/parallel/parallel.py +372 -142
  44. sgis/raster/base.py +9 -3
  45. sgis/raster/cube.py +331 -213
  46. sgis/raster/cubebase.py +15 -29
  47. sgis/raster/image_collection.py +2560 -0
  48. sgis/raster/indices.py +17 -12
  49. sgis/raster/raster.py +356 -275
  50. sgis/raster/sentinel_config.py +104 -0
  51. sgis/raster/zonal.py +38 -14
  52. {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/LICENSE +1 -1
  53. {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/METADATA +87 -16
  54. ssb_sgis-1.0.3.dist-info/RECORD +61 -0
  55. {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/WHEEL +1 -1
  56. sgis/raster/bands.py +0 -48
  57. sgis/raster/gradient.py +0 -78
  58. sgis/raster/methods_as_functions.py +0 -124
  59. sgis/raster/torchgeo.py +0 -150
  60. 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(*dicts, fillvalue=None):
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 | None, str | None] | list[str] | str | int | float
105
- ) -> tuple[str | int | float, str | int | float | None]:
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 hasattr(vals, "__iter__"):
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 | None:
159
- """Searches through the local variables down one level at a time."""
160
- frame = inspect.currentframe()
161
-
162
- for _ in range(start):
163
- frame = frame.f_back
164
-
165
- for _ in np.arange(start, stop):
166
- names = [
167
- var_name for var_name, var_val in frame.f_locals.items() if var_val is var
168
- ]
169
- if names and len(names) == 1:
170
- if ignore_self and names[0] == "self":
171
- frame = frame.f_back
172
- continue
173
- return names[0]
174
-
175
- names = [name for name in names if not name.startswith("_")]
176
-
177
- if names and len(names) == 1:
178
- if ignore_self and names[0] == "self":
179
- frame = frame.f_back
180
- continue
181
-
182
- return names[0]
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
- def __init__(self, func: str):
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"
@@ -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: Optional[dp.gcs.GCSFileSystem] = None,
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 a file on Google Cloud Storage.
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 as
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
- Returns:
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: gpd.GeoDataFrame,
89
+ df: GeoDataFrame,
81
90
  gcs_path: str | Path,
82
91
  overwrite: bool = True,
83
92
  pandas_fallback: bool = False,
84
- file_system: Optional[dp.gcs.GCSFileSystem] = None,
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
- pd.io.parquet.BaseImpl.validate_dataframe(df)
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 += get_files_in_subfolders(folderinfo)
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 get_files_in_subfolders(folderinfo: list[dict]) -> list[dict]:
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
- import dapla as dp
6
+ from dapla import FileClient
7
+ from dapla.gcs import GCSFileSystem
6
8
  except ImportError:
7
- pass
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(path, mode="rb", file_system=None):
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 = dp.FileClient.get_gcs_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, explore, samplemap
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
- Args:
23
- *gdfs: One or more GeoDataFrames. The rows of the first GeoDataFrame
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 = Map(*gdfs, column=column, **kwargs)
90
- self._gdfs: dict[str, gpd.GeoDataFrame] = dict(zip(m.labels, m.gdfs))
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