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/map.py CHANGED
@@ -4,6 +4,8 @@ This module holds the Map class, which is the basis for the Explore class.
4
4
  """
5
5
 
6
6
  import warnings
7
+ from collections.abc import Sequence
8
+ from statistics import mean
7
9
  from typing import Any
8
10
 
9
11
  import matplotlib
@@ -22,6 +24,10 @@ from ..geopandas_tools.general import clean_geoms
22
24
  from ..geopandas_tools.general import drop_inactive_geometry_columns
23
25
  from ..geopandas_tools.general import get_common_crs
24
26
  from ..helpers import get_object_name
27
+ from ..helpers import unit_is_meters
28
+ from ..raster.image_collection import Band
29
+ from ..raster.image_collection import Image
30
+ from ..raster.image_collection import ImageCollection
25
31
 
26
32
  try:
27
33
  from torchgeo.datasets.geo import RasterDataset
@@ -91,12 +97,12 @@ class Map:
91
97
  self,
92
98
  *gdfs: GeoDataFrame,
93
99
  column: str | None = None,
94
- labels: tuple[str] | None = None,
95
100
  k: int = 5,
96
101
  bins: tuple[float] | None = None,
97
102
  nan_label: str = "Missing",
98
103
  nan_color="#c2c2c2",
99
104
  scheme: str = DEFAULT_SCHEME,
105
+ cmap: str | None = None,
100
106
  **kwargs,
101
107
  ) -> None:
102
108
  """Initialiser.
@@ -104,12 +110,13 @@ class Map:
104
110
  Args:
105
111
  *gdfs: Variable length GeoDataFrame list.
106
112
  column: The column name to work with.
107
- labels: Tuple of labels for each GeoDataFrame.
108
113
  k: Number of bins or classes for classification (default: 5).
109
114
  bins: Predefined bins for data classification.
110
115
  nan_label: Label for missing data.
111
116
  nan_color: Color for missing data.
112
117
  scheme: Classification scheme to be used.
118
+ cmap (str): Colormap of the plot. See:
119
+ https://matplotlib.org/stable/tutorials/colors/colormaps.html
113
120
  **kwargs: Arbitrary keyword arguments.
114
121
  """
115
122
  gdfs, column, kwargs = self._separate_args(gdfs, column, kwargs)
@@ -119,27 +126,14 @@ class Map:
119
126
  self._k = k
120
127
  self.nan_label = nan_label
121
128
  self.nan_color = nan_color
122
- self._cmap = kwargs.pop("cmap", None)
129
+ self._cmap = cmap
123
130
  self.scheme = scheme
124
131
 
125
- if not all(isinstance(gdf, GeoDataFrame) for gdf in gdfs):
126
- gdfs = [
127
- to_gdf(gdf) if not isinstance(gdf, GeoDataFrame) else gdf
128
- for gdf in gdfs
129
- ]
130
- if not all(isinstance(gdf, GeoDataFrame) for gdf in gdfs):
131
- raise ValueError("gdfs must be GeoDataFrames.")
132
-
133
- if "namedict" in kwargs:
134
- for i, gdf in enumerate(gdfs):
135
- gdf.name = kwargs["namedict"][i]
136
- kwargs.pop("namedict")
137
-
138
132
  # need to get the object names of the gdfs before copying. Only getting,
139
133
  # not setting, labels. So the original gdfs don't get the label column.
140
- self.labels = labels
141
- if not self.labels:
142
- self._get_labels(gdfs)
134
+ self.labels: list[str] = [
135
+ _determine_best_name(gdf, column, i) for i, gdf in enumerate(gdfs)
136
+ ]
143
137
 
144
138
  show = kwargs.pop("show", True)
145
139
  if isinstance(show, (int, bool)):
@@ -165,31 +159,18 @@ class Map:
165
159
  if not len(gdf):
166
160
  continue
167
161
 
168
- self._gdfs.append(gdf)
162
+ self._gdfs.append(to_gdf(gdf))
169
163
  new_labels.append(label)
170
164
  self.show.append(show)
171
165
  self.labels = new_labels
172
166
 
173
- # if len(self._gdfs):
174
- # last_show = self.show[-1]
175
- # else:
176
- # last_show = show
177
-
178
167
  # pop all geometry-like items from kwargs into self._gdfs
179
168
  self.kwargs = {}
180
169
  i = 0
181
170
  for key, value in kwargs.items():
182
- # if isinstance(value, GeoDataFrame):
183
- # self._gdfs.append(value)
184
- # self.labels.append(key)
185
- # try:
186
- # show = show_kwargs[i]
187
- # except IndexError:
188
- # pass
189
- # self.show.append(show)
190
- # i += 1
191
- # continue
192
171
  try:
172
+ if not len(value):
173
+ continue
193
174
  self._gdfs.append(to_gdf(value))
194
175
  self.labels.append(key)
195
176
  try:
@@ -208,10 +189,10 @@ class Map:
208
189
  )
209
190
 
210
191
  if not any(len(gdf) for gdf in self._gdfs):
211
- warnings.warn("None of the GeoDataFrames have rows.", stacklevel=1)
212
- self._gdfs = None
192
+ self._gdfs = []
213
193
  self._is_categorical = True
214
194
  self._unique_values = []
195
+ self._nan_idx = []
215
196
  return
216
197
 
217
198
  if not self.labels:
@@ -262,7 +243,7 @@ class Map:
262
243
  Because floats don't always equal each other. This will make very
263
244
  similar values count as the same value in the color classification.
264
245
  """
265
- array = self._gdf.loc[~self._nan_idx, self._column]
246
+ array = self._gdf.loc[list(~self._nan_idx), self._column]
266
247
  self._min = np.min(array)
267
248
  self._max = np.max(array)
268
249
  self._get_multiplier(array)
@@ -313,29 +294,70 @@ class Map:
313
294
  # make sure they are lists
314
295
  bins = [bin_ for bin_ in bins]
315
296
 
316
- if min(bins) > 0 and min(self._gdf.loc[~self._nan_idx, self._column]) < min(
317
- bins
318
- ):
319
- bins = [min(self._gdf.loc[~self._nan_idx, self._column])] + bins
320
-
321
- if min(bins) < 0 and min(self._gdf.loc[~self._nan_idx, self._column]) < min(
322
- bins
323
- ):
324
- bins = [min(self._gdf.loc[~self._nan_idx, self._column])] + bins
297
+ if min(bins) > 0 and min(
298
+ self._gdf.loc[list(~self._nan_idx), self._column]
299
+ ) < min(bins):
300
+ num = min(self._gdf.loc[list(~self._nan_idx), self._column])
301
+ # if isinstance(num, float):
302
+ # num -= (
303
+ # float(f"1e-{abs(self.legend.rounding)}")
304
+ # if self.legend and self.legend.rounding
305
+ # else 0
306
+ # )
307
+ bins = [num] + bins
308
+
309
+ if min(bins) < 0 and min(
310
+ self._gdf.loc[list(~self._nan_idx), self._column]
311
+ ) < min(bins):
312
+ num = min(self._gdf.loc[list(~self._nan_idx), self._column])
313
+ # if isinstance(num, float):
314
+ # num -= (
315
+ # float(f"1e-{abs(self.legend.rounding)}")
316
+ # if self.legend and self.legend.rounding
317
+ # else 0
318
+ # )
319
+ bins = [num] + bins
325
320
 
326
321
  if max(bins) > 0 and max(
327
322
  self._gdf.loc[self._gdf[self._column].notna(), self._column]
328
323
  ) > max(bins):
329
- bins = bins + [
330
- max(self._gdf.loc[self._gdf[self._column].notna(), self._column])
331
- ]
324
+ num = max(self._gdf.loc[self._gdf[self._column].notna(), self._column])
325
+ # if isinstance(num, float):
326
+ # num += (
327
+ # float(f"1e-{abs(self.legend.rounding)}")
328
+ # if self.legend and self.legend.rounding
329
+ # else 0
330
+ # )
331
+ bins = bins + [num]
332
332
 
333
333
  if max(bins) < 0 and max(
334
334
  self._gdf.loc[self._gdf[self._column].notna(), self._column]
335
335
  ) < max(bins):
336
- bins = bins + [
337
- max(self._gdf.loc[self._gdf[self._column].notna(), self._column])
338
- ]
336
+ num = max(self._gdf.loc[self._gdf[self._column].notna(), self._column])
337
+ # if isinstance(num, float):
338
+ # num += (
339
+ # float(f"1e-{abs(self.legend.rounding)}")
340
+ # if self.legend and self.legend.rounding
341
+ # else 0
342
+ # )
343
+
344
+ bins = bins + [num]
345
+
346
+ def adjust_bin(num: int | float, i: int) -> int | float:
347
+ if isinstance(num, int):
348
+ return num
349
+ adjuster = (
350
+ float(f"1e-{abs(self.legend.rounding)}")
351
+ if self.legend and self.legend.rounding
352
+ else 0
353
+ )
354
+ if i == 0:
355
+ return num - adjuster
356
+ elif i == len(bins) - 1:
357
+ return num + adjuster
358
+ return num
359
+
360
+ bins = [adjust_bin(x, i) for i, x in enumerate(bins)]
339
361
 
340
362
  return bins
341
363
 
@@ -347,16 +369,26 @@ class Map:
347
369
  ) -> tuple[tuple[GeoDataFrame], str]:
348
370
  """Separate GeoDataFrames from string (column argument)."""
349
371
 
350
- def as_dict(obj):
372
+ def as_dict(obj) -> dict:
351
373
  if hasattr(obj, "__dict__"):
352
374
  return obj.__dict__
353
375
  elif isinstance(obj, dict):
354
376
  return obj
355
- raise TypeError
356
-
357
- allowed_types = (GeoDataFrame, GeoSeries, Geometry, RasterDataset)
377
+ raise TypeError(type(obj))
378
+
379
+ allowed_types = (
380
+ GeoDataFrame,
381
+ GeoSeries,
382
+ Geometry,
383
+ RasterDataset,
384
+ ImageCollection,
385
+ Image,
386
+ Band,
387
+ )
358
388
 
359
- gdfs: tuple[GeoDataFrame | GeoSeries | Geometry | RasterDataset] = ()
389
+ gdfs = ()
390
+ more_gdfs = {}
391
+ i = 0
360
392
  for arg in args:
361
393
  if isinstance(arg, str):
362
394
  if column is None:
@@ -367,12 +399,31 @@ class Map:
367
399
  )
368
400
  elif isinstance(arg, allowed_types):
369
401
  gdfs = gdfs + (arg,)
402
+ # elif isinstance(arg, Sequence) and not isinstance(arg, str):
370
403
  elif isinstance(arg, dict) or hasattr(arg, "__dict__"):
371
404
  # add dicts or classes with GeoDataFrames to kwargs
372
- more_gdfs = {}
373
405
  for key, value in as_dict(arg).items():
374
406
  if isinstance(value, allowed_types):
375
407
  more_gdfs[key] = value
408
+ elif isinstance(value, dict) or hasattr(value, "__dict__"):
409
+ # elif isinstance(value, Sequence) and not isinstance(value, str):
410
+ try:
411
+ # same as above, one level down
412
+ more_gdfs |= {
413
+ k: v
414
+ for k, v in as_dict(value).items()
415
+ if isinstance(v, allowed_types)
416
+ }
417
+ except Exception:
418
+ # ignore all exceptions
419
+ pass
420
+
421
+ elif isinstance(arg, Sequence) and not isinstance(arg, str):
422
+ # add dicts or classes with GeoDataFrames to kwargs
423
+ for value in arg:
424
+ if isinstance(value, allowed_types):
425
+ name = _determine_best_name(value, column, i)
426
+ more_gdfs[name] = value
376
427
  elif isinstance(value, dict) or hasattr(value, "__dict__"):
377
428
  try:
378
429
  # same as above, one level down
@@ -384,8 +435,15 @@ class Map:
384
435
  except Exception:
385
436
  # no need to raise here
386
437
  pass
438
+ elif isinstance(value, Sequence) and not isinstance(value, str):
439
+ for x in value:
440
+ if not isinstance(x, allowed_types):
441
+ continue
442
+ name = _determine_best_name(value, column, i)
443
+ more_gdfs[name] = x
444
+ i += 1
387
445
 
388
- kwargs |= more_gdfs
446
+ kwargs |= more_gdfs
389
447
 
390
448
  return gdfs, column, kwargs
391
449
 
@@ -394,7 +452,7 @@ class Map:
394
452
  if self.scheme is None:
395
453
  return
396
454
 
397
- if not self.bins:
455
+ if self.bins is None:
398
456
  self.bins = self._create_bins(self._gdf, self._column)
399
457
  if len(self.bins) <= self._k and len(self.bins) != len(self._unique_values):
400
458
  self._k = len(self.bins)
@@ -406,17 +464,6 @@ class Map:
406
464
  self._unique_values = self.nan_label
407
465
  self._k = 1
408
466
 
409
- def _get_labels(self, gdfs: tuple[GeoDataFrame]) -> None:
410
- """Putting the labels/names in a list before copying the gdfs."""
411
- self.labels: list[str] = []
412
- for i, gdf in enumerate(gdfs):
413
- if hasattr(gdf, "name") and isinstance(gdf.name, str):
414
- name = gdf.name
415
- else:
416
- name = get_object_name(gdf)
417
- name = name or str(i)
418
- self.labels.append(name)
419
-
420
467
  def _set_labels(self) -> None:
421
468
  """Setting the labels after copying the gdfs."""
422
469
  gdfs = []
@@ -470,7 +517,18 @@ class Map:
470
517
  if not self._column:
471
518
  return True
472
519
 
520
+ def is_maybe_km2():
521
+ if "area" in self._column and (
522
+ "km2" in self._column
523
+ or "kilomet" in self._column
524
+ and ("sq" in self._column or "2" in self._column)
525
+ ):
526
+ return True
527
+ else:
528
+ return False
529
+
473
530
  maybe_area = 1 if "area" in self._column else 0
531
+ maybe_area_km2 = 1 if is_maybe_km2() else 0
474
532
  maybe_length = (
475
533
  1 if any(x in self._column for x in ["meter", "metre", "leng"]) else 0
476
534
  )
@@ -479,7 +537,10 @@ class Map:
479
537
  col_not_present = 0
480
538
  for gdf in self._gdfs:
481
539
  if self._column not in gdf:
482
- if maybe_area:
540
+ if maybe_area_km2 and unit_is_meters(gdf):
541
+ gdf["area_km2"] = gdf.area / 1_000_000
542
+ maybe_area_km2 += 1
543
+ elif maybe_area:
483
544
  gdf["area"] = gdf.area
484
545
  maybe_area += 1
485
546
  elif maybe_length:
@@ -492,6 +553,9 @@ class Map:
492
553
  all_nan += 1
493
554
  return True
494
555
 
556
+ if maybe_area_km2 > 1:
557
+ self._column = "area_km2"
558
+ return False
495
559
  if maybe_area > 1:
496
560
  self._column = "area"
497
561
  return False
@@ -507,7 +571,7 @@ class Map:
507
571
 
508
572
  return False
509
573
 
510
- def _get_categorical_colors(self) -> None:
574
+ def _make_categories_colors_dict(self) -> None:
511
575
  # custom categorical cmap
512
576
  if not self._cmap and len(self._unique_values) <= len(_CATEGORICAL_CMAP):
513
577
  self._categories_colors_dict = {
@@ -529,6 +593,7 @@ class Map:
529
593
  for i, category in enumerate(self._unique_values)
530
594
  }
531
595
 
596
+ def _fix_nans(self) -> None:
532
597
  if any(self._nan_idx):
533
598
  self._gdf[self._column] = self._gdf[self._column].fillna(self.nan_label)
534
599
  self._categories_colors_dict[self.nan_label] = self.nan_color
@@ -549,7 +614,7 @@ class Map:
549
614
  If 'scheme' is not specified, the jenks_breaks function is used, which is
550
615
  much faster than the one from Mapclassifier.
