ssb-sgis 1.0.2__py3-none-any.whl → 1.0.4__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 (50) hide show
  1. sgis/__init__.py +20 -9
  2. sgis/debug_config.py +24 -0
  3. sgis/exceptions.py +2 -2
  4. sgis/geopandas_tools/bounds.py +33 -36
  5. sgis/geopandas_tools/buffer_dissolve_explode.py +136 -35
  6. sgis/geopandas_tools/centerlines.py +4 -91
  7. sgis/geopandas_tools/cleaning.py +1576 -583
  8. sgis/geopandas_tools/conversion.py +38 -19
  9. sgis/geopandas_tools/duplicates.py +29 -8
  10. sgis/geopandas_tools/general.py +263 -100
  11. sgis/geopandas_tools/geometry_types.py +4 -4
  12. sgis/geopandas_tools/neighbors.py +19 -15
  13. sgis/geopandas_tools/overlay.py +2 -2
  14. sgis/geopandas_tools/point_operations.py +5 -5
  15. sgis/geopandas_tools/polygon_operations.py +510 -105
  16. sgis/geopandas_tools/polygons_as_rings.py +40 -8
  17. sgis/geopandas_tools/sfilter.py +29 -12
  18. sgis/helpers.py +3 -3
  19. sgis/io/dapla_functions.py +238 -19
  20. sgis/io/read_parquet.py +1 -1
  21. sgis/maps/examine.py +27 -12
  22. sgis/maps/explore.py +450 -65
  23. sgis/maps/legend.py +177 -76
  24. sgis/maps/map.py +206 -103
  25. sgis/maps/maps.py +178 -105
  26. sgis/maps/thematicmap.py +243 -83
  27. sgis/networkanalysis/_service_area.py +6 -1
  28. sgis/networkanalysis/closing_network_holes.py +2 -2
  29. sgis/networkanalysis/cutting_lines.py +15 -8
  30. sgis/networkanalysis/directednetwork.py +1 -1
  31. sgis/networkanalysis/finding_isolated_networks.py +15 -8
  32. sgis/networkanalysis/networkanalysis.py +17 -19
  33. sgis/networkanalysis/networkanalysisrules.py +1 -1
  34. sgis/networkanalysis/traveling_salesman.py +1 -1
  35. sgis/parallel/parallel.py +64 -27
  36. sgis/raster/__init__.py +0 -6
  37. sgis/raster/base.py +208 -0
  38. sgis/raster/cube.py +54 -8
  39. sgis/raster/image_collection.py +3257 -0
  40. sgis/raster/indices.py +17 -5
  41. sgis/raster/raster.py +138 -243
  42. sgis/raster/sentinel_config.py +120 -0
  43. sgis/raster/zonal.py +0 -1
  44. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/METADATA +6 -7
  45. ssb_sgis-1.0.4.dist-info/RECORD +62 -0
  46. sgis/raster/methods_as_functions.py +0 -0
  47. sgis/raster/torchgeo.py +0 -171
  48. ssb_sgis-1.0.2.dist-info/RECORD +0 -61
  49. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/LICENSE +0 -0
  50. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/WHEEL +0 -0
sgis/maps/maps.py CHANGED
@@ -8,18 +8,22 @@ The 'qtm' function shows a simple static map of one or more GeoDataFrames.
8
8
  """
9
9
 
10
10
  import inspect
11
+ import random
11
12
  from numbers import Number
12
13
  from typing import Any
13
14
 
15
+ import pyproj
14
16
  from geopandas import GeoDataFrame
15
17
  from geopandas import GeoSeries
16
- from pyproj import CRS
17
18
  from shapely import Geometry
18
19
  from shapely import box
19
20
  from shapely.geometry import Polygon
20
21
 
22
+ from ..debug_config import _NoExplore
21
23
  from ..geopandas_tools.bounds import get_total_bounds
22
- from ..geopandas_tools.conversion import to_gdf as to_gdf_func
24
+ from ..geopandas_tools.conversion import to_bbox
25
+ from ..geopandas_tools.conversion import to_gdf
26
+ from ..geopandas_tools.conversion import to_shapely
23
27
  from ..geopandas_tools.general import clean_geoms
24
28
  from ..geopandas_tools.general import get_common_crs
25
29
  from ..geopandas_tools.general import is_wkt
@@ -40,10 +44,10 @@ except ImportError:
40
44
  def _get_location_mask(kwargs: dict, gdfs) -> tuple[GeoDataFrame | None, dict]:
41
45
  try:
42
46
  crs = get_common_crs(gdfs)
43
- except IndexError:
47
+ except (IndexError, pyproj.exceptions.CRSError):
44
48
  for x in kwargs.values():
45
49
  try:
46
- crs = CRS(x.crs) if hasattr(x, "crs") else CRS(x["crs"])
50
+ crs = pyproj.CRS(x.crs) if hasattr(x, "crs") else pyproj.CRS(x["crs"])
47
51
  break
48
52
  except Exception:
49
53
  crs = None
@@ -67,7 +71,7 @@ def _get_location_mask(kwargs: dict, gdfs) -> tuple[GeoDataFrame | None, dict]:
67
71
  kwargs.pop(key)
68
72
  if isinstance(value, Number) and value > 1:
69
73
  size = value
70
- the_mask = to_gdf_func([mask], crs=4326).to_crs(crs).buffer(size)
74
+ the_mask = to_gdf([mask], crs=4326).to_crs(crs).buffer(size)
71
75
  return the_mask, kwargs
72
76
 
73
77
  return None, kwargs
@@ -77,13 +81,14 @@ def explore(
77
81
  *gdfs: GeoDataFrame | dict[str, GeoDataFrame],
78
82
  column: str | None = None,
79
83
  center: Any | None = None,
80
- labels: tuple[str] | None = None,
81
84
  max_zoom: int = 40,
82
85
  browser: bool = False,
83
86
  smooth_factor: int | float = 1.5,
84
87
  size: int | None = None,
88
+ max_images: int = 10,
89
+ images_to_gdf: bool = False,
85
90
  **kwargs,
86
- ) -> None:
91
+ ) -> Explore:
87
92
  """Interactive map of GeoDataFrames with layers that can be toggled on/off.
88
93
 
89
94
  It takes all the given GeoDataFrames and displays them together in an
