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/legend.py CHANGED
@@ -17,7 +17,8 @@ from geopandas import GeoDataFrame
17
17
  from matplotlib.lines import Line2D
18
18
  from pandas import Series
19
19
 
20
- from ..geopandas_tools.bounds import points_in_bounds
20
+ from ..geopandas_tools.bounds import bounds_to_points
21
+ from ..geopandas_tools.general import points_in_bounds
21
22
 
22
23
  # the geopandas._explore raises a deprication warning. Ignoring for now.
23
24
  warnings.filterwarnings(
@@ -26,6 +27,82 @@ warnings.filterwarnings(
26
27
  pd.options.mode.chained_assignment = None
27
28
 
28
29
 
30
+ LEGEND_KWARGS = {
31
+ "title",
32
+ "size",
33
+ "position",
34
+ "fontsize",
35
+ "title_fontsize",
36
+ "markersize",
37
+ "framealpha",
38
+ "edgecolor",
39
+ "kwargs",
40
+ "labelspacing",
41
+ "title_color",
42
+ "width",
43
+ "height",
44
+ "labels",
45
+ "pretty_labels",
46
+ "thousand_sep",
47
+ "decimal_mark",
48
+ "label_sep",
49
+ "label_suffix",
50
+ "rounding",
51
+ "facecolor",
52
+ "labelcolor",
53
+ }
54
+
55
+ LOWERCASE_WORDS = {
56
+ "a",
57
+ "an",
58
+ "and",
59
+ "as",
60
+ "at",
61
+ "but",
62
+ "by",
63
+ "for",
64
+ "in",
65
+ "nor",
66
+ "of",
67
+ "on",
68
+ "or",
69
+ "the",
70
+ "up",
71
+ }
72
+
73
+
74
+ def prettify_label(label: str) -> str:
75
+ """Replace underscores with spaces and capitalize words that are all lowecase."""
76
+ return " ".join(
77
+ word.title() if word.islower() and word not in LOWERCASE_WORDS else word
78
+ for word in label.replace("_", " ").split()
79
+ )
80
+
81
+
82
+ def prettify_number(x: int | float, rounding: int) -> int:
83
+ rounding = int(float(f"1e+{abs(rounding)}"))
84
+ rounded_down = int(x // rounding * rounding)
85
+ rounded_up = rounded_down + rounding
86
+ diff_up = abs(x - rounded_up)
87
+ diff_down = abs(x - rounded_down)
88
+ if diff_up < diff_down:
89
+ return rounded_up
90
+ else:
91
+ return rounded_down
92
+
93
+
94
+ def prettify_bins(bins: list[int | float], rounding: int) -> list[int]:
95
+ return [
96
+ (
97
+ prettify_number(x, rounding)
98
+ if i != len(bins) - 1
99
+ else int(x)
100
+ # else prettify_number(x, rounding) + abs(rounding)
101
+ )
102
+ for i, x in enumerate(bins)
103
+ ]
104
+
105
+
29
106
  class Legend:
30
107
  """Holds the general attributes of the legend in the ThematicMap class.
31
108
 
@@ -36,27 +113,8 @@ class Legend:
36
113
  If a numeric column is used, additional attributes can be found in the
37
114
  ContinousLegend class.
38
115
 
39
- Attributes:
40
- title: Legend title. Defaults to the column name if used in the
41
- ThematicMap class.
42
- position: The legend's x and y position in the plot, specified as a tuple of
43
- x and y position between 0 and 1. E.g. position=(0.8, 0.2) for a position
44
- in the bottom right corner, (0.2, 0.8) for the upper left corner.
45
- fontsize: Text size of the legend labels. Defaults to the size of
46
- the ThematicMap class.
47
- title_fontsize: Text size of the legend title. Defaults to the
48
- size * 1.2 of the ThematicMap class.
49
- markersize: Size of the color circles in the legend. Defaults to the size of
50
- the ThematicMap class.
51
- framealpha: Transparency of the legend background.
52
- edgecolor: Color of the legend border. Defaults to #0f0f0f (almost black).
53
- kwargs: Stores additional keyword arguments taken by the matplotlib legend
54
- method. Specify this as e.g. m.legend.kwargs["labelcolor"] = "red", where
55
- 'm' is the name of the ThematicMap instance. See here:
56
- https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.legend.html
57
-
58
116
  Examples:
59
- --------
117
+ ---------
60
118
  Create ten points with a numeric column from 0 to 9.
61
119
 
62
120
  >>> import sgis as sg
@@ -89,38 +147,31 @@ class Legend:
89
147
  9 POINT (0.75000 0.25000) 9
90
148
 
91
149
  Creating the ThematicMap instance will also create the legend. Since we
92
- specify a numeric column, a ContinousLegend instance is created.
93
-
94
- >>> m = sg.ThematicMap(points, column="number")
150
+ pass a numeric column, a ContinousLegend is created.
151
+
152
+ >>> m = sg.ThematicMap(
153
+ ... points,
154
+ ... column="number"
155
+ ... legend_kwargs=dict(
156
+ ... title="Meters",
157
+ ... label_sep="to",
158
+ ... label_suffix="num",
159
+ ... rounding=2,
160
+ ... position = (0.35, 0.28),
161
+ ... title_fontsize=11,
162
+ ... fontsize=9,
163
+ ... markersize=7.5,
164
+ ... ),
165
+ ... )
166
+ >>> m.plot()
95
167
  >>> m.legend
96
168
  <sgis.maps.legend.ContinousLegend object at 0x00000222206738D0>
97
-
98
- Changing the attributes that apply to both numeric and categorical columns.
99
-
100
- >>> m.legend.title = "Meters"
101
- >>> m.legend.title_fontsize = 11
102
- >>> m.legend.fontsize = 9
103
- >>> m.legend.markersize = 7.5
104
- >>> m.legend.position = (0.35, 0.28)
105
- >>> m.plot()
106
-
107
- Additional matplotlib keyword arguments can be specified as kwargs.
108
-
109
- >>> m.legend.kwargs["labelcolor"] = "red"
110
-
111
- Since we are using a numeric column, the legend is of type ContinousLegend.
112
- We can therefore also access the attributes that only apply to numeric columns.
113
-
114
- >>> m.label_sep = "to"
115
- >>> m.label_suffix = "num"
116
- >>> m.rounding = 2
117
- >>> m.plot()
118
-
119
169
  """
120
170
 
121
171
  def __init__(
122
172
  self,
123
173
  title: str | None = None,
174
+ pretty_labels: bool = True,
124
175
  labels: list[str] | None = None,
125
176
  position: tuple[float] | None = None,
126
177
  markersize: int | None = None,
@@ -135,6 +186,8 @@ class Legend:
135
186
  Args:
136
187
  title: Legend title. Defaults to the column name if used in the
137
188
  ThematicMap class.
189
+ pretty_labels: If True, words will be capitalized and underscores turned to spaces.
190
+ If continous values, numbers will be rounded.
138
191
  labels: Labels of the categories.
139
192
  position: The legend's x and y position in the plot, specified as a tuple of
140
193
  x and y position between 0 and 1. E.g. position=(0.8, 0.2) for a position
@@ -163,6 +216,7 @@ class Legend:
163
216
  self._fontsize = fontsize
164
217
  self._markersize = markersize
165
218
 
219
+ self.pretty_labels = pretty_labels
166
220
  self.framealpha = framealpha
167
221
  self.edgecolor = edgecolor
168
222
  self.width = kwargs.pop("width", 0.1)
@@ -172,9 +226,23 @@ class Legend:
172
226
 
173
227
  self.labels = labels
174
228
  self._position = position
175
- self.kwargs = kwargs
176
229
  self._position_has_been_set = True if position else False
177
230
 
231
+ self.kwargs = {}
232
+ for key, value in kwargs.items():
233
+ if key not in LEGEND_KWARGS:
234
+ self.kwargs[key] = value
235
+ else:
236
+ try:
237
+ setattr(self, key, value)
238
+ except Exception:
239
+ setattr(self, f"_{key}", value)
240
+
241
+ @property
242
+ def valid_keywords(self) -> set[str]:
243
+ """List all valid keywords for the class initialiser."""
244
+ return LEGEND_KWARGS
245
+
178
246
  def _get_legend_sizes(self, size: int | float, kwargs: dict) -> None:
179
247
  """Adjust fontsize and markersize to size kwarg."""
180
248
  if "title_fontsize" in kwargs:
@@ -218,6 +286,8 @@ class Legend:
218
286
 
219
287
  self._patches, self._categories = [], []
220
288
  for category, color in categories_colors.items():
289
+ if self.pretty_labels:
290
+ category = prettify_label(category)
221
291
  if category == nan_label:
222
292
  self._categories.append(nan_label)
223
293
  else:
@@ -236,6 +306,8 @@ class Legend:
236
306
  )
237
307
 
238
308
  def _actually_add_legend(self, ax: matplotlib.axes.Axes) -> matplotlib.axes.Axes:
309
+ if self.pretty_labels:
310
+ self.title = prettify_label(self.title)
239
311
  legend = ax.legend(
240
312
  self._patches,
241
313
  self._categories,
@@ -262,13 +334,21 @@ class Legend:
262
334
  diffx = maxx - minx
263
335
  diffy = maxy - miny
264
336
 
265
- points = points_in_bounds(gdf, 30)
337
+ points = pd.concat(
338
+ [
339
+ points_in_bounds(gdf, 30),
340
+ bounds_to_points(gdf)
341
+ .geometry.explode(ignore_index=True)
342
+ .to_frame("geometry"),
343
+ ]
344
+ )
345
+
266
346
  gdf = gdf.loc[:, ~gdf.columns.str.contains("index|level_")]
267
347
  joined = points.sjoin_nearest(gdf, distance_col="nearest")
268
348
 
269
- max_distance = max(joined.nearest)
349
+ max_distance = max(joined["nearest"])
270
350
 
271
- best_position = joined.loc[joined.nearest == max_distance].drop_duplicates(
351
+ best_position = joined.loc[joined["nearest"] == max_distance].drop_duplicates(
272
352
  "geometry"
273
353
  )
274
354
 
@@ -360,10 +440,6 @@ class ContinousLegend(Legend):
360
440
  Defaults to None.
361
441
  label_sep: Text to put in between the two numbers in each color group in
362
442
  the legend. Defaults to '-'.
363
- rounding: Number of decimals in the labels. By default, the rounding
364
- depends on the column's maximum value and standard deviation.
365
- OBS: The bins will not be rounded, meaning the labels might be wrong
366
- if not bins are set manually.
367
443
  thousand_sep: Separator between each thousand for large numbers. Defaults to
368
444
  None, meaning no separator.
369
445
  decimal_mark: Text to use as decimal point. Defaults to None, meaning '.' (dot)
@@ -371,7 +447,7 @@ class ContinousLegend(Legend):
371
447
  decimal mark.
372
448
 
373
449
  Examples:
374
- --------
450
+ ---------
375
451
  Create ten random points with a numeric column from 0 to 9.
376
452
 
377
453
  >>> import sgis as sg
@@ -428,7 +504,7 @@ class ContinousLegend(Legend):
428
504
  def __init__(
429
505
  self,
430
506
  labels: list[str] | None = None,
431
- pretty_labels: bool = False,
507
+ pretty_labels: bool = True,
432
508
  label_suffix: str | None = None,
433
509
  label_sep: str = "-",
434
510
  rounding: int | None = None,
@@ -474,7 +550,7 @@ class ContinousLegend(Legend):
474
550
  self.label_sep = label_sep
475
551
  self.label_suffix = "" if not label_suffix else label_suffix
476
552
  self._rounding = rounding
477
- self._rounding_has_been_set = True if rounding else False
553
+ # self._rounding_has_been_set = True if rounding else False
478
554
 
479
555
  def _get_rounding(self, array: Series | np.ndarray) -> int:
480
556
  def isinteger(x):
@@ -502,8 +578,10 @@ class ContinousLegend(Legend):
502
578
 
503
579
  @staticmethod
504
580
  def _set_rounding(bins, rounding: int | float) -> list[int | float]:
505
- if rounding == 0:
581
+ if not rounding:
506
582
  return [int(round(bin_, 0)) for bin_ in bins]
583
+ elif rounding <= 0:
584
+ return [int(round(bin_, rounding)) for bin_ in bins]
507
585
  else:
508
586
  return [round(bin_, rounding) for bin_ in bins]
509
587
 
@@ -520,8 +598,8 @@ class ContinousLegend(Legend):
520
598
  ) -> None:
521
599
  # TODO: clean up this messy method
522
600
 
523
- for attr in self.__dict__.keys():
524
- if attr in self.kwargs:
601
+ for attr in self.kwargs:
602
+ if attr in self.__dict__:
525
603
  self[attr] = self.kwargs.pop(attr)
526
604
 
527
605
  self._patches, self._categories = [], []
@@ -544,9 +622,10 @@ class ContinousLegend(Legend):
544
622
  if len(self.labels) != len(colors):
545
623
  raise ValueError(
546
624
  "Label list must be same length as the number of groups. "
547
- f"Got k={len(colors)} and labels={len(colors)}."
625
+ f"Got k={len(colors)} and labels={len(self.labels)}."
548
626
  f"labels: {', '.join(self.labels)}"
549
627
  f"colors: {', '.join(colors)}"
628
+ f"bins: {bins}"
550
629
  )
551
630
  self._categories = self.labels
552
631
 
@@ -576,42 +655,59 @@ class ContinousLegend(Legend):
576
655
 
577
656
  min_ = np.min(bin_values[i])
578
657
  max_ = np.max(bin_values[i])
579
- min_rounded = self._set_rounding([min_], self._rounding)[0]
580
- max_rounded = self._set_rounding([max_], self._rounding)[0]
658
+
581
659
  if self.pretty_labels:
582
- if i != 0 and self._rounding == 0:
583
- cat1 = int(cat1 + 1)
584
- elif i != 0:
585
- cat1 = cat1 + float(f"1e-{self._rounding}")
660
+ if i == 0:
661
+ cat1 = int(min_) if (self.rounding or 0) <= 0 else min_
662
+
663
+ is_last = i == len(bins) - 2
664
+ if is_last:
665
+ cat2 = int(max_) if (self.rounding or 0) <= 0 else max_
666
+
667
+ if (self.rounding or 0) <= 0:
668
+ cat1 = int(cat1)
669
+ cat2 = int(cat2 - 1) if not is_last else int(cat2)
670
+ elif (self.rounding or 0) > 0:
671
+ cat1 = round(cat1, self._rounding)
672
+ cat2 = round(
673
+ cat2 - float(f"1e-{self._rounding}"), self._rounding
674
+ )
675
+ else:
676
+ cat1 = round(cat1, self._rounding)
677
+ cat2 = round(cat2, self._rounding)
586
678
 
587
679
  cat1 = self._format_number(cat1)
588
680
  cat2 = self._format_number(cat2)
589
681
 
590
682
  if min_ == max_:
591
- label = self._two_value_label(cat1, cat2)
683
+ label = self._get_two_value_label(cat1, cat2)
592
684
  self._categories.append(label)
593
685
  continue
594
686
 
595
- label = self._two_value_label(cat1, cat2)
687
+ label = self._get_two_value_label(cat1, cat2)
596
688
  self._categories.append(label)
597
689
 
598
- elif min_ == max_:
690
+ continue
691
+
692
+ min_rounded = self._set_rounding([min_], self._rounding)[0]
693
+ max_rounded = self._set_rounding([max_], self._rounding)[0]
694
+ if min_ == max_:
599
695
  min_rounded = self._format_number(min_rounded)
600
- label = self._one_value_label(min_rounded)
696
+ label = self._get_one_value_label(min_rounded)
601
697
  self._categories.append(label)
602
698
  else:
603
699
  min_rounded = self._format_number(min_rounded)
604
700
  max_rounded = self._format_number(max_rounded)
605
- label = self._two_value_label(min_rounded, max_rounded)
701
+ label = self._get_two_value_label(min_rounded, max_rounded)
606
702
  self._categories.append(label)
607
703
 
608
- def _two_value_label(self, value1: int | float, value2: int | float) -> str:
704
+ def _get_two_value_label(self, value1: int | float, value2: int | float) -> str:
609
705
  return (
610
706
  f"{value1} {self.label_suffix} {self.label_sep} "
611
707
  f"{value2} {self.label_suffix}"
612
708
  )
613
709
 
614
- def _one_value_label(self, value1: int | float) -> str:
710
+ def _get_one_value_label(self, value1: int | float) -> str:
615
711
  return f"{value1} {self.label_suffix}"
616
712
 
617
713
  def _format_number(self, number: int | float) -> int | float:
@@ -640,10 +736,15 @@ class ContinousLegend(Legend):
640
736
 
641
737
  @property
642
738
  def rounding(self) -> int:
643
- """Number rounding."""
739
+ """Number of decimals in the labels.
740
+
741
+ By default, the rounding
742
+ depends on the column's maximum value and standard deviation.
743
+ OBS: The bins will not be rounded, meaning the labels might be wrong
744
+ if not bins are set manually.
745
+ """
644
746
  return self._rounding
645
747
 
646
748
  @rounding.setter
647
749
  def rounding(self, new_value: int) -> None:
648
750
  self._rounding = new_value
649
- self._rounding_has_been_set = True