551
616
  """
552
- if not len(gdf.loc[~self._nan_idx, column]):
617
+ if not len(gdf.loc[list(~self._nan_idx), column]):
553
618
  return np.array([0])
554
619
 
555
620
  n_classes = (
@@ -565,29 +630,26 @@ class Map:
565
630
  n_classes = len(self._unique_values)
566
631
 
567
632
  if self.scheme == "jenks":
568
- try:
569
- bins = jenks_breaks(
570
- gdf.loc[~self._nan_idx, column], n_classes=n_classes
571
- )
572
- bins = self._add_minmax_to_bins(bins)
573
- except Exception:
574
- pass
633
+ bins = jenks_breaks(
634
+ gdf.loc[list(~self._nan_idx), column], n_classes=n_classes
635
+ )
575
636
  else:
576
637
  binning = classify(
577
- np.asarray(gdf.loc[~self._nan_idx, column]),
638
+ np.asarray(gdf.loc[list(~self._nan_idx), column]),
578
639
  scheme=self.scheme,
579
- k=self._k,
640
+ # k=self._k,
641
+ k=n_classes,
580
642
  )
581
643
  bins = binning.bins
582
- bins = self._add_minmax_to_bins(bins)
644
+
645
+ bins = self._add_minmax_to_bins(bins)
583
646
 
584
647
  unique_bins = list({round(bin_, 5) for bin_ in bins})
585
648
  unique_bins.sort()
586
649
 
587
- if self._k == len(self._unique_values) - 1:
588
- return np.array(unique_bins)
589
-
590
- if len(unique_bins) == len(self._unique_values):
650
+ if self._k == len(self._unique_values) - 1 or len(unique_bins) == len(
651
+ self._unique_values
652
+ ):
591
653
  return np.array(unique_bins)
592
654
 
593
655
  if len(unique_bins) == len(bins) - 1:
@@ -624,6 +686,8 @@ class Map:
624
686
 
625
687
  def _classify_from_bins(self, gdf: GeoDataFrame, bins: np.ndarray) -> np.ndarray:
626
688
  """Place the column values into groups."""
689
+ bins = bins.copy()
690
+
627
691
  # if equal lenght, convert to integer and check for equality
628
692
  if len(bins) == len(self._unique_values):
629
693
  if gdf[self._column].isna().all():
@@ -638,6 +702,14 @@ class Map:
638
702
  if len(bins) == self._k + 1:
639
703
  bins = bins[1:]
640
704
 
705
+ if (
706
+ self.legend
707
+ and self.legend.rounding
708
+ and (self.legend.rounding or 1) <= 0
709
+ ):
710
+ bins[0] = bins[0] - 1
711
+ bins[-1] = bins[-1] + 1
712
+
641
713
  if gdf[self._column].isna().all():
642
714
  return np.repeat(len(bins), len(gdf))
643
715
 
@@ -686,7 +758,8 @@ class Map:
686
758
  @cmap.setter
687
759
  def cmap(self, new_value: str) -> None:
688
760
  self._cmap = new_value
689
- self.change_cmap(cmap=new_value, start=self.cmap_start, stop=self.cmap_stop)
761
+ if not self._is_categorical:
762
+ self.change_cmap(cmap=new_value, start=self.cmap_start, stop=self.cmap_stop)
690
763
 
691
764
  @property
692
765
  def gdf(self) -> GeoDataFrame:
@@ -738,3 +811,24 @@ class Map:
738
811
  return self[key]
739
812
  except (KeyError, ValueError, IndexError, AttributeError):
740
813
  return default
814
+
815
+
816
+ def _determine_best_name(obj: Any, column: str | None, i: int) -> str:
817
+ try:
818
+ # Frame 3: actual object name Frame 2: maps.py:explore(). Frame 1: __init__. Frame 0: this function.
819
+ return str(get_object_name(obj, start=3))
820
+ except ValueError:
821
+ if isinstance(obj, GeoSeries) and obj.name:
822
+ return str(obj.name)
823
+ elif isinstance(obj, GeoDataFrame) and len(obj.columns) == 2 and not column:
824
+ series = obj.drop(columns=obj._geometry_column_name).iloc[:, 0]
825
+ if (
826
+ len(series.unique()) == 1
827
+ and mean(isinstance(x, str) for x in series) > 0.5
828
+ ):
829
+ return str(next(iter(series)))
830
+ elif series.name:
831
+ return str(series.name)
832
+ else:
833
+ # generic label e.g. Image(1)
834
+ return f"{obj.__class__.__name__}({i})"