@@ -101,9 +106,6 @@ def explore(
101
106
  center: Geometry-like object to center the map on. If a three-length tuple
102
107
  is given, the first two should be x and y coordinates and the third
103
108
  should be a number of meters to buffer the centerpoint by.
104
- labels: By default, the GeoDataFrames will be labeled by their object names.
105
- Alternatively, labels can be specified as a tuple of strings with the same
106
- length as the number of gdfs.
107
109
  max_zoom: The maximum allowed level of zoom. Higher number means more zoom
108
110
  allowed. Defaults to 30, which is higher than the geopandas default.
109
111
  browser: If False (default), the maps will be shown in Jupyter.
@@ -112,17 +114,21 @@ def explore(
112
114
  5 is quite a lot of simplification.
113
115
  size: The buffer distance. Only used when center is given. It then defaults to
114
116
  1000.
117
+ max_images: Maximum number of images (Image, ImageCollection, Band) to show per
118
+ map. Defaults to 10.
119
+ images_to_gdf: If True (not default), images (Image, ImageCollection, Band)
120
+ will be converted to GeoDataFrame and added to the map.
115
121
  **kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
116
122
  instance 'cmap' to change the colors, 'scheme' to change how the data
117
123
  is grouped. This defaults to 'fisherjenkssampled' for numeric data.
118
124
 
119
125
  See Also:
120
- --------
126
+ ---------
121
127
  samplemap: same functionality, but shows only a random area of a given size.
122
128
  clipmap: same functionality, but shows only the areas clipped by a given mask.
123
129
 
124
130
  Examples:
125
- --------
131
+ ---------
126
132
  >>> import sgis as sg
127
133
  >>> roads = sg.read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
128
134
  >>> points = sg.read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/points_oslo.parquet")
@@ -141,6 +147,9 @@ def explore(
141
147
  >>> points["meters"] = points.length
142
148
  >>> sg.explore(roads, points, column="meters", cmap="plasma", max_zoom=60, center_4326=(10.7463, 59.92, 500))
143
149
  """
150
+ if isinstance(center, _NoExplore):
151
+ return
152
+
144
153
  gdfs, column, kwargs = Map._separate_args(gdfs, column, kwargs)
145
154
 
146
155
  loc_mask, kwargs = _get_location_mask(kwargs | {"size": size}, gdfs)
@@ -154,7 +163,6 @@ def explore(
154
163
  *gdfs,
155
164
  column=column,
156
165
  mask=mask,
157
- labels=labels,
158
166
  browser=browser,
159
167
  max_zoom=max_zoom,
160
168
  **kwargs,
@@ -162,10 +170,10 @@ def explore(
162
170
 
163
171
  try:
164
172
  to_crs = gdfs[0].crs
165
- except IndexError:
173
+ except (IndexError, AttributeError):
166
174
  try:
167
175
  to_crs = next(x for x in kwargs.values() if hasattr(x, "crs")).crs
168
- except IndexError:
176
+ except (IndexError, StopIteration):
169
177
  to_crs = None
170
178
 
171
179
  if "crs" in kwargs:
@@ -182,16 +190,66 @@ def explore(
182
190
  else:
183
191
  if isinstance(center, (tuple, list)) and len(center) == 3:
184
192
  *center, size = center
185
- mask = to_gdf_func(center, crs=from_crs)
193
+ mask = to_gdf(center, crs=from_crs)
186
194
 
187
195
  bounds: Polygon = box(*get_total_bounds(*gdfs, *list(kwargs.values())))
188
- if not mask.intersects(bounds).any():
189
- mask = mask.set_crs(4326, allow_override=True)
190
196
 
191
- try:
192
- mask = mask.to_crs(to_crs)
193
- except ValueError:
194
- pass
197
+ any_intersections: bool = mask.intersects(bounds).any()
198
+ if not any_intersections and to_crs is None:
199
+ mask = to_gdf(Polygon(), to_crs)
200
+ elif not any_intersections:
201
+ bounds4326 = to_gdf(bounds, to_crs).to_crs(25833).geometry.iloc[0]
202
+ mask4326 = mask.set_crs(4326, allow_override=True).to_crs(25833)
203
+
204
+ if (mask4326.distance(bounds4326) > size).all():
205
+ # try flipping coordinates
206
+ x, y = next(iter(mask.geometry.iloc[0].coords))
207
+ mask4326 = to_gdf([y, x], 4326).to_crs(25833)
208
+
209
+ if (mask4326.distance(bounds4326) > size).all():
210
+ mask = to_gdf(Polygon(), to_crs)
211
+ else:
212
+ mask = mask4326.to_crs(to_crs)
213
+
214
+ # else:
215
+ # mask_flipped = mask
216
+
217
+ # # coords = mask.get_coordinates()
218
+ # if (
219
+ # (mask_flipped.distance(bounds) > size).all()
220
+ # # and coords["x"].max() < 180
221
+ # # and coords["y"].max() < 180
222
+ # # and coords["x"].min() > -180
223
+ # # and coords["y"].min() > -180
224
+ # ):
225
+ # try:
226
+ # bounds4326 = to_gdf(bounds, to_crs).to_crs(4326).geometry.iloc[0]
227
+ # except ValueError:
228
+ # bounds4326 = to_gdf(bounds, to_crs).set_crs(4326).geometry.iloc[0]
229
+
230
+ # mask4326 = mask.set_crs(4326, allow_override=True)
231
+
232
+ # if (mask4326.distance(bounds4326) > size).all():
233
+ # # try flipping coordinates
234
+ # x, y = list(mask4326.geometry.iloc[0].coords)[0]
235
+ # mask4326 = to_gdf([y, x], 4326)
236
+
237
+ # mask = mask4326
238
+
239
+ # # if mask4326.intersects(bounds4326).any():
240
+ # # mask = mask4326
241
+ # # else:
242
+ # # try:
243
+ # # mask = mask.to_crs(to_crs)
244
+ # # except ValueError:
245
+ # # pass
246
+ # else:
247
+ # mask = mask_flipped
248
+
249
+ # try:
250
+ # mask = mask.to_crs(to_crs)
251
+ # except ValueError:
252
+ # pass
195
253
 
196
254
  if get_geom_type(mask) in ["point", "line"]:
197
255
  mask = mask.buffer(size)
@@ -200,7 +258,6 @@ def explore(
200
258
  *gdfs,
201
259
  column=column,
202
260
  mask=mask,
203
- labels=labels,
204
261
  browser=browser,
205
262
  max_zoom=max_zoom,
206
263
  **kwargs,
@@ -209,34 +266,36 @@ def explore(
209
266
  m = Explore(
210
267
  *gdfs,
211
268
  column=column,
212
- labels=labels,
213
269
  browser=browser,
214
270
  max_zoom=max_zoom,
215
271
  smooth_factor=smooth_factor,
272
+ max_images=max_images,
216
273
  **kwargs,
217
274
  )
218
275
 
219
- if m.gdfs is None and not len(m.raster_datasets):
220
- return
276
+ if m.gdfs is None and not len(m.rasters):
277
+ return m
221
278
 
222
279
  if not kwargs.pop("explore", True):
223
280
  return qtm(m._gdf, column=m.column, cmap=m._cmap, k=m.k)
224
281
 
225
282
  m.explore()
226
283
 
284
+ return m
285
+
227
286
 
228
287
  def samplemap(
229
288
  *gdfs: GeoDataFrame,
230
289
  column: str | None = None,
231
290
  size: int = 1000,
232
291
  sample_from_first: bool = True,
233
- labels: tuple[str] | None = None,
234
292
  max_zoom: int = 40,
235
293
  smooth_factor: int = 1.5,
236
294
  explore: bool = True,
237
295
  browser: bool = False,
296
+ max_images: int = 10,
238
297
  **kwargs,
239
- ) -> None:
298
+ ) -> Explore:
240
299
  """Shows an interactive map of a random area of GeoDataFrames.
241
300
 
242
301
  It takes all the GeoDataFrames specified, takes a random sample point from the
@@ -258,9 +317,6 @@ def samplemap(
258
317
  Defaults to 1000 (meters).
259
318
  sample_from_first: If True (default), the sample point is taken form the
260
319
  first specified GeoDataFrame. If False, all GeoDataFrames are considered.
261
- labels: By default, the GeoDataFrames will be labeled by their object names.
262
- Alternatively, labels can be specified as a tuple of strings the same
263
- length as the number of gdfs.
264
320
  max_zoom: The maximum allowed level of zoom. Higher number means more zoom
265
321
  allowed. Defaults to 30, which is higher than the geopandas default.
266
322
  smooth_factor: How much to simplify the geometries. 1 is the minimum,
@@ -269,17 +325,19 @@ def samplemap(
269
325
  or not in Jupyter, a static plot will be shown.
270
326
  browser: If False (default), the maps will be shown in Jupyter.
271
327
  If True the maps will be opened in a browser folder.
328
+ max_images: Maximum number of images (Image, ImageCollection, Band) to show per
329
+ map. Defaults to 10.
272
330
  **kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
273
331
  instance 'cmap' to change the colors, 'scheme' to change how the data
274
332
  is grouped. This defaults to 'fisherjenkssampled' for numeric data.
275
333
 
276
334
  See Also:
277
- --------
335
+ ---------
278
336
  explore: Same functionality, but shows the entire area of the geometries.
279
337
  clipmap: Same functionality, but shows only the areas clipped by a given mask.
280
338
 
281
339
  Examples:
282
- --------
340
+ ---------
283
341
  >>> from sgis import read_parquet_url, samplemap
284
342
  >>> roads = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_eidskog_2022.parquet")
285
343
  >>> points = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/points_eidskog.parquet")
@@ -293,77 +351,80 @@ def samplemap(
293
351
  >>> samplemap(roads, points, size=5_000, column="meters")
294
352
 
295
353
  """
296
- if gdfs and isinstance(gdfs[-1], (float, int)):
354
+ if isinstance(kwargs.get("center", None), _NoExplore):
355
+ return
356
+
357
+ if gdfs and len(gdfs) > 1 and isinstance(gdfs[-1], (float, int)):
297
358
  *gdfs, size = gdfs
298
359
 
299
360
  gdfs, column, kwargs = Map._separate_args(gdfs, column, kwargs)
300
361
 
301
- mask, kwargs = _get_location_mask(kwargs | {"size": size}, gdfs)
362
+ loc_mask, kwargs = _get_location_mask(kwargs | {"size": size}, gdfs)
302
363
  kwargs.pop("size")
364
+ mask = kwargs.pop("mask", loc_mask)
303
365
 
304
- if explore:
305
- m = Explore(
306
- *gdfs,
307
- column=column,
308
- labels=labels,
309
- browser=browser,
310
- max_zoom=max_zoom,
311
- smooth_factor=smooth_factor,
312
- **kwargs,
313
- )
314
- if m.gdfs is None and not len(m.raster_datasets):
315
- return
316
- if mask is not None:
317
- m._gdfs = [gdf.clip(mask) for gdf in m._gdfs]
318
- m._gdf = m._gdf.clip(mask)
319
- m._nan_idx = m._gdf[m._column].isna()
320
- m._get_unique_values()
321
-
322
- m.samplemap(size, sample_from_first=sample_from_first)
366
+ i = 0 if sample_from_first else random.choice(list(range(len(gdfs))))
367
+ try:
368
+ sample = gdfs[i]
369
+ except IndexError:
370
+ sample = list(kwargs.values())[i]
323
371
 
372
+ if mask is None:
373
+ try:
374
+ sample = sample.geometry.loc[lambda x: ~x.is_empty].sample(1)
375
+ except Exception:
376
+ try:
377
+ sample = sample.sample(1)
378
+ except Exception:
379
+ pass
380
+ try:
381
+ sample = to_gdf(to_shapely(sample)).explode(ignore_index=True)
382
+ except Exception:
383
+ sample = to_gdf(to_shapely(to_bbox(sample))).explode(ignore_index=True)
324
384
  else:
325
- m = Map(
326
- *gdfs,
327
- column=column,
328
- labels=labels,
329
- **kwargs,
330
- )
331
-
332
- if sample_from_first:
333
- sample = m._gdfs[0].sample(1)
334
- else:
335
- sample = m._gdf.sample(1)
336
-
337
- # convert lines to polygons
338
- if get_geom_type(sample) == "line":
339
- sample["geometry"] = sample.buffer(1)
385
+ try:
386
+ sample = to_gdf(to_shapely(sample)).explode(ignore_index=True)
387
+ except Exception:
388
+ sample = to_gdf(to_shapely(to_bbox(sample))).explode(ignore_index=True)
340
389
 
341
- if get_geom_type(sample) == "polygon":
342
- random_point = sample.sample_points(size=1)
390
+ sample = sample.clip(mask).explode(ignore_index=True).sample(1)
343
391
 
344
- # if point or mixed geometries
345
- else:
346
- random_point = sample.centroid
392
+ print(locals())
393
+ random_point = sample.sample_points(size=1)
347
394
 
395
+ try:
348
396
  center = (random_point.geometry.iloc[0].x, random_point.geometry.iloc[0].y)
349
- print(f"center={center}, size={size}")
397
+ except AttributeError as e:
398
+ raise AttributeError(e, random_point.geometry.iloc[0]) from e
350
399
 
351
- m._gdf = m._gdf.clip(random_point.buffer(size))
400
+ print(f"center={center}, size={size}")
352
401
 
353
- qtm(m._gdf, column=m.column, cmap=m._cmap, k=m.k)
402
+ mask = random_point.buffer(size)
403
+
404
+ return clipmap(
405
+ *gdfs,
406
+ column=column,
407
+ mask=mask,
408
+ browser=browser,
409
+ max_zoom=max_zoom,
410
+ explore=explore,
411
+ smooth_factor=smooth_factor,
412
+ max_images=max_images,
413
+ **kwargs,
414
+ )
354
415
 
355
416
 
356
417
  def clipmap(
357
418
  *gdfs: GeoDataFrame,
358
419
  column: str | None = None,
359
420
  mask: GeoDataFrame | GeoSeries | Geometry = None,
360
- labels: tuple[str] | None = None,
361
421
  explore: bool = True,
362
422
  max_zoom: int = 40,
363
423
  smooth_factor: int | float = 1.5,
364
424
  browser: bool = False,
425
+ max_images: int = 10,
365
426
  **kwargs,
366
- ) -> None:
427
+ ) -> Explore | Map:
367
428
  """Shows an interactive map of a of GeoDataFrames clipped to the mask extent.
368
429
 
369
430
  It takes all the GeoDataFrames specified, clips them to the extent of the mask,
@@ -380,9 +441,6 @@ def clipmap(
380
441
  mask: the geometry to clip the data by.
381
442
  column: The column to color the geometries by. Defaults to None, which means
382
443
  each GeoDataFrame will get a unique color.
383
- labels: By default, the GeoDataFrames will be labeled by their object names.
384
- Alternatively, labels can be specified as a tuple of strings the same
385
- length as the number of gdfs.
386
444
  max_zoom: The maximum allowed level of zoom. Higher number means more zoom
387
445
  allowed. Defaults to 30, which is higher than the geopandas default.
388
446
  smooth_factor: How much to simplify the geometries. 1 is the minimum,
@@ -391,21 +449,31 @@ def clipmap(
391
449
  or not in Jupyter, a static plot will be shown.
392
450
  browser: If False (default), the maps will be shown in Jupyter.
393
451
  If True the maps will be opened in a browser folder.
452
+ max_images: Maximum number of images (Image, ImageCollection, Band) to show per
453
+ map. Defaults to 10.
394
454
  **kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
395
455
  instance 'cmap' to change the colors, 'scheme' to change how the data
396
456
  is grouped. This defaults to 'fisherjenkssampled' for numeric data.
397
457
 
398
458
  See Also:
399
- --------
459
+ ---------
400
460
  explore: same functionality, but shows the entire area of the geometries.
401
461
  samplemap: same functionality, but shows only a random area of a given size.
402
462
  """
403
- gdfs, column, kwargs = Map._separate_args(gdfs, column, kwargs)
463
+ if isinstance(kwargs.get("center", None), _NoExplore):
464
+ return
404
465
 
466
+ gdfs, column, kwargs = Map._separate_args(gdfs, column, kwargs)
405
467
  if mask is None and len(gdfs) > 1:
406
468
  mask = gdfs[-1]
407
469
  gdfs = gdfs[:-1]
408
470
 
471
+ if not isinstance(mask, (GeoDataFrame | GeoSeries | Geometry | tuple)):
472
+ try:
473
+ mask = to_gdf(mask)
474
+ except Exception:
475
+ mask = to_shapely(to_bbox(mask))
476
+
409
477
  center = kwargs.pop("center", None)
410
478
  size = kwargs.pop("size", None)
411
479
 
@@ -413,29 +481,31 @@ def clipmap(
413
481
  m = Explore(
414
482
  *gdfs,
415
483
  column=column,
416
- labels=labels,
417
484
  browser=browser,
418
485
  max_zoom=max_zoom,
419
486
  smooth_factor=smooth_factor,
487
+ max_images=max_images,
420
488
  **kwargs,
421
489
  )
422
- if m.gdfs is None and not len(m.raster_datasets):
423
- return
490
+ m.mask = mask
491
+
492
+ if m.gdfs is None and not len(m.rasters):
493
+ return m
424
494
 
425
495
  m._gdfs = [gdf.clip(mask) for gdf in m._gdfs]
426
496
  m._gdf = m._gdf.clip(mask)
427
497
  m._nan_idx = m._gdf[m._column].isna()
428
498
  m._get_unique_values()
429
499
  m.explore(center=center, size=size)
500
+ return m
430
501
  else:
431
502
  m = Map(
432
503
  *gdfs,
433
504
  column=column,
434
- labels=labels,
435
505
  **kwargs,
436
506
  )
437
507
  if m.gdfs is None:
438
- return
508
+ return m
439
509
 
440
510
  m._gdfs = [gdf.clip(mask) for gdf in m._gdfs]
441
511
  m._gdf = m._gdf.clip(mask)
@@ -444,8 +514,12 @@ def clipmap(
444
514
 
445
515
  qtm(m._gdf, column=m.column, cmap=m._cmap, k=m.k)
446
516
 
517
+ return m
447
518
 
448
- def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
519
+
520
+ def explore_locals(
521
+ *gdfs: GeoDataFrame, convert: bool = True, crs: Any | None = None, **kwargs
522
+ ) -> None:
449
523
  """Displays all local variables with geometries (GeoDataFrame etc.).
450
524
 
451
525
  Local means inside a function or file/notebook.
@@ -454,8 +528,11 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
454
528
  *gdfs: Additional GeoDataFrames.
455
529
  convert: If True (default), non-GeoDataFrames will be converted
456
530
  to GeoDataFrames if possible.
531
+ crs: Optional crs if no objects have any crs.
457
532
  **kwargs: keyword arguments passed to sg.explore.
458
533
  """
534
+ if isinstance(kwargs.get("center", None), _NoExplore):
535
+ return
459
536
 
460
537
  def as_dict(obj):
461
538
  if hasattr(obj, "__dict__"):
@@ -477,11 +554,19 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
477
554
  if not convert:
478
555
  continue
479
556
 
557
+ try:
558
+ gdf = clean_geoms(to_gdf(value, crs=crs))
559
+ if len(gdf):
560
+ local_gdfs[name] = gdf
561
+ continue
562
+ except Exception:
563
+ pass
564
+
480
565
  if isinstance(value, dict) or hasattr(value, "__dict__"):
481
566
  # add dicts or classes with GeoDataFrames to kwargs
482
567
  for key, val in as_dict(value).items():
483
568
  if isinstance(val, allowed_types):
484
- gdf = clean_geoms(to_gdf_func(val))
569
+ gdf = clean_geoms(to_gdf(val, crs=crs))
485
570
  if len(gdf):
486
571
  local_gdfs[key] = gdf
487
572
 
@@ -489,22 +574,13 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
489
574
  try:
490
575
  for k, v in val.items():
491
576
  if isinstance(v, allowed_types):
492
- gdf = clean_geoms(to_gdf_func(v))
577
+ gdf = clean_geoms(to_gdf(v, crs=crs))
493
578
  if len(gdf):
494
579
  local_gdfs[k] = gdf
495
580
  except Exception:
496
581
  # no need to raise here
497
582
  pass
498
583
 
499
- continue
500
- try:
501
- gdf = clean_geoms(to_gdf_func(value))
502
- if len(gdf):
503
- local_gdfs[name] = gdf
504
- continue
505
- except Exception:
506
- pass
507
-
508
584
  if local_gdfs:
509
585
  break
510
586
 
@@ -513,7 +589,7 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
513
589
  if not frame:
514
590
  break
515
591
 
516
- explore(*gdfs, **local_gdfs, **kwargs)
592
+ return explore(*gdfs, **(local_gdfs | kwargs))
517
593
 
518
594
 
519
595
  def qtm(
@@ -563,9 +639,6 @@ def qtm(
563
639
  else:
564
640
  new_kwargs[key] = value
565
641
 
566
- # self.labels.append(key)
567
- # self.show.append(last_show)
568
-
569
642
  m = ThematicMap(*gdfs, column=column, size=size, black=black)
570
643
 
571
644
  if m._gdfs is None: