ssb-sgis 1.0.2__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 (42) hide show
  1. sgis/__init__.py +10 -6
  2. sgis/exceptions.py +2 -2
  3. sgis/geopandas_tools/bounds.py +17 -15
  4. sgis/geopandas_tools/buffer_dissolve_explode.py +24 -5
  5. sgis/geopandas_tools/conversion.py +15 -6
  6. sgis/geopandas_tools/duplicates.py +2 -2
  7. sgis/geopandas_tools/general.py +9 -5
  8. sgis/geopandas_tools/geometry_types.py +3 -3
  9. sgis/geopandas_tools/neighbors.py +3 -3
  10. sgis/geopandas_tools/point_operations.py +2 -2
  11. sgis/geopandas_tools/polygon_operations.py +5 -5
  12. sgis/geopandas_tools/sfilter.py +3 -3
  13. sgis/helpers.py +3 -3
  14. sgis/io/read_parquet.py +1 -1
  15. sgis/maps/examine.py +16 -2
  16. sgis/maps/explore.py +370 -57
  17. sgis/maps/legend.py +164 -72
  18. sgis/maps/map.py +184 -90
  19. sgis/maps/maps.py +92 -90
  20. sgis/maps/thematicmap.py +236 -83
  21. sgis/networkanalysis/closing_network_holes.py +2 -2
  22. sgis/networkanalysis/cutting_lines.py +3 -3
  23. sgis/networkanalysis/directednetwork.py +1 -1
  24. sgis/networkanalysis/finding_isolated_networks.py +2 -2
  25. sgis/networkanalysis/networkanalysis.py +7 -7
  26. sgis/networkanalysis/networkanalysisrules.py +1 -1
  27. sgis/networkanalysis/traveling_salesman.py +1 -1
  28. sgis/parallel/parallel.py +39 -19
  29. sgis/raster/__init__.py +0 -6
  30. sgis/raster/cube.py +51 -5
  31. sgis/raster/image_collection.py +2560 -0
  32. sgis/raster/indices.py +14 -5
  33. sgis/raster/raster.py +131 -236
  34. sgis/raster/sentinel_config.py +104 -0
  35. sgis/raster/zonal.py +0 -1
  36. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.3.dist-info}/METADATA +1 -1
  37. ssb_sgis-1.0.3.dist-info/RECORD +61 -0
  38. sgis/raster/methods_as_functions.py +0 -0
  39. sgis/raster/torchgeo.py +0 -171
  40. ssb_sgis-1.0.2.dist-info/RECORD +0 -61
  41. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.3.dist-info}/LICENSE +0 -0
  42. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.3.dist-info}/WHEEL +0 -0
sgis/maps/maps.py CHANGED
@@ -8,18 +8,21 @@ 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
 
21
22
  from ..geopandas_tools.bounds import get_total_bounds
22
- from ..geopandas_tools.conversion import to_gdf as to_gdf_func
23
+ from ..geopandas_tools.conversion import to_bbox
24
+ from ..geopandas_tools.conversion import to_gdf
25
+ from ..geopandas_tools.conversion import to_shapely
23
26
  from ..geopandas_tools.general import clean_geoms
24
27
  from ..geopandas_tools.general import get_common_crs
25
28
  from ..geopandas_tools.general import is_wkt
@@ -40,10 +43,10 @@ except ImportError:
40
43
  def _get_location_mask(kwargs: dict, gdfs) -> tuple[GeoDataFrame | None, dict]:
41
44
  try:
42
45
  crs = get_common_crs(gdfs)
43
- except IndexError:
46
+ except (IndexError, pyproj.exceptions.CRSError):
44
47
  for x in kwargs.values():
45
48
  try:
46
- crs = CRS(x.crs) if hasattr(x, "crs") else CRS(x["crs"])
49
+ crs = pyproj.CRS(x.crs) if hasattr(x, "crs") else pyproj.CRS(x["crs"])
47
50
  break
48
51
  except Exception:
49
52
  crs = None
@@ -67,7 +70,7 @@ def _get_location_mask(kwargs: dict, gdfs) -> tuple[GeoDataFrame | None, dict]:
67
70
  kwargs.pop(key)
68
71
  if isinstance(value, Number) and value > 1:
69
72
  size = value
70
- the_mask = to_gdf_func([mask], crs=4326).to_crs(crs).buffer(size)
73
+ the_mask = to_gdf([mask], crs=4326).to_crs(crs).buffer(size)
71
74
  return the_mask, kwargs
72
75
 
73
76
  return None, kwargs
@@ -77,13 +80,13 @@ def explore(
77
80
  *gdfs: GeoDataFrame | dict[str, GeoDataFrame],
78
81
  column: str | None = None,
79
82
  center: Any | None = None,
80
- labels: tuple[str] | None = None,
81
83
  max_zoom: int = 40,
82
84
  browser: bool = False,
83
85
  smooth_factor: int | float = 1.5,
84
86
  size: int | None = None,
87
+ max_images: int = 15,
85
88
  **kwargs,
86
- ) -> None:
89
+ ) -> Explore:
87
90
  """Interactive map of GeoDataFrames with layers that can be toggled on/off.
88
91
 
89
92
  It takes all the given GeoDataFrames and displays them together in an
@@ -101,9 +104,6 @@ def explore(
101
104
  center: Geometry-like object to center the map on. If a three-length tuple
102
105
  is given, the first two should be x and y coordinates and the third
103
106
  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
107
  max_zoom: The maximum allowed level of zoom. Higher number means more zoom
108
108
  allowed. Defaults to 30, which is higher than the geopandas default.
109
109
  browser: If False (default), the maps will be shown in Jupyter.
@@ -112,17 +112,19 @@ def explore(
112
112
  5 is quite a lot of simplification.
113
113
  size: The buffer distance. Only used when center is given. It then defaults to
114
114
  1000.
115
+ max_images: Maximum number of images (Image, ImageCollection, Band) to show per
116
+ map. Defaults to 15.
115
117
  **kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
116
118
  instance 'cmap' to change the colors, 'scheme' to change how the data
117
119
  is grouped. This defaults to 'fisherjenkssampled' for numeric data.
118
120
 
119
121
  See Also:
120
- --------
122
+ ---------
121
123
  samplemap: same functionality, but shows only a random area of a given size.
122
124
  clipmap: same functionality, but shows only the areas clipped by a given mask.
123
125
 
124
126
  Examples:
125
- --------
127
+ ---------
126
128
  >>> import sgis as sg
127
129
  >>> roads = sg.read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
128
130
  >>> points = sg.read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/points_oslo.parquet")
@@ -154,7 +156,6 @@ def explore(
154
156
  *gdfs,
155
157
  column=column,
156
158
  mask=mask,
157
- labels=labels,
158
159
  browser=browser,
159
160
  max_zoom=max_zoom,
160
161
  **kwargs,
@@ -162,10 +163,10 @@ def explore(
162
163
 
163
164
  try:
164
165
  to_crs = gdfs[0].crs
165
- except IndexError:
166
+ except (IndexError, AttributeError):
166
167
  try:
167
168
  to_crs = next(x for x in kwargs.values() if hasattr(x, "crs")).crs
168
- except IndexError:
169
+ except (IndexError, StopIteration):
169
170
  to_crs = None
170
171
 
171
172
  if "crs" in kwargs:
@@ -182,7 +183,7 @@ def explore(
182
183
  else:
183
184
  if isinstance(center, (tuple, list)) and len(center) == 3:
184
185
  *center, size = center
185
- mask = to_gdf_func(center, crs=from_crs)
186
+ mask = to_gdf(center, crs=from_crs)
186
187
 
187
188
  bounds: Polygon = box(*get_total_bounds(*gdfs, *list(kwargs.values())))
188
189
  if not mask.intersects(bounds).any():
@@ -200,7 +201,6 @@ def explore(
200
201
  *gdfs,
201
202
  column=column,
202
203
  mask=mask,
203
- labels=labels,
204
204
  browser=browser,
205
205
  max_zoom=max_zoom,
206
206
  **kwargs,
@@ -209,34 +209,36 @@ def explore(
209
209
  m = Explore(
210
210
  *gdfs,
211
211
  column=column,
212
- labels=labels,
213
212
  browser=browser,
214
213
  max_zoom=max_zoom,
215
214
  smooth_factor=smooth_factor,
215
+ max_images=max_images,
216
216
  **kwargs,
217
217
  )
218
218
 
219
- if m.gdfs is None and not len(m.raster_datasets):
220
- return
219
+ if m.gdfs is None and not len(m.rasters):
220
+ return m
221
221
 
222
222
  if not kwargs.pop("explore", True):
223
223
  return qtm(m._gdf, column=m.column, cmap=m._cmap, k=m.k)
224
224
 
225
225
  m.explore()
226
226
 
227
+ return m
228
+
227
229
 
228
230
  def samplemap(
229
231
  *gdfs: GeoDataFrame,
230
232
  column: str | None = None,
231
233
  size: int = 1000,
232
234
  sample_from_first: bool = True,
233
- labels: tuple[str] | None = None,
234
235
  max_zoom: int = 40,
235
236
  smooth_factor: int = 1.5,
236
237
  explore: bool = True,
237
238
  browser: bool = False,
239
+ max_images: int = 15,
238
240
  **kwargs,
239
- ) -> None:
241
+ ) -> Explore:
240
242
  """Shows an interactive map of a random area of GeoDataFrames.
241
243
 
242
244
  It takes all the GeoDataFrames specified, takes a random sample point from the
@@ -258,9 +260,6 @@ def samplemap(
258
260
  Defaults to 1000 (meters).
259
261
  sample_from_first: If True (default), the sample point is taken form the
260
262
  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
263
  max_zoom: The maximum allowed level of zoom. Higher number means more zoom
265
264
  allowed. Defaults to 30, which is higher than the geopandas default.
266
265
  smooth_factor: How much to simplify the geometries. 1 is the minimum,
@@ -269,17 +268,19 @@ def samplemap(
269
268
  or not in Jupyter, a static plot will be shown.
270
269
  browser: If False (default), the maps will be shown in Jupyter.
271
270
  If True the maps will be opened in a browser folder.
271
+ max_images: Maximum number of images (Image, ImageCollection, Band) to show per
272
+ map. Defaults to 15.
272
273
  **kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
273
274
  instance 'cmap' to change the colors, 'scheme' to change how the data
274
275
  is grouped. This defaults to 'fisherjenkssampled' for numeric data.
275
276
 
276
277
  See Also:
277
- --------
278
+ ---------
278
279
  explore: Same functionality, but shows the entire area of the geometries.
279
280
  clipmap: Same functionality, but shows only the areas clipped by a given mask.
280
281
 
281
282
  Examples:
282
- --------
283
+ ---------
283
284
  >>> from sgis import read_parquet_url, samplemap
284
285
  >>> roads = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_eidskog_2022.parquet")
285
286
  >>> points = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/points_eidskog.parquet")
@@ -293,77 +294,72 @@ def samplemap(
293
294
  >>> samplemap(roads, points, size=5_000, column="meters")
294
295
 
295
296
  """
296
- if gdfs and isinstance(gdfs[-1], (float, int)):
297
+ if gdfs and len(gdfs) > 1 and isinstance(gdfs[-1], (float, int)):
297
298
  *gdfs, size = gdfs
298
299
 
299
300
  gdfs, column, kwargs = Map._separate_args(gdfs, column, kwargs)
300
301
 
301
- mask, kwargs = _get_location_mask(kwargs | {"size": size}, gdfs)
302
+ loc_mask, kwargs = _get_location_mask(kwargs | {"size": size}, gdfs)
302
303
  kwargs.pop("size")
304
+ mask = kwargs.pop("mask", loc_mask)
303
305
 
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)
306
+ i = 0 if sample_from_first else random.choice(list(range(len(gdfs))))
307
+ try:
308
+ sample = gdfs[i]
309
+ except IndexError:
310
+ sample = list(kwargs.values())[i]
323
311
 
312
+ if mask is None:
313
+ try:
314
+ sample = sample.geometry.dropna().sample(1)
315
+ except Exception:
316
+ try:
317
+ sample = sample.sample(1)
318
+ except Exception:
319
+ pass
320
+ try:
321
+ sample = to_gdf(to_shapely(sample)).explode(ignore_index=True)
322
+ except Exception:
323
+ sample = to_gdf(to_shapely(to_bbox(sample))).explode(ignore_index=True)
324
324
  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)
325
+ try:
326
+ sample = to_gdf(to_shapely(sample)).explode(ignore_index=True)
327
+ except Exception:
328
+ sample = to_gdf(to_shapely(to_bbox(sample))).explode(ignore_index=True)
340
329
 
341
- if get_geom_type(sample) == "polygon":
342
- random_point = sample.sample_points(size=1)
330
+ sample = sample.clip(mask).sample(1)
343
331
 
344
- # if point or mixed geometries
345
- else:
346
- random_point = sample.centroid
332
+ random_point = sample.sample_points(size=1)
347
333
 
348
- center = (random_point.geometry.iloc[0].x, random_point.geometry.iloc[0].y)
349
- print(f"center={center}, size={size}")
334
+ center = (random_point.geometry.iloc[0].x, random_point.geometry.iloc[0].y)
335
+ print(f"center={center}, size={size}")
350
336
 
351
- m._gdf = m._gdf.clip(random_point.buffer(size))
337
+ mask = random_point.buffer(size)
352
338
 
353
- qtm(m._gdf, column=m.column, cmap=m._cmap, k=m.k)
339
+ return clipmap(
340
+ *gdfs,
341
+ column=column,
342
+ mask=mask,
343
+ browser=browser,
344
+ max_zoom=max_zoom,
345
+ explore=explore,
346
+ smooth_factor=smooth_factor,
347
+ max_images=max_images,
348
+ **kwargs,
349
+ )
354
350
 
355
351
 
356
352
  def clipmap(
357
353
  *gdfs: GeoDataFrame,
358
354
  column: str | None = None,
359
355
  mask: GeoDataFrame | GeoSeries | Geometry = None,
360
- labels: tuple[str] | None = None,
361
356
  explore: bool = True,
362
357
  max_zoom: int = 40,
363
358
  smooth_factor: int | float = 1.5,
364
359
  browser: bool = False,
360
+ max_images: int = 15,
365
361
  **kwargs,
366
- ) -> None:
362
+ ) -> Explore | Map:
367
363
  """Shows an interactive map of a of GeoDataFrames clipped to the mask extent.
368
364
 
369
365
  It takes all the GeoDataFrames specified, clips them to the extent of the mask,
@@ -380,9 +376,6 @@ def clipmap(
380
376
  mask: the geometry to clip the data by.
381
377
  column: The column to color the geometries by. Defaults to None, which means
382
378
  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
379
  max_zoom: The maximum allowed level of zoom. Higher number means more zoom
387
380
  allowed. Defaults to 30, which is higher than the geopandas default.
388
381
  smooth_factor: How much to simplify the geometries. 1 is the minimum,
@@ -391,12 +384,14 @@ def clipmap(
391
384
  or not in Jupyter, a static plot will be shown.
392
385
  browser: If False (default), the maps will be shown in Jupyter.
393
386
  If True the maps will be opened in a browser folder.
387
+ max_images: Maximum number of images (Image, ImageCollection, Band) to show per
388
+ map. Defaults to 15.
394
389
  **kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
395
390
  instance 'cmap' to change the colors, 'scheme' to change how the data
396
391
  is grouped. This defaults to 'fisherjenkssampled' for numeric data.
397
392
 
398
393
  See Also:
399
- --------
394
+ ---------
400
395
  explore: same functionality, but shows the entire area of the geometries.
401
396
  samplemap: same functionality, but shows only a random area of a given size.
402
397
  """
@@ -406,6 +401,12 @@ def clipmap(
406
401
  mask = gdfs[-1]
407
402
  gdfs = gdfs[:-1]
408
403
 
404
+ if not isinstance(mask, (GeoDataFrame | GeoSeries | Geometry | tuple)):
405
+ try:
406
+ mask = to_gdf(mask)
407
+ except Exception:
408
+ mask = to_shapely(to_bbox(mask))
409
+
409
410
  center = kwargs.pop("center", None)
410
411
  size = kwargs.pop("size", None)
411
412
 
@@ -413,29 +414,31 @@ def clipmap(
413
414
  m = Explore(
414
415
  *gdfs,
415
416
  column=column,
416
- labels=labels,
417
417
  browser=browser,
418
418
  max_zoom=max_zoom,
419
419
  smooth_factor=smooth_factor,
420
+ max_images=max_images,
420
421
  **kwargs,
421
422
  )
422
- if m.gdfs is None and not len(m.raster_datasets):
423
- return
423
+ m.mask = mask
424
+
425
+ if m.gdfs is None and not len(m.rasters):
426
+ return m
424
427
 
425
428
  m._gdfs = [gdf.clip(mask) for gdf in m._gdfs]
426
429
  m._gdf = m._gdf.clip(mask)
427
430
  m._nan_idx = m._gdf[m._column].isna()
428
431
  m._get_unique_values()
429
432
  m.explore(center=center, size=size)
433
+ return m
430
434
  else:
431
435
  m = Map(
432
436
  *gdfs,
433
437
  column=column,
434
- labels=labels,
435
438
  **kwargs,
436
439
  )
437
440
  if m.gdfs is None:
438
- return
441
+ return m
439
442
 
440
443
  m._gdfs = [gdf.clip(mask) for gdf in m._gdfs]
441
444
  m._gdf = m._gdf.clip(mask)
@@ -444,6 +447,8 @@ def clipmap(
444
447
 
445
448
  qtm(m._gdf, column=m.column, cmap=m._cmap, k=m.k)
446
449
 
450
+ return m
451
+
447
452
 
448
453
  def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
449
454
  """Displays all local variables with geometries (GeoDataFrame etc.).
@@ -481,7 +486,7 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
481
486
  # add dicts or classes with GeoDataFrames to kwargs
482
487
  for key, val in as_dict(value).items():
483
488
  if isinstance(val, allowed_types):
484
- gdf = clean_geoms(to_gdf_func(val))
489
+ gdf = clean_geoms(to_gdf(val))
485
490
  if len(gdf):
486
491
  local_gdfs[key] = gdf
487
492
 
@@ -489,7 +494,7 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
489
494
  try:
490
495
  for k, v in val.items():
491
496
  if isinstance(v, allowed_types):
492
- gdf = clean_geoms(to_gdf_func(v))
497
+ gdf = clean_geoms(to_gdf(v))
493
498
  if len(gdf):
494
499
  local_gdfs[k] = gdf
495
500
  except Exception:
@@ -498,7 +503,7 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
498
503
 
499
504
  continue
500
505
  try:
501
- gdf = clean_geoms(to_gdf_func(value))
506
+ gdf = clean_geoms(to_gdf(value))
502
507
  if len(gdf):
503
508
  local_gdfs[name] = gdf
504
509
  continue
@@ -513,7 +518,7 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
513
518
  if not frame:
514
519
  break
515
520
 
516
- explore(*gdfs, **local_gdfs, **kwargs)
521
+ return explore(*gdfs, **local_gdfs, **kwargs)
517
522
 
518
523
 
519
524
  def qtm(
@@ -563,9 +568,6 @@ def qtm(
563
568
  else:
564
569
  new_kwargs[key] = value
565
570
 
566
- # self.labels.append(key)
567
- # self.show.append(last_show)
568
-
569
571
  m = ThematicMap(*gdfs, column=column, size=size, black=black)
570
572
 
571
573
  if m._gdfs is None: